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,658 @@
|
|
|
1
|
+
"""Module loading, path conversion, and cross-package discovery utilities.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive utilities for working with Python modules,
|
|
4
|
+
including bidirectional path/module name conversion, dynamic module creation,
|
|
5
|
+
object importing, and cross-package module discovery.
|
|
6
|
+
|
|
7
|
+
Key capabilities:
|
|
8
|
+
- Path conversion: Convert between filesystem paths and module names
|
|
9
|
+
- Module creation: Create new modules with proper package structure
|
|
10
|
+
- Object importing: Import objects from fully qualified import paths
|
|
11
|
+
- Cross-package discovery: Find equivalent modules across dependent packages
|
|
12
|
+
|
|
13
|
+
The cross-package discovery (`get_same_modules_from_deps_depen_on_dep`) is
|
|
14
|
+
central to pyrig's multi-package architecture, enabling automatic discovery
|
|
15
|
+
of ConfigFile implementations and other extensible components across all
|
|
16
|
+
packages that depend on pyrig.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
>>> from pyrig.src.modules.module import to_module_name, to_path
|
|
20
|
+
>>> to_module_name("src/package/module.py")
|
|
21
|
+
'src.package.module'
|
|
22
|
+
>>> to_path("src.package.module", is_package=False)
|
|
23
|
+
PosixPath('src/package/module.py')
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import importlib.util
|
|
27
|
+
import inspect
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
import sys
|
|
31
|
+
from collections.abc import Callable, Sequence
|
|
32
|
+
from importlib import import_module
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from types import ModuleType
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from pyrig.src.modules.class_ import (
|
|
38
|
+
get_all_cls_from_module,
|
|
39
|
+
get_all_methods_from_cls,
|
|
40
|
+
)
|
|
41
|
+
from pyrig.src.modules.function import get_all_functions_from_module
|
|
42
|
+
from pyrig.src.modules.inspection import (
|
|
43
|
+
get_qualname_of_obj,
|
|
44
|
+
get_unwrapped_obj,
|
|
45
|
+
)
|
|
46
|
+
from pyrig.src.modules.package import (
|
|
47
|
+
DependencyGraph,
|
|
48
|
+
get_modules_and_packages_from_package,
|
|
49
|
+
import_pkg_from_path,
|
|
50
|
+
module_is_package,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_module_content_as_str(module: ModuleType) -> str:
|
|
57
|
+
"""Retrieve the complete source code of a module as a string.
|
|
58
|
+
|
|
59
|
+
This function locates the physical file associated with the given module object
|
|
60
|
+
and reads its entire content. It works for both regular modules and packages
|
|
61
|
+
by determining the correct path using module_to_path.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
module: The module object whose source code should be retrieved
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The complete source code of the module as a string
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
path = to_path(module, is_package=False)
|
|
72
|
+
return path.read_text(encoding="utf-8")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_module_name(path: str | Path | ModuleType) -> str:
|
|
76
|
+
"""Convert a filesystem path to a Python module import name.
|
|
77
|
+
|
|
78
|
+
Transforms a file or directory path into the corresponding Python module name
|
|
79
|
+
by making it relative to the current directory, removing the file extension,
|
|
80
|
+
and replacing directory separators with dots.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path: a str that represents a path or a Path object or a ModuleType object
|
|
84
|
+
or a str that represents a module name
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The Python module name corresponding to the path
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
path_to_module_name("src/package/module.py") -> "src.package.module"
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
if isinstance(path, ModuleType):
|
|
94
|
+
return path.__name__
|
|
95
|
+
if isinstance(path, Path):
|
|
96
|
+
cwd = (
|
|
97
|
+
Path.cwd()
|
|
98
|
+
if not getattr(sys, "frozen", False)
|
|
99
|
+
else Path(getattr(sys, "_MEIPASS", ""))
|
|
100
|
+
)
|
|
101
|
+
if path.is_absolute():
|
|
102
|
+
path = path.relative_to(cwd)
|
|
103
|
+
if path.suffix:
|
|
104
|
+
path = path.with_suffix("")
|
|
105
|
+
# return joined on . parts
|
|
106
|
+
return ".".join(path.parts)
|
|
107
|
+
if path in (".", "./", ""):
|
|
108
|
+
return ""
|
|
109
|
+
# we get a str that can either be a dotted module name or a path
|
|
110
|
+
# e.g. package/module.py or package/module or
|
|
111
|
+
# package.module or just package/package2
|
|
112
|
+
# or just package with nothing
|
|
113
|
+
path = path.removesuffix(".py")
|
|
114
|
+
if "." in path:
|
|
115
|
+
# already a module name
|
|
116
|
+
return path
|
|
117
|
+
return to_module_name(Path(path))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def to_path(module_name: str | ModuleType | Path, *, is_package: bool) -> Path:
|
|
121
|
+
"""Convert a Python module import name to its filesystem path.
|
|
122
|
+
|
|
123
|
+
Transforms a Python module name into the corresponding file path by replacing
|
|
124
|
+
dots with directory separators and adding the .py extension. Uses the
|
|
125
|
+
package_name_to_path function for the directory structure.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
module_name: A Python module name to convert or Path or ModuleType
|
|
129
|
+
is_package: Whether to return the path to the package directory
|
|
130
|
+
without the .py extension
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
A Path object representing the filesystem path to the module
|
|
134
|
+
if is_package is True, returns the path to the package directory
|
|
135
|
+
without the .py extension
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
module_name_to_path("src.package.module") -> Path("src/package/module.py")
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(module_name, ModuleType) and hasattr(module_name, "__file__"):
|
|
142
|
+
file_str = module_name.__file__
|
|
143
|
+
if file_str is not None:
|
|
144
|
+
file_path = Path(file_str)
|
|
145
|
+
if is_package:
|
|
146
|
+
# this way if you want __init__ in the path then package=False
|
|
147
|
+
file_path = file_path.parent
|
|
148
|
+
return file_path
|
|
149
|
+
module_name = to_module_name(module_name)
|
|
150
|
+
path = Path(module_name.replace(".", os.sep))
|
|
151
|
+
# for smth like pyinstaller we support frozen path
|
|
152
|
+
if getattr(sys, "frozen", False):
|
|
153
|
+
path = Path(getattr(sys, "_MEIPASS", "")) / path
|
|
154
|
+
# if path is cwd or "."
|
|
155
|
+
if path in (Path.cwd(), Path()):
|
|
156
|
+
return Path()
|
|
157
|
+
# if path without with.py exists
|
|
158
|
+
with_py = path.with_suffix(".py")
|
|
159
|
+
if with_py.exists():
|
|
160
|
+
return with_py
|
|
161
|
+
not_with_py = path.with_suffix("")
|
|
162
|
+
if not_with_py.exists():
|
|
163
|
+
return not_with_py
|
|
164
|
+
if is_package:
|
|
165
|
+
return path
|
|
166
|
+
return path.with_suffix(".py")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def make_dir_with_init_file(path: Path) -> None:
|
|
170
|
+
"""Create a directory and add __init__.py files to make it a package.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
path: The directory path to create and initialize as a package
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
make_init_modules_for_package(path)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_module(
|
|
181
|
+
module_name: str | Path | ModuleType, *, is_package: bool
|
|
182
|
+
) -> ModuleType:
|
|
183
|
+
"""Create a new Python module file and import it.
|
|
184
|
+
|
|
185
|
+
Creates a new module file at the location specified by the module name,
|
|
186
|
+
ensuring all necessary parent directories and __init__.py files exist.
|
|
187
|
+
Optionally writes content to the module file and parent __init__.py files.
|
|
188
|
+
Finally imports and returns the newly created module.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
module_name: The fully qualified name of the module to create
|
|
192
|
+
is_package: Whether to create a package instead of a module
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The imported module object representing the newly created module
|
|
196
|
+
|
|
197
|
+
Note:
|
|
198
|
+
Includes a small delay (0.1s) before importing to ensure filesystem operations
|
|
199
|
+
are complete, preventing race conditions.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
path = to_path(module_name, is_package=is_package)
|
|
203
|
+
if path == Path():
|
|
204
|
+
msg = f"Cannot create module {module_name=} because it is the CWD"
|
|
205
|
+
logger.error(msg)
|
|
206
|
+
raise ValueError(msg)
|
|
207
|
+
|
|
208
|
+
make_dir_with_init_file(path if is_package else path.parent)
|
|
209
|
+
|
|
210
|
+
if not path.exists() and not is_package:
|
|
211
|
+
path.write_text(get_default_module_content())
|
|
212
|
+
return import_module_from_file(path)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def import_module_from_file(path: Path | str) -> ModuleType:
|
|
216
|
+
"""Import a module from a filesystem path.
|
|
217
|
+
|
|
218
|
+
Converts the path to a module name and imports it. Handles both regular
|
|
219
|
+
modules (.py files) and packages (directories with __init__.py).
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
path: Filesystem path to the module or package. Can be a Path object
|
|
223
|
+
or a string path.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The imported module object.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
FileNotFoundError: If the path does not exist.
|
|
230
|
+
ValueError: If the module cannot be loaded.
|
|
231
|
+
"""
|
|
232
|
+
module_name = to_module_name(path)
|
|
233
|
+
path = to_path(module_name, is_package=False)
|
|
234
|
+
module = import_module_with_default(module_name)
|
|
235
|
+
if module is not None:
|
|
236
|
+
return module
|
|
237
|
+
if path.is_dir():
|
|
238
|
+
return import_pkg_from_path(path)
|
|
239
|
+
return import_module_from_path(path)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def import_module_from_file_with_default(
|
|
243
|
+
path: Path, default: Any = None
|
|
244
|
+
) -> ModuleType | Any:
|
|
245
|
+
"""Import a module from a path, returning a default on failure.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
path: Filesystem path to the module.
|
|
249
|
+
default: Value to return if the module cannot be imported.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
The imported module, or `default` if import fails.
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
return import_module_from_file(path)
|
|
256
|
+
except FileNotFoundError:
|
|
257
|
+
return default
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def import_module_from_path(path: Path) -> ModuleType:
|
|
261
|
+
"""Import a module directly from a .py file.
|
|
262
|
+
|
|
263
|
+
Uses `importlib.util` to create a module spec and load the module
|
|
264
|
+
from the specified file. The module is registered in `sys.modules`
|
|
265
|
+
with a name derived from its path relative to the current directory.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
path: Path to the .py file to import.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
The imported module object.
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
ValueError: If a module spec or loader cannot be created for the path.
|
|
275
|
+
"""
|
|
276
|
+
# name is dotted path relative to cwd
|
|
277
|
+
name = to_module_name(path.resolve().relative_to(Path.cwd()))
|
|
278
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
279
|
+
if spec is None:
|
|
280
|
+
msg = f"Could not create spec for {path}"
|
|
281
|
+
raise ValueError(msg)
|
|
282
|
+
module = importlib.util.module_from_spec(spec)
|
|
283
|
+
sys.modules[name] = module
|
|
284
|
+
if spec.loader is None:
|
|
285
|
+
msg = f"Could not create loader for {path}"
|
|
286
|
+
raise ValueError(msg)
|
|
287
|
+
spec.loader.exec_module(module)
|
|
288
|
+
return module
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def make_obj_importpath(obj: Callable[..., Any] | type | ModuleType) -> str:
|
|
292
|
+
"""Create a fully qualified import path string for a Python object.
|
|
293
|
+
|
|
294
|
+
Generates the import path that would be used to import the given object.
|
|
295
|
+
Handles different types of objects (modules, classes, functions) appropriately.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
obj: The object (module, class, or function) to create an import path for
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A string representing the fully qualified import path for the object
|
|
302
|
+
|
|
303
|
+
Examples:
|
|
304
|
+
For a module: "package.subpackage.module"
|
|
305
|
+
For a class: "package.module.ClassName"
|
|
306
|
+
For a function: "package.module.function_name"
|
|
307
|
+
For a method: "package.module.ClassName.method_name"
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
if isinstance(obj, ModuleType):
|
|
311
|
+
return obj.__name__
|
|
312
|
+
module: str | None = get_module_of_obj(obj).__name__
|
|
313
|
+
obj_name = get_qualname_of_obj(obj)
|
|
314
|
+
if not module:
|
|
315
|
+
return obj_name
|
|
316
|
+
return module + "." + obj_name
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def import_obj_from_importpath(
|
|
320
|
+
importpath: str,
|
|
321
|
+
) -> Callable[..., Any] | type | ModuleType:
|
|
322
|
+
"""Import a Python object (module, class, or function) from its import path.
|
|
323
|
+
|
|
324
|
+
Attempts to import the object specified by the given import path. First tries
|
|
325
|
+
to import it as a module, and if that fails, attempts to import it as a class
|
|
326
|
+
or function by splitting the path and using getattr.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
importpath: The fully qualified import path of the object
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The imported object (module, class, or function)
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ImportError: If the module part of the path cannot be imported
|
|
336
|
+
AttributeError: If the object is not found in the module
|
|
337
|
+
|
|
338
|
+
"""
|
|
339
|
+
try:
|
|
340
|
+
return import_module(importpath)
|
|
341
|
+
except ImportError:
|
|
342
|
+
# might be a class or function
|
|
343
|
+
if "." not in importpath:
|
|
344
|
+
raise
|
|
345
|
+
module_name, obj_name = importpath.rsplit(".", 1)
|
|
346
|
+
module = import_module(module_name)
|
|
347
|
+
obj: Callable[..., Any] | type = getattr(module, obj_name)
|
|
348
|
+
return obj
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def get_isolated_obj_name(obj: Callable[..., Any] | type | ModuleType) -> str:
|
|
352
|
+
"""Extract the bare name of an object without its module prefix.
|
|
353
|
+
|
|
354
|
+
Retrieves just the name part of an object, without any module path information.
|
|
355
|
+
For modules, returns the last component of the module path.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
obj: The object (module, class, or function) to get the name for
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
The isolated name of the object without any module path
|
|
362
|
+
|
|
363
|
+
Examples:
|
|
364
|
+
For a module "package.subpackage.module": returns "module"
|
|
365
|
+
For a class: returns the class name
|
|
366
|
+
For a function: returns the function name
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
obj = get_unwrapped_obj(obj)
|
|
370
|
+
if isinstance(obj, ModuleType):
|
|
371
|
+
return obj.__name__.split(".")[-1]
|
|
372
|
+
if isinstance(obj, type):
|
|
373
|
+
return obj.__name__
|
|
374
|
+
return get_qualname_of_obj(obj).split(".")[-1]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def get_objs_from_obj(
|
|
378
|
+
obj: Callable[..., Any] | type | ModuleType,
|
|
379
|
+
) -> Sequence[Callable[..., Any] | type | ModuleType]:
|
|
380
|
+
"""Extract all contained objects from a container object.
|
|
381
|
+
|
|
382
|
+
Retrieves all relevant objects contained within the given object, with behavior
|
|
383
|
+
depending on the type of the container:
|
|
384
|
+
- For modules: returns all functions and classes defined in the module
|
|
385
|
+
- For packages: returns all submodules in the package
|
|
386
|
+
- For classes: returns all methods defined directly in the class
|
|
387
|
+
- For other objects: returns an empty list
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
obj: The container object to extract contained objects from
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
A sequence of objects contained within the given container object
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
if isinstance(obj, ModuleType):
|
|
397
|
+
if module_is_package(obj):
|
|
398
|
+
return get_modules_and_packages_from_package(obj)[1]
|
|
399
|
+
objs: list[Callable[..., Any] | type] = []
|
|
400
|
+
objs.extend(get_all_functions_from_module(obj))
|
|
401
|
+
objs.extend(get_all_cls_from_module(obj))
|
|
402
|
+
return objs
|
|
403
|
+
if isinstance(obj, type):
|
|
404
|
+
return get_all_methods_from_cls(obj, exclude_parent_methods=True)
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def execute_all_functions_from_module(module: ModuleType) -> list[Any]:
|
|
409
|
+
"""Execute all functions defined in a module with no arguments.
|
|
410
|
+
|
|
411
|
+
Retrieves all functions defined in the module and calls each one with no arguments.
|
|
412
|
+
Collects and returns the results of all function calls.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
module: The module containing functions to execute
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
A list containing the return values from all executed functions
|
|
419
|
+
|
|
420
|
+
Note:
|
|
421
|
+
Only executes functions defined directly in the module, not imported functions.
|
|
422
|
+
All functions must accept being called with no arguments.
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
return [f() for f in get_all_functions_from_module(module)]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def get_default_init_module_content() -> str:
|
|
429
|
+
"""Generate standardized content for an __init__.py file.
|
|
430
|
+
|
|
431
|
+
Creates a simple docstring for an __init__.py file based on its location,
|
|
432
|
+
following the project's documentation conventions.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
path: The path to the __init__.py file or its parent directory
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
A string containing a properly formatted docstring for the __init__.py file
|
|
439
|
+
|
|
440
|
+
"""
|
|
441
|
+
return '''"""__init__ module."""
|
|
442
|
+
'''
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_default_module_content() -> str:
|
|
446
|
+
"""Generate standardized content for a Python module file.
|
|
447
|
+
|
|
448
|
+
Creates a simple docstring for a module file based on its name,
|
|
449
|
+
following the project's documentation conventions.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
A string containing a properly formatted docstring for the module file
|
|
453
|
+
|
|
454
|
+
"""
|
|
455
|
+
return '''"""module."""
|
|
456
|
+
'''
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def get_module_of_obj(obj: Any, default: ModuleType | None = None) -> ModuleType:
|
|
460
|
+
"""Return the module name where a method-like object is defined.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
obj: Method-like object (funcs, method, property, staticmethod, classmethod...)
|
|
464
|
+
default: Default module to return if the module cannot be determined
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
The module name as a string, or None if module cannot be determined.
|
|
468
|
+
|
|
469
|
+
"""
|
|
470
|
+
unwrapped = get_unwrapped_obj(obj)
|
|
471
|
+
module = inspect.getmodule(unwrapped)
|
|
472
|
+
if not module:
|
|
473
|
+
msg = f"Could not determine module of {obj}"
|
|
474
|
+
if default:
|
|
475
|
+
return default
|
|
476
|
+
raise ValueError(msg)
|
|
477
|
+
return module
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def get_executing_module() -> ModuleType:
|
|
481
|
+
"""Get the module where execution has started.
|
|
482
|
+
|
|
483
|
+
The executing module is the module that contains the __main__ attribute as __name__
|
|
484
|
+
E.g. if you run `python -m pyrig.setup` from the command line,
|
|
485
|
+
then the executing module is pyrigmodules.setup
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
The module where execution has started
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
ValueError: If no __main__ module is found or if the executing module
|
|
492
|
+
cannot be determined
|
|
493
|
+
|
|
494
|
+
"""
|
|
495
|
+
main = sys.modules.get("__main__")
|
|
496
|
+
if main is None:
|
|
497
|
+
msg = "No __main__ module found"
|
|
498
|
+
raise ValueError(msg)
|
|
499
|
+
return main
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def import_module_with_default(
|
|
503
|
+
module_name: str, default: Any = None
|
|
504
|
+
) -> ModuleType | Any:
|
|
505
|
+
"""Import a module, returning a default if the module cannot be imported.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
module_name: Name of the module to import
|
|
509
|
+
default: Default module to return if the module cannot be imported
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
The imported module, or the default module if the module cannot be imported
|
|
513
|
+
|
|
514
|
+
Raises:
|
|
515
|
+
ValueError: If the module cannot be imported
|
|
516
|
+
|
|
517
|
+
"""
|
|
518
|
+
try:
|
|
519
|
+
return import_module(module_name)
|
|
520
|
+
except ImportError:
|
|
521
|
+
logger.debug("Could not import module %s", module_name)
|
|
522
|
+
return default
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def make_init_module(path: Path) -> None:
|
|
526
|
+
"""Create an __init__.py file in the specified directory.
|
|
527
|
+
|
|
528
|
+
Creates an __init__.py file with default content in the given directory,
|
|
529
|
+
making it a proper Python package.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
path: The directory path where the __init__.py file should be created
|
|
533
|
+
|
|
534
|
+
Note:
|
|
535
|
+
If the path already points to an __init__.py file, that file will be
|
|
536
|
+
overwritten with the default content.
|
|
537
|
+
Creates parent directories if they don't exist.
|
|
538
|
+
|
|
539
|
+
"""
|
|
540
|
+
init_path = path / "__init__.py"
|
|
541
|
+
|
|
542
|
+
if init_path.exists():
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
content = get_default_init_module_content()
|
|
546
|
+
init_path.write_text(content)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def make_init_modules_for_package(path: Path) -> None:
|
|
550
|
+
"""Create __init__.py files in all subdirectories of a package.
|
|
551
|
+
|
|
552
|
+
Ensures that all subdirectories of the given package have __init__.py files,
|
|
553
|
+
effectively converting them into proper Python packages. Skips directories
|
|
554
|
+
that match patterns in .gitignore.
|
|
555
|
+
|
|
556
|
+
Args:
|
|
557
|
+
path: The package path or module object to process
|
|
558
|
+
|
|
559
|
+
Note:
|
|
560
|
+
Does not modify directories that already have __init__.py files.
|
|
561
|
+
Uses the default content for __init__.py files
|
|
562
|
+
from get_default_init_module_content.
|
|
563
|
+
|
|
564
|
+
"""
|
|
565
|
+
# create init files in all subdirectories and in the root
|
|
566
|
+
make_init_module(path)
|
|
567
|
+
for p in path.rglob("*"):
|
|
568
|
+
if p.is_dir():
|
|
569
|
+
make_init_module(p)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def make_pkg_dir(path: Path) -> None:
|
|
573
|
+
"""Create __init__.py files in all parent directories of a path.
|
|
574
|
+
|
|
575
|
+
It does not include the CWD.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
path: The path to create __init__.py files for
|
|
579
|
+
|
|
580
|
+
Note:
|
|
581
|
+
Does not modify directories that already have __init__.py files.
|
|
582
|
+
Uses the default content for __init__.py files
|
|
583
|
+
from get_default_init_module_content.
|
|
584
|
+
|
|
585
|
+
"""
|
|
586
|
+
if path.is_absolute():
|
|
587
|
+
path = path.relative_to(Path.cwd())
|
|
588
|
+
# mkdir all parents
|
|
589
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
590
|
+
|
|
591
|
+
make_init_module(path)
|
|
592
|
+
for p in path.parents:
|
|
593
|
+
if p in (Path.cwd(), Path()):
|
|
594
|
+
continue
|
|
595
|
+
make_init_module(p)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def get_same_modules_from_deps_depen_on_dep(
|
|
599
|
+
module: ModuleType, dep: ModuleType, until_pkg: ModuleType | None = None
|
|
600
|
+
) -> list[ModuleType]:
|
|
601
|
+
"""Find equivalent modules across all packages depending on a dependency.
|
|
602
|
+
|
|
603
|
+
This is a key function for pyrig's multi-package architecture. Given a
|
|
604
|
+
module path within a dependency (e.g., smth.dev.configs`), it finds
|
|
605
|
+
the equivalent module path in all packages that depend on that dependency
|
|
606
|
+
(e.g., `myapp.dev.configs`, `other_pkg.dev.configs`).
|
|
607
|
+
|
|
608
|
+
This enables automatic discovery of ConfigFile implementations, Builder
|
|
609
|
+
subclasses, and other extensible components across the entire ecosystem
|
|
610
|
+
of packages that depend on pyrig.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
module: The module to use as a template (e.g., `smth.dev.configs`).
|
|
614
|
+
dep: The dependency package that other packages depend on (e.g., pyrig or smth).
|
|
615
|
+
until_pkg: Optional package to stop at. If provided, only modules from
|
|
616
|
+
packages that depend on `until_pkg` will be returned.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
A list of equivalent modules from all packages that depend on `dep`,
|
|
620
|
+
including the original module itself.
|
|
621
|
+
|
|
622
|
+
Example:
|
|
623
|
+
>>> import smth
|
|
624
|
+
>>> from smth.dev import configs
|
|
625
|
+
>>> modules = get_same_modules_from_deps_depen_on_dep(
|
|
626
|
+
... configs, smth
|
|
627
|
+
... )
|
|
628
|
+
>>> [m.__name__ for m in modules]
|
|
629
|
+
['smth.dev.configs', 'myapp.dev.configs', 'other_pkg.dev.configs']
|
|
630
|
+
"""
|
|
631
|
+
module_name = module.__name__
|
|
632
|
+
graph = DependencyGraph()
|
|
633
|
+
pkgs = graph.get_all_depending_on(dep, include_self=True)
|
|
634
|
+
|
|
635
|
+
modules: list[ModuleType] = []
|
|
636
|
+
for pkg in pkgs:
|
|
637
|
+
pkg_module_name = module_name.replace(dep.__name__, pkg.__name__, 1)
|
|
638
|
+
pkg_module = import_module_from_file(pkg_module_name)
|
|
639
|
+
modules.append(pkg_module)
|
|
640
|
+
if isinstance(until_pkg, ModuleType) and pkg.__name__ == until_pkg.__name__:
|
|
641
|
+
break
|
|
642
|
+
return modules
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def get_module_name_replacing_start_module(
|
|
646
|
+
module: ModuleType, new_start_module_name: str
|
|
647
|
+
) -> str:
|
|
648
|
+
"""Replace the root module name in a module's fully qualified name.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
module: The module whose name to transform.
|
|
652
|
+
new_start_module_name: The new root module name.
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
The transformed module name.
|
|
656
|
+
"""
|
|
657
|
+
module_current_start = module.__name__.split(".")[0]
|
|
658
|
+
return module.__name__.replace(module_current_start, new_start_module_name, 1)
|