python-delphi-lsp 3.6.0__cp310-abi3-win_amd64.whl
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.
- delphi_lsp/__init__.py +184 -0
- delphi_lsp/_version.py +1 -0
- delphi_lsp/agent_cache.py +1574 -0
- delphi_lsp/agent_cli.py +902 -0
- delphi_lsp/agent_context.py +3907 -0
- delphi_lsp/agent_cpg.py +349 -0
- delphi_lsp/agent_cpg_builder.py +666 -0
- delphi_lsp/agent_layers.py +1410 -0
- delphi_lsp/agent_metrics.py +526 -0
- delphi_lsp/agent_protocol.py +445 -0
- delphi_lsp/agent_relations.py +1881 -0
- delphi_lsp/agent_templates.py +614 -0
- delphi_lsp/agent_wiki.py +2853 -0
- delphi_lsp/agent_workspace.py +860 -0
- delphi_lsp/ast_serialize.py +126 -0
- delphi_lsp/binary.py +250 -0
- delphi_lsp/comment_builder.py +28 -0
- delphi_lsp/consts.py +345 -0
- delphi_lsp/delphi_lsp_native.pyd +0 -0
- delphi_lsp/delphiast_lexer.py +399 -0
- delphi_lsp/delphiast_parser.py +2189 -0
- delphi_lsp/delphiast_tokens.py +41 -0
- delphi_lsp/grammar.py +459 -0
- delphi_lsp/incremental.py +1139 -0
- delphi_lsp/lark_builder.py +2684 -0
- delphi_lsp/lark_tokens.py +236 -0
- delphi_lsp/lexical_scanner.py +30 -0
- delphi_lsp/lsp_server.py +6698 -0
- delphi_lsp/metrics.py +1452 -0
- delphi_lsp/native_bridge.py +300 -0
- delphi_lsp/navigation_cache.py +189 -0
- delphi_lsp/nodes.py +462 -0
- delphi_lsp/parallel_outline.py +662 -0
- delphi_lsp/parse_artifacts.py +969 -0
- delphi_lsp/parser.py +342 -0
- delphi_lsp/parser_backend.py +139 -0
- delphi_lsp/preprocessor.py +1122 -0
- delphi_lsp/progress.py +26 -0
- delphi_lsp/project_config.py +285 -0
- delphi_lsp/project_discovery.py +932 -0
- delphi_lsp/project_indexer.py +470 -0
- delphi_lsp/py.typed +1 -0
- delphi_lsp/response_cache.py +465 -0
- delphi_lsp/semantic.py +394 -0
- delphi_lsp/semantic_builder.py +2302 -0
- delphi_lsp/source_reader.py +17 -0
- delphi_lsp/wiki_external_sort.py +392 -0
- delphi_lsp/wiki_memory.py +548 -0
- delphi_lsp/workspace.py +83 -0
- delphi_lsp/writer.py +73 -0
- python_delphi_lsp-3.6.0.dist-info/METADATA +766 -0
- python_delphi_lsp-3.6.0.dist-info/RECORD +56 -0
- python_delphi_lsp-3.6.0.dist-info/WHEEL +4 -0
- python_delphi_lsp-3.6.0.dist-info/entry_points.txt +3 -0
- python_delphi_lsp-3.6.0.dist-info/licenses/LICENSE +373 -0
- python_delphi_lsp-3.6.0.dist-info/sboms/python-delphi-lsp.cyclonedx.json +770 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Reconstruct SyntaxNode trees from the Rust parser's serialized output.
|
|
2
|
+
|
|
3
|
+
The Rust parser (``parse_to_serialized``) produces a deterministic text
|
|
4
|
+
representation identical to Python's ``serialize_syntax_tree``. This module
|
|
5
|
+
provides the inverse: parsing that text back into real
|
|
6
|
+
``delphi_lsp.nodes.SyntaxNode`` objects so the rest of the LSP can operate on
|
|
7
|
+
native AST nodes instead of strings.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
from .consts import ATTRIBUTE_NAME_STRINGS, AttributeName, SyntaxNodeType
|
|
15
|
+
from .nodes import CommentNode, SyntaxNode, ValuedSyntaxNode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _unescape_value(value: str) -> str:
|
|
19
|
+
"""Unescape a serialized node value (reverse of ``_escape_value``)."""
|
|
20
|
+
result: list[str] = []
|
|
21
|
+
i = 0
|
|
22
|
+
while i < len(value):
|
|
23
|
+
if value[i] == "\\" and i + 1 < len(value):
|
|
24
|
+
nxt = value[i + 1]
|
|
25
|
+
if nxt == "\\":
|
|
26
|
+
result.append("\\")
|
|
27
|
+
i += 2
|
|
28
|
+
elif nxt == "n":
|
|
29
|
+
result.append("\n")
|
|
30
|
+
i += 2
|
|
31
|
+
elif nxt == "r":
|
|
32
|
+
result.append("\r")
|
|
33
|
+
i += 2
|
|
34
|
+
elif nxt == "t":
|
|
35
|
+
result.append("\t")
|
|
36
|
+
i += 2
|
|
37
|
+
else:
|
|
38
|
+
result.append(value[i])
|
|
39
|
+
i += 1
|
|
40
|
+
else:
|
|
41
|
+
result.append(value[i])
|
|
42
|
+
i += 1
|
|
43
|
+
return "".join(result)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _syntax_node_type_from_name(name: str) -> SyntaxNodeType:
|
|
47
|
+
"""Map a name string like ``'ntUnit'`` back to the ``SyntaxNodeType`` enum."""
|
|
48
|
+
try:
|
|
49
|
+
return SyntaxNodeType[name]
|
|
50
|
+
except KeyError:
|
|
51
|
+
if name.startswith("ntUnknown(") and name.endswith(")"):
|
|
52
|
+
try:
|
|
53
|
+
val = int(name[len("ntUnknown("):-1])
|
|
54
|
+
return SyntaxNodeType(val)
|
|
55
|
+
except (ValueError, OverflowError):
|
|
56
|
+
return SyntaxNodeType.ntUnknown
|
|
57
|
+
return SyntaxNodeType.ntUnknown
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_kv_pairs(token: str) -> list[tuple[str, str]]:
|
|
61
|
+
"""Parse ``key=value`` pairs from a token string.
|
|
62
|
+
|
|
63
|
+
Values may contain spaces but are delimited by the next whitespace
|
|
64
|
+
followed by a ``key=`` pattern.
|
|
65
|
+
"""
|
|
66
|
+
pairs: list[tuple[str, str]] = []
|
|
67
|
+
i = 0
|
|
68
|
+
length = len(token)
|
|
69
|
+
while i < length:
|
|
70
|
+
while i < length and token[i].isspace():
|
|
71
|
+
i += 1
|
|
72
|
+
if i >= length:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
eq_pos = token.find("=", i)
|
|
76
|
+
if eq_pos == -1:
|
|
77
|
+
break
|
|
78
|
+
key = token[i:eq_pos]
|
|
79
|
+
i = eq_pos + 1
|
|
80
|
+
|
|
81
|
+
while i < length and token[i].isspace():
|
|
82
|
+
i += 1
|
|
83
|
+
|
|
84
|
+
if i >= length:
|
|
85
|
+
pairs.append((key, ""))
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
value_start = i
|
|
89
|
+
while i < length:
|
|
90
|
+
if token[i].isspace():
|
|
91
|
+
j = i
|
|
92
|
+
while j < length and token[j].isspace():
|
|
93
|
+
j += 1
|
|
94
|
+
if j < length and token[j].isalpha():
|
|
95
|
+
k = j
|
|
96
|
+
while k < length and (token[k].isalnum() or token[k] == "_"):
|
|
97
|
+
k += 1
|
|
98
|
+
if k < length and token[k] == "=":
|
|
99
|
+
break
|
|
100
|
+
i += 1
|
|
101
|
+
else:
|
|
102
|
+
i += 1
|
|
103
|
+
|
|
104
|
+
value = token[value_start:i]
|
|
105
|
+
pairs.append((key, value))
|
|
106
|
+
|
|
107
|
+
return pairs
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_node_line(line: str) -> tuple[str, list[tuple[str, str]]]:
|
|
111
|
+
"""Parse a node line into (type_name, key_value_pairs).
|
|
112
|
+
|
|
113
|
+
The first whitespace-delimited token that does not contain ``=``
|
|
114
|
+
is the type name. Everything after that is parsed as key=value pairs.
|
|
115
|
+
"""
|
|
116
|
+
stripped = line.lstrip(" ")
|
|
117
|
+
# Find the first whitespace-delimited token
|
|
118
|
+
first_space = stripped.find(" ")
|
|
119
|
+
if first_space == -1:
|
|
120
|
+
# Only the type name, no key-value pairs
|
|
121
|
+
return stripped, []
|
|
122
|
+
|
|
123
|
+
first_token = stripped[:first_space]
|
|
124
|
+
if "=" not in first_token:
|
|
125
|
+
# First token is the type name
|
|
126
|
+
rest = stripped[first_space:]
|
|
127
|
+
kv_pairs = _parse_kv_pairs(rest)
|
|
128
|
+
return first_token, kv_pairs
|
|
129
|
+
|
|
130
|
+
# First token contains '=' — fall back to treating everything as kv pairs
|
|
131
|
+
kv_pairs = _parse_kv_pairs(stripped)
|
|
132
|
+
return kv_pairs[0][0] if kv_pairs else "", kv_pairs[1:] if len(kv_pairs) > 1 else []
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _attribute_name_to_index(name: str) -> int:
|
|
136
|
+
"""Map an attribute name string to its AttributeName int value."""
|
|
137
|
+
for i, s in enumerate(ATTRIBUTE_NAME_STRINGS):
|
|
138
|
+
if s == name:
|
|
139
|
+
return i
|
|
140
|
+
return len(ATTRIBUTE_NAME_STRINGS)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _looks_like_subclass_field(line: str) -> bool:
|
|
144
|
+
"""Check if a line looks like a subclass field (value= or text=)."""
|
|
145
|
+
return line.startswith(("value=", "text="))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _apply_subclass_field(node: SyntaxNode, line: str) -> SyntaxNode:
|
|
149
|
+
"""Apply a subclass field (value= or text=) to a node.
|
|
150
|
+
|
|
151
|
+
If the node is a plain SyntaxNode and needs a subclass attribute,
|
|
152
|
+
convert it to the appropriate subclass. Returns the (possibly new) node.
|
|
153
|
+
"""
|
|
154
|
+
if line.startswith("value="):
|
|
155
|
+
# Convert to ValuedSyntaxNode if needed
|
|
156
|
+
if isinstance(node, SyntaxNode) and not isinstance(node, ValuedSyntaxNode):
|
|
157
|
+
vnode: ValuedSyntaxNode = ValuedSyntaxNode(node.typ)
|
|
158
|
+
vnode._col = node._col
|
|
159
|
+
vnode._line = node._line
|
|
160
|
+
vnode._file_name = node._file_name
|
|
161
|
+
vnode._attributes = list(node._attributes)
|
|
162
|
+
vnode._child_nodes = list(node._child_nodes)
|
|
163
|
+
vnode._source_positions = node._source_positions
|
|
164
|
+
vnode._token_end_line = node._token_end_line
|
|
165
|
+
vnode._token_end_col = node._token_end_col
|
|
166
|
+
else:
|
|
167
|
+
vnode = cast(ValuedSyntaxNode, node)
|
|
168
|
+
vnode.value = _unescape_value(line[len("value="):])
|
|
169
|
+
return vnode
|
|
170
|
+
elif line.startswith("text="):
|
|
171
|
+
# Convert to CommentNode if needed
|
|
172
|
+
if isinstance(node, SyntaxNode) and not isinstance(node, CommentNode):
|
|
173
|
+
cnode: CommentNode = CommentNode(node.typ)
|
|
174
|
+
cnode._col = node._col
|
|
175
|
+
cnode._line = node._line
|
|
176
|
+
cnode._file_name = node._file_name
|
|
177
|
+
cnode._attributes = list(node._attributes)
|
|
178
|
+
cnode._child_nodes = list(node._child_nodes)
|
|
179
|
+
cnode._source_positions = node._source_positions
|
|
180
|
+
cnode._token_end_line = node._token_end_line
|
|
181
|
+
cnode._token_end_col = node._token_end_col
|
|
182
|
+
else:
|
|
183
|
+
cnode = cast(CommentNode, node)
|
|
184
|
+
cnode.text = _unescape_value(line[len("text="):])
|
|
185
|
+
return cnode
|
|
186
|
+
return node
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _parse_single_node(line: str, parent_file_name: str) -> tuple[SyntaxNode, int]:
|
|
190
|
+
"""Parse a single node line and return (node, next_line_index)."""
|
|
191
|
+
type_name, kv_pairs = _parse_node_line(line.lstrip(" "))
|
|
192
|
+
if not type_name:
|
|
193
|
+
raise ValueError(f"Cannot parse node line: {line!r}")
|
|
194
|
+
|
|
195
|
+
typ = _syntax_node_type_from_name(type_name)
|
|
196
|
+
|
|
197
|
+
node = SyntaxNode(typ)
|
|
198
|
+
|
|
199
|
+
for key, value in kv_pairs:
|
|
200
|
+
if key == "line":
|
|
201
|
+
node.line = int(value)
|
|
202
|
+
elif key == "col":
|
|
203
|
+
node.col = int(value)
|
|
204
|
+
elif key == "end_line":
|
|
205
|
+
node.token_end_line = int(value)
|
|
206
|
+
elif key == "end_col":
|
|
207
|
+
node.token_end_col = int(value)
|
|
208
|
+
elif key == "file_name":
|
|
209
|
+
node.file_name = _unescape_value(value)
|
|
210
|
+
elif key == "source_positions":
|
|
211
|
+
positions: list[tuple[int, int]] = []
|
|
212
|
+
for pos in value.split(","):
|
|
213
|
+
if ":" in pos:
|
|
214
|
+
l, c = pos.split(":", 1)
|
|
215
|
+
positions.append((int(l), int(c)))
|
|
216
|
+
node.source_positions = tuple(positions)
|
|
217
|
+
else:
|
|
218
|
+
attr_idx = _attribute_name_to_index(key)
|
|
219
|
+
node.set_attribute(AttributeName(attr_idx), _unescape_value(value))
|
|
220
|
+
|
|
221
|
+
return node, 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _parse_node_block(
|
|
225
|
+
lines: list[str],
|
|
226
|
+
start_idx: int,
|
|
227
|
+
expected_indent: int,
|
|
228
|
+
parent_file_name: str,
|
|
229
|
+
) -> tuple[SyntaxNode, int]:
|
|
230
|
+
"""Parse a block of nodes at a given indentation level."""
|
|
231
|
+
if start_idx >= len(lines):
|
|
232
|
+
raise ValueError(f"Expected node at line index {start_idx}, but no lines remain")
|
|
233
|
+
|
|
234
|
+
first_line = lines[start_idx]
|
|
235
|
+
if not first_line.strip():
|
|
236
|
+
raise ValueError(f"Empty line at index {start_idx}")
|
|
237
|
+
|
|
238
|
+
node, _ = _parse_single_node(first_line, parent_file_name)
|
|
239
|
+
|
|
240
|
+
child_indent = expected_indent + 2
|
|
241
|
+
idx = start_idx + 1
|
|
242
|
+
|
|
243
|
+
while idx < len(lines):
|
|
244
|
+
line = lines[idx]
|
|
245
|
+
if not line.strip():
|
|
246
|
+
idx += 1
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
stripped = line.lstrip(" ")
|
|
250
|
+
current_indent = len(line) - len(stripped)
|
|
251
|
+
|
|
252
|
+
if current_indent < child_indent:
|
|
253
|
+
break
|
|
254
|
+
|
|
255
|
+
if current_indent == child_indent:
|
|
256
|
+
if _looks_like_subclass_field(stripped):
|
|
257
|
+
node = _apply_subclass_field(node, stripped)
|
|
258
|
+
idx += 1
|
|
259
|
+
else:
|
|
260
|
+
# Parse the current line as a child and recurse into its subtree
|
|
261
|
+
child_node, next_idx = _parse_node_block(lines, idx, child_indent, parent_file_name)
|
|
262
|
+
node.add_child(child_node)
|
|
263
|
+
idx = next_idx
|
|
264
|
+
else:
|
|
265
|
+
break
|
|
266
|
+
|
|
267
|
+
return node, idx
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def reconstruct_syntax_tree(serialized: str) -> SyntaxNode:
|
|
271
|
+
"""Reconstruct a ``SyntaxNode`` tree from a serialized string.
|
|
272
|
+
|
|
273
|
+
The input format matches exactly the output of
|
|
274
|
+
``serialize_syntax_tree`` / the Rust ``parse_to_serialized`` function:
|
|
275
|
+
|
|
276
|
+
* Header line: ``# file_name=<value>``
|
|
277
|
+
* One line per node: `` TYPE line=N col=M end_line=N end_col=M [attr=val …]``
|
|
278
|
+
* Subclass fields: `` value=<val>`` or `` text=<text>``
|
|
279
|
+
* Children are indented one level deeper.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
SyntaxNode
|
|
284
|
+
The root node of the reconstructed tree.
|
|
285
|
+
"""
|
|
286
|
+
lines = serialized.split("\n") if serialized else []
|
|
287
|
+
|
|
288
|
+
root_file_name = ""
|
|
289
|
+
line_idx = 0
|
|
290
|
+
if lines and lines[0].startswith("# file_name="):
|
|
291
|
+
root_file_name = _unescape_value(lines[0][len("# file_name="):])
|
|
292
|
+
line_idx = 1
|
|
293
|
+
|
|
294
|
+
root, _ = _parse_node_block(lines, line_idx, 0, root_file_name)
|
|
295
|
+
# Set file_name on the root node so re-serialization includes the header
|
|
296
|
+
root.file_name = root_file_name
|
|
297
|
+
return root
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__all__ = ["reconstruct_syntax_tree"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
NAVIGATION_SHARD_SCHEMA = 4
|
|
12
|
+
_MAX_SHARD_BYTES = 256 * 1024**2
|
|
13
|
+
_KEY_PREFIX = b"python-delphi-lsp-navigation-shard-v4\0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def navigation_cache_key(text: str, defines: tuple[str, ...]) -> str:
|
|
17
|
+
digest = hashlib.sha256()
|
|
18
|
+
digest.update(_KEY_PREFIX)
|
|
19
|
+
digest.update(
|
|
20
|
+
json.dumps(
|
|
21
|
+
sorted(define.casefold() for define in defines),
|
|
22
|
+
ensure_ascii=False,
|
|
23
|
+
separators=(",", ":"),
|
|
24
|
+
).encode("utf-8")
|
|
25
|
+
)
|
|
26
|
+
digest.update(b"\0")
|
|
27
|
+
digest.update(text.encode("utf-8"))
|
|
28
|
+
return digest.hexdigest()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NavigationShardStore:
|
|
32
|
+
"""Content-addressed JSON storage for detached navigation symbols."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, root: str | Path) -> None:
|
|
35
|
+
self.root = Path(root)
|
|
36
|
+
|
|
37
|
+
def load(self, cache_key: str) -> Mapping[str, object] | None:
|
|
38
|
+
path = self._path(cache_key)
|
|
39
|
+
if self.root.is_symlink() or path.parent.is_symlink():
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
descriptor = os.open(
|
|
43
|
+
path,
|
|
44
|
+
os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0),
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
info = os.fstat(descriptor)
|
|
48
|
+
if (
|
|
49
|
+
not stat.S_ISREG(info.st_mode)
|
|
50
|
+
or info.st_size > _MAX_SHARD_BYTES
|
|
51
|
+
or (
|
|
52
|
+
os.name != "nt"
|
|
53
|
+
and (
|
|
54
|
+
info.st_uid != os.getuid()
|
|
55
|
+
or info.st_mode & 0o077
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
):
|
|
59
|
+
return None
|
|
60
|
+
with os.fdopen(descriptor, "rb", closefd=False) as stream:
|
|
61
|
+
raw = stream.read(_MAX_SHARD_BYTES + 1)
|
|
62
|
+
if len(raw) > _MAX_SHARD_BYTES:
|
|
63
|
+
return None
|
|
64
|
+
finally:
|
|
65
|
+
os.close(descriptor)
|
|
66
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
67
|
+
except (OSError, UnicodeError, ValueError):
|
|
68
|
+
return None
|
|
69
|
+
if (
|
|
70
|
+
not isinstance(payload, dict)
|
|
71
|
+
or payload.get("schema") != NAVIGATION_SHARD_SCHEMA
|
|
72
|
+
or payload.get("cache_key") != cache_key
|
|
73
|
+
or not isinstance(payload.get("symbols"), list)
|
|
74
|
+
):
|
|
75
|
+
return None
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
def store(self, cache_key: str, payload: Mapping[str, object]) -> None:
|
|
79
|
+
path = self._path(cache_key)
|
|
80
|
+
self._ensure_directory(path.parent)
|
|
81
|
+
record = {
|
|
82
|
+
**dict(payload),
|
|
83
|
+
"schema": NAVIGATION_SHARD_SCHEMA,
|
|
84
|
+
"cache_key": cache_key,
|
|
85
|
+
}
|
|
86
|
+
data = json.dumps(
|
|
87
|
+
record,
|
|
88
|
+
ensure_ascii=False,
|
|
89
|
+
sort_keys=True,
|
|
90
|
+
separators=(",", ":"),
|
|
91
|
+
allow_nan=False,
|
|
92
|
+
).encode("utf-8")
|
|
93
|
+
if len(data) > _MAX_SHARD_BYTES:
|
|
94
|
+
return
|
|
95
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".navigation-", dir=path.parent)
|
|
96
|
+
try:
|
|
97
|
+
if os.name != "nt":
|
|
98
|
+
os.fchmod(descriptor, 0o600)
|
|
99
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
100
|
+
stream.write(data)
|
|
101
|
+
stream.flush()
|
|
102
|
+
os.fsync(stream.fileno())
|
|
103
|
+
os.replace(temporary, path)
|
|
104
|
+
if os.name != "nt":
|
|
105
|
+
os.chmod(path, 0o600)
|
|
106
|
+
finally:
|
|
107
|
+
try:
|
|
108
|
+
os.unlink(temporary)
|
|
109
|
+
except FileNotFoundError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
def prune(self, live_keys: set[str], max_bytes: int) -> None:
|
|
113
|
+
if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 0:
|
|
114
|
+
raise ValueError("navigation cache byte budget must be non-negative")
|
|
115
|
+
if not self.root.exists() or self.root.is_symlink():
|
|
116
|
+
return
|
|
117
|
+
retained: list[tuple[int, str, Path, int]] = []
|
|
118
|
+
bucket_paths: list[Path] = []
|
|
119
|
+
try:
|
|
120
|
+
buckets = tuple(self.root.iterdir())
|
|
121
|
+
except OSError:
|
|
122
|
+
return
|
|
123
|
+
for bucket in buckets:
|
|
124
|
+
if bucket.is_symlink() or not bucket.is_dir():
|
|
125
|
+
continue
|
|
126
|
+
bucket_paths.append(bucket)
|
|
127
|
+
try:
|
|
128
|
+
shards = tuple(bucket.iterdir())
|
|
129
|
+
except OSError:
|
|
130
|
+
continue
|
|
131
|
+
for shard in shards:
|
|
132
|
+
if (
|
|
133
|
+
shard.is_symlink()
|
|
134
|
+
or not shard.is_file()
|
|
135
|
+
or shard.suffix != ".json"
|
|
136
|
+
):
|
|
137
|
+
continue
|
|
138
|
+
cache_key = shard.stem
|
|
139
|
+
try:
|
|
140
|
+
info = shard.stat(follow_symlinks=False)
|
|
141
|
+
except OSError:
|
|
142
|
+
continue
|
|
143
|
+
if cache_key not in live_keys:
|
|
144
|
+
try:
|
|
145
|
+
shard.unlink()
|
|
146
|
+
except OSError:
|
|
147
|
+
pass
|
|
148
|
+
continue
|
|
149
|
+
retained.append(
|
|
150
|
+
(info.st_mtime_ns, str(shard), shard, info.st_size)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
retained_bytes = sum(item[3] for item in retained)
|
|
154
|
+
for _mtime, _name, shard, size in sorted(retained):
|
|
155
|
+
if retained_bytes <= max_bytes:
|
|
156
|
+
break
|
|
157
|
+
try:
|
|
158
|
+
shard.unlink()
|
|
159
|
+
except OSError:
|
|
160
|
+
continue
|
|
161
|
+
retained_bytes -= size
|
|
162
|
+
for bucket in bucket_paths:
|
|
163
|
+
try:
|
|
164
|
+
bucket.rmdir()
|
|
165
|
+
except OSError:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
def _path(self, cache_key: str) -> Path:
|
|
169
|
+
if (
|
|
170
|
+
len(cache_key) != 64
|
|
171
|
+
or any(character not in "0123456789abcdef" for character in cache_key)
|
|
172
|
+
):
|
|
173
|
+
raise ValueError("navigation cache key is malformed")
|
|
174
|
+
return self.root / cache_key[:2] / f"{cache_key}.json"
|
|
175
|
+
|
|
176
|
+
def _ensure_directory(self, directory: Path) -> None:
|
|
177
|
+
for path in (self.root, directory):
|
|
178
|
+
if path.exists() and path.is_symlink():
|
|
179
|
+
raise OSError("navigation cache path is unsafe")
|
|
180
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
181
|
+
if os.name != "nt":
|
|
182
|
+
os.chmod(path, 0o700)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__all__ = [
|
|
186
|
+
"NAVIGATION_SHARD_SCHEMA",
|
|
187
|
+
"NavigationShardStore",
|
|
188
|
+
"navigation_cache_key",
|
|
189
|
+
]
|