winipedia-utils 0.2.63__py3-none-any.whl → 0.3.9__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 (33) hide show
  1. winipedia_utils/concurrent/concurrent.py +7 -2
  2. winipedia_utils/concurrent/multiprocessing.py +1 -2
  3. winipedia_utils/concurrent/multithreading.py +2 -2
  4. winipedia_utils/git/gitignore/config.py +54 -0
  5. winipedia_utils/git/gitignore/gitignore.py +5 -63
  6. winipedia_utils/git/pre_commit/config.py +45 -58
  7. winipedia_utils/git/pre_commit/hooks.py +13 -13
  8. winipedia_utils/git/pre_commit/run_hooks.py +2 -2
  9. winipedia_utils/git/workflows/base/base.py +109 -52
  10. winipedia_utils/git/workflows/publish.py +27 -50
  11. winipedia_utils/git/workflows/release.py +22 -48
  12. winipedia_utils/iterating/iterate.py +59 -1
  13. winipedia_utils/modules/class_.py +60 -10
  14. winipedia_utils/modules/function.py +18 -1
  15. winipedia_utils/modules/package.py +16 -7
  16. winipedia_utils/projects/poetry/config.py +122 -110
  17. winipedia_utils/projects/poetry/poetry.py +1 -8
  18. winipedia_utils/projects/project.py +7 -13
  19. winipedia_utils/setup.py +11 -29
  20. winipedia_utils/testing/config.py +95 -0
  21. winipedia_utils/testing/create_tests.py +4 -18
  22. winipedia_utils/testing/skip.py +10 -0
  23. winipedia_utils/testing/tests/base/fixtures/scopes/class_.py +3 -3
  24. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +6 -4
  25. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +28 -176
  26. winipedia_utils/testing/tests/base/utils/utils.py +11 -55
  27. winipedia_utils/text/config.py +143 -0
  28. winipedia_utils-0.3.9.dist-info/METADATA +324 -0
  29. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.3.9.dist-info}/RECORD +31 -28
  30. winipedia_utils/consts.py +0 -21
  31. winipedia_utils-0.2.63.dist-info/METADATA +0 -738
  32. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.3.9.dist-info}/WHEEL +0 -0
  33. {winipedia_utils-0.2.63.dist-info → winipedia_utils-0.3.9.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
+ PyProjectTomlConfig,
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,23 @@ 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
+ config = PyProjectTomlConfig()
43
+ if config.get_package_name() != winipedia_utils.__name__:
57
44
  # this const is only used in winipedia_utils
58
45
  # to be able to install them with setup.py
59
46
  return
60
- actual_dev_dependencies = get_dev_dependencies_from_pyproject_toml()
47
+ actual_dev_dependencies = config.get_dev_dependencies()
48
+ expected_dev_dependencies = config.get_configs()["tool"]["poetry"]["group"]["dev"][
49
+ "dependencies"
50
+ ].keys()
61
51
  assert_with_msg(
62
- set(actual_dev_dependencies) == set(_DEV_DEPENDENCIES),
52
+ set(actual_dev_dependencies) == set(expected_dev_dependencies),
63
53
  "Dev dependencies in consts.py are not correct",
64
54
  )
65
55
 
66
56
 
67
57
  @autouse_session_fixture
68
- def _test_dev_dependencies_are_in_pyproject_toml() -> None:
58
+ def assert_config_files_are_correct() -> None:
69
59
  """Verify that the dev dependencies are installed.
70
60
 
71
61
  This fixture runs once per test session and checks that the dev dependencies
@@ -75,131 +65,12 @@ def _test_dev_dependencies_are_in_pyproject_toml() -> None:
75
65
  ImportError: If a dev dependency is not installed
76
66
 
77
67
  """
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
- )
68
+ # subclasses of ConfigFile
69
+ ConfigFile.init_config_files()
170
70
 
171
71
 
172
72
  @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:
73
+ def assert_no_namespace_packages() -> None:
203
74
  """Verify that there are no namespace packages in the project.
204
75
 
205
76
  This fixture runs once per test session and checks that all packages in the
@@ -221,7 +92,7 @@ def _test_no_namespace_packages() -> None:
221
92
 
222
93
 
223
94
  @autouse_session_fixture
224
- def _test_all_src_code_in_one_package() -> None:
95
+ def assert_all_src_code_in_one_package() -> None:
225
96
  """Verify that all source code is in a single package.
226
97
 
227
98
  This fixture runs once per test session and checks that there is only one
