winipedia-utils 0.2.0__py3-none-any.whl → 0.2.1__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.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/concurrent/concurrent.py +245 -245
- winipedia_utils/concurrent/multiprocessing.py +130 -130
- winipedia_utils/concurrent/multithreading.py +93 -93
- winipedia_utils/consts.py +21 -23
- winipedia_utils/data/__init__.py +1 -1
- winipedia_utils/data/dataframe/__init__.py +1 -1
- winipedia_utils/data/dataframe/cleaning.py +378 -378
- winipedia_utils/data/structures/__init__.py +1 -1
- winipedia_utils/data/structures/dicts.py +16 -16
- winipedia_utils/git/__init__.py +1 -1
- winipedia_utils/git/gitignore/__init__.py +1 -1
- winipedia_utils/git/gitignore/gitignore.py +136 -136
- winipedia_utils/git/pre_commit/__init__.py +1 -1
- winipedia_utils/git/pre_commit/config.py +70 -70
- winipedia_utils/git/pre_commit/hooks.py +109 -109
- winipedia_utils/git/pre_commit/run_hooks.py +49 -49
- winipedia_utils/iterating/__init__.py +1 -1
- winipedia_utils/iterating/iterate.py +29 -29
- winipedia_utils/logging/ansi.py +6 -6
- winipedia_utils/logging/config.py +64 -64
- winipedia_utils/logging/logger.py +26 -26
- winipedia_utils/modules/class_.py +119 -119
- winipedia_utils/modules/function.py +101 -101
- winipedia_utils/modules/module.py +379 -379
- winipedia_utils/modules/package.py +390 -390
- winipedia_utils/oop/mixins/meta.py +333 -333
- winipedia_utils/oop/mixins/mixin.py +37 -37
- winipedia_utils/os/__init__.py +1 -1
- winipedia_utils/os/os.py +63 -63
- winipedia_utils/projects/__init__.py +1 -1
- winipedia_utils/projects/poetry/__init__.py +1 -1
- winipedia_utils/projects/poetry/config.py +91 -91
- winipedia_utils/projects/poetry/poetry.py +31 -31
- winipedia_utils/projects/project.py +48 -48
- winipedia_utils/resources/__init__.py +1 -1
- winipedia_utils/resources/svgs/__init__.py +1 -1
- winipedia_utils/resources/svgs/download_arrow.svg +2 -2
- winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +5 -5
- winipedia_utils/resources/svgs/fullscreen_icon.svg +2 -2
- winipedia_utils/resources/svgs/menu_icon.svg +3 -3
- winipedia_utils/resources/svgs/pause_icon.svg +3 -3
- winipedia_utils/resources/svgs/play_icon.svg +16 -16
- winipedia_utils/resources/svgs/plus_icon.svg +23 -23
- winipedia_utils/resources/svgs/svg.py +15 -15
- winipedia_utils/security/__init__.py +1 -1
- winipedia_utils/security/cryptography.py +29 -29
- winipedia_utils/security/keyring.py +70 -70
- winipedia_utils/setup.py +47 -47
- winipedia_utils/testing/assertions.py +23 -23
- winipedia_utils/testing/convention.py +177 -177
- winipedia_utils/testing/create_tests.py +291 -291
- winipedia_utils/testing/fixtures.py +28 -28
- winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -1
- winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -6
- winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -33
- winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -31
- winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -7
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +312 -312
- winipedia_utils/testing/tests/base/utils/utils.py +82 -82
- winipedia_utils/testing/tests/conftest.py +32 -32
- winipedia_utils/text/string.py +126 -126
- {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/METADATA +1 -4
- winipedia_utils-0.2.1.dist-info/RECORD +80 -0
- {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/licenses/LICENSE +21 -21
- winipedia_utils/django/__init__.py +0 -24
- winipedia_utils/django/bulk.py +0 -538
- winipedia_utils/django/command.py +0 -334
- winipedia_utils/django/database.py +0 -289
- winipedia_utils/pyside/__init__.py +0 -1
- winipedia_utils/pyside/core/__init__.py +0 -1
- winipedia_utils/pyside/core/py_qiodevice.py +0 -476
- winipedia_utils/pyside/ui/__init__.py +0 -1
- winipedia_utils/pyside/ui/base/__init__.py +0 -1
- winipedia_utils/pyside/ui/base/base.py +0 -180
- winipedia_utils/pyside/ui/pages/__init__.py +0 -1
- winipedia_utils/pyside/ui/pages/base/__init__.py +0 -1
- winipedia_utils/pyside/ui/pages/base/base.py +0 -92
- winipedia_utils/pyside/ui/pages/browser.py +0 -26
- winipedia_utils/pyside/ui/pages/player.py +0 -85
- winipedia_utils/pyside/ui/widgets/__init__.py +0 -1
- winipedia_utils/pyside/ui/widgets/browser.py +0 -243
- winipedia_utils/pyside/ui/widgets/clickable_widget.py +0 -57
- winipedia_utils/pyside/ui/widgets/media_player.py +0 -430
- winipedia_utils/pyside/ui/widgets/notification.py +0 -78
- winipedia_utils/pyside/ui/windows/__init__.py +0 -1
- winipedia_utils/pyside/ui/windows/base/__init__.py +0 -1
- winipedia_utils/pyside/ui/windows/base/base.py +0 -49
- winipedia_utils-0.2.0.dist-info/RECORD +0 -103
- {winipedia_utils-0.2.0.dist-info → winipedia_utils-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -1,390 +1,390 @@
|
|
|
1
|
-
"""Package utilities for introspection and manipulation.
|
|
2
|
-
|
|
3
|
-
This module provides comprehensive utility functions for working with Python packages,
|
|
4
|
-
including package discovery, creation, traversal, and module extraction. It handles
|
|
5
|
-
both regular packages and namespace packages, offering tools for filesystem operations
|
|
6
|
-
and module imports related to package structures.
|
|
7
|
-
|
|
8
|
-
The utilities support both static package analysis and dynamic package manipulation,
|
|
9
|
-
making them suitable for code generation, testing frameworks, and package management.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
import pkgutil
|
|
14
|
-
import sys
|
|
15
|
-
from collections.abc import Generator, Iterable
|
|
16
|
-
from importlib import import_module
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from types import ModuleType
|
|
19
|
-
|
|
20
|
-
from setuptools import find_namespace_packages as _find_namespace_packages
|
|
21
|
-
from setuptools import find_packages as _find_packages
|
|
22
|
-
|
|
23
|
-
from winipedia_utils.git.gitignore.gitignore import (
|
|
24
|
-
load_gitignore,
|
|
25
|
-
walk_os_skipping_gitignore_patterns,
|
|
26
|
-
)
|
|
27
|
-
from winipedia_utils.logging.logger import get_logger
|
|
28
|
-
|
|
29
|
-
logger = get_logger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def get_src_package() -> ModuleType:
|
|
33
|
-
"""Identify and return the main source package of the project.
|
|
34
|
-
|
|
35
|
-
Discovers the main source package by finding all top-level packages
|
|
36
|
-
and filtering out the test package. This is useful for automatically
|
|
37
|
-
determining the package that contains the actual implementation code.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
The main source package as a module object
|
|
41
|
-
|
|
42
|
-
Raises:
|
|
43
|
-
StopIteration: If no source package can be found or
|
|
44
|
-
if only the test package exists
|
|
45
|
-
|
|
46
|
-
"""
|
|
47
|
-
from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
|
|
48
|
-
|
|
49
|
-
packages = find_packages_as_modules(depth=0)
|
|
50
|
-
return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def make_dir_with_init_file(path: str | Path) -> None:
|
|
54
|
-
"""Create a directory and initialize it as a Python package.
|
|
55
|
-
|
|
56
|
-
Creates the specified directory (including any necessary parent directories)
|
|
57
|
-
and adds __init__.py files to make it a proper Python package. Optionally
|
|
58
|
-
writes custom content to the __init__.py file.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
path: The directory path to create and initialize as a package
|
|
62
|
-
|
|
63
|
-
Note:
|
|
64
|
-
If the directory already exists, it will not be modified, but __init__.py
|
|
65
|
-
files will still be added if missing.
|
|
66
|
-
|
|
67
|
-
"""
|
|
68
|
-
path = Path(path)
|
|
69
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
70
|
-
make_init_modules_for_package(path)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def module_is_package(obj: ModuleType) -> bool:
|
|
74
|
-
"""Determine if a module object represents a package.
|
|
75
|
-
|
|
76
|
-
Checks if the given module object is a package by looking for the __path__
|
|
77
|
-
attribute, which is only present in package modules.
|
|
78
|
-
|
|
79
|
-
Args:
|
|
80
|
-
obj: The module object to check
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
True if the module is a package, False otherwise
|
|
84
|
-
|
|
85
|
-
Note:
|
|
86
|
-
This works for both regular packages and namespace packages.
|
|
87
|
-
|
|
88
|
-
"""
|
|
89
|
-
return hasattr(obj, "__path__")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def package_name_to_path(package_name: str | Path | ModuleType) -> Path:
|
|
93
|
-
"""Convert a Python package import name to its filesystem path.
|
|
94
|
-
|
|
95
|
-
Transforms a Python package name (with dots) into the corresponding
|
|
96
|
-
directory path by replacing dots with the appropriate directory separator
|
|
97
|
-
for the current operating system.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
package_name: A Python package name to convert
|
|
101
|
-
or a Path object or a ModuleType object
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
A Path object representing the filesystem path to the package
|
|
105
|
-
|
|
106
|
-
Example:
|
|
107
|
-
package_name_to_path("package.subpackage") -> Path("package/subpackage")
|
|
108
|
-
|
|
109
|
-
"""
|
|
110
|
-
if isinstance(package_name, ModuleType):
|
|
111
|
-
package_name = package_name.__name__
|
|
112
|
-
elif isinstance(package_name, Path):
|
|
113
|
-
package_name = package_name.as_posix()
|
|
114
|
-
return Path(package_name.replace(".", os.sep))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def get_modules_and_packages_from_package(
|
|
118
|
-
package: ModuleType,
|
|
119
|
-
) -> tuple[list[ModuleType], list[ModuleType]]:
|
|
120
|
-
"""Extract all direct subpackages and modules from a package.
|
|
121
|
-
|
|
122
|
-
Discovers and imports all direct child modules and subpackages within
|
|
123
|
-
the given package. Returns them as separate lists.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
package: The package module to extract subpackages and modules from
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
A tuple containing (list of subpackages, list of modules)
|
|
130
|
-
|
|
131
|
-
Note:
|
|
132
|
-
Only includes direct children, not recursive descendants.
|
|
133
|
-
All discovered modules and packages are imported during this process.
|
|
134
|
-
|
|
135
|
-
"""
|
|
136
|
-
packages: list[ModuleType] = []
|
|
137
|
-
modules: list[ModuleType] = []
|
|
138
|
-
for _, name, is_pkg in pkgutil.iter_modules(
|
|
139
|
-
package.__path__, prefix=package.__name__ + "."
|
|
140
|
-
):
|
|
141
|
-
mod = import_module(name)
|
|
142
|
-
if is_pkg:
|
|
143
|
-
packages.append(mod)
|
|
144
|
-
else:
|
|
145
|
-
modules.append(mod)
|
|
146
|
-
|
|
147
|
-
# make consistent order
|
|
148
|
-
packages.sort(key=lambda p: p.__name__)
|
|
149
|
-
modules.sort(key=lambda m: m.__name__)
|
|
150
|
-
|
|
151
|
-
return packages, modules
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def find_packages(
|
|
155
|
-
*,
|
|
156
|
-
depth: int | None = None,
|
|
157
|
-
include_namespace_packages: bool = False,
|
|
158
|
-
where: str = ".",
|
|
159
|
-
exclude: Iterable[str] | None = None,
|
|
160
|
-
include: Iterable[str] = ("*",),
|
|
161
|
-
) -> list[str]:
|
|
162
|
-
"""Discover Python packages in the specified directory.
|
|
163
|
-
|
|
164
|
-
Finds all Python packages in the given directory, with options to filter
|
|
165
|
-
by depth, include/exclude patterns, and namespace packages. This is a wrapper
|
|
166
|
-
around setuptools' find_packages and find_namespace_packages functions with
|
|
167
|
-
additional filtering capabilities.
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
depth: Optional maximum depth of package nesting to include (None for unlimited)
|
|
171
|
-
include_namespace_packages: Whether to include namespace packages
|
|
172
|
-
where: Directory to search for packages (default: current directory)
|
|
173
|
-
exclude: Patterns of package names to exclude
|
|
174
|
-
include: Patterns of package names to include
|
|
175
|
-
|
|
176
|
-
Returns:
|
|
177
|
-
A list of package names as strings
|
|
178
|
-
|
|
179
|
-
Example:
|
|
180
|
-
find_packages(depth=1) might return ["package1", "package2"]
|
|
181
|
-
|
|
182
|
-
"""
|
|
183
|
-
if exclude is None:
|
|
184
|
-
exclude = load_gitignore()
|
|
185
|
-
exclude = [
|
|
186
|
-
p.replace("/", ".").removesuffix(".") for p in exclude if p.endswith("/")
|
|
187
|
-
]
|
|
188
|
-
if include_namespace_packages:
|
|
189
|
-
package_names = _find_namespace_packages(
|
|
190
|
-
where=where, exclude=exclude, include=include
|
|
191
|
-
)
|
|
192
|
-
else:
|
|
193
|
-
package_names = _find_packages(where=where, exclude=exclude, include=include)
|
|
194
|
-
|
|
195
|
-
# Convert to list of strings explicitly
|
|
196
|
-
package_names_list: list[str] = list(map(str, package_names))
|
|
197
|
-
|
|
198
|
-
if depth is not None:
|
|
199
|
-
package_names_list = [p for p in package_names_list if p.count(".") <= depth]
|
|
200
|
-
|
|
201
|
-
return package_names_list
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def find_packages_as_modules(
|
|
205
|
-
*,
|
|
206
|
-
depth: int | None = None,
|
|
207
|
-
include_namespace_packages: bool = False,
|
|
208
|
-
where: str = ".",
|
|
209
|
-
exclude: Iterable[str] | None = None,
|
|
210
|
-
include: Iterable[str] = ("*",),
|
|
211
|
-
) -> list[ModuleType]:
|
|
212
|
-
"""Discover and import Python packages in the specified directory.
|
|
213
|
-
|
|
214
|
-
Similar to find_packages, but imports and returns the actual module objects
|
|
215
|
-
instead of just the package names.
|
|
216
|
-
|
|
217
|
-
Args:
|
|
218
|
-
depth: Optional maximum depth of package nesting to include (None for unlimited)
|
|
219
|
-
include_namespace_packages: Whether to include namespace packages
|
|
220
|
-
where: Directory to search for packages (default: current directory)
|
|
221
|
-
exclude: Patterns of package names to exclude
|
|
222
|
-
include: Patterns of package names to include
|
|
223
|
-
|
|
224
|
-
Returns:
|
|
225
|
-
A list of imported package module objects
|
|
226
|
-
|
|
227
|
-
Note:
|
|
228
|
-
All discovered packages are imported during this process.
|
|
229
|
-
|
|
230
|
-
"""
|
|
231
|
-
package_names = find_packages(
|
|
232
|
-
depth=depth,
|
|
233
|
-
include_namespace_packages=include_namespace_packages,
|
|
234
|
-
where=where,
|
|
235
|
-
exclude=exclude,
|
|
236
|
-
include=include,
|
|
237
|
-
)
|
|
238
|
-
return [import_module(package_name) for package_name in package_names]
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def walk_package(
|
|
242
|
-
package: ModuleType,
|
|
243
|
-
) -> Generator[tuple[ModuleType, list[ModuleType]], None, None]:
|
|
244
|
-
"""Recursively walk through a package and all its subpackages.
|
|
245
|
-
|
|
246
|
-
Performs a depth-first traversal of the package hierarchy, yielding each
|
|
247
|
-
package along with its direct module children.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
package: The root package module to start walking from
|
|
251
|
-
|
|
252
|
-
Yields:
|
|
253
|
-
Tuples of (package, list of modules in package)
|
|
254
|
-
|
|
255
|
-
Note:
|
|
256
|
-
All packages and modules are imported during this process.
|
|
257
|
-
The traversal is depth-first, so subpackages are fully processed
|
|
258
|
-
before moving to siblings.
|
|
259
|
-
|
|
260
|
-
"""
|
|
261
|
-
subpackages, submodules = get_modules_and_packages_from_package(package)
|
|
262
|
-
yield package, submodules
|
|
263
|
-
for subpackage in subpackages:
|
|
264
|
-
yield from walk_package(subpackage)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def make_init_modules_for_package(path: str | Path | ModuleType) -> None:
|
|
268
|
-
"""Create __init__.py files in all subdirectories of a package.
|
|
269
|
-
|
|
270
|
-
Ensures that all subdirectories of the given package have __init__.py files,
|
|
271
|
-
effectively converting them into proper Python packages. Skips directories
|
|
272
|
-
that match patterns in .gitignore.
|
|
273
|
-
|
|
274
|
-
Args:
|
|
275
|
-
path: The package path or module object to process
|
|
276
|
-
|
|
277
|
-
Note:
|
|
278
|
-
Does not modify directories that already have __init__.py files.
|
|
279
|
-
Uses the default content for __init__.py files
|
|
280
|
-
from get_default_init_module_content.
|
|
281
|
-
|
|
282
|
-
"""
|
|
283
|
-
from winipedia_utils.modules.module import to_path
|
|
284
|
-
|
|
285
|
-
path = to_path(path, is_package=True)
|
|
286
|
-
|
|
287
|
-
for root, _dirs, files in walk_os_skipping_gitignore_patterns(path):
|
|
288
|
-
if "__init__.py" in files:
|
|
289
|
-
continue
|
|
290
|
-
make_init_module(root)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def make_init_module(path: str | Path) -> None:
|
|
294
|
-
"""Create an __init__.py file in the specified directory.
|
|
295
|
-
|
|
296
|
-
Creates an __init__.py file with default content in the given directory,
|
|
297
|
-
making it a proper Python package.
|
|
298
|
-
|
|
299
|
-
Args:
|
|
300
|
-
path: The directory path where the __init__.py file should be created
|
|
301
|
-
|
|
302
|
-
Note:
|
|
303
|
-
If the path already points to an __init__.py file, that file will be
|
|
304
|
-
overwritten with the default content.
|
|
305
|
-
Creates parent directories if they don't exist.
|
|
306
|
-
|
|
307
|
-
"""
|
|
308
|
-
from winipedia_utils.modules.module import get_default_init_module_content, to_path
|
|
309
|
-
|
|
310
|
-
path = to_path(path, is_package=True)
|
|
311
|
-
|
|
312
|
-
# if __init__.py not in path add it
|
|
313
|
-
if path.name != "__init__.py":
|
|
314
|
-
path = path / "__init__.py"
|
|
315
|
-
|
|
316
|
-
content = get_default_init_module_content()
|
|
317
|
-
|
|
318
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
319
|
-
path.write_text(content)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def copy_package(
|
|
323
|
-
src_package: ModuleType,
|
|
324
|
-
dst: str | Path | ModuleType,
|
|
325
|
-
*,
|
|
326
|
-
with_file_content: bool = True,
|
|
327
|
-
) -> None:
|
|
328
|
-
"""Copy a package to a different destination.
|
|
329
|
-
|
|
330
|
-
Takes a ModuleType of package and a destination package name and then copies
|
|
331
|
-
the package to the destination. If with_file_content is True, it copies the
|
|
332
|
-
content of the files, otherwise it just creates the files.
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
src_package (ModuleType): The package to copy
|
|
336
|
-
dst (str | Path): destination package name as a
|
|
337
|
-
Path with / or as a str with dots
|
|
338
|
-
with_file_content (bool, optional): copies the content of the files.
|
|
339
|
-
|
|
340
|
-
"""
|
|
341
|
-
from winipedia_utils.modules.module import (
|
|
342
|
-
create_module,
|
|
343
|
-
get_isolated_obj_name,
|
|
344
|
-
get_module_content_as_str,
|
|
345
|
-
to_path,
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
src_path = to_path(src_package, is_package=True)
|
|
349
|
-
dst_path = to_path(dst, is_package=True) / get_isolated_obj_name(src_package)
|
|
350
|
-
for package, modules in walk_package(src_package):
|
|
351
|
-
# we need to make right path from the package to the dst
|
|
352
|
-
# so that if we have a package package.package2.package3
|
|
353
|
-
# and dst is a path like package4/package5/package6
|
|
354
|
-
# we get the right path which is package4/package5/package6/package3
|
|
355
|
-
package_path = to_path(package, is_package=True)
|
|
356
|
-
dst_package_path = dst_path / package_path.relative_to(src_path)
|
|
357
|
-
create_module(dst_package_path, is_package=True)
|
|
358
|
-
for module in modules:
|
|
359
|
-
module_name = get_isolated_obj_name(module)
|
|
360
|
-
module_path = dst_package_path / module_name
|
|
361
|
-
create_module(module_path, is_package=False)
|
|
362
|
-
if with_file_content:
|
|
363
|
-
module_path.write_text(get_module_content_as_str(module))
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
def get_main_package() -> ModuleType:
|
|
367
|
-
"""Gets the main package of the executing code.
|
|
368
|
-
|
|
369
|
-
Even when this package is installed as a module.
|
|
370
|
-
"""
|
|
371
|
-
from winipedia_utils.modules.module import to_module_name
|
|
372
|
-
|
|
373
|
-
main = sys.modules.get("__main__")
|
|
374
|
-
if main is None:
|
|
375
|
-
msg = "No __main__ module found"
|
|
376
|
-
raise ValueError(msg)
|
|
377
|
-
|
|
378
|
-
package_name = getattr(main, "__package__", None)
|
|
379
|
-
if package_name:
|
|
380
|
-
package_name = package_name.split(".")[0]
|
|
381
|
-
return import_module(package_name)
|
|
382
|
-
|
|
383
|
-
file_name = getattr(main, "__file__", None)
|
|
384
|
-
if file_name:
|
|
385
|
-
package_name = to_module_name(file_name)
|
|
386
|
-
package_name = package_name.split(".")[0]
|
|
387
|
-
return import_module(package_name)
|
|
388
|
-
|
|
389
|
-
msg = "Not able to determine the main package"
|
|
390
|
-
raise ValueError(msg)
|
|
1
|
+
"""Package utilities for introspection and manipulation.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive utility functions for working with Python packages,
|
|
4
|
+
including package discovery, creation, traversal, and module extraction. It handles
|
|
5
|
+
both regular packages and namespace packages, offering tools for filesystem operations
|
|
6
|
+
and module imports related to package structures.
|
|
7
|
+
|
|
8
|
+
The utilities support both static package analysis and dynamic package manipulation,
|
|
9
|
+
making them suitable for code generation, testing frameworks, and package management.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import pkgutil
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Generator, Iterable
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
|
|
20
|
+
from setuptools import find_namespace_packages as _find_namespace_packages
|
|
21
|
+
from setuptools import find_packages as _find_packages
|
|
22
|
+
|
|
23
|
+
from winipedia_utils.git.gitignore.gitignore import (
|
|
24
|
+
load_gitignore,
|
|
25
|
+
walk_os_skipping_gitignore_patterns,
|
|
26
|
+
)
|
|
27
|
+
from winipedia_utils.logging.logger import get_logger
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_src_package() -> ModuleType:
|
|
33
|
+
"""Identify and return the main source package of the project.
|
|
34
|
+
|
|
35
|
+
Discovers the main source package by finding all top-level packages
|
|
36
|
+
and filtering out the test package. This is useful for automatically
|
|
37
|
+
determining the package that contains the actual implementation code.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The main source package as a module object
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
StopIteration: If no source package can be found or
|
|
44
|
+
if only the test package exists
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
|
|
48
|
+
|
|
49
|
+
packages = find_packages_as_modules(depth=0)
|
|
50
|
+
return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def make_dir_with_init_file(path: str | Path) -> None:
|
|
54
|
+
"""Create a directory and initialize it as a Python package.
|
|
55
|
+
|
|
56
|
+
Creates the specified directory (including any necessary parent directories)
|
|
57
|
+
and adds __init__.py files to make it a proper Python package. Optionally
|
|
58
|
+
writes custom content to the __init__.py file.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: The directory path to create and initialize as a package
|
|
62
|
+
|
|
63
|
+
Note:
|
|
64
|
+
If the directory already exists, it will not be modified, but __init__.py
|
|
65
|
+
files will still be added if missing.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
path = Path(path)
|
|
69
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
make_init_modules_for_package(path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def module_is_package(obj: ModuleType) -> bool:
|
|
74
|
+
"""Determine if a module object represents a package.
|
|
75
|
+
|
|
76
|
+
Checks if the given module object is a package by looking for the __path__
|
|
77
|
+
attribute, which is only present in package modules.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
obj: The module object to check
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if the module is a package, False otherwise
|
|
84
|
+
|
|
85
|
+
Note:
|
|
86
|
+
This works for both regular packages and namespace packages.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
return hasattr(obj, "__path__")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def package_name_to_path(package_name: str | Path | ModuleType) -> Path:
|
|
93
|
+
"""Convert a Python package import name to its filesystem path.
|
|
94
|
+
|
|
95
|
+
Transforms a Python package name (with dots) into the corresponding
|
|
96
|
+
directory path by replacing dots with the appropriate directory separator
|
|
97
|
+
for the current operating system.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
package_name: A Python package name to convert
|
|
101
|
+
or a Path object or a ModuleType object
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A Path object representing the filesystem path to the package
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
package_name_to_path("package.subpackage") -> Path("package/subpackage")
|
|
108
|
+
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(package_name, ModuleType):
|
|
111
|
+
package_name = package_name.__name__
|
|
112
|
+
elif isinstance(package_name, Path):
|
|
113
|
+
package_name = package_name.as_posix()
|
|
114
|
+
return Path(package_name.replace(".", os.sep))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_modules_and_packages_from_package(
|
|
118
|
+
package: ModuleType,
|
|
119
|
+
) -> tuple[list[ModuleType], list[ModuleType]]:
|
|
120
|
+
"""Extract all direct subpackages and modules from a package.
|
|
121
|
+
|
|
122
|
+
Discovers and imports all direct child modules and subpackages within
|
|
123
|
+
the given package. Returns them as separate lists.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
package: The package module to extract subpackages and modules from
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A tuple containing (list of subpackages, list of modules)
|
|
130
|
+
|
|
131
|
+
Note:
|
|
132
|
+
Only includes direct children, not recursive descendants.
|
|
133
|
+
All discovered modules and packages are imported during this process.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
packages: list[ModuleType] = []
|
|
137
|
+
modules: list[ModuleType] = []
|
|
138
|
+
for _, name, is_pkg in pkgutil.iter_modules(
|
|
139
|
+
package.__path__, prefix=package.__name__ + "."
|
|
140
|
+
):
|
|
141
|
+
mod = import_module(name)
|
|
142
|
+
if is_pkg:
|
|
143
|
+
packages.append(mod)
|
|
144
|
+
else:
|
|
145
|
+
modules.append(mod)
|
|
146
|
+
|
|
147
|
+
# make consistent order
|
|
148
|
+
packages.sort(key=lambda p: p.__name__)
|
|
149
|
+
modules.sort(key=lambda m: m.__name__)
|
|
150
|
+
|
|
151
|
+
return packages, modules
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def find_packages(
|
|
155
|
+
*,
|
|
156
|
+
depth: int | None = None,
|
|
157
|
+
include_namespace_packages: bool = False,
|
|
158
|
+
where: str = ".",
|
|
159
|
+
exclude: Iterable[str] | None = None,
|
|
160
|
+
include: Iterable[str] = ("*",),
|
|
161
|
+
) -> list[str]:
|
|
162
|
+
"""Discover Python packages in the specified directory.
|
|
163
|
+
|
|
164
|
+
Finds all Python packages in the given directory, with options to filter
|
|
165
|
+
by depth, include/exclude patterns, and namespace packages. This is a wrapper
|
|
166
|
+
around setuptools' find_packages and find_namespace_packages functions with
|
|
167
|
+
additional filtering capabilities.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
depth: Optional maximum depth of package nesting to include (None for unlimited)
|
|
171
|
+
include_namespace_packages: Whether to include namespace packages
|
|
172
|
+
where: Directory to search for packages (default: current directory)
|
|
173
|
+
exclude: Patterns of package names to exclude
|
|
174
|
+
include: Patterns of package names to include
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A list of package names as strings
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
find_packages(depth=1) might return ["package1", "package2"]
|
|
181
|
+
|
|
182
|
+
"""
|
|
183
|
+
if exclude is None:
|
|
184
|
+
exclude = load_gitignore()
|
|
185
|
+
exclude = [
|
|
186
|
+
p.replace("/", ".").removesuffix(".") for p in exclude if p.endswith("/")
|
|
187
|
+
]
|
|
188
|
+
if include_namespace_packages:
|
|
189
|
+
package_names = _find_namespace_packages(
|
|
190
|
+
where=where, exclude=exclude, include=include
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
package_names = _find_packages(where=where, exclude=exclude, include=include)
|
|
194
|
+
|
|
195
|
+
# Convert to list of strings explicitly
|
|
196
|
+
package_names_list: list[str] = list(map(str, package_names))
|
|
197
|
+
|
|
198
|
+
if depth is not None:
|
|
199
|
+
package_names_list = [p for p in package_names_list if p.count(".") <= depth]
|
|
200
|
+
|
|
201
|
+
return package_names_list
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def find_packages_as_modules(
|
|
205
|
+
*,
|
|
206
|
+
depth: int | None = None,
|
|
207
|
+
include_namespace_packages: bool = False,
|
|
208
|
+
where: str = ".",
|
|
209
|
+
exclude: Iterable[str] | None = None,
|
|
210
|
+
include: Iterable[str] = ("*",),
|
|
211
|
+
) -> list[ModuleType]:
|
|
212
|
+
"""Discover and import Python packages in the specified directory.
|
|
213
|
+
|
|
214
|
+
Similar to find_packages, but imports and returns the actual module objects
|
|
215
|
+
instead of just the package names.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
depth: Optional maximum depth of package nesting to include (None for unlimited)
|
|
219
|
+
include_namespace_packages: Whether to include namespace packages
|
|
220
|
+
where: Directory to search for packages (default: current directory)
|
|
221
|
+
exclude: Patterns of package names to exclude
|
|
222
|
+
include: Patterns of package names to include
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
A list of imported package module objects
|
|
226
|
+
|
|
227
|
+
Note:
|
|
228
|
+
All discovered packages are imported during this process.
|
|
229
|
+
|
|
230
|
+
"""
|
|
231
|
+
package_names = find_packages(
|
|
232
|
+
depth=depth,
|
|
233
|
+
include_namespace_packages=include_namespace_packages,
|
|
234
|
+
where=where,
|
|
235
|
+
exclude=exclude,
|
|
236
|
+
include=include,
|
|
237
|
+
)
|
|
238
|
+
return [import_module(package_name) for package_name in package_names]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def walk_package(
|
|
242
|
+
package: ModuleType,
|
|
243
|
+
) -> Generator[tuple[ModuleType, list[ModuleType]], None, None]:
|
|
244
|
+
"""Recursively walk through a package and all its subpackages.
|
|
245
|
+
|
|
246
|
+
Performs a depth-first traversal of the package hierarchy, yielding each
|
|
247
|
+
package along with its direct module children.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
package: The root package module to start walking from
|
|
251
|
+
|
|
252
|
+
Yields:
|
|
253
|
+
Tuples of (package, list of modules in package)
|
|
254
|
+
|
|
255
|
+
Note:
|
|
256
|
+
All packages and modules are imported during this process.
|
|
257
|
+
The traversal is depth-first, so subpackages are fully processed
|
|
258
|
+
before moving to siblings.
|
|
259
|
+
|
|
260
|
+
"""
|
|
261
|
+
subpackages, submodules = get_modules_and_packages_from_package(package)
|
|
262
|
+
yield package, submodules
|
|
263
|
+
for subpackage in subpackages:
|
|
264
|
+
yield from walk_package(subpackage)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def make_init_modules_for_package(path: str | Path | ModuleType) -> None:
|
|
268
|
+
"""Create __init__.py files in all subdirectories of a package.
|
|
269
|
+
|
|
270
|
+
Ensures that all subdirectories of the given package have __init__.py files,
|
|
271
|
+
effectively converting them into proper Python packages. Skips directories
|
|
272
|
+
that match patterns in .gitignore.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
path: The package path or module object to process
|
|
276
|
+
|
|
277
|
+
Note:
|
|
278
|
+
Does not modify directories that already have __init__.py files.
|
|
279
|
+
Uses the default content for __init__.py files
|
|
280
|
+
from get_default_init_module_content.
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
from winipedia_utils.modules.module import to_path
|
|
284
|
+
|
|
285
|
+
path = to_path(path, is_package=True)
|
|
286
|
+
|
|
287
|
+
for root, _dirs, files in walk_os_skipping_gitignore_patterns(path):
|
|
288
|
+
if "__init__.py" in files:
|
|
289
|
+
continue
|
|
290
|
+
make_init_module(root)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def make_init_module(path: str | Path) -> None:
|
|
294
|
+
"""Create an __init__.py file in the specified directory.
|
|
295
|
+
|
|
296
|
+
Creates an __init__.py file with default content in the given directory,
|
|
297
|
+
making it a proper Python package.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
path: The directory path where the __init__.py file should be created
|
|
301
|
+
|
|
302
|
+
Note:
|
|
303
|
+
If the path already points to an __init__.py file, that file will be
|
|
304
|
+
overwritten with the default content.
|
|
305
|
+
Creates parent directories if they don't exist.
|
|
306
|
+
|
|
307
|
+
"""
|
|
308
|
+
from winipedia_utils.modules.module import get_default_init_module_content, to_path
|
|
309
|
+
|
|
310
|
+
path = to_path(path, is_package=True)
|
|
311
|
+
|
|
312
|
+
# if __init__.py not in path add it
|
|
313
|
+
if path.name != "__init__.py":
|
|
314
|
+
path = path / "__init__.py"
|
|
315
|
+
|
|
316
|
+
content = get_default_init_module_content()
|
|
317
|
+
|
|
318
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
path.write_text(content)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def copy_package(
|
|
323
|
+
src_package: ModuleType,
|
|
324
|
+
dst: str | Path | ModuleType,
|
|
325
|
+
*,
|
|
326
|
+
with_file_content: bool = True,
|
|
327
|
+
) -> None:
|
|
328
|
+
"""Copy a package to a different destination.
|
|
329
|
+
|
|
330
|
+
Takes a ModuleType of package and a destination package name and then copies
|
|
331
|
+
the package to the destination. If with_file_content is True, it copies the
|
|
332
|
+
content of the files, otherwise it just creates the files.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
src_package (ModuleType): The package to copy
|
|
336
|
+
dst (str | Path): destination package name as a
|
|
337
|
+
Path with / or as a str with dots
|
|
338
|
+
with_file_content (bool, optional): copies the content of the files.
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
from winipedia_utils.modules.module import (
|
|
342
|
+
create_module,
|
|
343
|
+
get_isolated_obj_name,
|
|
344
|
+
get_module_content_as_str,
|
|
345
|
+
to_path,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
src_path = to_path(src_package, is_package=True)
|
|
349
|
+
dst_path = to_path(dst, is_package=True) / get_isolated_obj_name(src_package)
|
|
350
|
+
for package, modules in walk_package(src_package):
|
|
351
|
+
# we need to make right path from the package to the dst
|
|
352
|
+
# so that if we have a package package.package2.package3
|
|
353
|
+
# and dst is a path like package4/package5/package6
|
|
354
|
+
# we get the right path which is package4/package5/package6/package3
|
|
355
|
+
package_path = to_path(package, is_package=True)
|
|
356
|
+
dst_package_path = dst_path / package_path.relative_to(src_path)
|
|
357
|
+
create_module(dst_package_path, is_package=True)
|
|
358
|
+
for module in modules:
|
|
359
|
+
module_name = get_isolated_obj_name(module)
|
|
360
|
+
module_path = dst_package_path / module_name
|
|
361
|
+
create_module(module_path, is_package=False)
|
|
362
|
+
if with_file_content:
|
|
363
|
+
module_path.write_text(get_module_content_as_str(module))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def get_main_package() -> ModuleType:
|
|
367
|
+
"""Gets the main package of the executing code.
|
|
368
|
+
|
|
369
|
+
Even when this package is installed as a module.
|
|
370
|
+
"""
|
|
371
|
+
from winipedia_utils.modules.module import to_module_name
|
|
372
|
+
|
|
373
|
+
main = sys.modules.get("__main__")
|
|
374
|
+
if main is None:
|
|
375
|
+
msg = "No __main__ module found"
|
|
376
|
+
raise ValueError(msg)
|
|
377
|
+
|
|
378
|
+
package_name = getattr(main, "__package__", None)
|
|
379
|
+
if package_name:
|
|
380
|
+
package_name = package_name.split(".")[0]
|
|
381
|
+
return import_module(package_name)
|
|
382
|
+
|
|
383
|
+
file_name = getattr(main, "__file__", None)
|
|
384
|
+
if file_name:
|
|
385
|
+
package_name = to_module_name(file_name)
|
|
386
|
+
package_name = package_name.split(".")[0]
|
|
387
|
+
return import_module(package_name)
|
|
388
|
+
|
|
389
|
+
msg = "Not able to determine the main package"
|
|
390
|
+
raise ValueError(msg)
|