avrae-ls 0.6.4__py3-none-any.whl → 0.7.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.
@@ -0,0 +1,729 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import re
5
+ import typing
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache
8
+ from html import unescape
9
+ from pathlib import Path
10
+ from typing import Any, Callable, ClassVar, Dict, Iterable, List
11
+
12
+ from .argparser import ParsedArguments
13
+ from .api import (
14
+ AliasAction,
15
+ AliasAttack,
16
+ AliasAttackList,
17
+ AliasBaseStats,
18
+ AliasCoinpurse,
19
+ AliasContextAPI,
20
+ AliasCustomCounter,
21
+ AliasDeathSaves,
22
+ AliasResistances,
23
+ AliasSaves,
24
+ AliasSkill,
25
+ AliasSkills,
26
+ AliasSpellbook,
27
+ AliasSpellbookSpell,
28
+ AliasLevels,
29
+ AuthorAPI,
30
+ CategoryAPI,
31
+ ChannelAPI,
32
+ CharacterAPI,
33
+ GuildAPI,
34
+ RoleAPI,
35
+ SimpleCombat,
36
+ SimpleCombatant,
37
+ SimpleEffect,
38
+ SimpleGroup,
39
+ SimpleRollResult,
40
+ )
41
+
42
+
43
+ class _BuiltinList:
44
+ ATTRS: ClassVar[list[str]] = []
45
+ METHODS: ClassVar[list[str]] = [
46
+ "append",
47
+ "extend",
48
+ "insert",
49
+ "remove",
50
+ "pop",
51
+ "clear",
52
+ "index",
53
+ "count",
54
+ "sort",
55
+ "reverse",
56
+ "copy",
57
+ ]
58
+
59
+ def __iter__(self) -> Iterable[Any]:
60
+ return iter([])
61
+
62
+ def append(self, value: Any) -> None: ...
63
+ def extend(self, iterable: Iterable[Any]) -> None: ...
64
+ def insert(self, index: int, value: Any) -> None: ...
65
+ def remove(self, value: Any) -> None: ...
66
+ def pop(self, index: int = -1) -> Any: ...
67
+ def clear(self) -> None: ...
68
+ def index(self, value: Any, start: int = 0, stop: int | None = None) -> int: ...
69
+ def count(self, value: Any) -> int: ...
70
+ def sort(self, *, key=None, reverse: bool = False) -> None: ...
71
+ def reverse(self) -> None: ...
72
+ def copy(self) -> list[Any]: ...
73
+
74
+
75
+ class _BuiltinDict:
76
+ ATTRS: ClassVar[list[str]] = []
77
+ METHODS: ClassVar[list[str]] = [
78
+ "get",
79
+ "keys",
80
+ "values",
81
+ "items",
82
+ "pop",
83
+ "popitem",
84
+ "update",
85
+ "setdefault",
86
+ "clear",
87
+ "copy",
88
+ ]
89
+
90
+ def __iter__(self) -> Iterable[Any]:
91
+ return iter({})
92
+
93
+ def get(self, key: Any, default: Any = None) -> Any: ...
94
+ def keys(self) -> Any: ...
95
+ def values(self) -> Any: ...
96
+ def items(self) -> Any: ...
97
+ def pop(self, key: Any, default: Any = None) -> Any: ...
98
+ def popitem(self) -> tuple[Any, Any]: ...
99
+ def update(self, *args, **kwargs) -> None: ...
100
+ def setdefault(self, key: Any, default: Any = None) -> Any: ...
101
+ def clear(self) -> None: ...
102
+ def copy(self) -> dict[Any, Any]: ...
103
+
104
+
105
+ class _BuiltinStr:
106
+ ATTRS: ClassVar[list[str]] = []
107
+ METHODS: ClassVar[list[str]] = [
108
+ "lower",
109
+ "upper",
110
+ "title",
111
+ "split",
112
+ "join",
113
+ "replace",
114
+ "strip",
115
+ "startswith",
116
+ "endswith",
117
+ "format",
118
+ ]
119
+
120
+ def __iter__(self) -> Iterable[str]:
121
+ return iter("")
122
+
123
+ def lower(self) -> str: ...
124
+ def upper(self) -> str: ...
125
+ def title(self) -> str: ...
126
+ def split(self, sep: str | None = None, maxsplit: int = -1) -> list[str]: ...
127
+ def join(self, iterable: Iterable[str]) -> str: ...
128
+ def replace(self, old: str, new: str, count: int = -1) -> str: ...
129
+ def strip(self, chars: str | None = None) -> str: ...
130
+ def startswith(self, prefix, start: int = 0, end: int | None = None) -> bool: ...
131
+ def endswith(self, suffix, start: int = 0, end: int | None = None) -> bool: ...
132
+ def format(self, *args, **kwargs) -> str: ...
133
+
134
+
135
+ TypeResolver = Callable[[str | None], str | None]
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class TypeEntry:
140
+ cls: type
141
+ resolver: TypeResolver | None = None
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class TypeSpec:
146
+ name: str
147
+ cls: type
148
+ parents: tuple[str, ...] = ()
149
+ safe_methods: tuple[str, ...] = ()
150
+
151
+
152
+ @dataclass
153
+ class AttrMeta:
154
+ doc: str = ""
155
+ type_name: str = ""
156
+ element_type: str = ""
157
+
158
+
159
+ @dataclass
160
+ class MethodMeta:
161
+ signature: str = ""
162
+ doc: str = ""
163
+
164
+
165
+ @dataclass
166
+ class TypeMeta:
167
+ attrs: Dict[str, AttrMeta]
168
+ methods: Dict[str, MethodMeta]
169
+ element_type: str = ""
170
+
171
+
172
+ def _allow_from(type_key: str, *receiver_types: str) -> TypeResolver:
173
+ allowed = set(receiver_types)
174
+
175
+ def _resolver(receiver_type: str | None) -> str | None:
176
+ if receiver_type in allowed:
177
+ return type_key
178
+ return None
179
+
180
+ return _resolver
181
+
182
+
183
+ TYPE_SPECS: list[TypeSpec] = [
184
+ TypeSpec("character", CharacterAPI, safe_methods=("get_cvar", "get_cc")),
185
+ TypeSpec("combat", SimpleCombat, safe_methods=("get_combatant", "get_group", "get_metadata")),
186
+ TypeSpec("SimpleCombat", SimpleCombat, safe_methods=("get_combatant", "get_group", "get_metadata")),
187
+ TypeSpec("ctx", AliasContextAPI),
188
+ TypeSpec("SimpleRollResult", SimpleRollResult),
189
+ TypeSpec("stats", AliasBaseStats),
190
+ TypeSpec("levels", AliasLevels, parents=("character",), safe_methods=("get",)),
191
+ TypeSpec("attacks", AliasAttackList, parents=("character",)),
192
+ TypeSpec("attack", AliasAttack, parents=("attacks", "actions")),
193
+ TypeSpec("skills", AliasSkills, parents=("character",)),
194
+ TypeSpec("AliasSkills", AliasSkills, parents=("character",)),
195
+ TypeSpec("skill", AliasSkill, parents=("skills",)),
196
+ TypeSpec("AliasSkill", AliasSkill, parents=("skills",)),
197
+ TypeSpec("saves", AliasSaves, parents=("character",), safe_methods=("get",)),
198
+ TypeSpec(
199
+ "resistances",
200
+ AliasResistances,
201
+ parents=("character",),
202
+ safe_methods=("is_resistant", "is_immune", "is_vulnerable", "is_neutral"),
203
+ ),
204
+ TypeSpec("coinpurse", AliasCoinpurse, parents=("character",), safe_methods=("get_coins",)),
205
+ TypeSpec("custom_counter", AliasCustomCounter, parents=("character",)),
206
+ TypeSpec("consumable", AliasCustomCounter, parents=("character",)),
207
+ TypeSpec("death_saves", AliasDeathSaves, parents=("character",), safe_methods=("is_stable", "is_dead")),
208
+ TypeSpec("action", AliasAction, parents=("actions", "character")),
209
+ TypeSpec(
210
+ "spellbook",
211
+ AliasSpellbook,
212
+ parents=("character",),
213
+ safe_methods=("find", "get_slots", "get_max_slots", "remaining_casts_of", "can_cast"),
214
+ ),
215
+ TypeSpec("spell", AliasSpellbookSpell, parents=("spellbook",)),
216
+ TypeSpec("guild", GuildAPI, parents=("ctx",)),
217
+ TypeSpec("channel", ChannelAPI, parents=("ctx",)),
218
+ TypeSpec("category", CategoryAPI, parents=("channel",)),
219
+ TypeSpec("author", AuthorAPI, parents=("ctx",)),
220
+ TypeSpec("role", RoleAPI, parents=("author",)),
221
+ TypeSpec(
222
+ "combatant",
223
+ SimpleCombatant,
224
+ parents=("combat", "SimpleCombat", "group", "SimpleGroup"),
225
+ safe_methods=("get_effect",),
226
+ ),
227
+ TypeSpec(
228
+ "SimpleCombatant",
229
+ SimpleCombatant,
230
+ parents=("combat", "SimpleCombat", "group", "SimpleGroup"),
231
+ safe_methods=("get_effect",),
232
+ ),
233
+ TypeSpec("group", SimpleGroup, parents=("combat", "SimpleCombat"), safe_methods=("get_combatant",)),
234
+ TypeSpec("SimpleGroup", SimpleGroup, parents=("combat", "SimpleCombat"), safe_methods=("get_combatant",)),
235
+ TypeSpec("effect", SimpleEffect, parents=("combatant", "SimpleCombatant")),
236
+ TypeSpec("SimpleEffect", SimpleEffect, parents=("combatant", "SimpleCombatant")),
237
+ TypeSpec("list", _BuiltinList),
238
+ TypeSpec("int", int),
239
+ TypeSpec("float", float),
240
+ TypeSpec("dict", _BuiltinDict, safe_methods=("get",)),
241
+ TypeSpec("str", _BuiltinStr),
242
+ TypeSpec("ParsedArguments", ParsedArguments),
243
+ ]
244
+
245
+
246
+ def _build_type_maps(specs: list[TypeSpec]) -> tuple[Dict[str, TypeEntry], dict[type, set[str]]]:
247
+ type_map: dict[str, TypeEntry] = {}
248
+ safe_methods: dict[type, set[str]] = {}
249
+ for spec in specs:
250
+ resolver = _allow_from(spec.name, *spec.parents) if spec.parents else None
251
+ type_map[spec.name] = TypeEntry(spec.cls, resolver=resolver)
252
+ if spec.safe_methods:
253
+ safe_methods.setdefault(spec.cls, set()).update(spec.safe_methods)
254
+ return type_map, safe_methods
255
+
256
+
257
+ TYPE_MAP, SAFE_METHODS = _build_type_maps(TYPE_SPECS)
258
+
259
+
260
+ def resolve_type_key(type_key: str, receiver_type: str | None = None) -> str | None:
261
+ entry = TYPE_MAP.get(type_key)
262
+ if not entry:
263
+ return None
264
+ return entry.resolver(receiver_type) if entry.resolver else type_key
265
+
266
+
267
+ def type_cls(type_key: str) -> type | None:
268
+ entry = TYPE_MAP.get(type_key)
269
+ if not entry:
270
+ return None
271
+ return entry.cls
272
+
273
+
274
+ def display_type_label(type_key: str) -> str:
275
+ cls = type_cls(type_key)
276
+ if cls is None:
277
+ return type_key
278
+ name = cls.__name__
279
+ if name.startswith("_Builtin"):
280
+ return type_key
281
+ return name
282
+
283
+
284
+ _SKILL_DOCS: dict[str, str] = {
285
+ "acrobatics": "Acrobatics skill bonus.",
286
+ "animalHandling": "Animal Handling skill bonus.",
287
+ "arcana": "Arcana skill bonus.",
288
+ "athletics": "Athletics skill bonus.",
289
+ "deception": "Deception skill bonus.",
290
+ "history": "History skill bonus.",
291
+ "initiative": "Initiative modifier.",
292
+ "insight": "Insight skill bonus.",
293
+ "intimidation": "Intimidation skill bonus.",
294
+ "investigation": "Investigation skill bonus.",
295
+ "medicine": "Medicine skill bonus.",
296
+ "nature": "Nature skill bonus.",
297
+ "perception": "Perception skill bonus.",
298
+ "performance": "Performance skill bonus.",
299
+ "persuasion": "Persuasion skill bonus.",
300
+ "religion": "Religion skill bonus.",
301
+ "sleightOfHand": "Sleight of Hand skill bonus.",
302
+ "stealth": "Stealth skill bonus.",
303
+ "survival": "Survival skill bonus.",
304
+ "strength": "Strength ability score for this skill block.",
305
+ "dexterity": "Dexterity ability score for this skill block.",
306
+ "constitution": "Constitution ability score for this skill block.",
307
+ "intelligence": "Intelligence ability score for this skill block.",
308
+ "wisdom": "Wisdom ability score for this skill block.",
309
+ "charisma": "Charisma ability score for this skill block.",
310
+ }
311
+
312
+ _COUNTER_DOCS: dict[str, str] = {
313
+ "name": "Internal name of the counter.",
314
+ "title": "Display title for the counter.",
315
+ "desc": "Description text for the counter.",
316
+ "value": "Current counter value.",
317
+ "max": "Maximum value for the counter.",
318
+ "min": "Minimum value for the counter.",
319
+ "reset_on": "Reset cadence for the counter (e.g., long/short rest).",
320
+ "display_type": "Display style for the counter.",
321
+ "reset_to": "Value to reset the counter to.",
322
+ "reset_by": "Increment applied when the counter resets.",
323
+ }
324
+
325
+ _EFFECT_DOCS: dict[str, str] = {
326
+ "name": "Effect name.",
327
+ "duration": "Configured duration for the effect.",
328
+ "remaining": "Remaining duration for the effect.",
329
+ "effect": "Raw effect payload.",
330
+ "attacks": "Attack data attached to the effect, if any.",
331
+ "buttons": "Buttons provided by the effect.",
332
+ "conc": "Whether the effect requires concentration.",
333
+ "desc": "Effect description text.",
334
+ "ticks_on_end": "Whether the effect ticks when it ends.",
335
+ "combatant_name": "Name of the owning combatant.",
336
+ "parent": "Parent effect, if nested.",
337
+ "children": "Child effects nested under this effect.",
338
+ }
339
+
340
+ _ATTR_DOC_OVERRIDES: dict[str, dict[str, str]] = {
341
+ "SimpleRollResult": {
342
+ "dice": "Markdown representation of the dice that were rolled.",
343
+ "total": "Numeric total of the resolved roll.",
344
+ "full": "Rendered roll result string.",
345
+ "result": "Underlying d20 RollResult object.",
346
+ "raw": "Original d20 expression for the roll.",
347
+ },
348
+ "stats": {
349
+ "prof_bonus": "Proficiency bonus for the character.",
350
+ "strength": "Strength ability score.",
351
+ "dexterity": "Dexterity ability score.",
352
+ "constitution": "Constitution ability score.",
353
+ "intelligence": "Intelligence ability score.",
354
+ "wisdom": "Wisdom ability score.",
355
+ "charisma": "Charisma ability score.",
356
+ },
357
+ "AliasBaseStats": {
358
+ "prof_bonus": "Proficiency bonus for the character.",
359
+ "strength": "Strength ability score.",
360
+ "dexterity": "Dexterity ability score.",
361
+ "constitution": "Constitution ability score.",
362
+ "intelligence": "Intelligence ability score.",
363
+ "wisdom": "Wisdom ability score.",
364
+ "charisma": "Charisma ability score.",
365
+ },
366
+ "levels": {
367
+ "total_level": "Sum of all class levels.",
368
+ },
369
+ "AliasLevels": {
370
+ "total_level": "Sum of all class levels.",
371
+ },
372
+ "attack": {
373
+ "name": "Attack name.",
374
+ "verb": "Attack verb or action phrase.",
375
+ "proper": "Whether the attack name is treated as proper.",
376
+ "activation_type": "Activation type identifier for this attack.",
377
+ "raw": "Raw attack payload from the statblock.",
378
+ },
379
+ "AliasAttack": {
380
+ "name": "Attack name.",
381
+ "verb": "Attack verb or action phrase.",
382
+ "proper": "Whether the attack name is treated as proper.",
383
+ "activation_type": "Activation type identifier for this attack.",
384
+ "raw": "Raw attack payload from the statblock.",
385
+ },
386
+ "skills": _SKILL_DOCS,
387
+ "AliasSkills": _SKILL_DOCS,
388
+ "skill": {
389
+ "value": "Total modifier for the skill.",
390
+ "prof": "Proficiency value applied to the skill.",
391
+ "bonus": "Base bonus before rolling.",
392
+ "adv": "Advantage state for the skill roll (True/False/None).",
393
+ },
394
+ "AliasSkill": {
395
+ "value": "Total modifier for the skill.",
396
+ "prof": "Proficiency value applied to the skill.",
397
+ "bonus": "Base bonus before rolling.",
398
+ "adv": "Advantage state for the skill roll (True/False/None).",
399
+ },
400
+ "resistances": {
401
+ "resist": "Damage types resisted.",
402
+ "vuln": "Damage types this target is vulnerable to.",
403
+ "immune": "Damage types the target is immune to.",
404
+ "neutral": "Damage types with no modifiers.",
405
+ },
406
+ "AliasResistances": {
407
+ "resist": "Damage types resisted.",
408
+ "vuln": "Damage types this target is vulnerable to.",
409
+ "immune": "Damage types the target is immune to.",
410
+ "neutral": "Damage types with no modifiers.",
411
+ },
412
+ "coinpurse": {
413
+ "pp": "Platinum pieces carried.",
414
+ "gp": "Gold pieces carried.",
415
+ "ep": "Electrum pieces carried.",
416
+ "sp": "Silver pieces carried.",
417
+ "cp": "Copper pieces carried.",
418
+ "total": "Total value of all coins.",
419
+ },
420
+ "AliasCoinpurse": {
421
+ "pp": "Platinum pieces carried.",
422
+ "gp": "Gold pieces carried.",
423
+ "ep": "Electrum pieces carried.",
424
+ "sp": "Silver pieces carried.",
425
+ "cp": "Copper pieces carried.",
426
+ "total": "Total value of all coins.",
427
+ },
428
+ "custom_counter": _COUNTER_DOCS,
429
+ "consumable": _COUNTER_DOCS,
430
+ "AliasCustomCounter": _COUNTER_DOCS,
431
+ "death_saves": {
432
+ "successes": "Number of successful death saves.",
433
+ "fails": "Number of failed death saves.",
434
+ },
435
+ "AliasDeathSaves": {
436
+ "successes": "Number of successful death saves.",
437
+ "fails": "Number of failed death saves.",
438
+ },
439
+ "spellbook": {
440
+ "dc": "Save DC for spells in this spellbook.",
441
+ "sab": "Spell attack bonus for this spellbook.",
442
+ "caster_level": "Caster level used for the spellbook.",
443
+ "spell_mod": "Spellcasting ability modifier.",
444
+ "spells": "Spells grouped by level.",
445
+ "pact_slot_level": "Level of pact slots, if any.",
446
+ "num_pact_slots": "Number of pact slots available.",
447
+ "max_pact_slots": "Maximum pact slots available.",
448
+ },
449
+ "AliasSpellbook": {
450
+ "dc": "Save DC for spells in this spellbook.",
451
+ "sab": "Spell attack bonus for this spellbook.",
452
+ "caster_level": "Caster level used for the spellbook.",
453
+ "spell_mod": "Spellcasting ability modifier.",
454
+ "spells": "Spells grouped by level.",
455
+ "pact_slot_level": "Level of pact slots, if any.",
456
+ "num_pact_slots": "Number of pact slots available.",
457
+ "max_pact_slots": "Maximum pact slots available.",
458
+ },
459
+ "spell": {
460
+ "name": "Spell name.",
461
+ "dc": "Save DC for this spell.",
462
+ "sab": "Spell attack bonus for this spell.",
463
+ "mod": "Spellcasting modifier applied to the spell.",
464
+ "prepared": "Whether the spell is prepared/known.",
465
+ },
466
+ "AliasSpellbookSpell": {
467
+ "name": "Spell name.",
468
+ "dc": "Save DC for this spell.",
469
+ "sab": "Spell attack bonus for this spell.",
470
+ "mod": "Spellcasting modifier applied to the spell.",
471
+ "prepared": "Whether the spell is prepared/known.",
472
+ },
473
+ "guild": {
474
+ "name": "Guild (server) name.",
475
+ "id": "Guild (server) id.",
476
+ },
477
+ "channel": {
478
+ "name": "Channel name.",
479
+ "id": "Channel id.",
480
+ "topic": "Channel topic, if set.",
481
+ "category": "Parent category for the channel.",
482
+ "parent": "Parent channel, if present.",
483
+ },
484
+ "category": {
485
+ "name": "Category name.",
486
+ "id": "Category id.",
487
+ },
488
+ "author": {
489
+ "name": "User name for the invoking author.",
490
+ "id": "User id for the invoking author.",
491
+ "discriminator": "User discriminator/tag.",
492
+ "display_name": "Display name for the author.",
493
+ "roles": "Roles held by the author.",
494
+ },
495
+ "role": {
496
+ "name": "Role name.",
497
+ "id": "Role id.",
498
+ },
499
+ "effect": _EFFECT_DOCS,
500
+ "SimpleEffect": _EFFECT_DOCS,
501
+ }
502
+
503
+ _METHOD_DOC_OVERRIDES: dict[str, dict[str, str]] = {
504
+ "ParsedArguments": {
505
+ "get": "returns all values for the arg cast to the given type.",
506
+ "last": "returns the most recent value cast to the given type.",
507
+ "adv": "returns -1/0/1/2 indicator for dis/normal/adv/elven accuracy.",
508
+ "join": "joins all argument values with a separator into a string.",
509
+ "ignore": "removes argument values so later reads skip them.",
510
+ "update": "replaces values for an argument.",
511
+ "update_nx": "sets values only if the argument is missing.",
512
+ "set_context": "associates a context bucket for nested parsing.",
513
+ "add_context": "appends a context bucket for nested parsing.",
514
+ },
515
+ }
516
+
517
+
518
+ def _load_method_docs_from_html(path: Path | str = "tmp_avrae_api.html") -> dict[str, dict[str, str]]:
519
+ docs: dict[str, dict[str, str]] = {}
520
+ try:
521
+ html = Path(path).read_text(encoding="utf-8")
522
+ except Exception:
523
+ return docs
524
+ pattern = re.compile(
525
+ r'<dt class="sig[^"]*" id="aliasing\.api\.[^\.]+\.(?P<class>\w+)\.(?P<method>\w+)">.*?</dt>\s*(?P<body><dd.*?</dd>)',
526
+ re.DOTALL,
527
+ )
528
+ tag_re = re.compile(r"<[^>]+>")
529
+ for match in pattern.finditer(html):
530
+ cls = match.group("class")
531
+ method = match.group("method")
532
+ body = match.group("body")
533
+ raw_text = unescape(tag_re.sub("", body)).strip()
534
+ text = _strip_signature_prefix(raw_text)
535
+ if not text:
536
+ continue
537
+ docs.setdefault(cls, {})[method] = text
538
+ return docs
539
+
540
+
541
+ def _strip_signature_prefix(text: str) -> str:
542
+ cleaned = re.sub(r"^[A-Za-z_][\w]*\s*\([^)]*\)\s*(?:->|→)?\s*", "", text)
543
+ if cleaned != text:
544
+ return cleaned.strip()
545
+ # Fallback: split on common dash separators after a signature-like prefix.
546
+ for sep in ("–", "—", "-"):
547
+ parts = text.split(sep, 1)
548
+ if len(parts) == 2 and "(" in parts[0] and ")" in parts[0]:
549
+ return parts[1].strip()
550
+ return text.strip()
551
+
552
+
553
+ # Enrich method docs from the bundled API HTML when available.
554
+ _METHOD_DOC_OVERRIDES.update(_load_method_docs_from_html())
555
+
556
+
557
+ def type_meta(type_name: str) -> TypeMeta:
558
+ return _type_meta_map().get(type_name, TypeMeta(attrs={}, methods={}, element_type=""))
559
+
560
+
561
+ @lru_cache()
562
+ def _type_meta_map() -> Dict[str, TypeMeta]:
563
+ meta: dict[str, TypeMeta] = {}
564
+ reverse_type_map: dict[type, str] = {entry.cls: key for key, entry in TYPE_MAP.items()}
565
+
566
+ def _iter_element_for_type_name(type_name: str) -> str:
567
+ cls = type_cls(type_name)
568
+ if not cls:
569
+ return ""
570
+ return _element_type_from_iterable(cls, reverse_type_map)
571
+
572
+ def _getitem_element_for_type_name(type_name: str) -> str:
573
+ cls = type_cls(type_name)
574
+ if not cls:
575
+ return ""
576
+ return _element_type_from_getitem(cls, reverse_type_map)
577
+
578
+ for type_name, entry in TYPE_MAP.items():
579
+ cls = entry.cls
580
+ attrs: dict[str, AttrMeta] = {}
581
+ methods: dict[str, MethodMeta] = {}
582
+ iterable_element = _iter_element_for_type_name(type_name)
583
+ getitem_element = _getitem_element_for_type_name(type_name)
584
+ element_hint = iterable_element or getitem_element
585
+ override_docs = {
586
+ **_ATTR_DOC_OVERRIDES.get(type_name, {}),
587
+ **_ATTR_DOC_OVERRIDES.get(cls.__name__, {}),
588
+ }
589
+ method_override_docs = {
590
+ **_METHOD_DOC_OVERRIDES.get(type_name, {}),
591
+ **_METHOD_DOC_OVERRIDES.get(cls.__name__, {}),
592
+ }
593
+
594
+ for attr in getattr(cls, "ATTRS", []):
595
+ doc = ""
596
+ type_name_hint = ""
597
+ element_type_hint = ""
598
+ try:
599
+ attr_obj = getattr(cls, attr)
600
+ except Exception:
601
+ attr_obj = None
602
+ if isinstance(attr_obj, property) and attr_obj.fget:
603
+ doc = (attr_obj.fget.__doc__ or "").strip()
604
+ ann = _return_annotation(attr_obj.fget, cls)
605
+ type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
606
+ elif attr_obj is not None:
607
+ doc = (getattr(attr_obj, "__doc__", "") or "").strip()
608
+ if not type_name_hint and not element_type_hint:
609
+ ann = _class_annotation(cls, attr)
610
+ type_name_hint, element_type_hint = _type_names_from_annotation(ann, reverse_type_map)
611
+ if not type_name_hint and element_hint:
612
+ type_name_hint = element_hint
613
+ if type_name_hint and not element_type_hint:
614
+ element_type_hint = _iter_element_for_type_name(type_name_hint)
615
+ if not doc:
616
+ doc = override_docs.get(attr, doc)
617
+ attrs[attr] = AttrMeta(doc=doc, type_name=type_name_hint, element_type=element_type_hint)
618
+
619
+ for meth in getattr(cls, "METHODS", []):
620
+ doc = ""
621
+ sig_label = ""
622
+ try:
623
+ meth_obj = getattr(cls, meth)
624
+ except Exception:
625
+ meth_obj = None
626
+ if callable(meth_obj):
627
+ sig_label = _format_method_signature(meth, meth_obj)
628
+ doc = (meth_obj.__doc__ or "").strip()
629
+ if not doc:
630
+ doc = method_override_docs.get(meth, doc)
631
+ methods[meth] = MethodMeta(signature=sig_label, doc=doc)
632
+
633
+ meta[type_name] = TypeMeta(attrs=attrs, methods=methods, element_type=element_hint)
634
+ return meta
635
+
636
+
637
+ def _format_method_signature(name: str, obj: Any) -> str:
638
+ try:
639
+ sig = inspect.signature(obj)
640
+ except (TypeError, ValueError):
641
+ return f"{name}()"
642
+ params = list(sig.parameters.values())
643
+ if params and params[0].name in {"self", "cls"}:
644
+ params = params[1:]
645
+ sig = sig.replace(parameters=params)
646
+ return f"{name}{sig}"
647
+
648
+
649
+ def _return_annotation(func: Any, cls: type) -> Any:
650
+ try:
651
+ module = inspect.getmodule(func) or inspect.getmodule(cls)
652
+ globalns = module.__dict__ if module else None
653
+ hints = typing.get_type_hints(func, globalns=globalns, include_extras=False)
654
+ return hints.get("return")
655
+ except Exception:
656
+ return getattr(func, "__annotations__", {}).get("return")
657
+
658
+
659
+ def _class_annotation(cls: type, attr: str) -> Any:
660
+ try:
661
+ module = inspect.getmodule(cls)
662
+ globalns = module.__dict__ if module else None
663
+ hints = typing.get_type_hints(cls, globalns=globalns, include_extras=False)
664
+ if attr in hints:
665
+ return hints[attr]
666
+ except Exception:
667
+ pass
668
+ return getattr(getattr(cls, "__annotations__", {}), "get", lambda _k: None)(attr)
669
+
670
+
671
+ def _type_names_from_annotation(ann: Any, reverse_type_map: Dict[type, str]) -> tuple[str, str]:
672
+ if ann is None:
673
+ return "", ""
674
+ if isinstance(ann, str):
675
+ return "", ""
676
+ try:
677
+ origin = getattr(ann, "__origin__", None)
678
+ except Exception:
679
+ origin = None
680
+ args = getattr(ann, "__args__", ()) if origin else ()
681
+
682
+ if ann in reverse_type_map:
683
+ return reverse_type_map[ann], ""
684
+
685
+ iterable_origins = {list, List, Iterable, typing.Sequence, typing.Iterable}
686
+ try:
687
+ from collections.abc import Iterable as ABCIterable, Sequence as ABCSequence
688
+
689
+ iterable_origins.update({ABCIterable, ABCSequence})
690
+ except Exception:
691
+ pass
692
+ if origin in iterable_origins:
693
+ if args:
694
+ elem = args[0]
695
+ elem_name, _ = _type_names_from_annotation(elem, reverse_type_map)
696
+ container_name = reverse_type_map.get(origin) or "list"
697
+ return container_name, elem_name
698
+ return reverse_type_map.get(origin) or "list", ""
699
+
700
+ if isinstance(ann, type) and ann in reverse_type_map:
701
+ return reverse_type_map[ann], ""
702
+ return "", ""
703
+
704
+
705
+ def _element_type_from_iterable(cls: type, reverse_type_map: Dict[type, str]) -> str:
706
+ try:
707
+ hints = typing.get_type_hints(cls.__iter__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
708
+ ret_ann = hints.get("return")
709
+ _, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
710
+ return elem
711
+ except Exception:
712
+ return ""
713
+
714
+
715
+ def _element_type_from_getitem(cls: type, reverse_type_map: Dict[type, str]) -> str:
716
+ try:
717
+ hints = typing.get_type_hints(cls.__getitem__, globalns=inspect.getmodule(cls).__dict__, include_extras=False)
718
+ ret_ann = hints.get("return")
719
+ name, elem = _type_names_from_annotation(ret_ann, reverse_type_map)
720
+ return name or elem
721
+ except Exception:
722
+ return ""
723
+
724
+
725
+ def is_safe_call(base: Any, method: str) -> bool:
726
+ for cls, allowed in SAFE_METHODS.items():
727
+ if isinstance(base, cls) and method in allowed:
728
+ return True
729
+ return False