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.
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/PKG-INFO +2 -2
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/pyproject.toml +2 -2
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/pyproject.toml.orig +2 -2
- sqlakit_lsp-0.4.0/sqlakit_lsp/_live.py +45 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/sqlakit_lsp/_server.py +14 -80
- sqlakit_lsp-0.3.2/sqlakit_lsp/_live.py +0 -92
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/LICENSE +0 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/README.md +0 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/sqlakit_lsp/__init__.py +0 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/sqlakit_lsp/__main__.py +0 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/sqlakit_lsp/_cli.py +0 -0
- {sqlakit_lsp-0.3.2 → sqlakit_lsp-0.4.0}/sqlakit_lsp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlakit-lsp
|
|
3
|
-
Version: 0.
|
|
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
|
+
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
|
+
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.
|
|
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
|
+
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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(),
|
|
899
|
-
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|