winipedia-utils 0.2.63__py3-none-any.whl → 0.6.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.
Potentially problematic release.
This version of winipedia-utils might be problematic. Click here for more details.
- winipedia_utils/artifacts/build.py +78 -0
- winipedia_utils/concurrent/concurrent.py +7 -2
- winipedia_utils/concurrent/multiprocessing.py +1 -2
- winipedia_utils/concurrent/multithreading.py +2 -2
- winipedia_utils/data/dataframe/cleaning.py +337 -100
- winipedia_utils/git/github/__init__.py +1 -0
- winipedia_utils/git/github/github.py +31 -0
- winipedia_utils/git/github/repo/__init__.py +1 -0
- winipedia_utils/git/github/repo/protect.py +103 -0
- winipedia_utils/git/github/repo/repo.py +205 -0
- winipedia_utils/git/github/workflows/base/__init__.py +1 -0
- winipedia_utils/git/github/workflows/base/base.py +889 -0
- winipedia_utils/git/github/workflows/health_check.py +69 -0
- winipedia_utils/git/github/workflows/publish.py +51 -0
- winipedia_utils/git/github/workflows/release.py +90 -0
- winipedia_utils/git/gitignore/config.py +77 -0
- winipedia_utils/git/gitignore/gitignore.py +5 -63
- winipedia_utils/git/pre_commit/config.py +49 -59
- winipedia_utils/git/pre_commit/hooks.py +46 -46
- winipedia_utils/git/pre_commit/run_hooks.py +19 -12
- winipedia_utils/iterating/iterate.py +63 -1
- winipedia_utils/modules/class_.py +69 -12
- winipedia_utils/modules/function.py +26 -3
- winipedia_utils/modules/inspection.py +56 -0
- winipedia_utils/modules/module.py +22 -28
- winipedia_utils/modules/package.py +116 -10
- winipedia_utils/projects/poetry/config.py +255 -112
- winipedia_utils/projects/poetry/poetry.py +230 -13
- winipedia_utils/projects/project.py +11 -42
- winipedia_utils/setup.py +11 -29
- winipedia_utils/testing/config.py +127 -0
- winipedia_utils/testing/create_tests.py +5 -19
- winipedia_utils/testing/skip.py +19 -0
- winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
- winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +3 -3
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +9 -6
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +27 -176
- winipedia_utils/testing/tests/base/utils/utils.py +27 -57
- winipedia_utils/text/config.py +250 -0
- winipedia_utils/text/string.py +30 -0
- winipedia_utils-0.6.6.dist-info/METADATA +390 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/RECORD +46 -34
- winipedia_utils/consts.py +0 -21
- winipedia_utils/git/workflows/base/base.py +0 -77
- winipedia_utils/git/workflows/publish.py +0 -79
- winipedia_utils/git/workflows/release.py +0 -91
- winipedia_utils-0.2.63.dist-info/METADATA +0 -738
- /winipedia_utils/{git/workflows/base → artifacts}/__init__.py +0 -0
- /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/WHEEL +0 -0
- {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,11 +15,10 @@ from winipedia_utils.os.os import run_subprocess
|
|
|
15
15
|
logger = get_logger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def run_hooks() -> None:
|
|
19
19
|
"""Import all funcs defined in hooks.py and runs them."""
|
|
20
20
|
hook_funcs = get_all_functions_from_module(hooks)
|
|
21
21
|
|
|
22
|
-
exit_code = 0
|
|
23
22
|
for hook_func in hook_funcs:
|
|
24
23
|
subprocess_args = hook_func()
|
|
25
24
|
result = run_subprocess(
|
|
@@ -28,22 +27,30 @@ def _run_all_hooks() -> None:
|
|
|
28
27
|
passed = result.returncode == 0
|
|
29
28
|
|
|
30
29
|
log_method = logger.info
|
|
31
|
-
|
|
30
|
+
status_str = (f"{GREEN}PASSED" if passed else f"{RED}FAILED") + RESET
|
|
32
31
|
if not passed:
|
|
33
32
|
log_method = logger.error
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
status_str += f"""
|
|
34
|
+
---------------------------------------------------------------------------------------------
|
|
35
|
+
Stdout:
|
|
36
|
+
|
|
37
|
+
{result.stdout}
|
|
38
|
+
|
|
39
|
+
---------------------------------------------------------------------------------------------
|
|
40
|
+
Stderr:
|
|
41
|
+
|
|
42
|
+
{result.stderr}
|
|
43
|
+
|
|
44
|
+
---------------------------------------------------------------------------------------------
|
|
45
|
+
"""
|
|
46
|
+
|
|
36
47
|
# make the dashes always the same lentgth by adjusting to len of hook name
|
|
37
48
|
num_dashes = 50 - len(hook_func.__name__)
|
|
38
49
|
log_method(
|
|
39
50
|
"Hook %s -%s> %s",
|
|
40
51
|
hook_func.__name__,
|
|
41
52
|
"-" * num_dashes,
|
|
42
|
-
|
|
53
|
+
status_str,
|
|
43
54
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if __name__ == "__main__":
|
|
49
|
-
_run_all_hooks()
|
|
55
|
+
if not passed:
|
|
56
|
+
sys.exit(1)
|
|
@@ -5,7 +5,7 @@ including getting the length of an iterable with a default value.
|
|
|
5
5
|
These utilities help with iterable operations and manipulations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from collections.abc import Iterable
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
|
|
@@ -27,3 +27,65 @@ def get_len_with_default(iterable: Iterable[Any], default: int | None = None) ->
|
|
|
27
27
|
msg = "Can't get length of iterable and no default value provided"
|
|
28
28
|
raise TypeError(msg) from e
|
|
29
29
|
return default
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def nested_structure_is_subset(
|
|
33
|
+
subset: dict[Any, Any] | list[Any] | Any,
|
|
34
|
+
superset: dict[Any, Any] | list[Any] | Any,
|
|
35
|
+
on_false_dict_action: Callable[[dict[Any, Any], dict[Any, Any], Any], Any]
|
|
36
|
+
| None = None,
|
|
37
|
+
on_false_list_action: Callable[[list[Any], list[Any], int], Any] | None = None,
|
|
38
|
+
) -> bool:
|
|
39
|
+
"""Check if a dictionary is a nested subset of another dictionary.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
subset: Dictionary to check
|
|
43
|
+
superset: Dictionary to check against
|
|
44
|
+
on_false_dict_action: Action to take on each false dict comparison
|
|
45
|
+
must return a bool to indicate if after action is still false
|
|
46
|
+
on_false_list_action: Action to take on each false list comparison
|
|
47
|
+
must return a bool to indicate if after action is still false
|
|
48
|
+
|
|
49
|
+
Each value of a key must be equal to the value of the same key in the superset.
|
|
50
|
+
If the value is dictionary, the function is called recursively.
|
|
51
|
+
If the value is list, each item must be in the list of the same key in the superset.
|
|
52
|
+
The order in lists does not matter.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
True if subset is a nested subset of superset, False otherwise
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(subset, dict) and isinstance(superset, dict):
|
|
58
|
+
iterable: Iterable[tuple[Any, Any]] = subset.items()
|
|
59
|
+
on_false_action: Callable[[Any, Any, Any], Any] | None = on_false_dict_action
|
|
60
|
+
|
|
61
|
+
def get_actual(key_or_index: Any) -> Any:
|
|
62
|
+
"""Get actual value from superset."""
|
|
63
|
+
return superset.get(key_or_index)
|
|
64
|
+
|
|
65
|
+
elif isinstance(subset, list) and isinstance(superset, list):
|
|
66
|
+
iterable = enumerate(subset)
|
|
67
|
+
on_false_action = on_false_list_action
|
|
68
|
+
|
|
69
|
+
def get_actual(key_or_index: Any) -> Any:
|
|
70
|
+
"""Get actual value from superset."""
|
|
71
|
+
subset_val = subset[key_or_index]
|
|
72
|
+
for superset_val in superset:
|
|
73
|
+
if nested_structure_is_subset(subset_val, superset_val):
|
|
74
|
+
return superset_val
|
|
75
|
+
|
|
76
|
+
return superset[key_or_index] if key_or_index < len(superset) else None
|
|
77
|
+
else:
|
|
78
|
+
return subset == superset
|
|
79
|
+
|
|
80
|
+
all_good = True
|
|
81
|
+
for key_or_index, value in iterable:
|
|
82
|
+
actual_value = get_actual(key_or_index)
|
|
83
|
+
if not nested_structure_is_subset(
|
|
84
|
+
value, actual_value, on_false_dict_action, on_false_list_action
|
|
85
|
+
):
|
|
86
|
+
all_good = False
|
|
87
|
+
if on_false_action is not None:
|
|
88
|
+
on_false_action(subset, superset, key_or_index)
|
|
89
|
+
all_good = nested_structure_is_subset(subset, superset)
|
|
90
|
+
|
|
91
|
+
return all_good
|
|
@@ -13,10 +13,14 @@ from types import ModuleType
|
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
15
|
from winipedia_utils.modules.function import is_func
|
|
16
|
+
from winipedia_utils.modules.inspection import get_def_line, get_obj_members
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def get_all_methods_from_cls(
|
|
19
|
-
class_: type,
|
|
20
|
+
class_: type,
|
|
21
|
+
*,
|
|
22
|
+
exclude_parent_methods: bool = False,
|
|
23
|
+
include_annotate: bool = False,
|
|
20
24
|
) -> list[Callable[..., Any]]:
|
|
21
25
|
"""Get all methods from a class.
|
|
22
26
|
|
|
@@ -27,14 +31,21 @@ def get_all_methods_from_cls(
|
|
|
27
31
|
class_: The class to extract methods from
|
|
28
32
|
exclude_parent_methods: If True, only include methods defined in this class,
|
|
29
33
|
excluding those inherited from parent classes
|
|
34
|
+
include_annotate: If False, exclude __annotate__ method
|
|
35
|
+
introduced in Python 3.14, defaults to False
|
|
36
|
+
|
|
30
37
|
Returns:
|
|
31
38
|
A list of callable methods from the class
|
|
32
39
|
|
|
33
40
|
"""
|
|
34
|
-
from winipedia_utils.modules.module import
|
|
41
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
42
|
+
get_module_of_obj,
|
|
43
|
+
)
|
|
35
44
|
|
|
36
45
|
methods = [
|
|
37
|
-
(method, name)
|
|
46
|
+
(method, name)
|
|
47
|
+
for name, method in get_obj_members(class_, include_annotate=include_annotate)
|
|
48
|
+
if is_func(method)
|
|
38
49
|
]
|
|
39
50
|
|
|
40
51
|
if exclude_parent_methods:
|
|
@@ -63,7 +74,9 @@ def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
|
|
|
63
74
|
A list of class types defined in the module
|
|
64
75
|
|
|
65
76
|
"""
|
|
66
|
-
from winipedia_utils.modules.module import
|
|
77
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
78
|
+
get_module_of_obj,
|
|
79
|
+
)
|
|
67
80
|
|
|
68
81
|
if isinstance(module, str):
|
|
69
82
|
module = import_module(module)
|
|
@@ -79,7 +92,9 @@ def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
|
|
|
79
92
|
return sorted(classes, key=get_def_line)
|
|
80
93
|
|
|
81
94
|
|
|
82
|
-
def get_all_subclasses(
|
|
95
|
+
def get_all_subclasses(
|
|
96
|
+
cls: type, load_package_before: ModuleType | None = None
|
|
97
|
+
) -> set[type]:
|
|
83
98
|
"""Get all subclasses of a class.
|
|
84
99
|
|
|
85
100
|
Retrieves all classes that are subclasses of the specified class.
|
|
@@ -87,18 +102,37 @@ def get_all_subclasses(cls: type) -> list[type]:
|
|
|
87
102
|
|
|
88
103
|
Args:
|
|
89
104
|
cls: The class to find subclasses of
|
|
105
|
+
load_package_before: If provided,
|
|
106
|
+
walks the package before loading the subclasses
|
|
107
|
+
This is useful when the subclasses are defined in other modules that need
|
|
108
|
+
to be imported before they can be found by __subclasses__
|
|
90
109
|
|
|
91
110
|
Returns:
|
|
92
111
|
A list of subclasses of the given class
|
|
93
112
|
|
|
94
113
|
"""
|
|
114
|
+
from winipedia_utils.modules.package import ( # noqa: PLC0415 # avoid circular import
|
|
115
|
+
walk_package,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if load_package_before:
|
|
119
|
+
_ = list(walk_package(load_package_before))
|
|
95
120
|
subclasses_set = set(cls.__subclasses__())
|
|
96
121
|
for subclass in cls.__subclasses__():
|
|
97
122
|
subclasses_set.update(get_all_subclasses(subclass))
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
123
|
+
if load_package_before is not None:
|
|
124
|
+
# remove all not in the package
|
|
125
|
+
subclasses_set = {
|
|
126
|
+
subclass
|
|
127
|
+
for subclass in subclasses_set
|
|
128
|
+
if subclass.__module__.startswith(load_package_before.__name__)
|
|
129
|
+
}
|
|
130
|
+
return subclasses_set
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_all_nonabstract_subclasses(
|
|
134
|
+
cls: type, load_package_before: ModuleType | None = None
|
|
135
|
+
) -> set[type]:
|
|
102
136
|
"""Get all non-abstract subclasses of a class.
|
|
103
137
|
|
|
104
138
|
Retrieves all classes that are subclasses of the specified class,
|
|
@@ -107,13 +141,36 @@ def get_all_nonabstract_subclasses(cls: type) -> list[type]:
|
|
|
107
141
|
|
|
108
142
|
Args:
|
|
109
143
|
cls: The class to find subclasses of
|
|
144
|
+
load_package_before: If provided,
|
|
145
|
+
walks the package before loading the subclasses
|
|
146
|
+
This is useful when the subclasses are defined in other modules that need
|
|
147
|
+
to be imported before they can be found by __subclasses__
|
|
110
148
|
|
|
111
149
|
Returns:
|
|
112
150
|
A list of non-abstract subclasses of the given class
|
|
113
151
|
|
|
114
152
|
"""
|
|
115
|
-
return
|
|
153
|
+
return {
|
|
116
154
|
subclass
|
|
117
|
-
for subclass in get_all_subclasses(cls)
|
|
155
|
+
for subclass in get_all_subclasses(cls, load_package_before=load_package_before)
|
|
118
156
|
if not inspect.isabstract(subclass)
|
|
119
|
-
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def init_all_nonabstract_subclasses(
|
|
161
|
+
cls: type, load_package_before: ModuleType | None = None
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Initialize all non-abstract subclasses of a class.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
cls: The class to find subclasses of
|
|
167
|
+
load_package_before: If provided,
|
|
168
|
+
walks the package before loading the subclasses
|
|
169
|
+
This is useful when the subclasses are defined in other modules that need
|
|
170
|
+
to be imported before they can be found by __subclasses__
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
for subclass in get_all_nonabstract_subclasses(
|
|
174
|
+
cls, load_package_before=load_package_before
|
|
175
|
+
):
|
|
176
|
+
subclass()
|
|
@@ -12,6 +12,8 @@ from importlib import import_module
|
|
|
12
12
|
from types import ModuleType
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
from winipedia_utils.modules.inspection import get_def_line, get_obj_members
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
def is_func_or_method(obj: Any) -> bool:
|
|
17
19
|
"""Return True if *obj* is a function or method.
|
|
@@ -57,7 +59,9 @@ def is_func(obj: Any) -> bool:
|
|
|
57
59
|
return is_func_or_method(unwrapped)
|
|
58
60
|
|
|
59
61
|
|
|
60
|
-
def get_all_functions_from_module(
|
|
62
|
+
def get_all_functions_from_module(
|
|
63
|
+
module: ModuleType | str, *, include_annotate: bool = False
|
|
64
|
+
) -> list[Callable[..., Any]]:
|
|
61
65
|
"""Get all functions defined in a module.
|
|
62
66
|
|
|
63
67
|
Retrieves all function objects that are defined directly in the specified module,
|
|
@@ -66,18 +70,23 @@ def get_all_functions_from_module(module: ModuleType | str) -> list[Callable[...
|
|
|
66
70
|
|
|
67
71
|
Args:
|
|
68
72
|
module: The module to extract functions from
|
|
73
|
+
include_annotate: If False, exclude __annotate__ method
|
|
74
|
+
introduced in Python 3.14, defaults to False
|
|
69
75
|
|
|
70
76
|
Returns:
|
|
71
77
|
A list of callable functions defined in the module
|
|
72
78
|
|
|
73
79
|
"""
|
|
74
|
-
from winipedia_utils.modules.module import
|
|
80
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
81
|
+
get_module_of_obj,
|
|
82
|
+
)
|
|
75
83
|
|
|
76
84
|
if isinstance(module, str):
|
|
77
85
|
module = import_module(module)
|
|
78
86
|
funcs = [
|
|
79
87
|
func
|
|
80
|
-
for _name, func in
|
|
88
|
+
for _name, func in get_obj_members(module, include_annotate=include_annotate)
|
|
89
|
+
if is_func(func)
|
|
81
90
|
if get_module_of_obj(func).__name__ == module.__name__
|
|
82
91
|
]
|
|
83
92
|
# sort by definition order
|
|
@@ -99,3 +108,17 @@ def unwrap_method(method: Any) -> Callable[..., Any] | Any:
|
|
|
99
108
|
if isinstance(method, property):
|
|
100
109
|
method = method.fget
|
|
101
110
|
return inspect.unwrap(method)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_abstractmethod(method: Any) -> bool:
|
|
114
|
+
"""Check if a method is an abstract method.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
method: The method to check
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if the method is an abstract method, False otherwise
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
method = unwrap_method(method)
|
|
124
|
+
return getattr(method, "__isabstractmethod__", False)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Inspection utilities for introspecting Python objects.
|
|
2
|
+
|
|
3
|
+
This module provides utility functions for inspecting Python objects,
|
|
4
|
+
including checking if an object is a function or method, and unwrapping
|
|
5
|
+
methods to their underlying functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import inspect
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_obj_members(
|
|
15
|
+
obj: Any, *, include_annotate: bool = False
|
|
16
|
+
) -> list[tuple[str, Any]]:
|
|
17
|
+
"""Get all members of an object."""
|
|
18
|
+
members = [(member, value) for member, value in inspect.getmembers(obj)]
|
|
19
|
+
if not include_annotate:
|
|
20
|
+
members = [
|
|
21
|
+
(member, value)
|
|
22
|
+
for member, value in members
|
|
23
|
+
if member not in ("__annotate__", "__annotate_func__")
|
|
24
|
+
]
|
|
25
|
+
return members
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inside_frozen_bundle() -> bool:
|
|
29
|
+
"""Return True if the code is running inside a frozen bundle."""
|
|
30
|
+
return getattr(sys, "frozen", False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_def_line(obj: Any) -> int:
|
|
34
|
+
"""Return the line number where a method-like object is defined."""
|
|
35
|
+
if isinstance(obj, property):
|
|
36
|
+
obj = obj.fget
|
|
37
|
+
unwrapped = inspect.unwrap(obj)
|
|
38
|
+
if hasattr(unwrapped, "__code__"):
|
|
39
|
+
return int(unwrapped.__code__.co_firstlineno)
|
|
40
|
+
# getsourcelines does not work if in a pyinstaller bundle or something
|
|
41
|
+
if inside_frozen_bundle():
|
|
42
|
+
return 0
|
|
43
|
+
return inspect.getsourcelines(unwrapped)[1]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_unwrapped_obj(obj: Any) -> Any:
|
|
47
|
+
"""Return the unwrapped version of a method-like object."""
|
|
48
|
+
if isinstance(obj, property):
|
|
49
|
+
obj = obj.fget # get the getter function of the property
|
|
50
|
+
return inspect.unwrap(obj)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_qualname_of_obj(obj: Callable[..., Any] | type) -> str:
|
|
54
|
+
"""Return the name of a method-like object."""
|
|
55
|
+
unwrapped = get_unwrapped_obj(obj)
|
|
56
|
+
return cast("str", unwrapped.__qualname__)
|
|
@@ -17,7 +17,7 @@ from collections.abc import Callable, Sequence
|
|
|
17
17
|
from importlib import import_module
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from types import ModuleType
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
21
21
|
|
|
22
22
|
from winipedia_utils.logging.logger import get_logger
|
|
23
23
|
from winipedia_utils.modules.class_ import (
|
|
@@ -25,6 +25,7 @@ from winipedia_utils.modules.class_ import (
|
|
|
25
25
|
get_all_methods_from_cls,
|
|
26
26
|
)
|
|
27
27
|
from winipedia_utils.modules.function import get_all_functions_from_module
|
|
28
|
+
from winipedia_utils.modules.inspection import get_qualname_of_obj, get_unwrapped_obj
|
|
28
29
|
from winipedia_utils.modules.package import (
|
|
29
30
|
get_modules_and_packages_from_package,
|
|
30
31
|
make_dir_with_init_file,
|
|
@@ -215,6 +216,8 @@ def import_obj_from_importpath(
|
|
|
215
216
|
return import_module(importpath)
|
|
216
217
|
except ImportError:
|
|
217
218
|
# might be a class or function
|
|
219
|
+
if "." not in importpath:
|
|
220
|
+
raise
|
|
218
221
|
module_name, obj_name = importpath.rsplit(".", 1)
|
|
219
222
|
module = import_module(module_name)
|
|
220
223
|
obj: Callable[..., Any] | type | ModuleType = getattr(module, obj_name)
|
|
@@ -327,24 +330,6 @@ def get_default_module_content() -> str:
|
|
|
327
330
|
return '''"""module."""'''
|
|
328
331
|
|
|
329
332
|
|
|
330
|
-
def inside_frozen_bundle() -> bool:
|
|
331
|
-
"""Return True if the code is running inside a frozen bundle."""
|
|
332
|
-
return getattr(sys, "frozen", False)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def get_def_line(obj: Any) -> int:
|
|
336
|
-
"""Return the line number where a method-like object is defined."""
|
|
337
|
-
if isinstance(obj, property):
|
|
338
|
-
obj = obj.fget
|
|
339
|
-
unwrapped = inspect.unwrap(obj)
|
|
340
|
-
if hasattr(unwrapped, "__code__"):
|
|
341
|
-
return int(unwrapped.__code__.co_firstlineno)
|
|
342
|
-
# getsourcelines does not work if in a pyinstaller bundle or something
|
|
343
|
-
if inside_frozen_bundle():
|
|
344
|
-
return 0
|
|
345
|
-
return inspect.getsourcelines(unwrapped)[1]
|
|
346
|
-
|
|
347
|
-
|
|
348
333
|
def get_module_of_obj(obj: Any, default: ModuleType | None = None) -> ModuleType:
|
|
349
334
|
"""Return the module name where a method-like object is defined.
|
|
350
335
|
|
|
@@ -366,14 +351,23 @@ def get_module_of_obj(obj: Any, default: ModuleType | None = None) -> ModuleType
|
|
|
366
351
|
return module
|
|
367
352
|
|
|
368
353
|
|
|
369
|
-
def
|
|
370
|
-
"""
|
|
371
|
-
unwrapped = get_unwrapped_obj(obj)
|
|
372
|
-
return cast("str", unwrapped.__qualname__)
|
|
354
|
+
def get_executing_module() -> ModuleType:
|
|
355
|
+
"""Get the module where execution has started.
|
|
373
356
|
|
|
357
|
+
The executing module is the module that contains the __main__ attribute as __name__
|
|
358
|
+
E.g. if you run `python -m winipedia_utils.setup` from the command line,
|
|
359
|
+
then the executing module is winipedia_utils.modules.setup
|
|
374
360
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
361
|
+
Returns:
|
|
362
|
+
The module where execution has started
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
ValueError: If no __main__ module is found or if the executing module
|
|
366
|
+
cannot be determined
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
main = sys.modules.get("__main__")
|
|
370
|
+
if main is None:
|
|
371
|
+
msg = "No __main__ module found"
|
|
372
|
+
raise ValueError(msg)
|
|
373
|
+
return main
|
|
@@ -9,21 +9,23 @@ The utilities support both static package analysis and dynamic package manipulat
|
|
|
9
9
|
making them suitable for code generation, testing frameworks, and package management.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import importlib.metadata
|
|
13
|
+
import importlib.util
|
|
12
14
|
import os
|
|
13
15
|
import pkgutil
|
|
16
|
+
import re
|
|
14
17
|
import sys
|
|
15
18
|
from collections.abc import Generator, Iterable
|
|
16
19
|
from importlib import import_module
|
|
17
20
|
from pathlib import Path
|
|
18
21
|
from types import ModuleType
|
|
22
|
+
from typing import Any
|
|
19
23
|
|
|
24
|
+
import networkx as nx
|
|
20
25
|
from setuptools import find_namespace_packages as _find_namespace_packages
|
|
21
26
|
from setuptools import find_packages as _find_packages
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
load_gitignore,
|
|
25
|
-
walk_os_skipping_gitignore_patterns,
|
|
26
|
-
)
|
|
28
|
+
import winipedia_utils
|
|
27
29
|
from winipedia_utils.logging.logger import get_logger
|
|
28
30
|
|
|
29
31
|
logger = get_logger(__name__)
|
|
@@ -44,7 +46,9 @@ def get_src_package() -> ModuleType:
|
|
|
44
46
|
if only the test package exists
|
|
45
47
|
|
|
46
48
|
"""
|
|
47
|
-
from winipedia_utils.testing.convention import
|
|
49
|
+
from winipedia_utils.testing.convention import ( # noqa: PLC0415 # avoid circular import
|
|
50
|
+
TESTS_PACKAGE_NAME,
|
|
51
|
+
)
|
|
48
52
|
|
|
49
53
|
packages = find_packages_as_modules(depth=0)
|
|
50
54
|
return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
|
|
@@ -180,8 +184,13 @@ def find_packages(
|
|
|
180
184
|
find_packages(depth=1) might return ["package1", "package2"]
|
|
181
185
|
|
|
182
186
|
"""
|
|
187
|
+
from winipedia_utils.git.gitignore.config import ( # noqa: PLC0415
|
|
188
|
+
GitIgnoreConfigFile, # avoid circular import
|
|
189
|
+
)
|
|
190
|
+
|
|
183
191
|
if exclude is None:
|
|
184
|
-
|
|
192
|
+
# must init GitIgnoreConfigFile to create .gitignore if it does not exist
|
|
193
|
+
exclude = GitIgnoreConfigFile.load()
|
|
185
194
|
exclude = [
|
|
186
195
|
p.replace("/", ".").removesuffix(".") for p in exclude if p.endswith("/")
|
|
187
196
|
]
|
|
@@ -280,7 +289,12 @@ def make_init_modules_for_package(path: str | Path | ModuleType) -> None:
|
|
|
280
289
|
from get_default_init_module_content.
|
|
281
290
|
|
|
282
291
|
"""
|
|
283
|
-
from winipedia_utils.
|
|
292
|
+
from winipedia_utils.git.gitignore.gitignore import ( # noqa: PLC0415
|
|
293
|
+
walk_os_skipping_gitignore_patterns, # avoid circular import
|
|
294
|
+
)
|
|
295
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415
|
|
296
|
+
to_path, # avoid circular import
|
|
297
|
+
)
|
|
284
298
|
|
|
285
299
|
path = to_path(path, is_package=True)
|
|
286
300
|
|
|
@@ -305,7 +319,10 @@ def make_init_module(path: str | Path) -> None:
|
|
|
305
319
|
Creates parent directories if they don't exist.
|
|
306
320
|
|
|
307
321
|
"""
|
|
308
|
-
from winipedia_utils.modules.module import
|
|
322
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
323
|
+
get_default_init_module_content,
|
|
324
|
+
to_path,
|
|
325
|
+
)
|
|
309
326
|
|
|
310
327
|
path = to_path(path, is_package=True)
|
|
311
328
|
|
|
@@ -338,7 +355,7 @@ def copy_package(
|
|
|
338
355
|
with_file_content (bool, optional): copies the content of the files.
|
|
339
356
|
|
|
340
357
|
"""
|
|
341
|
-
from winipedia_utils.modules.module import (
|
|
358
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
342
359
|
create_module,
|
|
343
360
|
get_isolated_obj_name,
|
|
344
361
|
get_module_content_as_str,
|
|
@@ -368,7 +385,9 @@ def get_main_package() -> ModuleType:
|
|
|
368
385
|
|
|
369
386
|
Even when this package is installed as a module.
|
|
370
387
|
"""
|
|
371
|
-
from winipedia_utils.modules.module import
|
|
388
|
+
from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
|
|
389
|
+
to_module_name,
|
|
390
|
+
)
|
|
372
391
|
|
|
373
392
|
main = sys.modules.get("__main__")
|
|
374
393
|
if main is None:
|
|
@@ -388,3 +407,90 @@ def get_main_package() -> ModuleType:
|
|
|
388
407
|
|
|
389
408
|
msg = "Not able to determine the main package"
|
|
390
409
|
raise ValueError(msg)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
|
|
413
|
+
"""A directed graph representing Python package dependencies."""
|
|
414
|
+
|
|
415
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
416
|
+
"""Initialize the dependency graph and build it immediately."""
|
|
417
|
+
super().__init__(*args, **kwargs)
|
|
418
|
+
self.build()
|
|
419
|
+
|
|
420
|
+
def build(self) -> None:
|
|
421
|
+
"""Build the graph from installed Python distributions."""
|
|
422
|
+
for dist in importlib.metadata.distributions():
|
|
423
|
+
name = self.parse_distname_from_metadata(dist)
|
|
424
|
+
self.add_node(name)
|
|
425
|
+
|
|
426
|
+
requires = dist.requires or []
|
|
427
|
+
for req in requires:
|
|
428
|
+
dep = self.parse_pkg_name_from_req(req)
|
|
429
|
+
if dep:
|
|
430
|
+
self.add_edge(name, dep) # package → dependency
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def parse_distname_from_metadata(dist: importlib.metadata.Distribution) -> str:
|
|
434
|
+
"""Extract the distribution name from its metadata."""
|
|
435
|
+
# replace - with _ to handle packages like winipedia-utils
|
|
436
|
+
name: str = dist.metadata["Name"]
|
|
437
|
+
return DependencyGraph.normalize_package_name(name)
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def normalize_package_name(name: str) -> str:
|
|
441
|
+
"""Normalize a package name."""
|
|
442
|
+
return name.lower().replace("-", "_").strip()
|
|
443
|
+
|
|
444
|
+
@staticmethod
|
|
445
|
+
def parse_pkg_name_from_req(req: str) -> str | None:
|
|
446
|
+
"""Extract the bare dependency name from a requirement string."""
|
|
447
|
+
# split on the first non alphanumeric character like >, <, =, etc.
|
|
448
|
+
# keep - and _ for names like winipedia-utils or winipedia_utils
|
|
449
|
+
dep = re.split(r"[^a-zA-Z0-9_-]", req.strip())[0].strip()
|
|
450
|
+
return DependencyGraph.normalize_package_name(dep) if dep else None
|
|
451
|
+
|
|
452
|
+
def get_all_depending_on(
|
|
453
|
+
self, package: ModuleType, *, include_self: bool = False
|
|
454
|
+
) -> set[ModuleType]:
|
|
455
|
+
"""Return all packages that directly or indirectly depend on the given package.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
package: The module whose dependents should be found.
|
|
459
|
+
include_self: Whether to include the package itself in the result.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
A set of imported module objects representing dependents.
|
|
463
|
+
"""
|
|
464
|
+
# replace - with _ to handle packages like winipedia-utils
|
|
465
|
+
target = package.__name__.lower()
|
|
466
|
+
if target not in self:
|
|
467
|
+
msg = f"Package '{target}' not found in dependency graph"
|
|
468
|
+
raise ValueError(msg)
|
|
469
|
+
|
|
470
|
+
dependents = nx.ancestors(self, target)
|
|
471
|
+
if include_self:
|
|
472
|
+
dependents.add(target)
|
|
473
|
+
|
|
474
|
+
return self.import_packages(dependents)
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def import_packages(names: set[str]) -> set[ModuleType]:
|
|
478
|
+
"""Attempt to import all module names that can be resolved."""
|
|
479
|
+
modules: set[ModuleType] = set()
|
|
480
|
+
for name in names:
|
|
481
|
+
spec = importlib.util.find_spec(name)
|
|
482
|
+
if spec is not None:
|
|
483
|
+
modules.add(importlib.import_module(name))
|
|
484
|
+
return modules
|
|
485
|
+
|
|
486
|
+
def get_all_depending_on_winipedia_utils(
|
|
487
|
+
self, *, include_winipedia_utils: bool = False
|
|
488
|
+
) -> set[ModuleType]:
|
|
489
|
+
"""Return all packages that directly or indirectly depend on winipedia_utils."""
|
|
490
|
+
if get_src_package() == winipedia_utils:
|
|
491
|
+
deps: set[ModuleType] = set()
|
|
492
|
+
else:
|
|
493
|
+
deps = self.get_all_depending_on(winipedia_utils, include_self=False)
|
|
494
|
+
if include_winipedia_utils:
|
|
495
|
+
deps.add(winipedia_utils)
|
|
496
|
+
return deps
|