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.
Files changed (32) hide show
  1. python_introspect-0.1.6/PKG-INFO +91 -0
  2. python_introspect-0.1.6/README.md +55 -0
  3. {python_introspect-0.1.4 → python_introspect-0.1.6}/pyproject.toml +9 -7
  4. python_introspect-0.1.6/src/python_introspect/__init__.py +118 -0
  5. python_introspect-0.1.6/src/python_introspect/annotation_types.py +87 -0
  6. python_introspect-0.1.6/src/python_introspect/dataclass_projection.py +269 -0
  7. python_introspect-0.1.6/src/python_introspect/enableable.py +156 -0
  8. python_introspect-0.1.6/src/python_introspect/environment_projection.py +130 -0
  9. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/signature_analyzer.py +590 -414
  10. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/unified_parameter_analyzer.py +169 -56
  11. python_introspect-0.1.6/src/python_introspect/validation.py +276 -0
  12. python_introspect-0.1.6/src/python_introspect.egg-info/PKG-INFO +91 -0
  13. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/SOURCES.txt +9 -1
  14. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/requires.txt +2 -0
  15. python_introspect-0.1.6/tests/test_annotation_types.py +46 -0
  16. python_introspect-0.1.6/tests/test_dataclass_projection.py +126 -0
  17. python_introspect-0.1.6/tests/test_environment_projection.py +103 -0
  18. {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_init.py +5 -1
  19. {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_signature_analyzer.py +142 -4
  20. {python_introspect-0.1.4 → python_introspect-0.1.6}/tests/test_unified_parameter_analyzer.py +67 -25
  21. python_introspect-0.1.6/tests/test_validation.py +127 -0
  22. python_introspect-0.1.4/PKG-INFO +0 -134
  23. python_introspect-0.1.4/README.md +0 -99
  24. python_introspect-0.1.4/src/python_introspect/__init__.py +0 -64
  25. python_introspect-0.1.4/src/python_introspect/enableable.py +0 -79
  26. python_introspect-0.1.4/src/python_introspect.egg-info/PKG-INFO +0 -134
  27. {python_introspect-0.1.4 → python_introspect-0.1.6}/LICENSE +0 -0
  28. {python_introspect-0.1.4 → python_introspect-0.1.6}/setup.cfg +0 -0
  29. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect/exceptions.py +0 -0
  30. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  31. {python_introspect-0.1.4 → python_introspect-0.1.6}/src/python_introspect.egg-info/top_level.txt +0 -0
  32. {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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
43
+ [![PyPI version](https://badge.fury.io/py/python-introspect.svg)](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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+ [![PyPI version](https://badge.fury.io/py/python-introspect.svg)](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.4"
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.9"
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 = "0.1.4"
63
+ target-version = "py310"
62
64
 
63
65
  [tool.black]
64
66
  line-length = 100
65
- target-version = ["py39"]
67
+ target-version = ["py310"]
66
68
 
67
69
  [tool.mypy]
68
- python_version = "0.1.4"
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