sqlakit-lsp 0.1.1__tar.gz → 0.2.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.1.1 → sqlakit_lsp-0.2.0}/PKG-INFO +1 -1
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/pyproject.toml +2 -2
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/pyproject.toml.orig +2 -4
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/sqlakit_lsp/_server.py +138 -29
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/LICENSE +0 -0
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/README.md +0 -0
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/sqlakit_lsp/__init__.py +0 -0
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/sqlakit_lsp/__main__.py +0 -0
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/sqlakit_lsp/_cli.py +0 -0
- {sqlakit_lsp-0.1.1 → sqlakit_lsp-0.2.0}/sqlakit_lsp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit-lsp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "The language server for SQLAKit templates."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -131,7 +131,7 @@ dev = [
|
|
|
131
131
|
"pytest>=9.1.1",
|
|
132
132
|
"pytest-cov>=7.1.0",
|
|
133
133
|
"anyio>=4.14.2",
|
|
134
|
-
"ty>=0.0.
|
|
134
|
+
"ty>=0.0.84",
|
|
135
135
|
"ruff>=0.16.6",
|
|
136
136
|
"poethepoet>=0.48.0",
|
|
137
137
|
"codespell>=2.4.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sqlakit-lsp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "The language server for SQLAKit templates."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,8 +22,6 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
requires-python = ">=3.11"
|
|
24
24
|
dependencies = [
|
|
25
|
-
# The server reads the private modules of `sqlakit`, which a minor version
|
|
26
|
-
# may change.
|
|
27
25
|
"sqlakit>=0.21.0,<0.23",
|
|
28
26
|
"pygls>=2.1,<3",
|
|
29
27
|
]
|
|
@@ -51,7 +49,7 @@ dev = [
|
|
|
51
49
|
"pytest>=9.1.1",
|
|
52
50
|
"pytest-cov>=7.1.0",
|
|
53
51
|
"anyio>=4.14.2",
|
|
54
|
-
"ty>=0.0.
|
|
52
|
+
"ty>=0.0.84",
|
|
55
53
|
"ruff>=0.16.6",
|
|
56
54
|
"poethepoet>=0.48.0",
|
|
57
55
|
"codespell>=2.4.3",
|
|
@@ -23,16 +23,18 @@ import asyncio
|
|
|
23
23
|
import bisect
|
|
24
24
|
import difflib
|
|
25
25
|
import inspect
|
|
26
|
+
import os
|
|
26
27
|
import re
|
|
27
28
|
import tempfile
|
|
28
29
|
import uuid
|
|
29
30
|
from collections import OrderedDict
|
|
30
|
-
from dataclasses import dataclass, field
|
|
31
|
-
from functools import lru_cache
|
|
31
|
+
from dataclasses import dataclass, field, replace
|
|
32
|
+
from functools import cached_property, lru_cache
|
|
32
33
|
from importlib.metadata import version
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
from typing import TYPE_CHECKING, Any
|
|
35
36
|
|
|
37
|
+
import sqlakit
|
|
36
38
|
import sqlalchemy as sa
|
|
37
39
|
import sqlalchemy.engine.default
|
|
38
40
|
import sqlalchemy.exc
|
|
@@ -421,6 +423,34 @@ class _Assistant:
|
|
|
421
423
|
resolved = path.resolve()
|
|
422
424
|
return any(resolved in files for files in self._files)
|
|
423
425
|
|
|
426
|
+
def _source(self, macro: Macro) -> Target | None:
|
|
427
|
+
"""Return where a macro is written, in the project's own `sqlakit`.
|
|
428
|
+
|
|
429
|
+
A server that `uvx` runs imports `sqlakit` from a cache of its own. The
|
|
430
|
+
project holds the same version in `.venv`, which is the file to open.
|
|
431
|
+
"""
|
|
432
|
+
target = _source_of(macro)
|
|
433
|
+
if target is None or self._library is None:
|
|
434
|
+
return target
|
|
435
|
+
try:
|
|
436
|
+
inside = target.path.resolve().relative_to(_LIBRARY)
|
|
437
|
+
except ValueError:
|
|
438
|
+
return target
|
|
439
|
+
return replace(target, path=self._library / inside)
|
|
440
|
+
|
|
441
|
+
@cached_property
|
|
442
|
+
def _library(self) -> Path | None:
|
|
443
|
+
"""Return the project's `sqlakit` in `.venv`, when it is this version."""
|
|
444
|
+
installed = f"sqlakit-{version('sqlakit')}.dist-info"
|
|
445
|
+
for site in (
|
|
446
|
+
*self._root.glob(".venv/lib/python*/site-packages"),
|
|
447
|
+
self._root / ".venv/Lib/site-packages",
|
|
448
|
+
):
|
|
449
|
+
library = site / "sqlakit"
|
|
450
|
+
if (site / installed).is_dir() and library.resolve() != _LIBRARY:
|
|
451
|
+
return library
|
|
452
|
+
return None
|
|
453
|
+
|
|
424
454
|
def owns(self, path: Path) -> bool:
|
|
425
455
|
"""Whether a file is the project's own, where `walk` goes.
|
|
426
456
|
|
|
@@ -447,9 +477,27 @@ class _Assistant:
|
|
|
447
477
|
def names(self) -> list[str]:
|
|
448
478
|
"""Every template's name, read once until `forget_files`."""
|
|
449
479
|
if self._names is None:
|
|
450
|
-
self._names = self.
|
|
480
|
+
self._names = sorted(self._on_disk())
|
|
451
481
|
return self._names
|
|
452
482
|
|
|
483
|
+
def _on_disk(self) -> dict[str, Path]:
|
|
484
|
+
"""Return each template's name and file, as `Templates.names` finds them.
|
|
485
|
+
|
|
486
|
+
One walk of the directories, in strings: a file made has the next
|
|
487
|
+
keystroke read them again, and paths cost more than the walk.
|
|
488
|
+
"""
|
|
489
|
+
skipped = {str(path) for path in self._macro_files | self._macro_sql}
|
|
490
|
+
found: dict[str, Path] = {}
|
|
491
|
+
for root in self._template_roots:
|
|
492
|
+
start = len(str(root)) + 1
|
|
493
|
+
for directory, _, files in os.walk(root):
|
|
494
|
+
for file in files:
|
|
495
|
+
written = os.path.join(directory, file) # noqa: PTH118 - strings, for speed
|
|
496
|
+
if file.endswith(".sql") and written not in skipped:
|
|
497
|
+
name = written[start:].replace(os.sep, "/")
|
|
498
|
+
found.setdefault(name, Path(written))
|
|
499
|
+
return found
|
|
500
|
+
|
|
453
501
|
def defines_macros(self, path: Path) -> bool:
|
|
454
502
|
"""Whether a file defines a macro the project calls, its own or built in.
|
|
455
503
|
|
|
@@ -460,7 +508,7 @@ class _Assistant:
|
|
|
460
508
|
self._sources = {
|
|
461
509
|
target.path.resolve()
|
|
462
510
|
for macro in self.project.templates.macros.values()
|
|
463
|
-
if (target :=
|
|
511
|
+
if (target := self._source(macro)) is not None
|
|
464
512
|
}
|
|
465
513
|
return path.resolve() in self._sources
|
|
466
514
|
|
|
@@ -688,7 +736,7 @@ class _Assistant:
|
|
|
688
736
|
signature_of(macro, namespace),
|
|
689
737
|
)
|
|
690
738
|
for macro in self.project.templates.macros.values()
|
|
691
|
-
if (target :=
|
|
739
|
+
if (target := self._source(macro)) is not None
|
|
692
740
|
]
|
|
693
741
|
everything.extend(
|
|
694
742
|
(name, "template", Target(path, 0), "")
|
|
@@ -942,7 +990,7 @@ class _Assistant:
|
|
|
942
990
|
return None if path is None else Target(path, 0)
|
|
943
991
|
macro = self.project.templates.macros.get(self._macro_at(source, offset) or "")
|
|
944
992
|
if macro is not None:
|
|
945
|
-
return
|
|
993
|
+
return self._source(macro)
|
|
946
994
|
if path is None:
|
|
947
995
|
return None
|
|
948
996
|
# The SQL of `@sql_macro("file.sql")` goes to its function.
|
|
@@ -963,7 +1011,7 @@ class _Assistant:
|
|
|
963
1011
|
if macro is None or sql is None:
|
|
964
1012
|
return None
|
|
965
1013
|
if Path(sql).resolve() == path.resolve():
|
|
966
|
-
return
|
|
1014
|
+
return self._source(macro)
|
|
967
1015
|
statement = next(
|
|
968
1016
|
(one for one in sql_macros(Path(sql)) if one.name == macro.name), None
|
|
969
1017
|
)
|
|
@@ -1002,7 +1050,7 @@ class _Assistant:
|
|
|
1002
1050
|
return next(
|
|
1003
1051
|
(
|
|
1004
1052
|
(start, end)
|
|
1005
|
-
for start, end, _ in
|
|
1053
|
+
for start, end, _ in self._files_named(source, path)
|
|
1006
1054
|
if start <= offset <= end
|
|
1007
1055
|
),
|
|
1008
1056
|
defined,
|
|
@@ -1100,25 +1148,46 @@ class _Assistant:
|
|
|
1100
1148
|
def python_definition(
|
|
1101
1149
|
self, source: str, offset: int, path: Path | None = None
|
|
1102
1150
|
) -> Target | None:
|
|
1103
|
-
"""Return the
|
|
1151
|
+
"""Return the file the name under the offset reads.
|
|
1104
1152
|
|
|
1105
|
-
|
|
1153
|
+
That is a template in `db.sql("...")`, or the SQL of a macro in
|
|
1154
|
+
`@sql_macro("...")`. On the name of a `def` that defines a macro, it is
|
|
1155
|
+
the name itself.
|
|
1106
1156
|
"""
|
|
1107
|
-
for start, end,
|
|
1157
|
+
for start, end, found in self._files_named(source, path):
|
|
1108
1158
|
if start <= offset <= end:
|
|
1109
|
-
found
|
|
1110
|
-
return None if found is None else Target(found, 0)
|
|
1159
|
+
return Target(found, 0)
|
|
1111
1160
|
return None if path is None else self._itself(path, source, offset)
|
|
1112
1161
|
|
|
1162
|
+
def _files_named(
|
|
1163
|
+
self, source: str, path: Path | None
|
|
1164
|
+
) -> list[tuple[int, int, Path]]:
|
|
1165
|
+
"""Return each file the Python names, where the name is, and the file.
|
|
1166
|
+
|
|
1167
|
+
A template is looked for under the template directories, and the SQL of
|
|
1168
|
+
`@sql_macro("...")` next to the module, as `sql_macro` reads it.
|
|
1169
|
+
"""
|
|
1170
|
+
found = [
|
|
1171
|
+
(start, end, target)
|
|
1172
|
+
for start, end, name in _template_names(source)
|
|
1173
|
+
if (target := self.project.path_of(name)) is not None
|
|
1174
|
+
]
|
|
1175
|
+
if path is not None:
|
|
1176
|
+
found.extend(
|
|
1177
|
+
(start, end, target)
|
|
1178
|
+
for start, end, name in _sql_macro_files(source)
|
|
1179
|
+
if (target := path.parent / name).is_file()
|
|
1180
|
+
)
|
|
1181
|
+
return found
|
|
1182
|
+
|
|
1113
1183
|
def links(self, path: Path, source: str) -> list[tuple[int, int, Path]]:
|
|
1114
|
-
"""Return each
|
|
1115
|
-
if
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
]
|
|
1184
|
+
"""Return each file the text names, where the name is, and the file."""
|
|
1185
|
+
if path.suffix == ".py":
|
|
1186
|
+
return self._files_named(source, path)
|
|
1187
|
+
named = [
|
|
1188
|
+
(found.start(1), found.end(1), found.group(1))
|
|
1189
|
+
for found in self._include_path.finditer(source)
|
|
1190
|
+
]
|
|
1122
1191
|
return [
|
|
1123
1192
|
(start, end, target)
|
|
1124
1193
|
for start, end, name in named
|
|
@@ -1271,7 +1340,7 @@ class _Assistant:
|
|
|
1271
1340
|
def declaration(self, name: str) -> Reference | None:
|
|
1272
1341
|
"""Return where a macro's name is written, for a list of its references."""
|
|
1273
1342
|
macro = self.project.templates.macros.get(name)
|
|
1274
|
-
if macro is None or (target :=
|
|
1343
|
+
if macro is None or (target := self._source(macro)) is None:
|
|
1275
1344
|
return None
|
|
1276
1345
|
text = _read(target.path)
|
|
1277
1346
|
start = _line_span(text, target.line + 1)[0] + target.column
|
|
@@ -1300,7 +1369,7 @@ class _Assistant:
|
|
|
1300
1369
|
sql = getattr(macro, "sql_path", None)
|
|
1301
1370
|
if sql is not None and Path(sql).resolve() == path.resolve():
|
|
1302
1371
|
return word
|
|
1303
|
-
target =
|
|
1372
|
+
target = self._source(macro)
|
|
1304
1373
|
if target is None or target.path.resolve() != path.resolve():
|
|
1305
1374
|
return None
|
|
1306
1375
|
return word if target.line + 1 == line else None
|
|
@@ -1368,12 +1437,9 @@ class _Assistant:
|
|
|
1368
1437
|
The paths are resolved, as the editor's open files are keyed.
|
|
1369
1438
|
"""
|
|
1370
1439
|
if self._files is None:
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
if (root / name).is_file():
|
|
1375
|
-
named[root / name] = name
|
|
1376
|
-
break
|
|
1440
|
+
templates = self._on_disk()
|
|
1441
|
+
self._names = sorted(templates)
|
|
1442
|
+
named = {path: name for name, path in templates.items()}
|
|
1377
1443
|
sql_files = [
|
|
1378
1444
|
*named,
|
|
1379
1445
|
*(path.resolve() for path in self.project.macro_files()),
|
|
@@ -1615,6 +1681,46 @@ def _template_names(source: str) -> list[tuple[int, int, str]]:
|
|
|
1615
1681
|
return found
|
|
1616
1682
|
|
|
1617
1683
|
|
|
1684
|
+
def _sql_macro_files(source: str) -> list[tuple[int, int, str]]:
|
|
1685
|
+
"""Return where `@sql_macro("file.sql")` names its SQL, the quotes left out."""
|
|
1686
|
+
if "sql_macro" not in source:
|
|
1687
|
+
return []
|
|
1688
|
+
try:
|
|
1689
|
+
tree = ast.parse(source)
|
|
1690
|
+
except SyntaxError:
|
|
1691
|
+
return []
|
|
1692
|
+
starts = _line_starts(source)
|
|
1693
|
+
found = []
|
|
1694
|
+
for node in ast.walk(tree):
|
|
1695
|
+
if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
1696
|
+
continue
|
|
1697
|
+
for decorator in node.decorator_list:
|
|
1698
|
+
if not (isinstance(decorator, ast.Call) and decorator.args):
|
|
1699
|
+
continue
|
|
1700
|
+
func = decorator.func
|
|
1701
|
+
called = (
|
|
1702
|
+
func.attr
|
|
1703
|
+
if isinstance(func, ast.Attribute)
|
|
1704
|
+
else getattr(func, "id", "")
|
|
1705
|
+
)
|
|
1706
|
+
name = decorator.args[0]
|
|
1707
|
+
if not (
|
|
1708
|
+
called == "sql_macro"
|
|
1709
|
+
and isinstance(name, ast.Constant)
|
|
1710
|
+
and isinstance(name.value, str)
|
|
1711
|
+
and name.value.endswith(".sql")
|
|
1712
|
+
and name.end_lineno is not None
|
|
1713
|
+
and name.end_col_offset is not None
|
|
1714
|
+
):
|
|
1715
|
+
continue
|
|
1716
|
+
start = _char_offset(source, starts, name.lineno, name.col_offset)
|
|
1717
|
+
end = _char_offset(source, starts, name.end_lineno, name.end_col_offset)
|
|
1718
|
+
quote = source.find(name.value, start, end)
|
|
1719
|
+
if quote >= 0:
|
|
1720
|
+
found.append((quote, quote + len(name.value), name.value))
|
|
1721
|
+
return found
|
|
1722
|
+
|
|
1723
|
+
|
|
1618
1724
|
def _char_offset(source: str, starts: list[int], line: int, column: int) -> int:
|
|
1619
1725
|
"""Return the offset of a position `ast` gives, its column in UTF-8 bytes."""
|
|
1620
1726
|
begin = starts[line - 1]
|
|
@@ -1673,6 +1779,9 @@ def _source_of(macro: Macro) -> Target | None:
|
|
|
1673
1779
|
|
|
1674
1780
|
_DEF = re.compile(r"\s*(?:async\s+)?def\s+(\w+)")
|
|
1675
1781
|
|
|
1782
|
+
_LIBRARY = Path(sqlakit.__file__).resolve().parent
|
|
1783
|
+
"""The `sqlakit` this server imports."""
|
|
1784
|
+
|
|
1676
1785
|
|
|
1677
1786
|
@dataclass(frozen=True, slots=True)
|
|
1678
1787
|
class _At:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|