python-introspect 0.1.16__tar.gz → 0.2.2__tar.gz

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.
Files changed (39) hide show
  1. {python_introspect-0.1.16/src/python_introspect.egg-info → python_introspect-0.2.2}/PKG-INFO +18 -1
  2. {python_introspect-0.1.16 → python_introspect-0.2.2}/README.md +16 -0
  3. {python_introspect-0.1.16 → python_introspect-0.2.2}/pyproject.toml +2 -1
  4. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/__init__.py +25 -3
  5. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/annotation_types.py +46 -1
  6. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/dataclass_projection.py +2 -3
  7. python_introspect-0.2.2/src/python_introspect/jsonable.py +120 -0
  8. python_introspect-0.2.2/src/python_introspect/public_api.py +111 -0
  9. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/signature_analyzer.py +243 -116
  10. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/unified_parameter_analyzer.py +24 -123
  11. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/validation.py +2 -3
  12. {python_introspect-0.1.16 → python_introspect-0.2.2/src/python_introspect.egg-info}/PKG-INFO +18 -1
  13. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect.egg-info/SOURCES.txt +5 -0
  14. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect.egg-info/requires.txt +3 -0
  15. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_dataclass_projection.py +44 -1
  16. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_dataclass_source_preparation.py +80 -3
  17. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_init.py +0 -8
  18. python_introspect-0.2.2/tests/test_jsonable.py +130 -0
  19. python_introspect-0.2.2/tests/test_public_api.py +93 -0
  20. python_introspect-0.2.2/tests/test_release_readiness.py +22 -0
  21. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_unified_parameter_analyzer.py +4 -69
  22. {python_introspect-0.1.16 → python_introspect-0.2.2}/LICENSE +0 -0
  23. {python_introspect-0.1.16 → python_introspect-0.2.2}/setup.cfg +0 -0
  24. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/callable_declaration.py +0 -0
  25. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/docstring_annotations.py +0 -0
  26. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/enableable.py +0 -0
  27. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/environment_projection.py +0 -0
  28. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/exceptions.py +0 -0
  29. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect/runtime_parameter.py +0 -0
  30. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  31. {python_introspect-0.1.16 → python_introspect-0.2.2}/src/python_introspect.egg-info/top_level.txt +0 -0
  32. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_annotation_types.py +0 -0
  33. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_callable_declaration.py +0 -0
  34. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_environment_projection.py +0 -0
  35. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_exceptions.py +0 -0
  36. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_runtime_parameter.py +0 -0
  37. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_signature_analyzer.py +0 -0
  38. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_validate_python_release_action.py +0 -0
  39. {python_introspect-0.1.16 → python_introspect-0.2.2}/tests/test_validation.py +0 -0
@@ -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.
@@ -58,3 +58,19 @@ changes are checked by the repository's [documentation
58
58
  workflow](https://github.com/OpenHCSDev/python-introspect/actions/workflows/docs.yml);
59
59
  the local warnings-as-errors build command is documented in
60
60
  [`development.rst`](docs/source/development.rst).
61
+
62
+ ### Parameter declarations in 0.2
63
+
64
+ `UnifiedParameterAnalyzer` now returns the original `ParameterInfo` declarations
65
+ from `SignatureAnalyzer`. `UnifiedParameterInfo`, its `source_type` tag, and
66
+ `analyze_nested` were removed. Use `analyze` and the declared `param_type` for
67
+ parameter topology.
68
+
69
+ `ParameterInfo` retains its five public fields, constructor and `_replace`
70
+ operation, and is now a frozen declaration rather than a tuple. Tuple unpacking
71
+ and NamedTuple helpers are no longer supported. Dataclass descriptions are
72
+ derived on first help access; value/default overlays preserve that deferred
73
+ source, and renaming preserves the original field's help. Callable descriptions
74
+ still participate eagerly in inferred parameter types. A presentation extraction
75
+ error yields absent help without discarding valid types/defaults. Factory errors
76
+ remain uncached and retryable.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-introspect"
7
- version = "0.1.16"
7
+ version = "0.2.2"
8
8
  description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -33,6 +33,7 @@ dependencies = [
33
33
  [project.optional-dependencies]
34
34
  dev = [
35
35
  "pytest>=7.0",
36
+ "tomli>=2.0.1; python_version < '3.11'",
36
37
  "pytest-cov>=4.0",
37
38
  "ruff>=0.1.0",
38
39
  "black>=23.0",
@@ -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)