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.

Files changed (51) hide show
  1. winipedia_utils/artifacts/build.py +78 -0
  2. winipedia_utils/concurrent/concurrent.py +7 -2
  3. winipedia_utils/concurrent/multiprocessing.py +1 -2
  4. winipedia_utils/concurrent/multithreading.py +2 -2
  5. winipedia_utils/data/dataframe/cleaning.py +337 -100
  6. winipedia_utils/git/github/__init__.py +1 -0
  7. winipedia_utils/git/github/github.py +31 -0
  8. winipedia_utils/git/github/repo/__init__.py +1 -0
  9. winipedia_utils/git/github/repo/protect.py +103 -0
  10. winipedia_utils/git/github/repo/repo.py +205 -0
  11. winipedia_utils/git/github/workflows/base/__init__.py +1 -0
  12. winipedia_utils/git/github/workflows/base/base.py +889 -0
  13. winipedia_utils/git/github/workflows/health_check.py +69 -0
  14. winipedia_utils/git/github/workflows/publish.py +51 -0
  15. winipedia_utils/git/github/workflows/release.py +90 -0
  16. winipedia_utils/git/gitignore/config.py +77 -0
  17. winipedia_utils/git/gitignore/gitignore.py +5 -63
  18. winipedia_utils/git/pre_commit/config.py +49 -59
  19. winipedia_utils/git/pre_commit/hooks.py +46 -46
  20. winipedia_utils/git/pre_commit/run_hooks.py +19 -12
  21. winipedia_utils/iterating/iterate.py +63 -1
  22. winipedia_utils/modules/class_.py +69 -12
  23. winipedia_utils/modules/function.py +26 -3
  24. winipedia_utils/modules/inspection.py +56 -0
  25. winipedia_utils/modules/module.py +22 -28
  26. winipedia_utils/modules/package.py +116 -10
  27. winipedia_utils/projects/poetry/config.py +255 -112
  28. winipedia_utils/projects/poetry/poetry.py +230 -13
  29. winipedia_utils/projects/project.py +11 -42
  30. winipedia_utils/setup.py +11 -29
  31. winipedia_utils/testing/config.py +127 -0
  32. winipedia_utils/testing/create_tests.py +5 -19
  33. winipedia_utils/testing/skip.py +19 -0
  34. winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
  35. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +3 -3
  36. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +9 -6
  37. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +27 -176
  38. winipedia_utils/testing/tests/base/utils/utils.py +27 -57
  39. winipedia_utils/text/config.py +250 -0
  40. winipedia_utils/text/string.py +30 -0
  41. winipedia_utils-0.6.6.dist-info/METADATA +390 -0
  42. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/RECORD +46 -34
  43. winipedia_utils/consts.py +0 -21
  44. winipedia_utils/git/workflows/base/base.py +0 -77
  45. winipedia_utils/git/workflows/publish.py +0 -79
  46. winipedia_utils/git/workflows/release.py +0 -91
  47. winipedia_utils-0.2.63.dist-info/METADATA +0 -738
  48. /winipedia_utils/{git/workflows/base → artifacts}/__init__.py +0 -0
  49. /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
  50. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.6.6.dist-info}/WHEEL +0 -0
  51. {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
- from winipedia_utils.consts import _DEV_DEPENDENCIES
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
- _pyproject_tool_configs_are_correct,
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.testing.tests.base.utils.utils import (
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 _test_dev_dependencies_const_correct() -> None:
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 get_poetry_package_name() != "winipedia_utils":
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 = get_dev_dependencies_from_pyproject_toml()
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(_DEV_DEPENDENCIES),
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 _test_dev_dependencies_are_in_pyproject_toml() -> None:
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
- dev_dependencies = get_dev_dependencies_from_pyproject_toml()
79
- assert_with_msg(
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 _test_publish_workflow_is_correct() -> None:
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 _test_all_src_code_in_one_package() -> None:
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 _test_src_package_correctly_named() -> None:
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 == get_poetry_package_name(),
257
- f"Expected source package to be named {get_poetry_package_name()}, "
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 _test_py_typed_exists() -> None:
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 _test_no_unitest_package_usage() -> None:
315
- """Verify that the unittest package is not used in the project.
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 unittest package
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 unittest package is used
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
- "unittest" not in path.read_text(encoding="utf-8"),
329
- f"Found unittest usage in {path}. Use pytest instead.",
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.modules.module import get_objs_from_obj, make_obj_importpath
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 _assert_no_untested_objs(
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
- obj = get_obj_from_test_obj(test_obj)
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 _get_conftest_content() -> str:
55
- """Get the content for a conftest.py file when using winipedia_utils."""
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
- conftest_path: The path to the conftest.py file
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
- def _get_test_0_content() -> str:
86
- """Get the content for a test_0.py file when using winipedia_utils."""
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
- if not test_0_path.exists():
110
- return False
111
- return test_0_path.read_text().startswith(_get_test_0_content())
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()
@@ -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)