@@ -241,7 +112,7 @@ def _test_all_src_code_in_one_package() -> None:
241
112
 
242
113
 
243
114
  @autouse_session_fixture
244
- def _test_src_package_correctly_named() -> None:
115
+ def assert_src_package_correctly_named() -> None:
245
116
  """Verify that the source package is correctly named.
246
117
 
247
118
  This fixture runs once per test session and checks that the source package
@@ -252,34 +123,17 @@ def _test_src_package_correctly_named() -> None:
252
123
 
253
124
  """
254
125
  src_package = get_src_package().__name__
126
+ config = PyProjectTomlConfig()
127
+ expected_package = config.get_package_name()
255
128
  assert_with_msg(
256
- src_package == get_poetry_package_name(),
257
- f"Expected source package to be named {get_poetry_package_name()}, "
129
+ src_package == expected_package,
130
+ f"Expected source package to be named {expected_package}, "
258
131
  f"but it is named {src_package}",
259
132
  )
260
133
 
261
134
 
262
135
  @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:
136
+ def assert_project_structure_mirrored() -> None:
283
137
  """Verify that the project structure is mirrored in tests.
284
138
 
285
139
  This fixture runs once per test session and checks that for every package and
@@ -311,20 +165,18 @@ def _test_project_structure_mirrored() -> None:
311
165
 
312
166
 
313
167
  @autouse_session_fixture
314
- def _test_no_unitest_package_usage() -> None:
315
- """Verify that the unittest package is not used in the project.
168
+ def assert_no_unit_test_package_usage() -> None:
169
+ """Verify that the unit test package is not used in the project.
316
170
 
317
- This fixture runs once per test session and checks that the unittest package
171
+ This fixture runs once per test session and checks that the unit test package
318
172
  is not used in the project.
319
173
 
320
174
  Raises:
321
- AssertionError: If the unittest package is used
175
+ AssertionError: If the unit test package is used
322
176
 
323
177
  """
324
178
  for path in Path().rglob("*.py"):
325
- if path == to_path(__name__, is_package=False):
326
- continue
327
179
  assert_with_msg(
328
- "unittest" not in path.read_text(encoding="utf-8"),
329
- f"Found unittest usage in {path}. Use pytest instead.",
180
+ "UnitTest".lower() not in path.read_text(encoding="utf-8"),
181
+ f"Found unit test package usage in {path}. Use pytest instead.",
330
182
  )
@@ -10,10 +10,10 @@ 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
 
16
+ from winipedia_utils.modules.function import is_abstractmethod
17
17
  from winipedia_utils.modules.module import get_objs_from_obj, make_obj_importpath
18
18
  from winipedia_utils.testing.assertions import assert_with_msg
