winipedia-utils 0.2.58__py3-none-any.whl → 0.3.7__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/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 +102 -52
  10. winipedia_utils/git/workflows/publish.py +25 -49
  11. winipedia_utils/git/workflows/release.py +22 -46
  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.7.dist-info/METADATA +324 -0
  29. {winipedia_utils-0.2.58.dist-info → winipedia_utils-0.3.7.dist-info}/RECORD +31 -28
  30. winipedia_utils/consts.py +0 -21
  31. winipedia_utils-0.2.58.dist-info/METADATA +0 -738
  32. {winipedia_utils-0.2.58.dist-info → winipedia_utils-0.3.7.dist-info}/WHEEL +0 -0
  33. {winipedia_utils-0.2.58.dist-info → winipedia_utils-0.3.7.dist-info}/licenses/LICENSE +0 -0
@@ -6,47 +6,43 @@ This workflow is used to publish the package to PyPI with poetry.
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- import yaml
9
+ from winipedia_utils.git.workflows.base.base import Workflow
10
+ from winipedia_utils.git.workflows.release import ReleaseWorkflow
10
11
 
11
- from winipedia_utils.git.workflows.base.base import _get_poetry_setup_steps
12
- from winipedia_utils.git.workflows.release import WORKFLOW_NAME
13
12
 
14
- PUBLISH_WORKFLOW_PATH = Path(".github/workflows/publish.yaml")
13
+ class PublishWorkflow(Workflow):
14
+ """Publish workflow."""
15
15
 
16
+ PATH = Path(".github/workflows/publish.yaml")
16
17
 
17
- def load_publish_workflow() -> dict[str, Any]:
18
- """Load the publish workflow."""
19
- path = PUBLISH_WORKFLOW_PATH
20
- if not path.exists():
21
- path.parent.mkdir(parents=True, exist_ok=True)
22
- path.touch()
23
- return yaml.safe_load(path.read_text()) or {}
18
+ def get_path(self) -> Path:
19
+ """Get the path to the config file."""
20
+ return self.PATH
24
21
 
25
-
26
- def dump_publish_workflow(config: dict[str, Any]) -> None:
27
- """Dump the publish workflow."""
28
- path = PUBLISH_WORKFLOW_PATH
29
- with path.open("w") as f:
30
- yaml.safe_dump(config, f, sort_keys=False)
31
-
32
-
33
- def _get_publish_config() -> dict[str, Any]:
34
- """Dict that represents the publish workflow yaml."""
35
- return {
36
- "name": "Publish to PyPI",
37
- "on": {
22
+ def get_workflow_triggers(self) -> dict[str, Any]:
23
+ """Get the workflow triggers."""
24
+ return {
38
25
  "workflow_run": {
39
- "workflows": [WORKFLOW_NAME],
26
+ "workflows": [ReleaseWorkflow.get_workflow_name()],
40
27
  "types": ["completed"],
41
28
  },
42
- },
43
- "jobs": {
29
+ }
30
+
31
+ def get_permissions(self) -> dict[str, Any]:
32
+ """Get the workflow permissions."""
33
+ return {
34
+ "contents": "read",
35
+ }
36
+
37
+ def get_jobs(self) -> dict[str, Any]:
38
+ """Get the workflow jobs."""
39
+ return {
44
40
  "publish": {
45
41
  "runs-on": "ubuntu-latest",
46
42
  "if": "${{ github.event.workflow_run.conclusion == 'success' }}",
47
43
  "steps": [
48
44
  *(
49
- _get_poetry_setup_steps(
45
+ self.get_poetry_setup_steps(
50
46
  configure_pipy_token=True,
51
47
  )
52
48
  ),
@@ -56,24 +52,4 @@ def _get_publish_config() -> dict[str, Any]:
56
52
  },
57
53
  ],
58
54
  }
59
- },
60
- }
61
-
62
-
63
- def _publish_config_is_correct() -> bool:
64
- """Check if the publish workflow is correct."""
65
- config = load_publish_workflow()
66
- return bool(config == _get_publish_config())
67
-
68
-
69
- def _add_publish_workflow() -> None:
70
- """Add the publish workflow.
71
-
72
- If you delete the .github/workflows/publish.yaml file, then the tests will not fail.
73
- Not all projects need publishing to pypi. It is added on setup, but if you remove
74
- the file, then the tests will not fail and the tests will assume you don't want it.
75
- """
76
- if _publish_config_is_correct():
77
- return
78
- config = _get_publish_config()
79
- dump_publish_workflow(config)
55
+ }
@@ -6,45 +6,36 @@ This workflow is used to create a release on GitHub.
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- import yaml
9
+ from winipedia_utils.git.workflows.base.base import Workflow
10
10
 
