python-introspect 0.1.16__py3-none-any.whl → 0.2.2__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.
@@ -9,7 +9,7 @@ Extensibility:
9
9
  type resolution for framework-specific types (lazy configs, proxies, etc.)
10
10
  """
11
11
 
12
- __version__ = "0.1.16"
12
+ __version__ = "0.2.2"
13
13
 
14
14
  from .signature_analyzer import (
15
15
  SignatureAnalyzer,
@@ -24,7 +24,6 @@ from .signature_analyzer import (
24
24
  )
25
25
  from .unified_parameter_analyzer import (
26
26
  UnifiedParameterAnalyzer,
27
- UnifiedParameterInfo,
28
27
  add_parameter_exclusions,
29
28
  set_parameter_exclusions,
30
29
  parameter_exclusions,
@@ -51,6 +50,19 @@ from .dataclass_projection import (
51
50
  dataclass_from_mapping,
52
51
  project_dataclass,
53
52
  )
53
+ from .jsonable import (
54
+ JsonObject,
55
+ JsonScalar,
56
+ JsonValue,
57
+ to_jsonable,
58
+ )
59
+ from .public_api import (
60
+ declared_public_names,
61
+ exported_public_names,
62
+ is_declared_public_name,
63
+ lazy_exports,
64
+ public_names_from_objects,
65
+ )
54
66
  from .environment_projection import (
55
67
  EnvironmentVariable,
56
68
  overlay_dataclass_from_environment,
@@ -89,7 +101,6 @@ __all__ = [
89
101
  "signature_analysis_target",
90
102
  # Unified analysis
91
103
  "UnifiedParameterAnalyzer",
92
- "UnifiedParameterInfo",
93
104
  "add_parameter_exclusions",
94
105
  "set_parameter_exclusions",
95
106
  "parameter_exclusions",
@@ -111,6 +122,17 @@ __all__ = [
111
122
  # Dataclass projection
112
123
  "dataclass_from_mapping",
113
124
  "project_dataclass",
125
+ # JSON-native projection
126
+ "JsonObject",
127
+ "JsonScalar",
128
+ "JsonValue",
129
+ "to_jsonable",
130
+ # Module public surfaces
131
+ "declared_public_names",
132
+ "exported_public_names",
133
+ "is_declared_public_name",
134
+ "lazy_exports",
135
+ "public_names_from_objects",
114
136
  # Environment projection
115
137
  "EnvironmentVariable",
116
138
  "overlay_dataclass_from_environment",
@@ -2,9 +2,54 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import sys
5
6
  import types
6
7
  from enum import Enum
7
- from typing import Annotated, Union, get_args, get_origin
8
+ from typing import Annotated, Literal, Union, get_args, get_origin, get_type_hints
9
+
10
+
11
+ def resolved_class_annotations(owner: type) -> dict[str, object]:
12
+ """Resolve nested type strings in the class that declared each field.
13
+
14
+ Python 3.10 leaves strings inside PEP 585 aliases unresolved. Complete
15
+ those declarations without interpreting Literal values or Annotated metadata.
16
+ """
17
+ annotations = get_type_hints(owner, include_extras=True)
18
+ unresolved = set(annotations)
19
+ for declaration in owner.__mro__:
20
+ class_namespace = dict(vars(declaration))
21
+ module = sys.modules.get(declaration.__module__)
22
+ module_namespace = {} if module is None else vars(module)
23
+ for name in class_namespace.get("__annotations__", {}):
24
+ if name in unresolved:
25
+ annotations[name] = _resolve_nested_type_strings(
26
+ annotations[name], class_namespace, module_namespace
27
+ )
28
+ unresolved.remove(name)
29
+ return annotations
30
+
31
+
32
+ def _resolve_nested_type_strings(annotation: object, globalns: dict, localns: dict) -> object:
33
+ if isinstance(annotation, str):
34
+ return _resolve_nested_type_strings(eval(annotation, globalns, localns), globalns, localns)
35
+ if isinstance(annotation, list):
36
+ resolved = [_resolve_nested_type_strings(member, globalns, localns) for member in annotation]
37
+ return annotation if all(a is b for a, b in zip(resolved, annotation)) else resolved
38
+ origin = get_origin(annotation)
39
+ members = get_args(annotation)
40
+ if not members or origin is Literal:
41
+ return annotation
42
+ if origin is Annotated:
43
+ base = _resolve_nested_type_strings(members[0], globalns, localns)
44
+ return annotation if base is members[0] else Annotated[(base, *members[1:])]
45
+ resolved = tuple(_resolve_nested_type_strings(member, globalns, localns) for member in members)
46
+ if all(member is original for member, original in zip(resolved, members)):
47
+ return annotation
48
+ if isinstance(annotation, types.GenericAlias):
49
+ return types.GenericAlias(origin, resolved)
50
+ if is_union_type(annotation):
51
+ return Union[resolved] # noqa: UP007 - dynamically derived member tuple
52
+ return origin[resolved]
8
53
 
9
54
 
10
55
  def is_union_type(annotation: object) -> bool:
@@ -13,10 +13,9 @@ from typing import (
13
13
  TypeVar,
14
14
  get_args,
15
15
  get_origin,
16
- get_type_hints,
17
16
  )
18
17
 
19
- from .annotation_types import is_union_type
18
+ from .annotation_types import is_union_type, resolved_class_annotations
20
19
  from .validation import validate_annotated_dataclass, validate_annotation_value
21
20
 
22
21
  DataclassT = TypeVar("DataclassT")
@@ -53,7 +52,7 @@ def dataclass_from_mapping(
53
52
  f"{target_type.__name__} received undeclared field(s): {', '.join(extras)}."
54
53
  )
55
54
 
56
- annotations = get_type_hints(target_type, include_extras=True)
55
+ annotations = resolved_class_annotations(target_type)
57
56
  decoded_values: dict[str, object] = {}
58
57
  missing: list[str] = []
59
58
  for declared_field in declared_fields:
@@ -0,0 +1,120 @@
1
+ """JSON-native projection of declared Python values.
2
+
3
+ ``to_jsonable`` is the encoder paired with ``dataclass_from_mapping``: a
4
+ dataclass projects through its declared fields, and the decoder rebuilds it
5
+ from the same declarations. Further value types join through
6
+ ``to_jsonable.register``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import inspect
12
+ from collections.abc import Callable, Mapping
13
+ from dataclasses import fields, is_dataclass
14
+ from enum import Enum
15
+ from functools import singledispatch
16
+ from pathlib import Path
17
+ from typing import ForwardRef, TypeAlias
18
+
19
+ from metaclass_registry import AutoRegisterMeta
20
+
21
+ from .signature_analyzer import signature_analysis_target
22
+
23
+ JsonScalar: TypeAlias = str | int | float | bool | None
24
+ # Recursive aliases retain this declaration's namespace when imported by
25
+ # dataclasses; get_type_hints evaluates the reference in this module.
26
+ _JSON_VALUE_REFERENCE = ForwardRef("JsonValue", module=__name__)
27
+ JsonValue: TypeAlias = (
28
+ JsonScalar
29
+ | Mapping[str, _JSON_VALUE_REFERENCE]
30
+ | tuple[_JSON_VALUE_REFERENCE, ...]
31
+ | list[_JSON_VALUE_REFERENCE]
32
+ )
33
+ JsonObject: TypeAlias = Mapping[str, JsonValue]
34
+
35
+
36
+ def _jsonable_dataclass(value: object) -> JsonValue:
37
+ """Project fields without ``asdict`` deep-copying immutable containers."""
38
+
39
+ return {
40
+ declared_field.name: to_jsonable(getattr(value, declared_field.name))
41
+ for declared_field in fields(value)
42
+ }
43
+
44
+
45
+ @singledispatch
46
+ def to_jsonable(value: object) -> JsonValue:
47
+ """Project dataclasses and registered values into JSON-native data."""
48
+
49
+ if is_dataclass(value) and not isinstance(value, type):
50
+ return _jsonable_dataclass(value)
51
+ raise TypeError(f"Value is not JSON-serializable: {type(value).__name__}")
52
+
53
+
54
+ @to_jsonable.register(Mapping)
55
+ def _jsonable_mapping(value: Mapping) -> JsonValue:
56
+ return {str(to_jsonable(key)): to_jsonable(item) for key, item in value.items()}
57
+
58
+
59
+ @to_jsonable.register(tuple)
60
+ @to_jsonable.register(list)
61
+ @to_jsonable.register(set)
62
+ @to_jsonable.register(frozenset)
63
+ def _jsonable_sequence(value) -> JsonValue:
64
+ return [to_jsonable(item) for item in value]
65
+
66
+
67
+ @to_jsonable.register(Callable)
68
+ def _jsonable_callable(value: Callable[..., object]) -> JsonValue:
69
+ if is_dataclass(value) and not isinstance(value, type):
70
+ return _jsonable_dataclass(value)
71
+ target = signature_analysis_target(value)
72
+ module = inspect.getmodule(target)
73
+ module_name = type(target).__module__ if module is None else module.__name__
74
+ if inspect.isfunction(target) or inspect.ismethod(target) or inspect.isclass(target):
75
+ qualname = target.__qualname__
76
+ else:
77
+ qualname = type(target).__qualname__
78
+ return {
79
+ "kind": "callable",
80
+ "name": qualname.rsplit(".", 1)[-1],
81
+ "module": module_name,
82
+ "qualname": qualname,
83
+ "import_path": f"{module_name}.{qualname}",
84
+ }
85
+
86
+
87
+ @to_jsonable.register(AutoRegisterMeta)
88
+ def _jsonable_registered_type(value: AutoRegisterMeta) -> JsonValue:
89
+ key_attribute = value.__registry_key__
90
+ declaring_owner = next(
91
+ (owner for owner in value.__mro__ if key_attribute in vars(owner)),
92
+ None,
93
+ )
94
+ if declaring_owner is None:
95
+ raise TypeError(
96
+ f"Registered type {value.__qualname__} has no declared " f"{key_attribute!r} key."
97
+ )
98
+ key = vars(declaring_owner)[key_attribute]
99
+ if key is None:
100
+ raise TypeError(f"Registered type {value.__qualname__} has no registry key.")
101
+ return to_jsonable(key)
102
+
103
+
104
+ @to_jsonable.register(Enum)
105
+ def _jsonable_enum(value: Enum) -> JsonValue:
106
+ return to_jsonable(value.value)
107
+
108
+
109
+ @to_jsonable.register(Path)
110
+ def _jsonable_path(value: Path) -> JsonValue:
111
+ return str(value)
112
+
113
+
114
+ @to_jsonable.register(type(None))
115
+ @to_jsonable.register(str)
116
+ @to_jsonable.register(int)
117
+ @to_jsonable.register(float)
118
+ @to_jsonable.register(bool)
119
+ def _jsonable_scalar(value) -> JsonValue:
120
+ return value
@@ -0,0 +1,111 @@
1
+ """Module public surfaces derived from what a module declares."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from importlib import import_module
7
+ from inspect import isclass, isfunction
8
+ from types import ModuleType
9
+ from typing import Any
10
+
11
+
12
+ def declared_public_names(
13
+ module_globals: Mapping[str, object],
14
+ *,
15
+ constant_prefixes: Iterable[str] = (),
16
+ excluded_names: Iterable[str] = (),
17
+ extra_names: Iterable[str] = (),
18
+ ) -> tuple[str, ...]:
19
+ """Return public names declared by the module represented by globals()."""
20
+ module_name = module_globals["__name__"]
21
+ prefixes = tuple(constant_prefixes)
22
+ excluded = set(excluded_names)
23
+ declared_names = tuple(
24
+ name
25
+ for name, value in module_globals.items()
26
+ if name not in excluded
27
+ if is_declared_public_name(
28
+ module_name,
29
+ name,
30
+ value,
31
+ constant_prefixes=prefixes,
32
+ )
33
+ )
34
+ return declared_names + tuple(name for name in extra_names if name not in excluded)
35
+
36
+
37
+ def exported_public_names(
38
+ module_globals: Mapping[str, object],
39
+ *,
40
+ excluded_names: Iterable[str] = (),
41
+ ) -> tuple[str, ...]:
42
+ """Return public re-export names declared by explicit module imports."""
43
+ excluded = set(excluded_names)
44
+ return tuple(
45
+ name
46
+ for name, value in module_globals.items()
47
+ if not name.startswith("_")
48
+ if name not in excluded
49
+ if not isinstance(value, ModuleType)
50
+ )
51
+
52
+
53
+ def public_names_from_objects(*objects: Any, extra_names: Iterable[str] = ()) -> tuple[str, ...]:
54
+ """Return public names from exported object identities plus explicit aliases."""
55
+ return tuple(item if isinstance(item, str) else item.__name__ for item in objects) + tuple(
56
+ extra_names
57
+ )
58
+
59
+
60
+ def is_declared_public_name(
61
+ module_name: str,
62
+ name: str,
63
+ value: object,
64
+ *,
65
+ constant_prefixes: tuple[str, ...] = (),
66
+ ) -> bool:
67
+ """Return whether a global is a public module declaration."""
68
+ if name.startswith("_"):
69
+ return False
70
+ if name.isupper():
71
+ return any(name.startswith(prefix) for prefix in constant_prefixes)
72
+ return (isclass(value) or isfunction(value)) and value.__module__ == module_name
73
+
74
+
75
+ def lazy_exports(
76
+ module_globals: dict[str, object],
77
+ exports: Mapping[str, Iterable[str]],
78
+ ) -> tuple[str, ...]:
79
+ """Install PEP 562 ``__getattr__``/``__dir__`` resolving names on first access.
80
+
81
+ ``exports`` maps each owning module path (absolute, or relative to the
82
+ package whose ``globals()`` are passed) to the names it supplies. A name is
83
+ imported from its owner on first access and cached in the module globals.
84
+ Returns the exported names in declaration order, for ``__all__``.
85
+ """
86
+
87
+ module_name = module_globals["__name__"]
88
+ package = module_globals.get("__package__") or module_name
89
+ owners: dict[str, str] = {}
90
+ for owner, names in exports.items():
91
+ for name in names:
92
+ if name in owners:
93
+ raise ValueError(
94
+ f"{module_name} lazy export {name!r} is declared by both "
95
+ f"{owners[name]!r} and {owner!r}."
96
+ )
97
+ owners[name] = owner
98
+
99
+ def __getattr__(name: str) -> object:
100
+ if name not in owners:
101
+ raise AttributeError(f"module {module_name!r} has no attribute {name!r}")
102
+ value = getattr(import_module(owners[name], package), name)
103
+ module_globals[name] = value
104
+ return value
105
+
106
+ def __dir__() -> list[str]:
107
+ return sorted(set(module_globals) | set(owners))
108
+
109
+ module_globals["__getattr__"] = __getattr__
110
+ module_globals["__dir__"] = __dir__
111
+ return tuple(owners)
@@ -15,7 +15,7 @@ from functools import lru_cache
15
15
  import dataclasses
16
16
  import re
17
17
  from abc import ABC, abstractmethod
18
- from typing import Annotated, Any, Dict, Callable, get_type_hints, NamedTuple, Union, Optional, Type, List, ClassVar, Tuple, get_args, get_origin
18
+ from typing import Annotated, Any, Dict, Callable, get_type_hints, NamedTuple, Union, Optional, Type, List, ClassVar, Tuple, get_args, get_origin, Iterable
19
19
  from weakref import WeakKeyDictionary
20
20
 
21
21
  from dataclasses import dataclass, field
@@ -146,13 +146,71 @@ class AnalysisConstants:
146
146
  CONSTANTS = AnalysisConstants()
147
147
 
148
148
 
149
- class ParameterInfo(NamedTuple):
150
- """Information about a parameter."""
149
+ @dataclass(frozen=True, init=False, eq=False)
150
+ class ParameterInfo:
151
+ """Admitted parameter values with presentation derived from their declaration.
152
+
153
+ Callable descriptions remain concrete because they can determine inferred
154
+ types. Dataclass descriptions are requested independently of value admission.
155
+ """
156
+
151
157
  name: str
152
158
  param_type: type
153
159
  default_value: Any
154
160
  is_required: bool
155
- description: Optional[str] = None # Add parameter description from docstring
161
+ _description: Optional[str]
162
+ _description_owner: Optional[type]
163
+
164
+ def __init__(
165
+ self, name: str, param_type: type, default_value: Any, is_required: bool,
166
+ description: Optional[str] = None, *, description_owner: Optional[type] = None,
167
+ ):
168
+ object.__setattr__(self, "name", name)
169
+ object.__setattr__(self, "param_type", param_type)
170
+ object.__setattr__(self, "default_value", default_value)
171
+ object.__setattr__(self, "is_required", is_required)
172
+ object.__setattr__(self, "_description", description)
173
+ object.__setattr__(self, "_description_owner", description_owner)
174
+
175
+ @property
176
+ def description(self) -> Optional[str]:
177
+ if self._description_owner is not None:
178
+ SignatureAnalyzer._materialize_dataclass_descriptions(self._description_owner, self)
179
+ return self._description
180
+
181
+ def __eq__(self, other: object) -> bool:
182
+ if not isinstance(other, ParameterInfo):
183
+ return NotImplemented
184
+ return (
185
+ self.name, self.param_type, self.default_value, self.is_required, self.description,
186
+ ) == (
187
+ other.name, other.param_type, other.default_value, other.is_required, other.description,
188
+ )
189
+
190
+ def __hash__(self) -> int:
191
+ return hash((self.name, self.param_type, self.default_value, self.is_required, self.description))
192
+
193
+ def _replace(self, **changes: Any) -> "ParameterInfo":
194
+ """Replace declared fields through the same public parameter owner."""
195
+ values = dict(
196
+ name=self.name, param_type=self.param_type,
197
+ default_value=self.default_value, is_required=self.is_required,
198
+ description=self._description,
199
+ )
200
+ unknown = changes.keys() - values.keys()
201
+ if unknown:
202
+ raise ValueError(f"Got unexpected field names: {sorted(unknown)!r}")
203
+ owner = self._description_owner
204
+ if "description" in changes:
205
+ owner = None
206
+ elif "name" in changes and changes["name"] != self.name:
207
+ # A renamed field keeps the original declaration's help, rather
208
+ # than querying a different field in the same source class.
209
+ values["description"] = self.description
210
+ owner = None
211
+ values.update(changes)
212
+ return type(self)(**values, description_owner=owner)
213
+
156
214
 
157
215
  class DocstringInfo(NamedTuple):
158
216
  """Information extracted from a docstring."""
@@ -713,6 +771,70 @@ class CallableAnalysisContext:
713
771
  )
714
772
 
715
773
 
774
+ class ClassSourceDeclaration(NamedTuple):
775
+ """Immutable located class source and its source-only documentation views."""
776
+
777
+ source: str
778
+ inline_docs: Tuple[Tuple[str, Tuple[Tuple[str, str], ...]], ...]
779
+
780
+ @classmethod
781
+ def from_block(cls, source: str, tree: ast.AST, first_line: int = 1) -> "ClassSourceDeclaration":
782
+ # The public extractor historically parses the selected block without
783
+ # dedenting. Keep nested/local blocks' empty inline-documentation view.
784
+ first = next((line for line in source.splitlines() if line.strip()), '')
785
+ if first[:1].isspace():
786
+ return cls(source, ())
787
+ source_lines = source.split('\n')
788
+ declarations = []
789
+ for class_node in ast.walk(tree):
790
+ if not isinstance(class_node, ast.ClassDef):
791
+ continue
792
+ field_docs = {}
793
+
794
+ # Method 1: Look for field assignments followed by string literals (next line)
795
+ for i, node in enumerate(class_node.body):
796
+ if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
797
+ field_name = node.target.id
798
+
799
+ # Check if the next node is a string literal (documentation)
800
+ if i + 1 < len(class_node.body):
801
+ next_node = class_node.body[i + 1]
802
+ if isinstance(next_node, ast.Expr):
803
+ if isinstance(next_node.value, ast.Constant) and isinstance(next_node.value.value, str):
804
+ field_docs[field_name] = next_node.value.value.strip()
805
+ continue
806
+
807
+ # Method 2: Check for inline comments on the same line
808
+ # Get the line number of the field definition
809
+ field_line_num = node.lineno - first_line
810
+ if 0 <= field_line_num < len(source_lines):
811
+ line = source_lines[field_line_num]
812
+
813
+ # Look for string literals in comments on the same line
814
+ # Pattern: field: type = value # """Documentation"""
815
+ comment_match = re.search(r'#\s*["\']([^"\']+)["\']', line)
816
+ if comment_match:
817
+ field_docs[field_name] = comment_match.group(1).strip()
818
+ continue
819
+
820
+ # Look for triple-quoted strings on the same line
821
+ # Pattern: field: type = value """Documentation"""
822
+ triple_quote_match = re.search(r'"""([^"]+)"""|\'\'\'([^\']+)\'\'\'', line)
823
+ if triple_quote_match:
824
+ doc_text = triple_quote_match.group(1) or triple_quote_match.group(2)
825
+ field_docs[field_name] = doc_text.strip()
826
+
827
+ declarations.append((class_node.name, tuple(field_docs.items())))
828
+ return cls(source, tuple(declarations))
829
+
830
+ def field_documentation(self, class_name: str) -> Dict[str, str]:
831
+ """Return a fresh view selected by the caller's current public name."""
832
+ for name, entries in self.inline_docs:
833
+ if name == class_name:
834
+ return dict(entries)
835
+ return {}
836
+
837
+
716
838
  class SignatureAnalyzer:
717
839
  """Universal analyzer for extracting parameter information from any target."""
718
840
 
@@ -907,16 +1029,6 @@ class SignatureAnalyzer:
907
1029
  except Exception:
908
1030
  type_hints = inspect.get_annotations(dataclass_type, eval_str=False)
909
1031
 
910
- # Extract docstring information from dataclass
911
- docstring_info = DocstringExtractor.extract(dataclass_type)
912
-
913
- # Extract inline field documentation using AST
914
- inline_docs = SignatureAnalyzer._extract_inline_field_docs(dataclass_type)
915
-
916
- # ENHANCEMENT: For dataclasses modified by decorators,
917
- # also extract field documentation from the field types themselves
918
- field_type_docs = SignatureAnalyzer._extract_field_type_docs(dataclass_type)
919
-
920
1032
  parameters = {}
921
1033
 
922
1034
  for field in dataclasses.fields(dataclass_type):
@@ -937,31 +1049,12 @@ class SignatureAnalyzer:
937
1049
  default_value = None
938
1050
  is_required = True
939
1051
 
940
- # Get field description from multiple sources (priority order)
941
- field_description = None
942
-
943
- # 1. Field metadata (highest priority)
944
- if 'description' in field.metadata:
945
- field_description = field.metadata['description']
946
- # 2. Inline documentation strings (from AST parsing)
947
- elif field.name in inline_docs:
948
- field_description = inline_docs[field.name]
949
- # 3. Field type documentation (for decorator-modified classes)
950
- elif field.name in field_type_docs:
951
- field_description = field_type_docs[field.name]
952
- # 4. Docstring parameters (fallback)
953
- elif docstring_info.parameters and field.name in docstring_info.parameters:
954
- field_description = docstring_info.parameters.get(field.name)
955
- # 5. CRITICAL FIX: Use inheritance-aware field documentation extraction
956
- else:
957
- field_description = SignatureAnalyzer.extract_field_documentation(dataclass_type, field.name)
958
-
959
1052
  parameters[field.name] = ParameterInfo(
960
1053
  name=field.name,
961
1054
  param_type=param_type,
962
1055
  default_value=default_value,
963
1056
  is_required=is_required,
964
- description=field_description
1057
+ description_owner=dataclass_type,
965
1058
  )
966
1059
 
967
1060
  # PERFORMANCE: Cache the result to avoid re-parsing
@@ -972,21 +1065,110 @@ class SignatureAnalyzer:
972
1065
  # Return empty dict on error (don't cache errors)
973
1066
  return {}
974
1067
 
1068
+ @staticmethod
1069
+ def _materialize_dataclass_descriptions(dataclass_type: type, requested: ParameterInfo) -> None:
1070
+ """Fill presentation on the existing admitted declarations, once requested.
1071
+
1072
+ Presentation extraction errors no longer discard usable value/type
1073
+ declarations. They yield absent help, matching the existing help fallback.
1074
+ """
1075
+ parameters = SignatureAnalyzer._dataclass_analysis_cache.get(dataclass_type, {})
1076
+ admitted = parameters.get(requested.name)
1077
+ if admitted is not None and admitted is not requested:
1078
+ object.__setattr__(requested, "_description", admitted.description)
1079
+ object.__setattr__(requested, "_description_owner", None)
1080
+ return
1081
+ if admitted is not requested:
1082
+ parameters = {requested.name: requested}
1083
+ pending = [info for info in parameters.values() if info._description_owner is not None]
1084
+ if not pending:
1085
+ return
1086
+ try:
1087
+ docstring_info = DocstringExtractor.extract(dataclass_type)
1088
+ inline_docs = SignatureAnalyzer._extract_inline_field_docs(dataclass_type)
1089
+ field_type_docs = SignatureAnalyzer._extract_field_type_docs(dataclass_type)
1090
+ descriptions = {}
1091
+ for field in dataclasses.fields(dataclass_type):
1092
+ if field.name not in parameters:
1093
+ continue
1094
+ if 'description' in field.metadata:
1095
+ description = field.metadata['description']
1096
+ elif field.name in inline_docs:
1097
+ description = inline_docs[field.name]
1098
+ elif field.name in field_type_docs:
1099
+ description = field_type_docs[field.name]
1100
+ elif field.name in docstring_info.parameters_dict:
1101
+ description = docstring_info.parameters_dict[field.name]
1102
+ else:
1103
+ description = SignatureAnalyzer.extract_field_documentation(dataclass_type, field.name)
1104
+ descriptions[field.name] = description
1105
+ except Exception:
1106
+ descriptions = {}
1107
+ for info in pending:
1108
+ object.__setattr__(info, "_description", descriptions.get(info.name))
1109
+ object.__setattr__(info, "_description_owner", None)
1110
+
1111
+ @staticmethod
1112
+ def prepare_dataclass_declarations(declarations: Iterable[type]) -> None:
1113
+ """Prepare source facts for declared schemas without resolving values.
1114
+
1115
+ MRO, raw annotations and declared dataclass defaults expose schema
1116
+ relationships. Unresolved strings/ForwardRefs remain live for analysis;
1117
+ factories, namespace providers and type resolvers are never invoked.
1118
+ """
1119
+ pending = list(declarations)
1120
+ visited = set()
1121
+ while pending:
1122
+ declaration = pending.pop()
1123
+ if not inspect.isclass(declaration) or declaration in visited:
1124
+ continue
1125
+ visited.add(declaration)
1126
+ if declaration is object:
1127
+ continue
1128
+ if not dataclasses.is_dataclass(declaration):
1129
+ continue
1130
+ SignatureAnalyzer._extract_inline_field_docs(declaration)
1131
+ pending.extend(
1132
+ base for base in inspect.getmro(declaration)[1:]
1133
+ if dataclasses.is_dataclass(base)
1134
+ )
1135
+ for declared_field in dataclasses.fields(declaration):
1136
+ field_types = [declared_field.type]
1137
+ while field_types:
1138
+ field_type = field_types.pop()
1139
+ if inspect.isclass(field_type) and dataclasses.is_dataclass(field_type):
1140
+ pending.append(field_type)
1141
+ field_types.extend(get_args(field_type))
1142
+ if dataclasses.is_dataclass(declared_field.default):
1143
+ default = declared_field.default
1144
+ pending.append(default if inspect.isclass(default) else type(default))
1145
+ factory = declared_field.default_factory
1146
+ if inspect.isclass(factory) and dataclasses.is_dataclass(factory):
1147
+ pending.append(factory)
1148
+
975
1149
  @staticmethod
976
1150
  def prepare_dataclass_declaration(dataclass_type: type) -> None:
977
- """Warm pure source lookup without evaluating fields or default factories."""
978
- SignatureAnalyzer._extract_inline_field_docs(dataclass_type)
1151
+ """Prepare one declaration's source closure without evaluating defaults."""
1152
+ SignatureAnalyzer.prepare_dataclass_declarations((dataclass_type,))
979
1153
 
980
1154
  @staticmethod
981
- def _dataclass_source(dataclass_type: type) -> str:
1155
+ def _dataclass_source(dataclass_type: type) -> ClassSourceDeclaration:
982
1156
  """Read current loader source, deriving its class block by immutable content."""
983
1157
  # Python 3.13+ locates class source from __firstlineno__ directly;
984
1158
  # its native locator does not perform the repeated module AST traversal.
985
1159
  if sys.version_info >= (3, 13):
986
- return inspect.getsource(dataclass_type)
1160
+ source = inspect.getsource(dataclass_type)
1161
+ if source[:1].isspace():
1162
+ return ClassSourceDeclaration(source, ())
1163
+ tree = SignatureAnalyzer._module_source_tree(source)
1164
+ class_node = next((node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)), None)
1165
+ if class_node is None:
1166
+ return ClassSourceDeclaration(source, ())
1167
+ return SignatureAnalyzer._qualified_class_source(source, class_node.name)
987
1168
  target = inspect.unwrap(dataclass_type)
