pyrig 2.2.6__py3-none-any.whl
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.
- pyrig/__init__.py +1 -0
- pyrig/dev/__init__.py +6 -0
- pyrig/dev/builders/__init__.py +1 -0
- pyrig/dev/builders/base/__init__.py +5 -0
- pyrig/dev/builders/base/base.py +256 -0
- pyrig/dev/builders/pyinstaller.py +229 -0
- pyrig/dev/cli/__init__.py +5 -0
- pyrig/dev/cli/cli.py +95 -0
- pyrig/dev/cli/commands/__init__.py +1 -0
- pyrig/dev/cli/commands/build_artifacts.py +16 -0
- pyrig/dev/cli/commands/create_root.py +25 -0
- pyrig/dev/cli/commands/create_tests.py +244 -0
- pyrig/dev/cli/commands/init_project.py +160 -0
- pyrig/dev/cli/commands/make_inits.py +27 -0
- pyrig/dev/cli/commands/protect_repo.py +145 -0
- pyrig/dev/cli/shared_subcommands.py +20 -0
- pyrig/dev/cli/subcommands.py +73 -0
- pyrig/dev/configs/__init__.py +1 -0
- pyrig/dev/configs/base/__init__.py +5 -0
- pyrig/dev/configs/base/base.py +826 -0
- pyrig/dev/configs/containers/__init__.py +1 -0
- pyrig/dev/configs/containers/container_file.py +111 -0
- pyrig/dev/configs/dot_env.py +95 -0
- pyrig/dev/configs/dot_python_version.py +88 -0
- pyrig/dev/configs/git/__init__.py +5 -0
- pyrig/dev/configs/git/gitignore.py +181 -0
- pyrig/dev/configs/git/pre_commit.py +170 -0
- pyrig/dev/configs/licence.py +112 -0
- pyrig/dev/configs/markdown/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/index.py +38 -0
- pyrig/dev/configs/markdown/readme.py +132 -0
- pyrig/dev/configs/py_typed.py +28 -0
- pyrig/dev/configs/pyproject.py +436 -0
- pyrig/dev/configs/python/__init__.py +5 -0
- pyrig/dev/configs/python/builders_init.py +27 -0
- pyrig/dev/configs/python/configs_init.py +28 -0
- pyrig/dev/configs/python/dot_experiment.py +46 -0
- pyrig/dev/configs/python/main.py +59 -0
- pyrig/dev/configs/python/resources_init.py +27 -0
- pyrig/dev/configs/python/shared_subcommands.py +29 -0
- pyrig/dev/configs/python/src_init.py +27 -0
- pyrig/dev/configs/python/subcommands.py +27 -0
- pyrig/dev/configs/testing/__init__.py +5 -0
- pyrig/dev/configs/testing/conftest.py +64 -0
- pyrig/dev/configs/testing/fixtures_init.py +27 -0
- pyrig/dev/configs/testing/main_test.py +74 -0
- pyrig/dev/configs/testing/zero_test.py +43 -0
- pyrig/dev/configs/workflows/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/base.py +1662 -0
- pyrig/dev/configs/workflows/build.py +106 -0
- pyrig/dev/configs/workflows/health_check.py +133 -0
- pyrig/dev/configs/workflows/publish.py +68 -0
- pyrig/dev/configs/workflows/release.py +90 -0
- pyrig/dev/tests/__init__.py +5 -0
- pyrig/dev/tests/conftest.py +40 -0
- pyrig/dev/tests/fixtures/__init__.py +1 -0
- pyrig/dev/tests/fixtures/assertions.py +147 -0
- pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
- pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
- pyrig/dev/tests/fixtures/autouse/module.py +40 -0
- pyrig/dev/tests/fixtures/autouse/session.py +589 -0
- pyrig/dev/tests/fixtures/factories.py +118 -0
- pyrig/dev/utils/__init__.py +1 -0
- pyrig/dev/utils/cli.py +17 -0
- pyrig/dev/utils/git.py +312 -0
- pyrig/dev/utils/packages.py +93 -0
- pyrig/dev/utils/resources.py +77 -0
- pyrig/dev/utils/testing.py +66 -0
- pyrig/dev/utils/versions.py +268 -0
- pyrig/main.py +9 -0
- pyrig/py.typed +0 -0
- pyrig/resources/GITIGNORE +216 -0
- pyrig/resources/LATEST_PYTHON_VERSION +1 -0
- pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
- pyrig/resources/__init__.py +1 -0
- pyrig/src/__init__.py +1 -0
- pyrig/src/git/__init__.py +6 -0
- pyrig/src/git/git.py +146 -0
- pyrig/src/graph.py +255 -0
- pyrig/src/iterate.py +107 -0
- pyrig/src/modules/__init__.py +22 -0
- pyrig/src/modules/class_.py +369 -0
- pyrig/src/modules/function.py +189 -0
- pyrig/src/modules/inspection.py +148 -0
- pyrig/src/modules/module.py +658 -0
- pyrig/src/modules/package.py +452 -0
- pyrig/src/os/__init__.py +6 -0
- pyrig/src/os/os.py +121 -0
- pyrig/src/project/__init__.py +5 -0
- pyrig/src/project/mgt.py +83 -0
- pyrig/src/resource.py +58 -0
- pyrig/src/string.py +100 -0
- pyrig/src/testing/__init__.py +6 -0
- pyrig/src/testing/assertions.py +66 -0
- pyrig/src/testing/convention.py +203 -0
- pyrig-2.2.6.dist-info/METADATA +174 -0
- pyrig-2.2.6.dist-info/RECORD +102 -0
- pyrig-2.2.6.dist-info/WHEEL +4 -0
- pyrig-2.2.6.dist-info/entry_points.txt +3 -0
- pyrig-2.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Class introspection and subclass discovery utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for inspecting Python classes, extracting their
|
|
4
|
+
methods, and discovering subclasses across packages. The subclass discovery
|
|
5
|
+
system is central to pyrig's plugin architecture, enabling automatic discovery
|
|
6
|
+
of ConfigFile implementations, Builder subclasses, and other extensible
|
|
7
|
+
components.
|
|
8
|
+
|
|
9
|
+
Key features:
|
|
10
|
+
- Method extraction with optional parent class filtering
|
|
11
|
+
- Class discovery within modules
|
|
12
|
+
- Recursive subclass discovery with package loading
|
|
13
|
+
- Intelligent "leaf class" filtering via `discard_parents`
|
|
14
|
+
|
|
15
|
+
The `discard_parents` feature is particularly important: when multiple packages
|
|
16
|
+
define subclasses in a chain (e.g., BaseConfig -> PyrigConfig -> UserConfig),
|
|
17
|
+
only the most specific (leaf) classes are kept. This enables clean override
|
|
18
|
+
behavior where user customizations replace base implementations.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> from pyrig.src.modules.class_ import get_all_nonabstract_subclasses
|
|
22
|
+
>>> subclasses = get_all_nonabstract_subclasses(
|
|
23
|
+
... ConfigFile,
|
|
24
|
+
... load_package_before=my_package.dev.configs,
|
|
25
|
+
... discard_parents=True
|
|
26
|
+
... )
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import inspect
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from importlib import import_module
|
|
32
|
+
from types import ModuleType
|
|
33
|
+
from typing import Any, overload
|
|
34
|
+
|
|
35
|
+
from pyrig.src.modules.function import is_func
|
|
36
|
+
from pyrig.src.modules.inspection import get_def_line, get_obj_members
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_all_methods_from_cls(
|
|
40
|
+
class_: type,
|
|
41
|
+
*,
|
|
42
|
+
exclude_parent_methods: bool = False,
|
|
43
|
+
include_annotate: bool = False,
|
|
44
|
+
) -> list[Callable[..., Any]]:
|
|
45
|
+
"""Extract all methods from a class.
|
|
46
|
+
|
|
47
|
+
Retrieves all method-like attributes from a class, including instance
|
|
48
|
+
methods, static methods, class methods, and properties. Methods are
|
|
49
|
+
returned sorted by their definition order in the source code.
|
|
50
|
+
|
|
51
|
+
This is used by pyrig to generate test skeletons for each method
|
|
52
|
+
in a class.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
class_: The class to extract methods from.
|
|
56
|
+
exclude_parent_methods: If True, only includes methods defined directly
|
|
57
|
+
in this class, excluding inherited methods. Useful when generating
|
|
58
|
+
tests only for new methods in a subclass.
|
|
59
|
+
include_annotate: If False (default), excludes `__annotate__` methods
|
|
60
|
+
introduced in Python 3.14.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A list of callable method objects, sorted by their line number
|
|
64
|
+
in the source file.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> class MyClass:
|
|
68
|
+
... def method_a(self): pass
|
|
69
|
+
... def method_b(self): pass
|
|
70
|
+
>>> methods = get_all_methods_from_cls(MyClass)
|
|
71
|
+
>>> [m.__name__ for m in methods]
|
|
72
|
+
['method_a', 'method_b']
|
|
73
|
+
"""
|
|
74
|
+
from pyrig.src.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
75
|
+
get_module_of_obj,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
methods = [
|
|
79
|
+
(method, name)
|
|
80
|
+
for name, method in get_obj_members(class_, include_annotate=include_annotate)
|
|
81
|
+
if is_func(method)
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
if exclude_parent_methods:
|
|
85
|
+
methods = [
|
|
86
|
+
(method, name)
|
|
87
|
+
for method, name in methods
|
|
88
|
+
if get_module_of_obj(method).__name__ == class_.__module__
|
|
89
|
+
and name in class_.__dict__
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
only_methods = [method for method, _name in methods]
|
|
93
|
+
# sort by definition order
|
|
94
|
+
return sorted(only_methods, key=get_def_line)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
|
|
98
|
+
"""Extract all classes defined directly in a module.
|
|
99
|
+
|
|
100
|
+
Retrieves all class objects that are defined in the specified module,
|
|
101
|
+
excluding classes imported from other modules. Classes are returned
|
|
102
|
+
sorted by their definition order.
|
|
103
|
+
|
|
104
|
+
This is used by pyrig to discover classes for test skeleton generation.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
module: The module to extract classes from. Can be a module object
|
|
108
|
+
or a fully qualified module name string.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A list of class types defined in the module, sorted by their
|
|
112
|
+
definition order in the source file.
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
Handles edge cases like Rust-backed classes (e.g., cryptography's
|
|
116
|
+
AESGCM) that may not have standard `__module__` attributes.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> import my_module
|
|
120
|
+
>>> classes = get_all_cls_from_module(my_module)
|
|
121
|
+
>>> [c.__name__ for c in classes]
|
|
122
|
+
['ClassA', 'ClassB']
|
|
123
|
+
"""
|
|
124
|
+
from pyrig.src.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
125
|
+
get_module_of_obj,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if isinstance(module, str):
|
|
129
|
+
module = import_module(module)
|
|
130
|
+
|
|
131
|
+
# necessary for bindings packages like AESGCM from cryptography._rust backend
|
|
132
|
+
default = ModuleType("default")
|
|
133
|
+
classes = [
|
|
134
|
+
obj
|
|
135
|
+
for _, obj in inspect.getmembers(module, inspect.isclass)
|
|
136
|
+
if get_module_of_obj(obj, default).__name__ == module.__name__
|
|
137
|
+
]
|
|
138
|
+
# sort by definition order
|
|
139
|
+
return sorted(classes, key=get_def_line)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_all_subclasses[T: type](
|
|
143
|
+
cls: T,
|
|
144
|
+
load_package_before: ModuleType | None = None,
|
|
145
|
+
*,
|
|
146
|
+
discard_parents: bool = False,
|
|
147
|
+
) -> set[T]:
|
|
148
|
+
"""Recursively discover all subclasses of a class.
|
|
149
|
+
|
|
150
|
+
Finds all direct and indirect subclasses of the given class. Because
|
|
151
|
+
Python's `__subclasses__()` only returns classes that have been imported,
|
|
152
|
+
you can optionally specify a package to walk (import) before discovery.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
cls: The base class to find subclasses of.
|
|
156
|
+
load_package_before: Optional package to walk before discovery. All
|
|
157
|
+
modules in this package will be imported, ensuring any subclasses
|
|
158
|
+
defined there are registered with Python's subclass tracking.
|
|
159
|
+
When provided, results are filtered to only include classes from
|
|
160
|
+
this package.
|
|
161
|
+
discard_parents: If True, removes parent classes from the result when
|
|
162
|
+
both a parent and its child are present. This keeps only the most
|
|
163
|
+
specific (leaf) classes, enabling clean override behavior.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
A set of all subclasses (including the original class itself when
|
|
167
|
+
`load_package_before` is not specified).
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> class Base: pass
|
|
171
|
+
>>> class Child(Base): pass
|
|
172
|
+
>>> class GrandChild(Child): pass
|
|
173
|
+
>>> get_all_subclasses(Base)
|
|
174
|
+
{Base, Child, GrandChild}
|
|
175
|
+
>>> get_all_subclasses(Base, discard_parents=True)
|
|
176
|
+
{GrandChild}
|
|
177
|
+
"""
|
|
178
|
+
from pyrig.src.modules.package import ( # noqa: PLC0415 # avoid circular import
|
|
179
|
+
walk_package,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if load_package_before:
|
|
183
|
+
_ = list(walk_package(load_package_before))
|
|
184
|
+
subclasses_set: set[T] = {cls}
|
|
185
|
+
subclasses_set.update(cls.__subclasses__())
|
|
186
|
+
for subclass in cls.__subclasses__():
|
|
187
|
+
subclasses_set.update(get_all_subclasses(subclass))
|
|
188
|
+
if load_package_before is not None:
|
|
189
|
+
# remove all not in the package
|
|
190
|
+
subclasses_set = {
|
|
191
|
+
subclass
|
|
192
|
+
for subclass in subclasses_set
|
|
193
|
+
if load_package_before.__name__ in subclass.__module__
|
|
194
|
+
}
|
|
195
|
+
if discard_parents:
|
|
196
|
+
subclasses_set = discard_parent_classes(subclasses_set)
|
|
197
|
+
return subclasses_set
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_all_nonabstract_subclasses[T: type](
|
|
201
|
+
cls: T,
|
|
202
|
+
load_package_before: ModuleType | None = None,
|
|
203
|
+
*,
|
|
204
|
+
discard_parents: bool = False,
|
|
205
|
+
) -> set[T]:
|
|
206
|
+
"""Find all concrete (non-abstract) subclasses of a class.
|
|
207
|
+
|
|
208
|
+
Similar to `get_all_subclasses`, but filters out abstract classes
|
|
209
|
+
(those with unimplemented abstract methods). This is the primary
|
|
210
|
+
function used by pyrig to discover implementations of ConfigFile,
|
|
211
|
+
Builder, and other extensible base classes.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
cls: The base class to find subclasses of.
|
|
215
|
+
load_package_before: Optional package to walk before discovery.
|
|
216
|
+
See `get_all_subclasses` for details.
|
|
217
|
+
discard_parents: If True, keeps only leaf classes when a parent
|
|
218
|
+
and child are both present.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
A set of all non-abstract subclasses.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> from abc import ABC, abstractmethod
|
|
225
|
+
>>> class Base(ABC):
|
|
226
|
+
... @abstractmethod
|
|
227
|
+
... def method(self): pass
|
|
228
|
+
>>> class Concrete(Base):
|
|
229
|
+
... def method(self): pass
|
|
230
|
+
>>> get_all_nonabstract_subclasses(Base)
|
|
231
|
+
{Concrete}
|
|
232
|
+
"""
|
|
233
|
+
return {
|
|
234
|
+
subclass
|
|
235
|
+
for subclass in get_all_subclasses(
|
|
236
|
+
cls,
|
|
237
|
+
load_package_before=load_package_before,
|
|
238
|
+
discard_parents=discard_parents,
|
|
239
|
+
)
|
|
240
|
+
if not inspect.isabstract(subclass)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def init_all_nonabstract_subclasses[T: type](
|
|
245
|
+
cls: T,
|
|
246
|
+
load_package_before: ModuleType | None = None,
|
|
247
|
+
*,
|
|
248
|
+
discard_parents: bool = False,
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Discover and instantiate all concrete subclasses of a class.
|
|
251
|
+
|
|
252
|
+
Finds all non-abstract subclasses and calls their default constructor
|
|
253
|
+
(no arguments). This is used by pyrig's ConfigFile and Builder systems
|
|
254
|
+
to automatically initialize all discovered implementations.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
cls: The base class to find and instantiate subclasses of.
|
|
258
|
+
load_package_before: Optional package to walk before discovery.
|
|
259
|
+
discard_parents: If True, only instantiates leaf classes.
|
|
260
|
+
|
|
261
|
+
Note:
|
|
262
|
+
All subclasses must have a no-argument `__init__` or be classes
|
|
263
|
+
that can be called with no arguments (e.g., using `__init_subclass__`
|
|
264
|
+
or `__new__` for initialization).
|
|
265
|
+
"""
|
|
266
|
+
for subclass in get_all_nonabstract_subclasses(
|
|
267
|
+
cls,
|
|
268
|
+
load_package_before=load_package_before,
|
|
269
|
+
discard_parents=discard_parents,
|
|
270
|
+
):
|
|
271
|
+
subclass()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_all_nonabst_subcls_from_mod_in_all_deps_depen_on_dep[T: type](
|
|
275
|
+
cls: T,
|
|
276
|
+
dep: ModuleType,
|
|
277
|
+
load_package_before: ModuleType,
|
|
278
|
+
*,
|
|
279
|
+
discard_parents: bool = False,
|
|
280
|
+
) -> list[T]:
|
|
281
|
+
"""Find non-abstract subclasses across all packages depending on a dependency.
|
|
282
|
+
|
|
283
|
+
This is the core discovery function for pyrig's multi-package architecture.
|
|
284
|
+
It finds all packages that depend on `dep`, looks for the same relative
|
|
285
|
+
module path as `load_package_before` in each, and discovers subclasses
|
|
286
|
+
of `cls` in those modules.
|
|
287
|
+
|
|
288
|
+
For example, if `dep` is smth and `load_package_before` is
|
|
289
|
+
`smth.dev.configs`, this will find `myapp.dev.configs` in any package
|
|
290
|
+
that depends on smth, and discover ConfigFile subclasses there.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
cls: The base class to find subclasses of.
|
|
294
|
+
dep: The dependency package that other packages depend on (e.g., pyrig or smth).
|
|
295
|
+
load_package_before: The module path within `dep` to use as a template
|
|
296
|
+
for finding equivalent modules in dependent packages.
|
|
297
|
+
discard_parents: If True, keeps only leaf classes when inheritance
|
|
298
|
+
chains span multiple packages.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A list of all discovered non-abstract subclasses. Classes from the
|
|
302
|
+
same module are grouped together, but ordering between packages
|
|
303
|
+
depends on the dependency graph traversal order.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> # Find all ConfigFile implementations across the ecosystem
|
|
307
|
+
>>> subclasses = get_all_nonabst_subcls_from_mod_in_all_deps_depen_on_dep(
|
|
308
|
+
... ConfigFile,
|
|
309
|
+
... smth,
|
|
310
|
+
... smth.dev.configs,
|
|
311
|
+
... discard_parents=True
|
|
312
|
+
... )
|
|
313
|
+
"""
|
|
314
|
+
from pyrig.src.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
315
|
+
get_same_modules_from_deps_depen_on_dep,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
subclasses: list[T] = []
|
|
319
|
+
for pkg in get_same_modules_from_deps_depen_on_dep(load_package_before, dep):
|
|
320
|
+
subclasses.extend(
|
|
321
|
+
get_all_nonabstract_subclasses(
|
|
322
|
+
cls,
|
|
323
|
+
load_package_before=pkg,
|
|
324
|
+
discard_parents=discard_parents,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
# as these are different modules and pks we need to discard parents again
|
|
328
|
+
if discard_parents:
|
|
329
|
+
subclasses = discard_parent_classes(subclasses)
|
|
330
|
+
return subclasses
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@overload
|
|
334
|
+
def discard_parent_classes[T: type](classes: list[T]) -> list[T]: ...
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@overload
|
|
338
|
+
def discard_parent_classes[T: type](classes: set[T]) -> set[T]: ...
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def discard_parent_classes[T: type](
|
|
342
|
+
classes: list[T] | set[T],
|
|
343
|
+
) -> list[T] | set[T]:
|
|
344
|
+
"""Remove parent classes when their children are also present.
|
|
345
|
+
|
|
346
|
+
Filters a collection of classes to keep only "leaf" classes - those
|
|
347
|
+
that have no subclasses in the collection. This enables clean override
|
|
348
|
+
behavior: if you subclass a ConfigFile, only your subclass will be used.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
classes: A list or set of class types to filter. Modified in place.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
The same collection with parent classes removed. The return type
|
|
355
|
+
matches the input type (list or set).
|
|
356
|
+
|
|
357
|
+
Example:
|
|
358
|
+
>>> class A: pass
|
|
359
|
+
>>> class B(A): pass
|
|
360
|
+
>>> class C(B): pass
|
|
361
|
+
>>> discard_parent_classes({A, B, C})
|
|
362
|
+
{C}
|
|
363
|
+
>>> discard_parent_classes([A, C]) # B not in list, so A stays
|
|
364
|
+
[A, C]
|
|
365
|
+
"""
|
|
366
|
+
for cls in classes.copy():
|
|
367
|
+
if any(child in classes for child in cls.__subclasses__()):
|
|
368
|
+
classes.remove(cls)
|
|
369
|
+
return classes
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Function detection and extraction utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for identifying callable objects and extracting
|
|
4
|
+
functions from modules. It handles the various forms that "functions" can take
|
|
5
|
+
in Python, including plain functions, methods, staticmethods, classmethods,
|
|
6
|
+
properties, and decorated functions.
|
|
7
|
+
|
|
8
|
+
These utilities are used by pyrig to discover CLI subcommands from modules
|
|
9
|
+
and to extract testable functions for automatic test skeleton generation.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from pyrig.src.modules.function import get_all_functions_from_module
|
|
13
|
+
>>> import my_module
|
|
14
|
+
>>> functions = get_all_functions_from_module(my_module)
|
|
15
|
+
>>> [f.__name__ for f in functions]
|
|
16
|
+
['func_a', 'func_b', 'func_c']
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from importlib import import_module
|
|
22
|
+
from types import ModuleType
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from pyrig.src.modules.inspection import get_def_line, get_obj_members
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_func_or_method(obj: Any) -> bool:
|
|
29
|
+
"""Check if an object is a plain function or bound method.
|
|
30
|
+
|
|
31
|
+
This is a basic check using `inspect.isfunction` and `inspect.ismethod`.
|
|
32
|
+
For a more comprehensive check that includes staticmethods, classmethods,
|
|
33
|
+
and properties, use `is_func()` instead.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
obj: The object to check.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
True if the object is a function or bound method, False otherwise.
|
|
40
|
+
|
|
41
|
+
Note:
|
|
42
|
+
This does NOT detect staticmethod/classmethod descriptors or properties.
|
|
43
|
+
Use `is_func()` for those cases.
|
|
44
|
+
"""
|
|
45
|
+
return inspect.isfunction(obj) or inspect.ismethod(obj)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_func(obj: Any) -> bool:
|
|
49
|
+
"""Check if an object is any kind of callable method-like attribute.
|
|
50
|
+
|
|
51
|
+
Provides comprehensive detection of callable objects as they appear in
|
|
52
|
+
class bodies or modules. This includes:
|
|
53
|
+
- Plain functions (which become instance methods in classes)
|
|
54
|
+
- staticmethod descriptors
|
|
55
|
+
- classmethod descriptors
|
|
56
|
+
- property descriptors (the getter is considered a method)
|
|
57
|
+
- Decorated functions with a `__wrapped__` chain
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
obj: The object to check.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if the object is a method-like callable, False otherwise.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> class MyClass:
|
|
67
|
+
... def method(self): pass
|
|
68
|
+
... @staticmethod
|
|
69
|
+
... def static(): pass
|
|
70
|
+
... @property
|
|
71
|
+
... def prop(self): return 1
|
|
72
|
+
>>> is_func(MyClass.method)
|
|
73
|
+
True
|
|
74
|
+
>>> is_func(MyClass.__dict__['static'])
|
|
75
|
+
True
|
|
76
|
+
>>> is_func(MyClass.prop)
|
|
77
|
+
True
|
|
78
|
+
"""
|
|
79
|
+
if is_func_or_method(obj):
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
if isinstance(obj, (staticmethod, classmethod, property)):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
unwrapped = inspect.unwrap(obj)
|
|
86
|
+
|
|
87
|
+
return is_func_or_method(unwrapped)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_all_functions_from_module(
|
|
91
|
+
module: ModuleType | str, *, include_annotate: bool = False
|
|
92
|
+
) -> list[Callable[..., Any]]:
|
|
93
|
+
"""Extract all functions defined directly in a module.
|
|
94
|
+
|
|
95
|
+
Retrieves all function objects that are defined in the specified module,
|
|
96
|
+
excluding functions imported from other modules. Functions are returned
|
|
97
|
+
sorted by their definition order (line number in source).
|
|
98
|
+
|
|
99
|
+
This is used by pyrig to discover CLI subcommands and to generate test
|
|
100
|
+
skeletons for module-level functions.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
module: The module to extract functions from. Can be a module object
|
|
104
|
+
or a module name string.
|
|
105
|
+
include_annotate: If False (default), excludes `__annotate__` methods
|
|
106
|
+
introduced in Python 3.14. These are internal and not user-defined.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
A list of callable functions defined in the module, sorted by their
|
|
110
|
+
definition order in the source file.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> import my_module
|
|
114
|
+
>>> funcs = get_all_functions_from_module(my_module)
|
|
115
|
+
>>> [f.__name__ for f in funcs]
|
|
116
|
+
['first_function', 'second_function', 'third_function']
|
|
117
|
+
"""
|
|
118
|
+
from pyrig.src.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
119
|
+
get_module_of_obj,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if isinstance(module, str):
|
|
123
|
+
module = import_module(module)
|
|
124
|
+
funcs = [
|
|
125
|
+
func
|
|
126
|
+
for _name, func in get_obj_members(module, include_annotate=include_annotate)
|
|
127
|
+
if is_func(func)
|
|
128
|
+
if get_module_of_obj(func).__name__ == module.__name__
|
|
129
|
+
]
|
|
130
|
+
# sort by definition order
|
|
131
|
+
return sorted(funcs, key=get_def_line)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def unwrap_method(method: Any) -> Callable[..., Any] | Any:
|
|
135
|
+
"""Unwrap a method to its underlying function object.
|
|
136
|
+
|
|
137
|
+
Handles staticmethod/classmethod descriptors (extracts `__func__`),
|
|
138
|
+
properties (extracts the getter), and decorated functions (follows
|
|
139
|
+
the `__wrapped__` chain).
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
method: The method-like object to unwrap. Can be a function,
|
|
143
|
+
staticmethod, classmethod, property, or decorated callable.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The underlying unwrapped function object.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> class MyClass:
|
|
150
|
+
... @staticmethod
|
|
151
|
+
... def static_method():
|
|
152
|
+
... pass
|
|
153
|
+
>>> raw_descriptor = MyClass.__dict__['static_method']
|
|
154
|
+
>>> unwrap_method(raw_descriptor).__name__
|
|
155
|
+
'static_method'
|
|
156
|
+
"""
|
|
157
|
+
if isinstance(method, (staticmethod, classmethod)):
|
|
158
|
+
method = method.__func__
|
|
159
|
+
if isinstance(method, property):
|
|
160
|
+
method = method.fget
|
|
161
|
+
return inspect.unwrap(method)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def is_abstractmethod(method: Any) -> bool:
|
|
165
|
+
"""Check if a method is marked as abstract.
|
|
166
|
+
|
|
167
|
+
Detects methods decorated with `@abstractmethod` (or variants like
|
|
168
|
+
`@abstractclassmethod`, `@abstractproperty`). Handles wrapped methods
|
|
169
|
+
by unwrapping them first.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
method: The method to check. Can be wrapped in staticmethod,
|
|
173
|
+
classmethod, property, or decorators.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
True if the method has `__isabstractmethod__` set to True,
|
|
177
|
+
False otherwise.
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> from abc import ABC, abstractmethod
|
|
181
|
+
>>> class MyABC(ABC):
|
|
182
|
+
... @abstractmethod
|
|
183
|
+
... def must_implement(self):
|
|
184
|
+
... pass
|
|
185
|
+
>>> is_abstractmethod(MyABC.must_implement)
|
|
186
|
+
True
|
|
187
|
+
"""
|
|
188
|
+
method = unwrap_method(method)
|
|
189
|
+
return getattr(method, "__isabstractmethod__", False)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Low-level inspection utilities for Python object introspection.
|
|
2
|
+
|
|
3
|
+
This module provides foundational utilities for inspecting Python objects,
|
|
4
|
+
particularly focused on unwrapping decorated methods and accessing object
|
|
5
|
+
metadata. These utilities handle edge cases like properties, staticmethods,
|
|
6
|
+
classmethods, and decorator chains.
|
|
7
|
+
|
|
8
|
+
The module also provides detection for PyInstaller frozen bundles, where
|
|
9
|
+
some inspection operations behave differently or are unavailable.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from pyrig.src.modules.inspection import get_def_line, get_unwrapped_obj
|
|
13
|
+
>>> class MyClass:
|
|
14
|
+
... @property
|
|
15
|
+
... def value(self):
|
|
16
|
+
... return 42
|
|
17
|
+
>>> get_def_line(MyClass.value)
|
|
18
|
+
3
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import inspect
|
|
22
|
+
import sys
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from typing import Any, cast
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_obj_members(
|
|
28
|
+
obj: Any, *, include_annotate: bool = False
|
|
29
|
+
) -> list[tuple[str, Any]]:
|
|
30
|
+
"""Get all members of an object as name-value pairs.
|
|
31
|
+
|
|
32
|
+
Retrieves all attributes of an object using `inspect.getmembers()`,
|
|
33
|
+
optionally filtering out Python 3.14+ annotation-related methods
|
|
34
|
+
that are typically not relevant for introspection.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
obj: The object to inspect (typically a class or module).
|
|
38
|
+
include_annotate: If False (default), excludes `__annotate__` and
|
|
39
|
+
`__annotate_func__` methods introduced in Python 3.14. These
|
|
40
|
+
are internal methods for deferred annotation evaluation.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A list of (name, value) tuples for each member of the object.
|
|
44
|
+
"""
|
|
45
|
+
members = [(member, value) for member, value in inspect.getmembers(obj)]
|
|
46
|
+
if not include_annotate:
|
|
47
|
+
members = [
|
|
48
|
+
(member, value)
|
|
49
|
+
for member, value in members
|
|
50
|
+
if member not in ("__annotate__", "__annotate_func__")
|
|
51
|
+
]
|
|
52
|
+
return members
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def inside_frozen_bundle() -> bool:
|
|
56
|
+
"""Check if the code is running inside a PyInstaller frozen bundle.
|
|
57
|
+
|
|
58
|
+
PyInstaller sets `sys.frozen` to True when running from a bundled
|
|
59
|
+
executable. Some inspection operations (like `getsourcelines`) are
|
|
60
|
+
unavailable in frozen bundles.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if running inside a frozen PyInstaller bundle, False otherwise.
|
|
64
|
+
"""
|
|
65
|
+
return getattr(sys, "frozen", False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_def_line(obj: Any) -> int:
|
|
69
|
+
"""Get the source line number where an object is defined.
|
|
70
|
+
|
|
71
|
+
Handles various callable types including plain functions, methods,
|
|
72
|
+
properties, staticmethods, classmethods, and decorated functions.
|
|
73
|
+
Used for sorting functions/methods in definition order.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
obj: A callable object (function, method, property, etc.).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The 1-based line number where the object is defined in its source
|
|
80
|
+
file. Returns 0 if running in a frozen bundle where source is
|
|
81
|
+
unavailable.
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
For properties, returns the line where the getter is defined.
|
|
85
|
+
Automatically unwraps decorator chains to find the original function.
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(obj, property):
|
|
88
|
+
obj = obj.fget
|
|
89
|
+
unwrapped = inspect.unwrap(obj)
|
|
90
|
+
if hasattr(unwrapped, "__code__"):
|
|
91
|
+
return int(unwrapped.__code__.co_firstlineno)
|
|
92
|
+
# getsourcelines does not work if in a pyinstaller bundle or something
|
|
93
|
+
if inside_frozen_bundle():
|
|
94
|
+
return 0
|
|
95
|
+
return inspect.getsourcelines(unwrapped)[1]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_unwrapped_obj(obj: Any) -> Any:
|
|
99
|
+
"""Unwrap a method-like object to its underlying function.
|
|
100
|
+
|
|
101
|
+
Handles properties (extracts the getter), staticmethod/classmethod
|
|
102
|
+
descriptors, and decorator chains. Useful for accessing the actual
|
|
103
|
+
function object when you need to inspect its attributes.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
obj: A callable object that may be wrapped (property, staticmethod,
|
|
107
|
+
classmethod, or decorated function).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The underlying unwrapped function object.
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> class MyClass:
|
|
114
|
+
... @staticmethod
|
|
115
|
+
... def static_method():
|
|
116
|
+
... pass
|
|
117
|
+
>>> unwrapped = get_unwrapped_obj(MyClass.__dict__['static_method'])
|
|
118
|
+
>>> unwrapped.__name__
|
|
119
|
+
'static_method'
|
|
120
|
+
"""
|
|
121
|
+
if isinstance(obj, property):
|
|
122
|
+
obj = obj.fget # get the getter function of the property
|
|
123
|
+
return inspect.unwrap(obj)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_qualname_of_obj(obj: Callable[..., Any] | type) -> str:
|
|
127
|
+
"""Get the qualified name of a callable or type.
|
|
128
|
+
|
|
129
|
+
The qualified name includes the class name for methods (e.g.,
|
|
130
|
+
"MyClass.my_method") and handles wrapped/decorated objects.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
obj: A callable (function, method) or type (class) to get the
|
|
134
|
+
qualified name of.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
The qualified name string (e.g., "ClassName.method_name" for
|
|
138
|
+
methods, "function_name" for module-level functions).
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> class MyClass:
|
|
142
|
+
... def my_method(self):
|
|
143
|
+
... pass
|
|
144
|
+
>>> get_qualname_of_obj(MyClass.my_method)
|
|
145
|
+
'MyClass.my_method'
|
|
146
|
+
"""
|
|
147
|
+
unwrapped = get_unwrapped_obj(obj)
|
|
148
|
+
return cast("str", unwrapped.__qualname__)
|