python-introspect 0.1.14__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.
- python_introspect/__init__.py +25 -3
- python_introspect/annotation_types.py +46 -1
- python_introspect/dataclass_projection.py +28 -11
- python_introspect/jsonable.py +120 -0
- python_introspect/public_api.py +111 -0
- python_introspect/signature_analyzer.py +290 -147
- python_introspect/unified_parameter_analyzer.py +24 -123
- python_introspect/validation.py +2 -3
- {python_introspect-0.1.14.dist-info → python_introspect-0.2.2.dist-info}/METADATA +18 -1
- python_introspect-0.2.2.dist-info/RECORD +19 -0
- python_introspect-0.1.14.dist-info/RECORD +0 -17
- {python_introspect-0.1.14.dist-info → python_introspect-0.2.2.dist-info}/WHEEL +0 -0
- {python_introspect-0.1.14.dist-info → python_introspect-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {python_introspect-0.1.14.dist-info → python_introspect-0.2.2.dist-info}/top_level.txt +0 -0
python_introspect/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ Extensibility:
|
|
|
9
9
|
type resolution for framework-specific types (lazy configs, proxies, etc.)
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
__version__ = "0.
|
|
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")
|
|
@@ -26,7 +25,11 @@ def dataclass_from_mapping(
|
|
|
26
25
|
target_type: type[DataclassT],
|
|
27
26
|
values: Mapping[str, object],
|
|
28
27
|
) -> DataclassT:
|
|
29
|
-
"""Construct
|
|
28
|
+
"""Construct declared init fields and verify supplied constructor-owned fields.
|
|
29
|
+
|
|
30
|
+
Non-init fields belong to the dataclass's defaults or post-init behavior,
|
|
31
|
+
not the input mapping. If supplied, they must agree with that owner.
|
|
32
|
+
"""
|
|
30
33
|
|
|
31
34
|
if not isinstance(target_type, type) or not is_dataclass(target_type):
|
|
32
35
|
raise TypeError(
|
|
@@ -41,9 +44,7 @@ def dataclass_from_mapping(
|
|
|
41
44
|
f"got {non_text_keys!r}."
|
|
42
45
|
)
|
|
43
46
|
|
|
44
|
-
declared_fields =
|
|
45
|
-
declared_field for declared_field in fields(target_type) if declared_field.init
|
|
46
|
-
)
|
|
47
|
+
declared_fields = fields(target_type)
|
|
47
48
|
declared_names = {declared_field.name for declared_field in declared_fields}
|
|
48
49
|
extras = tuple(sorted(set(values) - declared_names))
|
|
49
50
|
if extras:
|
|
@@ -51,19 +52,20 @@ def dataclass_from_mapping(
|
|
|
51
52
|
f"{target_type.__name__} received undeclared field(s): {', '.join(extras)}."
|
|
52
53
|
)
|
|
53
54
|
|
|
54
|
-
annotations =
|
|
55
|
-
|
|
55
|
+
annotations = resolved_class_annotations(target_type)
|
|
56
|
+
decoded_values: dict[str, object] = {}
|
|
56
57
|
missing: list[str] = []
|
|
57
58
|
for declared_field in declared_fields:
|
|
58
59
|
if declared_field.name not in values:
|
|
59
60
|
if (
|
|
60
|
-
declared_field.
|
|
61
|
+
declared_field.init
|
|
62
|
+
and declared_field.default is MISSING
|
|
61
63
|
and declared_field.default_factory is MISSING
|
|
62
64
|
):
|
|
63
65
|
missing.append(declared_field.name)
|
|
64
66
|
continue
|
|
65
67
|
annotation = annotations.get(declared_field.name, declared_field.type)
|
|
66
|
-
|
|
68
|
+
decoded_values[declared_field.name] = _mapping_value_for_annotation(
|
|
67
69
|
annotation,
|
|
68
70
|
values[declared_field.name],
|
|
69
71
|
path=f"{target_type.__name__}.{declared_field.name}",
|
|
@@ -73,8 +75,23 @@ def dataclass_from_mapping(
|
|
|
73
75
|
f"{target_type.__name__} is missing required field(s): {', '.join(missing)}."
|
|
74
76
|
)
|
|
75
77
|
|
|
76
|
-
result = target_type(
|
|
78
|
+
result = target_type(
|
|
79
|
+
**{
|
|
80
|
+
declared_field.name: decoded_values[declared_field.name]
|
|
81
|
+
for declared_field in declared_fields
|
|
82
|
+
if declared_field.init and declared_field.name in decoded_values
|
|
83
|
+
}
|
|
84
|
+
)
|
|
77
85
|
validate_annotated_dataclass(result)
|
|
86
|
+
for declared_field in declared_fields:
|
|
87
|
+
if not declared_field.init and declared_field.name in decoded_values:
|
|
88
|
+
if decoded_values[declared_field.name] != object.__getattribute__(
|
|
89
|
+
result, declared_field.name
|
|
90
|
+
):
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"{target_type.__name__}.{declared_field.name} disagrees with "
|
|
93
|
+
"its constructed value."
|
|
94
|
+
)
|
|
78
95
|
return result
|
|
79
96
|
|
|
80
97
|
|
|
@@ -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)
|
|
@@ -9,10 +9,13 @@ resolvers to extend functionality without modifying this code.
|
|
|
9
9
|
|
|
10
10
|
import ast
|
|
11
11
|
import inspect
|
|
12
|
+
import sys
|
|
13
|
+
import linecache
|
|
14
|
+
from functools import lru_cache
|
|
12
15
|
import dataclasses
|
|
13
16
|
import re
|
|
14
17
|
from abc import ABC, abstractmethod
|
|
15
|
-
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
|
|
16
19
|
from weakref import WeakKeyDictionary
|
|
17
20
|
|
|
18
21
|
from dataclasses import dataclass, field
|
|
@@ -143,13 +146,71 @@ class AnalysisConstants:
|
|
|
143
146
|
CONSTANTS = AnalysisConstants()
|
|
144
147
|
|
|
145
148
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
|
|
148
157
|
name: str
|
|
149
158
|
param_type: type
|
|
150
159
|
default_value: Any
|
|
151
160
|
is_required: bool
|
|
152
|
-
|
|
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
|
+
|
|
153
214
|
|
|
154
215
|
class DocstringInfo(NamedTuple):
|
|
155
216
|
"""Information extracted from a docstring."""
|
|
@@ -434,12 +495,7 @@ class DocstringExtractor:
|
|
|
434
495
|
if not docstring:
|
|
435
496
|
return DocstringInfo(parameters={})
|
|
436
497
|
|
|
437
|
-
|
|
438
|
-
try:
|
|
439
|
-
return DocstringExtractor._parse_docstring_ast(actual_target, docstring)
|
|
440
|
-
except Exception:
|
|
441
|
-
# Fall back to regex-based parsing
|
|
442
|
-
return DocstringExtractor._parse_docstring(docstring)
|
|
498
|
+
return DocstringExtractor._parse_docstring(docstring)
|
|
443
499
|
|
|
444
500
|
@staticmethod
|
|
445
501
|
def _resolve_lazy_target(target: Union[Callable, type]) -> Union[Callable, type]:
|
|
@@ -448,38 +504,6 @@ class DocstringExtractor:
|
|
|
448
504
|
return target
|
|
449
505
|
return _resolve_type(target)
|
|
450
506
|
|
|
451
|
-
@staticmethod
|
|
452
|
-
def _parse_docstring_ast(target: Union[Callable, type], docstring: str) -> DocstringInfo:
|
|
453
|
-
"""Parse docstring using AST for more accurate extraction.
|
|
454
|
-
|
|
455
|
-
This method uses AST to parse the source code and extract docstring
|
|
456
|
-
information more accurately, especially for complex multiline descriptions.
|
|
457
|
-
"""
|
|
458
|
-
try:
|
|
459
|
-
# Get source code
|
|
460
|
-
source = inspect.getsource(target)
|
|
461
|
-
tree = ast.parse(source)
|
|
462
|
-
|
|
463
|
-
# Find the function/class node
|
|
464
|
-
for node in ast.walk(tree):
|
|
465
|
-
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
|
|
466
|
-
if ast.get_docstring(node) == docstring:
|
|
467
|
-
return DocstringExtractor._parse_ast_docstring(node, docstring)
|
|
468
|
-
|
|
469
|
-
# Fallback to regex parsing if AST parsing fails
|
|
470
|
-
return DocstringExtractor._parse_docstring(docstring)
|
|
471
|
-
|
|
472
|
-
except Exception:
|
|
473
|
-
# Fallback to regex parsing
|
|
474
|
-
return DocstringExtractor._parse_docstring(docstring)
|
|
475
|
-
|
|
476
|
-
@staticmethod
|
|
477
|
-
def _parse_ast_docstring(node: Union[ast.FunctionDef, ast.ClassDef], docstring: str) -> DocstringInfo:
|
|
478
|
-
"""Parse docstring from AST node with enhanced multiline support."""
|
|
479
|
-
# For now, use the improved regex parser
|
|
480
|
-
# This can be extended later with more sophisticated AST-based parsing
|
|
481
|
-
return DocstringExtractor._parse_docstring(docstring)
|
|
482
|
-
|
|
483
507
|
@staticmethod
|
|
484
508
|
def _parse_docstring(docstring: str) -> DocstringInfo:
|
|
485
509
|
"""Parse a docstring into structured components with improved multiline support.
|
|
@@ -747,6 +771,70 @@ class CallableAnalysisContext:
|
|
|
747
771
|
)
|
|
748
772
|
|
|
749
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
|
+
|
|
750
838
|
class SignatureAnalyzer:
|
|
751
839
|
"""Universal analyzer for extracting parameter information from any target."""
|
|
752
840
|
|
|
@@ -941,16 +1029,6 @@ class SignatureAnalyzer:
|
|
|
941
1029
|
except Exception:
|
|
942
1030
|
type_hints = inspect.get_annotations(dataclass_type, eval_str=False)
|
|
943
1031
|
|
|
944
|
-
# Extract docstring information from dataclass
|
|
945
|
-
docstring_info = DocstringExtractor.extract(dataclass_type)
|
|
946
|
-
|
|
947
|
-
# Extract inline field documentation using AST
|
|
948
|
-
inline_docs = SignatureAnalyzer._extract_inline_field_docs(dataclass_type)
|
|
949
|
-
|
|
950
|
-
# ENHANCEMENT: For dataclasses modified by decorators,
|
|
951
|
-
# also extract field documentation from the field types themselves
|
|
952
|
-
field_type_docs = SignatureAnalyzer._extract_field_type_docs(dataclass_type)
|
|
953
|
-
|
|
954
1032
|
parameters = {}
|
|
955
1033
|
|
|
956
1034
|
for field in dataclasses.fields(dataclass_type):
|
|
@@ -971,31 +1049,12 @@ class SignatureAnalyzer:
|
|
|
971
1049
|
default_value = None
|
|
972
1050
|
is_required = True
|
|
973
1051
|
|
|
974
|
-
# Get field description from multiple sources (priority order)
|
|
975
|
-
field_description = None
|
|
976
|
-
|
|
977
|
-
# 1. Field metadata (highest priority)
|
|
978
|
-
if 'description' in field.metadata:
|
|
979
|
-
field_description = field.metadata['description']
|
|
980
|
-
# 2. Inline documentation strings (from AST parsing)
|
|
981
|
-
elif field.name in inline_docs:
|
|
982
|
-
field_description = inline_docs[field.name]
|
|
983
|
-
# 3. Field type documentation (for decorator-modified classes)
|
|
984
|
-
elif field.name in field_type_docs:
|
|
985
|
-
field_description = field_type_docs[field.name]
|
|
986
|
-
# 4. Docstring parameters (fallback)
|
|
987
|
-
elif docstring_info.parameters and field.name in docstring_info.parameters:
|
|
988
|
-
field_description = docstring_info.parameters.get(field.name)
|
|
989
|
-
# 5. CRITICAL FIX: Use inheritance-aware field documentation extraction
|
|
990
|
-
else:
|
|
991
|
-
field_description = SignatureAnalyzer.extract_field_documentation(dataclass_type, field.name)
|
|
992
|
-
|
|
993
1052
|
parameters[field.name] = ParameterInfo(
|
|
994
1053
|
name=field.name,
|
|
995
1054
|
param_type=param_type,
|
|
996
1055
|
default_value=default_value,
|
|
997
1056
|
is_required=is_required,
|
|
998
|
-
|
|
1057
|
+
description_owner=dataclass_type,
|
|
999
1058
|
)
|
|
1000
1059
|
|
|
1001
1060
|
# PERFORMANCE: Cache the result to avoid re-parsing
|
|
@@ -1006,6 +1065,152 @@ class SignatureAnalyzer:
|
|
|
1006
1065
|
# Return empty dict on error (don't cache errors)
|
|
1007
1066
|
return {}
|
|
1008
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
|
+
|
|
1149
|
+
@staticmethod
|
|
1150
|
+
def prepare_dataclass_declaration(dataclass_type: type) -> None:
|
|
1151
|
+
"""Prepare one declaration's source closure without evaluating defaults."""
|
|
1152
|
+
SignatureAnalyzer.prepare_dataclass_declarations((dataclass_type,))
|
|
1153
|
+
|
|
1154
|
+
@staticmethod
|
|
1155
|
+
def _dataclass_source(dataclass_type: type) -> ClassSourceDeclaration:
|
|
1156
|
+
"""Read current loader source, deriving its class block by immutable content."""
|
|
1157
|
+
# Python 3.13+ locates class source from __firstlineno__ directly;
|
|
1158
|
+
# its native locator does not perform the repeated module AST traversal.
|
|
1159
|
+
if sys.version_info >= (3, 13):
|
|
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)
|
|
1168
|
+
target = inspect.unwrap(dataclass_type)
|
|
1169
|
+
if not inspect.isclass(target):
|
|
1170
|
+
source = inspect.getsource(target)
|
|
1171
|
+
return ClassSourceDeclaration.from_block(source, SignatureAnalyzer._module_source_tree(source))
|
|
1172
|
+
file = inspect.getsourcefile(target)
|
|
1173
|
+
if file:
|
|
1174
|
+
linecache.checkcache(file)
|
|
1175
|
+
else:
|
|
1176
|
+
file = inspect.getfile(target)
|
|
1177
|
+
if not (file.startswith('<') and file.endswith('>')):
|
|
1178
|
+
raise OSError('source code not available')
|
|
1179
|
+
module = inspect.getmodule(target, file)
|
|
1180
|
+
lines = linecache.getlines(file, module.__dict__) if module else linecache.getlines(file)
|
|
1181
|
+
if not lines:
|
|
1182
|
+
raise OSError('could not get source code')
|
|
1183
|
+
source = SignatureAnalyzer._qualified_class_source(''.join(lines), target.__qualname__)
|
|
1184
|
+
if source is None:
|
|
1185
|
+
raise OSError('could not find class definition')
|
|
1186
|
+
return source
|
|
1187
|
+
|
|
1188
|
+
@staticmethod
|
|
1189
|
+
@lru_cache(maxsize=32)
|
|
1190
|
+
def _module_source_tree(source: str) -> ast.AST:
|
|
1191
|
+
"""Parse current immutable module bytes once; never expose the retained AST."""
|
|
1192
|
+
return ast.parse(source)
|
|
1193
|
+
|
|
1194
|
+
@staticmethod
|
|
1195
|
+
@lru_cache(maxsize=256)
|
|
1196
|
+
def _qualified_class_source(source: str, qualname: str) -> Optional[ClassSourceDeclaration]:
|
|
1197
|
+
"""Use Python's qualified class/decorator locator on current source content."""
|
|
1198
|
+
tree = SignatureAnalyzer._module_source_tree(source)
|
|
1199
|
+
finder = inspect._ClassFinder(qualname)
|
|
1200
|
+
try:
|
|
1201
|
+
finder.visit(tree)
|
|
1202
|
+
except inspect.ClassFoundException as found:
|
|
1203
|
+
lines = source.splitlines(keepends=True)
|
|
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)
|
|
1212
|
+
return None
|
|
1213
|
+
|
|
1009
1214
|
@staticmethod
|
|
1010
1215
|
def _extract_inline_field_docs(dataclass_type: type) -> Dict[str, str]:
|
|
1011
1216
|
"""Extract inline field documentation strings using AST parsing.
|
|
@@ -1033,84 +1238,22 @@ class SignatureAnalyzer:
|
|
|
1033
1238
|
field_name: str = "default"
|
|
1034
1239
|
"""
|
|
1035
1240
|
try:
|
|
1036
|
-
import ast
|
|
1037
|
-
import re
|
|
1038
|
-
|
|
1039
|
-
# Try to get source code - handle cases where it might not be available
|
|
1040
|
-
source = None
|
|
1041
1241
|
try:
|
|
1042
|
-
|
|
1242
|
+
declaration = SignatureAnalyzer._dataclass_source(dataclass_type)
|
|
1043
1243
|
except (OSError, TypeError):
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
tree = ast.parse(source)
|
|
1056
|
-
|
|
1057
|
-
# Find the class definition - be more flexible with class name matching
|
|
1058
|
-
class_node = None
|
|
1059
|
-
target_class_name = dataclass_type.__name__
|
|
1060
|
-
|
|
1061
|
-
# Handle cases where the class might have been renamed or modified
|
|
1062
|
-
for node in ast.walk(tree):
|
|
1063
|
-
if isinstance(node, ast.ClassDef):
|
|
1064
|
-
# Try exact match first
|
|
1065
|
-
if node.name == target_class_name:
|
|
1066
|
-
class_node = node
|
|
1067
|
-
break
|
|
1068
|
-
# Also try without common prefixes/suffixes that decorators might add
|
|
1069
|
-
|
|
1070
|
-
if not class_node:
|
|
1071
|
-
return {}
|
|
1072
|
-
|
|
1073
|
-
field_docs = {}
|
|
1074
|
-
source_lines = source.split('\n')
|
|
1075
|
-
|
|
1076
|
-
# Method 1: Look for field assignments followed by string literals (next line)
|
|
1077
|
-
for i, node in enumerate(class_node.body):
|
|
1078
|
-
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
1079
|
-
field_name = node.target.id
|
|
1080
|
-
|
|
1081
|
-
# Check if the next node is a string literal (documentation)
|
|
1082
|
-
if i + 1 < len(class_node.body):
|
|
1083
|
-
next_node = class_node.body[i + 1]
|
|
1084
|
-
if isinstance(next_node, ast.Expr):
|
|
1085
|
-
if isinstance(next_node.value, ast.Constant) and isinstance(next_node.value.value, str):
|
|
1086
|
-
field_docs[field_name] = next_node.value.value.strip()
|
|
1087
|
-
continue
|
|
1088
|
-
|
|
1089
|
-
# Method 2: Check for inline comments on the same line
|
|
1090
|
-
# Get the line number of the field definition
|
|
1091
|
-
field_line_num = node.lineno - 1 # Convert to 0-based indexing
|
|
1092
|
-
if 0 <= field_line_num < len(source_lines):
|
|
1093
|
-
line = source_lines[field_line_num]
|
|
1094
|
-
|
|
1095
|
-
# Look for string literals in comments on the same line
|
|
1096
|
-
# Pattern: field: type = value # """Documentation"""
|
|
1097
|
-
comment_match = re.search(r'#\s*["\']([^"\']+)["\']', line)
|
|
1098
|
-
if comment_match:
|
|
1099
|
-
field_docs[field_name] = comment_match.group(1).strip()
|
|
1100
|
-
continue
|
|
1101
|
-
|
|
1102
|
-
# Look for triple-quoted strings on the same line
|
|
1103
|
-
# Pattern: field: type = value """Documentation"""
|
|
1104
|
-
triple_quote_match = re.search(r'"""([^"]+)"""|\'\'\'([^\']+)\'\'\'', line)
|
|
1105
|
-
if triple_quote_match:
|
|
1106
|
-
doc_text = triple_quote_match.group(1) or triple_quote_match.group(2)
|
|
1107
|
-
field_docs[field_name] = doc_text.strip()
|
|
1108
|
-
|
|
1109
|
-
return field_docs
|
|
1110
|
-
|
|
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__)
|
|
1111
1255
|
except Exception:
|
|
1112
|
-
#
|
|
1113
|
-
# Could add logging here for debugging: logger.debug(f"AST parsing failed: {e}")
|
|
1256
|
+
# Preserve the extractor's unavailable/unparsable-source boundary.
|
|
1114
1257
|
return {}
|
|
1115
1258
|
|
|
1116
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
208
|
+
def _analyze_callable(callable_obj: Callable) -> Dict[str, ParameterInfo]:
|
|
233
209
|
"""Analyze a callable (function, method, etc.)."""
|
|
234
|
-
|
|
235
|
-
|
|
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,
|
|
249
|
-
"""Analyze a dataclass
|
|
250
|
-
|
|
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,
|
|
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
|
-
|
|
299
|
-
|
|
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] =
|
|
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] =
|
|
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,
|
|
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
|
-
|
|
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)
|
python_introspect/validation.py
CHANGED
|
@@ -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 =
|
|
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.
|
|
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=Xkk438372mA8C5CiG5oAOeuuClndjkTVEFmqKMOnTj0,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=gmYv-Oqk8q6zsz7dYauuVwIKfxOcOqOW2C3Oz4uAEWA,10105
|
|
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=c2AOVGdTZAlk1LpTCP34CO06Sm8IVBfYrAdF2eJ0jtc,53781
|
|
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.14.dist-info/licenses/LICENSE,sha256=xagEoeTAj1WT64RmyR3E6HH-eTGdgXN6gqPMUUt7L_Y,1070
|
|
14
|
-
python_introspect-0.1.14.dist-info/METADATA,sha256=YbKYZ-SeidB0KOEpV_s69yM_AXttxBng6MeKvUNMmF4,3733
|
|
15
|
-
python_introspect-0.1.14.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
16
|
-
python_introspect-0.1.14.dist-info/top_level.txt,sha256=TZq9Yj1LeXI7A96PDqf7ZbWGSB1zLlQDww0esC91bwY,18
|
|
17
|
-
python_introspect-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|