988
1169
  if not inspect.isclass(target):
989
- return inspect.getsource(target)
1170
+ source = inspect.getsource(target)
1171
+ return ClassSourceDeclaration.from_block(source, SignatureAnalyzer._module_source_tree(source))
990
1172
  file = inspect.getsourcefile(target)
991
1173
  if file:
992
1174
  linecache.checkcache(file)
@@ -1011,7 +1193,7 @@ class SignatureAnalyzer:
1011
1193
 
1012
1194
  @staticmethod
1013
1195
  @lru_cache(maxsize=256)
1014
- def _qualified_class_source(source: str, qualname: str) -> Optional[str]:
1196
+ def _qualified_class_source(source: str, qualname: str) -> Optional[ClassSourceDeclaration]:
1015
1197
  """Use Python's qualified class/decorator locator on current source content."""
1016
1198
  tree = SignatureAnalyzer._module_source_tree(source)
1017
1199
  finder = inspect._ClassFinder(qualname)
@@ -1019,7 +1201,14 @@ class SignatureAnalyzer:
1019
1201
  finder.visit(tree)
1020
1202
  except inspect.ClassFoundException as found:
1021
1203
  lines = source.splitlines(keepends=True)
1022
- return ''.join(inspect.getblock(lines[found.args[0]:]))
1204
+ first_line = found.args[0] + 1
1205
+ block = ''.join(inspect.getblock(lines[found.args[0]:]))
1206
+ class_node = next(
1207
+ node for node in ast.walk(tree)
1208
+ if isinstance(node, ast.ClassDef)
1209
+ and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
1210
+ )
1211
+ return ClassSourceDeclaration.from_block(block, class_node, first_line)
1023
1212
  return None
