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/alias_preview.py +175 -9
- avrae_ls/code_actions.py +282 -0
- avrae_ls/codes.py +3 -0
- avrae_ls/completions.py +485 -78
- avrae_ls/diagnostics.py +62 -5
- avrae_ls/server.py +14 -5
- avrae_ls/signature_help.py +56 -5
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/METADATA +1 -1
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/RECORD +13 -11
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/WHEEL +0 -0
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {avrae_ls-0.4.0.dist-info → avrae_ls-0.4.1.dist-info}/top_level.txt +0 -0
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
self.
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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=
|
|
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):
|