harlequin 2.2.1__tar.gz → 2.3.0__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 (74) hide show
  1. {harlequin-2.2.1 → harlequin-2.3.0}/CHANGELOG.md +12 -1
  2. {harlequin-2.2.1 → harlequin-2.3.0}/PKG-INFO +6 -12
  3. {harlequin-2.2.1 → harlequin-2.3.0}/pyproject.toml +20 -25
  4. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/app.py +42 -19
  5. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/app.tcss +6 -2
  6. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/autocomplete/completers.py +1 -1
  7. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/cli.py +4 -6
  8. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/code_editor.py +22 -10
  9. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/data_catalog/__init__.py +5 -5
  10. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/data_catalog/database_tree.py +4 -13
  11. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/debug_info.py +1 -1
  12. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/error_modal.py +1 -1
  13. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/keys_app.py +1 -1
  14. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/plugins.py +1 -6
  15. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/adapter.py +1 -0
  16. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/adapter.py +1 -1
  17. {harlequin-2.2.1 → harlequin-2.3.0}/.gitignore +0 -0
  18. {harlequin-2.2.1 → harlequin-2.3.0}/LICENSE +0 -0
  19. {harlequin-2.2.1 → harlequin-2.3.0}/README.md +0 -0
  20. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/__init__.py +0 -0
  21. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/__main__.py +0 -0
  22. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/actions.py +0 -0
  23. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/adapter.py +0 -0
  24. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/app_base.py +0 -0
  25. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/autocomplete/__init__.py +0 -0
  26. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/autocomplete/completion.py +0 -0
  27. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/autocomplete/constants.py +0 -0
  28. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/bindings.py +0 -0
  29. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/cache.py +0 -0
  30. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/catalog.py +0 -0
  31. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/catalog_cache.py +0 -0
  32. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/colors.py +0 -0
  33. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/__init__.py +0 -0
  34. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/confirm_modal.py +0 -0
  35. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
  36. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
  37. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/data_catalog/tree.py +0 -0
  38. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/export_screen.py +0 -0
  39. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/help_screen.md +0 -0
  40. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/help_screen.py +0 -0
  41. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/history_screen.py +0 -0
  42. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/results_viewer.py +0 -0
  43. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/components/run_query_bar.py +0 -0
  44. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/config.py +0 -0
  45. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/config_wizard.py +0 -0
  46. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/copy_formats.py +0 -0
  47. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/copy_widgets.py +0 -0
  48. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/driver.py +0 -0
  49. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/editor_cache.py +0 -0
  50. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/exception.py +0 -0
  51. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/export.py +0 -0
  52. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/global.tcss +0 -0
  53. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/history.py +0 -0
  54. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/keymap.py +0 -0
  55. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/keys_app.tcss +0 -0
  56. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/locale_manager.py +0 -0
  57. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/messages.py +0 -0
  58. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/options.py +0 -0
  59. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/py.typed +0 -0
  60. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/transaction_mode.py +0 -0
  61. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin/windows_timezone.py +0 -0
  62. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/__init__.py +0 -0
  63. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/catalog.py +0 -0
  64. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/cli_options.py +0 -0
  65. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/completions.py +0 -0
  66. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/interactions.py +0 -0
  67. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_duckdb/py.typed +0 -0
  68. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/__init__.py +0 -0
  69. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/catalog.py +0 -0
  70. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/cli_options.py +0 -0
  71. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/completions.py +0 -0
  72. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/interactions.py +0 -0
  73. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_sqlite/py.typed +0 -0
  74. {harlequin-2.2.1 → harlequin-2.3.0}/src/harlequin_vscode/__init__.py +0 -0
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.3.0] - 2025-10-24
8
+
9
+ ### Breaking Changes
10
+
11
+ - Drops support for Python 3.9.
12
+
13
+ ### Bug Fixes
14
+
15
+ - Harlequin will no longer execute an empty query; the Run Query button will appear disabled if the buffer is empty ([#873](https://github.com/tconbeer/harlequin/issues/873)).
16
+
7
17
  ## [2.2.1] - 2025-10-16
8
18
 
9
19
  ### Bug Fixes
@@ -835,7 +845,8 @@ All notable changes to this project will be documented in this file.
835
845
 
836
846
  - Use the DuckDB CLI.
837
847
 
838
- [unreleased]: https://github.com/tconbeer/harlequin/compare/2.2.1...HEAD
848
+ [unreleased]: https://github.com/tconbeer/harlequin/compare/2.3.0...HEAD
849
+ [2.3.0]: https://github.com/tconbeer/harlequin/compare/2.2.1...2.3.0
839
850
  [2.2.1]: https://github.com/tconbeer/harlequin/compare/2.2.0...2.2.1
840
851
  [2.2.0]: https://github.com/tconbeer/harlequin/compare/2.1.3...2.2.0
841
852
  [2.1.3]: https://github.com/tconbeer/harlequin/compare/2.1.2...2.1.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: harlequin
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: The SQL IDE for Your Terminal.
5
5
  Project-URL: Homepage, https://harlequin.sh
6
6
  Project-URL: Documentation, https://harlequin.sh/docs/getting-started/index
@@ -10,24 +10,18 @@ Project-URL: Changelog, https://github.com/tconbeer/harlequin/blob/main/CHANGELO
10
10
  Author-email: Ted Conbeer <tconbeer@users.noreply.github.com>
11
11
  License-Expression: MIT
12
12
  License-File: LICENSE
13
- Keywords: harlequin,ide,sql,tui
14
- Requires-Python: <3.14,>=3.9
13
+ Keywords: harlequin,ide,sql,textual,tui
14
+ Requires-Python: <3.14,>=3.10
15
15
  Requires-Dist: click==8.1.8
16
16
  Requires-Dist: duckdb>=0.8.0
17
- Requires-Dist: importlib-metadata>=8.0; python_version < '3.10'
18
- Requires-Dist: numpy<2.1.0,>=2.0.0; python_version < '3.10'
19
- Requires-Dist: numpy<2.3.0,>=2.1.0; python_version >= '3.10'
20
17
  Requires-Dist: platformdirs<5.0,>=3.10
21
- Requires-Dist: pyarrow>=18.1.0; python_version >= '3.12'
22
18
  Requires-Dist: questionary==2.0.1
23
19
  Requires-Dist: rich-click==1.8.5
24
20
  Requires-Dist: shandy-sqlfmt>=0.19.0
25
- Requires-Dist: textual-fastdatatable==0.12.0
26
- Requires-Dist: textual-textarea==0.15.0
27
- Requires-Dist: textual==0.89.1
21
+ Requires-Dist: textual-fastdatatable==0.13.0
22
+ Requires-Dist: textual-textarea==0.17.2
23
+ Requires-Dist: textual==6.4.0
28
24
  Requires-Dist: tomlkit<0.14.0,>=0.12.5
29
- Requires-Dist: tree-sitter-sql==0.3.7
30
- Requires-Dist: tree-sitter<0.25.0,>=0.23.0
31
25
  Provides-Extra: adbc
32
26
  Requires-Dist: harlequin-adbc; extra == 'adbc'
33
27
  Provides-Extra: bigquery
@@ -1,20 +1,20 @@
1
1
  [project]
2
2
 
3
3
  name = "harlequin"
4
- version = "2.2.1"
5
- requires-python = ">=3.9,<3.14"
4
+ version = "2.3.0"
5
+ requires-python = ">=3.10,<3.14"
6
6
  description = "The SQL IDE for Your Terminal."
7
7
  authors = [
8
8
  { name = "Ted Conbeer", email = "tconbeer@users.noreply.github.com" }
9
9
  ]
10
10
  license = "MIT"
11
11
  readme = "README.md"
12
- keywords = ["sql", "tui", "ide", "harlequin"]
12
+ keywords = ["sql", "tui", "ide", "textual", "harlequin"]
13
13
  dependencies = [
14
14
  # textual and component libraries
15
- "textual==0.89.1",
16
- "textual-fastdatatable==0.12.0",
17
- "textual-textarea==0.15.0",
15
+ "textual==6.4.0",
16
+ "textual-fastdatatable==0.13.0",
17
+ "textual-textarea==0.17.2",
18
18
 
19
19
  # click
20
20
  "click==8.1.8",
@@ -26,20 +26,6 @@ dependencies = [
26
26
  "platformdirs>=3.10,<5.0",
27
27
  "tomlkit>=0.12.5,<0.14.0",
28
28
  "questionary==2.0.1",
29
-
30
- # tree-sitter-sql 0.3.8 seems incompatible with tree-sitter 0.24.0
31
- "tree-sitter-sql==0.3.7",
32
- "tree-sitter>=0.23.0,<0.25.0",
33
-
34
- # backports
35
- "importlib_metadata>=8.0; python_version < '3.10'",
36
-
37
- # wheel pins: these packages don't build wheels for all Pythons
38
-
39
- "numpy>=2.0.0,<2.1.0; python_version < '3.10'",
40
- "numpy>=2.1.0,<2.3.0; python_version >= '3.10'",
41
-
42
- "pyarrow>=18.1.0; python_version >= '3.12'",
43
29
  ]
44
30
 
45
31
 
@@ -60,14 +46,14 @@ nebulagraph = ["harlequin-nebulagraph"]
60
46
  [dependency-groups]
61
47
  dev = [
62
48
  "pre-commit>=3.3,<4",
63
- "textual-dev==1.0.1; python_version < '3.12'",
49
+ "textual-dev==1.8.0",
64
50
  "harlequin-postgres>=1.1.1,<2",
65
51
  "harlequin-mysql>=1,<2",
66
52
  "pyinstrument>=5,<6",
67
53
  ]
68
54
  static = [
69
- "ruff>=0.6,<0.10",
70
- "mypy>=1.11.0,<2",
55
+ "ruff>=0.14.1",
56
+ "mypy==1.18.2,<2",
71
57
  "pandas-stubs>=2,<3",
72
58
  "boto3-stubs>=1.34.23,<2",
73
59
  ]
@@ -75,12 +61,20 @@ test = [
75
61
  "pytest>=7.3.1,<9.0.0",
76
62
  "pytest-asyncio==0.21.0",
77
63
  "pytest-textual-snapshot @ https://github.com/tconbeer/pytest-textual-snapshot.git#main",
64
+ "boto3==1.34.22",
78
65
  # extension tests require consistent version of duckdb.
79
66
  "duckdb==1.1.1",
80
67
  "flaky>=3.8.1",
81
68
  ]
82
69
 
83
70
 
71
+ # For development of related libraries
72
+ # [tool.uv.sources]
73
+
74
+ # textual-fastdatatable = { path = "../textual-fastdatatable" }
75
+ # textual-textarea = { path = "../textual-textarea" }
76
+
77
+
84
78
  [project.urls]
85
79
 
86
80
  Homepage = "https://harlequin.sh"
@@ -134,7 +128,7 @@ only-include = [
134
128
 
135
129
  [tool.ruff]
136
130
 
137
- target-version = "py39"
131
+ target-version = "py310"
138
132
 
139
133
 
140
134
  [tool.ruff.lint]
@@ -144,7 +138,7 @@ select = ["A", "B", "E", "F", "I"]
144
138
 
145
139
  [tool.mypy]
146
140
 
147
- python_version = "3.9"
141
+ python_version = "3.10"
148
142
  files = [
149
143
  "src/**/*.py",
150
144
  "tests/**/*.py",
@@ -180,4 +174,5 @@ strict_equality = true
180
174
  markers = [
181
175
  "online: requeries internet connection (deselect with '-m \"not online\"')",
182
176
  "use_cache: override the autouse fixture the disables the buffer cache",
177
+ "py12: Only run on Python 3.12 or higher",
183
178
  ]
@@ -326,12 +326,14 @@ class Harlequin(AppBase):
326
326
  @on(Button.Pressed, "#run_query")
327
327
  def submit_query_from_run_query_bar(self, message: Button.Pressed) -> None:
328
328
  message.stop()
329
- self.post_message(
330
- QuerySubmitted(
331
- queries=self._get_selected_queries(),
332
- limit=self.run_query_bar.limit_value,
329
+ queries = self._get_selected_queries()
330
+ if queries:
331
+ self.post_message(
332
+ QuerySubmitted(
333
+ queries=queries,
334
+ limit=self.run_query_bar.limit_value,
335
+ )
333
336
  )
334
- )
335
337
 
336
338
  @on(Button.Pressed, "#cancel_query")
337
339
  def cancel_query(self, message: Button.Pressed) -> None:
@@ -374,12 +376,14 @@ class Harlequin(AppBase):
374
376
  @on(CodeEditor.Submitted)
375
377
  def submit_query_from_editor(self, message: CodeEditor.Submitted) -> None:
376
378
  message.stop()
377
- self.post_message(
378
- QuerySubmitted(
379
- queries=self._get_selected_queries(),
380
- limit=self.run_query_bar.limit_value,
379
+ queries = self._get_selected_queries()
380
+ if queries:
381
+ self.post_message(
382
+ QuerySubmitted(
383
+ queries=queries,
384
+ limit=self.run_query_bar.limit_value,
385
+ )
381
386
  )
382
- )
383
387
 
384
388
  @on(DatabaseConnected)
385
389
  def initialize_app(self, message: DatabaseConnected) -> None:
@@ -494,12 +498,14 @@ class Harlequin(AppBase):
494
498
  self.set_timer(delay=0.1, callback=callback)
495
499
  return
496
500
  self.editor.focus()
501
+ self._sync_run_button_disabled()
502
+ self._sync_run_button_text()
503
+
504
+ def on_text_area_changed(self) -> None:
505
+ self._sync_run_button_disabled()
497
506
 
498
507
  def on_text_area_selection_changed(self) -> None:
499
- if self._validate_selection():
500
- self.run_query_bar.run_button.label = "Run Selection"
501
- else:
502
- self.run_query_bar.run_button.label = "Run Query"
508
+ self._sync_run_button_text()
503
509
 
504
510
  @on(Input.Changed, "#limit_input")
505
511
  def update_limit_tooltip(self, message: Input.Changed) -> None:
@@ -522,12 +528,14 @@ class Harlequin(AppBase):
522
528
  and message.validation_result
523
529
  and message.validation_result.is_valid
524
530
  ):
525
- self.post_message(
526
- QuerySubmitted(
527
- queries=self._get_selected_queries(),
528
- limit=self.run_query_bar.limit_value,
531
+ queries = self._get_selected_queries()
532
+ if queries:
533
+ self.post_message(
534
+ QuerySubmitted(
535
+ queries=queries,
536
+ limit=self.run_query_bar.limit_value,
537
+ )
529
538
  )
530
- )
531
539
 
532
540
  @on(DataTable.SelectionCopied)
533
541
  def copy_data_to_clipboard(self, message: DataTable.SelectionCopied) -> None:
@@ -1024,6 +1032,21 @@ class Harlequin(AppBase):
1024
1032
  self.data_catalog.update_file_tree()
1025
1033
  self.data_catalog.update_s3_tree()
1026
1034
 
1035
+ def _sync_run_button_text(self) -> None:
1036
+ if self._validate_selection():
1037
+ self.run_query_bar.run_button.label = "Run Selection"
1038
+ else:
1039
+ self.run_query_bar.run_button.label = "Run Query"
1040
+
1041
+ def _sync_run_button_disabled(self) -> None:
1042
+ if self.editor is None or self.editor.text_input is None:
1043
+ return
1044
+
1045
+ if self.editor.text.strip():
1046
+ self.run_query_bar.run_button.disabled = False
1047
+ else:
1048
+ self.run_query_bar.run_button.disabled = True
1049
+
1027
1050
  @work(
1028
1051
  thread=True,
1029
1052
  exclusive=True,
@@ -7,6 +7,7 @@ $border-title-color-focus: $primary;
7
7
  /* ALL WIDGETS */
8
8
  DataCatalog,
9
9
  EditorCollection,
10
+ FooterContainer > Input,
10
11
  ResultsViewer,
11
12
  Lazy {
12
13
  border: round $border-color-nofocus;
@@ -20,6 +21,7 @@ LoadingIndicator {
20
21
 
21
22
  DataCatalog:focus-within,
22
23
  EditorCollection:focus-within,
24
+ FooterContainer > Input:focus,
23
25
  ResultsViewer:focus-within,
24
26
  HistoryScreen TextArea:focus,
25
27
  HistoryScreen OptionList:focus {
@@ -97,8 +99,7 @@ DataCatalog * .directory-tree--folder {
97
99
  }
98
100
 
99
101
  DataCatalog * .directory-tree--hidden {
100
- text-style: italic;
101
- color: $text-muted;
102
+ text-style: dim italic;
102
103
  }
103
104
 
104
105
  DataCatalog ContextMenu {
@@ -211,6 +212,9 @@ RunQueryBar Button#run_query {
211
212
  margin: 0 0 0 4;
212
213
  &:hover {
213
214
  background: $secondary;
215
+ &:disabled {
216
+ background: $primary;
217
+ }
214
218
  }
215
219
  &:focus {
216
220
  text-style: reverse;
@@ -140,7 +140,7 @@ class MemberCompleter(WordCompleter):
140
140
  match_context = context.strip("'`\"").lower()
141
141
  separators = SEPARATOR_PROG.findall(prefix)
142
142
  value_prefix = "".join(
143
- f"{w}{sep}" for w, sep in zip([*others, context], separators)
143
+ f"{w}{sep}" for w, sep in zip([*others, context], separators, strict=False)
144
144
  )
145
145
 
146
146
  context_completions = [
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ from importlib.metadata import entry_points, version
4
5
  from pathlib import Path
5
6
  from typing import Any, Sequence
6
7
 
@@ -24,11 +25,6 @@ from harlequin.options import AbstractOption
24
25
  from harlequin.plugins import load_adapter_plugins
25
26
  from harlequin.windows_timezone import check_and_install_tzdata
26
27
 
27
- if sys.version_info < (3, 10):
28
- from importlib_metadata import entry_points, version
29
- else:
30
- from importlib.metadata import entry_points, version
31
-
32
28
  # configure defaults
33
29
  DEFAULT_ADAPTER = "duckdb"
34
30
  DEFAULT_LIMIT = 100_000
@@ -106,7 +102,9 @@ def _version_option() -> str:
106
102
  adapter_eps = entry_points(group="harlequin.adapter")
107
103
  adapter_versions: dict[str, str] = {}
108
104
  for ep in adapter_eps:
109
- adapter_versions.update({ep.name: ep.dist.version})
105
+ adapter_versions.update(
106
+ {ep.name: ep.dist.version if ep.dist is not None else "unknown"}
107
+ )
110
108
 
111
109
  adapter_output = "\n".join(
112
110
  [f" - {name}, version {version}" for name, version in adapter_versions.items()]
@@ -2,10 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from typing import List, Union
4
4
 
5
- from rich.text import TextType
6
5
  from sqlfmt.api import Mode, format_string
7
6
  from sqlfmt.exception import SqlfmtError
8
- from textual.css.query import NoMatches
7
+ from textual.content import ContentType
8
+ from textual.css.query import InvalidQueryFormat, NoMatches
9
9
  from textual.message import Message
10
10
  from textual.reactive import reactive
11
11
  from textual.widgets import ContentSwitcher, TabbedContent, TabPane, Tabs
@@ -38,18 +38,23 @@ class CodeEditor(TextEditor, inherit_bindings=False):
38
38
  Returns the list of queries that intersect
39
39
  with the current selection.
40
40
  """
41
- if self.text_input is None:
41
+ if self.text_input is None or not self.text.strip():
42
42
  return []
43
43
 
44
- semicolons = self._query_semicolons()
44
+ if ";" not in self.text:
45
+ return [self.text]
45
46
 
46
- if not semicolons:
47
+ separators = self._query_separators()
48
+ if not separators:
49
+ # a semicolon could be in a string literal,
50
+ # so there may not be query separators even if
51
+ # there are literal semicolons in the text.
47
52
  return [self.text]
48
53
 
49
54
  queries: list[str] = []
50
55
  prev_query: str | None = None
51
56
  query_start = (0, 0)
52
- for query_end in [*semicolons, self.text_input.document.end]:
57
+ for query_end in [*separators, self.text_input.document.end]:
53
58
  if query_start > self.selection.end:
54
59
  break
55
60
  q = self.text_input.get_text_range(start=query_start, end=query_end).strip()
@@ -116,7 +121,11 @@ class CodeEditor(TextEditor, inherit_bindings=False):
116
121
  if hasattr(self.app, "action_focus_data_catalog"):
117
122
  self.app.action_focus_data_catalog()
118
123
 
119
- def _query_semicolons(self) -> list[tuple[int, int]]:
124
+ def _query_separators(self) -> list[tuple[int, int]]:
125
+ """
126
+ Return a list of tuples that represent the row and col
127
+ positions of query separators (semicolons) in the buffer text.
128
+ """
120
129
  if self.text_input is None:
121
130
  return []
122
131
 
@@ -124,6 +133,7 @@ class CodeEditor(TextEditor, inherit_bindings=False):
124
133
  assert self._semicolon_query is not None
125
134
  query_result = self.query_syntax_tree(query=self._semicolon_query)
126
135
  return [n.end_point for n in query_result.get("semicolon", [])]
136
+
127
137
  else:
128
138
  # tree-sitter is not installed. naively split on semicolons and
129
139
  # show a warning.
@@ -138,10 +148,12 @@ class CodeEditor(TextEditor, inherit_bindings=False):
138
148
  timeout=10,
139
149
  )
140
150
  self.has_shown_tree_sitter_error = True
151
+
141
152
  semicolons: list[tuple[int, int]] = []
142
153
  for i, line in enumerate(self.text.splitlines()):
143
154
  for pos in [m.span()[1] for m in re.finditer(";", line)]:
144
155
  semicolons.append((i, pos))
156
+
145
157
  return semicolons
146
158
 
147
159
 
@@ -156,7 +168,7 @@ class EditorCollection(TabbedContent):
156
168
 
157
169
  def __init__(
158
170
  self,
159
- *titles: TextType,
171
+ *titles: ContentType,
160
172
  initial: str = "",
161
173
  name: Union[str, None] = None,
162
174
  id: Union[str, None] = None, # noqa: A002
@@ -188,7 +200,7 @@ class EditorCollection(TabbedContent):
188
200
  try:
189
201
  tab_pane = content.query_one(f"#{active_tab_id}", TabPane)
190
202
  return tab_pane.query_one(CodeEditor)
191
- except NoMatches:
203
+ except (NoMatches, InvalidQueryFormat):
192
204
  pass
193
205
  all_editors = content.query(CodeEditor)
194
206
  return all_editors.first(CodeEditor)
@@ -310,7 +322,7 @@ class EditorCollection(TabbedContent):
310
322
  tabs = self.query(TabPane)
311
323
  next_tabs = tabs[1:]
312
324
  next_tabs.append(tabs[0])
313
- lookup = {t.id: nt.id for t, nt in zip(tabs, next_tabs)}
325
+ lookup = {t.id: nt.id for t, nt in zip(tabs, next_tabs, strict=False)}
314
326
  self.active = lookup[active] # type: ignore
315
327
  self.post_message(self.EditorSwitched(active_editor=None))
316
328
  self.current_editor.focus()
@@ -4,9 +4,9 @@ from pathlib import Path
4
4
  from typing import TYPE_CHECKING, ClassVar, Sequence
5
5
 
6
6
  from rich.markup import escape
7
- from rich.text import TextType
8
7
  from textual import on
9
- from textual.css.query import NoMatches
8
+ from textual.content import ContentType
9
+ from textual.css.query import InvalidQueryFormat, NoMatches
10
10
  from textual.message import Message
11
11
  from textual.widgets import (
12
12
  DirectoryTree,
@@ -108,7 +108,7 @@ class DataCatalog(TabbedContent, can_focus=True):
108
108
 
109
109
  def __init__(
110
110
  self,
111
- *titles: TextType,
111
+ *titles: ContentType,
112
112
  initial: str = "",
113
113
  name: str | None = None,
114
114
  id: str | None = None, # noqa: A002
@@ -150,7 +150,7 @@ class DataCatalog(TabbedContent, can_focus=True):
150
150
  error=Exception(
151
151
  "Could not load s3 catalog because boto3 is not available.\n\n"
152
152
  "Re-install harlequin with the s3 extra, like this:\n"
153
- "pip install harlequin[s3]"
153
+ "uv tool install harlequin[s3]"
154
154
  ),
155
155
  )
156
156
  )
@@ -166,7 +166,7 @@ class DataCatalog(TabbedContent, can_focus=True):
166
166
  def on_focus(self) -> None:
167
167
  try:
168
168
  active_widget = self.query_one(f"#{self.active}").children[0]
169
- except NoMatches:
169
+ except (NoMatches, InvalidQueryFormat):
170
170
  self.database_tree.focus()
171
171
  else:
172
172
  active_widget.focus()
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from asyncio import PriorityQueue
4
4
  from contextlib import suppress
5
- from pathlib import Path
6
5
  from typing import TYPE_CHECKING, Generator, Iterable, TypeVar
7
6
 
8
7
  from rich.style import Style
@@ -77,7 +76,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
77
76
  async def watch_catalog(self, catalog: Catalog | None) -> None:
78
77
  """Watch for changes to the `catalog` of the database tree.
79
78
 
80
- If the Catalog is changed the directory tree will be repopulated using
79
+ If the Catalog is changed the database tree will be repopulated using
81
80
  the new value as the root.
82
81
  """
83
82
  assert isinstance(self.root.data, CatalogItem)
@@ -91,18 +90,12 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
91
90
 
92
91
  def _add_to_load_queue(
93
92
  self, node: TreeNode[InteractiveCatalogItem], priority: int = 100
94
- ) -> AwaitComplete:
93
+ ) -> None:
95
94
  """Add the given node to the load priority queue.
96
95
 
97
- The return value can optionally be awaited until the queue is empty.
98
-
99
96
  Args:
100
97
  node: The node to add to the load queue.
101
98
  priority: An order for this node to be loaded; lowest first.
102
-
103
- Returns:
104
- An optionally awaitable object that can be awaited until the
105
- load queue has finished processing.
106
99
  """
107
100
  assert node.data is not None
108
101
  if not node.data.loaded:
@@ -111,8 +104,6 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
111
104
  # since TreeNodes do not implement cmp operators.
112
105
  self._load_queue.put_nowait((priority, str(node.label), id(node), node))
113
106
 
114
- return AwaitComplete(self._load_queue.join())
115
-
116
107
  def reload(self) -> AwaitComplete:
117
108
  """Reload the `DirectoryTree` contents.
118
109
 
@@ -322,7 +313,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
322
313
  # Get the next node that needs loading off the queue. Note that
323
314
  # this blocks if the queue is empty.
324
315
  *_, node = await self._load_queue.get()
325
- content: list[Path] = []
316
+ content: list[CatalogItem] = []
326
317
  async with self.lock:
327
318
  try:
328
319
  # Spin up a short-lived thread that will load the content of
@@ -354,7 +345,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
354
345
  return
355
346
  if isinstance(node.data, InteractiveCatalogItem) and not node.data.loaded:
356
347
  # if this node isn't loaded yet, add it to the front of the queue
357
- await self._add_to_load_queue(node, priority=0) # type: ignore[arg-type]
348
+ self._add_to_load_queue(node, priority=0) # type: ignore[arg-type]
358
349
  if (
359
350
  isinstance(node.data, CatalogItem)
360
351
  and node.data.children
@@ -293,7 +293,7 @@ class DebugInfoScreen(ModalScreen):
293
293
  self.set_focus(focusables[next_idx])
294
294
 
295
295
  def on_key(self, event: events.Key) -> None:
296
- if event.key in ("escape", "esc"):
296
+ if event.key == "escape":
297
297
  self.app.pop_screen()
298
298
  event.stop()
299
299
  elif event.key == "pageup":
@@ -13,7 +13,7 @@ class ClickableStatic(Static):
13
13
  def on_click(self, message: events.Click) -> None:
14
14
  message.stop()
15
15
  try:
16
- pyperclip.copy(str(self.renderable))
16
+ pyperclip.copy(str(self.content))
17
17
  except pyperclip.PyperclipException:
18
18
  pass
19
19
  else:
@@ -522,7 +522,7 @@ class HarlequinKeys(AppBase):
522
522
  modified_bindings = [
523
523
  new_binding
524
524
  for new_binding, old_binding in zip(
525
- self.bindings.values(), self.unmodifed_bindings.values()
525
+ self.bindings.values(), self.unmodifed_bindings.values(), strict=False
526
526
  )
527
527
  if new_binding != old_binding
528
528
  ]
@@ -1,17 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- import sys
3
+ from importlib.metadata import entry_points
4
4
  from typing import Literal, Sequence, overload
5
5
 
6
6
  from harlequin.adapter import HarlequinAdapter
7
7
  from harlequin.exception import HarlequinConfigError
8
8
  from harlequin.keymap import HarlequinKeyMap
9
9
 
10
- if sys.version_info < (3, 10):
11
- from importlib_metadata import entry_points
12
- else:
13
- from importlib.metadata import entry_points
14
-
15
10
 
16
11
  def load_adapter_plugins() -> dict[str, type[HarlequinAdapter]]:
17
12
  return _load_plugins(group="harlequin.adapter")
@@ -35,6 +35,7 @@ class DuckDbCursor(HarlequinCursor):
35
35
  zip(
36
36
  self.relation.columns,
37
37
  map(self.conn._short_column_type, self.relation.dtypes),
38
+ strict=False,
38
39
  )
39
40
  )
40
41
 
@@ -374,7 +374,7 @@ class HarlequinSqliteAdapter(HarlequinAdapter):
374
374
  if hasattr(conn, "autocommit"):
375
375
  conn.autocommit = True
376
376
 
377
- for uri, name in zip(other_dbs, db_names[1:]):
377
+ for uri, name in zip(other_dbs, db_names[1:], strict=False):
378
378
  try:
379
379
  conn.execute(f"attach database '{uri}' as {name}")
380
380
  except sqlite3.Error as e:
File without changes
File without changes
File without changes