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
|
@@ -9,26 +9,14 @@ through pytest's autouse mechanism.
|
|
|
9
9
|
from importlib import import_module
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
from winipedia_utils.git.gitignore.gitignore import _gitignore_is_correct
|
|
14
|
-
from winipedia_utils.git.pre_commit.config import (
|
|
15
|
-
_pre_commit_config_is_correct,
|
|
16
|
-
)
|
|
17
|
-
from winipedia_utils.git.workflows.publish import (
|
|
18
|
-
PUBLISH_WORKFLOW_PATH,
|
|
19
|
-
_publish_config_is_correct,
|
|
20
|
-
)
|
|
21
|
-
from winipedia_utils.git.workflows.release import _release_config_is_correct
|
|
22
|
-
from winipedia_utils.modules.module import to_path
|
|
12
|
+
import winipedia_utils
|
|
23
13
|
from winipedia_utils.modules.package import (
|
|
24
14
|
find_packages,
|
|
25
15
|
get_src_package,
|
|
26
16
|
walk_package,
|
|
27
17
|
)
|
|
28
18
|
from winipedia_utils.projects.poetry.config import (
|
|
29
|
-
|
|
30
|
-
get_dev_dependencies_from_pyproject_toml,
|
|
31
|
-
get_poetry_package_name,
|
|
19
|
+
PyprojectConfigFile,
|
|
32
20
|
)
|
|
33
21
|
from winipedia_utils.testing.assertions import assert_with_msg
|
|
34
22
|
from winipedia_utils.testing.convention import (
|
|
@@ -36,13 +24,11 @@ from winipedia_utils.testing.convention import (
|
|
|
36
24
|
make_test_obj_importpath_from_obj,
|
|
37
25
|
)
|
|
38
26
|
from winipedia_utils.testing.fixtures import autouse_session_fixture
|
|
39
|
-
from winipedia_utils.
|
|
40
|
-
_conftest_content_is_correct,
|
|
41
|
-
)
|
|
27
|
+
from winipedia_utils.text.config import ConfigFile
|
|
42
28
|
|
|
43
29
|
|
|
44
30
|
@autouse_session_fixture
|
|
45
|
-
def
|
|
31
|
+
def assert_dev_dependencies_config_is_correct() -> None:
|
|
46
32
|
"""Verify that the dev dependencies in consts.py are correct.
|
|
47
33
|
|
|
48
34
|
This fixture runs once per test session and checks that the dev dependencies
|
|
@@ -53,19 +39,22 @@ def _test_dev_dependencies_const_correct() -> None:
|
|
|
53
39
|
AssertionError: If the dev dependencies in consts.py are not correct
|
|
54
40
|
|
|
55
41
|
"""
|
|
56
|
-
if
|
|
42
|
+
if PyprojectConfigFile.get_package_name() != winipedia_utils.__name__:
|
|
57
43
|
# this const is only used in winipedia_utils
|
|
58
44
|
# to be able to install them with setup.py
|
|
59
45
|
return
|
|
60
|
-
actual_dev_dependencies =
|
|
46
|
+
actual_dev_dependencies = PyprojectConfigFile.get_dev_dependencies()
|
|
47
|
+
expected_dev_dependencies = PyprojectConfigFile.get_configs()["tool"]["poetry"][
|
|
48
|
+
"group"
|
|
49
|
+
]["dev"]["dependencies"].keys()
|
|
61
50
|
assert_with_msg(
|
|
62
|
-
set(actual_dev_dependencies) == set(
|
|
51
|
+
set(actual_dev_dependencies) == set(expected_dev_dependencies),
|
|
63
52
|
"Dev dependencies in consts.py are not correct",
|
|
64
53
|
)
|
|
65
54
|
|
|
66
55
|
|
|
67
56
|
@autouse_session_fixture
|
|
68
|
-
def
|
|
57
|
+
def assert_config_files_are_correct() -> None:
|
|
69
58
|
"""Verify that the dev dependencies are installed.
|
|
70
59
|
|
|
71
60
|
This fixture runs once per test session and checks that the dev dependencies
|
|
@@ -75,131 +64,12 @@ def _test_dev_dependencies_are_in_pyproject_toml() -> None:
|
|
|
75
64
|
ImportError: If a dev dependency is not installed
|
|
76
65
|
|
|
77
66
|
"""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
set(_DEV_DEPENDENCIES).issubset(set(dev_dependencies)),
|
|
81
|
-
"Dev dependencies in consts.py are not a subset of the ones in pyproject.toml",
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@autouse_session_fixture
|
|
86
|
-
def _test_conftest_exists_and_is_correct() -> None:
|
|
87
|
-
"""Verify that the conftest.py file exists and has the correct content.
|
|
88
|
-
|
|
89
|
-
This fixture runs once per test session and checks that the conftest.py file
|
|
90
|
-
exists in the tests directory and contains the correct pytest_plugins configuration.
|
|
91
|
-
|
|
92
|
-
Raises:
|
|
93
|
-
AssertionError: If the conftest.py file doesn't exist or has incorrect content
|
|
94
|
-
|
|
95
|
-
"""
|
|
96
|
-
conftest_path = Path(TESTS_PACKAGE_NAME, "conftest.py")
|
|
97
|
-
assert_with_msg(
|
|
98
|
-
conftest_path.is_file(),
|
|
99
|
-
f"Expected conftest.py file at {conftest_path} but it doesn't exist",
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
assert_with_msg(
|
|
103
|
-
_conftest_content_is_correct(conftest_path),
|
|
104
|
-
"conftest.py has incorrect content",
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@autouse_session_fixture
|
|
109
|
-
def _test_pyproject_toml_is_correct() -> None:
|
|
110
|
-
"""Verify that the pyproject.toml file exists and has the correct content.
|
|
111
|
-
|
|
112
|
-
This fixture runs once per test session and checks that the pyproject.toml file
|
|
113
|
-
exists in the root directory and contains the correct content.
|
|
114
|
-
|
|
115
|
-
Raises:
|
|
116
|
-
AssertionError: If the pyproject.toml file doesn't exist
|
|
117
|
-
or has incorrect content
|
|
118
|
-
|
|
119
|
-
"""
|
|
120
|
-
pyproject_toml_path = Path("pyproject.toml")
|
|
121
|
-
assert_with_msg(
|
|
122
|
-
pyproject_toml_path.is_file(),
|
|
123
|
-
f"Expected pyproject.toml file at {pyproject_toml_path} but it doesn't exist",
|
|
124
|
-
)
|
|
125
|
-
assert_with_msg(
|
|
126
|
-
_pyproject_tool_configs_are_correct(),
|
|
127
|
-
"pyproject.toml has incorrect content.",
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@autouse_session_fixture
|
|
132
|
-
def _test_pre_commit_config_yaml_is_correct() -> None:
|
|
133
|
-
"""Verify that the pre-commit yaml is correctly defining winipedia utils hook.
|
|
134
|
-
|
|
135
|
-
Checks that the yaml starts with the winipedia utils hook.
|
|
136
|
-
"""
|
|
137
|
-
pre_commit_config = Path(".pre-commit-config.yaml")
|
|
138
|
-
|
|
139
|
-
assert_with_msg(
|
|
140
|
-
pre_commit_config.is_file(),
|
|
141
|
-
f"Expected {pre_commit_config} to exist but it doesn't.",
|
|
142
|
-
)
|
|
143
|
-
assert_with_msg(
|
|
144
|
-
_pre_commit_config_is_correct(),
|
|
145
|
-
"Pre commit config is not correct.",
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@autouse_session_fixture
|
|
150
|
-
def _test_gitignore_is_correct() -> None:
|
|
151
|
-
"""Verify that the .gitignore file exists and has the correct content.
|
|
152
|
-
|
|
153
|
-
This fixture runs once per test session and checks that the .gitignore file
|
|
154
|
-
exists in the root directory and contains the correct content.
|
|
155
|
-
|
|
156
|
-
Raises:
|
|
157
|
-
AssertionError: If the .gitignore file doesn't exist
|
|
158
|
-
or has incorrect content
|
|
159
|
-
|
|
160
|
-
"""
|
|
161
|
-
gitignore_path = Path(".gitignore")
|
|
162
|
-
assert_with_msg(
|
|
163
|
-
gitignore_path.is_file(),
|
|
164
|
-
f"Expected {gitignore_path} to exist but it doesn't.",
|
|
165
|
-
)
|
|
166
|
-
assert_with_msg(
|
|
167
|
-
_gitignore_is_correct(),
|
|
168
|
-
"Gitignore is not correct.",
|
|
169
|
-
)
|
|
67
|
+
# subclasses of ConfigFile
|
|
68
|
+
ConfigFile.init_config_files()
|
|
170
69
|
|
|
171
70
|
|
|
172
71
|
@autouse_session_fixture
|
|
173
|
-
def
|
|
174
|
-
"""Verify that the publish workflow is correctly defined.
|
|
175
|
-
|
|
176
|
-
If the file does not exist, we skip this test bc not all projects necessarily
|
|
177
|
-
need to publish to pypi, e.g. they are binaries or private usage only or for profit.
|
|
178
|
-
"""
|
|
179
|
-
path = PUBLISH_WORKFLOW_PATH
|
|
180
|
-
# if folder exists but the file not then we skip this test
|
|
181
|
-
if path.parent.exists() and not path.exists():
|
|
182
|
-
return
|
|
183
|
-
assert_with_msg(
|
|
184
|
-
_publish_config_is_correct(),
|
|
185
|
-
"Publish workflow is not correct.",
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@autouse_session_fixture
|
|
190
|
-
def _test_release_workflow_is_correct() -> None:
|
|
191
|
-
"""Verify that the release workflow is correctly defined.
|
|
192
|
-
|
|
193
|
-
This workflow is mandatory for all projects.
|
|
194
|
-
"""
|
|
195
|
-
assert_with_msg(
|
|
196
|
-
_release_config_is_correct(),
|
|
197
|
-
"Release workflow is not correct.",
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@autouse_session_fixture
|
|
202
|
-
def _test_no_namespace_packages() -> None:
|
|
72
|
+
def assert_no_namespace_packages() -> None:
|
|
203
73
|
"""Verify that there are no namespace packages in the project.
|
|
204
74
|
|
|
205
75
|
This fixture runs once per test session and checks that all packages in the
|
|
@@ -221,7 +91,7 @@ def _test_no_namespace_packages() -> None:
|
|
|
221
91
|
|
|
222
92
|
|
|
223
93
|
@autouse_session_fixture
|
|
224
|
-
def
|
|
94
|
+
def assert_all_src_code_in_one_package() -> None:
|
|
225
95
|
"""Verify that all source code is in a single package.
|
|
226
96
|
|
|
227
97
|
This fixture runs once per test session and checks that there is only one
|
|
@@ -241,7 +111,7 @@ def _test_all_src_code_in_one_package() -> None:
|
|
|
241
111
|
|
|
242
112
|
|
|
243
113
|
@autouse_session_fixture
|
|
244
|
-
def
|
|
114
|
+
def assert_src_package_correctly_named() -> None:
|
|
245
115
|
"""Verify that the source package is correctly named.
|
|
246
116
|
|
|
247
117
|
This fixture runs once per test session and checks that the source package
|
|
@@ -252,34 +122,17 @@ def _test_src_package_correctly_named() -> None:
|
|
|
252
122
|
|
|
253
123
|
"""
|
|
254
124
|
src_package = get_src_package().__name__
|
|
125
|
+
config = PyprojectConfigFile()
|
|
126
|
+
expected_package = config.get_package_name()
|
|
255
127
|
assert_with_msg(
|
|
256
|
-
src_package ==
|
|
257
|
-
f"Expected source package to be named {
|
|
128
|
+
src_package == expected_package,
|
|
129
|
+
f"Expected source package to be named {expected_package}, "
|
|
258
130
|
f"but it is named {src_package}",
|
|
259
131
|
)
|
|
260
132
|
|
|
261
133
|
|
|
262
134
|
@autouse_session_fixture
|
|
263
|
-
def
|
|
264
|
-
"""Verify that the py.typed file exists in the source package.
|
|
265
|
-
|
|
266
|
-
This fixture runs once per test session and checks that the py.typed file
|
|
267
|
-
exists in the source package.
|
|
268
|
-
|
|
269
|
-
Raises:
|
|
270
|
-
AssertionError: If the py.typed file doesn't exist
|
|
271
|
-
|
|
272
|
-
"""
|
|
273
|
-
src_package = get_src_package()
|
|
274
|
-
py_typed_path = to_path(src_package.__name__, is_package=True) / "py.typed"
|
|
275
|
-
assert_with_msg(
|
|
276
|
-
py_typed_path.exists(),
|
|
277
|
-
f"Expected py.typed file to exist at {py_typed_path}",
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@autouse_session_fixture
|
|
282
|
-
def _test_project_structure_mirrored() -> None:
|
|
135
|
+
def assert_project_structure_mirrored() -> None:
|
|
283
136
|
"""Verify that the project structure is mirrored in tests.
|
|
284
137
|
|
|
285
138
|
This fixture runs once per test session and checks that for every package and
|
|
@@ -311,20 +164,18 @@ def _test_project_structure_mirrored() -> None:
|
|
|
311
164
|
|
|
312
165
|
|
|
313
166
|
@autouse_session_fixture
|
|
314
|
-
def
|
|
315
|
-
"""Verify that the
|
|
167
|
+
def assert_no_unit_test_package_usage() -> None:
|
|
168
|
+
"""Verify that the unit test package is not used in the project.
|
|
316
169
|
|
|
317
|
-
This fixture runs once per test session and checks that the
|
|
170
|
+
This fixture runs once per test session and checks that the unit test package
|
|
318
171
|
is not used in the project.
|
|
319
172
|
|
|
320
173
|
Raises:
|
|
321
|
-
AssertionError: If the
|
|
174
|
+
AssertionError: If the unit test package is used
|
|
322
175
|
|
|
323
176
|
"""
|
|
324
177
|
for path in Path().rglob("*.py"):
|
|
325
|
-
if path == to_path(__name__, is_package=False):
|
|
326
|
-
continue
|
|
327
178
|
assert_with_msg(
|
|
328
|
-
"
|
|
329
|
-
f"Found
|
|
179
|
+
"UnitTest".lower() not in path.read_text(encoding="utf-8"),
|
|
180
|
+
f"Found unit test package usage in {path}. Use pytest instead.",
|
|
330
181
|
)
|
|
@@ -10,11 +10,15 @@ Returns:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from collections.abc import Callable
|
|
13
|
-
from pathlib import Path
|
|
14
13
|
from types import ModuleType
|
|
15
14
|
from typing import Any
|
|
16
15
|
|
|
17
|
-
from winipedia_utils.
|
|
16
|
+
from winipedia_utils.logging.logger import get_logger
|
|
17
|
+
from winipedia_utils.modules.function import is_abstractmethod
|
|
18
|
+
from winipedia_utils.modules.module import (
|
|
19
|
+
get_objs_from_obj,
|
|
20
|
+
make_obj_importpath,
|
|
21
|
+
)
|
|
18
22
|
from winipedia_utils.testing.assertions import assert_with_msg
|
|
19
23
|
from winipedia_utils.testing.convention import (
|
|
20
24
|
get_obj_from_test_obj,
|
|
@@ -22,8 +26,10 @@ from winipedia_utils.testing.convention import (
|
|
|
22
26
|
make_untested_summary_error_msg,
|
|
23
27
|
)
|
|
24
28
|
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
25
31
|
|
|
26
|
-
def
|
|
32
|
+
def assert_no_untested_objs(
|
|
27
33
|
test_obj: ModuleType | type | Callable[..., Any],
|
|
28
34
|
) -> None:
|
|
29
35
|
"""Assert that all objects in the source have corresponding test objects.
|
|
@@ -42,7 +48,15 @@ def _assert_no_untested_objs(
|
|
|
42
48
|
test_objs = get_objs_from_obj(test_obj)
|
|
43
49
|
test_objs_paths = {make_obj_importpath(o) for o in test_objs}
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
try:
|
|
52
|
+
obj = get_obj_from_test_obj(test_obj)
|
|
53
|
+
except ImportError:
|
|
54
|
+
if isinstance(test_obj, ModuleType):
|
|
55
|
+
# we skip if module not found bc that means it has custom tests
|
|
56
|
+
# and is not part of the mirrored structure
|
|
57
|
+
logger.warning("No source module found for %s, skipping", test_obj)
|
|
58
|
+
return
|
|
59
|
+
raise
|
|
46
60
|
objs = get_objs_from_obj(obj)
|
|
47
61
|
supposed_test_objs_paths = {make_test_obj_importpath_from_obj(o) for o in objs}
|
|
48
62
|
|
|
@@ -51,61 +65,17 @@ def _assert_no_untested_objs(
|
|
|
51
65
|
assert_with_msg(not untested_objs, make_untested_summary_error_msg(untested_objs))
|
|
52
66
|
|
|
53
67
|
|
|
54
|
-
def
|
|
55
|
-
"""
|
|
56
|
-
return '''
|
|
57
|
-
"""Pytest configuration for tests.
|
|
58
|
-
|
|
59
|
-
This module configures pytest plugins for the test suite, setting up the necessary
|
|
60
|
-
fixtures and hooks for the different
|
|
61
|
-
test scopes (function, class, module, package, session).
|
|
62
|
-
It also import custom plugins from tests/base/scopes.
|
|
63
|
-
This file should not be modified manually.
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
pytest_plugins = ["winipedia_utils.testing.tests.conftest"]
|
|
67
|
-
'''.strip()
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _conftest_content_is_correct(conftest_path: Path) -> bool:
|
|
71
|
-
"""Check if the conftest.py file has the correct content.
|
|
68
|
+
def assert_isabstrct_method(method: Any) -> None:
|
|
69
|
+
"""Assert that a method is an abstract method.
|
|
72
70
|
|
|
73
71
|
Args:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
True if the conftest.py file exists and has the correct content, False otherwise
|
|
78
|
-
|
|
79
|
-
"""
|
|
80
|
-
if not conftest_path.exists():
|
|
81
|
-
return False
|
|
82
|
-
return conftest_path.read_text().startswith(_get_conftest_content())
|
|
83
|
-
|
|
72
|
+
method: The method to check
|
|
84
73
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return '''
|
|
88
|
-
"""Contains an empty test."""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_0() -> None:
|
|
92
|
-
"""Empty test.
|
|
93
|
-
|
|
94
|
-
Exists so that when no tests are written yet the base fixtures are executed.
|
|
95
|
-
"""
|
|
96
|
-
'''.strip()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _test_0_content_is_correct(test_0_path: Path) -> bool:
|
|
100
|
-
"""Check if the test_0.py file has the correct content.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
test_0_path: The path to the test_0.py file
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
True if the test_0.py file exists and has the correct content, False otherwise
|
|
74
|
+
Raises:
|
|
75
|
+
AssertionError: If the method is not an abstract method
|
|
107
76
|
|
|
108
77
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
78
|
+
assert_with_msg(
|
|
79
|
+
is_abstractmethod(method),
|
|
80
|
+
f"Expected {method} to be abstract method",
|
|
81
|
+
)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Base class for config files."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import tomlkit
|
|
9
|
+
import yaml
|
|
10
|
+
from dotenv import dotenv_values
|
|
11
|
+
|
|
12
|
+
from winipedia_utils.iterating.iterate import nested_structure_is_subset
|
|
13
|
+
from winipedia_utils.modules.class_ import init_all_nonabstract_subclasses
|
|
14
|
+
from winipedia_utils.modules.package import DependencyGraph, get_src_package
|
|
15
|
+
from winipedia_utils.projects.poetry.poetry import (
|
|
16
|
+
get_poetry_run_module_script,
|
|
17
|
+
)
|
|
18
|
+
from winipedia_utils.text.string import split_on_uppercase
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigFile(ABC):
|
|
22
|
+
"""Base class for config files."""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def get_parent_path(cls) -> Path:
|
|
27
|
+
"""Get the path to the config file."""
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
32
|
+
"""Load the config file."""
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
37
|
+
"""Dump the config file."""
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_file_extension(cls) -> str:
|
|
42
|
+
"""Get the file extension of the config file."""
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def get_configs(cls) -> dict[str, Any] | list[Any]:
|
|
47
|
+
"""Get the config."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
"""Initialize the config file."""
|
|
51
|
+
self.get_path().parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
if not self.get_path().exists():
|
|
53
|
+
self.get_path().touch()
|
|
54
|
+
self.dump(self.get_configs())
|
|
55
|
+
|
|
56
|
+
if not self.is_correct():
|
|
57
|
+
config = self.add_missing_configs()
|
|
58
|
+
self.dump(config)
|
|
59
|
+
|
|
60
|
+
if not self.is_correct():
|
|
61
|
+
msg = f"Config file {self.get_path()} is not correct."
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_path(cls) -> Path:
|
|
66
|
+
"""Get the path to the config file."""
|
|
67
|
+
return (
|
|
68
|
+
cls.get_parent_path() / f"{cls.get_filename()}.{cls.get_file_extension()}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get_filename(cls) -> str:
|
|
73
|
+
"""Get the filename of the config file."""
|
|
74
|
+
name = cls.__name__
|
|
75
|
+
abstract_parents = [
|
|
76
|
+
parent.__name__ for parent in cls.__mro__ if inspect.isabstract(parent)
|
|
77
|
+
]
|
|
78
|
+
for parent in abstract_parents:
|
|
79
|
+
name = name.removesuffix(parent)
|
|
80
|
+
return "_".join(split_on_uppercase(name)).lower()
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def add_missing_configs(cls) -> dict[str, Any] | list[Any]:
|
|
84
|
+
"""Add any missing configs to the config file."""
|
|
85
|
+
current_config = cls.load()
|
|
86
|
+
expected_config = cls.get_configs()
|
|
87
|
+
nested_structure_is_subset(
|
|
88
|
+
expected_config,
|
|
89
|
+
current_config,
|
|
90
|
+
cls.add_missing_dict_val,
|
|
91
|
+
cls.insert_missing_list_val,
|
|
92
|
+
)
|
|
93
|
+
return current_config
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def add_missing_dict_val(
|
|
97
|
+
expected_dict: dict[str, Any], actual_dict: dict[str, Any], key: str
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Add a missing dict value."""
|
|
100
|
+
actual_dict[key] = expected_dict[key]
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def insert_missing_list_val(
|
|
104
|
+
expected_list: list[Any], actual_list: list[Any], index: int
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Append a missing list value."""
|
|
107
|
+
actual_list.insert(index, expected_list[index])
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def is_correct(cls) -> bool:
|
|
111
|
+
"""Check if the config is correct.
|
|
112
|
+
|
|
113
|
+
If the file is empty, it is considered correct.
|
|
114
|
+
This is so bc if a user does not want a specific config file,
|
|
115
|
+
they can just make it empty and the tests will not fail.
|
|
116
|
+
"""
|
|
117
|
+
return cls.is_unwanted() or cls.is_correct_recursively(
|
|
118
|
+
cls.get_configs(), cls.load()
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def is_unwanted(cls) -> bool:
|
|
123
|
+
"""Check if the config file is unwanted.
|
|
124
|
+
|
|
125
|
+
If the file is empty, it is considered unwanted.
|
|
126
|
+
"""
|
|
127
|
+
return cls.get_path().exists() and cls.get_path().read_text() == ""
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def is_correct_recursively(
|
|
131
|
+
expected_config: dict[str, Any] | list[Any],
|
|
132
|
+
actual_config: dict[str, Any] | list[Any],
|
|
133
|
+
) -> bool:
|
|
134
|
+
"""Check if the config is correct.
|
|
135
|
+
|
|
136
|
+
Checks if expected is a subset recursively of actual.
|
|
137
|
+
If a value is Any, it is considered correct.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
expected_config: The expected config
|
|
141
|
+
actual_config: The actual config
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if the config is correct, False otherwise
|
|
145
|
+
"""
|
|
146
|
+
return nested_structure_is_subset(expected_config, actual_config)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def init_config_files(cls) -> None:
|
|
150
|
+
"""Initialize all subclasses."""
|
|
151
|
+
pkgs_depending_on_winipedia_utils = (
|
|
152
|
+
DependencyGraph().get_all_depending_on_winipedia_utils(
|
|
153
|
+
include_winipedia_utils=True
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
pkgs_depending_on_winipedia_utils.add(get_src_package())
|
|
157
|
+
for pkg in pkgs_depending_on_winipedia_utils:
|
|
158
|
+
init_all_nonabstract_subclasses(cls, load_package_before=pkg)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def get_poetry_run_setup_script() -> str:
|
|
162
|
+
"""Get the poetry run setup script."""
|
|
163
|
+
from winipedia_utils import setup # noqa: PLC0415 # avoid circular import
|
|
164
|
+
|
|
165
|
+
return get_poetry_run_module_script(setup)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class YamlConfigFile(ConfigFile):
|
|
169
|
+
"""Base class for yaml config files."""
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
173
|
+
"""Load the config file."""
|
|
174
|
+
return yaml.safe_load(cls.get_path().read_text()) or {}
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
178
|
+
"""Dump the config file."""
|
|
179
|
+
with cls.get_path().open("w") as f:
|
|
180
|
+
yaml.safe_dump(config, f, sort_keys=False)
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_file_extension(cls) -> str:
|
|
184
|
+
"""Get the file extension of the config file."""
|
|
185
|
+
return "yaml"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TomlConfigFile(ConfigFile):
|
|
189
|
+
"""Base class for toml config files."""
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def load(cls) -> dict[str, Any]:
|
|
193
|
+
"""Load the config file."""
|
|
194
|
+
return tomlkit.parse(cls.get_path().read_text())
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
198
|
+
"""Dump the config file."""
|
|
199
|
+
if not isinstance(config, dict):
|
|
200
|
+
msg = f"Cannot dump {config} to toml file."
|
|
201
|
+
raise TypeError(msg)
|
|
202
|
+
with cls.get_path().open("w") as f:
|
|
203
|
+
tomlkit.dump(config, f, sort_keys=False)
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def get_file_extension(cls) -> str:
|
|
207
|
+
"""Get the file extension of the config file."""
|
|
208
|
+
return "toml"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DotEnvConfigFile(ConfigFile):
|
|
212
|
+
"""config class for .env config files."""
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def load(cls) -> dict[str, str | None]:
|
|
216
|
+
"""Load the config file."""
|
|
217
|
+
return dotenv_values(cls.get_path())
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
221
|
+
"""Dump the config file."""
|
|
222
|
+
# is not supposed to be dumped to, so just raise error
|
|
223
|
+
if config:
|
|
224
|
+
msg = f"Cannot dump {config} to .env file."
|
|
225
|
+
raise ValueError(msg)
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def get_file_extension(cls) -> str:
|
|
229
|
+
"""Get the file extension of the config file."""
|
|
230
|
+
return "env"
|
|
231
|
+
|
|
232
|
+
@classmethod
|
|
233
|
+
def get_filename(cls) -> str:
|
|
234
|
+
"""Get the filename of the config file."""
|
|
235
|
+
return "" # so it builds the path .env and not env.env
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def get_parent_path(cls) -> Path:
|
|
239
|
+
"""Get the path to the config file."""
|
|
240
|
+
return Path()
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
244
|
+
"""Get the config."""
|
|
245
|
+
return {}
|
|
246
|
+
|
|
247
|
+
@classmethod
|
|
248
|
+
def is_correct(cls) -> bool:
|
|
249
|
+
"""Check if the config is correct."""
|
|
250
|
+
return super().is_correct() or cls.get_path().exists()
|
winipedia_utils/text/string.py
CHANGED
|
@@ -7,7 +7,10 @@ These utilities simplify common string manipulation tasks throughout the applica
|
|
|
7
7
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import textwrap
|
|
10
|
+
from collections.abc import Callable
|
|
10
11
|
from io import StringIO
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any
|
|
11
14
|
|
|
12
15
|
from defusedxml import ElementTree as DefusedElementTree
|
|
13
16
|
|
|
@@ -124,3 +127,30 @@ def split_on_uppercase(string: str) -> list[str]:
|
|
|
124
127
|
current_part += letter
|
|
125
128
|
parts.append(current_part)
|
|
126
129
|
return parts
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def make_name_from_obj(
|
|
133
|
+
package: ModuleType | Callable[..., Any] | type,
|
|
134
|
+
split_on: str = "_",
|
|
135
|
+
join_on: str = "-",
|
|
136
|
+
*,
|
|
137
|
+
capitalize: bool = True,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""Make a name from a package.
|
|
140
|
+
|
|
141
|
+
takes a package and makes a name from it that is readable by humans.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
package (ModuleType): The package to make a name from
|
|
145
|
+
split_on (str, optional): what to split the package name on. Defaults to "_".
|
|
146
|
+
join_on (str, optional): what to join the package name with. Defaults to "-".
|
|
147
|
+
capitalize (bool, optional): Whether to capitalize each part. Defaults to True.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
str: _description_
|
|
151
|
+
"""
|
|
152
|
+
package_name = package.__name__.split(".")[-1]
|
|
153
|
+
parts = package_name.split(split_on)
|
|
154
|
+
if capitalize:
|
|
155
|
+
parts = [part.capitalize() for part in parts]
|
|
156
|
+
return join_on.join(parts)
|