19
19
  from winipedia_utils.testing.convention import (
@@ -23,7 +23,7 @@ from winipedia_utils.testing.convention import (
23
23
  )
24
24
 
25
25
 
26
- def _assert_no_untested_objs(
26
+ def assert_no_untested_objs(
27
27
  test_obj: ModuleType | type | Callable[..., Any],
28
28
  ) -> None:
29
29
  """Assert that all objects in the source have corresponding test objects.
@@ -51,61 +51,17 @@ def _assert_no_untested_objs(
51
51
  assert_with_msg(not untested_objs, make_untested_summary_error_msg(untested_objs))
52
52
 
53
53
 
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.
54
+ def assert_isabstrct_method(method: Any) -> None:
55
+ """Assert that a method is an abstract method.
72
56
 
73
57
  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
-
84
-
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."""
58
+ method: The method to check
89
59
 
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
60
+ Raises:
61
+ AssertionError: If the method is not an abstract method
107
62
 
108
63
  """
109
- if not test_0_path.exists():
110
- return False
111
- return test_0_path.read_text().startswith(_get_test_0_content())
64
+ assert_with_msg(
65
+ is_abstractmethod(method),
66
+ f"Expected {method} to be abstract method",
67
+ )
@@ -0,0 +1,143 @@
1
+ """Base class for config files."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import tomlkit
8
+ import yaml
9
+
10
+ import winipedia_utils
11
+ from winipedia_utils.iterating.iterate import nested_structure_is_subset
12
+ from winipedia_utils.modules.class_ import init_all_nonabstract_subclasses
13
+
14
+
15
+ class ConfigFile(ABC):
16
+ """Base class for config files."""
17
+
18
+ @abstractmethod
19
+ def get_path(self) -> Path:
20
+ """Get the path to the config file."""
21
+
22
+ @abstractmethod
23
+ def load(self) -> dict[str, Any]:
24
+ """Load the config file."""
25
+
26
+ @abstractmethod
27
+ def dump(self, config: dict[str, Any]) -> None:
28
+ """Dump the config file."""
29
+
30
+ @abstractmethod
31
+ def get_configs(self) -> dict[str, Any]:
32
+ """Get the config."""
33
+
34
+ def __init__(self) -> None:
35
+ """Initialize the config file."""
36
+ self.path = self.get_path()
37
+ self.path.parent.mkdir(parents=True, exist_ok=True)
38
+ if not self.path.exists():
39
+ self.path.touch()
40
+ self.dump(self.get_configs())
41
+
42
+ if not self.is_correct():
43
+ config = self.add_missing_configs()
44
+ self.dump(config)
45
+
46
+ self.config = self.load()
47
+ if not self.is_correct():
48
+ msg = f"Config file {self.path} is not correct."
49
+ raise ValueError(msg)
50
+
51
+ def add_missing_configs(self) -> dict[str, Any]:
52
+ """Add any missing configs to the config file."""
53
+ current_config = self.load()
54
+ expected_config = self.get_configs()
55
+ nested_structure_is_subset(
56
+ expected_config,
57
+ current_config,
58
+ self.add_missing_dict_val,
59
+ self.insert_missing_list_val,
60
+ )
61
+ return current_config
62
+
63
+ @staticmethod
64
+ def add_missing_dict_val(
65
+ expected_dict: dict[str, Any], actual_dict: dict[str, Any], key: str
66
+ ) -> None:
67
+ """Add a missing dict value."""
68
+ actual_dict[key] = expected_dict[key]
69
+
70
+ @staticmethod
71
+ def insert_missing_list_val(
72
+ expected_list: list[Any], actual_list: list[Any], index: int
73
+ ) -> None:
74
+ """Append a missing list value."""
75
+ actual_list.insert(index, expected_list[index])
76
+
77
+ def is_correct(self) -> bool:
78
+ """Check if the config is correct.
79
+
80
+ If the file is empty, it is considered correct.
81
+ This is so bc if a user does not want a specific config file,
82
+ they can just make it empty and the tests will not fail.
83
+ """
84
+ return self.is_unwanted() or self.is_correct_recursively(
85
+ self.get_configs(), self.load()
86
+ )
87
+
88
+ def is_unwanted(self) -> bool:
89
+ """Check if the config file is unwanted.
90
+
91
+ If the file is empty, it is considered unwanted.
92
+ """
93
+ return self.path.exists() and self.path.read_text() == ""
94
+
95
+ @staticmethod
96
+ def is_correct_recursively(
97
+ expected_config: Any,
98
+ actual_config: Any,
99
+ ) -> bool:
100
+ """Check if the config is correct.
101
+
102
+ Checks if expected is a subset recursively of actual.
103
+ If a value is Any, it is considered correct.
104
+
105
+ Args:
106
+ expected_config: The expected config
107
+ actual_config: The actual config
108
+
109
+ Returns:
110
+ True if the config is correct, False otherwise
111
+ """
112
+ return nested_structure_is_subset(expected_config, actual_config)
113
+
114
+ @classmethod
115
+ def init_config_files(cls) -> None:
116
+ """Initialize all subclasses."""
117
+ init_all_nonabstract_subclasses(cls, load_package_before=winipedia_utils)
118
+
119
+
120
+ class YamlConfigFile(ConfigFile):
121
+ """Base class for yaml config files."""
122
+
123
+ def load(self) -> dict[str, Any]:
124
+ """Load the config file."""
125
+ return yaml.safe_load(self.path.read_text()) or {}
126
+
127
+ def dump(self, config: dict[str, Any]) -> None:
128
+ """Dump the config file."""
129
+ with self.path.open("w") as f:
130
+ yaml.safe_dump(config, f, sort_keys=False)
131
+
132
+
133
+ class TomlConfigFile(ConfigFile):
134
+ """Base class for toml config files."""
135
+
136
+ def load(self) -> dict[str, Any]:
137
+ """Load the config file."""
138
+ return tomlkit.parse(self.path.read_text())
139
+
140
+ def dump(self, config: dict[str, Any]) -> None:
141
+ """Dump the config file."""
142
+ with self.path.open("w") as f:
143
+ tomlkit.dump(config, f)