python-introspect 0.2.2__tar.gz → 0.2.3__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.2.2/src/python_introspect.egg-info → python_introspect-0.2.3}/PKG-INFO +1 -1
- {python_introspect-0.2.2 → python_introspect-0.2.3}/pyproject.toml +1 -1
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/__init__.py +4 -1
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/annotation_types.py +8 -1
- python_introspect-0.2.3/src/python_introspect/choices.py +65 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/validation.py +33 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3/src/python_introspect.egg-info}/PKG-INFO +1 -1
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/SOURCES.txt +2 -0
- python_introspect-0.2.3/tests/test_annotation_choices.py +61 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/LICENSE +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/README.md +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/setup.cfg +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/callable_declaration.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/dataclass_projection.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/docstring_annotations.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/enableable.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/environment_projection.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/jsonable.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/public_api.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/runtime_parameter.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/signature_analyzer.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/unified_parameter_analyzer.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/requires.txt +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_annotation_types.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_callable_declaration.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_dataclass_projection.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_dataclass_source_preparation.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_environment_projection.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_exceptions.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_init.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_jsonable.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_public_api.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_release_readiness.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_runtime_parameter.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_signature_analyzer.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_unified_parameter_analyzer.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_validate_python_release_action.py +0 -0
- {python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_validation.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-introspect"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
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"
|
|
@@ -9,7 +9,7 @@ Extensibility:
|
|
|
9
9
|
type resolution for framework-specific types (lazy configs, proxies, etc.)
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
__version__ = "0.2.
|
|
12
|
+
__version__ = "0.2.3"
|
|
13
13
|
|
|
14
14
|
from .signature_analyzer import (
|
|
15
15
|
SignatureAnalyzer,
|
|
@@ -39,6 +39,7 @@ from .enableable import (
|
|
|
39
39
|
is_enableable,
|
|
40
40
|
mark_enableable,
|
|
41
41
|
)
|
|
42
|
+
from .choices import AnnotationChoices, declared_annotation_choices
|
|
42
43
|
from .validation import (
|
|
43
44
|
AnnotatedDataclassValidationMixin,
|
|
44
45
|
AnnotationValidationError,
|
|
@@ -119,6 +120,8 @@ __all__ = [
|
|
|
119
120
|
"overlay_non_none_dataclass",
|
|
120
121
|
"validate_annotated_dataclass",
|
|
121
122
|
"validate_annotation_value",
|
|
123
|
+
"AnnotationChoices",
|
|
124
|
+
"declared_annotation_choices",
|
|
122
125
|
# Dataclass projection
|
|
123
126
|
"dataclass_from_mapping",
|
|
124
127
|
"project_dataclass",
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/annotation_types.py
RENAMED
|
@@ -143,9 +143,16 @@ def enum_input_values(annotation: object) -> tuple[str, ...]:
|
|
|
143
143
|
|
|
144
144
|
String-valued members expose their value. Members whose values are not
|
|
145
145
|
strings expose their name, which keeps JSON and command-line projections
|
|
146
|
-
unambiguous, including enums whose concrete value is ``None``.
|
|
146
|
+
unambiguous, including enums whose concrete value is ``None``. A field
|
|
147
|
+
declaring :class:`~python_introspect.choices.AnnotationChoices` exposes its
|
|
148
|
+
choice labels.
|
|
147
149
|
"""
|
|
148
150
|
|
|
151
|
+
from .choices import declared_annotation_choices
|
|
152
|
+
|
|
153
|
+
choices = declared_annotation_choices(annotation)
|
|
154
|
+
if choices is not None:
|
|
155
|
+
return tuple(choices.label(choice) for choice in choices.choices())
|
|
149
156
|
enum_type = declared_enum_type(annotation)
|
|
150
157
|
if enum_type is None:
|
|
151
158
|
return ()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Finite value sets declared as ``Annotated`` metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Annotated, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
from .annotation_types import is_union_type
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AnnotationChoices(ABC):
|
|
12
|
+
"""``Annotated`` metadata declaring the finite values a field may hold.
|
|
13
|
+
|
|
14
|
+
The declaring package owns the choice set, which may be computed when it is
|
|
15
|
+
asked for. Validation checks each value (or each item of a sequence value)
|
|
16
|
+
against it, and form builders render the choices with their labels.
|
|
17
|
+
``None`` is never a choice: an ``Optional`` annotation admits it separately.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def choices(self) -> tuple[object, ...]:
|
|
22
|
+
"""Return the admissible values in display order."""
|
|
23
|
+
|
|
24
|
+
def label(self, choice: object) -> str:
|
|
25
|
+
"""Return the display label for one admissible value."""
|
|
26
|
+
|
|
27
|
+
return str(choice)
|
|
28
|
+
|
|
29
|
+
def choice_for_label(self, label: str) -> object:
|
|
30
|
+
"""Decode one display label back to its admissible value."""
|
|
31
|
+
|
|
32
|
+
for choice in self.choices():
|
|
33
|
+
if self.label(choice) == label:
|
|
34
|
+
return choice
|
|
35
|
+
raise ValueError(f"{label!r} is not one of {[self.label(c) for c in self.choices()]}.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def declared_annotation_choices(annotation: object) -> AnnotationChoices | None:
|
|
39
|
+
"""Return the choice set declared on an annotation.
|
|
40
|
+
|
|
41
|
+
The metadata is found through ``Annotated``, ``Optional`` and homogeneous
|
|
42
|
+
containers, mirroring how enum declarations are recognised.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
if get_origin(annotation) is Annotated:
|
|
46
|
+
base, *metadata = get_args(annotation)
|
|
47
|
+
for item in metadata:
|
|
48
|
+
if isinstance(item, AnnotationChoices):
|
|
49
|
+
return item
|
|
50
|
+
return declared_annotation_choices(base)
|
|
51
|
+
members = get_args(annotation)
|
|
52
|
+
if is_union_type(annotation):
|
|
53
|
+
found = [
|
|
54
|
+
choices
|
|
55
|
+
for member in members
|
|
56
|
+
if member is not type(None)
|
|
57
|
+
if (choices := declared_annotation_choices(member)) is not None
|
|
58
|
+
]
|
|
59
|
+
return found[0] if len(found) == 1 else None
|
|
60
|
+
origin = get_origin(annotation)
|
|
61
|
+
if origin in {list, set, frozenset} and len(members) == 1:
|
|
62
|
+
return declared_annotation_choices(members[0])
|
|
63
|
+
if origin is tuple and len(members) == 2 and members[1] is Ellipsis:
|
|
64
|
+
return declared_annotation_choices(members[0])
|
|
65
|
+
return None
|
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
from annotated_types import Ge, Gt, Interval, Le, Len, Lt, MaxLen, MinLen, Predicate
|
|
20
20
|
|
|
21
21
|
from .annotation_types import is_union_type, resolved_class_annotations
|
|
22
|
+
from .choices import AnnotationChoices
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class AnnotationValidationError(ValueError):
|
|
@@ -146,6 +147,9 @@ def validate_annotation_value(
|
|
|
146
147
|
if origin in {dict, Mapping}:
|
|
147
148
|
_validate_mapping(annotation, value, path)
|
|
148
149
|
return
|
|
150
|
+
if origin is type:
|
|
151
|
+
_validate_class_value(annotation, value, path)
|
|
152
|
+
return
|
|
149
153
|
if origin is Callable:
|
|
150
154
|
if not callable(value):
|
|
151
155
|
raise TypeError(f"{path} must be callable; got {type(value).__name__}.")
|
|
@@ -205,6 +209,21 @@ def _validate_sequence(annotation: object, value: object, path: str) -> None:
|
|
|
205
209
|
validate_annotation_value(members[0], item, path=f"{path}[{index}]")
|
|
206
210
|
|
|
207
211
|
|
|
212
|
+
def _validate_class_value(annotation: object, value: object, path: str) -> None:
|
|
213
|
+
if not isinstance(value, type):
|
|
214
|
+
raise TypeError(f"{path} must be a class; got {type(value).__name__}.")
|
|
215
|
+
members = get_args(annotation)
|
|
216
|
+
if not members or members[0] is Any:
|
|
217
|
+
return
|
|
218
|
+
bounds = get_args(members[0]) if is_union_type(members[0]) else (members[0],)
|
|
219
|
+
if not any(isinstance(bound, type) and issubclass(value, bound) for bound in bounds):
|
|
220
|
+
raise TypeError(
|
|
221
|
+
f"{path} must be a subclass of "
|
|
222
|
+
f"{' | '.join(_annotation_label(bound) for bound in bounds)}; "
|
|
223
|
+
f"got {value.__qualname__}."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
208
227
|
def _validate_mapping(annotation: object, value: object, path: str) -> None:
|
|
209
228
|
if not isinstance(value, Mapping):
|
|
210
229
|
raise TypeError(f"{path} must be a mapping; got {type(value).__name__}.")
|
|
@@ -226,6 +245,20 @@ def _validate_constraint(metadata: object, value: object, path: str) -> None:
|
|
|
226
245
|
"""Ignore annotation metadata that does not declare a runtime constraint."""
|
|
227
246
|
|
|
228
247
|
|
|
248
|
+
@_validate_constraint.register
|
|
249
|
+
def _(metadata: AnnotationChoices, value: object, path: str) -> None:
|
|
250
|
+
if value is None:
|
|
251
|
+
return
|
|
252
|
+
choices = metadata.choices()
|
|
253
|
+
items = value if isinstance(value, (list, tuple, frozenset, set)) else (value,)
|
|
254
|
+
for item in items:
|
|
255
|
+
if item not in choices:
|
|
256
|
+
raise AnnotationValidationError(
|
|
257
|
+
f"{path} must hold values from "
|
|
258
|
+
f"{[metadata.label(choice) for choice in choices]}; got {item!r}."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
229
262
|
@_validate_constraint.register
|
|
230
263
|
def _(metadata: Ge, value: object, path: str) -> None:
|
|
231
264
|
if value < metadata.ge:
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/SOURCES.txt
RENAMED
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
src/python_introspect/__init__.py
|
|
5
5
|
src/python_introspect/annotation_types.py
|
|
6
6
|
src/python_introspect/callable_declaration.py
|
|
7
|
+
src/python_introspect/choices.py
|
|
7
8
|
src/python_introspect/dataclass_projection.py
|
|
8
9
|
src/python_introspect/docstring_annotations.py
|
|
9
10
|
src/python_introspect/enableable.py
|
|
@@ -20,6 +21,7 @@ src/python_introspect.egg-info/SOURCES.txt
|
|
|
20
21
|
src/python_introspect.egg-info/dependency_links.txt
|
|
21
22
|
src/python_introspect.egg-info/requires.txt
|
|
22
23
|
src/python_introspect.egg-info/top_level.txt
|
|
24
|
+
tests/test_annotation_choices.py
|
|
23
25
|
tests/test_annotation_types.py
|
|
24
26
|
tests/test_callable_declaration.py
|
|
25
27
|
tests/test_dataclass_projection.py
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Annotated, Optional
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from python_introspect import (
|
|
7
|
+
AnnotatedDataclassValidationMixin,
|
|
8
|
+
AnnotationChoices,
|
|
9
|
+
AnnotationValidationError,
|
|
10
|
+
declared_annotation_choices,
|
|
11
|
+
enum_input_values,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _Kind:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Alpha(_Kind):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Beta(_Kind):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Other:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _KindChoices(AnnotationChoices):
|
|
32
|
+
def choices(self):
|
|
33
|
+
return (_Alpha, _Beta)
|
|
34
|
+
|
|
35
|
+
def label(self, choice):
|
|
36
|
+
return choice.__name__.strip("_").lower()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_CHOICES = _KindChoices()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class _Declared(AnnotatedDataclassValidationMixin):
|
|
44
|
+
kind: Annotated[Optional[type[_Kind]], _CHOICES] = None
|
|
45
|
+
kinds: Annotated[list[type[_Kind]], _CHOICES] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_choices_are_found_through_optional_and_containers():
|
|
49
|
+
assert declared_annotation_choices(Optional[Annotated[list[type[_Kind]], _CHOICES]]) is _CHOICES
|
|
50
|
+
assert declared_annotation_choices(Annotated[Optional[type[_Kind]], _CHOICES]) is _CHOICES
|
|
51
|
+
assert declared_annotation_choices(list[int]) is None
|
|
52
|
+
assert enum_input_values(Annotated[list[type[_Kind]], _CHOICES]) == ("alpha", "beta")
|
|
53
|
+
assert _CHOICES.choice_for_label("beta") is _Beta
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_validation_admits_only_declared_choices_and_class_bounds():
|
|
57
|
+
_Declared(kind=_Alpha, kinds=[_Beta])
|
|
58
|
+
with pytest.raises(AnnotationValidationError):
|
|
59
|
+
_Declared(kinds=[_Kind])
|
|
60
|
+
with pytest.raises(TypeError):
|
|
61
|
+
_Declared(kind=_Other)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/callable_declaration.py
RENAMED
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/dataclass_projection.py
RENAMED
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/docstring_annotations.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/environment_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/runtime_parameter.py
RENAMED
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect/signature_analyzer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/src/python_introspect.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_dataclass_source_preparation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_unified_parameter_analyzer.py
RENAMED
|
File without changes
|
{python_introspect-0.2.2 → python_introspect-0.2.3}/tests/test_validate_python_release_action.py
RENAMED
|
File without changes
|
|
File without changes
|