sqlakit-lsp 0.3.2__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit-lsp
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: The language server for SQLAKit templates.
5
5
  Keywords: sqlalchemy,sqlakit,lsp,language-server,sql
6
6
  Author: Anton Ruhlov
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: Database
19
19
  Classifier: Topic :: Text Editors :: Integrated Development Environments (IDE)
20
20
  Classifier: Typing :: Typed
21
- Requires-Dist: sqlakit>=0.21.0,<0.23
21
+ Requires-Dist: sqlakit>=0.22.4,<0.23
22
22
  Requires-Dist: pygls>=2.1,<3
23
23
  Requires-Python: >=3.11
24
24
  Project-URL: Repository, https://github.com/sqlakit/sqlakit-lsp
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit-lsp"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "The language server for SQLAKit templates."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  ]
28
28
  requires-python = ">=3.11"
29
29
  dependencies = [
30
- "sqlakit>=0.21.0,<0.23",
30
+ "sqlakit>=0.22.4,<0.23",
31
31
  "pygls>=2.1,<3",
32
32
  ]
33
33
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit-lsp"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "The language server for SQLAKit templates."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,7 +22,7 @@ classifiers = [
22
22
  ]
23
23
  requires-python = ">=3.11"
24
24
  dependencies = [
25
- "sqlakit>=0.21.0,<0.23",
25
+ "sqlakit>=0.22.4,<0.23",
26
26
  "pygls>=2.1,<3",
27
27
  ]
28
28
 
@@ -0,0 +1,45 @@
1
+ """Render a template with the project's own macros, in the project's Python.
2
+
3
+ The server reads the project's code and never runs it, so a macro written in
4
+ Python stays a call when it renders a template. **Show rendered SQL** asks for
5
+ more: the server runs this file with the project's interpreter, which imports
6
+ the modules of those macros, and prints the SQL as JSON.
7
+
8
+ It reads its request as JSON on stdin, and imports nothing of the server: the
9
+ project's environment has `sqlakit`, and not the server.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from sqlakit._project import load_project
20
+ from sqlakit._render import Example, _preparer, import_macros
21
+ from sqlakit._sql import _rendered, calls_kept
22
+
23
+
24
+ def main() -> None:
25
+ request = json.load(sys.stdin)
26
+ project = load_project(Path(request["root"]))
27
+ try:
28
+ import_macros(project)
29
+ except Exception as error: # noqa: BLE001 - the project's code raises anything
30
+ problem = (
31
+ f"the project's macros cannot be imported: {type(error).__name__}: {error}"
32
+ )
33
+ sys.stdout.write(json.dumps({"error": problem}) + "\n")
34
+ return
35
+ template = project.load(request["name"], request["source"])
36
+ values: dict[str, Any] = dict.fromkeys(template.parameters(), Example())
37
+ values.update(dict.fromkeys(request["not_given"]))
38
+ dialect = request["dialect"]
39
+ with calls_kept():
40
+ sql, _ = _rendered(template, {"dialect": dialect, **values}, _preparer(dialect))
41
+ sys.stdout.write(json.dumps({"sql": sql.strip()}) + "\n")
42
+
43
+
44
+ if __name__ == "__main__":
45
+ main()
@@ -37,9 +37,6 @@ from pathlib import Path
37
37
  from typing import TYPE_CHECKING, Any
38
38
 
39
39
  import sqlakit
40
- import sqlalchemy as sa
41
- import sqlalchemy.engine.default
42
- import sqlalchemy.exc
43
40
  from lsprotocol import types
44
41
  from pygls.exceptions import JsonRpcException
45
42
  from pygls.lsp.server import LanguageServer
@@ -47,11 +44,12 @@ from pygls.protocol import LanguageServerProtocol
47
44
  from pygls.protocol.language_server import lsp_method
48
45
  from pygls.uris import to_fs_path
49
46
  from sqlakit._project import Project, load_project
47
+ from sqlakit._render import Example, _calls, _refused
48
+ from sqlakit._render import _preparer as _dialect_preparer
50
49
  from sqlakit._sql import (
51
50
  _LITERAL,
52
51
  INCLUDE,
53
52
  NAMESPACE,
54
- Context,
55
53
  Macro,
56
54
  MacroTemplate,
57
55
  Param,
@@ -60,6 +58,7 @@ from sqlakit._sql import (
60
58
  signature_of,
61
59
  sql_macros,
62
60
  )
61
+ from sqlakit._sql import _rendered as _bound
63
62
  from sqlakit._static import SKIPPED, StaticMacro, walk
64
63
  from sqlakit.exceptions import (
65
64
  MacroArgumentError,
@@ -79,29 +78,6 @@ _PARAMETERS = re.compile(r"(?<![:\w\\]):([A-Za-z_]\w*)(?:\.\w+)*")
79
78
  name, and the match all of it."""
