avrae-ls 0.4.0__py3-none-any.whl → 0.4.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
@@ -124,7 +124,9 @@ TYPE_MAP: Dict[str, object] = {
124
124
  "attacks": AliasAttackList,
125
125
  "attack": AliasAttack,
126
126
  "skills": AliasSkills,
127
+ "AliasSkills": AliasSkills,
127
128
  "skill": AliasSkill,
129
+ "AliasSkill": AliasSkill,
128
130
  "saves": AliasSaves,
129
131
  "resistances": AliasResistances,
130
132
  "coinpurse": AliasCoinpurse,
@@ -186,6 +188,226 @@ class TypeMeta:
186
188
  element_type: str = ""
187
189
 
188
190
 
191
+ _SKILL_DOCS: dict[str, str] = {
192
+ "acrobatics": "Acrobatics skill bonus.",
193
+ "animalHandling": "Animal Handling skill bonus.",
194
+ "arcana": "Arcana skill bonus.",
195
+ "athletics": "Athletics skill bonus.",
196
+ "deception": "Deception skill bonus.",
197
+ "history": "History skill bonus.",
198
+ "initiative": "Initiative modifier.",
199
+ "insight": "Insight skill bonus.",
200
+ "intimidation": "Intimidation skill bonus.",
201
+ "investigation": "Investigation skill bonus.",
202
+ "medicine": "Medicine skill bonus.",
203
+ "nature": "Nature skill bonus.",
204
+ "perception": "Perception skill bonus.",
205
+ "performance": "Performance skill bonus.",
206
+ "persuasion": "Persuasion skill bonus.",
207
+ "religion": "Religion skill bonus.",
208
+ "sleightOfHand": "Sleight of Hand skill bonus.",
209
+ "stealth": "Stealth skill bonus.",
210
+ "survival": "Survival skill bonus.",
211
+ "strength": "Strength ability score for this skill block.",
212
+ "dexterity": "Dexterity ability score for this skill block.",
213
+ "constitution": "Constitution ability score for this skill block.",
214
+ "intelligence": "Intelligence ability score for this skill block.",
215
+ "wisdom": "Wisdom ability score for this skill block.",
216
+ "charisma": "Charisma ability score for this skill block.",
217
+ }
218
+
219
+ _COUNTER_DOCS: dict[str, str] = {
220
+ "name": "Internal name of the counter.",
221
+ "title": "Display title for the counter.",
222
+ "desc": "Description text for the counter.",
223
+ "value": "Current counter value.",
224
+ "max": "Maximum value for the counter.",
225
+ "min": "Minimum value for the counter.",
226
+ "reset_on": "Reset cadence for the counter (e.g., long/short rest).",
227
+ "display_type": "Display style for the counter.",
228
+ "reset_to": "Value to reset the counter to.",
229
+ "reset_by": "Increment applied when the counter resets.",
230
+ }
231
+
232
+ _EFFECT_DOCS: dict[str, str] = {
233
+ "name": "Effect name.",
234
+ "duration": "Configured duration for the effect.",
235
+ "remaining": "Remaining duration for the effect.",
236
+ "effect": "Raw effect payload.",
237
+ "attacks": "Attack data attached to the effect, if any.",
238
+ "buttons": "Buttons provided by the effect.",
239
+ "conc": "Whether the effect requires concentration.",
240
+ "desc": "Effect description text.",
241
+ "ticks_on_end": "Whether the effect ticks when it ends.",
242
+ "combatant_name": "Name of the owning combatant.",
243
+ "parent": "Parent effect, if nested.",
244
+ "children": "Child effects nested under this effect.",
245
+ }
246
+
247
+ _ATTR_DOC_OVERRIDES: dict[str, dict[str, str]] = {
248
+ "SimpleRollResult": {
249
+ "dice": "Markdown representation of the dice that were rolled.",
250
+ "total": "Numeric total of the resolved roll.",
251
+ "full": "Rendered roll result string.",
252
+ "result": "Underlying d20 RollResult object.",
253
+ "raw": "Original d20 expression for the roll.",
254
+ },
255
+ "stats": {
256
+ "prof_bonus": "Proficiency bonus for the character.",
257
+ "strength": "Strength ability score.",
258
+ "dexterity": "Dexterity ability score.",
259
+ "constitution": "Constitution ability score.",
260
+ "intelligence": "Intelligence ability score.",
261
+ "wisdom": "Wisdom ability score.",
262
+ "charisma": "Charisma ability score.",
263
+ },
264
+ "AliasBaseStats": {
265
+ "prof_bonus": "Proficiency bonus for the character.",
266
+ "strength": "Strength ability score.",
267
+ "dexterity": "Dexterity ability score.",
268
+ "constitution": "Constitution ability score.",
269
+ "intelligence": "Intelligence ability score.",
270
+ "wisdom": "Wisdom ability score.",
271
+ "charisma": "Charisma ability score.",
272
+ },
273
+ "levels": {
274
+ "total_level": "Sum of all class levels.",
275
+ },
276
+ "AliasLevels": {
277
+ "total_level": "Sum of all class levels.",
278
+ },
279
+ "attack": {
280
+ "name": "Attack name.",
281
+ "verb": "Attack verb or action phrase.",
282
+ "proper": "Whether the attack name is treated as proper.",
283
+ "activation_type": "Activation type identifier for this attack.",
284
+ "raw": "Raw attack payload from the statblock.",
285
+ },
286
+ "AliasAttack": {
287
+ "name": "Attack name.",
288
+ "verb": "Attack verb or action phrase.",
289
+ "proper": "Whether the attack name is treated as proper.",
290
+ "activation_type": "Activation type identifier for this attack.",
291
+ "raw": "Raw attack payload from the statblock.",
292
+ },
293
+ "skills": _SKILL_DOCS,
294
+ "AliasSkills": _SKILL_DOCS,
295
+ "skill": {
296
+ "value": "Total modifier for the skill.",
297
+ "prof": "Proficiency value applied to the skill.",
298
+ "bonus": "Base bonus before rolling.",
299
+ "adv": "Advantage state for the skill roll (True/False/None).",
300
+ },
301
+ "AliasSkill": {
302
+ "value": "Total modifier for the skill.",
303
+ "prof": "Proficiency value applied to the skill.",
304
+ "bonus": "Base bonus before rolling.",
305
+ "adv": "Advantage state for the skill roll (True/False/None).",
306
+ },
307
+ "resistances": {
308
+ "resist": "Damage types resisted.",
309
+ "vuln": "Damage types this target is vulnerable to.",
310
+ "immune": "Damage types the target is immune to.",
311
+ "neutral": "Damage types with no modifiers.",
312
+ },
313
+ "AliasResistances": {
314
+ "resist": "Damage types resisted.",
315
+ "vuln": "Damage types this target is vulnerable to.",
316
+ "immune": "Damage types the target is immune to.",
317
+ "neutral": "Damage types with no modifiers.",
318
+ },
319
+ "coinpurse": {
320
+ "pp": "Platinum pieces carried.",
321
+ "gp": "Gold pieces carried.",
322
+ "ep": "Electrum pieces carried.",
323
+ "sp": "Silver pieces carried.",
324
+ "cp": "Copper pieces carried.",
325
+ "total": "Total value of all coins.",
326
+ },
327
+ "AliasCoinpurse": {
328
+ "pp": "Platinum pieces carried.",
329
+ "gp": "Gold pieces carried.",
330
+ "ep": "Electrum pieces carried.",
331
+ "sp": "Silver pieces carried.",
332
+ "cp": "Copper pieces carried.",
333
+ "total": "Total value of all coins.",
334
+ },
335
+ "custom_counter": _COUNTER_DOCS,
336
+ "consumable": _COUNTER_DOCS,
337
+ "AliasCustomCounter": _COUNTER_DOCS,
338
+ "death_saves": {
339
+ "successes": "Number of successful death saves.",
340
+ "fails": "Number of failed death saves.",
341
+ },
342
+ "AliasDeathSaves": {
343
+ "successes": "Number of successful death saves.",
344
+ "fails": "Number of failed death saves.",
345
+ },
346
+ "spellbook": {
347
+ "dc": "Save DC for spells in this spellbook.",
348
+ "sab": "Spell attack bonus for this spellbook.",
349
+ "caster_level": "Caster level used for the spellbook.",
350
+ "spell_mod": "Spellcasting ability modifier.",
351
+ "spells": "Spells grouped by level.",
352
+ "pact_slot_level": "Level of pact slots, if any.",
353
+ "num_pact_slots": "Number of pact slots available.",
354
+ "max_pact_slots": "Maximum pact slots available.",
355
+ },
356
+ "AliasSpellbook": {
357
+ "dc": "Save DC for spells in this spellbook.",
358
+ "sab": "Spell attack bonus for this spellbook.",
359
+ "caster_level": "Caster level used for the spellbook.",
360
+ "spell_mod": "Spellcasting ability modifier.",
361
+ "spells": "Spells grouped by level.",
362
+ "pact_slot_level": "Level of pact slots, if any.",
363
+ "num_pact_slots": "Number of pact slots available.",
364
+ "max_pact_slots": "Maximum pact slots available.",
365
+ },
366
+ "spell": {
367
+ "name": "Spell name.",
368
+ "dc": "Save DC for this spell.",
369
+ "sab": "Spell attack bonus for this spell.",
370
+ "mod": "Spellcasting modifier applied to the spell.",
371
+ "prepared": "Whether the spell is prepared/known.",
372
+ },
373
+ "AliasSpellbookSpell": {
374
+ "name": "Spell name.",
375
+ "dc": "Save DC for this spell.",
376
+ "sab": "Spell attack bonus for this spell.",
377
+ "mod": "Spellcasting modifier applied to the spell.",
378
+ "prepared": "Whether the spell is prepared/known.",
379
+ },
380
+ "guild": {
381
+ "name": "Guild (server) name.",
382
+ "id": "Guild (server) id.",
383
+ },
384
+ "channel": {
385
+ "name": "Channel name.",
386
+ "id": "Channel id.",
387
+ "topic": "Channel topic, if set.",
388
+ "category": "Parent category for the channel.",
389
+ "parent": "Parent channel, if present.",
390
+ },
391
+ "category": {
392
+ "name": "Category name.",
393
+ "id": "Category id.",
394
+ },
395
+ "author": {
396
+ "name": "User name for the invoking author.",
397
+ "id": "User id for the invoking author.",
398
+ "discriminator": "User discriminator/tag.",
399
+ "display_name": "Display name for the author.",
400
+ "roles": "Roles held by the author.",
401
+ },
402
+ "role": {
403
+ "name": "Role name.",
404
+ "id": "Role id.",
405
+ },
406
+ "effect": _EFFECT_DOCS,
407
+ "SimpleEffect": _EFFECT_DOCS,
408
+ }
409
+
410
+
189
411
  def gather_suggestions(
190
412
  ctx_data: ContextData,
191
413
  resolver: GVarResolver,
@@ -461,98 +683,257 @@ def _infer_type_map(code: str) -> Dict[str, str]:
461
683
  tree = ast.parse(code)
462
684
  except SyntaxError:
463
685
  return {}
464
- type_map: dict[str, str] = {}
465
-
466
- class Visitor(ast.NodeVisitor):
467
- def visit_Assign(self, node: ast.Assign):
468
- val_type, elem_type = self._value_type(node.value)
469
- for target in node.targets:
470
- if not isinstance(target, ast.Name):
471
- continue
472
- if val_type:
473
- type_map[target.id] = val_type
474
- if elem_type:
475
- type_map[f"{target.id}.__element__"] = elem_type
476
- self._record_dict_key_types(target.id, node.value)
477
- self.generic_visit(node)
686
+ visitor = _TypeInferencer(code)
687
+ visitor.visit(tree)
688
+ return visitor.type_map
689
+
690
+
691
+ class _TypeInferencer(ast.NodeVisitor):
692
+ def __init__(self, code: str) -> None:
693
+ self.code = code
694
+ self.type_map: dict[str, str] = {}
695
+
696
+ def visit_Assign(self, node: ast.Assign):
697
+ val_type, elem_type = self._value_type(node.value)
698
+ for target in node.targets:
699
+ self._bind_target(target, val_type, elem_type, node.value)
700
+ self.generic_visit(node)
701
+
702
+ def visit_AnnAssign(self, node: ast.AnnAssign):
703
+ val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
704
+ self._bind_target(node.target, val_type, elem_type, node.value)
705
+ self.generic_visit(node)
706
+
707
+ def visit_AugAssign(self, node: ast.AugAssign):
708
+ val_type, elem_type = self._value_type(node.value)
709
+ self._bind_target(
710
+ node.target,
711
+ val_type or self._existing_type(node.target),
712
+ elem_type or self._existing_element(node.target),
713
+ None,
714
+ )
715
+ self.generic_visit(node)
716
+
717
+ def visit_For(self, node: ast.For):
718
+ _, elem_type = self._value_type(node.iter)
719
+ if not elem_type and isinstance(node.iter, ast.Name):
720
+ elem_type = self.type_map.get(f"{node.iter.id}.__element__")
721
+ self._bind_target(node.target, elem_type, None, None)
722
+ self.generic_visit(node)
723
+
724
+ def visit_AsyncFor(self, node: ast.AsyncFor):
725
+ _, elem_type = self._value_type(node.iter)
726
+ if not elem_type and isinstance(node.iter, ast.Name):
727
+ elem_type = self.type_map.get(f"{node.iter.id}.__element__")
728
+ self._bind_target(node.target, elem_type, None, None)
729
+ self.generic_visit(node)
730
+
731
+ def visit_If(self, node: ast.If):
732
+ self.visit(node.test)
733
+ base_map = self.type_map.copy()
734
+ body_map = self._visit_block(node.body, base_map.copy())
735
+ orelse_seed = base_map.copy()
736
+ orelse_map = self._visit_block(node.orelse, orelse_seed) if node.orelse else orelse_seed
737
+ self.type_map = self._merge_branch_types(base_map, body_map, orelse_map)
738
+
739
+ def _visit_block(self, nodes: Iterable[ast.stmt], seed: dict[str, str]) -> dict[str, str]:
740
+ walker = _TypeInferencer(self.code)
741
+ walker.type_map = seed
742
+ for stmt in nodes:
743
+ walker.visit(stmt)
744
+ return walker.type_map
745
+
746
+ def _merge_branch_types(self, base: dict[str, str], left: dict[str, str], right: dict[str, str]) -> dict[str, str]:
747
+ merged = base.copy()
748
+ for key in set(left) | set(right):
749
+ l_val = left.get(key)
750
+ r_val = right.get(key)
751
+ if l_val and r_val and l_val == r_val:
752
+ merged[key] = l_val
753
+ elif key in base:
754
+ merged[key] = base[key]
755
+ elif l_val and not r_val:
756
+ merged[key] = l_val
757
+ elif r_val and not l_val:
758
+ merged[key] = r_val
759
+ elif key in merged:
760
+ merged.pop(key, None)
761
+ return merged
762
+
763
+ def _bind_target(self, target: ast.AST, val_type: Optional[str], elem_type: Optional[str], source: ast.AST | None):
764
+ if isinstance(target, ast.Name):
765
+ if val_type:
766
+ self.type_map[target.id] = val_type
767
+ if elem_type:
768
+ self.type_map[f"{target.id}.__element__"] = elem_type
769
+ if source is not None:
770
+ self._record_dict_key_types(target.id, source)
771
+ elif isinstance(target, (ast.Tuple, ast.List)):
772
+ for elt in target.elts:
773
+ self._bind_target(elt, val_type, elem_type, source)
478
774
 
479
- def visit_For(self, node: ast.For):
480
- iter_type, elem_type = self._value_type(node.iter)
481
- if not elem_type and isinstance(node.iter, ast.Name):
482
- elem_type = type_map.get(f"{node.iter.id}.__element__")
483
- if elem_type and isinstance(node.target, ast.Name):
484
- type_map[node.target.id] = elem_type
485
- self.generic_visit(node)
775
+ def _existing_type(self, target: ast.AST) -> Optional[str]:
776
+ if isinstance(target, ast.Name):
777
+ return self.type_map.get(target.id)
778
+ return None
486
779
 
487
- def visit_AnnAssign(self, node: ast.AnnAssign):
488
- val_type, elem_type = self._value_type(node.value) if node.value else (None, None)
489
- if isinstance(node.target, ast.Name):
490
- if val_type:
491
- type_map[node.target.id] = val_type
492
- if elem_type:
493
- type_map[f"{node.target.id}.__element__"] = elem_type
494
- self._record_dict_key_types(node.target.id, node.value)
495
- self.generic_visit(node)
780
+ def _existing_element(self, target: ast.AST) -> Optional[str]:
781
+ if isinstance(target, ast.Name):
782
+ return self.type_map.get(f"{target.id}.__element__")
783
+ return None
496
784
 
497
- def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
498
- if isinstance(value, ast.Call) and isinstance(value.func, ast.Name):
785
+ def _value_type(self, value: ast.AST | None) -> tuple[Optional[str], Optional[str]]:
786
+ if isinstance(value, ast.Call):
787
+ if isinstance(value.func, ast.Name):
499
788
  if value.func.id in {"character", "combat"}:
500
789
  return value.func.id, None
501
790
  if value.func.id == "vroll":
502
791
  return "SimpleRollResult", None
503
792
  if value.func.id == "argparse":
504
793
  return "ParsedArguments", None
794
+ if value.func.id == "range":
795
+ return "range", "int"
505
796
  if value.func.id in {"list", "dict", "str"}:
506
797
  return value.func.id, None
507
- if isinstance(value, ast.List):
508
- return "list", None
509
- if isinstance(value, ast.Dict):
510
- return "dict", None
511
- if isinstance(value, ast.Constant):
512
- if isinstance(value.value, str):
513
- return "str", None
514
- if isinstance(value, ast.Name):
515
- if value.id in type_map:
516
- return type_map[value.id], type_map.get(f"{value.id}.__element__")
517
- if value.id in {"character", "combat", "ctx"}:
518
- return value.id, None
519
- if isinstance(value, ast.Attribute):
520
- attr_name = value.attr
521
- base_type = None
522
- base_elem = None
523
- if isinstance(value.value, ast.Name):
524
- base_type = type_map.get(value.value.id)
525
- base_elem = type_map.get(f"{value.value.id}.__element__")
526
- if base_type is None:
527
- base_type, base_elem = self._value_type(value.value)
528
- if base_type:
529
- meta = _type_meta(base_type)
530
- attr_meta = meta.attrs.get(attr_name)
531
- if attr_meta:
532
- if attr_meta.type_name:
533
- return attr_meta.type_name, attr_meta.element_type or None
534
- if attr_meta.element_type:
535
- return base_type, attr_meta.element_type
798
+ if isinstance(value.func, ast.Attribute):
799
+ base_type, base_elem = self._value_type(value.func.value)
800
+ if value.func.attr == "get" and value.args:
801
+ key_literal = self._literal_key(value.args[0])
802
+ val_type, elem_type = self._subscript_type(value.func.value, key_literal, base_type, base_elem)
803
+ if val_type:
804
+ return val_type, elem_type
536
805
  if base_elem:
537
806
  return base_elem, None
538
- if attr_name in TYPE_MAP:
539
- return attr_name, None
540
- return None, None
807
+ if isinstance(value, ast.List):
808
+ elem_type, _ = self._iterable_element_from_values(value.elts)
809
+ return "list", elem_type
810
+ if isinstance(value, ast.Tuple):
811
+ elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
812
+ return "tuple", elem_type
813
+ if isinstance(value, ast.Set):
814
+ elem_type, _ = self._iterable_element_from_values(getattr(value, "elts", []))
815
+ return "set", elem_type
816
+ if isinstance(value, ast.ListComp):
817
+ comp_type, comp_elem = self._value_type(value.elt)
818
+ return "list", comp_type or comp_elem
819
+ if isinstance(value, ast.Dict):
820
+ elem_type, _ = self._iterable_element_from_values(value.values or [])
821
+ return "dict", elem_type
822
+ if isinstance(value, ast.Subscript):
823
+ return self._subscript_value_type(value)
824
+ if isinstance(value, ast.Constant):
825
+ if isinstance(value.value, str):
826
+ return "str", None
827
+ if isinstance(value, ast.Name):
828
+ if value.id in self.type_map:
829
+ return self.type_map[value.id], self.type_map.get(f"{value.id}.__element__")
830
+ if value.id in {"character", "combat", "ctx"}:
831
+ return value.id, None
832
+ if isinstance(value, ast.Attribute):
833
+ attr_name = value.attr
834
+ base_type = None
835
+ base_elem = None
836
+ if isinstance(value.value, ast.Name):
837
+ base_type = self.type_map.get(value.value.id)
838
+ base_elem = self.type_map.get(f"{value.value.id}.__element__")
839
+ if base_type is None:
840
+ base_type, base_elem = self._value_type(value.value)
841
+ if base_type:
842
+ meta = _type_meta(base_type)
843
+ attr_meta = meta.attrs.get(attr_name)
844
+ if attr_meta:
845
+ if attr_meta.type_name:
846
+ return attr_meta.type_name, attr_meta.element_type or None
847
+ if attr_meta.element_type:
848
+ return base_type, attr_meta.element_type
849
+ if base_elem:
850
+ return base_elem, None
851
+ if attr_name in TYPE_MAP:
852
+ return attr_name, None
541
853
  return None, None
854
+ if isinstance(value, ast.IfExp):
855
+ t_type, t_elem = self._value_type(value.body)
856
+ e_type, e_elem = self._value_type(value.orelse)
857
+ if t_type and e_type and t_type == e_type:
858
+ merged_elem = t_elem or e_elem
859
+ if t_elem and e_elem and t_elem != e_elem:
860
+ merged_elem = None
861
+ return t_type, merged_elem
862
+ return t_type or e_type, t_elem or e_elem
863
+ return None, None
864
+
865
+ def _iterable_element_from_values(self, values: Iterable[ast.AST]) -> tuple[Optional[str], Optional[str]]:
866
+ elem_type: Optional[str] = None
867
+ nested_elem: Optional[str] = None
868
+ for node in values:
869
+ val_type, inner_elem = self._value_type(node)
870
+ if not val_type:
871
+ return None, None
872
+ if elem_type is None:
873
+ elem_type = val_type
874
+ nested_elem = inner_elem
875
+ elif elem_type != val_type:
876
+ return None, None
877
+ if inner_elem:
878
+ if nested_elem is None:
879
+ nested_elem = inner_elem
880
+ elif nested_elem != inner_elem:
881
+ nested_elem = None
882
+ return elem_type, nested_elem
883
+
884
+ def _literal_key(self, node: ast.AST | None) -> str | int | None:
885
+ if isinstance(node, ast.Constant):
886
+ if isinstance(node.value, (str, int)):
887
+ return node.value
888
+ if hasattr(ast, "Index") and isinstance(node, getattr(ast, "Index")):
889
+ return self._literal_key(getattr(node, "value", None))
890
+ return None
542
891
 
543
- def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
544
- if not isinstance(value, ast.Dict):
545
- return
546
- for key_node, val_node in zip(value.keys or [], value.values or []):
547
- if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
548
- val_type, elem_type = self._value_type(val_node)
549
- if val_type:
550
- type_map[f"{var_name}.{key_node.value}"] = val_type
551
- if elem_type:
552
- type_map[f"{var_name}.{key_node.value}.__element__"] = elem_type
553
-
554
- Visitor().visit(tree)
555
- return type_map
892
+ def _subscript_type(
893
+ self,
894
+ base_expr: ast.AST,
895
+ key_literal: str | int | None,
896
+ base_type: Optional[str],
897
+ base_elem: Optional[str],
898
+ ) -> tuple[Optional[str], Optional[str]]:
899
+ base_name = base_expr.id if isinstance(base_expr, ast.Name) else None
900
+ if base_name and key_literal is not None:
901
+ dict_key = f"{base_name}.{key_literal}"
902
+ if dict_key in self.type_map:
903
+ return self.type_map[dict_key], self.type_map.get(f"{dict_key}.__element__")
904
+ elem_hint = base_elem
905
+ if base_name and not elem_hint:
906
+ elem_hint = self.type_map.get(f"{base_name}.__element__")
907
+ if base_type:
908
+ meta = _type_meta(base_type)
909
+ if key_literal is not None and key_literal in meta.attrs:
910
+ attr_meta = meta.attrs[key_literal]
911
+ if attr_meta.type_name:
912
+ return attr_meta.type_name, attr_meta.element_type or None
913
+ if attr_meta.element_type:
914
+ return base_type, attr_meta.element_type
915
+ elem_hint = elem_hint or meta.element_type
916
+ if elem_hint:
917
+ return elem_hint, None
918
+ return base_type, None
919
+
920
+ def _subscript_value_type(self, node: ast.Subscript) -> tuple[Optional[str], Optional[str]]:
921
+ base_type, base_elem = self._value_type(node.value)
922
+ key_literal = self._literal_key(getattr(node, "slice", None))
923
+ return self._subscript_type(node.value, key_literal, base_type, base_elem)
924
+
925
+ def _record_dict_key_types(self, var_name: str, value: ast.AST | None) -> None:
926
+ if not isinstance(value, ast.Dict):
927
+ return
928
+ for key_node, val_node in zip(value.keys or [], value.values or []):
929
+ key_literal = self._literal_key(key_node)
930
+ if key_literal is None:
931
+ continue
932
+ val_type, elem_type = self._value_type(val_node)
933
+ if val_type:
934
+ self.type_map[f"{var_name}.{key_literal}"] = val_type
935
+ if elem_type:
936
+ self.type_map[f"{var_name}.{key_literal}.__element__"] = elem_type
556
937
 
557
938
 
558
939
  def _resolve_type_name(receiver: str, code: str, type_map: Dict[str, str] | None = None) -> str:
@@ -619,10 +1000,22 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
619
1000
  return ""
620
1001
  return _element_type_from_iterable(cls, reverse_type_map)
621
1002
 
1003
+ def _getitem_element_for_type_name(type_name: str) -> str:
1004
+ cls = TYPE_MAP.get(type_name)
1005
+ if not cls:
1006
+ return ""
1007
+ return _element_type_from_getitem(cls, reverse_type_map)
1008
+
622
1009
  for type_name, cls in TYPE_MAP.items():
623
1010
  attrs: dict[str, AttrMeta] = {}
624
1011
  methods: dict[str, MethodMeta] = {}
625
1012
  iterable_element = _iter_element_for_type_name(type_name)
1013
+ getitem_element = _getitem_element_for_type_name(type_name)
1014
+ element_hint = iterable_element or getitem_element
1015
+ override_docs = {
1016
+ **_ATTR_DOC_OVERRIDES.get(type_name, {}),
1017
+ **_ATTR_DOC_OVERRIDES.get(cls.__name__, {}),
1018
+ }
626
1019
 
627
1020
  for attr in getattr(cls, "ATTRS", []):
628
1021
  doc = ""
@@ -641,8 +1034,12 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
641
1034
  if not type_name_hint and not element_type_hint:
642
1035
  ann = _class_annotation(cls, attr)
643
1036
  type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
1037
+ if not type_name_hint and element_hint:
1038
+ type_name_hint = element_hint
644
1039
  if type_name_hint and not element_type_hint:
645
1040
  element_type_hint = _iter_element_for_type_name(type_name_hint)
1041
+ if not doc:
1042
+ doc = override_docs.get(attr, doc)
646
1043
  attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
647
1044
 
648
1045
  for meth in getattr(cls, "METHODS", []):
@@ -657,7 +1054,7 @@ def _type_meta_map() -> Dict[str, TypeMeta]:
657
1054
  doc = (meth_obj.__doc__ or "").strip()
658
1055
  methods[meth] = MethodMeta(signature=sig_label, doc=doc)
659
1056
 
660
- meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=iterable_element)
1057
+ meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=element_hint)
661
1058
  return meta
662
1059
 
663
1060
 
@@ -739,6 +1136,16 @@ def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) ->
739
1136
  return ""
740
1137
 
741
1138
 
1139
+ def _element_type_from_getitem(cls: type, reverse_type_map: Dict[type, str]) -> str:
1140
+ try:
1141
+ hints = typing.get_type_hints(cls.__getitem__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
1142
+ ret_ann = hints.get("return")
1143
+ name, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
1144
+ return name or elem
1145
+ except Exception:
1146
+ return ""
1147
+
1148
+
742
1149
  def _attribute_at_position(line_text: str, cursor: int) -> tuple[Optional[str], Optional[str]]:
743
1150
  cursor = max(0, min(cursor, len(line_text)))
744
1151
  for match in ATTR_AT_CURSOR_RE.finditer(line_text):