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.
Files changed (23) hide show
  1. python_introspect-0.1.5/PKG-INFO +90 -0
  2. python_introspect-0.1.5/README.md +55 -0
  3. {python_introspect-0.1.4 → python_introspect-0.1.5}/pyproject.toml +8 -7
  4. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/__init__.py +11 -3
  5. python_introspect-0.1.5/src/python_introspect/enableable.py +156 -0
  6. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/signature_analyzer.py +577 -355
  7. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/unified_parameter_analyzer.py +169 -50
  8. python_introspect-0.1.5/src/python_introspect.egg-info/PKG-INFO +90 -0
  9. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/requires.txt +1 -0
  10. {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_init.py +5 -1
  11. {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_signature_analyzer.py +111 -4
  12. {python_introspect-0.1.4 → python_introspect-0.1.5}/tests/test_unified_parameter_analyzer.py +67 -5
  13. python_introspect-0.1.4/PKG-INFO +0 -134
  14. python_introspect-0.1.4/README.md +0 -99
  15. python_introspect-0.1.4/src/python_introspect/enableable.py +0 -79
  16. python_introspect-0.1.4/src/python_introspect.egg-info/PKG-INFO +0 -134
  17. {python_introspect-0.1.4 → python_introspect-0.1.5}/LICENSE +0 -0
  18. {python_introspect-0.1.4 → python_introspect-0.1.5}/setup.cfg +0 -0
  19. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect/exceptions.py +0 -0
  20. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/SOURCES.txt +0 -0
  21. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/dependency_links.txt +0 -0
  22. {python_introspect-0.1.4 → python_introspect-0.1.5}/src/python_introspect.egg-info/top_level.txt +0 -0
  23. {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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
42
+ [![PyPI version](https://badge.fury.io/py/python-introspect.svg)](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
+ [![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.5"
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,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 = "0.1.4"
62
+ target-version = "py310"
62
63
 
63
64
  [tool.black]
64
65
  line-length = 100
65
- target-version = ["py39"]
66
+ target-version = ["py310"]
66
67
 
67
68
  [tool.mypy]
68
- python_version = "0.1.4"
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.4"
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