80
79
 
81
80
 
82
- class _Example:
83
- """A value to render a template with: any path reads another of it.
84
-
85
- `:criteria.teams` and `:filters["kind"]` both read one, so a template
86
- renders whatever shape its values have. It writes `x`, and holds one value.
87
- """
88
-
89
- def __getattr__(self, _: str) -> _Example:
90
- return self
91
-
92
- def __getitem__(self, _: object) -> _Example:
93
- return self
94
-
95
- def __iter__(self) -> Iterator[_Example]:
96
- return iter((self,))
97
-
98
- def __len__(self) -> int:
99
- return 1
100
-
101
- def __str__(self) -> str:
102
- return "x"
103
-
104
-
105
81
  _PARAMETER_TYPED = re.compile(r"(?<![:\w\\]):\w*$")
106
82
 
107
83
 
@@ -209,38 +185,24 @@ def _run_live(root: Path, request: dict[str, Any]) -> tuple[str | None, str]:
209
185
  return None, str(answer.get("error", "no SQL came back"))
210
186
 
211
187
 
212
- def _calls(parts: Sequence[Any]) -> Iterator[Any]:
213
- """Yield every macro call of a read template, the ones in arguments too."""
214
- for part in parts:
215
- if isinstance(part, str):
216
- continue
217
- yield part
218
- for argument in part.args:
219
- yield from _calls(argument.parts)
220
-
221
-
222
188
  def _rendered(template: Any, dialect: str, values: dict[str, Any]) -> str | None: # noqa: ANN401
223
- """Return the SQL a read template writes for these values, if it can.
189
+ """Return the SQL a read template writes for these values, as `sqlakit` binds it.
224
190
 
225
191
  A call that cannot be made with them, such as one of a macro of the
226
192
  project's own Python, which is read and not run, stays a call.
227
193
  """
228
194
  try:
229
195
  with calls_kept():
230
- return template.render(Context(dialect, _preparer(dialect), values)).strip()
196
+ sql, _ = _bound(
197
+ template, {"dialect": dialect, **values}, _preparer(dialect)
198
+ )
231
199
  except Exception: # noqa: BLE001 - a macro may want a value of its own kind
232
200
  return None
201
+ return sql.strip()
233
202
 
234
203
 
235
- @lru_cache
236
- def _preparer(dialect: str) -> Any: # noqa: ANN401
237
- """Return how a dialect quotes names, loaded without a driver or a server."""
238
- try:
239
- return sa.engine.make_url(f"{dialect}://").get_dialect()().identifier_preparer
240
- except sa.exc.NoSuchModuleError:
241
- default = sa.engine.default.DefaultDialect()
242
- default.name = dialect
243
- return default.identifier_preparer
204
+ _preparer = lru_cache(_dialect_preparer)
205
+ """How a dialect quotes names, made once for each: a hover renders often."""
244
206
 
245
207
 
246
208
  def _open_brackets(source: str, start: int, end: int) -> list[tuple[int, int]]:
@@ -895,8 +857,9 @@ class _Assistant:
895
857
  except SQLAKitError as error:
896
858
  return f"-- {name} cannot be read: {error}\n"
897
859
  calls = list(_calls(template.parts))
898
- values: dict[str, Any] = dict.fromkeys(template.parameters(), _Example())
899
- values.update(dict.fromkeys(self._not_given(name, source, calls, dialect)))
860
+ values: dict[str, Any] = dict.fromkeys(template.parameters(), Example())
861
+ refused = _refused(self.project, name, source, calls, _preparer(dialect))
862
+ values.update(dict.fromkeys(refused))
900
863
  sql = _rendered(template, dialect, values)
901
864
  if sql is None:
902
865
  return f"-- {name} cannot be written with made-up values.\n"
@@ -919,9 +882,6 @@ class _Assistant:
919
882
  "name": name,
920
883
  "source": source,
921
884
  "dialect": dialect,
922
- # Every one, as a macro may call another inside, as a file macro's SQL does.
923
- "modules": sorted({str(macro.path) for macro in static.values()}),
924
- "macros": sorted(static),
925
885
  "not_given": sorted(key for key, value in values.items() if value is None),
926
886
  }
927
887
  run, problem = _run_live(self.project.root, request)
@@ -953,32 +913,6 @@ class _Assistant:
953
913
  listed = ", ".join(f":{name}" for name in names)
954
914
  return f"{first}\n-- ? in order: {listed}\n{rest}"
955
915
 
956
- def _not_given(
957
- self, name: str, source: str, calls: Sequence[Any], dialect: str
958
- ) -> set[str]:
959
- """Return the parameters to render as not given.
960
-
961
- They are those of a call of a built-in macro that cannot be made with a
962
- made-up value, and can with none: `:sort` of `tpl.order_by`, whose value
963
- must be a column it lists.
964
- """
965
- found: set[str] = set()
966
- for call in calls:
967
- if isinstance(self.project.templates.macros.get(call.name), StaticMacro):
968
- continue
969
- text = source[call.span[0] : call.span[1]]
970
- try:
971
- alone = self.compiled(name, text)
972
- except SQLAKitError:
973
- continue
974
- names = alone.parameters()
975
- given = _rendered(alone, dialect, dict.fromkeys(names, _Example()))
976
- if names and self._kept(given, call.name):
977
- missing = _rendered(alone, dialect, dict.fromkeys(names))
978
- if not self._kept(missing, call.name):
979
- found.update(names)
980
- return found
981
-
982
916
  def _kept(self, sql: str | None, macro: str) -> bool:
983
917
  """Whether a call rendered alone could not be made, and wrote itself.
