winipedia-utils 0.4.53__py3-none-any.whl → 0.5.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.

@@ -74,36 +74,46 @@ class Workflow(YamlConfigFile):
74
74
  "on": cls.get_workflow_triggers(),
75
75
  "permissions": cls.get_permissions(),
76
76
  "run-name": cls.get_run_name(),
77
+ "defaults": {"run": {"shell": "bash"}},
77
78
  "jobs": cls.get_jobs(),
78
79
  }
79
80
 
80
81
  @classmethod
81
- def get_standard_job(
82
+ def get_standard_job( # noqa: PLR0913
82
83
  cls,
83
84
  name: str | None = None,
84
85
  runs_on: str = "ubuntu-latest",
86
+ strategy: dict[str, Any] | None = None,
85
87
  permissions: dict[str, Any] | None = None,
86
88
  if_condition: str | None = None,
87
89
  steps: list[dict[str, Any]] | None = None,
90
+ needs: list[str] | None = None,
88
91
  ) -> dict[str, Any]:
89
92
  """Get a standard job."""
93
+ job: dict[str, Any] = {}
90
94
  if name is None:
91
95
  name = cls.get_filename()
96
+ job[name] = {}
97
+ job_config = job[name]
92
98
 
93
- if steps is None:
94
- steps = []
95
-
96
- job: dict[str, Any] = {
97
- name: {
98
- "runs-on": runs_on,
99
- "steps": steps,
100
- }
101
- }
102
99
  if permissions is not None:
103
- job[name]["permissions"] = permissions
100
+ job_config["permissions"] = permissions
101
+
102
+ if strategy is not None:
103
+ job_config["strategy"] = strategy
104
+
105
+ job_config["runs-on"] = runs_on
106
+
107
+ if needs is not None:
108
+ job_config["needs"] = needs
104
109
 
105
110
  if if_condition is not None:
106
- job[name]["if"] = if_condition
111
+ job_config["if"] = if_condition
112
+
113
+ if steps is None:
114
+ steps = []
115
+ job_config["steps"] = steps
116
+
107
117
  return job
108
118
 
109
119
  @classmethod
@@ -151,8 +161,9 @@ class Workflow(YamlConfigFile):
151
161
  fetch_depth: int | None = None,
152
162
  configure_pipy_token: bool = False,
153
163
  force_main_head: bool = False,
154
- token: bool = False,
164
+ repo_token: bool = False,
155
165
  with_keyring: bool = False,
166
+ strategy_matrix: bool = False,
156
167
  ) -> list[dict[str, Any]]:
157
168
  """Get the poetry steps.
158
169
 
@@ -163,13 +174,15 @@ class Workflow(YamlConfigFile):
163
174
  force_main_head: Whether to exit if the running branch or current commit is not
164
175
  equal to the most recent commit on main. This is useful for workflows that
165
176
  should only run on main.
166
- token: Whether to use the repository token.
177
+ repo_token: Whether to use the repository token.
167
178
  with_keyring: Whether to setup the keyring.
179
+ strategy_matrix: Whether to use the strategy matrix python-version.
180
+ This is useful for jobs that use a matrix.
168
181
 
169
182
  Returns:
170
183
  The poetry steps.
171
184
  """
172
- steps = [cls.get_checkout_step(fetch_depth, token=token)]
185
+ steps = [cls.get_checkout_step(fetch_depth, token=repo_token)]
173
186
  if force_main_head:
174
187
  # exit with code 1 if the running branch is not main
175
188
  steps.append(
@@ -184,21 +197,33 @@ class Workflow(YamlConfigFile):
184
197
  "name": "Setup Python",
185
198
  "uses": "actions/setup-python@main",
186
199
  "with": {
187
- "python-version": PyprojectConfigFile.get_latest_possible_python_version() # noqa: E501
200
+ # get latest if strategy matrix python-version is not set
201
+ "python-version": "${{ matrix.python-version }}"
202
+ if strategy_matrix
203
+ else str(PyprojectConfigFile.get_latest_possible_python_version())
188
204
  },
189
205
  }
190
206
  )
191
207
  steps.append(
192
208
  {
193
- "name": "Install Poetry",
194
- "run": "curl -sSL https://install.python-poetry.org | python3 -",
209
+ "name": "Setup Poetry",
210
+ "uses": "snok/install-poetry@main",
195
211
  }
196
212
  )
197
213
 
214
+ if strategy_matrix:
215
+ steps.append(
216
+ {
217
+ # windows needs this step to find poetry
218
+ "name": "Add Poetry to PATH",
219
+ "run": "echo 'C:/Users/runneradmin/.local/bin' >> $GITHUB_PATH",
220
+ }
221
+ )
222
+
198
223
  if configure_pipy_token:
199
224
  steps.append(
200
225
  {
201
- "name": "Configure Poetry",
226
+ "name": "Configure Poetry with PyPI Token",
202
227
  "run": "poetry config pypi-token.pypi ${{ secrets.PYPI_TOKEN }}",
203
228
  }
204
229
  )
@@ -215,6 +240,13 @@ class Workflow(YamlConfigFile):
215
240
  def get_release_steps(cls) -> list[dict[str, Any]]:
216
241
  """Get the release steps."""