1024
1213
 
1025
1214
  @staticmethod
@@ -1049,84 +1238,22 @@ class SignatureAnalyzer:
1049
1238
  field_name: str = "default"
1050
1239
  """
1051
1240
  try:
1052
- import ast
1053
- import re
1054
-
1055
- # Try to get source code - handle cases where it might not be available
1056
- source = None
1057
1241
  try:
1058
- source = SignatureAnalyzer._dataclass_source(dataclass_type)
1242
+ declaration = SignatureAnalyzer._dataclass_source(dataclass_type)
1059
1243
  except (OSError, TypeError):
1060
- try:
1061
- source_file = inspect.getfile(dataclass_type)
1062
- with open(source_file, 'r', encoding='utf-8') as f:
1063
- file_content = f.read()
1064
- source = SignatureAnalyzer._extract_class_source_from_file(file_content, dataclass_type.__name__)
1065
- except Exception:
1066
- pass
1067
-
1068
- if not source:
1069
- return {}
1070
-
1071
- tree = ast.parse(source)
1072
-
1073
- # Find the class definition - be more flexible with class name matching
1074
- class_node = None
1075
- target_class_name = dataclass_type.__name__
1076
-
1077
- # Handle cases where the class might have been renamed or modified
1078
- for node in ast.walk(tree):
1079
- if isinstance(node, ast.ClassDef):
1080
- # Try exact match first
1081
- if node.name == target_class_name:
1082
- class_node = node
1083
- break
1084
- # Also try without common prefixes/suffixes that decorators might add
1085
-
1086
- if not class_node:
1087
- return {}
1088
-
1089
- field_docs = {}
1090
- source_lines = source.split('\n')
1091
-
1092
- # Method 1: Look for field assignments followed by string literals (next line)
1093
- for i, node in enumerate(class_node.body):
1094
- if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
1095
- field_name = node.target.id
1096
-
1097
- # Check if the next node is a string literal (documentation)
1098
- if i + 1 < len(class_node.body):
1099
- next_node = class_node.body[i + 1]
1100
- if isinstance(next_node, ast.Expr):
1101
- if isinstance(next_node.value, ast.Constant) and isinstance(next_node.value.value, str):
1102
- field_docs[field_name] = next_node.value.value.strip()
1103
- continue
1104
-
1105
- # Method 2: Check for inline comments on the same line
1106
- # Get the line number of the field definition
1107
- field_line_num = node.lineno - 1 # Convert to 0-based indexing
1108
- if 0 <= field_line_num < len(source_lines):
1109
- line = source_lines[field_line_num]
1110
-
1111
- # Look for string literals in comments on the same line
1112
- # Pattern: field: type = value # """Documentation"""
1113
- comment_match = re.search(r'#\s*["\']([^"\']+)["\']', line)
1114
- if comment_match:
1115
- field_docs[field_name] = comment_match.group(1).strip()
1116
- continue
1117
-
1118
- # Look for triple-quoted strings on the same line
1119
- # Pattern: field: type = value """Documentation"""
1120
- triple_quote_match = re.search(r'"""([^"]+)"""|\'\'\'([^\']+)\'\'\'', line)
1121
- if triple_quote_match:
1122
- doc_text = triple_quote_match.group(1) or triple_quote_match.group(2)
1123
- field_docs[field_name] = doc_text.strip()
1124
-
1125
- return field_docs
1126
-
1244
+ source_file = inspect.getfile(dataclass_type)
1245
+ with open(source_file, 'r', encoding='utf-8') as source_stream:
1246
+ source = SignatureAnalyzer._extract_class_source_from_file(
1247
+ source_stream.read(), dataclass_type.__name__
1248
+ )
1249
+ if not source:
1250
+ return {}
1251
+ declaration = SignatureAnalyzer._qualified_class_source(source, dataclass_type.__name__)
1252
+ if declaration is None:
1253
+ return {}
1254
+ return declaration.field_documentation(dataclass_type.__name__)
1127
1255
  except Exception:
1128
- # Return empty dict if AST parsing fails
1129
- # Could add logging here for debugging: logger.debug(f"AST parsing failed: {e}")
1256
+ # Preserve the extractor's unavailable/unparsable-source boundary.
1130
1257
  return {}
1131
1258
 
1132
1259
  @staticmethod
@@ -13,7 +13,6 @@ import inspect
13
13
  import dataclasses
14
14
  from abc import ABC, abstractmethod
15
15
  from typing import Dict, Union, Callable, Type, Any, Optional, ClassVar
16
- from dataclasses import dataclass
17
16
  from weakref import WeakKeyDictionary
18
17
 
19
18
  from metaclass_registry import AutoRegisterMeta
@@ -59,29 +58,6 @@ def add_parameter_exclusions(
59
58
  set_parameter_exclusions(target, (*parameter_exclusions(target), *normalized))
60
59
 
61
60
 
62
- @dataclass
63
- class UnifiedParameterInfo:
64
- """Unified parameter information that works for all parameter sources."""
65
- name: str
66
- param_type: Type
67
- default_value: Any
68
- is_required: bool
69
- description: Optional[str] = None
70
- source_type: str = "unknown" # "function", "dataclass", "nested"
71
-
72
- @classmethod
73
- def from_parameter_info(cls, param_info: ParameterInfo, source_type: str = "function") -> "UnifiedParameterInfo":
74
- """Convert from existing ParameterInfo to unified format."""
75
- return cls(
76
- name=param_info.name,
77
- param_type=param_info.param_type,
78
- default_value=param_info.default_value,
79
- is_required=param_info.is_required,
80
- description=param_info.description,
81
- source_type=source_type
82
- )
83
-
84
-
85
61
  class UnifiedParameterTargetAnalyzer(ABC, metaclass=AutoRegisterMeta):
86
62
  """Nominal target-kind family for unified parameter analysis."""
87
63
 
@@ -91,7 +67,7 @@ class UnifiedParameterTargetAnalyzer(ABC, metaclass=AutoRegisterMeta):
91
67
  target_kind: ClassVar[Optional[str]] = None
92
68
 
93
69
  @classmethod
94
- def analyze_target(cls, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
70
+ def analyze_target(cls, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
95
71
  """Analyze a target using the first registered target-kind analyzer."""
96
72
  for analyzer_cls in cls.__registry__.values():
97
73
  analyzer = analyzer_cls()
@@ -104,7 +80,7 @@ class UnifiedParameterTargetAnalyzer(ABC, metaclass=AutoRegisterMeta):
104
80
  """Return whether this analyzer owns the target."""
105
81
 
106
82
  @abstractmethod
107
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
83
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
108
84
  """Analyze the target."""
109
85
 
110
86
 
@@ -116,7 +92,7 @@ class CallableTargetAnalyzer(UnifiedParameterTargetAnalyzer):
116
92
  def matches(self, target: Union[Callable, Type, object]) -> bool:
117
93
  return callable(target) and not inspect.isclass(target)
118
94
 
119
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
95
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
120
96
  return UnifiedParameterAnalyzer._analyze_callable(target)
121
97
 
122
98
 
@@ -128,7 +104,7 @@ class DataclassTypeTargetAnalyzer(UnifiedParameterTargetAnalyzer):
128
104
  def matches(self, target: Union[Callable, Type, object]) -> bool:
129
105
  return inspect.isclass(target) and dataclasses.is_dataclass(target)
130
106
 
131
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
107
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
132
108
  return UnifiedParameterAnalyzer._analyze_dataclass_type(target)
133
109
 
134
110
 
@@ -140,7 +116,7 @@ class ClassTargetAnalyzer(UnifiedParameterTargetAnalyzer):
140
116
  def matches(self, target: Union[Callable, Type, object]) -> bool:
141
117
  return inspect.isclass(target)
142
118
 
143
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
119
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
144
120
  return UnifiedParameterAnalyzer._analyze_callable(target.__init__)
145
121
 
146
122
 
@@ -152,7 +128,7 @@ class DataclassInstanceTargetAnalyzer(UnifiedParameterTargetAnalyzer):
152
128
  def matches(self, target: Union[Callable, Type, object]) -> bool:
153
129
  return dataclasses.is_dataclass(target)
154
130
 
155
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
131
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
156
132
  return UnifiedParameterAnalyzer._analyze_dataclass_instance(target)
157
133
 
158
134
 
@@ -164,7 +140,7 @@ class ObjectInstanceTargetAnalyzer(UnifiedParameterTargetAnalyzer):
164
140
  def matches(self, target: Union[Callable, Type, object]) -> bool:
165
141
  return True
166
142
 
167
- def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, UnifiedParameterInfo]:
143
+ def analyze(self, target: Union[Callable, Type, object]) -> Dict[str, ParameterInfo]:
168
144
  return UnifiedParameterAnalyzer._analyze_object_instance(target)
169
145
 
170
146
 
@@ -177,7 +153,7 @@ class UnifiedParameterAnalyzer:
177
153
  """
178
154
 
179
155
  @staticmethod
180
- def analyze(target: Union[Callable, Type, object], exclude_params: Optional[list] = None) -> Dict[str, UnifiedParameterInfo]:
156
+ def analyze(target: Union[Callable, Type, object], exclude_params: Optional[list] = None) -> Dict[str, ParameterInfo]:
181
157
  """Analyze parameters from any source.
182
158
 
183
159
  Args:
@@ -185,7 +161,7 @@ class UnifiedParameterAnalyzer:
185
161
  exclude_params: Optional list of parameter names to exclude from analysis
186
162
 
187
163
  Returns:
188
- Dictionary mapping parameter names to UnifiedParameterInfo objects
164
+ Dictionary mapping parameter names to ParameterInfo objects
189
165
 
190
166
  Examples:
191
167
  # Function analysis
@@ -229,40 +205,17 @@ class UnifiedParameterAnalyzer:
229
205
  return frozenset(names)
230
206
 
231
207
  @staticmethod
232
- def _analyze_callable(callable_obj: Callable) -> Dict[str, UnifiedParameterInfo]:
208
+ def _analyze_callable(callable_obj: Callable) -> Dict[str, ParameterInfo]:
233
209
  """Analyze a callable (function, method, etc.)."""
234
- # Use existing SignatureAnalyzer for callables
235
- param_info_dict = SignatureAnalyzer.analyze(callable_obj)
236
-
237
- # Convert to unified format
238
- unified_params = {}
239
- for name, param_info in param_info_dict.items():
240
- unified_params[name] = UnifiedParameterInfo.from_parameter_info(
241
- param_info,
242
- source_type="function"
243
- )
244
-
245
- return unified_params
246
-
210
+ return SignatureAnalyzer.analyze(callable_obj)
211
+
247
212
  @staticmethod
248
- def _analyze_dataclass_type(dataclass_type: Type) -> Dict[str, UnifiedParameterInfo]:
249
- """Analyze a dataclass type using existing SignatureAnalyzer infrastructure."""
250
- # CRITICAL FIX: Use existing SignatureAnalyzer._analyze_dataclass method
251
- # which already handles all the docstring extraction properly
252
- param_info_dict = SignatureAnalyzer._analyze_dataclass(dataclass_type)
253
-
254
- # Convert to unified format
255
- unified_params = {}
256
- for name, param_info in param_info_dict.items():
257
- unified_params[name] = UnifiedParameterInfo.from_parameter_info(
258
- param_info,
259
- source_type="dataclass"
260
- )
261
-
262
- return unified_params
213
+ def _analyze_dataclass_type(dataclass_type: Type) -> Dict[str, ParameterInfo]:
214
+ """Analyze a dataclass through its authoritative field declarations."""
215
+ return SignatureAnalyzer._analyze_dataclass(dataclass_type)
263
216
 
264
217
  @staticmethod
265
- def _analyze_object_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
218
+ def _analyze_object_instance(instance: object) -> Dict[str, ParameterInfo]:
266
219
  """Analyze a regular object instance by examining its full inheritance hierarchy.
267
220
 
268
221
  For dynamic containers like SimpleNamespace (which use **kwargs in __init__),
@@ -295,8 +248,9 @@ class UnifiedParameterAnalyzer:
295
248
  class_params = UnifiedParameterAnalyzer._analyze_callable(cls.__init__)
296
249
 
297
250
  # Remove 'self' parameter
298
- if 'self' in class_params:
299
- del class_params['self']
251
+ class_params = {
252
+ name: info for name, info in class_params.items() if name != 'self'
253
+ }
300
254
 
301
255
  _logger.debug(f"🔧 _analyze_object_instance: cls={cls.__name__}, class_params after removing self={list(class_params.keys())}")
302
256
 
@@ -312,14 +266,7 @@ class UnifiedParameterAnalyzer:
312
266
  # Add parameters that haven't been seen yet (most specific wins)
313
267
  for param_name, param_info in class_params.items():
314
268
  if param_name not in all_params and param_name != 'kwargs':
315
- all_params[param_name] = UnifiedParameterInfo(
316
- name=param_name,
317
- param_type=param_info.param_type,
318
- default_value=param_info.default_value,
319
- is_required=param_info.is_required,
320
- description=param_info.description,
321
- source_type="object_instance"
322
- )
269
+ all_params[param_name] = param_info
323
270
 
324
271
  except Exception:
325
272
  # Skip classes that can't be analyzed - this is legitimate since some classes
@@ -337,67 +284,21 @@ class UnifiedParameterAnalyzer:
337
284
  continue
338
285
  # Infer type from value
339
286
  attr_type = type(attr_value) if attr_value is not None else type(None)
340
- all_params[attr_name] = UnifiedParameterInfo(
287
+ all_params[attr_name] = ParameterInfo(
341
288
  name=attr_name,
342
289
  param_type=attr_type,
343
290
  default_value=attr_value,
344
291
  is_required=False,
345
- description=None,
346
- source_type="dynamic_attr"
292
+ description=None
347
293
  )
348
294
  _logger.debug(f"🔧 _analyze_object_instance: after fallback, all_params={list(all_params.keys())}")
349
295
 
350
296
  return all_params
351
297
 
352
298
  @staticmethod
353
- def _analyze_dataclass_instance(instance: object) -> Dict[str, UnifiedParameterInfo]:
299
+ def _analyze_dataclass_instance(instance: object) -> Dict[str, ParameterInfo]:
354
300
  """Analyze a dataclass instance.
355
301
 
356
302
  Uses current instance values as defaults.
357
303
  """
358
- param_info_dict = SignatureAnalyzer.analyze(instance)
359
- return {
360
- name: UnifiedParameterInfo.from_parameter_info(
361
- param_info,
362
- source_type="dataclass_instance",
363
- )
364
- for name, param_info in param_info_dict.items()
365
- }
366
-
367
- @staticmethod
368
- def analyze_nested(
369
- target: Union[Callable, Type, object],
370
- parent_info: Dict[str, UnifiedParameterInfo] = None,
371
- ) -> Dict[str, UnifiedParameterInfo]:
372
- """Analyze parameters with nested dataclass support.
373
-
374
- This method provides enhanced analysis that can handle nested dataclasses
375
- and maintain parent context information.
376
-
377
- Args:
378
- target: The target to analyze
379
- parent_info: Optional parent parameter information for context
380
-
381
- Returns:
382
- Dictionary of unified parameter information with nested support
383
- """
384
- base_params = UnifiedParameterAnalyzer.analyze(target)
385
-
386
- # For each parameter, check if it's a nested dataclass
387
- enhanced_params = {}
388
- for name, param_info in base_params.items():
389
- enhanced_params[name] = param_info
390
-
391
- # If this parameter is a dataclass, mark it as having nested structure
392
- if dataclasses.is_dataclass(param_info.param_type):
393
- # Update source type to indicate nesting capability
394
- enhanced_params[name] = UnifiedParameterInfo(
395
- name=param_info.name,
396
- param_type=param_info.param_type,
397
- default_value=param_info.default_value,
398
- is_required=param_info.is_required,
399
- description=param_info.description,
400
- source_type=f"{param_info.source_type}_nested"
401
- )
402
-
403
- return enhanced_params
304
+ return SignatureAnalyzer.analyze(instance)
@@ -14,12 +14,11 @@ from typing import (
14
14
  TypeVar,
15
15
  get_args,
16
16
  get_origin,
17
- get_type_hints,
18
17
  )
19
18
 
20
19
  from annotated_types import Ge, Gt, Interval, Le, Len, Lt, MaxLen, MinLen, Predicate
21
20
 
22
- from .annotation_types import is_union_type
21
+ from .annotation_types import is_union_type, resolved_class_annotations
23
22
 
24
23
 
25
24
  class AnnotationValidationError(ValueError):
@@ -86,7 +85,7 @@ def validate_annotated_dataclass(instance: object) -> None:
86
85
  f"got {owner_type.__name__}."
87
86
  )
88
87
 
89
- annotations = get_type_hints(owner_type, include_extras=True)
88
+ annotations = resolved_class_annotations(owner_type)
90
89
  for declared_field in fields(instance):
91
90
  annotation = annotations.get(declared_field.name, declared_field.type)
92
91
  value = object.__getattribute__(instance, declared_field.name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-introspect
3
- Version: 0.1.16
3
+ Version: 0.2.2
4
4
  Summary: Pure Python introspection toolkit for function signatures, dataclasses, and type hints
5
5
  Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
6
6
  License: MIT
@@ -24,6 +24,7 @@ Requires-Dist: annotated-types>=0.7.0
24
24
  Requires-Dist: metaclass-registry>=0.1.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11" and extra == "dev"
27
28
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
28
29
  Requires-Dist: ruff>=0.1.0; extra == "dev"
29
30
  Requires-Dist: black>=23.0; extra == "dev"
@@ -94,3 +95,19 @@ changes are checked by the repository's [documentation
94
95
  workflow](https://github.com/OpenHCSDev/python-introspect/actions/workflows/docs.yml);
95
96
  the local warnings-as-errors build command is documented in
96
97
  [`development.rst`](docs/source/development.rst).
98
+
99
+ ### Parameter declarations in 0.2
100
+
101
+ `UnifiedParameterAnalyzer` now returns the original `ParameterInfo` declarations
102
+ from `SignatureAnalyzer`. `UnifiedParameterInfo`, its `source_type` tag, and
103
+ `analyze_nested` were removed. Use `analyze` and the declared `param_type` for
104
+ parameter topology.
105
+
106
+ `ParameterInfo` retains its five public fields, constructor and `_replace`
107
+ operation, and is now a frozen declaration rather than a tuple. Tuple unpacking
108
+ and NamedTuple helpers are no longer supported. Dataclass descriptions are
109
+ derived on first help access; value/default overlays preserve that deferred
110
+ source, and renaming preserves the original field's help. Callable descriptions
111
+ still participate eagerly in inferred parameter types. A presentation extraction
112
+ error yields absent help without discarding valid types/defaults. Factory errors
113
+ remain uncached and retryable.
@@ -0,0 +1,19 @@
1
+ python_introspect/__init__.py,sha256=IHmyKt23hfkRVeRaVD9mDuoWAG7Q7BorK-cP4B9o0Eg,3985
2
+ python_introspect/annotation_types.py,sha256=KZDv0fUAuvVQDEj0U_cozA_31vl3MyNsOfSOcdo3aJk,7557
3
+ python_introspect/callable_declaration.py,sha256=O7i0dGJP5rKWSlwfmmqlQLJwNQC_oSTngImVy3idaVE,1409
4
+ python_introspect/dataclass_projection.py,sha256=531PIrAunWNuQBwnZDBRpDa0Gy8iK_F6B4ThwYqOhCU,10869
5
+ python_introspect/docstring_annotations.py,sha256=RE6AZPgz8aBEIIvUoVEHpraItZfjareKxYvuaZa1H8k,6129
6
+ python_introspect/enableable.py,sha256=Al6t1kpqbxZIvDb70XJPYEk1WgubGiWUQySNcBcLjrY,5253
7
+ python_introspect/environment_projection.py,sha256=irfR4B6aRSnqC3fYmWpf7rlL3C6QXdYmOvso0RvCF6A,4654
8
+ python_introspect/exceptions.py,sha256=TG8Vo2JdArBbL8hlHmfmt31LtEKtGAuFEh2Rh07DQHs,488
9
+ python_introspect/jsonable.py,sha256=j-BlM6bc4Xm313eStqx2jCpkoennm76SaJNJj64DW7Q,3914
10
+ python_introspect/public_api.py,sha256=8wkxQKmnxl_46U49Dzq4XXIOqmrYQOD0S0cl9bc-YFk,3738
11
+ python_introspect/runtime_parameter.py,sha256=Wf7S1GfnUWhkRzIvjhmoYz2WR0HT2K_MuFDcUEphmJc,3067
12
+ python_introspect/signature_analyzer.py,sha256=DlFaxudB-dANV0FD9MmLJWdvxBKobJbtEn5n6tda2JU,60926
13
+ python_introspect/unified_parameter_analyzer.py,sha256=Qw39mQsYDYq5zEiB6ALRoZqwQeZ4PJmlkFxpTaKkccg,12231
14
+ python_introspect/validation.py,sha256=vj3OwJFShA2n4Zg45rXj8qghsJxDe1JhND8gkuNbfLQ,10447
15
+ python_introspect-0.2.2.dist-info/licenses/LICENSE,sha256=xagEoeTAj1WT64RmyR3E6HH-eTGdgXN6gqPMUUt7L_Y,1070
16
+ python_introspect-0.2.2.dist-info/METADATA,sha256=rwnVJA2UW6z5V1pznJtSjYqb0-H9_Y55ynK00qji8SE,4677
17
+ python_introspect-0.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
18
+ python_introspect-0.2.2.dist-info/top_level.txt,sha256=TZq9Yj1LeXI7A96PDqf7ZbWGSB1zLlQDww0esC91bwY,18
19
+ python_introspect-0.2.2.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- python_introspect/__init__.py,sha256=NIhjamPBxGzCEI1RBGUbwgB8tvTYATnSZ4OdM7WgPWI,3518
2
- python_introspect/annotation_types.py,sha256=3BJoeailgc6gdY7sTfGOwgxHykseE1q94jaZtlt5C94,5346
3
- python_introspect/callable_declaration.py,sha256=O7i0dGJP5rKWSlwfmmqlQLJwNQC_oSTngImVy3idaVE,1409
4
- python_introspect/dataclass_projection.py,sha256=0r4ncFGwcNc1AwJaOt3N6AFnh-GKNpxFtgvgwO22X-o,10870
5
- python_introspect/docstring_annotations.py,sha256=RE6AZPgz8aBEIIvUoVEHpraItZfjareKxYvuaZa1H8k,6129
6
- python_introspect/enableable.py,sha256=Al6t1kpqbxZIvDb70XJPYEk1WgubGiWUQySNcBcLjrY,5253
7
- python_introspect/environment_projection.py,sha256=irfR4B6aRSnqC3fYmWpf7rlL3C6QXdYmOvso0RvCF6A,4654
8
- python_introspect/exceptions.py,sha256=TG8Vo2JdArBbL8hlHmfmt31LtEKtGAuFEh2Rh07DQHs,488
9
- python_introspect/runtime_parameter.py,sha256=Wf7S1GfnUWhkRzIvjhmoYz2WR0HT2K_MuFDcUEphmJc,3067
10
- python_introspect/signature_analyzer.py,sha256=fiWgNVFZyG02GiGnr8RJ7HWMv4oy6bctBt5mqSTwQVk,54471
11
- python_introspect/unified_parameter_analyzer.py,sha256=rFKrmX-zhUJZIGFEUejMdU_s4Bdrq0zJsTLq6MkwqxU,16284
12
- python_introspect/validation.py,sha256=Rvuni9Box1yJuRuqSly-jNypaS51ooXq92MRGD33e2Q,10448
13
- python_introspect-0.1.16.dist-info/licenses/LICENSE,sha256=xagEoeTAj1WT64RmyR3E6HH-eTGdgXN6gqPMUUt7L_Y,1070
14
- python_introspect-0.1.16.dist-info/METADATA,sha256=8FHNfnKDQy2tCCHZ3KAulNTLX0zr6SMxImBSwb_XN9I,3733
15
- python_introspect-0.1.16.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
16
- python_introspect-0.1.16.dist-info/top_level.txt,sha256=TZq9Yj1LeXI7A96PDqf7ZbWGSB1zLlQDww0esC91bwY,18
17
- python_introspect-0.1.16.dist-info/RECORD,,