sqlakit-lsp 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlakit-lsp
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: The language server for SQLAKit templates.
5
5
  Keywords: sqlalchemy,sqlakit,lsp,language-server,sql
6
6
  Author: Anton Ruhlov
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit-lsp"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "The language server for SQLAKit templates."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlakit-lsp"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "The language server for SQLAKit templates."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -23,6 +23,7 @@ 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
@@ -476,9 +477,27 @@ class _Assistant:
476
477
  def names(self) -> list[str]:
477
478
  """Every template's name, read once until `forget_files`."""
478
479
  if self._names is None:
479
- self._names = self.project.templates.names()
480
+ self._names = sorted(self._on_disk())
480
481
  return self._names
481
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
+
482
501
  def defines_macros(self, path: Path) -> bool:
483
502
  """Whether a file defines a macro the project calls, its own or built in.
484
503
 
@@ -1031,7 +1050,7 @@ class _Assistant:
1031
1050
  return next(
1032
1051
  (
1033
1052
  (start, end)
1034
- for start, end, _ in _template_names(source)
1053
+ for start, end, _ in self._files_named(source, path)
1035
1054
  if start <= offset <= end
1036
1055
  ),
1037
1056
  defined,
@@ -1129,25 +1148,46 @@ class _Assistant:
1129
1148
  def python_definition(
1130
1149
  self, source: str, offset: int, path: Path | None = None
1131
1150
  ) -> Target | None:
1132
- """Return the template the name under the offset reads.
1151
+ """Return the file the name under the offset reads.
1133
1152
 
1134
- On the name of a `def` that defines a macro, that is the name itself.
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.
1135
1156
  """
1136
- for start, end, name in _template_names(source):
1157
+ for start, end, found in self._files_named(source, path):
1137
1158
  if start <= offset <= end:
1138
- found = self.project.path_of(name)
1139
- return None if found is None else Target(found, 0)
1159
+ return Target(found, 0)
1140
1160
  return None if path is None else self._itself(path, source, offset)
1141
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
+
1142
1183
  def links(self, path: Path, source: str) -> list[tuple[int, int, Path]]:
1143
- """Return each template the text names, where the name is, and its file."""
1144
- if self.reads_python(path):
1145
- named = _template_names(source)
1146
- else:
1147
- named = [
1148
- (found.start(1), found.end(1), found.group(1))
1149
- for found in self._include_path.finditer(source)
1150
- ]
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
+ ]
1151
1191
  return [
1152
1192
  (start, end, target)
1153
1193
  for start, end, name in named
@@ -1397,12 +1437,9 @@ class _Assistant:
1397
1437
  The paths are resolved, as the editor's open files are keyed.
1398
1438
  """
1399
1439
  if self._files is None:
1400
- named = {}
1401
- for name in self.names():
1402
- for root in self._template_roots:
1403
- if (root / name).is_file():
1404
- named[root / name] = name
1405
- break
1440
+ templates = self._on_disk()
1441
+ self._names = sorted(templates)
1442
+ named = {path: name for name, path in templates.items()}
1406
1443
  sql_files = [
1407
1444
  *named,
1408
1445
  *(path.resolve() for path in self.project.macro_files()),
@@ -1644,6 +1681,46 @@ def _template_names(source: str) -> list[tuple[int, int, str]]:
1644
1681
  return found
1645
1682
 
1646
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
+
1647
1724
  def _char_offset(source: str, starts: list[int], line: int, column: int) -> int:
1648
1725
  """Return the offset of a position `ast` gives, its column in UTF-8 bytes."""
1649
1726
  begin = starts[line - 1]
File without changes
File without changes