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.
Files changed (189) hide show
  1. {mycli-2.13.0/mycli.egg-info → mycli-2.13.2}/PKG-INFO +3 -2
  2. {mycli-2.13.0 → mycli-2.13.2}/changelog.md +27 -0
  3. {mycli-2.13.0 → mycli-2.13.2}/mycli/boundary_tunnel.py +5 -0
  4. {mycli-2.13.0 → mycli-2.13.2}/mycli/client.py +4 -0
  5. {mycli-2.13.0 → mycli-2.13.2}/mycli/client_query.py +1 -0
  6. {mycli-2.13.0 → mycli-2.13.2}/mycli/completion_refresher.py +54 -4
  7. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/repl.py +8 -2
  8. {mycli-2.13.0 → mycli-2.13.2/mycli.egg-info}/PKG-INFO +3 -2
  9. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/requires.txt +1 -0
  10. mycli-2.13.2/mycli.egg-info/scm_version.json +8 -0
  11. {mycli-2.13.0 → mycli-2.13.2}/pyproject.toml +3 -2
  12. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_boundary_tunnel.py +8 -2
  13. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client.py +11 -13
  14. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_query.py +14 -3
  15. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completion_refresher.py +206 -0
  16. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_repl.py +24 -1
  17. mycli-2.13.0/mycli.egg-info/scm_version.json +0 -8
  18. {mycli-2.13.0 → mycli-2.13.2}/.git-blame-ignore-revs +0 -0
  19. {mycli-2.13.0 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {mycli-2.13.0 → mycli-2.13.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {mycli-2.13.0 → mycli-2.13.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {mycli-2.13.0 → mycli-2.13.2}/.github/dependabot.yml +0 -0
  23. {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/ci.yml +0 -0
  24. {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/lint.yml +0 -0
  25. {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/publish.yml +0 -0
  26. {mycli-2.13.0 → mycli-2.13.2}/.github/workflows/typecheck.yml +0 -0
  27. {mycli-2.13.0 → mycli-2.13.2}/.gitignore +0 -0
  28. {mycli-2.13.0 → mycli-2.13.2}/AGENTS.md +0 -0
  29. {mycli-2.13.0 → mycli-2.13.2}/AUTHORS.rst +0 -0
  30. {mycli-2.13.0 → mycli-2.13.2}/CONTRIBUTING.md +0 -0
  31. {mycli-2.13.0 → mycli-2.13.2}/LICENSE.txt +0 -0
  32. {mycli-2.13.0 → mycli-2.13.2}/MANIFEST.in +0 -0
  33. {mycli-2.13.0 → mycli-2.13.2}/README.md +0 -0
  34. {mycli-2.13.0 → mycli-2.13.2}/SPONSORS.rst +0 -0
  35. {mycli-2.13.0 → mycli-2.13.2}/doc/key_bindings.rst +0 -0
  36. {mycli-2.13.0 → mycli-2.13.2}/doc/llm.md +0 -0
  37. {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/main.gif +0 -0
  38. {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/tables.png +0 -0
  39. {mycli-2.13.0 → mycli-2.13.2}/doc/screenshots/total_histogram.png +0 -0
  40. {mycli-2.13.0 → mycli-2.13.2}/doc/transforms.md +0 -0
  41. {mycli-2.13.0 → mycli-2.13.2}/mycli/AUTHORS +0 -0
  42. {mycli-2.13.0 → mycli-2.13.2}/mycli/SPONSORS +0 -0
  43. {mycli-2.13.0 → mycli-2.13.2}/mycli/TIPS +0 -0
  44. {mycli-2.13.0 → mycli-2.13.2}/mycli/__init__.py +0 -0
  45. {mycli-2.13.0 → mycli-2.13.2}/mycli/app_state.py +0 -0
  46. {mycli-2.13.0 → mycli-2.13.2}/mycli/cli_runner.py +0 -0
  47. {mycli-2.13.0 → mycli-2.13.2}/mycli/clibuffer.py +0 -0
  48. {mycli-2.13.0 → mycli-2.13.2}/mycli/client_commands.py +0 -0
  49. {mycli-2.13.0 → mycli-2.13.2}/mycli/client_connection.py +0 -0
  50. {mycli-2.13.0 → mycli-2.13.2}/mycli/clistyle.py +0 -0
  51. {mycli-2.13.0 → mycli-2.13.2}/mycli/clitoolbar.py +0 -0
  52. {mycli-2.13.0 → mycli-2.13.2}/mycli/compat.py +0 -0
  53. {mycli-2.13.0 → mycli-2.13.2}/mycli/config.py +0 -0
  54. {mycli-2.13.0 → mycli-2.13.2}/mycli/constants.py +0 -0
  55. {mycli-2.13.0 → mycli-2.13.2}/mycli/key_bindings.py +0 -0
  56. {mycli-2.13.0 → mycli-2.13.2}/mycli/lexer.py +0 -0
  57. {mycli-2.13.0 → mycli-2.13.2}/mycli/main.py +0 -0
  58. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/batch.py +0 -0
  59. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/checkup.py +0 -0
  60. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/completions.py +0 -0
  61. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/execute.py +0 -0
  62. {mycli-2.13.0 → mycli-2.13.2}/mycli/main_modes/list_dsn.py +0 -0
  63. {mycli-2.13.0 → mycli-2.13.2}/mycli/myclirc +0 -0
  64. {mycli-2.13.0 → mycli-2.13.2}/mycli/output.py +0 -0
  65. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/__init__.py +0 -0
  66. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/batch_utils.py +0 -0
  67. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/cli_utils.py +0 -0
  68. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/completion_engine.py +0 -0
  69. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/filepaths.py +0 -0
  70. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/hybrid_redirection.py +0 -0
  71. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/interactive_utils.py +0 -0
  72. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/key_binding_utils.py +0 -0
  73. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/polars_transform.py +0 -0
  74. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/__init__.py +0 -0
  75. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/fzf.py +0 -0
  76. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/history.py +0 -0
  77. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/ptoolkit/utils.py +0 -0
  78. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/__init__.py +0 -0
  79. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/dbcommands.py +0 -0
  80. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/delimitercommand.py +0 -0
  81. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/dsn_aliases.py +0 -0
  82. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/favoritequeries.py +0 -0
  83. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/iocommands.py +0 -0
  84. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/llm.py +0 -0
  85. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/main.py +0 -0
  86. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/special/utils.py +0 -0
  87. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/sql_utils.py +0 -0
  88. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/sqlresult.py +0 -0
  89. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/string_utils.py +0 -0
  90. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/tabular_output/__init__.py +0 -0
  91. {mycli-2.13.0 → mycli-2.13.2}/mycli/packages/tabular_output/sql_format.py +0 -0
  92. {mycli-2.13.0 → mycli-2.13.2}/mycli/password_sources.py +0 -0
  93. {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/bash/mycli +0 -0
  94. {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/fish/mycli.fish +0 -0
  95. {mycli-2.13.0 → mycli-2.13.2}/mycli/resources/completions/zsh/_mycli +0 -0
  96. {mycli-2.13.0 → mycli-2.13.2}/mycli/schema_prefetcher.py +0 -0
  97. {mycli-2.13.0 → mycli-2.13.2}/mycli/sqlcompleter.py +0 -0
  98. {mycli-2.13.0 → mycli-2.13.2}/mycli/sqlexecute.py +0 -0
  99. {mycli-2.13.0 → mycli-2.13.2}/mycli/ssh_tunnel.py +0 -0
  100. {mycli-2.13.0 → mycli-2.13.2}/mycli/types.py +0 -0
  101. {mycli-2.13.0 → mycli-2.13.2}/mycli/vault.py +0 -0
  102. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/SOURCES.txt +0 -0
  103. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/dependency_links.txt +0 -0
  104. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/entry_points.txt +0 -0
  105. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/scm_file_list.json +0 -0
  106. {mycli-2.13.0 → mycli-2.13.2}/mycli.egg-info/top_level.txt +0 -0
  107. {mycli-2.13.0 → mycli-2.13.2}/setup.cfg +0 -0
  108. {mycli-2.13.0 → mycli-2.13.2}/test/__init__.py +0 -0
  109. {mycli-2.13.0 → mycli-2.13.2}/test/features/__init__.py +0 -0
  110. {mycli-2.13.0 → mycli-2.13.2}/test/features/auto_vertical.feature +0 -0
  111. {mycli-2.13.0 → mycli-2.13.2}/test/features/basic_commands.feature +0 -0
  112. {mycli-2.13.0 → mycli-2.13.2}/test/features/connection.feature +0 -0
  113. {mycli-2.13.0 → mycli-2.13.2}/test/features/crud_database.feature +0 -0
  114. {mycli-2.13.0 → mycli-2.13.2}/test/features/crud_table.feature +0 -0
  115. {mycli-2.13.0 → mycli-2.13.2}/test/features/db_utils.py +0 -0
  116. {mycli-2.13.0 → mycli-2.13.2}/test/features/environment.py +0 -0
  117. {mycli-2.13.0 → mycli-2.13.2}/test/features/fixture_data/help_commands.txt +0 -0
  118. {mycli-2.13.0 → mycli-2.13.2}/test/features/fixture_utils.py +0 -0
  119. {mycli-2.13.0 → mycli-2.13.2}/test/features/iocommands.feature +0 -0
  120. {mycli-2.13.0 → mycli-2.13.2}/test/features/named_queries.feature +0 -0
  121. {mycli-2.13.0 → mycli-2.13.2}/test/features/specials.feature +0 -0
  122. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/__init__.py +0 -0
  123. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/auto_vertical.py +0 -0
  124. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/basic_commands.py +0 -0
  125. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/connection.py +0 -0
  126. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/crud_database.py +0 -0
  127. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/crud_table.py +0 -0
  128. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/iocommands.py +0 -0
  129. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/named_queries.py +0 -0
  130. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/specials.py +0 -0
  131. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/utils.py +0 -0
  132. {mycli-2.13.0 → mycli-2.13.2}/test/features/steps/wrappers.py +0 -0
  133. {mycli-2.13.0 → mycli-2.13.2}/test/features/wrappager.py +0 -0
  134. {mycli-2.13.0 → mycli-2.13.2}/test/myclirc +0 -0
  135. {mycli-2.13.0 → mycli-2.13.2}/test/mylogin.cnf +0 -0
  136. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/conftest.py +0 -0
  137. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_app_state.py +0 -0
  138. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_bash_completion.py +0 -0
  139. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_batch_utils.py +0 -0
  140. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_checkup.py +0 -0
  141. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_cli_runner.py +0 -0
  142. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_cli_utils.py +0 -0
  143. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clibuffer.py +0 -0
  144. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_commands.py +0 -0
  145. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_client_connection.py +0 -0
  146. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clistyle.py +0 -0
  147. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_clitoolbar.py +0 -0
  148. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completer_use_switch.py +0 -0
  149. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_completion_engine.py +0 -0
  150. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_config.py +0 -0
  151. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_delimitercommand.py +0 -0
  152. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_dsn_aliases.py +0 -0
  153. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_favoritequeries.py +0 -0
  154. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_filepaths.py +0 -0
  155. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_fish_completion.py +0 -0
  156. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_hybrid_redirection.py +0 -0
  157. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_interactive_utils.py +0 -0
  158. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_key_binding_utils.py +0 -0
  159. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_key_bindings.py +0 -0
  160. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_lexer.py +0 -0
  161. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main.py +0 -0
  162. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_batch.py +0 -0
  163. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_execute.py +0 -0
  164. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_main_modes_list_dsn.py +0 -0
  165. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_naive_completion.py +0 -0
  166. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_output.py +0 -0
  167. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_password_sources.py +0 -0
  168. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_polars_transform.py +0 -0
  169. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_fzf.py +0 -0
  170. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_history.py +0 -0
  171. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ptoolkit_utils.py +0 -0
  172. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_schema_prefetcher.py +0 -0
  173. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  174. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_dbcommands.py +0 -0
  175. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_init.py +0 -0
  176. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_iocommands.py +0 -0
  177. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_llm.py +0 -0
  178. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_main.py +0 -0
  179. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_special_utils.py +0 -0
  180. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sql_utils.py +0 -0
  181. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlcompleter.py +0 -0
  182. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlexecute.py +0 -0
  183. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_sqlresult.py +0 -0
  184. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_ssh_tunnel.py +0 -0
  185. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_string_utils.py +0 -0
  186. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_tabular_output.py +0 -0
  187. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_vault.py +0 -0
  188. {mycli-2.13.0 → mycli-2.13.2}/test/pytests/test_zsh_completion.py +0 -0
  189. {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.0
4
- Summary: Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes
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
- for callback in callbacks:
113
- callback(completer)
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
- executor.close()
116
- self._finish_refreshing()
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 is_dropping_database(text, sqlexecute.dbname):
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.0
4
- Summary: Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes
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"
@@ -42,3 +42,4 @@ ruff~=0.15.0
42
42
  llm~=0.31.1
43
43
  setuptools==83.*
44
44
  pip~=26.1.2
45
+ openai~=2.48.0
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.13.2",
3
+ "distance": 0,
4
+ "node": "g8da77508588513a62fa7ec1811a17f0c6ae14e86",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-12"
8
+ }
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mycli"
3
3
  dynamic = ["version"]
4
- description = "Rich terminal client for MySQL with autocompletion, syntax highlighting, and dataframes"
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, openai = false, cryptography = 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 BoundaryTunnel, BoundaryTunnelError
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 test_close_stops_schema_prefetcher_and_closes_sqlexecute() -> None:
394
+ def test_close_stops_refreshers_before_closing_connection_and_tunnels() -> None:
395
395
  cli = MyCli.__new__(MyCli)
396
- stopped: list[bool] = []
397
- closed: list[bool] = []
398
- ssh_tunnel_closed: list[bool] = []
399
- boundary_tunnel_closed: list[bool] = []
400
- cli.schema_prefetcher = SimpleNamespace(stop=lambda: stopped.append(True))
401
- cli.sqlexecute = SimpleNamespace(close=lambda: closed.append(True)) # type: ignore[assignment]
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 stopped == [True]
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
- refresh=lambda executor, callbacks, options: state['refresh_calls'].append((executor, callbacks, options))
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