python-introspect 0.1.14__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.
- {python_introspect-0.1.14/src/python_introspect.egg-info → python_introspect-0.2.2}/PKG-INFO +18 -1
- {python_introspect-0.1.14 → python_introspect-0.2.2}/README.md +16 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/pyproject.toml +2 -1
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/__init__.py +25 -3
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/annotation_types.py +46 -1
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/dataclass_projection.py +28 -11
- python_introspect-0.2.2/src/python_introspect/jsonable.py +120 -0
- python_introspect-0.2.2/src/python_introspect/public_api.py +111 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/signature_analyzer.py +290 -147
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/unified_parameter_analyzer.py +24 -123
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/validation.py +2 -3
- {python_introspect-0.1.14 → python_introspect-0.2.2/src/python_introspect.egg-info}/PKG-INFO +18 -1
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect.egg-info/SOURCES.txt +6 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect.egg-info/requires.txt +3 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_dataclass_projection.py +121 -2
- python_introspect-0.2.2/tests/test_dataclass_source_preparation.py +291 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_init.py +2 -9
- python_introspect-0.2.2/tests/test_jsonable.py +130 -0
- python_introspect-0.2.2/tests/test_public_api.py +93 -0
- python_introspect-0.2.2/tests/test_release_readiness.py +22 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_unified_parameter_analyzer.py +4 -69
- {python_introspect-0.1.14 → python_introspect-0.2.2}/LICENSE +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/setup.cfg +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/callable_declaration.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/docstring_annotations.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/enableable.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/environment_projection.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/runtime_parameter.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_annotation_types.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_callable_declaration.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_environment_projection.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_exceptions.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_runtime_parameter.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_signature_analyzer.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_validate_python_release_action.py +0 -0
- {python_introspect-0.1.14 → python_introspect-0.2.2}/tests/test_validation.py +0 -0
{python_introspect-0.1.14/src/python_introspect.egg-info → python_introspect-0.2.2}/PKG-INFO
RENAMED
|
@@ -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.
|
|
@@ -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.
|
|
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.
|
|
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",
|
{python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/annotation_types.py
RENAMED
|
@@ -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:
|
{python_introspect-0.1.14 → python_introspect-0.2.2}/src/python_introspect/dataclass_projection.py
RENAMED
|
@@ -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)
|