winipedia-utils 0.1.0__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 (64) hide show
  1. winipedia_utils/__init__.py +1 -0
  2. winipedia_utils/concurrent/__init__.py +1 -0
  3. winipedia_utils/concurrent/concurrent.py +242 -0
  4. winipedia_utils/concurrent/multiprocessing.py +115 -0
  5. winipedia_utils/concurrent/multithreading.py +93 -0
  6. winipedia_utils/consts.py +22 -0
  7. winipedia_utils/data/__init__.py +1 -0
  8. winipedia_utils/data/dataframe.py +7 -0
  9. winipedia_utils/django/__init__.py +27 -0
  10. winipedia_utils/django/bulk.py +536 -0
  11. winipedia_utils/django/command.py +334 -0
  12. winipedia_utils/django/database.py +304 -0
  13. winipedia_utils/git/__init__.py +1 -0
  14. winipedia_utils/git/gitignore.py +80 -0
  15. winipedia_utils/git/pre_commit/__init__.py +1 -0
  16. winipedia_utils/git/pre_commit/config.py +60 -0
  17. winipedia_utils/git/pre_commit/hooks.py +109 -0
  18. winipedia_utils/git/pre_commit/run_hooks.py +49 -0
  19. winipedia_utils/iterating/__init__.py +1 -0
  20. winipedia_utils/iterating/iterate.py +29 -0
  21. winipedia_utils/logging/__init__.py +1 -0
  22. winipedia_utils/logging/ansi.py +6 -0
  23. winipedia_utils/logging/config.py +64 -0
  24. winipedia_utils/logging/logger.py +26 -0
  25. winipedia_utils/modules/__init__.py +1 -0
  26. winipedia_utils/modules/class_.py +76 -0
  27. winipedia_utils/modules/function.py +86 -0
  28. winipedia_utils/modules/module.py +361 -0
  29. winipedia_utils/modules/package.py +350 -0
  30. winipedia_utils/oop/__init__.py +1 -0
  31. winipedia_utils/oop/mixins/__init__.py +1 -0
  32. winipedia_utils/oop/mixins/meta.py +315 -0
  33. winipedia_utils/oop/mixins/mixin.py +28 -0
  34. winipedia_utils/os/__init__.py +1 -0
  35. winipedia_utils/os/os.py +61 -0
  36. winipedia_utils/projects/__init__.py +1 -0
  37. winipedia_utils/projects/poetry/__init__.py +1 -0
  38. winipedia_utils/projects/poetry/config.py +91 -0
  39. winipedia_utils/projects/poetry/poetry.py +30 -0
  40. winipedia_utils/setup.py +36 -0
  41. winipedia_utils/testing/__init__.py +1 -0
  42. winipedia_utils/testing/assertions.py +23 -0
  43. winipedia_utils/testing/convention.py +177 -0
  44. winipedia_utils/testing/create_tests.py +286 -0
  45. winipedia_utils/testing/fixtures.py +28 -0
  46. winipedia_utils/testing/tests/__init__.py +1 -0
  47. winipedia_utils/testing/tests/base/__init__.py +1 -0
  48. winipedia_utils/testing/tests/base/fixtures/__init__.py +1 -0
  49. winipedia_utils/testing/tests/base/fixtures/fixture.py +6 -0
  50. winipedia_utils/testing/tests/base/fixtures/scopes/__init__.py +1 -0
  51. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +33 -0
  52. winipedia_utils/testing/tests/base/fixtures/scopes/function.py +7 -0
  53. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +31 -0
  54. winipedia_utils/testing/tests/base/fixtures/scopes/package.py +7 -0
  55. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +224 -0
  56. winipedia_utils/testing/tests/base/utils/__init__.py +1 -0
  57. winipedia_utils/testing/tests/base/utils/utils.py +82 -0
  58. winipedia_utils/testing/tests/conftest.py +26 -0
  59. winipedia_utils/text/__init__.py +1 -0
  60. winipedia_utils/text/string.py +126 -0
  61. winipedia_utils-0.1.0.dist-info/LICENSE +21 -0
  62. winipedia_utils-0.1.0.dist-info/METADATA +350 -0
  63. winipedia_utils-0.1.0.dist-info/RECORD +64 -0
  64. winipedia_utils-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,361 @@
