python-introspect 0.1.4__tar.gz → 0.1.5__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.5/PKG-INFO +90 -0
- python_introspect-0.1.5/README.md +55 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/pyproject.toml +8 -7
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/__init__.py +11 -3
- python_introspect-0.1.5/src/python_introspect/enableable.py +156 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/signature_analyzer.py +577 -355
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/unified_parameter_analyzer.py +169 -50
- python_introspect-0.1.5/src/python_introspect.egg-info/PKG-INFO +90 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/requires.txt +1 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_init.py +5 -1
- {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_signature_analyzer.py +111 -4
- {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_unified_parameter_analyzer.py +67 -5
- 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/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.5}/LICENSE +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/setup.cfg +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/exceptions.py +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/SOURCES.txt +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/dependency_links.txt +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/top_level.txt +0 -0
- {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_exceptions.py +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-introspect
|
|
3
|
+
Version: 0.1.5
|
|
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: metaclass-registry>=0.1.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
32
|
+
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-autodoc-typehints>=1.24.0; extra == "docs"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# python-introspect
|
|
37
|
+
|
|
38
|
+
Extensible analysis of callable signatures, dataclass fields, type hints, and
|
|
39
|
+
docstrings.
|
|
40
|
+
|
|
41
|
+
[](https://www.python.org/downloads/)
|
|
42
|
+
[](https://badge.fury.io/py/python-introspect)
|
|
43
|
+
|
|
44
|
+
## Quick start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from python_introspect import SignatureAnalyzer
|
|
48
|
+
|
|
49
|
+
def resize(image, factor: float = 0.5, *, preserve_range: bool = True):
|
|
50
|
+
"""Resize an image.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
image: Input image.
|
|
54
|
+
factor: Scale factor.
|
|
55
|
+
preserve_range: Preserve the input intensity range.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
parameters = SignatureAnalyzer().analyze(resize)
|
|
59
|
+
|
|
60
|
+
for name, info in parameters.items():
|
|
61
|
+
print(name, info.param_type, info.default_value, info.is_required)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
``analyze`` is the unified entry point for functions, methods, classes,
|
|
65
|
+
dataclass types, and instances. It returns a mapping of names to
|
|
66
|
+
``ParameterInfo`` records.
|
|
67
|
+
|
|
68
|
+
## Extension points
|
|
69
|
+
|
|
70
|
+
Use ``register_namespace_provider`` to contribute names used while resolving
|
|
71
|
+
forward references and ``register_type_resolver`` to unwrap application proxy
|
|
72
|
+
types. Wrappers can declare their user-facing inspection target through the
|
|
73
|
+
signature-target helpers in ``python_introspect.signature_analyzer``.
|
|
74
|
+
|
|
75
|
+
## Installation
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m pip install python-introspect
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The runtime depends on metaclass-registry. Repository and issues:
|
|
82
|
+
[OpenHCSDev/python-introspect](https://github.com/OpenHCSDev/python-introspect).
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
The maintained sources are in [`docs/source`](docs/source). Documentation
|
|
87
|
+
changes are checked by the repository's [documentation
|
|
88
|
+
workflow](https://github.com/OpenHCSDev/python-introspect/actions/workflows/docs.yml);
|
|
89
|
+
the local warnings-as-errors build command is documented in
|
|
90
|
+
[`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.5"
|
|
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,9 @@ classifiers = [
|
|
|
26
25
|
"Topic :: Utilities",
|
|
27
26
|
]
|
|
28
27
|
|
|
29
|
-
dependencies = [
|
|
28
|
+
dependencies = [
|
|
29
|
+
"metaclass-registry>=0.1.0",
|
|
30
|
+
]
|
|
30
31
|
|
|
31
32
|
[project.optional-dependencies]
|
|
32
33
|
dev = [
|
|
@@ -58,14 +59,14 @@ python_functions = ["test_*"]
|
|
|
58
59
|
|
|
59
60
|
[tool.ruff]
|
|
60
61
|
line-length = 100
|
|
61
|
-
target-version = "
|
|
62
|
+
target-version = "py310"
|
|
62
63
|
|
|
63
64
|
[tool.black]
|
|
64
65
|
line-length = 100
|
|
65
|
-
target-version = ["
|
|
66
|
+
target-version = ["py310"]
|
|
66
67
|
|
|
67
68
|
[tool.mypy]
|
|
68
|
-
python_version = "
|
|
69
|
+
python_version = "3.10"
|
|
69
70
|
warn_return_any = true
|
|
70
71
|
warn_unused_configs = true
|
|
71
72
|
disallow_untyped_defs = true
|
|
@@ -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.5"
|
|
13
13
|
|
|
14
14
|
from .signature_analyzer import (
|
|
15
15
|
SignatureAnalyzer,
|
|
@@ -19,10 +19,15 @@ from .signature_analyzer import (
|
|
|
19
19
|
# Plugin registration
|
|
20
20
|
register_namespace_provider,
|
|
21
21
|
register_type_resolver,
|
|
22
|
+
set_signature_analysis_target,
|
|
23
|
+
signature_analysis_target,
|
|
22
24
|
)
|
|
23
25
|
from .unified_parameter_analyzer import (
|
|
24
26
|
UnifiedParameterAnalyzer,
|
|
25
27
|
UnifiedParameterInfo,
|
|
28
|
+
add_parameter_exclusions,
|
|
29
|
+
set_parameter_exclusions,
|
|
30
|
+
parameter_exclusions,
|
|
26
31
|
)
|
|
27
32
|
from .exceptions import (
|
|
28
33
|
IntrospectionError,
|
|
@@ -34,7 +39,6 @@ from .enableable import (
|
|
|
34
39
|
Enableable,
|
|
35
40
|
is_enableable,
|
|
36
41
|
mark_enableable,
|
|
37
|
-
ENABLED_FIELD,
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
__all__ = [
|
|
@@ -48,9 +52,14 @@ __all__ = [
|
|
|
48
52
|
# Plugin registration
|
|
49
53
|
"register_namespace_provider",
|
|
50
54
|
"register_type_resolver",
|
|
55
|
+
"set_signature_analysis_target",
|
|
56
|
+
"signature_analysis_target",
|
|
51
57
|
# Unified analysis
|
|
52
58
|
"UnifiedParameterAnalyzer",
|
|
53
59
|
"UnifiedParameterInfo",
|
|
60
|
+
"add_parameter_exclusions",
|
|
61
|
+
"set_parameter_exclusions",
|
|
62
|
+
"parameter_exclusions",
|
|
54
63
|
# Exceptions
|
|
55
64
|
"IntrospectionError",
|
|
56
65
|
"SignatureAnalysisError",
|
|
@@ -60,5 +69,4 @@ __all__ = [
|
|
|
60
69
|
"Enableable",
|
|
61
70
|
"is_enableable",
|
|
62
71
|
"mark_enableable",
|
|
63
|
-
"ENABLED_FIELD",
|
|
64
72
|
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Nominal enable semantics as type-safe metadata.
|
|
2
|
+
|
|
3
|
+
This module provides a single, shared "axis" for objects and callables that
|
|
4
|
+
participate in enabled semantics.
|
|
5
|
+
|
|
6
|
+
Design goals:
|
|
7
|
+
- Nominal (not structural): only explicitly branded callables qualify.
|
|
8
|
+
- Dataclass-friendly: configs can inherit Enableable to get an enabled field.
|
|
9
|
+
- Callable-safe: branded callables must declare an `enabled` parameter.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
from abc import ABC, ABCMeta
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass, fields
|
|
18
|
+
from typing import Any
|
|
19
|
+
from typing import get_type_hints
|
|
20
|
+
from weakref import WeakKeyDictionary
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_ENABLEABLE_TAG = object()
|
|
24
|
+
_enableable_objects: WeakKeyDictionary[Any, object] = WeakKeyDictionary()
|
|
25
|
+
_enableable_objects_by_id: dict[int, tuple[Any, object]] = {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _remember_enableable(obj: Any) -> None:
|
|
29
|
+
"""Record explicit enableable branding without mutating the object."""
|
|
30
|
+
try:
|
|
31
|
+
_enableable_objects[obj] = _ENABLEABLE_TAG
|
|
32
|
+
except TypeError:
|
|
33
|
+
_enableable_objects_by_id[id(obj)] = (obj, _ENABLEABLE_TAG)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_marked_enableable(obj: Any) -> bool:
|
|
37
|
+
"""Return whether an object was explicitly branded as enableable."""
|
|
38
|
+
try:
|
|
39
|
+
if _enableable_objects.get(obj) is _ENABLEABLE_TAG:
|
|
40
|
+
return True
|
|
41
|
+
except TypeError:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
fallback_record = _enableable_objects_by_id.get(id(obj))
|
|
45
|
+
return fallback_record is not None and fallback_record[0] is obj
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class EnableableMeta(ABCMeta):
|
|
49
|
+
"""Metaclass enabling nominal isinstance checks for branded callables."""
|
|
50
|
+
|
|
51
|
+
def __instancecheck__(cls, instance: Any) -> bool: # type: ignore[override]
|
|
52
|
+
if _is_marked_enableable(instance):
|
|
53
|
+
return True
|
|
54
|
+
return super().__instancecheck__(instance)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Enableable(ABC, metaclass=EnableableMeta):
|
|
59
|
+
"""Mixin indicating an object participates in enabled semantics."""
|
|
60
|
+
|
|
61
|
+
enabled: bool = True
|
|
62
|
+
"""Run this callable or configuration when enabled; skip it when disabled."""
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def callable_field(cls):
|
|
66
|
+
"""Return the dataclass field that defines callable enable semantics."""
|
|
67
|
+
return fields(Enableable)[0]
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def require_parameter_name(cls) -> str:
|
|
71
|
+
return cls.callable_field().name
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def default_value(cls) -> bool:
|
|
75
|
+
return cls.callable_field().default
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def annotation_type(cls) -> type[bool]:
|
|
79
|
+
return get_type_hints(Enableable)[cls.require_parameter_name()]
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def parameter(cls) -> inspect.Parameter:
|
|
83
|
+
return inspect.Parameter(
|
|
84
|
+
cls.require_parameter_name(),
|
|
85
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
86
|
+
default=cls.default_value(),
|
|
87
|
+
annotation=cls.annotation_type(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def parameter_in(cls, values: Mapping[Any, Any]) -> bool:
|
|
92
|
+
"""Return whether a kwargs-like mapping carries the enable parameter."""
|
|
93
|
+
return cls.require_parameter_name() in values
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def is_parameter_key(cls, key: Any) -> bool:
|
|
97
|
+
"""Return whether key names the enable parameter."""
|
|
98
|
+
return key == cls.require_parameter_name()
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def disabled_in(cls, values: Mapping[Any, Any]) -> bool:
|
|
102
|
+
"""Return whether a kwargs-like mapping explicitly disables execution."""
|
|
103
|
+
if not cls.parameter_in(values):
|
|
104
|
+
return False
|
|
105
|
+
return values[cls.require_parameter_name()] is False
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def without_parameter(cls, values: Mapping[Any, Any]) -> dict[Any, Any]:
|
|
109
|
+
"""Return a copy of mapping values without the enable parameter."""
|
|
110
|
+
return {
|
|
111
|
+
key: value
|
|
112
|
+
for key, value in values.items()
|
|
113
|
+
if not cls.is_parameter_key(key)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def is_enableable(obj: Any) -> bool:
|
|
118
|
+
"""Return True iff obj is nominally Enableable.
|
|
119
|
+
|
|
120
|
+
Works for both instances (using isinstance) and classes (using issubclass).
|
|
121
|
+
This is needed because widget creation code needs to check if a type (class)
|
|
122
|
+
is enableable, not just instances.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Check if obj is a type/class
|
|
126
|
+
if isinstance(obj, type):
|
|
127
|
+
# obj is a class - check if it's a subclass of Enableable
|
|
128
|
+
try:
|
|
129
|
+
return issubclass(obj, Enableable)
|
|
130
|
+
except TypeError:
|
|
131
|
+
# obj is not a class or is not class-like (e.g., a generic type)
|
|
132
|
+
return False
|
|
133
|
+
else:
|
|
134
|
+
# obj is an instance - use isinstance
|
|
135
|
+
return isinstance(obj, Enableable)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def mark_enableable(obj: Any, *, enabled_default: bool = True) -> Any:
|
|
139
|
+
"""Nominally brand an object/callable as Enableable.
|
|
140
|
+
|
|
141
|
+
This does not wrap and does not change call semantics.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
_ = enabled_default # reserved for future: default enabled semantics
|
|
145
|
+
|
|
146
|
+
# If we're branding a callable, require the enabled kwarg to exist.
|
|
147
|
+
if callable(obj) and not isinstance(obj, type):
|
|
148
|
+
sig = inspect.signature(obj)
|
|
149
|
+
parameter_name = Enableable.require_parameter_name()
|
|
150
|
+
if parameter_name not in sig.parameters:
|
|
151
|
+
raise TypeError(
|
|
152
|
+
f"Enableable callable {obj!r} must have an '{parameter_name}' parameter"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
_remember_enableable(obj)
|
|
156
|
+
return obj
|