winipedia-utils 0.3.43__py3-none-any.whl → 0.4.12__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 (33) hide show
  1. winipedia_utils/git/github/repo/__init__.py +1 -0
  2. winipedia_utils/git/github/repo/protect.py +104 -0
  3. winipedia_utils/git/github/repo/repo.py +205 -0
  4. winipedia_utils/git/github/workflows/base/__init__.py +1 -0
  5. winipedia_utils/git/{workflows → github/workflows}/base/base.py +95 -51
  6. winipedia_utils/git/github/workflows/health_check.py +55 -0
  7. winipedia_utils/git/{workflows → github/workflows}/publish.py +11 -8
  8. winipedia_utils/git/github/workflows/release.py +45 -0
  9. winipedia_utils/git/gitignore/config.py +49 -29
  10. winipedia_utils/git/gitignore/gitignore.py +1 -1
  11. winipedia_utils/git/pre_commit/config.py +18 -13
  12. winipedia_utils/git/pre_commit/hooks.py +22 -4
  13. winipedia_utils/git/pre_commit/run_hooks.py +2 -1
  14. winipedia_utils/iterating/iterate.py +3 -4
  15. winipedia_utils/modules/module.py +2 -0
  16. winipedia_utils/modules/package.py +2 -1
  17. winipedia_utils/projects/poetry/config.py +74 -36
  18. winipedia_utils/projects/project.py +2 -2
  19. winipedia_utils/setup.py +2 -0
  20. winipedia_utils/testing/config.py +83 -29
  21. winipedia_utils/testing/tests/base/fixtures/fixture.py +36 -0
  22. winipedia_utils/testing/tests/base/fixtures/scopes/module.py +6 -5
  23. winipedia_utils/testing/tests/base/fixtures/scopes/session.py +7 -8
  24. winipedia_utils/testing/tests/base/utils/utils.py +43 -2
  25. winipedia_utils/text/config.py +84 -37
  26. {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.12.dist-info}/METADATA +23 -8
  27. {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.12.dist-info}/RECORD +31 -27
  28. winipedia_utils/git/workflows/health_check.py +0 -51
  29. winipedia_utils/git/workflows/release.py +0 -33
  30. /winipedia_utils/git/{workflows/base → github}/__init__.py +0 -0
  31. /winipedia_utils/git/{workflows → github/workflows}/__init__.py +0 -0
  32. {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.12.dist-info}/WHEEL +0 -0
  33. {winipedia_utils-0.3.43.dist-info → winipedia_utils-0.4.12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,45 @@
1
+ """Contains the release workflow.
2
+
3
+ This workflow is used to create a release on GitHub.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from winipedia_utils.git.github.workflows.health_check import HealthCheckWorkflow
9
+
10
+
11
+ class ReleaseWorkflow(HealthCheckWorkflow):
12
+ """Release workflow.
13
+
14
+ This workflow is triggered by a push to the main branch.
15
+ It creates a tag for the release and builds a changelog.
16
+ With tag and changelog it creates a release on GitHub
17
+ """
18
+
19
+ @classmethod
20
+ def get_workflow_triggers(cls) -> dict[str, Any]:
21
+ """Get the workflow triggers."""
22
+ return {
23
+ "push": {"branches": ["main"]},
24
+ "workflow_dispatch": {},
25
+ "schedule": [
26
+ {
27
+ # run every Tuesday at 6 am
28
+ "cron": "0 6 * * 2",
29
+ },
30
+ ],
31
+ }
32
+
33
+ @classmethod
34
+ def get_permissions(cls) -> dict[str, Any]:
35
+ """Get the workflow permissions."""
36
+ return {
37
+ "contents": "write",
38
+ }
39
+
40
+ @classmethod
41
+ def get_jobs(cls) -> dict[str, Any]:
42
+ """Get the workflow jobs."""
43
+ steps = super().get_jobs()
44
+ steps[cls.get_filename()]["steps"].extend(cls.get_release_steps())
45
+ return steps
@@ -3,52 +3,72 @@
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ import requests
7
+
8
+ from winipedia_utils.testing.config import ExperimentConfigFile, LocalSecretsConfigFile
6
9
  from winipedia_utils.text.config import ConfigFile
7
10
 
8
11
 
9
12
  class GitIgnoreConfigFile(ConfigFile):
10
13
  """Config file for .gitignore."""
11
14
 
12
- PATH = Path(".gitignore")
13
-
14
- IGNORE_KEY = "ignore"
15
+ @classmethod
16
+ def get_filename(cls) -> str:
17
+ """Get the filename of the config file."""
18
+ return "" # so it builds the path .gitignore and not gitignore.gitignore
15
19
 
16
- def get_path(self) -> Path:
20
+ @classmethod
21
+ def get_parent_path(cls) -> Path:
17
22
  """Get the path to the config file."""
18
- return self.PATH
23
+ return Path()
19
24
 
20
- def load(self) -> dict[str, Any]:
21
- """Load the config file."""
22
- return self.load_static()
25
+ @classmethod
26
+ def get_file_extension(cls) -> str:
27
+ """Get the file extension of the config file."""
28
+ return "gitignore"
23
29
 
24
30
  @classmethod
25
- def load_static(cls) -> dict[str, list[str]]:
31
+ def load(cls) -> list[str]:
26
32
  """Load the config file."""
27
- paths = cls.PATH.read_text().splitlines()
28
- return {cls.IGNORE_KEY: paths}
33
+ return cls.get_path().read_text().splitlines()
29
34
 
30
- def dump(self, config: dict[str, Any]) -> None:
35
+ @classmethod
36
+ def dump(cls, config: list[str] | dict[str, Any]) -> None:
31
37
  """Dump the config file."""
32
- patterns = config.get(self.IGNORE_KEY, [])
33
- self.path.write_text("\n".join(patterns))
38
+ if not isinstance(config, list):
39
+ msg = f"Cannot dump {config} to .gitignore file."
40
+ raise TypeError(msg)
41
+ cls.get_path().write_text("\n".join(config))
34
42
 
35
- def get_configs(self) -> dict[str, Any]:
43
+ @classmethod
44
+ def get_configs(cls) -> list[str]:
36
45
  """Get the config."""
37
- from winipedia_utils.testing.config import ( # noqa: PLC0415 # avoid circular import
38
- ExperimentConfigFile,
39
- )
40
-
46
+ # fetch the standard github gitignore via https://github.com/github/gitignore/blob/main/Python.gitignore
41
47
  needed = [
42
- "__pycache__/",
43
- ".idea/",
44
- ".mypy_cache/",
45
- ".pytest_cache/",
46
- ".ruff_cache/",
48
+ *cls.get_github_python_gitignore(),
49
+ "# vscode stuff",
47
50
  ".vscode/",
48
- "dist/",
49
- ".git/", # for walk_os_skipping_gitignore_patterns func
50
- ExperimentConfigFile.PATH.name, # for executing experimental code
51
+ "",
52
+ "# winipedia_utils stuff",
53
+ "# for walk_os_skipping_gitignore_patterns func",
54
+ ".git/",
55
+ "# for executing experimental code",
56
+ ExperimentConfigFile.get_path().as_posix(),
57
+ "# for secrets used locally",
58
+ LocalSecretsConfigFile.get_path().as_posix(),
51
59
  ]
52
- existing = self.load()[self.IGNORE_KEY]
60
+ existing = cls.load()
53
61
  needed = [p for p in needed if p not in set(existing)]
54
- return {self.IGNORE_KEY: existing + needed}
62
+ return existing + needed
63
+
64
+ @classmethod
65
+ def get_github_python_gitignore(cls) -> list[str]:
66
+ """Get the standard github python gitignore."""
67
+ url = "https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore"
68
+ res = requests.get(url, timeout=10)
69
+ if not res.ok:
70
+ if not Path(".gitignore").exists():
71
+ msg = f"Failed to fetch {url}. Cannot create .gitignore."
72
+ raise RuntimeError(msg)
73
+ return []
74
+ return res.text.splitlines()
@@ -40,7 +40,7 @@ def path_is_in_gitignore(relative_path: str | Path) -> bool:
40
40
 
41
41
  spec = pathspec.PathSpec.from_lines(
42
42
  "gitwildmatch",
43
- GitIgnoreConfigFile.load_static()[GitIgnoreConfigFile.IGNORE_KEY],
43
+ GitIgnoreConfigFile.load(),
44
44
  )
45
45
 
46
46
  return spec.match_file(as_posix)
@@ -7,27 +7,27 @@ import winipedia_utils
7
7
  from winipedia_utils.logging.logger import get_logger
8
8
  from winipedia_utils.modules.package import make_name_from_package
9
9
  from winipedia_utils.os.os import run_subprocess
10
- from winipedia_utils.projects.poetry.poetry import POETRY_RUN_ARGS
11
10
  from winipedia_utils.text.config import YamlConfigFile
12
11
 
13
12
  logger = get_logger(__name__)
14
13
 
15
14
 
16
- class PreCommitConfigFile(YamlConfigFile):
15
+ class PreCommitConfigConfigFile(YamlConfigFile):
17
16
  """Config file for pre-commit."""
18
17
 
19
- PATH = Path(".pre-commit-config.yaml")
20
-
21
- def __init__(self) -> None:
22
- """Init the file."""
23
- super().__init__()
24
- self.install()
18
+ @classmethod
19
+ def get_filename(cls) -> str:
20
+ """Get the filename of the config file."""
21
+ filename = super().get_filename()
22
+ return f".{filename.replace('_', '-')}"
25
23
 
26
- def get_path(self) -> Path:
24
+ @classmethod
25
+ def get_parent_path(cls) -> Path:
27
26
  """Get the path to the config file."""
28
- return self.PATH
27
+ return Path()
29
28
 
30
- def get_configs(self) -> dict[str, Any]:
29
+ @classmethod
30
+ def get_configs(cls) -> dict[str, Any]:
31
31
  """Get the config."""
32
32
  hook_name = make_name_from_package(winipedia_utils, capitalize=False)
33
33
  return {
@@ -38,7 +38,7 @@ class PreCommitConfigFile(YamlConfigFile):
38
38
  {
39
39
  "id": hook_name,
40
40
  "name": hook_name,
41
- "entry": self.get_python_setup_script(),
41
+ "entry": cls.get_python_setup_script(),
42
42
  "language": "system",
43
43
  "always_run": True,
44
44
  "pass_filenames": False,
@@ -48,8 +48,13 @@ class PreCommitConfigFile(YamlConfigFile):
48
48
  ]
49
49
  }
50
50
 
51
+ def __init__(self) -> None:
52
+ """Init the file."""
53
+ super().__init__()
54
+ self.install()
55
+
51
56
  @classmethod
52
57
  def install(cls) -> None:
53
58
  """Installs the pre commits in the config."""
54
59
  logger.info("Running pre-commit install")
55
- run_subprocess([*POETRY_RUN_ARGS, "pre-commit", "install"], check=True)
60
+ run_subprocess(["pre-commit", "install"], check=True)
@@ -42,20 +42,29 @@ def update_package_manager() -> list[str | Path]:
42
42
  return [POETRY_ARG, "self", "update"]
43
43
 
44
44
 
45
- def install_packages() -> list[str | Path]:
45
+ def install_dependencies_with_dev() -> list[str | Path]:
46
46
  """Install all dependencies.
47
47
 
48
48
  This function returns the input for subprocess.run() to install all dependencies.
49
49
  """
50
- return [POETRY_ARG, "install"]
50
+ return [POETRY_ARG, "install", "--with", "dev"]
51
51
 
52
52
 
53
- def update_packages() -> list[str | Path]:
53
+ def update_dependencies_with_dev() -> list[str | Path]:
54
54
  """Update all dependencies.
55
55
 
56
56
  This function returns the input for subprocess.run() to update all dependencies.
57
57
  """
58
- return [POETRY_ARG, "update"]
58
+ return [POETRY_ARG, "update", "--with", "dev"]
59
+
60
+
61
+ def add_updates_to_git() -> list[str | Path]:
62
+ """Add the updated dependencies to git.
63
+
64
+ This function returns the input for subprocess.run() to add the updated
65
+ dependencies to git, so that the hook does not fail bc the file was changed.
66
+ """
67
+ return ["git", "add", "pyproject.toml"]
59
68
 
60
69
 
61
70
  def lock_dependencies() -> list[str | Path]:
@@ -66,6 +75,15 @@ def lock_dependencies() -> list[str | Path]:
66
75
  return [POETRY_ARG, "lock"]
67
76
 
68
77
 
78
+ def add_lock_file_to_git() -> list[str | Path]:
79
+ """Add the lock file to git.
80
+
81
+ This function returns the input for subprocess.run() to add the lock file
82
+ to git, so that the hook does not fail bc the file was changed.
83
+ """
84
+ return ["git", "add", "poetry.lock"]
85
+
86
+
69
87
  def check_package_manager_configs() -> list[str | Path]:
70
88
  """Check that poetry.lock and pyproject.toml is up to date.
71
89
 
@@ -42,4 +42,5 @@ def run_hooks() -> None:
42
42
  passed_str,
43
43
  )