984
918
 
@@ -1069,7 +1003,7 @@ class _Assistant:
1069
1003
  return ""
1070
1004
  names = sorted(alone.parameters())
1071
1005
  dialect = self.project.dialect or "postgresql"
1072
- given = _rendered(alone, dialect, dict.fromkeys(names, _Example()))
1006
+ given = _rendered(alone, dialect, dict.fromkeys(names, Example()))
1073
1007
  missing = _rendered(alone, dialect, dict.fromkeys(names))
1074
1008
  # A call that cannot be made writes itself, which says nothing new.
1075
1009
  given, missing = (
@@ -1,92 +0,0 @@
1
- """Render a template with the project's own macros, in the project's Python.
2
-
3
- The server reads the project's code and never runs it, so a macro written in
4
- Python stays a call when it renders a template. **Show rendered SQL** asks for
5
- more: the server runs this file with the project's interpreter, which imports
6
- the modules of those macros, and prints the SQL as JSON.
7
-
8
- It reads its request as JSON on stdin, and imports nothing of the server: the
9
- project's environment has `sqlakit`, and not the server.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import json
15
- import sys
16
- from pathlib import Path
17
- from typing import Any
18
-
19
-
20
- class _Example:
21
- """A value to render a template with: any path reads another of it."""
22
-
23
- def __getattr__(self, _: str) -> _Example:
24
- return self
25
-
26
- def __getitem__(self, _: object) -> _Example:
27
- return self
28
-
29
- def __iter__(self) -> Any: # noqa: ANN401
30
- return iter((self,))
31
-
32
- def __len__(self) -> int:
33
- return 1
34
-
35
- def __str__(self) -> str:
36
- return "x"
37
-
38
-
39
- def _module(path: Path) -> tuple[Path, str]:
40
- """Return the directory a module is imported from, and its dotted name."""
41
- parts = [path.stem]
42
- folder = path.parent
43
- while (folder / "__init__.py").is_file():
44
- parts.append(folder.name)
45
- folder = folder.parent
46
- return folder, ".".join(reversed(parts))
47
-
48
-
49
- def _preparer(dialect: str) -> Any: # noqa: ANN401
50
- import sqlalchemy as sa # noqa: PLC0415
51
- import sqlalchemy.engine.default # noqa: PLC0415
52
- import sqlalchemy.exc # noqa: PLC0415
53
-
54
- try:
55
- return sa.engine.make_url(f"{dialect}://").get_dialect()().identifier_preparer
56
- except sa.exc.NoSuchModuleError:
57
- default = sa.engine.default.DefaultDialect()
58
- default.name = dialect
59
- return default.identifier_preparer
60
-
61
-
62
- def main() -> None:
63
- request = json.load(sys.stdin)
64
- from sqlakit._project import load_project # noqa: PLC0415
65
- from sqlakit._sql import Context, calls_kept, registered # noqa: PLC0415
66
-
67
- project = load_project(Path(request["root"]))
68
- for file in request["modules"]:
69
- folder, name = _module(Path(file))
70
- if str(folder) not in sys.path:
71
- sys.path.insert(0, str(folder))
72
- try:
73
- found = registered([name])
74
- except Exception as error: # noqa: BLE001 - the project's code raises anything
75
- sys.stdout.write(
76
- json.dumps({"error": f"{name} cannot be imported: {error}"}) + "\n"
77
- )
78
- return
79
- project.templates.macros.update(
80
- (key, macro) for key, macro in found.items() if key in request["macros"]
81
- )
82
- template = project.load(request["name"], request["source"])
83
- values: dict[str, Any] = dict.fromkeys(template.parameters(), _Example())
84
- values.update(dict.fromkeys(request["not_given"]))
85
- dialect = request["dialect"]
86
- with calls_kept():
87
- sql = template.render(Context(dialect, _preparer(dialect), values)).strip()
88
- sys.stdout.write(json.dumps({"sql": sql}) + "\n")
89
-
90
-
91
- if __name__ == "__main__":
92
- main()
File without changes
File without changes