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/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 typing import Any, Dict, Iterable, List, Optional
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\.]*)\.([A-Za-z_]\w*)?\s*$")
79
- ATTR_AT_CURSOR_RE = re.compile(r"([A-Za-z_][\w\.]*?(?:\(\))?)\.([A-Za-z_]\w*)")
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
- line_text = _line_text_to_cursor(code, line, character)
134
- attr_match = ATTR_RE.search(line_text)
135
- if attr_match:
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
- attrs, methods = _type_meta(type_key)
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=f"{detail} method",
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
- receiver, attr_name = _attribute_at_position(line_text, character)
199
- if receiver and attr_name:
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
- attrs, methods = _type_meta(inferred)
202
- if attr_name in attrs:
203
- contents = f"`{inferred}().{attr_name}`"
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 attr_name in methods:
206
- contents = f"`{inferred}().{attr_name}()`"
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
- contents = f"`{word}` type: `{type_map[word]}()`"
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].rstrip()
263
- if prefix.endswith("."):
264
- prefix = prefix[:-1]
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
- if val_type:
291
- for target in node.targets:
292
- if isinstance(target, ast.Name):
293
- type_map[target.id] = val_type
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 val_type and isinstance(node.target, ast.Name):
299
- type_map[node.target.id] = val_type
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 and attr_name in TYPE_MAP:
321
- return attr_name
322
- return None
323
- return None
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) -> tuple[List[str], List[str]]:
343
- cls = TYPE_MAP.get(type_name)
344
- if cls is None:
345
- return [], []
346
- attrs = list(getattr(cls, "ATTRS", []))
347
- methods = list(getattr(cls, "METHODS", []))
348
- return sorted(set(attrs)), sorted(set(methods))
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 = type(value).__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: