winipedia-utils 0.3.43__py3-none-any.whl → 0.4.18__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/git/github/repo/__init__.py +1 -0
- winipedia_utils/git/github/repo/protect.py +104 -0
- winipedia_utils/git/github/repo/repo.py +205 -0
- winipedia_utils/git/github/workflows/base/__init__.py +1 -0
- winipedia_utils/git/{workflows → github/workflows}/base/base.py +118 -54
- winipedia_utils/git/github/workflows/health_check.py +57 -0
- winipedia_utils/git/{workflows → github/workflows}/publish.py +11 -8
- winipedia_utils/git/github/workflows/release.py +45 -0
- winipedia_utils/git/gitignore/config.py +49 -29
- winipedia_utils/git/gitignore/gitignore.py +1 -1
- winipedia_utils/git/pre_commit/config.py +18 -13
- winipedia_utils/git/pre_commit/hooks.py +22 -4
- winipedia_utils/git/pre_commit/run_hooks.py +2 -1
- winipedia_utils/iterating/iterate.py +3 -4
- winipedia_utils/modules/module.py +2 -0
- winipedia_utils/modules/package.py +2 -1
- winipedia_utils/projects/poetry/config.py +74 -36
- winipedia_utils/projects/project.py +2 -2
- winipedia_utils/setup.py +2 -0
- winipedia_utils/testing/config.py +83 -29
- winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
- winipedia_utils/testing/tests/base/fixtures/scopes/module.py +6 -5
- winipedia_utils/testing/tests/base/fixtures/scopes/session.py +7 -8
- winipedia_utils/testing/tests/base/utils/utils.py +43 -2
- winipedia_utils/text/config.py +84 -37
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.dist-info}/METADATA +23 -8
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.dist-info}/RECORD +31 -27
- winipedia_utils/git/workflows/health_check.py +0 -51
- winipedia_utils/git/workflows/release.py +0 -33
- /winipedia_utils/git/{workflows/base → github}/__init__.py +0 -0
- /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.dist-info}/WHEEL +0 -0
- {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.18.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,34 +13,65 @@ class PythonConfigFile(ConfigFile):
|
|
|
13
13
|
|
|
14
14
|
CONTENT_KEY = "content"
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
@classmethod
|
|
17
|
+
def load(cls) -> dict[str, str]:
|
|
17
18
|
"""Load the config file."""
|
|
18
|
-
return {
|
|
19
|
+
return {cls.CONTENT_KEY: cls.get_path().read_text()}
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
@classmethod
|
|
22
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
21
23
|
"""Dump the config file."""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if not isinstance(config, dict):
|
|
25
|
+
msg = f"Cannot dump {config} to python file."
|
|
26
|
+
raise TypeError(msg)
|
|
27
|
+
cls.get_path().write_text(config[cls.CONTENT_KEY])
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def get_file_extension(cls) -> str:
|
|
31
|
+
"""Get the file extension of the config file."""
|
|
32
|
+
return "py"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
25
36
|
"""Get the config."""
|
|
26
|
-
return {
|
|
37
|
+
return {cls.CONTENT_KEY: cls.get_content_str()}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_file_content(cls) -> str:
|
|
41
|
+
"""Get the file content."""
|
|
42
|
+
return cls.load()[cls.CONTENT_KEY]
|
|
27
43
|
|
|
44
|
+
@classmethod
|
|
28
45
|
@abstractmethod
|
|
29
|
-
def
|
|
46
|
+
def get_content_str(cls) -> str:
|
|
30
47
|
"""Get the content."""
|
|
31
|
-
return self.load()[self.CONTENT_KEY]
|
|
32
48
|
|
|
49
|
+
@classmethod
|
|
50
|
+
def is_correct(cls) -> bool:
|
|
51
|
+
"""Check if the config is correct.
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
Python files are correct if they exist and contain the correct content.
|
|
54
|
+
"""
|
|
55
|
+
return (
|
|
56
|
+
super().is_correct()
|
|
57
|
+
or cls.get_content_str().strip() in cls.load()[cls.CONTENT_KEY]
|
|
58
|
+
)
|
|
36
59
|
|
|
37
|
-
PATH = Path(f"{TESTS_PACKAGE_NAME}/conftest.py")
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
class PythonTestsConfigFile(PythonConfigFile):
|
|
62
|
+
"""Base class for python config files in the tests directory."""
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_parent_path(cls) -> Path:
|
|
40
66
|
"""Get the path to the config file."""
|
|
41
|
-
return
|
|
67
|
+
return Path(TESTS_PACKAGE_NAME)
|
|
68
|
+
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
class ConftestConfigFile(PythonTestsConfigFile):
|
|
71
|
+
"""Config file for conftest.py."""
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def get_content_str(cls) -> str:
|
|
44
75
|
"""Get the config content."""
|
|
45
76
|
return '''"""Pytest configuration for tests.
|
|
46
77
|
|
|
@@ -55,21 +86,22 @@ pytest_plugins = ["winipedia_utils.testing.tests.conftest"]
|
|
|
55
86
|
'''
|
|
56
87
|
|
|
57
88
|
|
|
58
|
-
class ZeroTestConfigFile(
|
|
59
|
-
"""Config file for
|
|
60
|
-
|
|
61
|
-
PATH = Path(f"{TESTS_PACKAGE_NAME}/test_0.py")
|
|
89
|
+
class ZeroTestConfigFile(PythonTestsConfigFile):
|
|
90
|
+
"""Config file for test_zero.py."""
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
92
|
+
@classmethod
|
|
93
|
+
def get_filename(cls) -> str:
|
|
94
|
+
"""Get the filename of the config file."""
|
|
95
|
+
filename = super().get_filename()
|
|
96
|
+
return "_".join(reversed(filename.split("_")))
|
|
66
97
|
|
|
67
|
-
|
|
98
|
+
@classmethod
|
|
99
|
+
def get_content_str(cls) -> str:
|
|
68
100
|
"""Get the config."""
|
|
69
101
|
return '''"""Contains an empty test."""
|
|
70
102
|
|
|
71
103
|
|
|
72
|
-
def
|
|
104
|
+
def test_zero() -> None:
|
|
73
105
|
"""Empty test.
|
|
74
106
|
|
|
75
107
|
Exists so that when no tests are written yet the base fixtures are executed.
|
|
@@ -83,13 +115,35 @@ class ExperimentConfigFile(PythonConfigFile):
|
|
|
83
115
|
Is at root level and in .gitignore for experimentation.
|
|
84
116
|
"""
|
|
85
117
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def get_path(self) -> Path:
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_parent_path(cls) -> Path:
|
|
89
120
|
"""Get the path to the config file."""
|
|
90
|
-
return
|
|
121
|
+
return Path()
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
@classmethod
|
|
124
|
+
def get_content_str(cls) -> str:
|
|
93
125
|
"""Get the config."""
|
|
94
126
|
return '''"""This file is for experimentation and is ignored by git."""
|
|
95
127
|
'''
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class LocalSecretsConfigFile(PythonConfigFile):
|
|
131
|
+
"""Config file for secrets.py.
|
|
132
|
+
|
|
133
|
+
Config file for secrets. Is added to .gitignore automatically.
|
|
134
|
+
Should be in .gitignore.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_parent_path(cls) -> Path:
|
|
139
|
+
"""Get the path to the config file."""
|
|
140
|
+
return Path()
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def get_content_str(cls) -> str:
|
|
144
|
+
"""Get the config."""
|
|
145
|
+
return '''"""This file is for secrets you might use and is ignored by git.
|
|
146
|
+
|
|
147
|
+
Can be used by tests or other developing code.
|
|
148
|
+
"""
|
|
149
|
+
'''
|
|
@@ -4,3 +4,39 @@ This module provides custom fixtures for pytest that can be used to
|
|
|
4
4
|
automate common testing tasks and provide consistent setup and teardown
|
|
5
5
|
for tests.
|
|
6
6
|
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from winipedia_utils.text.config import ConfigFile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def config_file_factory[T: ConfigFile](
|
|
18
|
+
tmp_path: Path,
|
|
19
|
+
) -> Callable[[type[T]], type[T]]:
|
|
20
|
+
"""Factory fixture for creating config file classes with tmp_path.
|
|
21
|
+
|
|
22
|
+
This factory wraps any ConfigFile subclass to use tmp_path for get_path().
|
|
23
|
+
Define tmp_path once here, then all test config classes inherit it.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def _make_test_config(
|
|
27
|
+
base_class: type[T],
|
|
28
|
+
) -> type[T]:
|
|
29
|
+
"""Create a test config class that uses tmp_path."""
|
|
30
|
+
|
|
31
|
+
class TestConfigFile(base_class): # type: ignore [misc, valid-type]
|
|
32
|
+
"""Test config file with tmp_path override."""
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def get_path(cls) -> Path:
|
|
36
|
+
"""Get the path to the config file in tmp_path."""
|
|
37
|
+
path = super().get_path()
|
|
38
|
+
return Path(tmp_path / path)
|
|
39
|
+
|
|
40
|
+
return TestConfigFile
|
|
41
|
+
|
|
42
|
+
return _make_test_config
|
|
@@ -6,13 +6,16 @@ These fixtures are automatically applied to all test modules through pytest's au
|
|
|
6
6
|
mechanism.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
9
11
|
import pytest
|
|
10
12
|
|
|
11
|
-
from winipedia_utils.modules.module import to_module_name
|
|
12
|
-
from winipedia_utils.testing.config import ZeroTestConfigFile
|
|
13
13
|
from winipedia_utils.testing.fixtures import autouse_module_fixture
|
|
14
14
|
from winipedia_utils.testing.tests.base.utils.utils import assert_no_untested_objs
|
|
15
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
@autouse_module_fixture
|
|
18
21
|
def assert_all_funcs_and_classes_tested(request: pytest.FixtureRequest) -> None:
|
|
@@ -29,7 +32,5 @@ def assert_all_funcs_and_classes_tested(request: pytest.FixtureRequest) -> None:
|
|
|
29
32
|
AssertionError: If any function or class in the source module lacks a test
|
|
30
33
|
|
|
31
34
|
"""
|
|
32
|
-
module = request.module
|
|
33
|
-
if module.__name__ == to_module_name(ZeroTestConfigFile().get_path()):
|
|
34
|
-
return
|
|
35
|
+
module: ModuleType = request.module
|
|
35
36
|
assert_no_untested_objs(module)
|
|
@@ -16,7 +16,7 @@ from winipedia_utils.modules.package import (
|
|
|
16
16
|
walk_package,
|
|
17
17
|
)
|
|
18
18
|
from winipedia_utils.projects.poetry.config import (
|
|
19
|
-
|
|
19
|
+
PyprojectConfigFile,
|
|
20
20
|
)
|
|
21
21
|
from winipedia_utils.testing.assertions import assert_with_msg
|
|
22
22
|
from winipedia_utils.testing.convention import (
|
|
@@ -39,15 +39,14 @@ def assert_dev_dependencies_config_is_correct() -> None:
|
|
|
39
39
|
AssertionError: If the dev dependencies in consts.py are not correct
|
|
40
40
|
|
|
41
41
|
"""
|
|
42
|
-
|
|
43
|
-
if config.get_package_name() != winipedia_utils.__name__:
|
|
42
|
+
if PyprojectConfigFile.get_package_name() != winipedia_utils.__name__:
|
|
44
43
|
# this const is only used in winipedia_utils
|
|
45
44
|
# to be able to install them with setup.py
|
|
46
45
|
return
|
|
47
|
-
actual_dev_dependencies =
|
|
48
|
-
expected_dev_dependencies =
|
|
49
|
-
"
|
|
50
|
-
].keys()
|
|
46
|
+
actual_dev_dependencies = PyprojectConfigFile.get_dev_dependencies()
|
|
47
|
+
expected_dev_dependencies = PyprojectConfigFile.get_configs()["tool"]["poetry"][
|
|
48
|
+
"group"
|
|
49
|
+
]["dev"]["dependencies"].keys()
|
|
51
50
|
assert_with_msg(
|
|
52
51
|
set(actual_dev_dependencies) == set(expected_dev_dependencies),
|
|
53
52
|
"Dev dependencies in consts.py are not correct",
|
|
@@ -123,7 +122,7 @@ def assert_src_package_correctly_named() -> None:
|
|
|
123
122
|
|
|
124
123
|
"""
|
|
125
124
|
src_package = get_src_package().__name__
|
|
126
|
-
config =
|
|
125
|
+
config = PyprojectConfigFile()
|
|
127
126
|
expected_package = config.get_package_name()
|
|
128
127
|
assert_with_msg(
|
|
129
128
|
src_package == expected_package,
|
|
@@ -9,19 +9,29 @@ Returns:
|
|
|
9
9
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import os
|
|
12
13
|
from collections.abc import Callable
|
|
13
14
|
from types import ModuleType
|
|
14
15
|
from typing import Any
|
|
15
16
|
|
|
17
|
+
from winipedia_utils.logging.logger import get_logger
|
|
16
18
|
from winipedia_utils.modules.function import is_abstractmethod
|
|
17
|
-
from winipedia_utils.modules.module import
|
|
19
|
+
from winipedia_utils.modules.module import (
|
|
20
|
+
get_objs_from_obj,
|
|
21
|
+
import_obj_from_importpath,
|
|
22
|
+
make_obj_importpath,
|
|
23
|
+
to_module_name,
|
|
24
|
+
)
|
|
18
25
|
from winipedia_utils.testing.assertions import assert_with_msg
|
|
26
|
+
from winipedia_utils.testing.config import LocalSecretsConfigFile
|
|
19
27
|
from winipedia_utils.testing.convention import (
|
|
20
28
|
get_obj_from_test_obj,
|
|
21
29
|
make_test_obj_importpath_from_obj,
|
|
22
30
|
make_untested_summary_error_msg,
|
|
23
31
|
)
|
|
24
32
|
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
25
35
|
|
|
26
36
|
def assert_no_untested_objs(
|
|
27
37
|
test_obj: ModuleType | type | Callable[..., Any],
|
|
@@ -42,7 +52,15 @@ def assert_no_untested_objs(
|
|
|
42
52
|
test_objs = get_objs_from_obj(test_obj)
|
|
43
53
|
test_objs_paths = {make_obj_importpath(o) for o in test_objs}
|
|
44
54
|
|
|
45
|
-
|
|
55
|
+
try:
|
|
56
|
+
obj = get_obj_from_test_obj(test_obj)
|
|
57
|
+
except ImportError:
|
|
58
|
+
if isinstance(test_obj, ModuleType):
|
|
59
|
+
# we skip if module not found bc that means it has custom tests
|
|
60
|
+
# and is not part of the mirrored structure
|
|
61
|
+
logger.warning("No source module found for %s, skipping", test_obj)
|
|
62
|
+
return
|
|
63
|
+
raise
|
|
46
64
|
objs = get_objs_from_obj(obj)
|
|
47
65
|
supposed_test_objs_paths = {make_test_obj_importpath_from_obj(o) for o in objs}
|
|
48
66
|
|
|
@@ -65,3 +83,26 @@ def assert_isabstrct_method(method: Any) -> None:
|
|
|
65
83
|
is_abstractmethod(method),
|
|
66
84
|
f"Expected {method} to be abstract method",
|
|
67
85
|
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_github_repo_token() -> str:
|
|
89
|
+
"""Get the GitHub token."""
|
|
90
|
+
# try os env first
|
|
91
|
+
token = os.getenv("REPO_TOKEN")
|
|
92
|
+
if token:
|
|
93
|
+
return token
|
|
94
|
+
|
|
95
|
+
local_secrets_module_path = to_module_name(LocalSecretsConfigFile.get_path())
|
|
96
|
+
local_secrets_module = import_obj_from_importpath(local_secrets_module_path)
|
|
97
|
+
token = getattr(local_secrets_module, "REPO_TOKEN", None)
|
|
98
|
+
if not isinstance(token, str):
|
|
99
|
+
msg = f"Expected REPO_TOKEN to be str, got {type(token)}"
|
|
100
|
+
raise TypeError(msg)
|
|
101
|
+
if token:
|
|
102
|
+
return token
|
|
103
|
+
|
|
104
|
+
msg = (
|
|
105
|
+
f"No token named REPO_TOKEN found "
|
|
106
|
+
f"in github secrets or {LocalSecretsConfigFile.get_path()}"
|
|
107
|
+
)
|
|
108
|
+
raise ValueError(msg)
|
winipedia_utils/text/config.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Base class for config files."""
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any
|
|
@@ -13,53 +14,80 @@ from winipedia_utils.modules.class_ import init_all_nonabstract_subclasses
|
|
|
13
14
|
from winipedia_utils.projects.poetry.poetry import (
|
|
14
15
|
get_python_module_script,
|
|
15
16
|
)
|
|
17
|
+
from winipedia_utils.text.string import split_on_uppercase
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class ConfigFile(ABC):
|
|
19
21
|
"""Base class for config files."""
|
|
20
22
|
|
|
23
|
+
@classmethod
|
|
21
24
|
@abstractmethod
|
|
22
|
-
def
|
|
25
|
+
def get_parent_path(cls) -> Path:
|
|
23
26
|
"""Get the path to the config file."""
|
|
24
27
|
|
|
28
|
+
@classmethod
|
|
25
29
|
@abstractmethod
|
|
26
|
-
def load(
|
|
30
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
27
31
|
"""Load the config file."""
|
|
28
32
|
|
|
33
|
+
@classmethod
|
|
29
34
|
@abstractmethod
|
|
30
|
-
def dump(
|
|
35
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
31
36
|
"""Dump the config file."""
|
|
32
37
|
|
|
38
|
+
@classmethod
|
|
33
39
|
@abstractmethod
|
|
34
|
-
def
|
|
40
|
+
def get_file_extension(cls) -> str:
|
|
41
|
+
"""Get the file extension of the config file."""
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def get_configs(cls) -> dict[str, Any] | list[Any]:
|
|
35
46
|
"""Get the config."""
|
|
36
47
|
|
|
37
48
|
def __init__(self) -> None:
|
|
38
49
|
"""Initialize the config file."""
|
|
39
|
-
self.
|
|
40
|
-
self.
|
|
41
|
-
|
|
42
|
-
self.path.touch()
|
|
50
|
+
self.get_path().parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
if not self.get_path().exists():
|
|
52
|
+
self.get_path().touch()
|
|
43
53
|
self.dump(self.get_configs())
|
|
44
54
|
|
|
45
55
|
if not self.is_correct():
|
|
46
56
|
config = self.add_missing_configs()
|
|
47
57
|
self.dump(config)
|
|
48
58
|
|
|
49
|
-
self.config = self.load()
|
|
50
59
|
if not self.is_correct():
|
|
51
|
-
msg = f"Config file {self.
|
|
60
|
+
msg = f"Config file {self.get_path()} is not correct."
|
|
52
61
|
raise ValueError(msg)
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_path(cls) -> Path:
|
|
65
|
+
"""Get the path to the config file."""
|
|
66
|
+
return (
|
|
67
|
+
cls.get_parent_path() / f"{cls.get_filename()}.{cls.get_file_extension()}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_filename(cls) -> str:
|
|
72
|
+
"""Get the filename of the config file."""
|
|
73
|
+
name = cls.__name__
|
|
74
|
+
abstract_parents = [
|
|
75
|
+
parent.__name__ for parent in cls.__mro__ if inspect.isabstract(parent)
|
|
76
|
+
]
|
|
77
|
+
for parent in abstract_parents:
|
|
78
|
+
name = name.removesuffix(parent)
|
|
79
|
+
return "_".join(split_on_uppercase(name)).lower()
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def add_missing_configs(cls) -> dict[str, Any] | list[Any]:
|
|
55
83
|
"""Add any missing configs to the config file."""
|
|
56
|
-
current_config =
|
|
57
|
-
expected_config =
|
|
84
|
+
current_config = cls.load()
|
|
85
|
+
expected_config = cls.get_configs()
|
|
58
86
|
nested_structure_is_subset(
|
|
59
87
|
expected_config,
|
|
60
88
|
current_config,
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
cls.add_missing_dict_val,
|
|
90
|
+
cls.insert_missing_list_val,
|
|
63
91
|
)
|
|
64
92
|
return current_config
|
|
65
93
|
|
|
@@ -77,28 +105,30 @@ class ConfigFile(ABC):
|
|
|
77
105
|
"""Append a missing list value."""
|
|
78
106
|
actual_list.insert(index, expected_list[index])
|
|
79
107
|
|
|
80
|
-
|
|
108
|
+
@classmethod
|
|
109
|
+
def is_correct(cls) -> bool:
|
|
81
110
|
"""Check if the config is correct.
|
|
82
111
|
|
|
83
112
|
If the file is empty, it is considered correct.
|
|
84
113
|
This is so bc if a user does not want a specific config file,
|
|
85
114
|
they can just make it empty and the tests will not fail.
|
|
86
115
|
"""
|
|
87
|
-
return
|
|
88
|
-
|
|
116
|
+
return cls.is_unwanted() or cls.is_correct_recursively(
|
|
117
|
+
cls.get_configs(), cls.load()
|
|
89
118
|
)
|
|
90
119
|
|
|
91
|
-
|
|
120
|
+
@classmethod
|
|
121
|
+
def is_unwanted(cls) -> bool:
|
|
92
122
|
"""Check if the config file is unwanted.
|
|
93
123
|
|
|
94
124
|
If the file is empty, it is considered unwanted.
|
|
95
125
|
"""
|
|
96
|
-
return
|
|
126
|
+
return cls.get_path().exists() and cls.get_path().read_text() == ""
|
|
97
127
|
|
|
98
128
|
@staticmethod
|
|
99
129
|
def is_correct_recursively(
|
|
100
|
-
expected_config: Any,
|
|
101
|
-
actual_config: Any,
|
|
130
|
+
expected_config: dict[str, Any] | list[Any],
|
|
131
|
+
actual_config: dict[str, Any] | list[Any],
|
|
102
132
|
) -> bool:
|
|
103
133
|
"""Check if the config is correct.
|
|
104
134
|
|
|
@@ -119,35 +149,52 @@ class ConfigFile(ABC):
|
|
|
119
149
|
"""Initialize all subclasses."""
|
|
120
150
|
init_all_nonabstract_subclasses(cls, load_package_before=winipedia_utils)
|
|
121
151
|
|
|
152
|
+
@staticmethod
|
|
153
|
+
def get_python_setup_script() -> str:
|
|
154
|
+
"""Get the poetry run setup script."""
|
|
155
|
+
from winipedia_utils import setup # noqa: PLC0415 # avoid circular import
|
|
156
|
+
|
|
157
|
+
return get_python_module_script(setup)
|
|
158
|
+
|
|
122
159
|
|
|
123
160
|
class YamlConfigFile(ConfigFile):
|
|
124
161
|
"""Base class for yaml config files."""
|
|
125
162
|
|
|
126
|
-
|
|
163
|
+
@classmethod
|
|
164
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
127
165
|
"""Load the config file."""
|
|
128
|
-
return yaml.safe_load(
|
|
166
|
+
return yaml.safe_load(cls.get_path().read_text()) or {}
|
|
129
167
|
|
|
130
|
-
|
|
168
|
+
@classmethod
|
|
169
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
131
170
|
"""Dump the config file."""
|
|
132
|
-
with
|
|
171
|
+
with cls.get_path().open("w") as f:
|
|
133
172
|
yaml.safe_dump(config, f, sort_keys=False)
|
|
134
173
|
|
|
135
|
-
@
|
|
136
|
-
def
|
|
137
|
-
"""Get the
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return get_python_module_script(setup)
|
|
174
|
+
@classmethod
|
|
175
|
+
def get_file_extension(cls) -> str:
|
|
176
|
+
"""Get the file extension of the config file."""
|
|
177
|
+
return "yaml"
|
|
141
178
|
|
|
142
179
|
|
|
143
180
|
class TomlConfigFile(ConfigFile):
|
|
144
181
|
"""Base class for toml config files."""
|
|
145
182
|
|
|
146
|
-
|
|
183
|
+
@classmethod
|
|
184
|
+
def load(cls) -> dict[str, Any]:
|
|
147
185
|
"""Load the config file."""
|
|
148
|
-
return tomlkit.parse(
|
|
186
|
+
return tomlkit.parse(cls.get_path().read_text())
|
|
149
187
|
|
|
150
|
-
|
|
188
|
+
@classmethod
|
|
189
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
151
190
|
"""Dump the config file."""
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
if not isinstance(config, dict):
|
|
192
|
+
msg = f"Cannot dump {config} to toml file."
|
|
193
|
+
raise TypeError(msg)
|
|
194
|
+
with cls.get_path().open("w") as f:
|
|
195
|
+
tomlkit.dump(config, f, sort_keys=False)
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def get_file_extension(cls) -> str:
|
|
199
|
+
"""Get the file extension of the config file."""
|
|
200
|
+
return "toml"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: winipedia-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.18
|
|
4
4
|
Summary: A package with many utility functions
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,6 +16,7 @@ Requires-Dist: defusedxml
|
|
|
16
16
|
Requires-Dist: keyring
|
|
17
17
|
Requires-Dist: pathspec
|
|
18
18
|
Requires-Dist: polars
|
|
19
|
+
Requires-Dist: pygithub
|
|
19
20
|
Requires-Dist: pyyaml
|
|
20
21
|
Requires-Dist: setuptools
|
|
21
22
|
Requires-Dist: tomlkit
|
|
@@ -62,13 +63,26 @@ A comprehensive Python utility package that enforces best practices, automates p
|
|
|
62
63
|
|
|
63
64
|
## Quick Start
|
|
64
65
|
|
|
65
|
-
###
|
|
66
|
+
### How to setup a new project
|
|
66
67
|
|
|
67
68
|
```bash
|
|
68
|
-
#
|
|
69
|
+
# 1: Create a new repository on GitHub
|
|
70
|
+
# The default branch must be called main
|
|
71
|
+
# add a PAT or Fine-Grained Access Token to your repo secrets called REPO_TOKEN that has write access to the repository (Adminstration and Contents)(needed for branch protection in health_check.yaml - see winipedia_utils.git.github.repo.protect and for commiting as an action in release.yaml)
|
|
72
|
+
|
|
73
|
+
# 2: Clone the repository
|
|
74
|
+
git clone https://github.com/owner/repo.git
|
|
75
|
+
|
|
76
|
+
# 3: Create a new poetry project
|
|
77
|
+
poetry init # or poetry new
|
|
78
|
+
# 4: Poetry will ask you some stuff when you run poetry init.
|
|
79
|
+
# First author name must be equal to the GitHub repository owner (username).
|
|
80
|
+
# The repository name must be equal to the package/project name.
|
|
81
|
+
|
|
82
|
+
# 5: Add winipedia-utils to your project
|
|
69
83
|
poetry add winipedia-utils
|
|
70
84
|
|
|
71
|
-
# Run the automated setup
|
|
85
|
+
# 6: Run the automated setup
|
|
72
86
|
poetry run python -m winipedia_utils.setup
|
|
73
87
|
```
|
|
74
88
|
|
|
@@ -88,8 +102,9 @@ The setup creates the following configuration files:
|
|
|
88
102
|
- `.pre-commit-config.yaml` - Pre-commit hook configuration
|
|
89
103
|
- `.gitignore` - Git ignore rules (assumes you added one on GitHub before.)
|
|
90
104
|
- `pyproject.toml` - Project configuration with Poetry settings
|
|
91
|
-
- `.github/workflows/
|
|
92
|
-
- `.github/workflows/
|
|
105
|
+
- `.github/workflows/health_check.yaml` - Health check workflow (Runs on every push and pull request)
|
|
106
|
+
- `.github/workflows/release.yaml` - Release workflow (Creates a release on GitHub when the same actions as in health check pass and commits are pushed to main)
|
|
107
|
+
- `.github/workflows/publish.yaml` - Publishing workflow (Publishes to PyPI when a release is created by the release workflow, if you use this workflow, you need to add a PYPI_TOKEN (named PYPI_TOKEN) to your GitHub secrets that has write access to the package on PyPI.)
|
|
93
108
|
- `py.typed` - PEP 561 marker for type hints
|
|
94
109
|
- `experiment.py` - For experimentation (ignored by git)
|
|
95
110
|
- `test0.py` - Test file with one empyt test (so that initial tests pass)
|
|
@@ -106,8 +121,8 @@ Usually VSCode or other IDEs activates the venv automatically when opening the t
|
|
|
106
121
|
1. Patch version (poetry version patch)
|
|
107
122
|
2. Add version patch to git (git add pyproject.toml)
|
|
108
123
|
3. Update package manager (poetry self update)
|
|
109
|
-
4. Install packages (poetry install)
|
|
110
|
-
5. Update packages (poetry update)
|
|
124
|
+
4. Install packages (poetry install --with dev)
|
|
125
|
+
5. Update packages (poetry update --with dev (winipedia_utils forces all dependencies with * to be updated to latest compatible version))
|
|
111
126
|
6. Lock dependencies (poetry lock)
|
|
112
127
|
7. Check package manager configs (poetry check --strict)
|
|
113
128
|
8. Create tests (python -m winipedia_utils.testing.create_tests)
|