python-introspect 0.1.10__tar.gz → 0.1.12__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.10/src/python_introspect.egg-info → python_introspect-0.1.12}/PKG-INFO +7 -2
- {python_introspect-0.1.10 → python_introspect-0.1.12}/README.md +6 -1
- {python_introspect-0.1.10 → python_introspect-0.1.12}/pyproject.toml +1 -1
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/__init__.py +4 -1
- python_introspect-0.1.12/src/python_introspect/docstring_annotations.py +193 -0
- python_introspect-0.1.12/src/python_introspect/runtime_parameter.py +85 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/signature_analyzer.py +49 -48
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/validation.py +10 -7
- {python_introspect-0.1.10 → python_introspect-0.1.12/src/python_introspect.egg-info}/PKG-INFO +7 -2
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/SOURCES.txt +4 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_init.py +9 -1
- python_introspect-0.1.12/tests/test_runtime_parameter.py +58 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_signature_analyzer.py +83 -1
- python_introspect-0.1.12/tests/test_validate_python_release_action.py +137 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_validation.py +16 -1
- {python_introspect-0.1.10 → python_introspect-0.1.12}/LICENSE +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/setup.cfg +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/annotation_types.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/callable_declaration.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/dataclass_projection.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/enableable.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/environment_projection.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/unified_parameter_analyzer.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/requires.txt +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_annotation_types.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_callable_declaration.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_dataclass_projection.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_environment_projection.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_exceptions.py +0 -0
- {python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_unified_parameter_analyzer.py +0 -0
{python_introspect-0.1.10/src/python_introspect.egg-info → python_introspect-0.1.12}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-introspect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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
|
|
@@ -37,7 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
# python-introspect
|
|
38
38
|
|
|
39
39
|
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
40
|
-
docstrings.
|
|
40
|
+
docstrings, plus signature-derived callable declaration projection.
|
|
41
41
|
|
|
42
42
|
[](https://www.python.org/downloads/)
|
|
43
43
|
[](https://badge.fury.io/py/python-introspect)
|
|
@@ -66,6 +66,11 @@ for name, info in parameters.items():
|
|
|
66
66
|
dataclass types, and instances. It returns a mapping of names to
|
|
67
67
|
``ParameterInfo`` records.
|
|
68
68
|
|
|
69
|
+
Use ``callable_declaration_kwargs`` when declaration identity should omit
|
|
70
|
+
keyword arguments equal to their signature defaults. The caller supplies value
|
|
71
|
+
equality so array, lazy, or other framework-specific values keep their owning
|
|
72
|
+
semantics.
|
|
73
|
+
|
|
69
74
|
## Extension points
|
|
70
75
|
|
|
71
76
|
Use ``register_namespace_provider`` to contribute names used while resolving
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# python-introspect
|
|
2
2
|
|
|
3
3
|
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
4
|
-
docstrings.
|
|
4
|
+
docstrings, plus signature-derived callable declaration projection.
|
|
5
5
|
|
|
6
6
|
[](https://www.python.org/downloads/)
|
|
7
7
|
[](https://badge.fury.io/py/python-introspect)
|
|
@@ -30,6 +30,11 @@ for name, info in parameters.items():
|
|
|
30
30
|
dataclass types, and instances. It returns a mapping of names to
|
|
31
31
|
``ParameterInfo`` records.
|
|
32
32
|
|
|
33
|
+
Use ``callable_declaration_kwargs`` when declaration identity should omit
|
|
34
|
+
keyword arguments equal to their signature defaults. The caller supplies value
|
|
35
|
+
equality so array, lazy, or other framework-specific values keep their owning
|
|
36
|
+
semantics.
|
|
37
|
+
|
|
33
38
|
## Extension points
|
|
34
39
|
|
|
35
40
|
Use ``register_namespace_provider`` to contribute names used while resolving
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-introspect"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.12"
|
|
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.1.
|
|
12
|
+
__version__ = "0.1.12"
|
|
13
13
|
|
|
14
14
|
from .signature_analyzer import (
|
|
15
15
|
SignatureAnalyzer,
|
|
@@ -72,6 +72,7 @@ from .annotation_types import (
|
|
|
72
72
|
resolve_optional,
|
|
73
73
|
)
|
|
74
74
|
from .callable_declaration import callable_declaration_kwargs
|
|
75
|
+
from .runtime_parameter import RuntimeParameterDeclarationABC
|
|
75
76
|
|
|
76
77
|
__all__ = [
|
|
77
78
|
# Version
|
|
@@ -130,4 +131,6 @@ __all__ = [
|
|
|
130
131
|
"resolve_optional",
|
|
131
132
|
# Callable declarations
|
|
132
133
|
"callable_declaration_kwargs",
|
|
134
|
+
# Runtime-supplied callable parameters
|
|
135
|
+
"RuntimeParameterDeclarationABC",
|
|
133
136
|
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Typed annotation inference from callable defaults and docstring declarations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
9
|
+
from functools import reduce
|
|
10
|
+
from operator import or_
|
|
11
|
+
from typing import Any, ClassVar
|
|
12
|
+
|
|
13
|
+
from metaclass_registry import AutoRegisterMeta
|
|
14
|
+
|
|
15
|
+
from .annotation_types import make_optional
|
|
16
|
+
from .validation import validate_annotation_value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DocstringTypeDeclaration(ABC, metaclass=AutoRegisterMeta):
|
|
20
|
+
"""Nominal owner for one family of docstring type expressions."""
|
|
21
|
+
|
|
22
|
+
__registry_key__ = "declaration_name"
|
|
23
|
+
__skip_if_no_key__ = True
|
|
24
|
+
|
|
25
|
+
declaration_name: ClassVar[str | None] = None
|
|
26
|
+
expression_pattern: ClassVar[re.Pattern[str] | None] = None
|
|
27
|
+
annotation: ClassVar[object | None] = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def annotations_for(cls, description: str | None) -> tuple[object, ...]:
|
|
31
|
+
"""Return every declaration matched by the parameter's type expression."""
|
|
32
|
+
|
|
33
|
+
expression = _type_expression(description)
|
|
34
|
+
if not expression:
|
|
35
|
+
return ()
|
|
36
|
+
annotations = {
|
|
37
|
+
declaration.require_annotation()
|
|
38
|
+
for declaration_type in cls.__registry__.values()
|
|
39
|
+
for declaration in (declaration_type(),)
|
|
40
|
+
if declaration.matches(expression)
|
|
41
|
+
}
|
|
42
|
+
return tuple(sorted(annotations, key=_annotation_sort_key))
|
|
43
|
+
|
|
44
|
+
def matches(self, expression: str) -> bool:
|
|
45
|
+
"""Return whether this declaration owns a token in ``expression``."""
|
|
46
|
+
|
|
47
|
+
pattern = type(self).expression_pattern
|
|
48
|
+
return pattern is not None and pattern.search(expression) is not None
|
|
49
|
+
|
|
50
|
+
def require_annotation(self) -> object:
|
|
51
|
+
"""Return the annotation owned by this concrete declaration."""
|
|
52
|
+
|
|
53
|
+
annotation = type(self).annotation
|
|
54
|
+
if annotation is None:
|
|
55
|
+
raise TypeError(f"{type(self).__name__}.annotation must declare a Python type.")
|
|
56
|
+
return annotation
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BooleanDocstringType(DocstringTypeDeclaration):
|
|
60
|
+
declaration_name = "boolean"
|
|
61
|
+
expression_pattern = re.compile(r"\b(?:bool|boolean|true|false)\b")
|
|
62
|
+
annotation = bool
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class IntegerDocstringType(DocstringTypeDeclaration):
|
|
66
|
+
declaration_name = "integer"
|
|
67
|
+
expression_pattern = re.compile(r"\b(?:int|integer|ints|integers)\b")
|
|
68
|
+
annotation = int
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FloatDocstringType(DocstringTypeDeclaration):
|
|
72
|
+
declaration_name = "float"
|
|
73
|
+
expression_pattern = re.compile(r"\b(?:float|double|scalar|scalars|number|numeric)\b")
|
|
74
|
+
annotation = float
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class StringDocstringType(DocstringTypeDeclaration):
|
|
78
|
+
declaration_name = "string"
|
|
79
|
+
expression_pattern = re.compile(r"\b(?:str|string|strings)\b")
|
|
80
|
+
annotation = str
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TupleDocstringType(DocstringTypeDeclaration):
|
|
84
|
+
declaration_name = "tuple"
|
|
85
|
+
expression_pattern = re.compile(r"\btuple\b")
|
|
86
|
+
annotation = tuple[Any, ...]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SequenceDocstringType(DocstringTypeDeclaration):
|
|
90
|
+
declaration_name = "sequence"
|
|
91
|
+
expression_pattern = re.compile(r"\b(?:ndarray|array(?:[_ -]?like)?|sequence|iterable)\b")
|
|
92
|
+
annotation = Sequence[Any]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class ListDocstringType(DocstringTypeDeclaration):
|
|
96
|
+
declaration_name = "list"
|
|
97
|
+
expression_pattern = re.compile(r"\blist\b")
|
|
98
|
+
annotation = list[Any]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class MappingDocstringType(DocstringTypeDeclaration):
|
|
102
|
+
declaration_name = "mapping"
|
|
103
|
+
expression_pattern = re.compile(r"\b(?:dict|dictionary|mapping)\b")
|
|
104
|
+
annotation = Mapping[str, Any]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CallableDocstringType(DocstringTypeDeclaration):
|
|
108
|
+
declaration_name = "callable"
|
|
109
|
+
expression_pattern = re.compile(r"\b(?:callable|function)\b")
|
|
110
|
+
annotation = Callable[..., Any]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def infer_parameter_annotation(
|
|
114
|
+
parameter: inspect.Parameter,
|
|
115
|
+
description: str | None,
|
|
116
|
+
) -> object:
|
|
117
|
+
"""Infer one missing annotation from declaration-owned runtime evidence."""
|
|
118
|
+
|
|
119
|
+
if parameter.annotation is not inspect.Parameter.empty:
|
|
120
|
+
return parameter.annotation
|
|
121
|
+
|
|
122
|
+
documented = DocstringTypeDeclaration.annotations_for(description)
|
|
123
|
+
default = parameter.default
|
|
124
|
+
if default is inspect.Parameter.empty:
|
|
125
|
+
return _union_annotation(documented) if documented else Any
|
|
126
|
+
if default is None:
|
|
127
|
+
inferred = _union_annotation(documented)
|
|
128
|
+
return Any if inferred is Any else make_optional(inferred)
|
|
129
|
+
if documented and any(
|
|
130
|
+
_annotation_accepts_default(annotation, default) for annotation in documented
|
|
131
|
+
):
|
|
132
|
+
return _union_annotation(documented)
|
|
133
|
+
return type(default)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _type_expression(description: str | None) -> str:
|
|
137
|
+
"""Return the first non-empty docstring line that declares a parameter type."""
|
|
138
|
+
|
|
139
|
+
if not description:
|
|
140
|
+
return ""
|
|
141
|
+
expression = next(
|
|
142
|
+
(line.strip().lower() for line in description.splitlines() if line.strip()),
|
|
143
|
+
"",
|
|
144
|
+
)
|
|
145
|
+
return _without_nested_type_arguments(expression)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _without_nested_type_arguments(expression: str) -> str:
|
|
149
|
+
"""Remove bracketed member signatures while retaining their owning type."""
|
|
150
|
+
|
|
151
|
+
depth = 0
|
|
152
|
+
projected: list[str] = []
|
|
153
|
+
for character in expression:
|
|
154
|
+
if character == "[":
|
|
155
|
+
depth += 1
|
|
156
|
+
continue
|
|
157
|
+
if character == "]":
|
|
158
|
+
depth = max(0, depth - 1)
|
|
159
|
+
continue
|
|
160
|
+
if depth == 0:
|
|
161
|
+
projected.append(character)
|
|
162
|
+
return "".join(projected)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _union_annotation(annotations: tuple[object, ...]) -> object:
|
|
166
|
+
if not annotations:
|
|
167
|
+
return Any
|
|
168
|
+
if len(annotations) == 1:
|
|
169
|
+
return annotations[0]
|
|
170
|
+
return reduce(or_, annotations)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _annotation_accepts_default(annotation: object, default: object) -> bool:
|
|
174
|
+
try:
|
|
175
|
+
validate_annotation_value(annotation, default, path="parameter default")
|
|
176
|
+
except (TypeError, ValueError):
|
|
177
|
+
return False
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _annotation_sort_key(annotation: object) -> tuple[str, str]:
|
|
182
|
+
origin = getattr(annotation, "__origin__", None)
|
|
183
|
+
owner = origin or annotation
|
|
184
|
+
return (
|
|
185
|
+
str(getattr(owner, "__module__", "")),
|
|
186
|
+
str(getattr(owner, "__qualname__", annotation)),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
__all__ = [
|
|
191
|
+
"DocstringTypeDeclaration",
|
|
192
|
+
"infer_parameter_annotation",
|
|
193
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Nominal declarations for runtime-supplied callable parameters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import ClassVar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RuntimeParameterDeclarationABC(ABC):
|
|
12
|
+
"""Semantic owner for one parameter supplied by runtime infrastructure."""
|
|
13
|
+
|
|
14
|
+
preserve_for_execution: ClassVar[bool] = False
|
|
15
|
+
is_semantic_control: ClassVar[bool] = False
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def require_parameter_name(cls) -> str:
|
|
20
|
+
"""Return the public callable parameter name."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def parameter(cls) -> inspect.Parameter:
|
|
25
|
+
"""Return the complete callable signature parameter declaration."""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def validated_parameter(cls) -> inspect.Parameter:
|
|
29
|
+
"""Return the parameter after proving its nominal declaration is coherent."""
|
|
30
|
+
|
|
31
|
+
parameter = cls.parameter()
|
|
32
|
+
if not isinstance(parameter, inspect.Parameter):
|
|
33
|
+
raise TypeError(f"{cls.__name__}.parameter() must return inspect.Parameter.")
|
|
34
|
+
parameter_name = cls.require_parameter_name()
|
|
35
|
+
if not isinstance(parameter_name, str) or not parameter_name.strip():
|
|
36
|
+
raise TypeError(
|
|
37
|
+
f"{cls.__name__}.require_parameter_name() must return a non-empty string."
|
|
38
|
+
)
|
|
39
|
+
if parameter.name != parameter_name:
|
|
40
|
+
raise TypeError(
|
|
41
|
+
f"{cls.__name__}.parameter() name {parameter.name!r} does not "
|
|
42
|
+
f"match require_parameter_name() {parameter_name!r}."
|
|
43
|
+
)
|
|
44
|
+
return parameter
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def require_declaration_type(
|
|
48
|
+
cls,
|
|
49
|
+
candidate: object,
|
|
50
|
+
*,
|
|
51
|
+
boundary: str,
|
|
52
|
+
) -> type[RuntimeParameterDeclarationABC]:
|
|
53
|
+
"""Require one nominal runtime-parameter declaration type."""
|
|
54
|
+
|
|
55
|
+
if not isinstance(candidate, type) or not issubclass(candidate, cls):
|
|
56
|
+
raise TypeError(
|
|
57
|
+
f"{boundary} must contain {cls.__name__} subclasses, got {candidate!r}."
|
|
58
|
+
)
|
|
59
|
+
candidate.validated_parameter()
|
|
60
|
+
return candidate
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def require_declaration_types(
|
|
64
|
+
cls,
|
|
65
|
+
candidates: Iterable[object],
|
|
66
|
+
*,
|
|
67
|
+
boundary: str,
|
|
68
|
+
) -> tuple[type[RuntimeParameterDeclarationABC], ...]:
|
|
69
|
+
"""Require coherent declarations with unique public parameter names."""
|
|
70
|
+
|
|
71
|
+
declarations: list[type[RuntimeParameterDeclarationABC]] = []
|
|
72
|
+
seen_names: set[str] = set()
|
|
73
|
+
for candidate in candidates:
|
|
74
|
+
declaration = cls.require_declaration_type(candidate, boundary=boundary)
|
|
75
|
+
parameter_name = declaration.require_parameter_name()
|
|
76
|
+
if parameter_name in seen_names:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"{boundary} declares duplicate runtime parameter {parameter_name!r}."
|
|
79
|
+
)
|
|
80
|
+
declarations.append(declaration)
|
|
81
|
+
seen_names.add(parameter_name)
|
|
82
|
+
return tuple(declarations)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ("RuntimeParameterDeclarationABC",)
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/signature_analyzer.py
RENAMED
|
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
|
|
20
20
|
from metaclass_registry import AutoRegisterMeta
|
|
21
21
|
from .annotation_types import optional_member_type
|
|
22
|
+
from .docstring_annotations import infer_parameter_annotation
|
|
22
23
|
|
|
23
24
|
# =============================================================================
|
|
24
25
|
# PLUGIN REGISTRY - Allows frameworks to extend type resolution
|
|
@@ -173,19 +174,21 @@ class DocstringParseState:
|
|
|
173
174
|
parameters: Dict[str, str] = field(default_factory=dict)
|
|
174
175
|
returns: Optional[str] = None
|
|
175
176
|
examples: Optional[str] = None
|
|
176
|
-
|
|
177
|
+
current_params: Tuple[str, ...] = ()
|
|
177
178
|
current_param_lines: List[str] = field(default_factory=list)
|
|
178
179
|
|
|
179
180
|
def finalize_current_param(self) -> None:
|
|
180
181
|
"""Commit the active parameter description, if one is being parsed."""
|
|
181
|
-
if self.
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
if self.current_params and self.current_param_lines:
|
|
183
|
+
description = "\n".join(self.current_param_lines).strip()
|
|
184
|
+
self.parameters.update(
|
|
185
|
+
(parameter_name, description)
|
|
186
|
+
for parameter_name in self.current_params
|
|
184
187
|
)
|
|
185
188
|
|
|
186
189
|
def reset_current_param(self) -> None:
|
|
187
190
|
"""Clear parameter continuation state after a section transition."""
|
|
188
|
-
self.
|
|
191
|
+
self.current_params = ()
|
|
189
192
|
self.current_param_lines = []
|
|
190
193
|
|
|
191
194
|
def transition_to(self, section: "DocstringSection") -> "DocstringSection":
|
|
@@ -283,6 +286,13 @@ class ParametersDocstringSection(DocstringSection):
|
|
|
283
286
|
"additional parameters:",
|
|
284
287
|
)
|
|
285
288
|
numpy_headers = ("args", "arguments", "parameters", "additional parameters")
|
|
289
|
+
parameter_declaration_pattern = re.compile(
|
|
290
|
+
r"^(?:"
|
|
291
|
+
r":param\s+(?P<sphinx_name>\w+)"
|
|
292
|
+
r"|[-•*]\s*(?P<bullet_names>\w+(?:\s*,\s*\w+)*)"
|
|
293
|
+
r"|(?P<plain_names>\w+(?:\s*,\s*\w+)*)"
|
|
294
|
+
r")\s*:\s*(?P<description>.+)$"
|
|
295
|
+
)
|
|
286
296
|
|
|
287
297
|
def consume(
|
|
288
298
|
self,
|
|
@@ -290,46 +300,32 @@ class ParametersDocstringSection(DocstringSection):
|
|
|
290
300
|
original_line: str,
|
|
291
301
|
line: str,
|
|
292
302
|
) -> None:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
param_match_numpy = re.match(r"^(\w+)\s*:\s*(.+)", line)
|
|
296
|
-
param_match_inline = re.match(
|
|
297
|
-
r"^(\w+):\s*(\w+(?:\[.*?\])?|\w+(?:\s*\|\s*\w+)*)\s+(.+)",
|
|
298
|
-
line,
|
|
299
|
-
)
|
|
300
|
-
param_match_bullet = re.match(r"^[-•*]\s*(\w+):\s*(.+)", line)
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
param_match_google
|
|
304
|
-
or param_match_sphinx
|
|
305
|
-
or param_match_numpy
|
|
306
|
-
or param_match_inline
|
|
307
|
-
or param_match_bullet
|
|
308
|
-
):
|
|
303
|
+
declaration = self.parameter_declaration_pattern.fullmatch(line)
|
|
304
|
+
if declaration is not None:
|
|
309
305
|
state.finalize_current_param()
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
elif state.
|
|
306
|
+
declared_names = next(
|
|
307
|
+
value
|
|
308
|
+
for value in (
|
|
309
|
+
declaration.group("sphinx_name"),
|
|
310
|
+
declaration.group("bullet_names"),
|
|
311
|
+
declaration.group("plain_names"),
|
|
312
|
+
)
|
|
313
|
+
if value is not None
|
|
314
|
+
)
|
|
315
|
+
state.current_params = tuple(
|
|
316
|
+
name.strip() for name in declared_names.split(",")
|
|
317
|
+
)
|
|
318
|
+
state.current_param_lines = [
|
|
319
|
+
declaration.group("description").strip()
|
|
320
|
+
]
|
|
321
|
+
elif state.current_params and (
|
|
326
322
|
original_line.startswith(" ") or original_line.startswith("\t")
|
|
327
323
|
):
|
|
328
324
|
state.current_param_lines.append(line)
|
|
329
325
|
elif not line:
|
|
330
326
|
state.finalize_current_param()
|
|
331
327
|
state.reset_current_param()
|
|
332
|
-
elif state.
|
|
328
|
+
elif state.current_params:
|
|
333
329
|
state.current_param_lines.append(line)
|
|
334
330
|
else:
|
|
335
331
|
state.parameters.update(DocstringExtractor._parse_inline_parameters(line))
|
|
@@ -731,12 +727,16 @@ class CallableAnalysisContext:
|
|
|
731
727
|
|
|
732
728
|
@staticmethod
|
|
733
729
|
def annotation_namespace_targets(target: Callable) -> Tuple[Callable, ...]:
|
|
734
|
-
"""Return
|
|
730
|
+
"""Return runtime-to-declaration callables whose namespaces own annotations."""
|
|
735
731
|
|
|
736
732
|
unwrapped = inspect.unwrap(target)
|
|
737
733
|
if unwrapped is target:
|
|
738
734
|
return (target,)
|
|
739
|
-
|
|
735
|
+
# ``functools.wraps`` copies the declaration's annotation strings onto a
|
|
736
|
+
# wrapper whose runtime globals belong to the decorator module. Merge
|
|
737
|
+
# outward so the unwrapped declaration, which authored those strings,
|
|
738
|
+
# has final authority over colliding names.
|
|
739
|
+
return (target, unwrapped)
|
|
740
740
|
|
|
741
741
|
def type_hints(self) -> Dict[str, Any]:
|
|
742
742
|
"""Resolve type hints using the context-owned namespace."""
|
|
@@ -854,14 +854,6 @@ class SignatureAnalyzer:
|
|
|
854
854
|
continue
|
|
855
855
|
|
|
856
856
|
from typing import Any
|
|
857
|
-
param_type = type_hints.get(param_name)
|
|
858
|
-
if param_type is None:
|
|
859
|
-
param_type = (
|
|
860
|
-
param.annotation
|
|
861
|
-
if param.annotation is not inspect.Parameter.empty
|
|
862
|
-
else Any
|
|
863
|
-
)
|
|
864
|
-
param_type, annotation_description = _parameter_annotation_help(param_type)
|
|
865
857
|
default_value = param.default if param.default != inspect.Parameter.empty else None
|
|
866
858
|
is_required = param.default == inspect.Parameter.empty
|
|
867
859
|
|
|
@@ -872,8 +864,17 @@ class SignatureAnalyzer:
|
|
|
872
864
|
else None
|
|
873
865
|
)
|
|
874
866
|
if param_description is None:
|
|
867
|
+
annotation = type_hints.get(param_name, param.annotation)
|
|
868
|
+
_base_annotation, annotation_description = _parameter_annotation_help(
|
|
869
|
+
annotation
|
|
870
|
+
)
|
|
875
871
|
param_description = annotation_description
|
|
876
872
|
|
|
873
|
+
annotation = type_hints.get(param_name, param.annotation)
|
|
874
|
+
if annotation is inspect.Parameter.empty:
|
|
875
|
+
annotation = infer_parameter_annotation(param, param_description)
|
|
876
|
+
param_type, _annotation_description = _parameter_annotation_help(annotation)
|
|
877
|
+
|
|
877
878
|
parameters[param_name] = ParameterInfo(
|
|
878
879
|
name=param_name,
|
|
879
880
|
param_type=param_type,
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from collections.abc import Mapping, Sequence
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from collections.abc import Set as AbstractSet
|
|
6
7
|
from dataclasses import fields, is_dataclass, replace
|
|
7
8
|
from functools import singledispatch
|
|
8
9
|
from typing import (
|
|
@@ -10,13 +11,14 @@ from typing import (
|
|
|
10
11
|
Any,
|
|
11
12
|
ClassVar,
|
|
12
13
|
Literal,
|
|
14
|
+
TypeVar,
|
|
13
15
|
get_args,
|
|
14
16
|
get_origin,
|
|
15
17
|
get_type_hints,
|
|
16
|
-
TypeVar,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
from annotated_types import Ge, Gt, Interval, Le, Len, Lt, MaxLen, MinLen, Predicate
|
|
21
|
+
|
|
20
22
|
from .annotation_types import is_union_type
|
|
21
23
|
|
|
22
24
|
|
|
@@ -133,21 +135,22 @@ def validate_annotation_value(
|
|
|
133
135
|
)
|
|
134
136
|
if origin is Literal:
|
|
135
137
|
choices = get_args(annotation)
|
|
136
|
-
if not any(
|
|
137
|
-
type(value) is type(choice) and value == choice
|
|
138
|
-
for choice in choices
|
|
139
|
-
):
|
|
138
|
+
if not any(type(value) is type(choice) and value == choice for choice in choices):
|
|
140
139
|
raise AnnotationValidationError(f"{path} must be one of {choices!r}.")
|
|
141
140
|
return
|
|
142
141
|
if origin is tuple:
|
|
143
142
|
_validate_tuple(annotation, value, path)
|
|
144
143
|
return
|
|
145
|
-
if origin in {list, set, frozenset, Sequence,
|
|
144
|
+
if origin in {list, set, frozenset, Sequence, AbstractSet}:
|
|
146
145
|
_validate_sequence(annotation, value, path)
|
|
147
146
|
return
|
|
148
147
|
if origin in {dict, Mapping}:
|
|
149
148
|
_validate_mapping(annotation, value, path)
|
|
150
149
|
return
|
|
150
|
+
if origin is Callable:
|
|
151
|
+
if not callable(value):
|
|
152
|
+
raise TypeError(f"{path} must be callable; got {type(value).__name__}.")
|
|
153
|
+
return
|
|
151
154
|
if origin is ClassVar:
|
|
152
155
|
return
|
|
153
156
|
if annotation is None or annotation is type(None):
|
{python_introspect-0.1.10 → python_introspect-0.1.12/src/python_introspect.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-introspect
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
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
|
|
@@ -37,7 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
# python-introspect
|
|
38
38
|
|
|
39
39
|
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
40
|
-
docstrings.
|
|
40
|
+
docstrings, plus signature-derived callable declaration projection.
|
|
41
41
|
|
|
42
42
|
[](https://www.python.org/downloads/)
|
|
43
43
|
[](https://badge.fury.io/py/python-introspect)
|
|
@@ -66,6 +66,11 @@ for name, info in parameters.items():
|
|
|
66
66
|
dataclass types, and instances. It returns a mapping of names to
|
|
67
67
|
``ParameterInfo`` records.
|
|
68
68
|
|
|
69
|
+
Use ``callable_declaration_kwargs`` when declaration identity should omit
|
|
70
|
+
keyword arguments equal to their signature defaults. The caller supplies value
|
|
71
|
+
equality so array, lazy, or other framework-specific values keep their owning
|
|
72
|
+
semantics.
|
|
73
|
+
|
|
69
74
|
## Extension points
|
|
70
75
|
|
|
71
76
|
Use ``register_namespace_provider`` to contribute names used while resolving
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/SOURCES.txt
RENAMED
|
@@ -5,9 +5,11 @@ src/python_introspect/__init__.py
|
|
|
5
5
|
src/python_introspect/annotation_types.py
|
|
6
6
|
src/python_introspect/callable_declaration.py
|
|
7
7
|
src/python_introspect/dataclass_projection.py
|
|
8
|
+
src/python_introspect/docstring_annotations.py
|
|
8
9
|
src/python_introspect/enableable.py
|
|
9
10
|
src/python_introspect/environment_projection.py
|
|
10
11
|
src/python_introspect/exceptions.py
|
|
12
|
+
src/python_introspect/runtime_parameter.py
|
|
11
13
|
src/python_introspect/signature_analyzer.py
|
|
12
14
|
src/python_introspect/unified_parameter_analyzer.py
|
|
13
15
|
src/python_introspect/validation.py
|
|
@@ -22,6 +24,8 @@ tests/test_dataclass_projection.py
|
|
|
22
24
|
tests/test_environment_projection.py
|
|
23
25
|
tests/test_exceptions.py
|
|
24
26
|
tests/test_init.py
|
|
27
|
+
tests/test_runtime_parameter.py
|
|
25
28
|
tests/test_signature_analyzer.py
|
|
26
29
|
tests/test_unified_parameter_analyzer.py
|
|
30
|
+
tests/test_validate_python_release_action.py
|
|
27
31
|
tests/test_validation.py
|
|
@@ -11,42 +11,48 @@ class TestPackageImports:
|
|
|
11
11
|
"""Test that __version__ is available."""
|
|
12
12
|
assert hasattr(python_introspect, "__version__")
|
|
13
13
|
assert isinstance(python_introspect.__version__, str)
|
|
14
|
-
assert python_introspect.__version__ == "0.1.
|
|
14
|
+
assert python_introspect.__version__ == "0.1.12"
|
|
15
15
|
|
|
16
16
|
def test_signature_analyzer_import(self):
|
|
17
17
|
"""Test SignatureAnalyzer is importable."""
|
|
18
18
|
assert hasattr(python_introspect, "SignatureAnalyzer")
|
|
19
19
|
from python_introspect import SignatureAnalyzer
|
|
20
|
+
|
|
20
21
|
assert SignatureAnalyzer is not None
|
|
21
22
|
|
|
22
23
|
def test_parameter_info_import(self):
|
|
23
24
|
"""Test ParameterInfo is importable."""
|
|
24
25
|
assert hasattr(python_introspect, "ParameterInfo")
|
|
25
26
|
from python_introspect import ParameterInfo
|
|
27
|
+
|
|
26
28
|
assert ParameterInfo is not None
|
|
27
29
|
|
|
28
30
|
def test_docstring_info_import(self):
|
|
29
31
|
"""Test DocstringInfo is importable."""
|
|
30
32
|
assert hasattr(python_introspect, "DocstringInfo")
|
|
31
33
|
from python_introspect import DocstringInfo
|
|
34
|
+
|
|
32
35
|
assert DocstringInfo is not None
|
|
33
36
|
|
|
34
37
|
def test_docstring_extractor_import(self):
|
|
35
38
|
"""Test DocstringExtractor is importable."""
|
|
36
39
|
assert hasattr(python_introspect, "DocstringExtractor")
|
|
37
40
|
from python_introspect import DocstringExtractor
|
|
41
|
+
|
|
38
42
|
assert DocstringExtractor is not None
|
|
39
43
|
|
|
40
44
|
def test_unified_parameter_analyzer_import(self):
|
|
41
45
|
"""Test UnifiedParameterAnalyzer is importable."""
|
|
42
46
|
assert hasattr(python_introspect, "UnifiedParameterAnalyzer")
|
|
43
47
|
from python_introspect import UnifiedParameterAnalyzer
|
|
48
|
+
|
|
44
49
|
assert UnifiedParameterAnalyzer is not None
|
|
45
50
|
|
|
46
51
|
def test_unified_parameter_info_import(self):
|
|
47
52
|
"""Test UnifiedParameterInfo is importable."""
|
|
48
53
|
assert hasattr(python_introspect, "UnifiedParameterInfo")
|
|
49
54
|
from python_introspect import UnifiedParameterInfo
|
|
55
|
+
|
|
50
56
|
assert UnifiedParameterInfo is not None
|
|
51
57
|
|
|
52
58
|
def test_exceptions_import(self):
|
|
@@ -57,6 +63,7 @@ class TestPackageImports:
|
|
|
57
63
|
DocstringParsingError,
|
|
58
64
|
TypeResolutionError,
|
|
59
65
|
)
|
|
66
|
+
|
|
60
67
|
assert IntrospectionError is not None
|
|
61
68
|
assert SignatureAnalysisError is not None
|
|
62
69
|
assert DocstringParsingError is not None
|
|
@@ -98,6 +105,7 @@ class TestPublicAPI:
|
|
|
98
105
|
"""Test that 'from python_introspect import *' works."""
|
|
99
106
|
# This is a sanity check that __all__ is properly defined
|
|
100
107
|
import python_introspect
|
|
108
|
+
|
|
101
109
|
all_names = python_introspect.__all__
|
|
102
110
|
|
|
103
111
|
for name in all_names:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for nominal runtime-parameter declarations."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from python_introspect import RuntimeParameterDeclarationABC
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExampleRuntimeParameter(RuntimeParameterDeclarationABC):
|
|
11
|
+
@classmethod
|
|
12
|
+
def require_parameter_name(cls) -> str:
|
|
13
|
+
return "runtime_value"
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def parameter(cls) -> inspect.Parameter:
|
|
17
|
+
return inspect.Parameter(
|
|
18
|
+
cls.require_parameter_name(),
|
|
19
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
20
|
+
annotation=int,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StructuralImpostor:
|
|
25
|
+
@classmethod
|
|
26
|
+
def require_parameter_name(cls) -> str:
|
|
27
|
+
return "runtime_value"
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def parameter(cls) -> inspect.Parameter:
|
|
31
|
+
return inspect.Parameter("runtime_value", inspect.Parameter.KEYWORD_ONLY)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MismatchedRuntimeParameter(ExampleRuntimeParameter):
|
|
35
|
+
@classmethod
|
|
36
|
+
def parameter(cls) -> inspect.Parameter:
|
|
37
|
+
return inspect.Parameter("different_name", inspect.Parameter.KEYWORD_ONLY)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_runtime_parameter_requires_nominal_subclass() -> None:
|
|
41
|
+
with pytest.raises(TypeError, match="RuntimeParameterDeclarationABC subclasses"):
|
|
42
|
+
RuntimeParameterDeclarationABC.require_declaration_type(
|
|
43
|
+
StructuralImpostor,
|
|
44
|
+
boundary="test declarations",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_runtime_parameter_requires_matching_declared_name() -> None:
|
|
49
|
+
with pytest.raises(TypeError, match="does not match require_parameter_name"):
|
|
50
|
+
MismatchedRuntimeParameter.validated_parameter()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_runtime_parameter_collection_requires_unique_names() -> None:
|
|
54
|
+
with pytest.raises(ValueError, match="duplicate runtime parameter 'runtime_value'"):
|
|
55
|
+
RuntimeParameterDeclarationABC.require_declaration_types(
|
|
56
|
+
(ExampleRuntimeParameter, ExampleRuntimeParameter),
|
|
57
|
+
boundary="test declarations",
|
|
58
|
+
)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Tests for SignatureAnalyzer."""
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
from collections.abc import Callable as CallableABC, Sequence
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from functools import wraps
|
|
7
|
-
from typing import get_args
|
|
8
|
+
from typing import get_args, get_origin
|
|
8
9
|
from typing import Annotated, Optional, List, Dict, Any
|
|
9
10
|
from python_introspect import (
|
|
10
11
|
SignatureAnalyzer,
|
|
@@ -90,6 +91,49 @@ class TestSignatureAnalyzer:
|
|
|
90
91
|
assert params["slice_by_slice"].param_type is bool
|
|
91
92
|
assert params["slice_by_slice"].default_value is False
|
|
92
93
|
|
|
94
|
+
def test_unannotated_callable_parameters_derive_types_from_declarations(self):
|
|
95
|
+
"""Defaults and NumPy-style type lines form a typed authoring signature."""
|
|
96
|
+
|
|
97
|
+
def external(
|
|
98
|
+
image,
|
|
99
|
+
values=None,
|
|
100
|
+
exclude_border=False,
|
|
101
|
+
block_size=2,
|
|
102
|
+
reducer=max,
|
|
103
|
+
):
|
|
104
|
+
"""External array operation.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
image : ndarray
|
|
109
|
+
Input image.
|
|
110
|
+
values : ndarray, optional
|
|
111
|
+
Optional values.
|
|
112
|
+
exclude_border : tuple of ints, int, or False, optional
|
|
113
|
+
Border exclusion.
|
|
114
|
+
block_size : array_like or int
|
|
115
|
+
Block dimensions.
|
|
116
|
+
reducer : callable
|
|
117
|
+
Reduction function.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
params = SignatureAnalyzer.analyze(external)
|
|
121
|
+
|
|
122
|
+
assert set(get_args(params["values"].param_type)) == {
|
|
123
|
+
Sequence[Any],
|
|
124
|
+
type(None),
|
|
125
|
+
}
|
|
126
|
+
assert set(get_args(params["exclude_border"].param_type)) == {
|
|
127
|
+
bool,
|
|
128
|
+
int,
|
|
129
|
+
tuple[Any, ...],
|
|
130
|
+
}
|
|
131
|
+
assert set(get_args(params["block_size"].param_type)) == {
|
|
132
|
+
int,
|
|
133
|
+
Sequence[Any],
|
|
134
|
+
}
|
|
135
|
+
assert get_origin(params["reducer"].param_type) is CallableABC
|
|
136
|
+
|
|
93
137
|
def test_wrapped_callable_resolves_postponed_annotations_from_original_namespace(self):
|
|
94
138
|
"""Wrapper modules do not own postponed annotations copied from originals."""
|
|
95
139
|
def original(mode: "WrappedAnnotationMode | str" = WrappedAnnotationMode.A):
|
|
@@ -106,6 +150,27 @@ class TestSignatureAnalyzer:
|
|
|
106
150
|
|
|
107
151
|
assert get_args(params["mode"].param_type) == (WrappedAnnotationMode, str)
|
|
108
152
|
|
|
153
|
+
def test_wrapped_declaration_namespace_overrides_decorator_name_collision(self):
|
|
154
|
+
"""Copied annotation strings resolve against their declaration owner."""
|
|
155
|
+
|
|
156
|
+
def original(mode: "WrappedAnnotationMode" = WrappedAnnotationMode.A):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
wrapper_namespace = {"wraps": wraps, "original": original}
|
|
160
|
+
exec(
|
|
161
|
+
"@wraps(original)\n"
|
|
162
|
+
"def wrapper(*args, **kwargs):\n"
|
|
163
|
+
" return original(*args, **kwargs)\n",
|
|
164
|
+
wrapper_namespace,
|
|
165
|
+
)
|
|
166
|
+
wrapper = wrapper_namespace["wrapper"]
|
|
167
|
+
wrapper.__globals__["WrappedAnnotationMode"] = lambda: None
|
|
168
|
+
wrapper.__signature__ = inspect.signature(original)
|
|
169
|
+
|
|
170
|
+
params = SignatureAnalyzer.analyze(wrapper)
|
|
171
|
+
|
|
172
|
+
assert params["mode"].param_type is WrappedAnnotationMode
|
|
173
|
+
|
|
109
174
|
def test_analyze_function_with_docstring(self):
|
|
110
175
|
"""Test analyzing function with docstring parameters."""
|
|
111
176
|
def documented_func(name: str, age: int = 25):
|
|
@@ -419,6 +484,23 @@ class TestDocstringExtractor:
|
|
|
419
484
|
# NumPy style parsing support
|
|
420
485
|
assert "x" in info.parameters or info.parameters == {}
|
|
421
486
|
|
|
487
|
+
def test_extract_numpy_style_multi_name_declaration(self):
|
|
488
|
+
"""One NumPy declaration can author the same description for many names."""
|
|
489
|
+
|
|
490
|
+
def func(in_range="image", out_range="dtype"):
|
|
491
|
+
"""Rescale values.
|
|
492
|
+
|
|
493
|
+
Parameters
|
|
494
|
+
----------
|
|
495
|
+
in_range, out_range : str or 2-tuple, optional
|
|
496
|
+
Accepted intensity bounds.
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
info = DocstringExtractor.extract(func)
|
|
500
|
+
|
|
501
|
+
assert info.parameters["in_range"].startswith("str or 2-tuple")
|
|
502
|
+
assert info.parameters["out_range"] == info.parameters["in_range"]
|
|
503
|
+
|
|
422
504
|
def test_extract_multiline_parameter_description(self):
|
|
423
505
|
"""Test extracting multiline parameter descriptions."""
|
|
424
506
|
def func(description: str):
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from zipfile import ZipFile
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
ACTION_SCRIPT = (
|
|
13
|
+
Path(__file__).parents[1]
|
|
14
|
+
/ ".github"
|
|
15
|
+
/ "actions"
|
|
16
|
+
/ "validate-python-release"
|
|
17
|
+
/ "validate_python_release.py"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_action() -> ModuleType:
|
|
22
|
+
spec = importlib.util.spec_from_file_location("validate_python_release", ACTION_SCRIPT)
|
|
23
|
+
assert spec is not None and spec.loader is not None
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
sys.modules[spec.name] = module
|
|
26
|
+
spec.loader.exec_module(module)
|
|
27
|
+
return module
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _git(repository: Path, *arguments: str) -> str:
|
|
31
|
+
return subprocess.run(
|
|
32
|
+
("git", *arguments),
|
|
33
|
+
cwd=repository,
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
check=True,
|
|
37
|
+
).stdout.strip()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _release_repository(tmp_path: Path, tag: str, *, annotated: bool = True) -> str:
|
|
41
|
+
_git(tmp_path, "init")
|
|
42
|
+
_git(tmp_path, "config", "user.email", "release@example.test")
|
|
43
|
+
_git(tmp_path, "config", "user.name", "Release Test")
|
|
44
|
+
(tmp_path / "tracked.txt").write_text("release\n", encoding="utf-8")
|
|
45
|
+
_git(tmp_path, "add", "tracked.txt")
|
|
46
|
+
_git(tmp_path, "commit", "-m", "Release candidate")
|
|
47
|
+
if annotated:
|
|
48
|
+
_git(tmp_path, "tag", "-a", tag, "-m", tag)
|
|
49
|
+
else:
|
|
50
|
+
_git(tmp_path, "tag", tag)
|
|
51
|
+
return _git(tmp_path, "rev-parse", "HEAD")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _wheel(tmp_path: Path, version: str = "1.2.3") -> None:
|
|
55
|
+
distribution = tmp_path / "dist" / f"demo_package-{version}-py3-none-any.whl"
|
|
56
|
+
distribution.parent.mkdir()
|
|
57
|
+
with ZipFile(distribution, "w") as wheel:
|
|
58
|
+
wheel.writestr(
|
|
59
|
+
f"demo_package-{version}.dist-info/METADATA",
|
|
60
|
+
"Metadata-Version: 2.1\n" "Name: demo-package\n" f"Version: {version}\n",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_action_accepts_one_matching_annotated_release(tmp_path: Path) -> None:
|
|
65
|
+
action = _load_action()
|
|
66
|
+
commit = _release_repository(tmp_path, "v1.2.3")
|
|
67
|
+
_wheel(tmp_path)
|
|
68
|
+
|
|
69
|
+
assert (
|
|
70
|
+
action.main(
|
|
71
|
+
[
|
|
72
|
+
"--repository-root",
|
|
73
|
+
str(tmp_path),
|
|
74
|
+
"--tag",
|
|
75
|
+
"v1.2.3",
|
|
76
|
+
"--commit",
|
|
77
|
+
commit,
|
|
78
|
+
]
|
|
79
|
+
)
|
|
80
|
+
== 0
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
("tag", "version", "annotated"),
|
|
86
|
+
[
|
|
87
|
+
("v1.2.4", "1.2.3", True),
|
|
88
|
+
("v1.2.3", "1.2.3", False),
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
def test_action_rejects_mismatched_or_lightweight_tags(
|
|
92
|
+
tmp_path: Path,
|
|
93
|
+
tag: str,
|
|
94
|
+
version: str,
|
|
95
|
+
annotated: bool,
|
|
96
|
+
) -> None:
|
|
97
|
+
action = _load_action()
|
|
98
|
+
commit = _release_repository(tmp_path, tag, annotated=annotated)
|
|
99
|
+
_wheel(tmp_path, version)
|
|
100
|
+
|
|
101
|
+
assert (
|
|
102
|
+
action.main(
|
|
103
|
+
[
|
|
104
|
+
"--repository-root",
|
|
105
|
+
str(tmp_path),
|
|
106
|
+
"--tag",
|
|
107
|
+
tag,
|
|
108
|
+
"--commit",
|
|
109
|
+
commit,
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
== 1
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_action_rejects_a_commit_other_than_the_tag_target(tmp_path: Path) -> None:
|
|
117
|
+
action = _load_action()
|
|
118
|
+
_release_repository(tmp_path, "v1.2.3")
|
|
119
|
+
_wheel(tmp_path)
|
|
120
|
+
(tmp_path / "tracked.txt").write_text("later\n", encoding="utf-8")
|
|
121
|
+
_git(tmp_path, "add", "tracked.txt")
|
|
122
|
+
_git(tmp_path, "commit", "-m", "Later commit")
|
|
123
|
+
later_commit = _git(tmp_path, "rev-parse", "HEAD")
|
|
124
|
+
|
|
125
|
+
assert (
|
|
126
|
+
action.main(
|
|
127
|
+
[
|
|
128
|
+
"--repository-root",
|
|
129
|
+
str(tmp_path),
|
|
130
|
+
"--tag",
|
|
131
|
+
"v1.2.3",
|
|
132
|
+
"--commit",
|
|
133
|
+
later_commit,
|
|
134
|
+
]
|
|
135
|
+
)
|
|
136
|
+
== 1
|
|
137
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from dataclasses import dataclass, make_dataclass
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import Annotated, Literal
|
|
4
|
+
from typing import Annotated, Any, Literal
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
from annotated_types import Ge, Gt, Le, MinLen, Predicate
|
|
@@ -9,6 +10,7 @@ from python_introspect import (
|
|
|
9
10
|
AnnotatedDataclassValidationMixin,
|
|
10
11
|
overlay_non_none_dataclass,
|
|
11
12
|
validate_annotated_dataclass,
|
|
13
|
+
validate_annotation_value,
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
|
|
@@ -93,6 +95,19 @@ def test_literal_validation_preserves_nominal_identity() -> None:
|
|
|
93
95
|
LiteralConfig(value=True)
|
|
94
96
|
|
|
95
97
|
|
|
98
|
+
def test_callable_validation_uses_the_declared_runtime_contract() -> None:
|
|
99
|
+
class CallableInstance:
|
|
100
|
+
def __call__(self) -> None:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
annotation = Callable[..., Any]
|
|
104
|
+
validate_annotation_value(annotation, max, path="reducer")
|
|
105
|
+
validate_annotation_value(annotation, CallableInstance(), path="reducer")
|
|
106
|
+
|
|
107
|
+
with pytest.raises(TypeError, match="reducer must be callable"):
|
|
108
|
+
validate_annotation_value(annotation, 7, path="reducer")
|
|
109
|
+
|
|
110
|
+
|
|
96
111
|
@dataclass(frozen=True)
|
|
97
112
|
class ConnectionSpec:
|
|
98
113
|
host: str = "localhost"
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/annotation_types.py
RENAMED
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/callable_declaration.py
RENAMED
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect/dataclass_projection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/src/python_introspect.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_introspect-0.1.10 → python_introspect-0.1.12}/tests/test_unified_parameter_analyzer.py
RENAMED
|
File without changes
|