1
+ """Module utilities for introspection and manipulation.
2
+
3
+ This module provides comprehensive utility functions for working with Python modules,
4
+ including path conversions, module creation, object importing, and content extraction.
5
+ It handles the complexities of Python's module system by providing a consistent API
6
+ for module operations across different contexts (packages vs. standalone modules).
7
+
8
+ The utilities support both runtime module manipulation and static analysis,
9
+ making them suitable for code generation, testing frameworks, and dynamic imports.
10
+ """
11
+
12
+ import inspect
13
+ import os
14
+ import time
15
+ from collections.abc import Callable, Sequence
16
+ from importlib import import_module
17
+ from pathlib import Path
18
+ from types import ModuleType
19
+ from typing import Any, cast
20
+
21
+ from winipedia_utils.logging.logger import get_logger
22
+ from winipedia_utils.modules.class_ import (
23
+ get_all_cls_from_module,
24
+ get_all_methods_from_cls,
25
+ )
26
+ from winipedia_utils.modules.function import get_all_functions_from_module
27
+ from winipedia_utils.modules.package import (
28
+ get_modules_and_packages_from_package,
29
+ make_dir_with_init_file,
30
+ module_is_package,
31
+ )
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ def get_module_content_as_str(module: ModuleType) -> str:
37
+ """Retrieve the complete source code of a module as a string.
38
+
39
+ This function locates the physical file associated with the given module object
40
+ and reads its entire content. It works for both regular modules and packages
41
+ by determining the correct path using module_to_path.
42
+
43
+ Args:
44
+ module: The module object whose source code should be retrieved
45
+
46
+
47
+ Returns:
48
+ The complete source code of the module as a string
49
+
50
+ """
51
+ path = to_path(module, is_package=False)
52
+ return path.read_text()
53
+
54
+
55
+ def to_module_name(path: str | Path | ModuleType) -> str:
56
+ """Convert a filesystem path to a Python module import name.
57
+
58
+ Transforms a file or directory path into the corresponding Python module name
59
+ by making it relative to the current directory, removing the file extension,
60
+ and replacing directory separators with dots.
61
+
62
+ Args:
63
+ path: a str that represents a path or a Path object or a ModuleType object
64
+ or a str that represents a module name
65
+
66
+ Returns:
67
+ The Python module name corresponding to the path
68
+
69
+ Example:
70
+ path_to_module_name("src/package/module.py") -> "src.package.module"
71
+
72
+ """
73
+ if isinstance(path, ModuleType):
74
+ return path.__name__
75
+ if isinstance(path, Path):
76
+ rel_path = path.relative_to(".")
77
+ if rel_path.suffix:
78
+ rel_path = rel_path.with_suffix("")
79
+ # return joined on . parts
80
+ return ".".join(rel_path.parts)
81
+ if path in (".", "./", ""):
82
+ return ""
83
+ # we get a str that can either be a dotted module name or a path
84
+ # e.g. package/module.py or package/module or
85
+ # package.module or just package/package2
86
+ # or just package with nothing
87
+ path = path.removesuffix(".py")
88
+ if "." in path:
89
+ # already a module name
90
+ return path
91
+ return to_module_name(Path(path))
92
+
93
+
94
+ def to_path(module_name: str | ModuleType | Path, *, is_package: bool) -> Path:
95
+ """Convert a Python module import name to its filesystem path.
96
+
97
+ Transforms a Python module name into the corresponding file path by replacing
98
+ dots with directory separators and adding the .py extension. Uses the
99
+ package_name_to_path function for the directory structure.
100
+
101
+ Args:
102
+ module_name: A Python module name to convert or Path or ModuleType
103
+ is_package: Whether to return the path to the package directory
104
+ without the .py extension
105
+
106
+ Returns:
107
+ A Path object representing the filesystem path to the module
108
+ if is_package is True, returns the path to the package directory
109
+ without the .py extension
110
+
111
+ Example:
112
+ module_name_to_path("src.package.module") -> Path("src/package/module.py")
113
+
114
+ """
115
+ module_name = to_module_name(module_name)
116
+ path = Path(module_name.replace(".", os.sep))
117
+ if is_package:
118
+ return path
119
+ return path.with_suffix(".py")
120
+
121
+
122
+ def create_module(
123
+ module_name: str | Path | ModuleType, *, is_package: bool
124
+ ) -> ModuleType:
125
+ """Create a new Python module file and import it.
126
+
127
+ Creates a new module file at the location specified by the module name,
128
+ ensuring all necessary parent directories and __init__.py files exist.
129
+ Optionally writes content to the module file and parent __init__.py files.
130
+ Finally imports and returns the newly created module.
131
+
132
+ Args:
133
+ module_name: The fully qualified name of the module to create
134
+ is_package: Whether to create a package instead of a module
135
+
136
+ Returns:
137
+ The imported module object representing the newly created module
138
+
139
+ Note:
140
+ Includes a small delay (0.1s) before importing to ensure filesystem operations
141
+ are complete, preventing race conditions.
142
+
143
+ """
144
+ path = to_path(module_name, is_package=is_package)
145
+ if path == Path():
146
+ msg = f"Cannot create module {module_name=} because it is the current directory"
147
+ logger.error(msg)
148
+ raise ValueError(msg)
149
+
150
+ make_dir_with_init_file(path if is_package else path.parent)
151
+ # create the module file if not exists
152
+ if not path.exists() and not is_package:
153
+ path.write_text(get_default_module_content())
154
+
155
+ module_name = to_module_name(path)
156
+ # wait before importing the module
157
+ time.sleep(0.1)
158
+ return import_module(module_name)
159
+
160
+
161
+ def make_obj_importpath(obj: Callable[..., Any] | type | ModuleType) -> str:
162
+ """Create a fully qualified import path string for a Python object.
163
+
164
+ Generates the import path that would be used to import the given object.
165
+ Handles different types of objects (modules, classes, functions) appropriately.
166
+
167
+ Args:
168
+ obj: The object (module, class, or function) to create an import path for
169
+
170
+ Returns:
171
+ A string representing the fully qualified import path for the object
172
+
173
+ Examples:
174
+ For a module: "package.subpackage.module"
175
+ For a class: "package.module.ClassName"
176
+ For a function: "package.module.function_name"
177
+ For a method: "package.module.ClassName.method_name"
178
+
179
+ """
180
+ if isinstance(obj, ModuleType):
181
+ return obj.__name__
182
+ module: str | None = get_module_of_obj(obj).__name__
183
+ obj_name = get_qualname_of_obj(obj)
184
+ if not module:
185
+ return obj_name
186
+ return module + "." + obj_name
187
+
188
+
189
+ def import_obj_from_importpath(
190
+ importpath: str,
191
+ ) -> Callable[..., Any] | type | ModuleType:
192
+ """Import a Python object (module, class, or function) from its import path.
193
+
194
+ Attempts to import the object specified by the given import path. First tries
195
+ to import it as a module, and if that fails, attempts to import it as a class
196
+ or function by splitting the path and using getattr.
197
+
198
+ Args:
199
+ importpath: The fully qualified import path of the object
200
+
201
+ Returns:
202
+ The imported object (module, class, or function)
203
+
204
+ Raises:
205
+ ImportError: If the module part of the path cannot be imported
206
+ AttributeError: If the object is not found in the module
207
+
208
+ """
209
+ try:
210
+ return import_module(importpath)
211
+ except ImportError:
212
+ # might be a class or function
213
+ module_name, obj_name = importpath.rsplit(".", 1)
214
+ module = import_module(module_name)
215
+ obj: Callable[..., Any] | type | ModuleType = getattr(module, obj_name)
216
+ return obj
217
+
218
+
219
+ def get_isolated_obj_name(obj: Callable[..., Any] | type | ModuleType) -> str:
220
+ """Extract the bare name of an object without its module prefix.
221
+
222
+ Retrieves just the name part of an object, without any module path information.
223
+ For modules, returns the last component of the module path.
224
+
225
+ Args:
226
+ obj: The object (module, class, or function) to get the name for
227
+
228
+ Returns:
229
+ The isolated name of the object without any module path
230
+
231
+ Examples:
232
+ For a module "package.subpackage.module": returns "module"
233
+ For a class: returns the class name
234
+ For a function: returns the function name
235
+
236
+ """
237
+ obj = get_unwrapped_obj(obj)
238
+ if isinstance(obj, ModuleType):
239
+ return obj.__name__.split(".")[-1]
240
+ if isinstance(obj, type):
241
+ return obj.__name__
242
+ return get_qualname_of_obj(obj).split(".")[-1]
243
+
244
+
245
+ def get_objs_from_obj(
246
+ obj: Callable[..., Any] | type | ModuleType,
247
+ ) -> Sequence[Callable[..., Any] | type | ModuleType]:
248
+ """Extract all contained objects from a container object.
249
+
250
+ Retrieves all relevant objects contained within the given object, with behavior
251
+ depending on the type of the container:
252
+ - For modules: returns all functions and classes defined in the module
253
+ - For packages: returns all submodules in the package
254
+ - For classes: returns all methods defined directly in the class
255
+ - For other objects: returns an empty list
256
+
257
+ Args:
258
+ obj: The container object to extract contained objects from
259
+
260
+ Returns:
261
+ A sequence of objects contained within the given container object
262
+
263
+ """
264
+ if isinstance(obj, ModuleType):
265
+ if module_is_package(obj):
266
+ return get_modules_and_packages_from_package(obj)[1]
267
+ objs: list[Callable[..., Any] | type] = []
268
+ objs.extend(get_all_functions_from_module(obj))
269
+ objs.extend(get_all_cls_from_module(obj))
270
+ return objs
271
+ if isinstance(obj, type):
272
+ return get_all_methods_from_cls(obj, exclude_parent_methods=True)
273
+ return []
274
+
275
+
276
+ def execute_all_functions_from_module(module: ModuleType) -> list[Any]:
277
+ """Execute all functions defined in a module with no arguments.
278
+
279
+ Retrieves all functions defined in the module and calls each one with no arguments.
280
+ Collects and returns the results of all function calls.
281
+
282
+ Args:
283
+ module: The module containing functions to execute
284
+
285
+ Returns:
286
+ A list containing the return values from all executed functions
287
+
288
+ Note:
289
+ Only executes functions defined directly in the module, not imported functions.
290
+ All functions must accept being called with no arguments.
291
+
292
+ """
293
+ return [f() for f in get_all_functions_from_module(module)]
294
+
295
+
296
+ def get_default_init_module_content() -> str:
297
+ """Generate standardized content for an __init__.py file.
298
+
299
+ Creates a simple docstring for an __init__.py file based on its location,
300
+ following the project's documentation conventions.
301
+
302
+ Args:
303
+ path: The path to the __init__.py file or its parent directory
304
+
305
+ Returns:
306
+ A string containing a properly formatted docstring for the __init__.py file
307
+
308
+ """
309
+ return '''"""__init__ module."""'''
310
+
311
+
312
+ def get_default_module_content() -> str:
313
+ """Generate standardized content for a Python module file.
314
+
315
+ Creates a simple docstring for a module file based on its name,
316
+ following the project's documentation conventions.
317
+
318
+ Returns:
319
+ A string containing a properly formatted docstring for the module file
320
+
321
+ """
322
+ return '''"""module."""'''
323
+
324
+
325
+ def get_def_line(obj: Any) -> int:
326
+ """Return the line number where a method-like object is defined."""
327
+ if isinstance(obj, property):
328
+ obj = obj.fget
329
+ unwrapped = inspect.unwrap(obj)
330
+ return inspect.getsourcelines(unwrapped)[1]
331
+
332
+
333
+ def get_module_of_obj(obj: Any) -> ModuleType:
334
+ """Return the module name where a method-like object is defined.
335
+
336
+ Args:
337
+ obj: Method-like object (funcs, method, property, staticmethod, classmethod...)
338
+
339
+ Returns:
340
+ The module name as a string, or None if module cannot be determined.
341
+
342
+ """
343
+ unwrapped = get_unwrapped_obj(obj)
344
+ module = inspect.getmodule(unwrapped)
345
+ if not module:
346
+ msg = f"Could not determine module of {obj}"
347
+ raise ValueError(msg)
348
+ return module
349
+
350
+
351
+ def get_qualname_of_obj(obj: Callable[..., Any] | type) -> str:
352
+ """Return the name of a method-like object."""
353
+ unwrapped = get_unwrapped_obj(obj)
354
+ return cast("str", unwrapped.__qualname__)
355
+
356
+
357
+ def get_unwrapped_obj(obj: Any) -> Any:
358
+ """Return the unwrapped version of a method-like object."""
359
+ if isinstance(obj, property):
360
+ obj = obj.fget # get the getter function of the property
361
+ return inspect.unwrap(obj)
@@ -0,0 +1,350 @@
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
+ from collections.abc import Generator, Iterable
15
+ from importlib import import_module
16
+ from pathlib import Path
17
+ from types import ModuleType
18
+
19
+ from setuptools import find_namespace_packages as _find_namespace_packages
20
+ from setuptools import find_packages as _find_packages
21
+
22
+ from winipedia_utils.git.gitignore import walk_os_skipping_gitignore_patterns
23
+ from winipedia_utils.logging.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ def get_scr_package() -> ModuleType:
29
+ """Identify and return the main source package of the project.
30
+
31
+ Discovers the main source package by finding all top-level packages
32
+ and filtering out the test package. This is useful for automatically
33
+ determining the package that contains the actual implementation code.
34
+
35
+ Returns:
36
+ The main source package as a module object
37
+
38
+ Raises:
39
+ StopIteration: If no source package can be found or
40
+ if only the test package exists
41
+
42
+ """
43
+ from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
44
+
45
+ packages = find_packages_as_modules(depth=0)
46
+ return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
47
+
48
+
49
+ def make_dir_with_init_file(path: str | Path) -> None:
50
+ """Create a directory and initialize it as a Python package.
51
+
52
+ Creates the specified directory (including any necessary parent directories)
53
+ and adds __init__.py files to make it a proper Python package. Optionally
54
+ writes custom content to the __init__.py file.
55
+
56
+ Args:
57
+ path: The directory path to create and initialize as a package
58
+
59
+ Note:
60
+ If the directory already exists, it will not be modified, but __init__.py
61
+ files will still be added if missing.
62
+
63
+ """
64
+ path = Path(path)
65
+ path.mkdir(parents=True, exist_ok=True)
66
+ make_init_modules_for_package(path)
67
+
68
+
69
+ def module_is_package(obj: ModuleType) -> bool:
70
+ """Determine if a module object represents a package.
71
+
72
+ Checks if the given module object is a package by looking for the __path__
73
+ attribute, which is only present in package modules.
74
+
75
+ Args:
76
+ obj: The module object to check
77
+
78
+ Returns:
79
+ True if the module is a package, False otherwise
80
+
81
+ Note:
82
+ This works for both regular packages and namespace packages.
83
+
84
+ """
85
+ return hasattr(obj, "__path__")
86
+
87
+
88
+ def package_name_to_path(package_name: str | Path | ModuleType) -> Path:
89
+ """Convert a Python package import name to its filesystem path.
90
+
91
+ Transforms a Python package name (with dots) into the corresponding
92
+ directory path by replacing dots with the appropriate directory separator
93
+ for the current operating system.
94
+
95
+ Args:
96
+ package_name: A Python package name to convert
97
+ or a Path object or a ModuleType object
98
+
99
+ Returns:
100
+ A Path object representing the filesystem path to the package
101
+
102
+ Example:
103
+ package_name_to_path("package.subpackage") -> Path("package/subpackage")
104
+
105
+ """
106
+ if isinstance(package_name, ModuleType):
107
+ package_name = package_name.__name__
108
+ elif isinstance(package_name, Path):
109
+ package_name = package_name.as_posix()
110
+ return Path(package_name.replace(".", os.sep))
111
+
112
+
113
+ def get_modules_and_packages_from_package(
114
+ package: ModuleType,
115
+ ) -> tuple[list[ModuleType], list[ModuleType]]:
116
+ """Extract all direct subpackages and modules from a package.
117
+
118
+ Discovers and imports all direct child modules and subpackages within
119
+ the given package. Returns them as separate lists.
120
+
121
+ Args:
122
+ package: The package module to extract subpackages and modules from
123
+
124
+ Returns:
125
+ A tuple containing (list of subpackages, list of modules)
126
+
127
+ Note:
128
+ Only includes direct children, not recursive descendants.
129
+ All discovered modules and packages are imported during this process.
130
+
131
+ """
132
+ packages: list[ModuleType] = []
133
+ modules: list[ModuleType] = []
134
+ for _, name, is_pkg in pkgutil.iter_modules(
135
+ package.__path__, prefix=package.__name__ + "."
136
+ ):
137
+ mod = import_module(name)
138
+ if is_pkg:
139
+ packages.append(mod)
140
+ else:
141
+ modules.append(mod)
142
+
143
+ return packages, modules
144
+
145
+
146
+ def find_packages(
147
+ *,
148
+ depth: int | None = None,
149
+ include_namespace_packages: bool = False,
150
+ where: str = ".",
151
+ exclude: Iterable[str] = (),
152
+ include: Iterable[str] = ("*",),
153
+ ) -> list[str]:
154
+ """Discover Python packages in the specified directory.
155
+
156
+ Finds all Python packages in the given directory, with options to filter
157
+ by depth, include/exclude patterns, and namespace packages. This is a wrapper
158
+ around setuptools' find_packages and find_namespace_packages functions with
159
+ additional filtering capabilities.
160
+
161
+ Args:
162
+ depth: Optional maximum depth of package nesting to include (None for unlimited)
163
+ include_namespace_packages: Whether to include namespace packages
164
+ where: Directory to search for packages (default: current directory)
165
+ exclude: Patterns of package names to exclude
166
+ include: Patterns of package names to include
167
+
168
+ Returns:
169
+ A list of package names as strings
170
+
171
+ Example:
172
+ find_packages(depth=1) might return ["package1", "package2"]
173
+
174
+ """
175
+ if include_namespace_packages:
176
+ package_names = _find_namespace_packages(
177
+ where=where, exclude=exclude, include=include
178
+ )
179
+ else:
180
+ package_names = _find_packages(where=where, exclude=exclude, include=include)
181
+
182
+ # Convert to list of strings explicitly
183
+ package_names_list: list[str] = list(map(str, package_names))
184
+
185
+ if depth is not None:
186
+ package_names_list = [p for p in package_names_list if p.count(".") <= depth]
187
+
188
+ return package_names_list
189
+
190
+
191
+ def find_packages_as_modules(
192
+ *,
193
+ depth: int | None = None,
194
+ include_namespace_packages: bool = False,
195
+ where: str = ".",
196
+ exclude: Iterable[str] = (),
197
+ include: Iterable[str] = ("*",),
198
+ ) -> list[ModuleType]:
199
+ """Discover and import Python packages in the specified directory.
200
+
201
+ Similar to find_packages, but imports and returns the actual module objects
202
+ instead of just the package names.
203
+
204
+ Args:
205
+ depth: Optional maximum depth of package nesting to include (None for unlimited)
206
+ include_namespace_packages: Whether to include namespace packages
207
+ where: Directory to search for packages (default: current directory)
208
+ exclude: Patterns of package names to exclude
209
+ include: Patterns of package names to include
210
+
211
+ Returns:
212
+ A list of imported package module objects
213
+
214
+ Note:
215
+ All discovered packages are imported during this process.
216
+
217
+ """
218
+ package_names = find_packages(
219
+ depth=depth,
220
+ include_namespace_packages=include_namespace_packages,
221
+ where=where,
222
+ exclude=exclude,
223
+ include=include,
224
+ )
225
+ return [import_module(package_name) for package_name in package_names]
226
+
227
+
228
+ def walk_package(
229
+ package: ModuleType,
230
+ ) -> Generator[tuple[ModuleType, list[ModuleType]], None, None]:
231
+ """Recursively walk through a package and all its subpackages.
232
+
233
+ Performs a depth-first traversal of the package hierarchy, yielding each
234
+ package along with its direct module children.
235
+
236
+ Args:
237
+ package: The root package module to start walking from
238
+
239
+ Yields:
240
+ Tuples of (package, list of modules in package)
241
+
242
+ Note:
243
+ All packages and modules are imported during this process.
244
+ The traversal is depth-first, so subpackages are fully processed
245
+ before moving to siblings.
246
+
247
+ """
248
+ subpackages, submodules = get_modules_and_packages_from_package(package)
249
+ yield package, submodules
250
+ for subpackage in subpackages:
251
+ yield from walk_package(subpackage)
252
+
253
+
254
+ def make_init_modules_for_package(path: str | Path | ModuleType) -> None:
255
+ """Create __init__.py files in all subdirectories of a package.
256
+
257
+ Ensures that all subdirectories of the given package have __init__.py files,
258
+ effectively converting them into proper Python packages. Skips directories
259
+ that match patterns in .gitignore.
260
+
261
+ Args:
262
+ path: The package path or module object to process
263
+
264
+ Note:
265
+ Does not modify directories that already have __init__.py files.
266
+ Uses the default content for __init__.py files
267
+ from get_default_init_module_content.
268
+
269
+ """
270
+ from winipedia_utils.modules.module import to_path
271
+
272
+ path = to_path(path, is_package=True)
273
+
274
+ for root, _dirs, files in walk_os_skipping_gitignore_patterns(path):
275
+ if "__init__.py" in files:
276
+ continue
277
+ make_init_module(root)
278
+
279
+
280
+ def make_init_module(path: str | Path) -> None:
281
+ """Create an __init__.py file in the specified directory.
282
+
283
+ Creates an __init__.py file with default content in the given directory,
284
+ making it a proper Python package.
285
+
286
+ Args:
287
+ path: The directory path where the __init__.py file should be created
288
+
289
+ Note:
290
+ If the path already points to an __init__.py file, that file will be
291
+ overwritten with the default content.
292
+ Creates parent directories if they don't exist.
293
+
294
+ """
295
+ from winipedia_utils.modules.module import get_default_init_module_content, to_path
296
+
297
+ path = to_path(path, is_package=True)
298
+
299
+ # if __init__.py not in path add it
300
+ if path.name != "__init__.py":
301
+ path = path / "__init__.py"
302
+
303
+ content = get_default_init_module_content()
304
+
305
+ path.parent.mkdir(parents=True, exist_ok=True)
306
+ path.write_text(content)
307
+
308
+
309
+ def copy_package(
310
+ src_package: ModuleType,
311
+ dst: str | Path | ModuleType,
312
+ *,
313
+ with_file_content: bool = True,
314
+ ) -> None:
315
+ """Copy a package to a different destination.
316
+
317
+ Takes a ModuleType of package and a destination package name and then copies
318
+ the package to the destination. If with_file_content is True, it copies the
319
+ content of the files, otherwise it just creates the files.
320
+
321
+ Args:
322
+ src_package (ModuleType): The package to copy
323
+ dst (str | Path): destination package name as a
324
+ Path with / or as a str with dots
325
+ with_file_content (bool, optional): copies the content of the files.
326
+
327
+ """
328
+ from winipedia_utils.modules.module import (
329
+ create_module,
330
+ get_isolated_obj_name,
331
+ get_module_content_as_str,
332
+ to_path,
333
+ )
334
+
335
+ src_path = to_path(src_package, is_package=True)
336
+ dst_path = to_path(dst, is_package=True) / get_isolated_obj_name(src_package)
337
+ for package, modules in walk_package(src_package):
338
+ # we need to make right path from the package to the dst
339
+ # so that if we have a package package.package2.package3
340
+ # and dst is a path like package4/package5/package6
341
+ # we get the right path which is package4/package5/package6/package3
342
+ package_path = to_path(package, is_package=True)
343
+ dst_package_path = dst_path / package_path.relative_to(src_path)
344
+ create_module(dst_package_path, is_package=True)
345
+ for module in modules:
346
+ module_name = get_isolated_obj_name(module)
347
+ module_path = dst_package_path / module_name
348
+ create_module(module_path, is_package=False)
349
+ if with_file_content:
350
+ module_path.write_text(get_module_content_as_str(module))