avrae-ls 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.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.
- avrae_ls/__main__.py +84 -0
- avrae_ls/api.py +113 -10
- avrae_ls/completions.py +461 -53
- avrae_ls/context.py +25 -7
- avrae_ls/diagnostics.py +32 -8
- avrae_ls/runtime.py +161 -39
- avrae_ls/server.py +1 -1
- avrae_ls/signature_help.py +73 -19
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/METADATA +4 -3
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/RECORD +14 -14
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/WHEEL +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.2.1.dist-info → avrae_ls-0.3.1.dist-info}/top_level.txt +0 -0
avrae_ls/completions.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
import inspect
|
|
4
5
|
import re
|
|
6
|
+
import typing
|
|
5
7
|
from dataclasses import dataclass
|
|
6
|
-
from
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Any, ClassVar, Dict, Iterable, List, Optional
|
|
7
10
|
|
|
8
11
|
from lsprotocol import types
|
|
9
12
|
|
|
@@ -39,6 +42,76 @@ from .api import (
|
|
|
39
42
|
)
|
|
40
43
|
from .signature_help import FunctionSig
|
|
41
44
|
|
|
45
|
+
|
|
46
|
+
class _BuiltinList:
|
|
47
|
+
ATTRS: ClassVar[list[str]] = []
|
|
48
|
+
METHODS: ClassVar[list[str]] = ["append", "extend", "insert", "remove", "pop", "clear", "index", "count", "sort", "reverse", "copy"]
|
|
49
|
+
|
|
50
|
+
def __iter__(self) -> Iterable[Any]:
|
|
51
|
+
return iter([])
|
|
52
|
+
|
|
53
|
+
def append(self, value: Any) -> None: ...
|
|
54
|
+
def extend(self, iterable: Iterable[Any]) -> None: ...
|
|
55
|
+
def insert(self, index: int, value: Any) -> None: ...
|
|
56
|
+
def remove(self, value: Any) -> None: ...
|
|
57
|
+
def pop(self, index: int = -1) -> Any: ...
|
|
58
|
+
def clear(self) -> None: ...
|
|
59
|
+
def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
|
|
60
|
+
def count(self, value: Any) -> int: ...
|
|
61
|
+
def sort(self, *, key=None, reverse: bool = False) -> None: ...
|
|
62
|
+
def reverse(self) -> None: ...
|
|
63
|
+
def copy(self) -> list[Any]: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _BuiltinDict:
|
|
67
|
+
ATTRS: ClassVar[list[str]] = []
|
|
68
|
+
METHODS: ClassVar[list[str]] = ["get", "keys", "values", "items", "pop", "popitem", "update", "setdefault", "clear", "copy"]
|
|
69
|
+
|
|
70
|
+
def __iter__(self) -> Iterable[Any]:
|
|
71
|
+
return iter({})
|
|
72
|
+
|
|
73
|
+
def get(self, key: Any, default: Any = None) -> Any: ...
|
|
74
|
+
def keys(self) -> Any: ...
|
|
75
|
+
def values(self) -> Any: ...
|
|
76
|
+
def items(self) -> Any: ...
|
|
77
|
+
def pop(self, key: Any, default: Any = None) -> Any: ...
|
|
78
|
+
def popitem(self) -> tuple[Any, Any]: ...
|
|
79
|
+
def update(self, *args, **kwargs) -> None: ...
|
|
80
|
+
def setdefault(self, key: Any, default: Any = None) -> Any: ...
|
|
81
|
+
def clear(self) -> None: ...
|
|
82
|
+
def copy(self) -> dict[Any, Any]: ...
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _BuiltinStr:
|
|
86
|
+
ATTRS: ClassVar[list[str]] = []
|
|
87
|
+
METHODS: ClassVar[list[str]] = [
|
|
88
|
+
"lower",
|
|
89
|
+
"upper",
|
|
90
|
+
"title",
|
|
91
|
+
"split",
|
|
92
|
+
"join",
|
|
93
|
+
"replace",
|
|
94
|
+
"strip",
|
|
95
|
+
"startswith",
|
|
96
|
+
"endswith",
|
|
97
|
+
"format",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
def __iter__(self) -> Iterable[str]:
|
|
101
|
+
return iter("")
|
|
102
|
+
|
|
103
|
+
def lower(self) -> str: ...
|
|
104
|
+
def upper(self) -> str: ...
|
|
105
|
+
def title(self) -> str: ...
|
|
106
|
+
def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
|
|
107
|
+
def join(self, iterable: Iterable[str]) -> str: ...
|
|
108
|
+
def replace(self, old: str, new: str, count: int = -1) -> str: ...
|
|
109
|
+
def strip(self, chars: str | None = None) -> str: ...
|
|
110
|
+
def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
|
|
111
|
+
def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
|
|
112
|
+
def format(self, *args, **kwargs) -> str: ...
|
|
113
|
+
|
|
114
|
+
|
|
42
115
|
TYPE_MAP: Dict[str, object] = {
|
|
43
116
|
"character": CharacterAPI,
|
|
44
117
|
"combat": SimpleCombat,
|
|
@@ -71,12 +144,16 @@ TYPE_MAP: Dict[str, object] = {
|
|
|
71
144
|
"SimpleGroup": SimpleGroup,
|
|
72
145
|
"effect": SimpleEffect,
|
|
73
146
|
"SimpleEffect": SimpleEffect,
|
|
147
|
+
"list": _BuiltinList,
|
|
148
|
+
"dict": _BuiltinDict,
|
|
149
|
+
"str": _BuiltinStr,
|
|
74
150
|
}
|
|
75
151
|
|
|
76
152
|
|
|
77
153
|
IDENT_RE = re.compile(r"[A-Za-z_]\w*$")
|
|
78
|
-
ATTR_RE = re.compile(r"([A-Za-z_][\w
|
|
79
|
-
|
|
154
|
+
ATTR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.(?:([A-Za-z_]\w*)\s*)?$")
|
|
155
|
+
DICT_GET_RE = re.compile(r"^([A-Za-z_]\w*)\.get\(\s*(['\"])(.+?)\2")
|
|
156
|
+
ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.\(\)]*)\.([A-Za-z_]\w*)")
|
|
80
157
|
|
|
81
158
|
|
|
82
159
|
@dataclass
|
|
@@ -87,6 +164,26 @@ class Suggestion:
|
|
|
87
164
|
documentation: str = ""
|
|
88
165
|
|
|
89
166
|
|
|
167
|
+
@dataclass
|
|
168
|
+
class AttrMeta:
|
|
169
|
+
doc: str = ""
|
|
170
|
+
type_name: str = ""
|
|
171
|
+
element_type: str = ""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class MethodMeta:
|
|
176
|
+
signature: str = ""
|
|
177
|
+
doc: str = ""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class TypeMeta:
|
|
182
|
+
attrs: Dict[str, AttrMeta]
|
|
183
|
+
methods: Dict[str, MethodMeta]
|
|
184
|
+
element_type: str = ""
|
|
185
|
+
|
|
186
|
+
|
|
90
187
|
def gather_suggestions(
|
|
91
188
|
ctx_data: ContextData,
|
|
92
189
|
resolver: GVarResolver,
|
|
@@ -130,15 +227,14 @@ def completion_items_for_position(
|
|
|
130
227
|
character: int,
|
|
131
228
|
suggestions: Iterable[Suggestion],
|
|
132
229
|
) -> List[types.CompletionItem]:
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
receiver = attr_match.group(1)
|
|
137
|
-
attr_prefix = attr_match.group(2) or ""
|
|
230
|
+
attr_ctx = _attribute_receiver_and_prefix(code, line, character)
|
|
231
|
+
if attr_ctx:
|
|
232
|
+
receiver, attr_prefix = attr_ctx
|
|
138
233
|
sanitized = _sanitize_incomplete_line(code, line, character)
|
|
139
234
|
type_map = _infer_type_map(sanitized)
|
|
140
235
|
return _attribute_completions(receiver, attr_prefix, sanitized, type_map)
|
|
141
236
|
|
|
237
|
+
line_text = _line_text_to_cursor(code, line, character)
|
|
142
238
|
prefix = _current_prefix(line_text)
|
|
143
239
|
items: list[types.CompletionItem] = []
|
|
144
240
|
for sugg in suggestions:
|
|
@@ -158,10 +254,10 @@ def completion_items_for_position(
|
|
|
158
254
|
def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict[str, str] | None = None) -> List[types.CompletionItem]:
|
|
159
255
|
items: list[types.CompletionItem] = []
|
|
160
256
|
type_key = _resolve_type_name(receiver, code, type_map)
|
|
161
|
-
|
|
257
|
+
meta = _type_meta(type_key)
|
|
162
258
|
detail = f"{type_key}()"
|
|
163
259
|
|
|
164
|
-
for name in attrs:
|
|
260
|
+
for name, attr_meta in meta.attrs.items():
|
|
165
261
|
if prefix and not name.startswith(prefix):
|
|
166
262
|
continue
|
|
167
263
|
items.append(
|
|
@@ -169,16 +265,19 @@ def _attribute_completions(receiver: str, prefix: str, code: str, type_map: Dict
|
|
|
169
265
|
label=name,
|
|
170
266
|
kind=types.CompletionItemKind.Field,
|
|
171
267
|
detail=detail,
|
|
268
|
+
documentation=attr_meta.doc or None,
|
|
172
269
|
)
|
|
173
270
|
)
|
|
174
|
-
for name in methods:
|
|
271
|
+
for name, method_meta in meta.methods.items():
|
|
175
272
|
if prefix and not name.startswith(prefix):
|
|
176
273
|
continue
|
|
274
|
+
method_detail = method_meta.signature or f"{name}()"
|
|
177
275
|
items.append(
|
|
178
276
|
types.CompletionItem(
|
|
179
277
|
label=name,
|
|
180
278
|
kind=types.CompletionItemKind.Method,
|
|
181
|
-
detail=
|
|
279
|
+
detail=method_detail,
|
|
280
|
+
documentation=method_meta.doc or None,
|
|
182
281
|
)
|
|
183
282
|
)
|
|
184
283
|
return items
|
|
@@ -195,29 +294,39 @@ def hover_for_position(
|
|
|
195
294
|
line_text = _line_text(code, line)
|
|
196
295
|
type_map = _infer_type_map(code)
|
|
197
296
|
bindings = _infer_constant_bindings(code, line, ctx_data)
|
|
198
|
-
|
|
199
|
-
if
|
|
297
|
+
attr_ctx = _attribute_receiver_and_prefix(code, line, character, capture_full_token=True)
|
|
298
|
+
if attr_ctx:
|
|
299
|
+
receiver, attr_prefix = attr_ctx
|
|
200
300
|
inferred = _resolve_type_name(receiver, code, type_map)
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
301
|
+
meta = _type_meta(inferred)
|
|
302
|
+
if attr_prefix in meta.attrs:
|
|
303
|
+
doc = meta.attrs[attr_prefix].doc
|
|
304
|
+
contents = f"```avrae\n{inferred}().{attr_prefix}\n```"
|
|
305
|
+
if doc:
|
|
306
|
+
contents += f"\n\n{doc}"
|
|
204
307
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
205
|
-
if
|
|
206
|
-
|
|
308
|
+
if attr_prefix in meta.methods:
|
|
309
|
+
method_meta = meta.methods[attr_prefix]
|
|
310
|
+
signature = method_meta.signature or f"{attr_prefix}()"
|
|
311
|
+
doc = method_meta.doc
|
|
312
|
+
contents = f"```avrae\n{signature}\n```"
|
|
313
|
+
if doc:
|
|
314
|
+
contents += f"\n\n{doc}"
|
|
207
315
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
208
316
|
|
|
209
317
|
word, _, _ = _word_at_position(line_text, character)
|
|
210
318
|
if not word:
|
|
211
319
|
return None
|
|
320
|
+
if word in bindings:
|
|
321
|
+
return _format_binding_hover(word, bindings[word], "local")
|
|
212
322
|
if word in type_map:
|
|
213
|
-
|
|
323
|
+
type_label = _display_type_label(type_map[word])
|
|
324
|
+
contents = f"`{word}` type: `{type_label}`"
|
|
214
325
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
215
326
|
if word in sigs:
|
|
216
327
|
sig = sigs[word]
|
|
217
328
|
contents = f"```avrae\n{sig.label}\n```\n\n{sig.doc}"
|
|
218
329
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
219
|
-
if word in bindings:
|
|
220
|
-
return _format_binding_hover(word, bindings[word], "local")
|
|
221
330
|
|
|
222
331
|
vars_map = ctx_data.vars.to_initial_names()
|
|
223
332
|
if word in vars_map:
|
|
@@ -256,13 +365,75 @@ def _line_text_to_cursor(code: str, line: int, character: int) -> str:
|
|
|
256
365
|
return lines[line][:character]
|
|
257
366
|
|
|
258
367
|
|
|
368
|
+
def _attribute_receiver_and_prefix(code: str, line: int, character: int, capture_full_token: bool = False) -> Optional[tuple[str, str]]:
|
|
369
|
+
lines = code.splitlines()
|
|
370
|
+
if line >= len(lines):
|
|
371
|
+
return None
|
|
372
|
+
line_text = lines[line]
|
|
373
|
+
end = character
|
|
374
|
+
if capture_full_token:
|
|
375
|
+
while end < len(line_text) and (line_text[end].isalnum() or line_text[end] == "_"):
|
|
376
|
+
end += 1
|
|
377
|
+
line_text = line_text[: end]
|
|
378
|
+
dot = line_text.rfind(".")
|
|
379
|
+
if dot == -1:
|
|
380
|
+
return None
|
|
381
|
+
tail = line_text[dot + 1 :]
|
|
382
|
+
prefix_match = re.match(r"\s*([A-Za-z_]\w*)?", tail)
|
|
383
|
+
prefix = prefix_match.group(1) or "" if prefix_match else ""
|
|
384
|
+
suffix = tail[prefix_match.end() if prefix_match else 0 :]
|
|
385
|
+
placeholder = "__COMPLETE__"
|
|
386
|
+
new_line = f"{line_text[:dot]}.{placeholder}{suffix}"
|
|
387
|
+
# Close unmatched parentheses so the temporary code parses.
|
|
388
|
+
paren_balance = new_line.count("(") - new_line.count(")")
|
|
389
|
+
if paren_balance > 0:
|
|
390
|
+
new_line = new_line + (")" * paren_balance)
|
|
391
|
+
mod_lines = list(lines)
|
|
392
|
+
mod_lines[line] = new_line
|
|
393
|
+
mod_code = "\n".join(mod_lines)
|
|
394
|
+
try:
|
|
395
|
+
tree = ast.parse(mod_code)
|
|
396
|
+
except SyntaxError:
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
receiver_src: Optional[str] = None
|
|
400
|
+
|
|
401
|
+
class Finder(ast.NodeVisitor):
|
|
402
|
+
def visit_Attribute(self, node: ast.Attribute):
|
|
403
|
+
nonlocal receiver_src
|
|
404
|
+
if isinstance(node.attr, str) and node.attr == placeholder:
|
|
405
|
+
try:
|
|
406
|
+
receiver_src = ast.unparse(node.value)
|
|
407
|
+
except Exception:
|
|
408
|
+
receiver_src = None
|
|
409
|
+
self.generic_visit(node)
|
|
410
|
+
|
|
411
|
+
Finder().visit(tree)
|
|
412
|
+
if receiver_src is None:
|
|
413
|
+
return None
|
|
414
|
+
return receiver_src, prefix
|
|
415
|
+
|
|
416
|
+
|
|
259
417
|
def _sanitize_incomplete_line(code: str, line: int, character: int) -> str:
|
|
260
418
|
lines = code.splitlines()
|
|
261
419
|
if 0 <= line < len(lines):
|
|
262
|
-
prefix = lines[line][:character]
|
|
263
|
-
|
|
264
|
-
|
|
420
|
+
prefix = lines[line][:character]
|
|
421
|
+
trimmed = prefix.rstrip()
|
|
422
|
+
if trimmed.endswith("."):
|
|
423
|
+
prefix = trimmed[:-1]
|
|
424
|
+
else:
|
|
425
|
+
dot = prefix.rfind(".")
|
|
426
|
+
if dot != -1:
|
|
427
|
+
after = prefix[dot + 1 :]
|
|
428
|
+
if not re.match(r"\s*[A-Za-z_]", after):
|
|
429
|
+
prefix = prefix[:dot] + after
|
|
265
430
|
lines[line] = prefix
|
|
431
|
+
candidate = "\n".join(lines)
|
|
432
|
+
try:
|
|
433
|
+
ast.parse(candidate)
|
|
434
|
+
except SyntaxError:
|
|
435
|
+
indent = re.match(r"[ \t]*", lines[line]).group(0)
|
|
436
|
+
lines[line] = indent + "pass"
|
|
266
437
|
return "\n".join(lines)
|
|
267
438
|
|
|
268
439
|
|
|
@@ -273,6 +444,12 @@ def _line_text(code: str, line: int) -> str:
|
|
|
273
444
|
return lines[line]
|
|
274
445
|
|
|
275
446
|
|
|
447
|
+
def _display_type_label(type_key: str) -> str:
|
|
448
|
+
if type_key in TYPE_MAP:
|
|
449
|
+
return TYPE_MAP[type_key].__name__
|
|
450
|
+
return type_key
|
|
451
|
+
|
|
452
|
+
|
|
276
453
|
def _infer_receiver_type(code: str, name: str) -> Optional[str]:
|
|
277
454
|
return _infer_type_map(code).get(name)
|
|
278
455
|
|
|
@@ -286,66 +463,276 @@ def _infer_type_map(code: str) -> Dict[str, str]:
|
|
|
286
463
|
|
|
287
464
|
class Visitor(ast.NodeVisitor):
|
|
288
465
|
def visit_Assign(self, node: ast.Assign):
|
|
289
|
-
val_type = self._value_type(node.value)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
466
|
+
val_type, elem_type = self._value_type(node.value)
|
|
467
|
+
for target in node.targets:
|
|
468
|
+
if not isinstance(target, ast.Name):
|
|
469
|
+
continue
|
|
470
|
+
if val_type:
|
|
471
|
+
type_map[target.id] = val_type
|
|
472
|
+
if elem_type:
|
|
473
|
+
type_map[f"{target.id}.__element__"] = elem_type
|
|
474
|
+
self._record_dict_key_types(target.id, node.value)
|
|
475
|
+
self.generic_visit(node)
|
|
476
|
+
|
|
477
|
+
def visit_For(self, node: ast.For):
|
|
478
|
+
iter_type, elem_type = self._value_type(node.iter)
|
|
479
|
+
if not elem_type and isinstance(node.iter, ast.Name):
|
|
480
|
+
elem_type = type_map.get(f"{node.iter.id}.__element__")
|
|
481
|
+
if elem_type and isinstance(node.target, ast.Name):
|
|
482
|
+
type_map[node.target.id] = elem_type
|
|
294
483
|
self.generic_visit(node)
|
|
295
484
|
|
|
296
485
|
def visit_AnnAssign(self, node: ast.AnnAssign):
|
|
297
|
-
val_type = self._value_type(node.value) if node.value else None
|
|
298
|
-
if
|
|
299
|
-
|
|
486
|
+
val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
|
|
487
|
+
if isinstance(node.target, ast.Name):
|
|
488
|
+
if val_type:
|
|
489
|
+
type_map[node.target.id] = val_type
|
|
490
|
+
if elem_type:
|
|
491
|
+
type_map[f"{node.target.id}.__element__"] = elem_type
|
|
492
|
+
self._record_dict_key_types(node.target.id, node.value)
|
|
300
493
|
self.generic_visit(node)
|
|
301
494
|
|
|
302
|
-
def _value_type(self, value: ast.AST | None) -> Optional[str]:
|
|
495
|
+
def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
|
|
303
496
|
if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
|
|
304
497
|
if value.func.id in {"character", "combat"}:
|
|
305
|
-
return value.func.id
|
|
498
|
+
return value.func.id, None
|
|
306
499
|
if value.func.id == "vroll":
|
|
307
|
-
return "SimpleRollResult"
|
|
500
|
+
return "SimpleRollResult", None
|
|
501
|
+
if value.func.id in {"list", "dict", "str"}:
|
|
502
|
+
return value.func.id, None
|
|
503
|
+
if isinstance(value, ast.List):
|
|
504
|
+
return "list", None
|
|
505
|
+
if isinstance(value, ast.Dict):
|
|
506
|
+
return "dict", None
|
|
507
|
+
if isinstance(value, ast.Constant):
|
|
508
|
+
if isinstance(value.value, str):
|
|
509
|
+
return "str", None
|
|
308
510
|
if isinstance(value, ast.Name):
|
|
309
511
|
if value.id in type_map:
|
|
310
|
-
return type_map[value.id]
|
|
512
|
+
return type_map[value.id], type_map.get(f"{value.id}.__element__")
|
|
311
513
|
if value.id in {"character", "combat", "ctx"}:
|
|
312
|
-
return value.id
|
|
514
|
+
return value.id, None
|
|
313
515
|
if isinstance(value, ast.Attribute):
|
|
314
516
|
attr_name = value.attr
|
|
315
517
|
base_type = None
|
|
518
|
+
base_elem = None
|
|
316
519
|
if isinstance(value.value, ast.Name):
|
|
317
520
|
base_type = type_map.get(value.value.id)
|
|
521
|
+
base_elem = type_map.get(f"{value.value.id}.__element__")
|
|
318
522
|
if base_type is None:
|
|
319
|
-
base_type = self._value_type(value.value)
|
|
320
|
-
if base_type
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
523
|
+
base_type, base_elem = self._value_type(value.value)
|
|
524
|
+
if base_type:
|
|
525
|
+
meta = _type_meta(base_type)
|
|
526
|
+
attr_meta = meta.attrs.get(attr_name)
|
|
527
|
+
if attr_meta:
|
|
528
|
+
if attr_meta.type_name:
|
|
529
|
+
return attr_meta.type_name, attr_meta.element_type or None
|
|
530
|
+
if attr_meta.element_type:
|
|
531
|
+
return base_type, attr_meta.element_type
|
|
532
|
+
if base_elem:
|
|
533
|
+
return base_elem, None
|
|
534
|
+
if attr_name in TYPE_MAP:
|
|
535
|
+
return attr_name, None
|
|
536
|
+
return None, None
|
|
537
|
+
return None, None
|
|
538
|
+
|
|
539
|
+
def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
|
|
540
|
+
if not isinstance(value, ast.Dict):
|
|
541
|
+
return
|
|
542
|
+
for key_node, val_node in zip(value.keys or [], value.values or []):
|
|
543
|
+
if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
|
|
544
|
+
val_type, elem_type = self._value_type(val_node)
|
|
545
|
+
if val_type:
|
|
546
|
+
type_map[f"{var_name}.{key_node.value}"] = val_type
|
|
547
|
+
if elem_type:
|
|
548
|
+
type_map[f"{var_name}.{key_node.value}.__element__"] = elem_type
|
|
324
549
|
|
|
325
550
|
Visitor().visit(tree)
|
|
326
551
|
return type_map
|
|
327
552
|
|
|
328
553
|
|
|
329
554
|
def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
|
|
330
|
-
receiver = receiver.rstrip("()")
|
|
331
555
|
mapping = type_map or _infer_type_map(code)
|
|
556
|
+
get_match = DICT_GET_RE.match(receiver)
|
|
557
|
+
if get_match:
|
|
558
|
+
base, _, key = get_match.groups()
|
|
559
|
+
dict_key = f"{base}.{key}"
|
|
560
|
+
if dict_key in mapping:
|
|
561
|
+
return mapping[dict_key]
|
|
562
|
+
bracket = receiver.rfind("[")
|
|
563
|
+
if bracket != -1 and receiver.endswith("]"):
|
|
564
|
+
base_expr = receiver[:bracket]
|
|
565
|
+
elem_hint = mapping.get(f"{base_expr}.__element__")
|
|
566
|
+
if elem_hint:
|
|
567
|
+
return elem_hint
|
|
568
|
+
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
569
|
+
if base_type:
|
|
570
|
+
base_meta = _type_meta(base_type)
|
|
571
|
+
if base_meta.element_type:
|
|
572
|
+
return base_meta.element_type
|
|
573
|
+
return base_type
|
|
574
|
+
receiver = receiver.rstrip("()")
|
|
575
|
+
if "." in receiver:
|
|
576
|
+
base_expr, attr_name = receiver.rsplit(".", 1)
|
|
577
|
+
base_type = _resolve_type_name(base_expr, code, mapping)
|
|
578
|
+
if base_type:
|
|
579
|
+
meta = _type_meta(base_type)
|
|
580
|
+
attr_key = attr_name.split("[", 1)[0]
|
|
581
|
+
attr_meta = meta.attrs.get(attr_key)
|
|
582
|
+
if attr_meta:
|
|
583
|
+
if attr_meta.element_type:
|
|
584
|
+
return attr_meta.element_type
|
|
585
|
+
if attr_meta.type_name:
|
|
586
|
+
return attr_meta.type_name
|
|
587
|
+
|
|
332
588
|
if receiver in mapping:
|
|
333
589
|
return mapping[receiver]
|
|
590
|
+
elem_key = f"{receiver}.__element__"
|
|
591
|
+
if elem_key in mapping:
|
|
592
|
+
return mapping[elem_key]
|
|
334
593
|
if receiver in TYPE_MAP:
|
|
335
594
|
return receiver
|
|
336
|
-
tail = receiver.split(".")[-1]
|
|
595
|
+
tail = receiver.split(".")[-1].split("[", 1)[0]
|
|
337
596
|
if tail in TYPE_MAP:
|
|
338
597
|
return tail
|
|
339
598
|
return receiver
|
|
340
599
|
|
|
341
600
|
|
|
342
|
-
def _type_meta(type_name: str) ->
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
601
|
+
def _type_meta(type_name: str) -> TypeMeta:
|
|
602
|
+
return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@lru_cache()
|
|
606
|
+
def _type_meta_map() -> Dict[str, TypeMeta]:
|
|
607
|
+
meta: dict[str, TypeMeta] = {}
|
|
608
|
+
reverse_type_map: dict[type, str] = {}
|
|
609
|
+
for key, cls in TYPE_MAP.items():
|
|
610
|
+
reverse_type_map[cls] = key
|
|
611
|
+
|
|
612
|
+
def _iter_element_for_type_name(type_name: str) -> str:
|
|
613
|
+
cls = TYPE_MAP.get(type_name)
|
|
614
|
+
if not cls:
|
|
615
|
+
return ""
|
|
616
|
+
return _element_type_from_iterable(cls, reverse_type_map)
|
|
617
|
+
|
|
618
|
+
for type_name, cls in TYPE_MAP.items():
|
|
619
|
+
attrs: dict[str, AttrMeta] = {}
|
|
620
|
+
methods: dict[str, MethodMeta] = {}
|
|
621
|
+
iterable_element = _iter_element_for_type_name(type_name)
|
|
622
|
+
|
|
623
|
+
for attr in getattr(cls, "ATTRS", []):
|
|
624
|
+
doc = ""
|
|
625
|
+
type_name_hint = ""
|
|
626
|
+
element_type_hint = ""
|
|
627
|
+
try:
|
|
628
|
+
attr_obj = getattr(cls, attr)
|
|
629
|
+
except Exception:
|
|
630
|
+
attr_obj = None
|
|
631
|
+
if isinstance(attr_obj, property) and attr_obj.fget:
|
|
632
|
+
doc = (attr_obj.fget.__doc__ or "").strip()
|
|
633
|
+
ann = _return_annotation(attr_obj.fget, cls)
|
|
634
|
+
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
635
|
+
elif attr_obj is not None:
|
|
636
|
+
doc = (getattr(attr_obj, "__doc__", "") or "").strip()
|
|
637
|
+
if not type_name_hint and not element_type_hint:
|
|
638
|
+
ann = _class_annotation(cls, attr)
|
|
639
|
+
type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
|
|
640
|
+
if type_name_hint and not element_type_hint:
|
|
641
|
+
element_type_hint = _iter_element_for_type_name(type_name_hint)
|
|
642
|
+
attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
|
|
643
|
+
|
|
644
|
+
for meth in getattr(cls, "METHODS", []):
|
|
645
|
+
doc = ""
|
|
646
|
+
sig_label = ""
|
|
647
|
+
try:
|
|
648
|
+
meth_obj = getattr(cls, meth)
|
|
649
|
+
except Exception:
|
|
650
|
+
meth_obj = None
|
|
651
|
+
if callable(meth_obj):
|
|
652
|
+
sig_label = _format_method_signature(meth, meth_obj)
|
|
653
|
+
doc = (meth_obj.__doc__ or "").strip()
|
|
654
|
+
methods[meth] = MethodMeta(signature=sig_label, doc=doc)
|
|
655
|
+
|
|
656
|
+
meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=iterable_element)
|
|
657
|
+
return meta
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _format_method_signature(name: str, obj: Any) -> str:
|
|
661
|
+
try:
|
|
662
|
+
sig = inspect.signature(obj)
|
|
663
|
+
except (TypeError, ValueError):
|
|
664
|
+
return f"{name}()"
|
|
665
|
+
params = list(sig.parameters.values())
|
|
666
|
+
if params and params[0].name in {"self", "cls"}:
|
|
667
|
+
params = params[1:]
|
|
668
|
+
sig = sig.replace(parameters=params)
|
|
669
|
+
return f"{name}{sig}"
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _return_annotation(func: Any, cls: type) -> Any:
|
|
673
|
+
try:
|
|
674
|
+
module = inspect.getmodule(func) or inspect.getmodule(cls)
|
|
675
|
+
globalns = module.__dict__ if module else None
|
|
676
|
+
hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
|
|
677
|
+
return hints.get("return")
|
|
678
|
+
except Exception:
|
|
679
|
+
return getattr(func, "__annotations__", {}).get("return")
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _class_annotation(cls: type, attr: str) -> Any:
|
|
683
|
+
try:
|
|
684
|
+
module = inspect.getmodule(cls)
|
|
685
|
+
globalns = module.__dict__ if module else None
|
|
686
|
+
hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
|
|
687
|
+
if attr in hints:
|
|
688
|
+
return hints[attr]
|
|
689
|
+
except Exception:
|
|
690
|
+
pass
|
|
691
|
+
return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
|
|
695
|
+
if ann is None:
|
|
696
|
+
return "", ""
|
|
697
|
+
if isinstance(ann, str):
|
|
698
|
+
return "", ""
|
|
699
|
+
try:
|
|
700
|
+
origin = getattr(ann, "__origin__", None)
|
|
701
|
+
except Exception:
|
|
702
|
+
origin = None
|
|
703
|
+
args = getattr(ann, "__args__", ()) if origin else ()
|
|
704
|
+
|
|
705
|
+
if ann in reverse_type_map:
|
|
706
|
+
return reverse_type_map[ann], ""
|
|
707
|
+
|
|
708
|
+
# handle list/sequence typing to detect element type
|
|
709
|
+
iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
|
|
710
|
+
try:
|
|
711
|
+
from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
|
|
712
|
+
iterable_origins.update({ABCIterable, ABCSequence})
|
|
713
|
+
except Exception:
|
|
714
|
+
pass
|
|
715
|
+
if origin in iterable_origins:
|
|
716
|
+
if args:
|
|
717
|
+
elem = args[0]
|
|
718
|
+
elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
|
|
719
|
+
container_name = reverse_type_map.get(origin) or "list"
|
|
720
|
+
return container_name, elem_name
|
|
721
|
+
return reverse_type_map.get(origin) or "list", ""
|
|
722
|
+
|
|
723
|
+
if isinstance(ann, type) and ann in reverse_type_map:
|
|
724
|
+
return reverse_type_map[ann], ""
|
|
725
|
+
return "", ""
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
|
|
729
|
+
try:
|
|
730
|
+
hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
|
|
731
|
+
ret_ann = hints.get("return")
|
|
732
|
+
_, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
|
|
733
|
+
return elem
|
|
734
|
+
except Exception:
|
|
735
|
+
return ""
|
|
349
736
|
|
|
350
737
|
|
|
351
738
|
def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
|
|
@@ -550,12 +937,33 @@ def _is_safe_call(base: Any, method: str) -> bool:
|
|
|
550
937
|
|
|
551
938
|
|
|
552
939
|
def _format_binding_hover(name: str, value: Any, label: str) -> types.Hover:
|
|
553
|
-
type_name =
|
|
940
|
+
type_name = _describe_type(value)
|
|
554
941
|
preview = _preview_value(value)
|
|
555
942
|
contents = f"**{label}** `{name}`\n\nType: `{type_name}`\nValue: `{preview}`"
|
|
556
943
|
return types.Hover(contents=types.MarkupContent(kind=types.MarkupKind.Markdown, value=contents))
|
|
557
944
|
|
|
558
945
|
|
|
946
|
+
def _describe_type(value: Any) -> str:
|
|
947
|
+
# Provide light element-type hints for common iterables so hover shows list[Foo].
|
|
948
|
+
def _iterable_type(iterable: Iterable[Any], container: str) -> str:
|
|
949
|
+
try:
|
|
950
|
+
seen = {type(item).__name__ for item in iterable if item is not None}
|
|
951
|
+
except Exception:
|
|
952
|
+
return container
|
|
953
|
+
return f"{container}[{seen.pop()}]" if len(seen) == 1 else container
|
|
954
|
+
|
|
955
|
+
try:
|
|
956
|
+
if isinstance(value, list):
|
|
957
|
+
return _iterable_type(value, "list")
|
|
958
|
+
if isinstance(value, tuple):
|
|
959
|
+
return _iterable_type(value, "tuple")
|
|
960
|
+
if isinstance(value, set):
|
|
961
|
+
return _iterable_type(value, "set")
|
|
962
|
+
except Exception:
|
|
963
|
+
pass
|
|
964
|
+
return type(value).__name__
|
|
965
|
+
|
|
966
|
+
|
|
559
967
|
def _preview_value(value: Any) -> str:
|
|
560
968
|
text = repr(value)
|
|
561
969
|
if len(text) > 120:
|