mycli 2.13.1__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.1/mycli.egg-info → mycli-2.13.2}/PKG-INFO +3 -2
- {mycli-2.13.1 → mycli-2.13.2}/changelog.md +19 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/client.py +4 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/client_query.py +1 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/completion_refresher.py +54 -4
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/repl.py +8 -2
- {mycli-2.13.1 → mycli-2.13.2/mycli.egg-info}/PKG-INFO +3 -2
- {mycli-2.13.1 → 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.1 → mycli-2.13.2}/pyproject.toml +3 -2
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client.py +11 -13
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_query.py +14 -3
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completion_refresher.py +206 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_repl.py +24 -1
- mycli-2.13.1/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.13.1 → mycli-2.13.2}/.git-blame-ignore-revs +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/dependabot.yml +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/ci.yml +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/lint.yml +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/publish.yml +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/.gitignore +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/AGENTS.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/AUTHORS.rst +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/CONTRIBUTING.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/LICENSE.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/MANIFEST.in +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/README.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/SPONSORS.rst +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/key_bindings.rst +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/llm.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/main.gif +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/tables.png +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/doc/transforms.md +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/AUTHORS +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/SPONSORS +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/TIPS +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/app_state.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/cli_runner.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/clibuffer.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/client_commands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/client_connection.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/clistyle.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/clitoolbar.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/compat.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/config.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/constants.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/key_bindings.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/lexer.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/batch.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/completions.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/execute.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/myclirc +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/output.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/filepaths.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/polars_transform.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/llm.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/main.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/special/utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/string_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/password_sources.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/sqlcompleter.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/sqlexecute.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/types.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli/vault.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/SOURCES.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/scm_file_list.json +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/setup.cfg +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/auto_vertical.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/basic_commands.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/connection.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/crud_database.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/crud_table.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/db_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/environment.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/fixture_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/iocommands.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/named_queries.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/specials.feature +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/__init__.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/connection.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/crud_database.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/crud_table.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/iocommands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/named_queries.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/specials.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/steps/wrappers.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/features/wrappager.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/myclirc +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/mylogin.cnf +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/conftest.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_app_state.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_checkup.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_config.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_lexer.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_output.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_polars_transform.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_init.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_main.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_vault.py +0 -0
- {mycli-2.13.1 → mycli-2.13.2}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.13.1 → 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,22 @@
|
|
|
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
|
+
|
|
1
20
|
2.13.1 (2026/08/11)
|
|
2
21
|
==============
|
|
3
22
|
|
|
@@ -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'
|
|
@@ -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
|
|
|
@@ -193,6 +193,7 @@ def make_repl_cli(sqlexecute: Any | None = None) -> Any:
|
|
|
193
193
|
timing_calls: list[tuple[str, bool]] = []
|
|
194
194
|
log_queries: list[str] = []
|
|
195
195
|
cli.refresh_calls = refresh_calls
|
|
196
|
+
cli.completion_stop_calls = []
|
|
196
197
|
cli.output_calls = output_calls
|
|
197
198
|
cli.echo_calls = echo_calls
|
|
198
199
|
cli.timing_calls = timing_calls
|
|
@@ -208,6 +209,7 @@ def make_repl_cli(sqlexecute: Any | None = None) -> Any:
|
|
|
208
209
|
return [SQLResult(status='refresh')]
|
|
209
210
|
|
|
210
211
|
cli.refresh_completions = refresh_completions
|
|
212
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: cli.completion_stop_calls.append(True))
|
|
211
213
|
|
|
212
214
|
def output_timing(timing: str, is_warnings_style: bool = False) -> None:
|
|
213
215
|
cli.timing_calls.append((timing, is_warnings_style))
|
|
@@ -1162,6 +1164,7 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
|
|
|
1162
1164
|
|
|
1163
1165
|
sqlexecute = FakeSQLExecute()
|
|
1164
1166
|
cli = make_repl_cli(sqlexecute)
|
|
1167
|
+
cli.completion_refresher = SimpleNamespace(stop=lambda: sqlexecute.calls.append('stop'))
|
|
1165
1168
|
cli.logfile = False
|
|
1166
1169
|
cli.destructive_warning = True
|
|
1167
1170
|
monkeypatch.setattr(repl_mode, 'is_redirect_command', lambda text: text == 'redirect')
|
|
@@ -1185,7 +1188,7 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
|
|
|
1185
1188
|
assert cli.query_history[-1].successful is True
|
|
1186
1189
|
assert cli.query_history[-1].mutating is True
|
|
1187
1190
|
assert sqlexecute.dbname is None
|
|
1188
|
-
assert sqlexecute.calls == ['dropdb', 'connect']
|
|
1191
|
+
assert sqlexecute.calls == ['stop', 'dropdb', 'connect']
|
|
1189
1192
|
assert 'Warning: This query was not logged.' in cli.echo_calls
|
|
1190
1193
|
|
|
1191
1194
|
repl_mode._one_iteration(cli, repl_mode.ReplState(), 'approved')
|
|
@@ -1195,6 +1198,26 @@ def test_one_iteration_covers_redirect_destructive_success_refresh_and_logfile(m
|
|
|
1195
1198
|
assert 'Wise choice!' in cli.echo_calls
|
|
1196
1199
|
|
|
1197
1200
|
|
|
1201
|
+
def test_one_iteration_restarts_completions_when_active_database_drop_fails(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1202
|
+
patch_repl_runtime_defaults(monkeypatch)
|
|
1203
|
+
|
|
1204
|
+
class FakeSQLExecute:
|
|
1205
|
+
dbname = 'db'
|
|
1206
|
+
connection_id = 0
|
|
1207
|
+
|
|
1208
|
+
def run(self, text: str) -> Iterator[SQLResult]:
|
|
1209
|
+
raise pymysql.OperationalError(1064, 'drop failed')
|
|
1210
|
+
|
|
1211
|
+
cli = make_repl_cli(FakeSQLExecute())
|
|
1212
|
+
monkeypatch.setattr(repl_mode, 'is_dropping_database', lambda text, dbname: True)
|
|
1213
|
+
|
|
1214
|
+
repl_mode._one_iteration(cli, repl_mode.ReplState(), 'drop database db')
|
|
1215
|
+
|
|
1216
|
+
assert cli.completion_stop_calls == [True]
|
|
1217
|
+
assert cli.refresh_calls == [False]
|
|
1218
|
+
assert cli.sqlexecute.dbname == 'db'
|
|
1219
|
+
|
|
1220
|
+
|
|
1198
1221
|
@pytest.mark.parametrize(
|
|
1199
1222
|
('terminator', 'setter_name'),
|
|
1200
1223
|
[(r'\x', 'set_explorer_output'), (r'\G', 'set_expanded_output')],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|