python-introspect 0.1.4__tar.gz → 0.1.6__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.6/PKG-INFO +91 -0
- python_introspect-0.1.6/README.md +55 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/pyproject.toml +9 -7
- python_introspect-0.1.6/src/python_introspect/__init__.py +118 -0
- python_introspect-0.1.6/src/python_introspect/annotation_types.py +87 -0
- python_introspect-0.1.6/src/python_introspect/dataclass_projection.py +269 -0
- python_introspect-0.1.6/src/python_introspect/enableable.py +156 -0
- python_introspect-0.1.6/src/python_introspect/environment_projection.py +130 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/signature_analyzer.py +590 -414
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/unified_parameter_analyzer.py +169 -56
- python_introspect-0.1.6/src/python_introspect/validation.py +276 -0
- python_introspect-0.1.6/src/python_introspect.egg-info/PKG-INFO +91 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/SOURCES.txt +9 -1
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/requires.txt +2 -0
- python_introspect-0.1.6/tests/test_annotation_types.py +46 -0
- python_introspect-0.1.6/tests/test_dataclass_projection.py +126 -0
- python_introspect-0.1.6/tests/test_environment_projection.py +103 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_init.py +5 -1
- {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_signature_analyzer.py +142 -4
- {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_unified_parameter_analyzer.py +67 -25
- python_introspect-0.1.6/tests/test_validation.py +127 -0
- python_introspect-0.1.4/PKG-INFO +0 -134
- python_introspect-0.1.4/README.md +0 -99
- python_introspect-0.1.4/src/python_introspect/__init__.py +0 -64
- python_introspect-0.1.4/src/python_introspect/enableable.py +0 -79
- python_introspect-0.1.4/src/python_introspect.egg-info/PKG-INFO +0 -134
- {python_introspect-0.1.4 → python_introspect-0.1.6}/LICENSE +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/setup.cfg +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_exceptions.py +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-introspect
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: Pure Python introspection toolkit for function signatures, dataclasses, and type hints
|
|
5
|
+
Author-email: Tristan Simas <tristan.simas@mail.mcgill.ca>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OpenHCSDev/python-introspect
|
|
8
|
+
Project-URL: Repository, https://github.com/OpenHCSDev/python-introspect
|
|
9
|
+
Project-URL: Issues, https://github.com/OpenHCSDev/python-introspect/issues
|
|
10
|
+
Keywords: introspection,reflection,signature,dataclass,type-hints,docstring
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: annotated-types>=0.7.0
|
|
24
|
+
Requires-Dist: metaclass-registry>=0.1.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
30
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
34
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# python-introspect
|
|
38
|
+
|
|
39
|
+
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
40
|
+
docstrings.
|
|
41
|
+
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
43
|
+
[](https://badge.fury.io/py/python-introspect)
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from python_introspect import SignatureAnalyzer
|
|
49
|
+
|
|
50
|
+
def resize(image, factor: float = 0.5, *, preserve_range: bool = True):
|
|
51
|
+
"""Resize an image.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
image: Input image.
|
|
55
|
+
factor: Scale factor.
|
|
56
|
+
preserve_range: Preserve the input intensity range.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
parameters = SignatureAnalyzer().analyze(resize)
|
|
60
|
+
|
|
61
|
+
for name, info in parameters.items():
|
|
62
|
+
print(name, info.param_type, info.default_value, info.is_required)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
``analyze`` is the unified entry point for functions, methods, classes,
|
|
66
|
+
dataclass types, and instances. It returns a mapping of names to
|
|
67
|
+
``ParameterInfo`` records.
|
|
68
|
+
|
|
69
|
+
## Extension points
|
|
70
|
+
|
|
71
|
+
Use ``register_namespace_provider`` to contribute names used while resolving
|
|
72
|
+
forward references and ``register_type_resolver`` to unwrap application proxy
|
|
73
|
+
types. Wrappers can declare their user-facing inspection target through the
|
|
74
|
+
signature-target helpers in ``python_introspect.signature_analyzer``.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install python-introspect
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The runtime depends on metaclass-registry. Repository and issues:
|
|
83
|
+
[OpenHCSDev/python-introspect](https://github.com/OpenHCSDev/python-introspect).
|
|
84
|
+
|
|
85
|
+
## Documentation
|
|
86
|
+
|
|
87
|
+
The maintained sources are in [`docs/source`](docs/source). Documentation
|
|
88
|
+
changes are checked by the repository's [documentation
|
|
89
|
+
workflow](https://github.com/OpenHCSDev/python-introspect/actions/workflows/docs.yml);
|
|
90
|
+
the local warnings-as-errors build command is documented in
|
|
91
|
+
[`development.rst`](docs/source/development.rst).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# python-introspect
|
|
2
|
+
|
|
3
|
+
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
4
|
+
docstrings.
|
|
5
|
+
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
[](https://badge.fury.io/py/python-introspect)
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from python_introspect import SignatureAnalyzer
|
|
13
|
+
|
|
14
|
+
def resize(image, factor: float = 0.5, *, preserve_range: bool = True):
|
|
15
|
+
"""Resize an image.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
image: Input image.
|
|
19
|
+
factor: Scale factor.
|
|
20
|
+
preserve_range: Preserve the input intensity range.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
parameters = SignatureAnalyzer().analyze(resize)
|
|
24
|
+
|
|
25
|
+
for name, info in parameters.items():
|
|
26
|
+
print(name, info.param_type, info.default_value, info.is_required)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
``analyze`` is the unified entry point for functions, methods, classes,
|
|
30
|
+
dataclass types, and instances. It returns a mapping of names to
|
|
31
|
+
``ParameterInfo`` records.
|
|
32
|
+
|
|
33
|
+
## Extension points
|
|
34
|
+
|
|
35
|
+
Use ``register_namespace_provider`` to contribute names used while resolving
|
|
36
|
+
forward references and ``register_type_resolver`` to unwrap application proxy
|
|
37
|
+
types. Wrappers can declare their user-facing inspection target through the
|
|
38
|
+
signature-target helpers in ``python_introspect.signature_analyzer``.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install python-introspect
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The runtime depends on metaclass-registry. Repository and issues:
|
|
47
|
+
[OpenHCSDev/python-introspect](https://github.com/OpenHCSDev/python-introspect).
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
The maintained sources are in [`docs/source`](docs/source). Documentation
|
|
52
|
+
changes are checked by the repository's [documentation
|
|
53
|
+
workflow](https://github.com/OpenHCSDev/python-introspect/actions/workflows/docs.yml);
|
|
54
|
+
the local warnings-as-errors build command is documented in
|
|
55
|
+
[`development.rst`](docs/source/development.rst).
|
|
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-introspect"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
8
8
|
description = "Pure Python introspection toolkit for function signatures, dataclasses, and type hints"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = {text = "MIT"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}
|
|
@@ -18,7 +18,6 @@ classifiers = [
|
|
|
18
18
|
"Intended Audience :: Developers",
|
|
19
19
|
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.9",
|
|
22
21
|
"Programming Language :: Python :: 3.10",
|
|
23
22
|
"Programming Language :: Python :: 3.11",
|
|
24
23
|
"Programming Language :: Python :: 3.12",
|
|
@@ -26,7 +25,10 @@ classifiers = [
|
|
|
26
25
|
"Topic :: Utilities",
|
|
27
26
|
]
|
|
28
27
|
|
|
29
|
-
dependencies = [
|
|
28
|
+
dependencies = [
|
|
29
|
+
"annotated-types>=0.7.0",
|
|
30
|
+
"metaclass-registry>=0.1.0",
|
|
31
|
+
]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
32
34
|
dev = [
|
|
@@ -58,14 +60,14 @@ python_functions = ["test_*"]
|
|
|
58
60
|
|
|
59
61
|
[tool.ruff]
|
|
60
62
|
line-length = 100
|
|
61
|
-
target-version = "
|
|
63
|
+
target-version = "py310"
|
|
62
64
|
|
|
63
65
|
[tool.black]
|
|
64
66
|
line-length = 100
|
|
65
|
-
target-version = ["
|
|
67
|
+
target-version = ["py310"]
|
|
66
68
|
|
|
67
69
|
[tool.mypy]
|
|
68
|
-
python_version = "
|
|
70
|
+
python_version = "3.10"
|
|
69
71
|
warn_return_any = true
|
|
70
72
|
warn_unused_configs = true
|
|
71
73
|
disallow_untyped_defs = true
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
python-introspect: Pure Python introspection toolkit
|
|
3
|
+
|
|
4
|
+
This package provides utilities for introspecting Python functions, methods,
|
|
5
|
+
dataclasses, and type hints.
|
|
6
|
+
|
|
7
|
+
Extensibility:
|
|
8
|
+
Use register_namespace_provider() and register_type_resolver() to extend
|
|
9
|
+
type resolution for framework-specific types (lazy configs, proxies, etc.)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.6"
|
|
13
|
+
|
|
14
|
+
from .signature_analyzer import (
|
|
15
|
+
SignatureAnalyzer,
|
|
16
|
+
ParameterInfo,
|
|
17
|
+
DocstringInfo,
|
|
18
|
+
DocstringExtractor,
|
|
19
|
+
# Plugin registration
|
|
20
|
+
register_namespace_provider,
|
|
21
|
+
register_type_resolver,
|
|
22
|
+
set_signature_analysis_target,
|
|
23
|
+
signature_analysis_target,
|
|
24
|
+
)
|
|
25
|
+
from .unified_parameter_analyzer import (
|
|
26
|
+
UnifiedParameterAnalyzer,
|
|
27
|
+
UnifiedParameterInfo,
|
|
28
|
+
add_parameter_exclusions,
|
|
29
|
+
set_parameter_exclusions,
|
|
30
|
+
parameter_exclusions,
|
|
31
|
+
)
|
|
32
|
+
from .exceptions import (
|
|
33
|
+
IntrospectionError,
|
|
34
|
+
SignatureAnalysisError,
|
|
35
|
+
DocstringParsingError,
|
|
36
|
+
TypeResolutionError,
|
|
37
|
+
)
|
|
38
|
+
from .enableable import (
|
|
39
|
+
Enableable,
|
|
40
|
+
is_enableable,
|
|
41
|
+
mark_enableable,
|
|
42
|
+
)
|
|
43
|
+
from .validation import (
|
|
44
|
+
AnnotationValidationError,
|
|
45
|
+
overlay_non_none_dataclass,
|
|
46
|
+
validate_annotated_dataclass,
|
|
47
|
+
validate_annotation_value,
|
|
48
|
+
)
|
|
49
|
+
from .dataclass_projection import (
|
|
50
|
+
dataclass_from_mapping,
|
|
51
|
+
project_dataclass,
|
|
52
|
+
)
|
|
53
|
+
from .environment_projection import (
|
|
54
|
+
EnvironmentVariable,
|
|
55
|
+
overlay_dataclass_from_environment,
|
|
56
|
+
)
|
|
57
|
+
from .annotation_types import (
|
|
58
|
+
enum_member_type,
|
|
59
|
+
get_enum_from_list,
|
|
60
|
+
is_enum_type,
|
|
61
|
+
is_list_of_enums,
|
|
62
|
+
is_union_type,
|
|
63
|
+
make_optional,
|
|
64
|
+
optional_member_type,
|
|
65
|
+
resolve_annotated,
|
|
66
|
+
resolve_optional,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
# Version
|
|
71
|
+
"__version__",
|
|
72
|
+
# Signature analysis
|
|
73
|
+
"SignatureAnalyzer",
|
|
74
|
+
"ParameterInfo",
|
|
75
|
+
"DocstringInfo",
|
|
76
|
+
"DocstringExtractor",
|
|
77
|
+
# Plugin registration
|
|
78
|
+
"register_namespace_provider",
|
|
79
|
+
"register_type_resolver",
|
|
80
|
+
"set_signature_analysis_target",
|
|
81
|
+
"signature_analysis_target",
|
|
82
|
+
# Unified analysis
|
|
83
|
+
"UnifiedParameterAnalyzer",
|
|
84
|
+
"UnifiedParameterInfo",
|
|
85
|
+
"add_parameter_exclusions",
|
|
86
|
+
"set_parameter_exclusions",
|
|
87
|
+
"parameter_exclusions",
|
|
88
|
+
# Exceptions
|
|
89
|
+
"IntrospectionError",
|
|
90
|
+
"SignatureAnalysisError",
|
|
91
|
+
"DocstringParsingError",
|
|
92
|
+
"TypeResolutionError",
|
|
93
|
+
# Enableable
|
|
94
|
+
"Enableable",
|
|
95
|
+
"is_enableable",
|
|
96
|
+
"mark_enableable",
|
|
97
|
+
# Runtime annotation validation
|
|
98
|
+
"AnnotationValidationError",
|
|
99
|
+
"overlay_non_none_dataclass",
|
|
100
|
+
"validate_annotated_dataclass",
|
|
101
|
+
"validate_annotation_value",
|
|
102
|
+
# Dataclass projection
|
|
103
|
+
"dataclass_from_mapping",
|
|
104
|
+
"project_dataclass",
|
|
105
|
+
# Environment projection
|
|
106
|
+
"EnvironmentVariable",
|
|
107
|
+
"overlay_dataclass_from_environment",
|
|
108
|
+
# Annotation type operations
|
|
109
|
+
"enum_member_type",
|
|
110
|
+
"get_enum_from_list",
|
|
111
|
+
"is_enum_type",
|
|
112
|
+
"is_list_of_enums",
|
|
113
|
+
"is_union_type",
|
|
114
|
+
"make_optional",
|
|
115
|
+
"optional_member_type",
|
|
116
|
+
"resolve_annotated",
|
|
117
|
+
"resolve_optional",
|
|
118
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Generic operations derived directly from Python type annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import types
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Annotated, Union, get_args, get_origin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_union_type(annotation: object) -> bool:
|
|
11
|
+
"""Return whether ``annotation`` is a typing or PEP 604 union."""
|
|
12
|
+
|
|
13
|
+
return get_origin(annotation) in {Union, types.UnionType}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def optional_member_type(annotation: object) -> object | None:
|
|
17
|
+
"""Return ``T`` only for the simple optional declaration ``T | None``."""
|
|
18
|
+
|
|
19
|
+
if not is_union_type(annotation):
|
|
20
|
+
return None
|
|
21
|
+
members = get_args(annotation)
|
|
22
|
+
if len(members) != 2 or type(None) not in members:
|
|
23
|
+
return None
|
|
24
|
+
return next(member for member in members if member is not type(None))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def make_optional(annotation: object) -> object:
|
|
28
|
+
"""Return the same declaration with ``None`` admitted exactly once."""
|
|
29
|
+
|
|
30
|
+
if is_union_type(annotation) and type(None) in get_args(annotation):
|
|
31
|
+
return annotation
|
|
32
|
+
return Union[annotation, type(None)]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_optional(annotation: object) -> object:
|
|
36
|
+
"""Resolve an optional declaration to its non-None member declaration."""
|
|
37
|
+
|
|
38
|
+
member_type = optional_member_type(annotation)
|
|
39
|
+
return annotation if member_type is None else member_type
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_annotated(annotation: object) -> object:
|
|
43
|
+
"""Resolve an ``Annotated[T, ...]`` declaration to its owned type ``T``."""
|
|
44
|
+
|
|
45
|
+
if get_origin(annotation) is Annotated:
|
|
46
|
+
return get_args(annotation)[0]
|
|
47
|
+
return annotation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_enum_type(annotation: object) -> bool:
|
|
51
|
+
"""Return whether the declaration is an enum type."""
|
|
52
|
+
|
|
53
|
+
return isinstance(annotation, type) and issubclass(annotation, Enum)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def enum_member_type(annotation: object) -> type[Enum] | None:
|
|
57
|
+
"""Return the enum type declared directly or as a simple optional."""
|
|
58
|
+
|
|
59
|
+
if is_enum_type(annotation):
|
|
60
|
+
return annotation
|
|
61
|
+
if not is_union_type(annotation):
|
|
62
|
+
return None
|
|
63
|
+
members = tuple(
|
|
64
|
+
member for member in get_args(annotation) if member is not type(None)
|
|
65
|
+
)
|
|
66
|
+
if len(members) != 1 or not is_enum_type(members[0]):
|
|
67
|
+
return None
|
|
68
|
+
return members[0]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_list_of_enums(annotation: object) -> bool:
|
|
72
|
+
"""Return whether the declaration is ``list[SomeEnum]``."""
|
|
73
|
+
|
|
74
|
+
members = get_args(annotation)
|
|
75
|
+
return (
|
|
76
|
+
get_origin(annotation) is list
|
|
77
|
+
and len(members) == 1
|
|
78
|
+
and is_enum_type(members[0])
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_enum_from_list(annotation: object) -> type[Enum] | None:
|
|
83
|
+
"""Return the enum type from ``list[SomeEnum]`` when declared."""
|
|
84
|
+
|
|
85
|
+
if not is_list_of_enums(annotation):
|
|
86
|
+
return None
|
|
87
|
+
return get_args(annotation)[0]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Dataclass construction and projection derived from declared annotations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import MISSING, fields, is_dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import (
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Literal,
|
|
13
|
+
TypeVar,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
16
|
+
get_type_hints,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from .annotation_types import is_union_type
|
|
20
|
+
from .validation import validate_annotated_dataclass, validate_annotation_value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DataclassT = TypeVar("DataclassT")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dataclass_from_mapping(
|
|
27
|
+
target_type: type[DataclassT],
|
|
28
|
+
values: Mapping[str, object],
|
|
29
|
+
) -> DataclassT:
|
|
30
|
+
"""Construct one dataclass from the fields declared by its class."""
|
|
31
|
+
|
|
32
|
+
if not isinstance(target_type, type) or not is_dataclass(target_type):
|
|
33
|
+
raise TypeError(
|
|
34
|
+
"dataclass_from_mapping requires a dataclass type; "
|
|
35
|
+
f"got {target_type!r}."
|
|
36
|
+
)
|
|
37
|
+
if not isinstance(values, Mapping):
|
|
38
|
+
raise TypeError("dataclass_from_mapping requires a mapping.")
|
|
39
|
+
non_text_keys = tuple(key for key in values if not isinstance(key, str))
|
|
40
|
+
if non_text_keys:
|
|
41
|
+
raise TypeError(
|
|
42
|
+
f"{target_type.__name__} field names must be strings; "
|
|
43
|
+
f"got {non_text_keys!r}."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
declared_fields = tuple(
|
|
47
|
+
declared_field for declared_field in fields(target_type) if declared_field.init
|
|
48
|
+
)
|
|
49
|
+
declared_names = {declared_field.name for declared_field in declared_fields}
|
|
50
|
+
extras = tuple(sorted(set(values) - declared_names))
|
|
51
|
+
if extras:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"{target_type.__name__} received undeclared field(s): {', '.join(extras)}."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
annotations = get_type_hints(target_type, include_extras=True)
|
|
57
|
+
constructor_values: dict[str, object] = {}
|
|
58
|
+
missing: list[str] = []
|
|
59
|
+
for declared_field in declared_fields:
|
|
60
|
+
if declared_field.name not in values:
|
|
61
|
+
if declared_field.default is MISSING and declared_field.default_factory is MISSING:
|
|
62
|
+
missing.append(declared_field.name)
|
|
63
|
+
continue
|
|
64
|
+
annotation = annotations.get(declared_field.name, declared_field.type)
|
|
65
|
+
constructor_values[declared_field.name] = _mapping_value_for_annotation(
|
|
66
|
+
annotation,
|
|
67
|
+
values[declared_field.name],
|
|
68
|
+
path=f"{target_type.__name__}.{declared_field.name}",
|
|
69
|
+
)
|
|
70
|
+
if missing:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"{target_type.__name__} is missing required field(s): {', '.join(missing)}."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
result = target_type(**constructor_values)
|
|
76
|
+
validate_annotated_dataclass(result)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def project_dataclass(
|
|
81
|
+
target_type: type[DataclassT],
|
|
82
|
+
source: object,
|
|
83
|
+
**overrides: object,
|
|
84
|
+
) -> DataclassT:
|
|
85
|
+
"""Project shared declared fields from one dataclass into another."""
|
|
86
|
+
|
|
87
|
+
if not isinstance(target_type, type) or not is_dataclass(target_type):
|
|
88
|
+
raise TypeError(
|
|
89
|
+
"project_dataclass requires a dataclass target type; "
|
|
90
|
+
f"got {target_type!r}."
|
|
91
|
+
)
|
|
92
|
+
if not is_dataclass(source) or isinstance(source, type):
|
|
93
|
+
raise TypeError("project_dataclass requires a dataclass source instance.")
|
|
94
|
+
|
|
95
|
+
target_fields = tuple(
|
|
96
|
+
declared_field for declared_field in fields(target_type) if declared_field.init
|
|
97
|
+
)
|
|
98
|
+
target_names = {declared_field.name for declared_field in target_fields}
|
|
99
|
+
invalid_overrides = tuple(sorted(set(overrides) - target_names))
|
|
100
|
+
if invalid_overrides:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"{target_type.__name__} received undeclared override(s): "
|
|
103
|
+
f"{', '.join(invalid_overrides)}."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
source_values = {
|
|
107
|
+
declared_field.name: object.__getattribute__(source, declared_field.name)
|
|
108
|
+
for declared_field in fields(source)
|
|
109
|
+
}
|
|
110
|
+
constructor_values = {
|
|
111
|
+
declared_field.name: source_values[declared_field.name]
|
|
112
|
+
for declared_field in target_fields
|
|
113
|
+
if declared_field.name in source_values
|
|
114
|
+
}
|
|
115
|
+
constructor_values.update(overrides)
|
|
116
|
+
result = target_type(**constructor_values)
|
|
117
|
+
validate_annotated_dataclass(result)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _mapping_value_for_annotation(
|
|
122
|
+
annotation: object,
|
|
123
|
+
value: object,
|
|
124
|
+
*,
|
|
125
|
+
path: str,
|
|
126
|
+
) -> object:
|
|
127
|
+
origin = get_origin(annotation)
|
|
128
|
+
if origin is Annotated:
|
|
129
|
+
base_type = get_args(annotation)[0]
|
|
130
|
+
converted = _mapping_value_for_annotation(base_type, value, path=path)
|
|
131
|
+
validate_annotation_value(annotation, converted, path=path)
|
|
132
|
+
return converted
|
|
133
|
+
if annotation is Any:
|
|
134
|
+
return value
|
|
135
|
+
if annotation is tuple:
|
|
136
|
+
if not isinstance(value, (list, tuple)):
|
|
137
|
+
raise TypeError(f"{path} must be an array.")
|
|
138
|
+
return tuple(value)
|
|
139
|
+
if is_union_type(annotation):
|
|
140
|
+
successes: list[object] = []
|
|
141
|
+
errors: list[Exception] = []
|
|
142
|
+
for member_type in get_args(annotation):
|
|
143
|
+
try:
|
|
144
|
+
converted = _mapping_value_for_annotation(
|
|
145
|
+
member_type,
|
|
146
|
+
value,
|
|
147
|
+
path=path,
|
|
148
|
+
)
|
|
149
|
+
validate_annotation_value(member_type, converted, path=path)
|
|
150
|
+
except (TypeError, ValueError) as error:
|
|
151
|
+
errors.append(error)
|
|
152
|
+
continue
|
|
153
|
+
successes.append(converted)
|
|
154
|
+
if len(successes) == 1:
|
|
155
|
+
return successes[0]
|
|
156
|
+
exact_matches = tuple(
|
|
157
|
+
converted
|
|
158
|
+
for converted in successes
|
|
159
|
+
if type(converted) is type(value)
|
|
160
|
+
)
|
|
161
|
+
if len(exact_matches) == 1:
|
|
162
|
+
return exact_matches[0]
|
|
163
|
+
if successes:
|
|
164
|
+
raise TypeError(
|
|
165
|
+
f"{path} ambiguously matches multiple members of {annotation!r}."
|
|
166
|
+
)
|
|
167
|
+
value_errors = tuple(
|
|
168
|
+
error for error in errors if isinstance(error, ValueError)
|
|
169
|
+
)
|
|
170
|
+
if len(value_errors) == 1:
|
|
171
|
+
raise value_errors[0]
|
|
172
|
+
if value_errors:
|
|
173
|
+
details = "; ".join(str(error) for error in value_errors)
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"{path} does not match any constrained union member: {details}"
|
|
176
|
+
)
|
|
177
|
+
detail = f" Last error: {errors[-1]}" if errors else ""
|
|
178
|
+
raise TypeError(f"{path} does not match its declared union.{detail}") from (
|
|
179
|
+
errors[-1] if errors else None
|
|
180
|
+
)
|
|
181
|
+
if origin is Literal:
|
|
182
|
+
validate_annotation_value(annotation, value, path=path)
|
|
183
|
+
return value
|
|
184
|
+
if origin is tuple:
|
|
185
|
+
if not isinstance(value, (list, tuple)):
|
|
186
|
+
raise TypeError(f"{path} must be an array.")
|
|
187
|
+
member_types = get_args(annotation)
|
|
188
|
+
if not member_types:
|
|
189
|
+
return tuple(value)
|
|
190
|
+
if len(member_types) == 2 and member_types[1] is Ellipsis:
|
|
191
|
+
return tuple(
|
|
192
|
+
_mapping_value_for_annotation(
|
|
193
|
+
member_types[0],
|
|
194
|
+
item,
|
|
195
|
+
path=f"{path}[{index}]",
|
|
196
|
+
)
|
|
197
|
+
for index, item in enumerate(value)
|
|
198
|
+
)
|
|
199
|
+
if member_types and len(value) != len(member_types):
|
|
200
|
+
raise ValueError(f"{path} must contain {len(member_types)} item(s); got {len(value)}.")
|
|
201
|
+
return tuple(
|
|
202
|
+
_mapping_value_for_annotation(
|
|
203
|
+
member_type,
|
|
204
|
+
item,
|
|
205
|
+
path=f"{path}[{index}]",
|
|
206
|
+
)
|
|
207
|
+
for index, (member_type, item) in enumerate(zip(member_types, value))
|
|
208
|
+
)
|
|
209
|
+
if origin is list:
|
|
210
|
+
if not isinstance(value, list):
|
|
211
|
+
raise TypeError(f"{path} must be an array.")
|
|
212
|
+
member_types = get_args(annotation)
|
|
213
|
+
if not member_types:
|
|
214
|
+
return list(value)
|
|
215
|
+
return [
|
|
216
|
+
_mapping_value_for_annotation(
|
|
217
|
+
member_types[0],
|
|
218
|
+
item,
|
|
219
|
+
path=f"{path}[{index}]",
|
|
220
|
+
)
|
|
221
|
+
for index, item in enumerate(value)
|
|
222
|
+
]
|
|
223
|
+
if origin in {dict, Mapping}:
|
|
224
|
+
if not isinstance(value, Mapping):
|
|
225
|
+
raise TypeError(f"{path} must be an object.")
|
|
226
|
+
member_types = get_args(annotation)
|
|
227
|
+
if len(member_types) != 2:
|
|
228
|
+
return dict(value)
|
|
229
|
+
key_type, item_type = member_types
|
|
230
|
+
return {
|
|
231
|
+
_mapping_value_for_annotation(
|
|
232
|
+
key_type,
|
|
233
|
+
key,
|
|
234
|
+
path=f"{path}.key",
|
|
235
|
+
): _mapping_value_for_annotation(
|
|
236
|
+
item_type,
|
|
237
|
+
item,
|
|
238
|
+
path=f"{path}[{key!r}]",
|
|
239
|
+
)
|
|
240
|
+
for key, item in value.items()
|
|
241
|
+
}
|
|
242
|
+
if annotation is type(None):
|
|
243
|
+
if value is not None:
|
|
244
|
+
raise TypeError(f"{path} must be null.")
|
|
245
|
+
return None
|
|
246
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
247
|
+
if isinstance(value, annotation):
|
|
248
|
+
return value
|
|
249
|
+
try:
|
|
250
|
+
return annotation(value)
|
|
251
|
+
except ValueError as error:
|
|
252
|
+
choices = tuple(member.value for member in annotation)
|
|
253
|
+
raise ValueError(f"{path} must be one of {choices!r}; got {value!r}.") from error
|
|
254
|
+
if isinstance(annotation, type) and is_dataclass(annotation):
|
|
255
|
+
if isinstance(value, annotation):
|
|
256
|
+
return value
|
|
257
|
+
if not isinstance(value, Mapping):
|
|
258
|
+
raise TypeError(f"{path} must be an object.")
|
|
259
|
+
return dataclass_from_mapping(annotation, value)
|
|
260
|
+
if annotation is Path:
|
|
261
|
+
if isinstance(value, Path):
|
|
262
|
+
return value
|
|
263
|
+
if not isinstance(value, str):
|
|
264
|
+
raise TypeError(f"{path} must be a path string.")
|
|
265
|
+
return Path(value)
|
|
266
|
+
if annotation is float and type(value) is int:
|
|
267
|
+
return float(value)
|
|
268
|
+
validate_annotation_value(annotation, value, path=path)
|
|
269
|
+
return value
|