mycli 2.13.0__tar.gz → 2.13.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mycli-2.13.0/mycli.egg-info → mycli-2.13.2}/PKG-INFO +3 -2
- {mycli-2.13.0 → mycli-2.13.2}/changelog.md +27 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/boundary_tunnel.py +5 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/client.py +4 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/client_query.py +1 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/completion_refresher.py +54 -4
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/repl.py +8 -2
- {mycli-2.13.0 → mycli-2.13.2/mycli.egg-info}/PKG-INFO +3 -2
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/requires.txt +1 -0
- mycli-2.13.2/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.13.0 → mycli-2.13.2}/pyproject.toml +3 -2
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_boundary_tunnel.py +8 -2
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client.py +11 -13
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_query.py +14 -3
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completion_refresher.py +206 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_repl.py +24 -1
- mycli-2.13.0/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.13.0 → mycli-2.13.2}/.git-blame-ignore-revs +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/dependabot.yml +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/ci.yml +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/lint.yml +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/publish.yml +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/.gitignore +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/AGENTS.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/AUTHORS.rst +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/CONTRIBUTING.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/LICENSE.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/MANIFEST.in +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/README.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/SPONSORS.rst +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/key_bindings.rst +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/llm.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/main.gif +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/tables.png +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/doc/transforms.md +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/AUTHORS +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/SPONSORS +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/TIPS +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/app_state.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/cli_runner.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/clibuffer.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/client_commands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/client_connection.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/clistyle.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/clitoolbar.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/compat.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/config.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/constants.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/key_bindings.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/lexer.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/batch.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/completions.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/execute.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/myclirc +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/output.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/filepaths.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/polars_transform.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/llm.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/main.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/string_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/password_sources.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/sqlcompleter.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/sqlexecute.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/types.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli/vault.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/SOURCES.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/scm_file_list.json +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/setup.cfg +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/auto_vertical.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/basic_commands.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/connection.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/crud_database.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/crud_table.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/db_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/environment.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/fixture_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/iocommands.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/named_queries.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/specials.feature +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/__init__.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/connection.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/crud_database.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/crud_table.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/iocommands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/named_queries.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/specials.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/wrappers.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/features/wrappager.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/myclirc +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/mylogin.cnf +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/conftest.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_app_state.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_checkup.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_config.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_lexer.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_output.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_polars_transform.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_init.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_main.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_vault.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.13.0 → mycli-2.13.2}/test/utils.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.13.
|
|
4
|
-
Summary: Rich terminal client
|
|
3
|
+
Version: 2.13.2
|
|
4
|
+
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://mycli.net
|
|
@@ -51,6 +51,7 @@ Provides-Extra: llm
|
|
|
51
51
|
Requires-Dist: llm~=0.31.1; extra == "llm"
|
|
52
52
|
Requires-Dist: setuptools==83.*; extra == "llm"
|
|
53
53
|
Requires-Dist: pip~=26.1.2; extra == "llm"
|
|
54
|
+
Requires-Dist: openai~=2.48.0; extra == "llm"
|
|
54
55
|
Provides-Extra: dataframe
|
|
55
56
|
Requires-Dist: polars~=1.42.1; extra == "dataframe"
|
|
56
57
|
Requires-Dist: altair~=6.2.2; extra == "dataframe"
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
2.13.2 (2026/08/12)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Bugfixes
|
|
5
|
+
---------
|
|
6
|
+
* Avoid an exception when exiting while completions are being refreshed.
|
|
7
|
+
* Avoid a completions exception when changing to a nonexistent database.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Documentation
|
|
11
|
+
---------
|
|
12
|
+
* Shorten/clarify `pyproject.toml` project description.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Internal
|
|
16
|
+
---------
|
|
17
|
+
* Fix CI by pinning the `openai` library to v2.48.0.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
2.13.1 (2026/08/11)
|
|
21
|
+
==============
|
|
22
|
+
|
|
23
|
+
Bugfixes
|
|
24
|
+
---------
|
|
25
|
+
* Pause before connecting over Boundary tunnels, to avoid SSL errors.
|
|
26
|
+
|
|
27
|
+
|
|
1
28
|
2.13.0 (2026/08/11)
|
|
2
29
|
==============
|
|
3
30
|
|
|
@@ -15,6 +15,8 @@ from typing import IO
|
|
|
15
15
|
|
|
16
16
|
from mycli.compat import WIN
|
|
17
17
|
|
|
18
|
+
TUNNEL_STABILIZATION_PAUSE = 0.25
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class BoundaryTunnelError(RuntimeError):
|
|
20
22
|
pass
|
|
@@ -146,6 +148,9 @@ class BoundaryTunnel:
|
|
|
146
148
|
self._raise_if_failed()
|
|
147
149
|
if self._is_listening():
|
|
148
150
|
self._ready.set()
|
|
151
|
+
# todo: if this works to ward off SSL errors at connection time,
|
|
152
|
+
# try making the pause as small as possible
|
|
153
|
+
time.sleep(TUNNEL_STABILIZATION_PAUSE)
|
|
149
154
|
return
|
|
150
155
|
time.sleep(0.05)
|
|
151
156
|
self.close()
|
|
@@ -241,6 +241,10 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
241
241
|
self.prompt_session.app.invalidate()
|
|
242
242
|
|
|
243
243
|
def close(self) -> None:
|
|
244
|
+
try:
|
|
245
|
+
self.completion_refresher.stop()
|
|
246
|
+
except Exception:
|
|
247
|
+
pass
|
|
244
248
|
try:
|
|
245
249
|
self.schema_prefetcher.stop()
|
|
246
250
|
except Exception:
|
|
@@ -40,6 +40,7 @@ class ClientQueryMixin:
|
|
|
40
40
|
|
|
41
41
|
assert self.sqlexecute is not None
|
|
42
42
|
if reset:
|
|
43
|
+
self.completion_refresher.stop()
|
|
43
44
|
# Update the active completer's current-schema pointer right
|
|
44
45
|
# away so unqualified completions reflect a schema switch
|
|
45
46
|
# even before the background refresh finishes.
|
|
@@ -3,6 +3,7 @@ from time import monotonic
|
|
|
3
3
|
from typing import Callable
|
|
4
4
|
|
|
5
5
|
import pymysql
|
|
6
|
+
from pymysql.constants.ER import BAD_DB_ERROR
|
|
6
7
|
|
|
7
8
|
from mycli.packages.special.main import COMMANDS
|
|
8
9
|
from mycli.packages.sqlresult import SQLResult
|
|
@@ -18,6 +19,9 @@ class CompletionRefresher:
|
|
|
18
19
|
def __init__(self, invalidate_app: Callable[[], None] | None = None) -> None:
|
|
19
20
|
self._completer_thread: threading.Thread | None = None
|
|
20
21
|
self._restart_refresh = threading.Event()
|
|
22
|
+
self._stop_refresh = threading.Event()
|
|
23
|
+
self._executor_lock = threading.Lock()
|
|
24
|
+
self._active_executor: SQLExecute | None = None
|
|
21
25
|
self._refresh_visible_until = 0.0
|
|
22
26
|
self._visibility_timer: threading.Timer | None = None
|
|
23
27
|
self._invalidate_app = invalidate_app
|
|
@@ -49,6 +53,7 @@ class CompletionRefresher:
|
|
|
49
53
|
if self._visibility_timer is not None:
|
|
50
54
|
self._visibility_timer.cancel()
|
|
51
55
|
self._visibility_timer = None
|
|
56
|
+
self._stop_refresh.clear()
|
|
52
57
|
self._refresh_visible_until = monotonic() + MIN_COMPLETION_REFRESH_MESSAGE_SECONDS
|
|
53
58
|
self._completer_thread = threading.Thread(
|
|
54
59
|
target=self._bg_refresh, args=(executor, callbacks, completer_options), name="completion_refresh"
|
|
@@ -63,6 +68,29 @@ class CompletionRefresher:
|
|
|
63
68
|
def _thread_is_alive(self) -> bool:
|
|
64
69
|
return bool(self._completer_thread and self._completer_thread.is_alive())
|
|
65
70
|
|
|
71
|
+
def stop(self) -> None:
|
|
72
|
+
"""Stop and wait for an in-flight completion refresh."""
|
|
73
|
+
self._stop_refresh.set()
|
|
74
|
+
self._restart_refresh.clear()
|
|
75
|
+
self._refresh_visible_until = 0.0
|
|
76
|
+
if self._visibility_timer is not None:
|
|
77
|
+
self._visibility_timer.cancel()
|
|
78
|
+
self._visibility_timer = None
|
|
79
|
+
|
|
80
|
+
with self._executor_lock:
|
|
81
|
+
executor = self._active_executor
|
|
82
|
+
if executor is not None:
|
|
83
|
+
try:
|
|
84
|
+
executor.close()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
thread = self._completer_thread
|
|
89
|
+
if thread is not None and thread.is_alive() and thread is not threading.current_thread():
|
|
90
|
+
thread.join()
|
|
91
|
+
if thread is not None and not thread.is_alive():
|
|
92
|
+
self._completer_thread = None
|
|
93
|
+
|
|
66
94
|
def _bg_refresh(
|
|
67
95
|
self,
|
|
68
96
|
sqlexecute: SQLExecute,
|
|
@@ -89,7 +117,12 @@ class CompletionRefresher:
|
|
|
89
117
|
self._finish_refreshing()
|
|
90
118
|
return
|
|
91
119
|
|
|
120
|
+
with self._executor_lock:
|
|
121
|
+
self._active_executor = executor
|
|
92
122
|
try:
|
|
123
|
+
if self._stop_refresh.is_set():
|
|
124
|
+
return
|
|
125
|
+
|
|
93
126
|
# If callbacks is a single function then push it into a list.
|
|
94
127
|
if callable(callbacks):
|
|
95
128
|
callbacks = [callbacks]
|
|
@@ -97,6 +130,8 @@ class CompletionRefresher:
|
|
|
97
130
|
while 1:
|
|
98
131
|
for refresher in self.refreshers.values():
|
|
99
132
|
refresher(completer, executor)
|
|
133
|
+
if self._stop_refresh.is_set():
|
|
134
|
+
return
|
|
100
135
|
if self._restart_refresh.is_set():
|
|
101
136
|
self._restart_refresh.clear()
|
|
102
137
|
break
|
|
@@ -109,11 +144,26 @@ class CompletionRefresher:
|
|
|
109
144
|
# break statement.
|
|
110
145
|
continue
|
|
111
146
|
|
|
112
|
-
|
|
113
|
-
callback
|
|
147
|
+
if not self._stop_refresh.is_set():
|
|
148
|
+
for callback in callbacks:
|
|
149
|
+
callback(completer)
|
|
150
|
+
except pymysql.err.OperationalError as error:
|
|
151
|
+
if not self._stop_refresh.is_set() and error.args[0] != BAD_DB_ERROR:
|
|
152
|
+
raise
|
|
153
|
+
except Exception:
|
|
154
|
+
if not self._stop_refresh.is_set():
|
|
155
|
+
raise
|
|
114
156
|
finally:
|
|
115
|
-
|
|
116
|
-
|
|
157
|
+
with self._executor_lock:
|
|
158
|
+
if self._active_executor is executor:
|
|
159
|
+
self._active_executor = None
|
|
160
|
+
try:
|
|
161
|
+
executor.close()
|
|
162
|
+
except Exception:
|
|
163
|
+
if not self._stop_refresh.is_set():
|
|
164
|
+
raise
|
|
165
|
+
finally:
|
|
166
|
+
self._finish_refreshing()
|
|
117
167
|
|
|
118
168
|
def _finish_refreshing(self) -> None:
|
|
119
169
|
self._invalidate()
|
|
@@ -742,6 +742,10 @@ def _one_iteration(
|
|
|
742
742
|
mycli.echo('Wise choice!')
|
|
743
743
|
return
|
|
744
744
|
|
|
745
|
+
dropping_active_database = is_dropping_database(text, sqlexecute.dbname)
|
|
746
|
+
if dropping_active_database:
|
|
747
|
+
mycli.completion_refresher.stop()
|
|
748
|
+
|
|
745
749
|
successful = False
|
|
746
750
|
try:
|
|
747
751
|
mycli.logger.debug('sql: %r', text)
|
|
@@ -870,13 +874,15 @@ def _one_iteration(
|
|
|
870
874
|
fg='yellow',
|
|
871
875
|
)
|
|
872
876
|
|
|
873
|
-
if
|
|
877
|
+
if dropping_active_database:
|
|
874
878
|
sqlexecute.dbname = None
|
|
875
879
|
sqlexecute.connect()
|
|
876
880
|
|
|
877
881
|
if need_completion_refresh(text):
|
|
878
|
-
mycli.refresh_completions(reset=need_completion_reset(text))
|
|
882
|
+
mycli.refresh_completions(reset=dropping_active_database or need_completion_reset(text))
|
|
879
883
|
finally:
|
|
884
|
+
if dropping_active_database and not successful:
|
|
885
|
+
mycli.refresh_completions()
|
|
880
886
|
if mycli.logfile is False:
|
|
881
887
|
mycli.echo('Warning: This query was not logged.', err=True, fg='red')
|
|
882
888
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.13.
|
|
4
|
-
Summary: Rich terminal client
|
|
3
|
+
Version: 2.13.2
|
|
4
|
+
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://mycli.net
|
|
@@ -51,6 +51,7 @@ Provides-Extra: llm
|
|
|
51
51
|
Requires-Dist: llm~=0.31.1; extra == "llm"
|
|
52
52
|
Requires-Dist: setuptools==83.*; extra == "llm"
|
|
53
53
|
Requires-Dist: pip~=26.1.2; extra == "llm"
|
|
54
|
+
Requires-Dist: openai~=2.48.0; extra == "llm"
|
|
54
55
|
Provides-Extra: dataframe
|
|
55
56
|
Requires-Dist: polars~=1.42.1; extra == "dataframe"
|
|
56
57
|
Requires-Dist: altair~=6.2.2; extra == "dataframe"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mycli"
|
|
3
3
|
dynamic = ["version"]
|
|
4
|
-
description = "Rich terminal client
|
|
4
|
+
description = "Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = "BSD-3-Clause"
|
|
@@ -67,6 +67,7 @@ llm = [
|
|
|
67
67
|
"llm ~= 0.31.1",
|
|
68
68
|
"setuptools == 83.*", # Required by llm commands to install models
|
|
69
69
|
"pip ~= 26.1.2",
|
|
70
|
+
"openai ~= 2.48.0", # openai v3.0.0 broke CI and would need work to support
|
|
70
71
|
]
|
|
71
72
|
dataframe = [
|
|
72
73
|
"polars ~= 1.42.1",
|
|
@@ -100,7 +101,7 @@ include = ["mycli*"]
|
|
|
100
101
|
|
|
101
102
|
[tool.uv]
|
|
102
103
|
exclude-newer = '1 week'
|
|
103
|
-
exclude-newer-package = { cli_helpers = false,
|
|
104
|
+
exclude-newer-package = { cli_helpers = false, cryptography = false }
|
|
104
105
|
|
|
105
106
|
[tool.ruff]
|
|
106
107
|
target-version = 'py310'
|
|
@@ -12,7 +12,11 @@ from typing import Any, cast
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
14
|
from mycli import boundary_tunnel
|
|
15
|
-
from mycli.boundary_tunnel import
|
|
15
|
+
from mycli.boundary_tunnel import (
|
|
16
|
+
TUNNEL_STABILIZATION_PAUSE,
|
|
17
|
+
BoundaryTunnel,
|
|
18
|
+
BoundaryTunnelError,
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
CONNECTION_DETAILS = (
|
|
18
22
|
'{"address":"127.0.0.1","credentials":[{"secret":{"decoded":{"username":"1234","password":"5678"}}}],'
|
|
@@ -478,6 +482,7 @@ def test_boundary_tunnel_start_reads_stdout_in_worker_thread(monkeypatch: pytest
|
|
|
478
482
|
monkeypatch.setattr(tunnel, '_read_stdout', fake_read_stdout)
|
|
479
483
|
checks = iter([False, True])
|
|
480
484
|
monkeypatch.setattr(tunnel, '_is_listening', lambda: next(checks))
|
|
485
|
+
monkeypatch.setattr(boundary_tunnel.time, 'sleep', lambda _seconds: None)
|
|
481
486
|
|
|
482
487
|
tunnel.start()
|
|
483
488
|
|
|
@@ -504,6 +509,7 @@ def test_boundary_tunnel_start_authenticates_before_starting_worker(monkeypatch:
|
|
|
504
509
|
monkeypatch.setattr(tunnel, '_authenticate_if_needed', fake_authenticate)
|
|
505
510
|
monkeypatch.setattr(tunnel, '_run', fake_run)
|
|
506
511
|
monkeypatch.setattr(tunnel, '_is_listening', lambda: True)
|
|
512
|
+
monkeypatch.setattr(boundary_tunnel.time, 'sleep', lambda _seconds: None)
|
|
507
513
|
|
|
508
514
|
tunnel.start()
|
|
509
515
|
|
|
@@ -526,7 +532,7 @@ def test_boundary_tunnel_start_waits_for_process_to_start(monkeypatch: pytest.Mo
|
|
|
526
532
|
|
|
527
533
|
tunnel.start()
|
|
528
534
|
|
|
529
|
-
assert sleeps == [0.05]
|
|
535
|
+
assert sleeps == [0.05, TUNNEL_STABILIZATION_PAUSE]
|
|
530
536
|
|
|
531
537
|
|
|
532
538
|
def test_boundary_tunnel_start_reports_cli_status_code(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -391,23 +391,18 @@ def test_init_reads_mylogin_cnf(monkeypatch: pytest.MonkeyPatch, tmp_path: Path)
|
|
|
391
391
|
assert cli.mylogin_cnf == {'client': 'config'}
|
|
392
392
|
|
|
393
393
|
|
|
394
|
-
def
|
|
394
|
+
def test_close_stops_refreshers_before_closing_connection_and_tunnels() -> None:
|
|
395
395
|
cli = MyCli.__new__(MyCli)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
cli.
|
|
401
|
-
cli.
|
|
402
|
-
cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: ssh_tunnel_closed.append(True))
|
|
403
|
-
cli.boundary_tunnel = SimpleNamespace(close=lambda: boundary_tunnel_closed.append(True)) # type: ignore[assignment]
|
|
396
|
+
calls: list[str] = []
|
|
397
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: calls.append('completion'))
|
|
398
|
+
cli.schema_prefetcher = SimpleNamespace(stop=lambda: calls.append('prefetch'))
|
|
399
|
+
cli.sqlexecute = SimpleNamespace(close=lambda: calls.append('connection')) # type: ignore[assignment]
|
|
400
|
+
cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: calls.append('ssh'))
|
|
401
|
+
cli.boundary_tunnel = SimpleNamespace(close=lambda: calls.append('boundary')) # type: ignore[assignment]
|
|
404
402
|
|
|
405
403
|
MyCli.close(cli)
|
|
406
404
|
|
|
407
|
-
assert
|
|
408
|
-
assert closed == [True]
|
|
409
|
-
assert ssh_tunnel_closed == [True]
|
|
410
|
-
assert boundary_tunnel_closed == [True]
|
|
405
|
+
assert calls == ['completion', 'prefetch', 'connection', 'ssh', 'boundary']
|
|
411
406
|
|
|
412
407
|
|
|
413
408
|
def test_close_swallows_cleanup_errors() -> None:
|
|
@@ -416,6 +411,7 @@ def test_close_swallows_cleanup_errors() -> None:
|
|
|
416
411
|
def fail() -> None:
|
|
417
412
|
raise RuntimeError('cleanup failed')
|
|
418
413
|
|
|
414
|
+
cli.completion_refresher = SimpleNamespace(stop=fail)
|
|
419
415
|
cli.schema_prefetcher = SimpleNamespace(stop=fail)
|
|
420
416
|
cli.sqlexecute = SimpleNamespace(close=fail) # type: ignore[assignment]
|
|
421
417
|
cast(Any, cli).ssh_tunnel = SimpleNamespace(close=fail)
|
|
@@ -425,6 +421,8 @@ def test_close_swallows_cleanup_errors() -> None:
|
|
|
425
421
|
|
|
426
422
|
def test_close_swallows_boundary_tunnel_close_error() -> None:
|
|
427
423
|
cli = MyCli.__new__(MyCli)
|
|
424
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: None)
|
|
425
|
+
cli.schema_prefetcher = SimpleNamespace(stop=lambda: None)
|
|
428
426
|
cli.sqlexecute = None
|
|
429
427
|
tunnel_closed: list[bool] = []
|
|
430
428
|
cast(Any, cli).ssh_tunnel = SimpleNamespace(close=lambda: tunnel_closed.append(True))
|
|
@@ -15,6 +15,7 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
|
|
|
15
15
|
cli = make_bare_mycli()
|
|
16
16
|
state: dict[str, Any] = {
|
|
17
17
|
'stopped': [],
|
|
18
|
+
'completion_stopped': [],
|
|
18
19
|
'refresh_calls': [],
|
|
19
20
|
'set_dbname_calls': [],
|
|
20
21
|
}
|
|
@@ -30,7 +31,8 @@ def make_refresh_cli() -> tuple[Any, dict[str, Any]]:
|
|
|
30
31
|
)
|
|
31
32
|
cli.main_formatter = SimpleNamespace(supported_formats=['ascii', 'csv'])
|
|
32
33
|
cli.completion_refresher = SimpleNamespace(
|
|
33
|
-
|
|
34
|
+
stop=lambda: state['completion_stopped'].append(True),
|
|
35
|
+
refresh=lambda executor, callbacks, options: state['refresh_calls'].append((executor, callbacks, options)),
|
|
34
36
|
)
|
|
35
37
|
cli.smart_completion = True
|
|
36
38
|
state['callback'] = callback
|
|
@@ -79,6 +81,15 @@ def test_refresh_completions_does_not_update_dbname_without_reset() -> None:
|
|
|
79
81
|
main.MyCli.refresh_completions(cli)
|
|
80
82
|
|
|
81
83
|
assert state['set_dbname_calls'] == []
|
|
84
|
+
assert state['completion_stopped'] == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_refresh_completions_stops_completion_worker_when_reset() -> None:
|
|
88
|
+
cli, state = make_refresh_cli()
|
|
89
|
+
|
|
90
|
+
main.MyCli.refresh_completions(cli, reset=True)
|
|
91
|
+
|
|
92
|
+
assert state['completion_stopped'] == [True]
|
|
82
93
|
|
|
83
94
|
|
|
84
95
|
def test_refresh_completions_updates_dbname_when_reset() -> None:
|
|
@@ -93,7 +104,7 @@ def test_refresh_completions_updates_dbname_when_reset() -> None:
|
|
|
93
104
|
set_dbname=lambda dbname: set_dbname_calls.append(dbname),
|
|
94
105
|
)
|
|
95
106
|
cli.main_formatter = SimpleNamespace(supported_formats=['table'])
|
|
96
|
-
cli.completion_refresher = SimpleNamespace(refresh=lambda executor, callbacks, options: None)
|
|
107
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: None, refresh=lambda executor, callbacks, options: None)
|
|
97
108
|
|
|
98
109
|
main.MyCli.refresh_completions(cli, reset=True)
|
|
99
110
|
|
|
@@ -113,7 +124,7 @@ def test_refresh_completions_uses_lock_when_reset() -> None:
|
|
|
113
124
|
set_dbname=lambda dbname: None,
|
|
114
125
|
)
|
|
115
126
|
cli.main_formatter = SimpleNamespace(supported_formats=['table'])
|
|
116
|
-
cli.completion_refresher = SimpleNamespace(refresh=lambda executor, callbacks, options: None)
|
|
127
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: None, refresh=lambda executor, callbacks, options: None)
|
|
117
128
|
|
|
118
129
|
main.MyCli.refresh_completions(cli, reset=True)
|
|
119
130
|
|
|
@@ -256,6 +256,108 @@ def test_refresh_cancels_pending_visibility_timer(monkeypatch, refresher) -> Non
|
|
|
256
256
|
assert refresher._visibility_timer is None
|
|
257
257
|
|
|
258
258
|
|
|
259
|
+
def test_stop_interrupts_and_joins_active_refresh(monkeypatch, refresher) -> None:
|
|
260
|
+
refresh_started = completion_refresher.threading.Event()
|
|
261
|
+
executor_closed = completion_refresher.threading.Event()
|
|
262
|
+
callback = Mock()
|
|
263
|
+
timer = Mock()
|
|
264
|
+
|
|
265
|
+
class FakeCompleter:
|
|
266
|
+
def __init__(self, **options) -> None:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
class FakeExecutor:
|
|
270
|
+
def __init__(self, *args) -> None:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
def close(self) -> None:
|
|
274
|
+
executor_closed.set()
|
|
275
|
+
|
|
276
|
+
def blocking_refresh(completer, executor) -> None:
|
|
277
|
+
refresh_started.set()
|
|
278
|
+
assert executor_closed.wait(timeout=1)
|
|
279
|
+
raise completion_refresher.pymysql.err.OperationalError(2013, 'connection closed')
|
|
280
|
+
|
|
281
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', FakeCompleter)
|
|
282
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', FakeExecutor)
|
|
283
|
+
refresher.refreshers = {'blocking': blocking_refresh}
|
|
284
|
+
|
|
285
|
+
refresher.refresh(make_sqlexecute(), callback)
|
|
286
|
+
assert refresh_started.wait(timeout=1)
|
|
287
|
+
refresher._visibility_timer = timer
|
|
288
|
+
|
|
289
|
+
refresher.stop()
|
|
290
|
+
|
|
291
|
+
assert executor_closed.is_set()
|
|
292
|
+
assert refresher._completer_thread is None
|
|
293
|
+
assert refresher._active_executor is None
|
|
294
|
+
assert refresher._restart_refresh.is_set() is False
|
|
295
|
+
assert refresher.is_refreshing() is False
|
|
296
|
+
timer.cancel.assert_called_once_with()
|
|
297
|
+
callback.assert_not_called()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_stop_before_executor_is_ready_prevents_refresh_and_callback(monkeypatch, refresher) -> None:
|
|
301
|
+
constructor_started = completion_refresher.threading.Event()
|
|
302
|
+
release_constructor = completion_refresher.threading.Event()
|
|
303
|
+
stop_finished = completion_refresher.threading.Event()
|
|
304
|
+
refresh = Mock()
|
|
305
|
+
callback = Mock()
|
|
306
|
+
|
|
307
|
+
class FakeCompleter:
|
|
308
|
+
def __init__(self, **options) -> None:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
class FakeExecutor:
|
|
312
|
+
def __init__(self, *args) -> None:
|
|
313
|
+
constructor_started.set()
|
|
314
|
+
assert release_constructor.wait(timeout=1)
|
|
315
|
+
|
|
316
|
+
def close(self) -> None:
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', FakeCompleter)
|
|
320
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', FakeExecutor)
|
|
321
|
+
refresher.refreshers = {'refresh': refresh}
|
|
322
|
+
|
|
323
|
+
refresher.refresh(make_sqlexecute(), callback)
|
|
324
|
+
assert constructor_started.wait(timeout=1)
|
|
325
|
+
stop_thread = completion_refresher.threading.Thread(target=lambda: (refresher.stop(), stop_finished.set()))
|
|
326
|
+
stop_thread.start()
|
|
327
|
+
assert refresher._stop_refresh.wait(timeout=1)
|
|
328
|
+
assert stop_finished.is_set() is False
|
|
329
|
+
release_constructor.set()
|
|
330
|
+
stop_thread.join(timeout=1)
|
|
331
|
+
|
|
332
|
+
assert stop_finished.is_set()
|
|
333
|
+
refresh.assert_not_called()
|
|
334
|
+
callback.assert_not_called()
|
|
335
|
+
assert refresher._completer_thread is None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_stop_tolerates_executor_close_error_without_worker(refresher) -> None:
|
|
339
|
+
executor = Mock()
|
|
340
|
+
executor.close.side_effect = RuntimeError('close failed')
|
|
341
|
+
refresher._active_executor = executor
|
|
342
|
+
|
|
343
|
+
refresher.stop()
|
|
344
|
+
|
|
345
|
+
executor.close.assert_called_once_with()
|
|
346
|
+
assert refresher._completer_thread is None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_stop_does_not_join_current_worker(monkeypatch, refresher) -> None:
|
|
350
|
+
thread = Mock()
|
|
351
|
+
thread.is_alive.return_value = True
|
|
352
|
+
refresher._completer_thread = thread
|
|
353
|
+
monkeypatch.setattr(completion_refresher.threading, 'current_thread', lambda: thread)
|
|
354
|
+
|
|
355
|
+
refresher.stop()
|
|
356
|
+
|
|
357
|
+
thread.join.assert_not_called()
|
|
358
|
+
assert refresher._completer_thread is thread
|
|
359
|
+
|
|
360
|
+
|
|
259
361
|
def test_finish_refreshing_schedules_delayed_invalidation_before_deadline(monkeypatch, refresher) -> None:
|
|
260
362
|
now = 10.0
|
|
261
363
|
monkeypatch.setattr(completion_refresher, 'monotonic', lambda: now)
|
|
@@ -474,6 +576,110 @@ def test_bg_refresh_returns_when_executor_connection_fails(monkeypatch, refreshe
|
|
|
474
576
|
callback.assert_not_called()
|
|
475
577
|
|
|
476
578
|
|
|
579
|
+
def test_bg_refresh_stops_after_current_refresher(monkeypatch, refresher) -> None:
|
|
580
|
+
callback = Mock()
|
|
581
|
+
executor = Mock()
|
|
582
|
+
|
|
583
|
+
def stop_refresh(completer, active_executor) -> None:
|
|
584
|
+
refresher._stop_refresh.set()
|
|
585
|
+
|
|
586
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
587
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
588
|
+
refresher.refreshers = {'stop': stop_refresh}
|
|
589
|
+
|
|
590
|
+
refresher._bg_refresh(make_sqlexecute(), callback, {})
|
|
591
|
+
|
|
592
|
+
callback.assert_not_called()
|
|
593
|
+
executor.close.assert_called_once_with()
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_bg_refresh_suppresses_non_operational_error_during_stop(monkeypatch, refresher) -> None:
|
|
597
|
+
executor = Mock()
|
|
598
|
+
|
|
599
|
+
def stop_with_error(completer, active_executor) -> None:
|
|
600
|
+
refresher._stop_refresh.set()
|
|
601
|
+
raise RuntimeError('cancelled refresh')
|
|
602
|
+
|
|
603
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
604
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
605
|
+
refresher.refreshers = {'stop': stop_with_error}
|
|
606
|
+
|
|
607
|
+
refresher._bg_refresh(make_sqlexecute(), Mock(), {})
|
|
608
|
+
|
|
609
|
+
executor.close.assert_called_once_with()
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def test_bg_refresh_skips_callbacks_when_stopped_after_refresh(monkeypatch, refresher) -> None:
|
|
613
|
+
callback = Mock()
|
|
614
|
+
executor = Mock()
|
|
615
|
+
is_stopped = Mock(side_effect=[False, True])
|
|
616
|
+
monkeypatch.setattr(refresher._stop_refresh, 'is_set', is_stopped)
|
|
617
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
618
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
619
|
+
refresher.refreshers = {}
|
|
620
|
+
|
|
621
|
+
refresher._bg_refresh(make_sqlexecute(), callback, {})
|
|
622
|
+
|
|
623
|
+
callback.assert_not_called()
|
|
624
|
+
assert is_stopped.call_count == 2
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def test_bg_refresh_propagates_unexpected_refresher_error(monkeypatch, refresher) -> None:
|
|
628
|
+
executor = Mock()
|
|
629
|
+
|
|
630
|
+
def fail_refresh(completer, active_executor) -> None:
|
|
631
|
+
refresher._active_executor = None
|
|
632
|
+
raise RuntimeError('refresh failed')
|
|
633
|
+
|
|
634
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
635
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
636
|
+
refresher.refreshers = {'fail': fail_refresh}
|
|
637
|
+
|
|
638
|
+
with pytest.raises(RuntimeError, match='refresh failed'):
|
|
639
|
+
refresher._bg_refresh(make_sqlexecute(), Mock(), {})
|
|
640
|
+
|
|
641
|
+
executor.close.assert_called_once_with()
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
@pytest.mark.parametrize('error_code', [completion_refresher.BAD_DB_ERROR, 2003])
|
|
645
|
+
def test_bg_refresh_only_suppresses_stale_database_error(monkeypatch, refresher, error_code) -> None:
|
|
646
|
+
executor = Mock()
|
|
647
|
+
callback = Mock()
|
|
648
|
+
|
|
649
|
+
def fail_refresh(completer, active_executor) -> None:
|
|
650
|
+
raise completion_refresher.pymysql.err.OperationalError(error_code, 'metadata failed')
|
|
651
|
+
|
|
652
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
653
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
654
|
+
refresher.refreshers = {'fail': fail_refresh}
|
|
655
|
+
|
|
656
|
+
if error_code == completion_refresher.BAD_DB_ERROR:
|
|
657
|
+
refresher._bg_refresh(make_sqlexecute(), callback, {})
|
|
658
|
+
else:
|
|
659
|
+
with pytest.raises(completion_refresher.pymysql.err.OperationalError, match='metadata failed'):
|
|
660
|
+
refresher._bg_refresh(make_sqlexecute(), callback, {})
|
|
661
|
+
|
|
662
|
+
callback.assert_not_called()
|
|
663
|
+
executor.close.assert_called_once_with()
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
@pytest.mark.parametrize('stopping', [False, True])
|
|
667
|
+
def test_bg_refresh_only_suppresses_executor_close_error_when_stopping(monkeypatch, refresher, stopping) -> None:
|
|
668
|
+
executor = Mock()
|
|
669
|
+
executor.close.side_effect = RuntimeError('close failed')
|
|
670
|
+
monkeypatch.setattr(completion_refresher, 'SQLCompleter', Mock())
|
|
671
|
+
monkeypatch.setattr(completion_refresher, 'SQLExecute', Mock(return_value=executor))
|
|
672
|
+
refresher.refreshers = {}
|
|
673
|
+
if stopping:
|
|
674
|
+
refresher._stop_refresh.set()
|
|
675
|
+
|
|
676
|
+
if stopping:
|
|
677
|
+
refresher._bg_refresh(make_sqlexecute(), Mock(), {})
|
|
678
|
+
else:
|
|
679
|
+
with pytest.raises(RuntimeError, match='close failed'):
|
|
680
|
+
refresher._bg_refresh(make_sqlexecute(), Mock(), {})
|
|
681
|
+
|
|
682
|
+
|
|
477
683
|
def test_refresher_decorator_registers_function() -> None:
|
|
478
684
|
refreshers: dict[str, object] = {}
|
|
479
685
|
|