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.
Files changed (102) hide show
  1. pyrig/__init__.py +1 -0
  2. pyrig/dev/__init__.py +6 -0
  3. pyrig/dev/builders/__init__.py +1 -0
  4. pyrig/dev/builders/base/__init__.py +5 -0
  5. pyrig/dev/builders/base/base.py +256 -0
  6. pyrig/dev/builders/pyinstaller.py +229 -0
  7. pyrig/dev/cli/__init__.py +5 -0
  8. pyrig/dev/cli/cli.py +95 -0
  9. pyrig/dev/cli/commands/__init__.py +1 -0
  10. pyrig/dev/cli/commands/build_artifacts.py +16 -0
  11. pyrig/dev/cli/commands/create_root.py +25 -0
  12. pyrig/dev/cli/commands/create_tests.py +244 -0
  13. pyrig/dev/cli/commands/init_project.py +160 -0
  14. pyrig/dev/cli/commands/make_inits.py +27 -0
  15. pyrig/dev/cli/commands/protect_repo.py +145 -0
  16. pyrig/dev/cli/shared_subcommands.py +20 -0
  17. pyrig/dev/cli/subcommands.py +73 -0
  18. pyrig/dev/configs/__init__.py +1 -0
  19. pyrig/dev/configs/base/__init__.py +5 -0
  20. pyrig/dev/configs/base/base.py +826 -0
  21. pyrig/dev/configs/containers/__init__.py +1 -0
  22. pyrig/dev/configs/containers/container_file.py +111 -0
  23. pyrig/dev/configs/dot_env.py +95 -0
  24. pyrig/dev/configs/dot_python_version.py +88 -0
  25. pyrig/dev/configs/git/__init__.py +5 -0
  26. pyrig/dev/configs/git/gitignore.py +181 -0
  27. pyrig/dev/configs/git/pre_commit.py +170 -0
  28. pyrig/dev/configs/licence.py +112 -0
  29. pyrig/dev/configs/markdown/__init__.py +1 -0
  30. pyrig/dev/configs/markdown/docs/__init__.py +1 -0
  31. pyrig/dev/configs/markdown/docs/index.py +38 -0
  32. pyrig/dev/configs/markdown/readme.py +132 -0
  33. pyrig/dev/configs/py_typed.py +28 -0
  34. pyrig/dev/configs/pyproject.py +436 -0
  35. pyrig/dev/configs/python/__init__.py +5 -0
  36. pyrig/dev/configs/python/builders_init.py +27 -0
  37. pyrig/dev/configs/python/configs_init.py +28 -0
  38. pyrig/dev/configs/python/dot_experiment.py +46 -0
  39. pyrig/dev/configs/python/main.py +59 -0
  40. pyrig/dev/configs/python/resources_init.py +27 -0
  41. pyrig/dev/configs/python/shared_subcommands.py +29 -0
  42. pyrig/dev/configs/python/src_init.py +27 -0
  43. pyrig/dev/configs/python/subcommands.py +27 -0
  44. pyrig/dev/configs/testing/__init__.py +5 -0
  45. pyrig/dev/configs/testing/conftest.py +64 -0
  46. pyrig/dev/configs/testing/fixtures_init.py +27 -0
  47. pyrig/dev/configs/testing/main_test.py +74 -0
  48. pyrig/dev/configs/testing/zero_test.py +43 -0
  49. pyrig/dev/configs/workflows/__init__.py +5 -0
  50. pyrig/dev/configs/workflows/base/__init__.py +5 -0
  51. pyrig/dev/configs/workflows/base/base.py +1662 -0
  52. pyrig/dev/configs/workflows/build.py +106 -0
  53. pyrig/dev/configs/workflows/health_check.py +133 -0
  54. pyrig/dev/configs/workflows/publish.py +68 -0
  55. pyrig/dev/configs/workflows/release.py +90 -0
  56. pyrig/dev/tests/__init__.py +5 -0
  57. pyrig/dev/tests/conftest.py +40 -0
  58. pyrig/dev/tests/fixtures/__init__.py +1 -0
  59. pyrig/dev/tests/fixtures/assertions.py +147 -0
  60. pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
  61. pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
  62. pyrig/dev/tests/fixtures/autouse/module.py +40 -0
  63. pyrig/dev/tests/fixtures/autouse/session.py +589 -0
  64. pyrig/dev/tests/fixtures/factories.py +118 -0
  65. pyrig/dev/utils/__init__.py +1 -0
  66. pyrig/dev/utils/cli.py +17 -0
  67. pyrig/dev/utils/git.py +312 -0
  68. pyrig/dev/utils/packages.py +93 -0
  69. pyrig/dev/utils/resources.py +77 -0
  70. pyrig/dev/utils/testing.py +66 -0
  71. pyrig/dev/utils/versions.py +268 -0
  72. pyrig/main.py +9 -0
  73. pyrig/py.typed +0 -0
  74. pyrig/resources/GITIGNORE +216 -0
  75. pyrig/resources/LATEST_PYTHON_VERSION +1 -0
  76. pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
  77. pyrig/resources/__init__.py +1 -0
  78. pyrig/src/__init__.py +1 -0
  79. pyrig/src/git/__init__.py +6 -0
  80. pyrig/src/git/git.py +146 -0
  81. pyrig/src/graph.py +255 -0
  82. pyrig/src/iterate.py +107 -0
  83. pyrig/src/modules/__init__.py +22 -0
  84. pyrig/src/modules/class_.py +369 -0
  85. pyrig/src/modules/function.py +189 -0
  86. pyrig/src/modules/inspection.py +148 -0
  87. pyrig/src/modules/module.py +658 -0
  88. pyrig/src/modules/package.py +452 -0
  89. pyrig/src/os/__init__.py +6 -0
  90. pyrig/src/os/os.py +121 -0
  91. pyrig/src/project/__init__.py +5 -0
  92. pyrig/src/project/mgt.py +83 -0
  93. pyrig/src/resource.py +58 -0
  94. pyrig/src/string.py +100 -0
  95. pyrig/src/testing/__init__.py +6 -0
  96. pyrig/src/testing/assertions.py +66 -0
  97. pyrig/src/testing/convention.py +203 -0
  98. pyrig-2.2.6.dist-info/METADATA +174 -0
  99. pyrig-2.2.6.dist-info/RECORD +102 -0
  100. pyrig-2.2.6.dist-info/WHEEL +4 -0
  101. pyrig-2.2.6.dist-info/entry_points.txt +3 -0
  102. 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__)