44
44
 
45
- sys.exit(exit_code)
45
+ if exit_code != 0:
46
+ sys.exit(exit_code)
@@ -78,10 +78,9 @@ def nested_structure_is_subset(
78
78
  if not nested_structure_is_subset(
79
79
  value, actual_value, on_false_dict_action, on_false_list_action
80
80
  ):
81
- fixed = False
81
+ all_good = False
82
82
  if on_false_action is not None:
83
83
  on_false_action(subset, superset, key_or_index)
84
- fixed = nested_structure_is_subset(subset, superset)
85
- if not fixed:
86
- all_good = False
84
+ all_good = nested_structure_is_subset(subset, superset)
85
+
87
86
  return all_good
@@ -215,6 +215,8 @@ def import_obj_from_importpath(
215
215
  return import_module(importpath)
216
216
  except ImportError:
217
217
  # might be a class or function
218
+ if "." not in importpath:
219
+ raise
218
220
  module_name, obj_name = importpath.rsplit(".", 1)
219
221
  module = import_module(module_name)
220
222
  obj: Callable[..., Any] | type | ModuleType = getattr(module, obj_name)
@@ -183,7 +183,8 @@ def find_packages(
183
183
  )
184
184
 
185
185
  if exclude is None:
186
- exclude = GitIgnoreConfigFile.load_static()[GitIgnoreConfigFile.IGNORE_KEY]
186
+ # must init GitIgnoreConfigFile to create .gitignore if it does not exist
187
+ exclude = GitIgnoreConfigFile.load()
187
188
  exclude = [
188
189
  p.replace("/", ".").removesuffix(".") for p in exclude if p.endswith("/")
189
190
  ]
@@ -1,23 +1,23 @@
1
1
  """Config utilities for poetry and pyproject.toml."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  from winipedia_utils.modules.package import get_src_package, make_name_from_package
7
7
  from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
8
8
  from winipedia_utils.text.config import ConfigFile, TomlConfigFile
9
9
 
10
10
 
11
- class PyProjectTomlConfig(TomlConfigFile):
11
+ class PyprojectConfigFile(TomlConfigFile):
12
12
  """Config file for pyproject.toml."""
13
13
 
14
- PATH = Path("pyproject.toml")
15
-
16
- def get_path(self) -> Path:
14
+ @classmethod
15
+ def get_parent_path(cls) -> Path:
17
16
  """Get the path to the config file."""
18
- return self.PATH
17
+ return Path()
19
18
 
20
- def get_configs(self) -> dict[str, Any]:
19
+ @classmethod
20
+ def get_configs(cls) -> dict[str, Any]:
21
21
  """Get the config."""
22
22
  return {
23
23
  "project": {
@@ -32,21 +32,14 @@ class PyProjectTomlConfig(TomlConfigFile):
32
32
  "tool": {
33
33
  "poetry": {
34
34
  "packages": [{"include": get_src_package().__name__}],
35
+ "dependencies": dict.fromkeys(
36
+ cls.get_dependencies(),
37
+ "*",
38
+ ),
35
39
  "group": {
36
40
  "dev": {
37
41
  "dependencies": dict.fromkeys(
38
- [
39
- "ruff",
40
- "pre-commit",
41
- "mypy",
42
- "pytest",
43
- "bandit",
44
- "types-setuptools",
45
- "types-tqdm",
46
- "types-defusedxml",
47
- "types-pyyaml",
48
- "pytest-mock",
49
- ],
42
+ cls.get_dev_dependencies(),
50
43
  "*",
51
44
  )
52
45
  }
@@ -72,16 +65,23 @@ class PyProjectTomlConfig(TomlConfigFile):
72
65
  },
73
66
  }
74
67
 
75
- def get_package_name(self) -> str:
68
+ @classmethod
69
+ def get_package_name(cls) -> str:
76
70
  """Get the package name."""
77
- project_dict = self.load().get("project", {})
71
+ project_dict = cls.load().get("project", {})
78
72
  package_name = str(project_dict.get("name", ""))
79
73
  return package_name.replace("-", "_")
80
74
 
81
- def get_dev_dependencies(self) -> set[str]:
75
+ @classmethod
76
+ def get_all_dependencies(cls) -> set[str]:
77
+ """Get all dependencies."""
78
+ return cls.get_dependencies() | cls.get_dev_dependencies()
79
+
80
+ @classmethod
81
+ def get_dev_dependencies(cls) -> set[str]:
82
82
  """Get the dev dependencies."""
83
83
  dev_dependencies = set(
84
- self.load()
84
+ cls.load()
85
85
  .get("tool", {})
86
86
  .get("poetry", {})
87
87
  .get("group", {})
@@ -91,38 +91,76 @@ class PyProjectTomlConfig(TomlConfigFile):
91
91
  )
92
92
  if not dev_dependencies:
93
93
  dev_dependencies = set(
94
- self.load().get("dependency-groups", {}).get("dev", [])
94
+ cls.load().get("dependency-groups", {}).get("dev", [])
95
95
  )
96
96
  dev_dependencies = {d.split("(")[0].strip() for d in dev_dependencies}
97
97
  return dev_dependencies
98
98
 
99
- def get_expected_dev_dependencies(self) -> set[str]:
99
+ @classmethod
100
+ def get_dependencies(cls) -> set[str]:
101
+ """Get the dependencies."""
102
+ return set(
103
+ cls.load().get("tool", {}).get("poetry", {}).get("dependencies", {}).keys()
104
+ )
105
+
106
+ @classmethod
107
+ def get_expected_dev_dependencies(cls) -> set[str]:
100
108
  """Get the expected dev dependencies."""
101
109
  return set(
102
- self.get_configs()["tool"]["poetry"]["group"]["dev"]["dependencies"].keys()
110
+ cls.get_configs()["tool"]["poetry"]["group"]["dev"]["dependencies"].keys()
103
111
  )
104
112
 
113
+ @classmethod
114
+ def get_authors(cls) -> list[dict[str, str]]:
115
+ """Get the authors."""
116
+ return cast(
117
+ "list[dict[str, str]]", cls.load().get("project", {}).get("authors", [])
118
+ )
105
119
 
106
- class PyTypedConfigFile(ConfigFile):
120
+ @classmethod
121
+ def get_main_author(cls) -> dict[str, str]:
122
+ """Get the main author.
123
+
124
+ Assumes the main author is the first author.
125
+ """
126
+ return cls.get_authors()[0]
127
+
128
+ @classmethod
129
+ def get_main_author_name(cls) -> str:
130
+ """Get the main author name."""
131
+ return cls.get_main_author()["name"]
132
+
133
+
134
+ class TypedConfigFile(ConfigFile):
107
135
  """Config file for py.typed."""
108
136
 
109
- def get_path(self) -> Path:
110
- """Get the path to the config file."""
111
- toml_config = PyProjectTomlConfig()
112
- package_name = toml_config.get_package_name()
113
- return Path(package_name) / "py.typed"
137
+ @classmethod
138
+ def get_file_extension(cls) -> str:
139
+ """Get the file extension of the config file."""
140
+ return "typed"
114
141
 
115
- def load(self) -> dict[str, Any]:
142
+ @classmethod
143
+ def load(cls) -> dict[str, Any] | list[Any]:
116
144
  """Load the config file."""
117
145
  return {}
118
146
 
119
- def dump(self, config: dict[str, Any]) -> None:
147
+ @classmethod
148
+ def dump(cls, config: dict[str, Any] | list[Any]) -> None:
120
149
  """Dump the config file."""
121
150
  if config:
122
151
  msg = "Cannot dump to py.typed file."
123
152
  raise ValueError(msg)
124
- self.path.touch()
125
153
 
126
- def get_configs(self) -> dict[str, Any]:
154
+ @classmethod
155
+ def get_configs(cls) -> dict[str, Any] | list[Any]:
127
156
  """Get the config."""
128
157
  return {}
158
+
159
+
160
+ class PyTypedConfigFile(ConfigFile):
161
+ """Config file for py.typed."""
162
+
163
+ @classmethod
164
+ def get_parent_path(cls) -> Path:
165
+ """Get the path to the config file."""
166
+ return Path(PyprojectConfigFile.get_package_name())
@@ -2,7 +2,7 @@
2
2
 
3
3
  from winipedia_utils.modules.module import create_module
4
4
  from winipedia_utils.projects.poetry.config import (
5
- PyProjectTomlConfig, # avoid circular import
5
+ PyprojectConfigFile, # avoid circular import
6
6
  )
7
7
  from winipedia_utils.text.config import (
8
8
  ConfigFile, # avoid circular import
@@ -13,5 +13,5 @@ def create_project_root() -> None:
13
13
  """Create the project root."""
14
14
  ConfigFile.init_config_files()
15
15
 
16
- src_package_name = PyProjectTomlConfig().get_package_name()
16
+ src_package_name = PyprojectConfigFile.get_package_name()
17
17
  create_module(src_package_name, is_package=True)
winipedia_utils/setup.py CHANGED
@@ -9,6 +9,7 @@ This script is intended to be called once at the beginning of a project.
9
9
  from collections.abc import Callable
10
10
  from typing import Any
11
11
 
12
+ from winipedia_utils.git.gitignore.config import GitIgnoreConfigFile
12
13
  from winipedia_utils.git.pre_commit.run_hooks import run_hooks
13
14
  from winipedia_utils.logging.logger import get_logger
14
15
  from winipedia_utils.projects.project import create_project_root
@@ -17,6 +18,7 @@ logger = get_logger(__name__)
17
18
 
18
19
 
19
20
  SETUP_STEPS: list[Callable[..., Any]] = [
21
+ GitIgnoreConfigFile, # must be first
20
22
  create_project_root,
21
23
  run_hooks,
22
24
  ]