217
242
  return [
243
+ *cls.get_poetry_setup_steps(
244
+ install_dependencies=True,
245
+ repo_token=True,
246
+ ),
247
+ cls.get_pre_commit_step(),
248
+ cls.get_commit_step(),
249
+ cls.get_extract_version_step(),
218
250
  {
219
251
  "name": "Tag and Push",
220
252
  "run": f"git push && git tag {cls.get_version()} && git push origin {cls.get_version()}", # noqa: E501
@@ -260,6 +292,9 @@ class Workflow(YamlConfigFile):
260
292
  """
261
293
  step: dict[str, Any] = {
262
294
  "name": "Run Hooks",
295
+ # poetry run is necessary although the hook itself uses poetry run as well.
296
+ # not sure why, but on windows-latest the venv is not continued to the hooks
297
+ # and if you leave it here then pre-commit command is not found
263
298
  "run": "poetry run pre-commit run --all-files --verbose",
264
299
  }
265
300
  if get_src_package() == winipedia_utils:
@@ -287,7 +322,7 @@ class Workflow(YamlConfigFile):
287
322
  """Get the setup keyring step."""
288
323
  return {
289
324
  "name": "Setup CI keyring",
290
- "run": """poetry run pip install keyrings.alt && poetry run python -c "import keyring; from keyrings.alt.file import PlaintextKeyring; keyring.set_keyring(PlaintextKeyring()); keyring.set_password('video_vault','ci_user','ci-secret-token'); print('Keyring OK:', keyring.get_password('video_vault','ci_user'))" """, # noqa: E501
325
+ "run": 'poetry run pip install keyrings.alt && poetry run python -c "import keyring; from keyrings.alt.file import PlaintextKeyring; keyring.set_keyring(PlaintextKeyring());"', # noqa: E501
291
326
  }
292
327
 
293
328
  @classmethod
@@ -6,6 +6,7 @@ This workflow is used to run tests on pull requests.
6
6
  from typing import Any
7
7
 
8
8
  from winipedia_utils.git.github.workflows.base.base import Workflow
9
+ from winipedia_utils.projects.poetry.config import PyprojectConfigFile
9
10
 
10
11
 
11
12
  class HealthCheckWorkflow(Workflow):
@@ -39,20 +40,41 @@ class HealthCheckWorkflow(Workflow):
39
40
  @classmethod
40
41
  def get_jobs(cls) -> dict[str, Any]:
41
42
  """Get the workflow jobs."""
43
+ matrix_job_name = f"{cls.get_filename()}_matrix"
42
44
  return {
43
45
  **cls.get_standard_job(
46
+ name=matrix_job_name,
47
+ runs_on="${{ matrix.os }}",
48
+ strategy={
49
+ "matrix": {
50
+ "os": ["ubuntu-latest", "windows-latest", "macos-latest"],
51
+ "python-version": [
52
+ str(v)
53
+ for v in PyprojectConfigFile.get_supported_python_versions()
54
+ ],
55
+ },
56
+ "fail-fast": True,
57
+ },
44
58
  steps=[
45
59
  *(
46
60
  cls.get_poetry_setup_steps(
47
61
  install_dependencies=True,
48
- token=True,
62
+ repo_token=True,
49
63
  with_keyring=True,
64
+ strategy_matrix=True,
50
65
  )
51
66
  ),
52
67
  cls.get_protect_repository_step(),
53
68
  cls.get_pre_commit_step(),
54
- cls.get_commit_step(),
55
- cls.get_extract_version_step(),
69
+ ],
70
+ ),
71
+ **cls.get_standard_job(
72
+ needs=[matrix_job_name],
73
+ steps=[
74
+ {
75
+ "name": "Aggregate Matrix Results",
76
+ "run": "echo 'Aggregating matrix results into one job.'",
77
+ }
56
78
  ],
57
79
  ),
58
80
  }
@@ -40,6 +40,12 @@ class ReleaseWorkflow(HealthCheckWorkflow):
40
40
  @classmethod
41
41
  def get_jobs(cls) -> dict[str, Any]:
42
42
  """Get the workflow jobs."""
43
- steps = super().get_jobs()
44
- steps[cls.get_filename()]["steps"].extend(cls.get_release_steps())
45
- return steps
43
+ jobs = HealthCheckWorkflow.get_jobs()
44
+ release_job = cls.get_standard_job(
45
+ needs=[HealthCheckWorkflow.get_filename()],
46
+ steps=[
47
+ *cls.get_release_steps(),
48
+ ],
49
+ )
50
+ jobs.update(release_job)
51
+ return jobs
@@ -38,7 +38,7 @@ class PreCommitConfigConfigFile(YamlConfigFile):
38
38
  {
39
39
  "id": hook_name,
40
40
  "name": hook_name,
41
- "entry": cls.get_python_setup_script(),
41
+ "entry": cls.get_poetry_run_setup_script(),
42
42
  "language": "system",
43
43
  "always_run": True,
44
44
  "pass_filenames": False,
@@ -7,15 +7,13 @@ strings are the arguments to the command. These funcs will be called by
7
7
  run_hooks.py, which will pass the returned list to subprocess.run().
8
8
  """
9
9
 
10
- from pathlib import Path
11
-
12
10
  from winipedia_utils.projects.poetry.poetry import (
13
11
  POETRY_ARG,
14
- get_run_python_module_args,
12
+ get_poetry_run_module_args,
15
13
  )
16
14
 
17
15
 
18
- def patch_version() -> list[str | Path]:
16
+ def patch_version() -> list[str]:
19
17
  """Patch the version in pyproject.toml.
20
18
 
21
19
  This function returns the input for subprocess.run() to patch the version
@@ -24,7 +22,7 @@ def patch_version() -> list[str | Path]:
24
22
  return [POETRY_ARG, "version", "patch"]
25
23
 
26
24
 
27
- def add_version_patch_to_git() -> list[str | Path]:
25
+ def add_version_patch_to_git() -> list[str]:
28
26
  """Add the version patch to git.
29
27
 
30
28
  This function returns the input for subprocess.run() to add the version
@@ -33,7 +31,7 @@ def add_version_patch_to_git() -> list[str | Path]:
33
31
  return ["git", "add", "pyproject.toml"]
34
32
 
35
33
 
36
- def update_package_manager() -> list[str | Path]:
34
+ def update_package_manager() -> list[str]:
37
35
  """Update the package manager.
38
36
 
39
37
  This function returns the input for subprocess.run() to update the package
@@ -42,7 +40,7 @@ def update_package_manager() -> list[str | Path]:
42
40
  return [POETRY_ARG, "self", "update"]
43
41
 
44
42
 
45
- def install_dependencies_with_dev() -> list[str | Path]:
43
+ def install_dependencies_with_dev() -> list[str]:
46
44
  """Install all dependencies.
47
45
 
48
46
  This function returns the input for subprocess.run() to install all dependencies.
@@ -50,7 +48,7 @@ def install_dependencies_with_dev() -> list[str | Path]:
50
48
  return [POETRY_ARG, "install", "--with", "dev"]
51
49
 
52
50
 
53
- def update_dependencies_with_dev() -> list[str | Path]:
51
+ def update_dependencies_with_dev() -> list[str]:
54
52
  """Update all dependencies.
55
53
 
56
54
  This function returns the input for subprocess.run() to update all dependencies.
@@ -58,7 +56,7 @@ def update_dependencies_with_dev() -> list[str | Path]:
58
56
  return [POETRY_ARG, "update", "--with", "dev"]
59
57
 
60
58
 
61
- def add_updates_to_git() -> list[str | Path]:
59
+ def add_updates_to_git() -> list[str]:
62
60
  """Add the updated dependencies to git.
63
61
 
64
62
  This function returns the input for subprocess.run() to add the updated
@@ -67,7 +65,7 @@ def add_updates_to_git() -> list[str | Path]:
67
65
  return ["git", "add", "pyproject.toml"]
68
66
 
69
67
 
70
- def lock_dependencies() -> list[str | Path]:
68
+ def lock_dependencies() -> list[str]:
71
69
  """Lock the dependencies.
72
70
 
73
71
  This function returns the input for subprocess.run() to lock the dependencies.
@@ -75,7 +73,7 @@ def lock_dependencies() -> list[str | Path]:
75
73
  return [POETRY_ARG, "lock"]
76
74
 
77
75
 
78
- def add_lock_file_to_git() -> list[str | Path]:
76
+ def add_lock_file_to_git() -> list[str]:
79
77
  """Add the lock file to git.
80
78
 
81
79
  This function returns the input for subprocess.run() to add the lock file
@@ -84,7 +82,7 @@ def add_lock_file_to_git() -> list[str | Path]:
84
82
  return ["git", "add", "poetry.lock"]
85
83
 
86
84
 
87
- def check_package_manager_configs() -> list[str | Path]:
85
+ def check_package_manager_configs() -> list[str]:
88
86
  """Check that poetry.lock and pyproject.toml is up to date.
89
87
 
90
88
  This function returns the input for subprocess.run() to check that poetry.lock
@@ -93,7 +91,7 @@ def check_package_manager_configs() -> list[str | Path]:
93
91
  return [POETRY_ARG, "check", "--strict"]
94
92
 
95
93
 
96
- def create_missing_tests() -> list[str | Path]:
94
+ def create_missing_tests() -> list[str]:
97
95
  """Create all tests for the project.
98
96
 
99
97
  This function returns the input for subprocess.run() to create all tests.
@@ -102,10 +100,10 @@ def create_missing_tests() -> list[str | Path]:
102
100
  create_tests,
103
101
  )
104
102
 
105
- return [*get_run_python_module_args(create_tests)]
103
+ return get_poetry_run_module_args(create_tests)
106
104
 
107
105
 
108
- def lint_code() -> list[str | Path]:
106
+ def lint_code() -> list[str]:
109
107
  """Check the code.
110
108
 
111
109
  This function returns the input for subprocess.run() to lint the code.
@@ -114,7 +112,7 @@ def lint_code() -> list[str | Path]:
114
112
  return ["ruff", "check", "--fix"]
115
113
 
116
114
 
117
- def format_code() -> list[str | Path]:
115
+ def format_code() -> list[str]:
118
116
  """Format the code.
119
117
 
120
118
  This function calls ruff format to format the code.
@@ -122,7 +120,7 @@ def format_code() -> list[str | Path]:
122
120
  return ["ruff", "format"]
123
121
 
124
122
 
125
- def check_static_types() -> list[str | Path]:
123
+ def check_static_types() -> list[str]:
126
124
  """Check the types.
127
125
 
128
126
  This function returns the input for subprocess.run() to check the static types.
@@ -130,7 +128,7 @@ def check_static_types() -> list[str | Path]:
130
128
  return ["mypy", "--exclude-gitignore"]
131
129
 
132
130
 
133
- def check_security() -> list[str | Path]:
131
+ def check_security() -> list[str]:
134
132
  """Check the security of the code.
135
133
 
136
134
  This function returns the input for subprocess.run() to check the security of
@@ -139,7 +137,7 @@ def check_security() -> list[str | Path]:
139
137
  return ["bandit", "-c", "pyproject.toml", "-r", "."]
140
138
 
141
139
 
142
- def run_tests() -> list[str | Path]:
140
+ def run_tests() -> list[str]:
143
141
  """Run the tests.
144
142
 
145
143
  This function returns the input for subprocess.run() to run all tests.
@@ -28,10 +28,22 @@ def run_hooks() -> None:
28
28
  passed = result.returncode == 0
29
29
 
30
30
  log_method = logger.info
31
- passed_str = (f"{GREEN}PASSED" if passed else f"{RED}FAILED") + RESET
31
+ status_str = (f"{GREEN}PASSED" if passed else f"{RED}FAILED") + RESET
32
32
  if not passed:
33
33
  log_method = logger.error
34
- passed_str += f"\n{result.stdout}"
34
+ status_str += f"""
35
+ ---------------------------------------------------------------------------------------------
36
+ Stdout:
37
+
38
+ {result.stdout}
39
+
40
+ ---------------------------------------------------------------------------------------------
41
+ Stderr:
42
+
43
+ {result.stderr}
44
+
45
+ ---------------------------------------------------------------------------------------------
46
+ """
35
47
  exit_code = 1
36
48
  # make the dashes always the same lentgth by adjusting to len of hook name
37
49
  num_dashes = 50 - len(hook_func.__name__)
@@ -39,7 +51,7 @@ def run_hooks() -> None:
39
51
  "Hook %s -%s> %s",
40
52
  hook_func.__name__,
41
53
  "-" * num_dashes,
42
- passed_str,
54
+ status_str,
43
55
  )
44
56
 
45
57
  if exit_code != 0:
@@ -13,10 +13,14 @@ from types import ModuleType
13
13
  from typing import Any
14
14
 
15
15
  from winipedia_utils.modules.function import is_func
16
+ from winipedia_utils.modules.inspection import get_def_line, get_obj_members
16
17
 
17
18
 
18
19
  def get_all_methods_from_cls(
19
- class_: type, *, exclude_parent_methods: bool = False
20
+ class_: type,
21
+ *,
22
+ exclude_parent_methods: bool = False,
23
+ include_annotate: bool = False,
20
24
  ) -> list[Callable[..., Any]]:
21
25
  """Get all methods from a class.
22
26
 
@@ -27,17 +31,21 @@ def get_all_methods_from_cls(
27
31
  class_: The class to extract methods from
28
32
  exclude_parent_methods: If True, only include methods defined in this class,
29
33
  excluding those inherited from parent classes
34
+ include_annotate: If False, exclude __annotate__ method
35
+ introduced in Python 3.14, defaults to False
36
+
30
37
  Returns:
31
38
  A list of callable methods from the class
32
39
 
33
40
  """
34
41
  from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
35
- get_def_line,
36
42
  get_module_of_obj,
37
43
  )
38
44
 
39
45
  methods = [
40
- (method, name) for name, method in inspect.getmembers(class_) if is_func(method)
46
+ (method, name)
47
+ for name, method in get_obj_members(class_, include_annotate=include_annotate)
48
+ if is_func(method)
41
49
  ]
42
50
 
43
51
  if exclude_parent_methods:
@@ -67,7 +75,6 @@ def get_all_cls_from_module(module: ModuleType | str) -> list[type]:
67
75
 
68
76
  """
69
77
  from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
70
- get_def_line,
71
78
  get_module_of_obj,
72
79
  )
73
80
 
@@ -12,6 +12,8 @@ from importlib import import_module
12
12
  from types import ModuleType
13
13
  from typing import Any
14
14
 
15
+ from winipedia_utils.modules.inspection import get_def_line, get_obj_members
16
+
15
17
 
16
18
  def is_func_or_method(obj: Any) -> bool:
17
19
  """Return True if *obj* is a function or method.
@@ -57,7 +59,9 @@ def is_func(obj: Any) -> bool:
57
59
  return is_func_or_method(unwrapped)
58
60
 
59
61
 
60
- def get_all_functions_from_module(module: ModuleType | str) -> list[Callable[..., Any]]:
62
+ def get_all_functions_from_module(
63
+ module: ModuleType | str, *, include_annotate: bool = False
64
+ ) -> list[Callable[..., Any]]:
61
65
  """Get all functions defined in a module.
62
66
 
63
67
  Retrieves all function objects that are defined directly in the specified module,
@@ -66,13 +70,14 @@ def get_all_functions_from_module(module: ModuleType | str) -> list[Callable[...
66
70
 
67
71
  Args:
68
72
  module: The module to extract functions from
73
+ include_annotate: If False, exclude __annotate__ method
74
+ introduced in Python 3.14, defaults to False
69
75
 
70
76
  Returns:
71
77
  A list of callable functions defined in the module
72
78
 
73
79
  """
74
80
  from winipedia_utils.modules.module import ( # noqa: PLC0415 # avoid circular import
75
- get_def_line,
76
81
  get_module_of_obj,
77
82
  )
78
83
 
@@ -80,7 +85,8 @@ def get_all_functions_from_module(module: ModuleType | str) -> list[Callable[...
80
85
  module = import_module(module)
81
86
  funcs = [
82
87
  func
83
- for _name, func in inspect.getmembers(module, is_func)
88
+ for _name, func in get_obj_members(module, include_annotate=include_annotate)
89
+ if is_func(func)
84
90
  if get_module_of_obj(func).__name__ == module.__name__
85
91
  ]
86
92
  # sort by definition order
@@ -0,0 +1,56 @@
1
+ """Inspection utilities for introspecting Python objects.
2
+
3
+ This module provides utility functions for inspecting Python objects,
4
+ including checking if an object is a function or method, and unwrapping
5
+ methods to their underlying functions.
6
+ """
7
+
8
+ import inspect
9
+ import sys
10
+ from collections.abc import Callable
11
+ from typing import Any, cast
12
+
13
+
14
+ def get_obj_members(
15
+ obj: Any, *, include_annotate: bool = False
16
+ ) -> list[tuple[str, Any]]:
17
+ """Get all members of an object."""
18
+ members = [(member, value) for member, value in inspect.getmembers(obj)]
19
+ if not include_annotate:
20
+ members = [
21
+ (member, value)
22
+ for member, value in members
23
+ if member not in ("__annotate__", "__annotate_func__")
24
+ ]
25
+ return members
26
+
27
+
28
+ def inside_frozen_bundle() -> bool:
29
+ """Return True if the code is running inside a frozen bundle."""
30
+ return getattr(sys, "frozen", False)
31
+
32
+
33
+ def get_def_line(obj: Any) -> int:
34
+ """Return the line number where a method-like object is defined."""
35
+ if isinstance(obj, property):
36
+ obj = obj.fget
37
+ unwrapped = inspect.unwrap(obj)
38
+ if hasattr(unwrapped, "__code__"):
39
+ return int(unwrapped.__code__.co_firstlineno)
40
+ # getsourcelines does not work if in a pyinstaller bundle or something
41
+ if inside_frozen_bundle():
42
+ return 0
43
+ return inspect.getsourcelines(unwrapped)[1]
44
+
45
+
46
+ def get_unwrapped_obj(obj: Any) -> Any:
47
+ """Return the unwrapped version of a method-like object."""
48
+ if isinstance(obj, property):
49
+ obj = obj.fget # get the getter function of the property
50
+ return inspect.unwrap(obj)
51
+
52
+
53
+ def get_qualname_of_obj(obj: Callable[..., Any] | type) -> str:
54
+ """Return the name of a method-like object."""
55
+ unwrapped = get_unwrapped_obj(obj)
56
+ return cast("str", unwrapped.__qualname__)
@@ -17,7 +17,7 @@ from collections.abc import Callable, Sequence
17
17
  from importlib import import_module
18
18
  from pathlib import Path
19
19
  from types import ModuleType
20
- from typing import Any, cast
20
+ from typing import Any
21
21
 
22
22
  from winipedia_utils.logging.logger import get_logger
23
23
  from winipedia_utils.modules.class_ import (
@@ -25,6 +25,7 @@ from winipedia_utils.modules.class_ import (
25
25
  get_all_methods_from_cls,
26
26
  )
27
27
  from winipedia_utils.modules.function import get_all_functions_from_module
28
+ from winipedia_utils.modules.inspection import get_qualname_of_obj, get_unwrapped_obj
28
29
  from winipedia_utils.modules.package import (
29
30
  get_modules_and_packages_from_package,
30
31
  make_dir_with_init_file,
@@ -329,24 +330,6 @@ def get_default_module_content() -> str:
329
330
  return '''"""module."""'''
330
331
 
331
332
 
332
- def inside_frozen_bundle() -> bool:
333
- """Return True if the code is running inside a frozen bundle."""
334
- return getattr(sys, "frozen", False)
335
-
336
-
337
- def get_def_line(obj: Any) -> int:
338
- """Return the line number where a method-like object is defined."""
339
- if isinstance(obj, property):
340
- obj = obj.fget
341
- unwrapped = inspect.unwrap(obj)
342
- if hasattr(unwrapped, "__code__"):
343
- return int(unwrapped.__code__.co_firstlineno)
344
- # getsourcelines does not work if in a pyinstaller bundle or something
345
- if inside_frozen_bundle():
346
- return 0
347
- return inspect.getsourcelines(unwrapped)[1]
348
-
349
-
350
333
  def get_module_of_obj(obj: Any, default: ModuleType | None = None) -> ModuleType:
351
334
  """Return the module name where a method-like object is defined.
352
335
 
@@ -368,19 +351,6 @@ def get_module_of_obj(obj: Any, default: ModuleType | None = None) -> ModuleType
368
351
  return module
369
352
 
370
353
 
371
- def get_qualname_of_obj(obj: Callable[..., Any] | type) -> str:
372
- """Return the name of a method-like object."""
373
- unwrapped = get_unwrapped_obj(obj)
374
- return cast("str", unwrapped.__qualname__)
375
-
376
-
377
- def get_unwrapped_obj(obj: Any) -> Any:
378
- """Return the unwrapped version of a method-like object."""
379
- if isinstance(obj, property):
380
- obj = obj.fget # get the getter function of the property
381
- return inspect.unwrap(obj)
382
-
383
-
384
354
  def get_executing_module() -> ModuleType:
385
355
  """Get the module where execution has started.
386
356
 
@@ -472,7 +472,8 @@ class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
472
472
  def parse_pkg_name_from_req(req: str) -> str | None:
473
473
  """Extract the bare dependency name from a requirement string."""
474
474
  # split on the first non alphanumeric character like >, <, =, etc.
475
- dep = re.split(r"[^a-zA-Z0-9]", req.strip())[0].strip()
475
+ # keep - and _ for names like winipedia-utils or winipedia_utils
476
+ dep = re.split(r"[^a-zA-Z0-9_-]", req.strip())[0].strip()
476
477
  return DependencyGraph.normalize_package_name(dep) if dep else None
477
478
 
478
479
  def get_all_depending_on(
@@ -1,8 +1,12 @@
1
1
  """Config utilities for poetry and pyproject.toml."""
2
2
 
3
+ from functools import cache
3
4
  from pathlib import Path
4
5
  from typing import Any, cast
5
6
 
7
+ import requests
8
+ from packaging.version import Version
9
+
6
10
  from winipedia_utils.modules.package import get_src_package, make_name_from_package
7
11
  from winipedia_utils.projects.poetry.poetry import VersionConstraint
8
12
  from winipedia_utils.testing.config import ExperimentConfigFile
@@ -135,25 +139,29 @@ class PyprojectConfigFile(TomlConfigFile):
135
139
  return cls.get_main_author()["name"]
136
140
 
137
141
  @classmethod
138
- def get_latest_possible_python_version(cls) -> str:
142
+ @cache
143
+ def fetch_latest_python_version(cls) -> Version:
144
+ """Fetch the latest python version from python.org."""
145
+ url = "https://endoflife.date/api/python.json"
146
+ resp = requests.get(url, timeout=10)
147
+ resp.raise_for_status()
148
+ data = resp.json()
149
+ # first element has metadata for latest stable
150
+ latest_version = data[0]["latest"]
151
+ return Version(latest_version)
152
+
153
+ @classmethod
154
+ def get_latest_possible_python_version(cls) -> Version:
139
155
  """Get the latest possible python version."""
140
156
  constraint = cls.load()["project"]["requires-python"]
141
157
  version_constraint = VersionConstraint(constraint)
142
- upper = version_constraint.get_upper_exclusive()
143
- if upper is None:
144
- return "3.x"
145
-
146
- # convert to inclusive
147
- if upper.micro != 0:
148
- micro = upper.micro - 1
149
- return f"{upper.major}.{upper.minor}" + (f".{micro}" if micro != 0 else "")
150
- if upper.minor != 0:
151
- minor = upper.minor - 1
152
- return f"{upper.major}" + (f".{minor}" if minor != 0 else "")
153
- return f"{upper.major - 1}.x"
158
+ version = version_constraint.get_upper_inclusive()
159
+ if version is None:
160
+ version = cls.fetch_latest_python_version()
161
+ return version
154
162
 
155
163
  @classmethod
156
- def get_first_supported_python_version(cls) -> str:
164
+ def get_first_supported_python_version(cls) -> Version:
157
165
  """Get the first supported python version."""
158
166
  constraint = cls.load()["project"]["requires-python"]
159
167
  version_constraint = VersionConstraint(constraint)
@@ -161,7 +169,16 @@ class PyprojectConfigFile(TomlConfigFile):
161
169
  if lower is None:
162
170
  msg = "Need a lower bound for python version"
163
171
  raise ValueError(msg)
164
- return str(lower)
172
+ return lower
173
+
174
+ @classmethod
175
+ def get_supported_python_versions(cls) -> list[Version]:
176
+ """Get all supported python versions."""
177
+ constraint = cls.load()["project"]["requires-python"]
178
+ version_constraint = VersionConstraint(constraint)
179
+ return version_constraint.get_version_range(
180
+ level="minor", upper_default=cls.fetch_latest_python_version()
181
+ )
165
182
 
166
183
 
167
184
  class TypedConfigFile(ConfigFile):
@@ -223,7 +240,9 @@ class DotPythonVersionConfigFile(ConfigFile):
223
240
  def get_configs(cls) -> dict[str, Any]:
224
241
  """Get the config."""
225
242
  return {
226
- cls.VERSION_KEY: PyprojectConfigFile.get_first_supported_python_version()
243
+ cls.VERSION_KEY: str(
244
+ PyprojectConfigFile.get_first_supported_python_version()
245
+ )
227
246
  }
228
247
 
229
248
  @classmethod
@@ -5,6 +5,7 @@ This module provides utility functions for working with Python projects
5
5
 
6
6
  from collections.abc import Iterable
7
7
  from types import ModuleType
8
+ from typing import Literal
8
9
 
9
10
  from packaging.specifiers import SpecifierSet
10
11
  from packaging.version import Version
@@ -35,6 +36,11 @@ def get_run_python_module_args(module: ModuleType) -> list[str]:
35
36
  return [*RUN_PYTHON_MODULE_ARGS, make_obj_importpath(module)]
36
37
 
37
38
 
39
+ def get_poetry_run_module_args(module: ModuleType) -> list[str]:
40
+ """Get the args to run a module."""
41
+ return [*POETRY_RUN_ARGS, *get_run_python_module_args(module)]
42
+
43
+
38
44
  def get_python_module_script(module: ModuleType) -> str:
39
45
  """Get the script to run a module."""
40
46
  return get_script_from_args(get_run_python_module_args(module))
@@ -42,7 +48,7 @@ def get_python_module_script(module: ModuleType) -> str:
42
48
 
43
49
  def get_poetry_run_module_script(module: ModuleType) -> str:
44
50
  """Get the script to run a module."""
45
- return get_script_from_args([*POETRY_RUN_ARGS, *get_run_python_module_args(module)])
51
+ return get_script_from_args(get_poetry_run_module_args(module))
46
52
 
47
53
 
48
54
  class VersionConstraint:
@@ -92,7 +98,9 @@ class VersionConstraint:
92
98
  max(self.lowers_inclusive) if self.lowers_inclusive else None
93
99
  )
94
100
 
95
- def get_lower_inclusive(self, default: str | None = None) -> Version | None:
101
+ def get_lower_inclusive(
102
+ self, default: str | Version | None = None
103
+ ) -> Version | None:
96
104
  """Get the minimum version.
97
105
 
98
106
  Is given inclusive. E.g. >=3.8, <3.12 -> 3.8
@@ -106,12 +114,15 @@ class VersionConstraint:
106
114
  Returns:
107
115
  The minimum version
108
116
  """
117
+ default = str(default) if default else None
109
118
  if self.lower_inclusive is None:
110
119
  return Version(default) if default else None
111
120
 
112
121
  return self.lower_inclusive
113
122
 
114
- def get_upper_exclusive(self, default: str | None = None) -> Version | None:
123
+ def get_upper_exclusive(
124
+ self, default: str | Version | None = None
125
+ ) -> Version | None:
115
126
  """Get the maximum version.
116
127
 
117
128
  Is given exclusive. E.g. >=3.8, <3.12 -> 3.12
@@ -123,7 +134,115 @@ class VersionConstraint:
123
134
  Returns:
124
135
  The maximum version
125
136
  """
137
+ default = str(default) if default else None
126
138
  if self.upper_exclusive is None:
127
139
  return Version(default) if default else None
128
140
 
129
141
  return self.upper_exclusive
142
+
143
+ def get_upper_inclusive(
144
+ self, default: str | Version | None = None
145
+ ) -> Version | None:
146
+ """Get the maximum version.
147
+
148
+ Is given inclusive. E.g. >=3.8, <3.12 -> 3.11
149
+ if >=3.8, <=3.12 -> 3.12
150
+
151
+ Args:
152
+ default: The default value to return if there is no maximum version
153
+
154
+ Returns:
155
+ The maximum version
156
+ """
157
+ # increment the default by 1 micro to make it exclusive
158
+ if default:
159
+ default = Version(str(default))
160
+ default = Version(f"{default.major}.{default.minor}.{default.micro + 1}")
161
+ upper_exclusive = self.get_upper_exclusive(default)
162
+ if upper_exclusive is None:
163
+ return None
164
+
165
+ if upper_exclusive.micro != 0:
166
+ return Version(
167
+ f"{upper_exclusive.major}.{upper_exclusive.minor}.{upper_exclusive.micro - 1}" # noqa: E501
168
+ )
169
+ if upper_exclusive.minor != 0:
170
+ return Version(f"{upper_exclusive.major}.{upper_exclusive.minor - 1}")
171
+ return Version(f"{upper_exclusive.major - 1}")
172
+
173
+ def get_version_range(
174
+ self,
175
+ level: Literal["major", "minor", "micro"] = "major",
176
+ lower_default: str | Version | None = None,
177
+ upper_default: str | Version | None = None,
178
+ ) -> list[Version]:
179
+ """Get the version range.
180
+
181
+ returns a range of versions according to the level
182
+
183
+ E.g. >=3.8, <3.12; level=major -> 3
184
+ >=3.8, <4.12; level=major -> 3, 4
185
+ E.g. >=3.8, <=3.12; level=minor -> 3.8, 3.9, 3.10, 3.11, 3.12
186
+ E.g. >=3.8.1, <=4.12.1; level=micro -> 3.8.1, 3.8.2, ... 4.12.1
187
+
188
+ Args:
189
+ level: The level of the version to return
190
+ lower_default: The default lower bound if none is specified
191
+ upper_default: The default upper bound if none is specified
192
+
193
+ Returns:
194
+ A list of versions
195
+ """
196
+ lower = self.get_lower_inclusive(lower_default)
197
+ upper = self.get_upper_inclusive(upper_default)
198
+
199
+ if lower is None or upper is None:
200
+ msg = "No lower or upper bound. Please specify default values."
201
+ raise ValueError(msg)
202
+
203
+ major_level, minor_level, micro_level = range(3)
204
+ level_int = {"major": major_level, "minor": minor_level, "micro": micro_level}[
205
+ level
206
+ ]
207
+ lower_as_list = [lower.major, lower.minor, lower.micro]
208
+ upper_as_list = [upper.major, upper.minor, upper.micro]
209
+
210
+ versions: list[list[int]] = []
211
+ for major in range(lower_as_list[major_level], upper_as_list[major_level] + 1):
212
+ version = [major]
213
+
214
+ minor_lower_og, minor_upper_og = (
215
+ lower_as_list[minor_level],
216
+ upper_as_list[minor_level],
217
+ )
218
+ diff = minor_upper_og - minor_lower_og
219
+ minor_lower = minor_lower_og if diff >= 0 else 0
220
+ minor_upper = minor_upper_og if diff >= 0 else minor_lower_og + abs(diff)
221
+ for minor in range(
222
+ minor_lower,
223
+ minor_upper + 1,
224
+ ):
225
+ # pop the minor if one already exists
226
+ if len(version) > minor_level:
227
+ version.pop()
228
+
229
+ version.append(minor)
230
+
231
+ micro_lower_og, micro_upper_og = (
232
+ lower_as_list[micro_level],
233
+ upper_as_list[micro_level],
234
+ )
235
+ diff = micro_upper_og - micro_lower_og
236
+ micro_lower = micro_lower_og if diff >= 0 else 0
237
+ micro_upper = (
238
+ micro_upper_og if diff >= 0 else micro_lower_og + abs(diff)
239
+ )
240
+ for micro in range(
241
+ micro_lower,
242
+ micro_upper + 1,
243
+ ):
244
+ version.append(micro)
245
+ versions.append(version[: level_int + 1])
246
+ version.pop()
247
+ version_versions = sorted({Version(".".join(map(str, v))) for v in versions})
248
+ return [v for v in version_versions if self.sset.contains(v)]
@@ -14,11 +14,11 @@ from winipedia_utils.modules.class_ import (
14
14
  get_all_methods_from_cls,
15
15
  )
16
16
  from winipedia_utils.modules.function import get_all_functions_from_module
17
+ from winipedia_utils.modules.inspection import get_qualname_of_obj
17
18
  from winipedia_utils.modules.module import (
18
19
  create_module,
19
20
  get_isolated_obj_name,
20
21
  get_module_content_as_str,
21
- get_qualname_of_obj,
22
22
  to_path,
23
23
  )
24
24
  from winipedia_utils.modules.package import (
@@ -13,7 +13,7 @@ from winipedia_utils.iterating.iterate import nested_structure_is_subset
13
13
  from winipedia_utils.modules.class_ import init_all_nonabstract_subclasses
14
14
  from winipedia_utils.modules.package import DependencyGraph, get_src_package
15
15
  from winipedia_utils.projects.poetry.poetry import (
16
- get_python_module_script,
16
+ get_poetry_run_module_script,
17
17
  )
18
18
  from winipedia_utils.text.string import split_on_uppercase
19
19
 
@@ -158,11 +158,11 @@ class ConfigFile(ABC):
158
158
  init_all_nonabstract_subclasses(cls, load_package_before=pkg)
159
159
 
160
160
  @staticmethod
161
- def get_python_setup_script() -> str:
161
+ def get_poetry_run_setup_script() -> str:
162
162
  """Get the poetry run setup script."""
163
163
  from winipedia_utils import setup # noqa: PLC0415 # avoid circular import
164
164
 
165
- return get_python_module_script(setup)
165
+ return get_poetry_run_module_script(setup)
166
166
 
167
167
 
168
168
  class YamlConfigFile(ConfigFile):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: winipedia-utils
3
- Version: 0.4.53
3
+ Version: 0.5.18
4
4
  Summary: A package with many utility functions
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -16,17 +16,17 @@ winipedia_utils/git/github/repo/protect.py,sha256=nOVjb5GVinGIClp7k9_qqgKnAl_gk1
16
16
  winipedia_utils/git/github/repo/repo.py,sha256=OqoOfqDhe_Iik71dNqi4h3fGrMno33hSjk0bpNg3eZk,7865
17
17
  winipedia_utils/git/github/workflows/__init__.py,sha256=BPdntTwFEyBMJ6MyT7gddPHswvRdH9tsRtfK72VSV7Y,57
18
18
  winipedia_utils/git/github/workflows/base/__init__.py,sha256=XHsbmjiaGom-KX-S3leCY9cJD3aP9p_0X6xYMcdkHBU,23
19
- winipedia_utils/git/github/workflows/base/base.py,sha256=4MAHU_oPqicwO7tZE0B99RK9ZKRMFIZhUYkWr1NrYqo,11405
20
- winipedia_utils/git/github/workflows/health_check.py,sha256=pjGZckvfPHuR6QepHia6_FKcwUQAXuhGGVaVCpLQNaY,1614
19
+ winipedia_utils/git/github/workflows/base/base.py,sha256=WowKcyrbDXwDs_GjJo1f3Vbfu2aWVhSqY-CTcknYl8w,12735
20
+ winipedia_utils/git/github/workflows/health_check.py,sha256=rSz3cV5xEgMIV-SaAjKBonucS_xqz1-t9FUDUbctRy8,2523
21
21
  winipedia_utils/git/github/workflows/publish.py,sha256=TPbSp5QH2vVl55UdqE_kjf1HIkdubcgqWlkLjWFX5EA,1378
22
- winipedia_utils/git/github/workflows/release.py,sha256=hnMT12J17LmFpPCTzva-lW95MOFDQ-1bAOrTVCxeaR8,1301
22
+ winipedia_utils/git/github/workflows/release.py,sha256=GkObB3LcH9J-L9dWhK5N3aowggGFu3kvmEFKHmILBu4,1456
23
23
  winipedia_utils/git/gitignore/__init__.py,sha256=k-2E26JaZPkF69UUOJkpQl8T_PudrC7EYCIOxwgIQVU,57
24
24
  winipedia_utils/git/gitignore/config.py,sha256=Oi1gAf2mbR7vxMi0zsAFpCGzDaLNDd5S2vXEmA3eKg4,2595
25
25
  winipedia_utils/git/gitignore/gitignore.py,sha256=uE2MdynWgQuTG-y2YLR0FU5_giSE7s_TqSVQ6vnNOf8,2419
26
26
  winipedia_utils/git/pre_commit/__init__.py,sha256=gFLVGQRmS6abgy5MfPQy_GZiF1_hGxuXtcOHX95WL-A,58
27
- winipedia_utils/git/pre_commit/config.py,sha256=UagrtEp_TSb0THjmXZpvtl7agekEVOWnMmRPdWJ4kf4,1830
28
- winipedia_utils/git/pre_commit/hooks.py,sha256=HP1byMnHk8DhaHnPjp3ioKpXIAo4_9FbrehHSg9_xfs,4264
29
- winipedia_utils/git/pre_commit/run_hooks.py,sha256=UIz1k3Hx5sB6LcdIJaPNWL-YDZzbkWd7NHCsC41nczQ,1495
27
+ winipedia_utils/git/pre_commit/config.py,sha256=6Qvb8hoSm7Dmt5KmHOGATuLGrQDWd2aRKtevWThePio,1834
28
+ winipedia_utils/git/pre_commit/hooks.py,sha256=d88F9Sgik3K6-lJpTio38_vx_ujL64BQ767xeBHHojY,4130
29
+ winipedia_utils/git/pre_commit/run_hooks.py,sha256=3D8LDkVZVBw6q4RM0KmqMiNc299rjIrtxoL0bLGP2MU,1817
30
30
  winipedia_utils/iterating/__init__.py,sha256=rlF9hzxbowq5yOfcXvOKOQdB-EQmfrislQpf659Zeu4,53
31
31
  winipedia_utils/iterating/iterate.py,sha256=k4U6qrnE4zij-GJhI5X0wM3pveSi8wtEsA1i8xQrfG0,3522
32
32
  winipedia_utils/logging/__init__.py,sha256=AMt1LwA_E7hexYjMpGzUempoyDdAF-dowWvq59wC5aM,51
@@ -34,10 +34,11 @@ winipedia_utils/logging/ansi.py,sha256=7Z-FITaUn5B1ZE0OUVARjNzuMui1V36iu4YTjlD6Q
34
34
  winipedia_utils/logging/config.py,sha256=sdC3GHMkJqlY-WwIsOMeTcy1bihthWV2LbvxLt9dhc4,2895
35
35
  winipedia_utils/logging/logger.py,sha256=NluvfRpY4SfJi6URjfV52l3cxeUYFMeCAJULK_PQXpQ,701
36
36
  winipedia_utils/modules/__init__.py,sha256=e3CFaC3FhK4ibknFOv1bqOZxA7XeVwmLqWX7oajUm78,51
37
- winipedia_utils/modules/class_.py,sha256=908MgZZzcJBPmlVmMPZCFssHnRzbQYOQlrF3GQhpWm4,5411
38
- winipedia_utils/modules/function.py,sha256=cjD6dXkZzhtCClUs4uiOLaDOURVASp64iwTwXmI3ICo,3217
39
- winipedia_utils/modules/module.py,sha256=s48epfMHKjEgEHRO7rGICIX0JaXppOfst2uT-jLQM6M,13766
40
- winipedia_utils/modules/package.py,sha256=9PMCsUzOAaJBGtqeMIYRTUyFG2DWdT_obg8a4FfCahI,17983
37
+ winipedia_utils/modules/class_.py,sha256=LhhDoXh664h1ILP2lfxlF6M95lFoFNJhsTozxCQ0Myo,5655
38
+ winipedia_utils/modules/function.py,sha256=CvMosbeL2wyAu5eT3vjAvArn9j7aUPh_X3fGoIzwWBk,3479
39
+ winipedia_utils/modules/inspection.py,sha256=XPSJfLN6WnNmbXn-6z9D01cmqmPS-HrTPy6-B6C34tw,1821
40
+ winipedia_utils/modules/module.py,sha256=spyg4yI46BLMyhCrSSXIUbY1fHFjcpmRCdKQRUJB7pQ,12792
41
+ winipedia_utils/modules/package.py,sha256=6Wb8Pu-SgRWGmKM2r1nhU-yftE7USSKWAuHQn2jRXlg,18058
41
42
  winipedia_utils/oop/__init__.py,sha256=wGjsVwLbTVEQWOfDJvN9nlvC-3NmAi8Doc2xIrm6e78,47
42
43
  winipedia_utils/oop/mixins/__init__.py,sha256=PDK-cJcdRUfDUCz36qQ5pmMW07G133WtN49OpmILGNI,54
43
44
  winipedia_utils/oop/mixins/meta.py,sha256=0G4CzzzCoeP1Eas3vWe-uxvB5n5ncyw7Wc-sI9zmEBc,11150
@@ -46,8 +47,8 @@ winipedia_utils/os/__init__.py,sha256=cBRq8hWhaWvYeC3cSBYL6Y70kM9COQWHj8vVxxSadI
46
47
  winipedia_utils/os/os.py,sha256=K_5FD1sC1h5aSdtqXAG0uq90sSweLYLkgkRPQS0Jfxg,1768
47
48
  winipedia_utils/projects/__init__.py,sha256=_iYHzUcTPmutpsExPDcMF9OQDgnz-kTSuWens9iP9bI,52
48
49
  winipedia_utils/projects/poetry/__init__.py,sha256=tbvV3wYd3H39hjjlKbF84Irj4hYgv1A7KWyXdCQzFro,59
49
- winipedia_utils/projects/poetry/config.py,sha256=SFFWtY5JkKDpe1VZNmZYKIj4RvrlAZCmVnC9wz0464I,7925
50
- winipedia_utils/projects/poetry/poetry.py,sha256=CM-MKuYSITRIS95qngtCpDpjQS6tCe6joZOM03yEFdU,4033
50
+ winipedia_utils/projects/poetry/config.py,sha256=D42eowQfkBCurmb-CenujFXudtt5j1joZk0kM29wujA,8570
51
+ winipedia_utils/projects/poetry/poetry.py,sha256=e22_ltYXXF_y6UQbCwRX0NiQieyALkxDFfTLc2U9TSA,8478
51
52
  winipedia_utils/projects/project.py,sha256=rirg4xCIOTI6w7cLWufAVHAix7FGicvaCd9OnZQP8dA,521
52
53
  winipedia_utils/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
53
54
  winipedia_utils/resources/__init__.py,sha256=XHsbmjiaGom-KX-S3leCY9cJD3aP9p_0X6xYMcdkHBU,23
@@ -69,7 +70,7 @@ winipedia_utils/testing/__init__.py,sha256=kXhB5xw02ec5xpcW_KV--9CBKdyCjnuR-NZzA
69
70
  winipedia_utils/testing/assertions.py,sha256=VoksKQKEXppY9e8Bk8d_WMAjRX5mDE8c5blBwzK6nWo,1494
70
71
  winipedia_utils/testing/config.py,sha256=Xtgu7Nsll6pIzHLUDsalzt-nPBNSNOs0JKCXWi5v36M,3508
71
72
  winipedia_utils/testing/convention.py,sha256=7vurpqS--awhN_FLSOviSKENGuFyY9Ejr1NKRm0MPsg,4833
72
- winipedia_utils/testing/create_tests.py,sha256=KJL23BtsnDsujUa9fwBA5AHo85vy61oxgRPSTztJT7k,9347
73
+ winipedia_utils/testing/create_tests.py,sha256=sr_j-0faeGWoIMFTdi8mLpkhUOqsgLdMkQ8MCHgF5j4,9389
73
74
  winipedia_utils/testing/fixtures.py,sha256=G8QIrZXndtud0uOk6PY3f8IIDoyX_ronogjeLAYGjrM,1033
74
75
  winipedia_utils/testing/skip.py,sha256=WpVk1J4DBISDfR6CXOKCEjP4MHy52DaGwPkmdDuM6Y0,486
75
76
  winipedia_utils/testing/tests/__init__.py,sha256=kL-1O6lAO5j4JPOqPdi3dHdbOQ_UXcgPFppj82HhrRU,57
@@ -86,9 +87,9 @@ winipedia_utils/testing/tests/base/utils/__init__.py,sha256=mC-8dCkp8xarqkQu2QQL
86
87
  winipedia_utils/testing/tests/base/utils/utils.py,sha256=D7N-PW4N8853nJ2m4eYjO3jBMByYB9oh1GK4Hl5Tbwg,2598
87
88
  winipedia_utils/testing/tests/conftest.py,sha256=BLgUJtLecOwuEsIyJ__0buqovd5AhiGvbMNk8CHgSQs,888
88
89
  winipedia_utils/text/__init__.py,sha256=j2bwtK6kyeHI6SnoBjpRju0C1W2n2paXBDlNjNtaUxA,48
89
- winipedia_utils/text/config.py,sha256=BPpuD4ywzzzt6Jz2M5Eln5d1NLlDp4r7CPwQOgHkxcg,7782
90
+ winipedia_utils/text/config.py,sha256=jjKmn-tSbyaK6jGL0FxFHSREP6A6V1ZSX3RuIgvQ4io,7794
90
91
  winipedia_utils/text/string.py,sha256=yXmwOab5hXyVQG1NwlWDpy2prj0U7Vb2F5HKLT2Y77Q,3382
91
- winipedia_utils-0.4.53.dist-info/METADATA,sha256=dHVCIWf2JB02S9ZNLS0WWKMjr_oH6kgudHrU682IrZo,14694
92
- winipedia_utils-0.4.53.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
93
- winipedia_utils-0.4.53.dist-info/licenses/LICENSE,sha256=o316mE2gGzd__JT69p7S_zlOmKiHh8YjpImCCcWyTvM,1066
94
- winipedia_utils-0.4.53.dist-info/RECORD,,
92
+ winipedia_utils-0.5.18.dist-info/METADATA,sha256=gzcMByHCXrpz0mx9ExddW8Ju8XusI0exo2LZYthyxhM,14694
93
+ winipedia_utils-0.5.18.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
94
+ winipedia_utils-0.5.18.dist-info/licenses/LICENSE,sha256=o316mE2gGzd__JT69p7S_zlOmKiHh8YjpImCCcWyTvM,1066
95
+ winipedia_utils-0.5.18.dist-info/RECORD,,