11
- from winipedia_utils.git.workflows.base.base import _get_poetry_setup_steps
12
11
 
13
- RELEASE_WORKFLOW_PATH = Path(".github/workflows/release.yaml")
12
+ class ReleaseWorkflow(Workflow):
13
+ """Release workflow."""
14
14
 
15
- WORKFLOW_NAME = "Create Release"
15
+ PATH = Path(".github/workflows/release.yaml")
16
16
 
17
+ def get_path(self) -> Path:
18
+ """Get the path to the config file."""
19
+ return self.PATH
17
20
 
18
- def load_release_workflow() -> dict[str, Any]:
19
- """Load the release workflow."""
20
- path = RELEASE_WORKFLOW_PATH
21
- if not path.exists():
22
- path.parent.mkdir(parents=True, exist_ok=True)
23
- path.touch()
24
- return yaml.safe_load(path.read_text()) or {}
21
+ def get_workflow_triggers(self) -> dict[str, Any]:
22
+ """Get the workflow triggers."""
23
+ return {"push": {"tags": ["v*"]}}
25
24
 
26
-
27
- def dump_release_workflow(config: dict[str, Any]) -> None:
28
- """Dump the release workflow."""
29
- path = RELEASE_WORKFLOW_PATH
30
- with path.open("w") as f:
31
- yaml.safe_dump(config, f, sort_keys=False)
32
-
33
-
34
- def _get_release_config() -> dict[str, Any]:
35
- """Dict that represents the release workflow yaml."""
36
- return {
37
- "name": WORKFLOW_NAME,
38
- "on": {"push": {"tags": ["v*"]}},
39
- "permissions": {
25
+ def get_permissions(self) -> dict[str, Any]:
26
+ """Get the workflow permissions."""
27
+ return {
40
28
  "contents": "write",
41
- },
42
- "jobs": {
29
+ }
30
+
31
+ def get_jobs(self) -> dict[str, Any]:
32
+ """Get the workflow jobs."""
33
+ return {
43
34
  "release": {
44
35
  "runs-on": "ubuntu-latest",
45
36
  "steps": [
46
37
  *(
47
- _get_poetry_setup_steps(
38
+ self.get_poetry_setup_steps(
48
39
  install_dependencies=True,
49
40
  fetch_depth=0,
50
41
  force_main_head=True,
@@ -52,7 +43,7 @@ def _get_release_config() -> dict[str, Any]:
52
43
  ),
53
44
  {
54
45
  "name": "Run Pre-commit Hooks",
55
- "run": "poetry run pre-commit run --all-files",
46
+ "run": "poetry run pre-commit run",
56
47
  },
57
48
  {
58
49
  "name": "Build Changelog",
@@ -65,25 +56,10 @@ def _get_release_config() -> dict[str, Any]:
65
56
  "uses": "ncipollo/release-action@v1",
66
57
  "with": {
67
58
  "tag": "${{ github.ref_name }}",
68
- "name": "${{ github.event.repository.name }}-${{ github.ref_name }}", # noqa: E501
59
+ "name": self.get_repo_and_ref_name_formatted(),
69
60
  "body": "${{ steps.build_changelog.outputs.changelog }}",
70
61
  },
71
62
  },
72
63
  ],
73
64
  }
74
- },
75
- }
76
-
77
-
78
- def _release_config_is_correct() -> bool:
79
- """Check if the release workflow is correct."""
80
- config = load_release_workflow()
81
- return bool(config == _get_release_config())
82
-
83
-
84
- def _add_release_workflow() -> None:
85
- """Add the release workflow."""
86
- if _release_config_is_correct():
87
- return
88
- config = _get_release_config()
89
- dump_release_workflow(config)
65
+ }
@@ -5,7 +5,7 @@ including getting the length of an iterable with a default value.
5
5
  These utilities help with iterable operations and manipulations.
6
6
  """
7
7
 
8
- from collections.abc import Iterable
8
+ from collections.abc import Callable, Iterable
9
9
  from typing import Any
10
10
 
11
11
 
@@ -27,3 +27,61 @@ def get_len_with_default(iterable: Iterable[Any], default: int | None = None) ->
27
27
  msg = "Can't get length of iterable and no default value provided"
28
28
  raise TypeError(msg) from e
29
29
  return default
30
+
31
+
32
+ def nested_structure_is_subset(
33
+ subset: dict[Any, Any] | list[Any] | Any,
34
+ superset: dict[Any, Any] | list[Any] | Any,
35
+ on_false_dict_action: Callable[[dict[Any, Any], dict[Any, Any], Any], Any]
36
+ | None = None,
37
+ on_false_list_action: Callable[[list[Any], list[Any], int], Any] | None = None,
38
+ ) -> bool:
39
+ """Check if a dictionary is a nested subset of another dictionary.
40
+
41
+ Args:
42
+ subset: Dictionary to check
43
+ superset: Dictionary to check against
44
+ on_false_dict_action: Action to take on each false dict comparison
45
+ must return a bool to indicate if after action is still false
46
+ on_false_list_action: Action to take on each false list comparison
47
+ must return a bool to indicate if after action is still false
48
+
49
+ Each value of a key must be equal to the value of the same key in the superset.
50
+ If the value is dictionary, the function is called recursively.
51
+ If the value is list, each item must be in the list of the same key in the superset.
52
+ The order in lists matters.
53
+
54
+ Returns:
55
+ True if subset is a nested subset of superset, False otherwise
56
+ """
57
+ if isinstance(subset, dict) and isinstance(superset, dict):
58
+ iterable: Iterable[tuple[Any, Any]] = subset.items()
59
+ on_false_action: Callable[[Any, Any, Any], Any] | None = on_false_dict_action
60
+
61
+ def get_actual(key_or_index: Any) -> Any:
62
+ """Get actual value from superset."""
63
+ return superset.get(key_or_index)
64
+
65
+ elif isinstance(subset, list) and isinstance(superset, list):
66
+ iterable = enumerate(subset)
67
+ on_false_action = on_false_list_action
68
+
69
+ def get_actual(key_or_index: Any) -> Any:
70
+ """Get actual value from superset."""
71
+ return superset[key_or_index] if key_or_index < len(superset) else None
72
+ else:
73
+ return (subset == superset) or (subset is Any)
74
+
75
+ all_good = True
76
+ for key_or_index, value in iterable:
77
+ actual_value = get_actual(key_or_index)
78
+ if not nested_structure_is_subset(
79
+ value, actual_value, on_false_dict_action, on_false_list_action
80
+ ):
81
+ fixed = False
82
+ if on_false_action is not None:
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
87
+ return all_good
@@ -31,7 +31,10 @@ def get_all_methods_from_cls(
31
31
  A list of callable methods from the class
32
32
 
33
33
  """
34
- from winipedia_utils.modules.module import get_def_line, get_module_of_obj
34
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
35
+ get_def_line,
36
+ get_module_of_obj,
37
+ )
35
38
 
36
39
  methods = [
37
40
  (method, name) for name, method in inspect.getmembers(class_) if is_func(method)
@@ -63,7 +66,10 @@ def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
63
66
  A list of class types defined in the module
64
67
 
65
68
  """
66
- from winipedia_utils.modules.module import get_def_line, get_module_of_obj
69
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
70
+ get_def_line,
71
+ get_module_of_obj,
72
+ )
67
73
 
68
74
  if isinstance(module, str):
69
75
  module = import_module(module)
@@ -79,7 +85,9 @@ def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
79
85
  return sorted(classes, key=get_def_line)
80
86
 
81
87
 
82
- def get_all_subclasses(cls: type) -> list[type]:
88
+ def get_all_subclasses(
89
+ cls: type, load_package_before: ModuleType | None = None
90
+ ) -> set[type]:
83
91
  """Get all subclasses of a class.
84
92
 
85
93
  Retrieves all classes that are subclasses of the specified class.
@@ -87,18 +95,37 @@ def get_all_subclasses(cls: type) -> list[type]:
87
95
 
88
96
  Args:
89
97
  cls: The class to find subclasses of
98
+ load_package_before: If provided,
99
+ walks the package before loading the subclasses
100
+ This is useful when the subclasses are defined in other modules that need
101
+ to be imported before they can be found by __subclasses__
90
102
 
91
103
  Returns:
92
104
  A list of subclasses of the given class
93
105
 
94
106
  """
107
+ from winipedia_utils.modules.package import ( # noqa: PLC0415 # avoid circular import
108
+ walk_package,
109
+ )
110
+
111
+ if load_package_before:
112
+ _ = list(walk_package(load_package_before))
95
113
  subclasses_set = set(cls.__subclasses__())
96
114
  for subclass in cls.__subclasses__():
97
115
  subclasses_set.update(get_all_subclasses(subclass))
98
- return list(subclasses_set)
99
-
100
-
101
- def get_all_nonabstract_subclasses(cls: type) -> list[type]:
116
+ if load_package_before is not None:
117
+ # remove all not in the package
118
+ subclasses_set = {
119
+ subclass
120
+ for subclass in subclasses_set
121
+ if subclass.__module__.startswith(load_package_before.__name__)
122
+ }
123
+ return subclasses_set
124
+
125
+
126
+ def get_all_nonabstract_subclasses(
127
+ cls: type, load_package_before: ModuleType | None = None
128
+ ) -> set[type]:
102
129
  """Get all non-abstract subclasses of a class.
103
130
 
104
131
  Retrieves all classes that are subclasses of the specified class,
@@ -107,13 +134,36 @@ def get_all_nonabstract_subclasses(cls: type) -> list[type]:
107
134
 
108
135
  Args:
109
136
  cls: The class to find subclasses of
137
+ load_package_before: If provided,
138
+ walks the package before loading the subclasses
139
+ This is useful when the subclasses are defined in other modules that need
140
+ to be imported before they can be found by __subclasses__
110
141
 
111
142
  Returns:
112
143
  A list of non-abstract subclasses of the given class
113
144
 
114
145
  """
115
- return [
146
+ return {
116
147
  subclass
117
- for subclass in get_all_subclasses(cls)
148
+ for subclass in get_all_subclasses(cls, load_package_before=load_package_before)
118
149
  if not inspect.isabstract(subclass)
119
- ]
150
+ }
151
+
152
+
153
+ def init_all_nonabstract_subclasses(
154
+ cls: type, load_package_before: ModuleType | None = None
155
+ ) -> None:
156
+ """Initialize all non-abstract subclasses of a class.
157
+
158
+ Args:
159
+ cls: The class to find subclasses of
160
+ load_package_before: If provided,
161
+ walks the package before loading the subclasses
162
+ This is useful when the subclasses are defined in other modules that need
163
+ to be imported before they can be found by __subclasses__
164
+
165
+ """
166
+ for subclass in get_all_nonabstract_subclasses(
167
+ cls, load_package_before=load_package_before
168
+ ):
169
+ subclass()
@@ -71,7 +71,10 @@ def get_all_functions_from_module(module: ModuleType | str) -> list[Callable[...
71
71
  A list of callable functions defined in the module
72
72
 
73
73
  """
74
- from winipedia_utils.modules.module import get_def_line, get_module_of_obj
74
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
75
+ get_def_line,
76
+ get_module_of_obj,
77
+ )
75
78
 
76
79
  if isinstance(module, str):
77
80
  module = import_module(module)
@@ -99,3 +102,17 @@ def unwrap_method(method: Any) -> Callable[..., Any] | Any:
99
102
  if isinstance(method, property):
100
103
  method = method.fget
101
104
  return inspect.unwrap(method)
105
+
106
+
107
+ def is_abstractmethod(method: Any) -> bool:
108
+ """Check if a method is an abstract method.
109
+
110
+ Args:
111
+ method: The method to check
112
+
113
+ Returns:
114
+ True if the method is an abstract method, False otherwise
115
+
116
+ """
117
+ method = unwrap_method(method)
118
+ return getattr(method, "__isabstractmethod__", False)
@@ -20,8 +20,8 @@ from types import ModuleType
20
20
  from setuptools import find_namespace_packages as _find_namespace_packages
21
21
  from setuptools import find_packages as _find_packages
22
22
 
23
+ from winipedia_utils.git.gitignore.config import GitIgnoreConfigFile
23
24
  from winipedia_utils.git.gitignore.gitignore import (
24
- load_gitignore,
25
25
  walk_os_skipping_gitignore_patterns,
26
26
  )
27
27
  from winipedia_utils.logging.logger import get_logger
@@ -44,7 +44,9 @@ def get_src_package() -> ModuleType:
44
44
  if only the test package exists
45
45
 
46
46
  """
47
- from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
47
+ from winipedia_utils.testing.convention import ( # noqa: PLC0415 # avoid circular import
48
+ TESTS_PACKAGE_NAME,
49
+ )
48
50
 
49
51
  packages = find_packages_as_modules(depth=0)
50
52
  return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
@@ -181,7 +183,7 @@ def find_packages(
181
183
 
182
184
  """
183
185
  if exclude is None:
184
- exclude = load_gitignore()
186
+ exclude = GitIgnoreConfigFile.load_static()[GitIgnoreConfigFile.IGNORE_KEY]
185
187
  exclude = [
186
188
  p.replace("/", ".").removesuffix(".") for p in exclude if p.endswith("/")
187
189
  ]
@@ -280,7 +282,9 @@ def make_init_modules_for_package(path: str | Path | ModuleType) -> None:
280
282
  from get_default_init_module_content.
281
283
 
282
284
  """
283
- from winipedia_utils.modules.module import to_path
285
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
286
+ to_path,
287
+ )
284
288
 
285
289
  path = to_path(path, is_package=True)
286
290
 
@@ -305,7 +309,10 @@ def make_init_module(path: str | Path) -> None:
305
309
  Creates parent directories if they don't exist.
306
310
 
307
311
  """
308
- from winipedia_utils.modules.module import get_default_init_module_content, to_path
312
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
313
+ get_default_init_module_content,
314
+ to_path,
315
+ )
309
316
 
310
317
  path = to_path(path, is_package=True)
311
318
 
@@ -338,7 +345,7 @@ def copy_package(
338
345
  with_file_content (bool, optional): copies the content of the files.
339
346
 
340
347
  """
341
- from winipedia_utils.modules.module import (
348
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
342
349
  create_module,
343
350
  get_isolated_obj_name,
344
351
  get_module_content_as_str,
@@ -368,7 +375,9 @@ def get_main_package() -> ModuleType:
368
375
 
369
376
  Even when this package is installed as a module.
370
377
  """
371
- from winipedia_utils.modules.module import to_module_name
378
+ from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
379
+ to_module_name,
380
+ )
372
381
 
373
382
  main = sys.modules.get("__main__")
374
383
  if main is None: