winipedia-utils 0.4.46__py3-none-any.whl → 0.6.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.
@@ -50,7 +50,7 @@ def get_src_package() -> ModuleType:
50
50
  TESTS_PACKAGE_NAME,
51
51
  )
52
52
 
53
- packages = find_packages_as_modules(depth=0)
53
+ packages = find_packages_as_modules(depth=0, include_namespace_packages=True)
54
54
  return next(p for p in packages if p.__name__ != TESTS_PACKAGE_NAME)
55
55
 
56
56
 
@@ -409,33 +409,6 @@ def get_main_package() -> ModuleType:
409
409
  raise ValueError(msg)
410
410
 
411
411
 
412
- def make_name_from_package(
413
- package: ModuleType,
414
- split_on: str = "_",
415
- join_on: str = "-",
416
- *,
417
- capitalize: bool = True,
418
- ) -> str:
419
- """Make a name from a package.
420
-
421
- takes a package and makes a name from it that is readable by humans.
422
-
423
- Args:
424
- package (ModuleType): The package to make a name from
425
- split_on (str, optional): what to split the package name on. Defaults to "_".
426
- join_on (str, optional): what to join the package name with. Defaults to "-".
427
- capitalize (bool, optional): Whether to capitalize each part. Defaults to True.
428
-
429
- Returns:
430
- str: _description_
431
- """
432
- package_name = package.__name__.split(".")[-1]
433
- parts = package_name.split(split_on)
434
- if capitalize:
435
- parts = [part.capitalize() for part in parts]
436
- return join_on.join(parts)
437
-
438
-
439
412
  class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
440
413
  """A directed graph representing Python package dependencies."""
441
414
 
@@ -447,7 +420,7 @@ class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
447
420
  def build(self) -> None:
448
421
  """Build the graph from installed Python distributions."""
449
422
  for dist in importlib.metadata.distributions():
450
- name = dist.metadata["Name"].lower()
423
+ name = self.parse_distname_from_metadata(dist)
451
424
  self.add_node(name)
452
425
 
453
426
  requires = dist.requires or []
@@ -456,14 +429,25 @@ class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
456
429
  if dep:
457
430
  self.add_edge(name, dep) # package → dependency
458
431
 
432
+ @staticmethod
433
+ def parse_distname_from_metadata(dist: importlib.metadata.Distribution) -> str:
434
+ """Extract the distribution name from its metadata."""
435
+ # replace - with _ to handle packages like winipedia-utils
436
+ name: str = dist.metadata["Name"]
437
+ return DependencyGraph.normalize_package_name(name)
438
+
439
+ @staticmethod
440
+ def normalize_package_name(name: str) -> str:
441
+ """Normalize a package name."""
442
+ return name.lower().replace("-", "_").strip()
443
+
459
444
  @staticmethod
460
445
  def parse_pkg_name_from_req(req: str) -> str | None:
461
446
  """Extract the bare dependency name from a requirement string."""
462
- if not req:
463
- return None
464
447
  # split on the first non alphanumeric character like >, <, =, etc.
465
- dep = re.split(r"[^a-zA-Z0-9]", req.strip())[0].strip()
466
- return dep.lower().strip() if dep else None
448
+ # keep - and _ for names like winipedia-utils or winipedia_utils
449
+ dep = re.split(r"[^a-zA-Z0-9_-]", req.strip())[0].strip()
450
+ return DependencyGraph.normalize_package_name(dep) if dep else None
467
451
 
468
452
  def get_all_depending_on(
469
453
  self, package: ModuleType, *, include_self: bool = False
@@ -477,6 +461,7 @@ class DependencyGraph(nx.DiGraph): # type: ignore [type-arg]
477
461
  Returns:
478
462
  A set of imported module objects representing dependents.
479
463
  """
464
+ # replace - with _ to handle packages like winipedia-utils
480
465
  target = package.__name__.lower()
481
466
  if target not in self:
482
467
  msg = f"Package '{target}' not found in dependency graph"
winipedia_utils/os/os.py CHANGED
@@ -7,7 +7,7 @@ These utilities help with system-level operations and configuration.
7
7
 
8
8
  import shutil
9
9
  import subprocess # nosec: B404
10
- from pathlib import Path
10
+ from collections.abc import Sequence
11
11
  from typing import Any
12
12
 
13
13
 
@@ -34,7 +34,7 @@ def which_with_raise(cmd: str, *, raise_error: bool = True) -> str | None:
34
34
 
35
35
 
36
36
  def run_subprocess(
37
- args: list[str | Path],
37
+ args: Sequence[str],
38
38
  *,
39
39
  input_: str | bytes | None = None,
40
40
  capture_output: bool = True,
@@ -1,31 +1,62 @@
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
 
6
- from winipedia_utils.modules.package import get_src_package, make_name_from_package
7
- from winipedia_utils.projects.poetry.poetry import VersionConstraint
7
+ import requests
8
+ from packaging.version import Version
9
+
10
+ from winipedia_utils.os.os import run_subprocess
11
+ from winipedia_utils.projects.poetry.dev_deps import DEV_DEPENDENCIES
12
+ from winipedia_utils.projects.poetry.poetry import POETRY_ARG, VersionConstraint
8
13
  from winipedia_utils.testing.config import ExperimentConfigFile
9
14
  from winipedia_utils.testing.convention import TESTS_PACKAGE_NAME
10
15
  from winipedia_utils.text.config import ConfigFile, TomlConfigFile
16
+ from winipedia_utils.text.string import make_name_from_obj
11
17
 
12
18
 
13
19
  class PyprojectConfigFile(TomlConfigFile):
14
20
  """Config file for pyproject.toml."""
15
21
 
22
+ @classmethod
23
+ def dump(cls, config: dict[str, Any] | list[Any]) -> None:
24
+ """Dump the config file.
25
+
26
+ We remove the wrong dependencies from the config before dumping.
27
+ So we do not want dependencies under tool.poetry.dependencies but
28
+ under project.dependencies. And we do not want dev dependencies under
29
+ tool.poetry.dev-dependencies but under tool.poetry.group.dev.dependencies.
30
+ """
31
+ if not isinstance(config, dict):
32
+ msg = f"Cannot dump {config} to pyproject.toml file."
33
+ raise TypeError(msg)
34
+ config = cls.remove_wrong_dependencies(config)
35
+ super().dump(config)
36
+
16
37
  @classmethod
17
38
  def get_parent_path(cls) -> Path:
18
39
  """Get the path to the config file."""
19
40
  return Path()
20
41
 
42
+ @classmethod
43
+ def get_repository_name(cls) -> str:
44
+ """Get the repository name.
45
+
46
+ Is the parent folder the project ives in and should be the same as the
47
+ project name.
48
+ """
49
+ cwd = Path.cwd()
50
+ return cwd.name
51
+
21
52
  @classmethod
22
53
  def get_configs(cls) -> dict[str, Any]:
23
54
  """Get the config."""
24
55
  return {
25
56
  "project": {
26
- "name": make_name_from_package(get_src_package(), capitalize=False),
57
+ "name": make_name_from_obj(cls.get_repository_name(), capitalize=False),
27
58
  "readme": "README.md",
28
- "dynamic": ["dependencies"],
59
+ "dependencies": list(cls.get_dependencies()),
29
60
  },
30
61
  "build-system": {
31
62
  "requires": ["poetry-core>=2.0.0,<3.0.0"],
@@ -33,15 +64,11 @@ class PyprojectConfigFile(TomlConfigFile):
33
64
  },
34
65
  "tool": {
35
66
  "poetry": {
36
- "packages": [{"include": get_src_package().__name__}],
37
- "dependencies": dict.fromkeys(
38
- cls.get_dependencies(),
39
- "*",
40
- ),
67
+ "packages": [{"include": cls.get_repository_name()}],
41
68
  "group": {
42
69
  "dev": {
43
70
  "dependencies": dict.fromkeys(
44
- cls.get_dev_dependencies(),
71
+ cls.get_dev_dependencies() | DEV_DEPENDENCIES,
45
72
  "*",
46
73
  )
47
74
  }
@@ -76,6 +103,29 @@ class PyprojectConfigFile(TomlConfigFile):
76
103
  package_name = str(project_dict.get("name", ""))
77
104
  return package_name.replace("-", "_")
78
105
 
106
+ @classmethod
107
+ def remove_wrong_dependencies(cls, config: dict[str, Any]) -> dict[str, Any]:
108
+ """Remove the wrong dependencies from the config."""
109
+ # raise if the right sections do not exist
110
+ if config.get("project", {}).get("dependencies") is None:
111
+ msg = "No dependencies section in config"
112
+ raise ValueError(msg)
113
+
114
+ if (
115
+ config.get("tool", {}).get("poetry", {}).get("group", {}).get("dev", {})
116
+ is None
117
+ ):
118
+ msg = "No dev dependencies section in config"
119
+ raise ValueError(msg)
120
+
121
+ # remove the wrong dependencies sections if they exist
122
+ if config.get("tool", {}).get("poetry", {}).get("dependencies") is not None:
123
+ del config["tool"]["poetry"]["dependencies"]
124
+ if config.get("tool", {}).get("poetry", {}).get("dev-dependencies") is not None:
125
+ del config["tool"]["poetry"]["dev-dependencies"]
126
+
127
+ return config
128
+
79
129
  @classmethod
80
130
  def get_all_dependencies(cls) -> set[str]:
81
131
  """Get all dependencies."""
@@ -103,9 +153,17 @@ class PyprojectConfigFile(TomlConfigFile):
103
153
  @classmethod
104
154
  def get_dependencies(cls) -> set[str]:
105
155
  """Get the dependencies."""
106
- return set(
107
- cls.load().get("tool", {}).get("poetry", {}).get("dependencies", {}).keys()
108
- )
156
+ deps = set(cls.load().get("project", {}).get("dependencies", {}))
157
+ deps = {d.split("(")[0].strip() for d in deps}
158
+ if not deps:
159
+ deps = set(
160
+ cls.load()
161
+ .get("tool", {})
162
+ .get("poetry", {})
163
+ .get("dependencies", {})
164
+ .keys()
165
+ )
166
+ return deps
109
167
 
110
168
  @classmethod
111
169
  def get_expected_dev_dependencies(cls) -> set[str]:
@@ -135,25 +193,29 @@ class PyprojectConfigFile(TomlConfigFile):
135
193
  return cls.get_main_author()["name"]
136
194
 
137
195
  @classmethod
138
- def get_latest_possible_python_version(cls) -> str:
196
+ @cache
197
+ def fetch_latest_python_version(cls) -> Version:
198
+ """Fetch the latest python version from python.org."""
199
+ url = "https://endoflife.date/api/python.json"
200
+ resp = requests.get(url, timeout=10)
201
+ resp.raise_for_status()
202
+ data = resp.json()
203
+ # first element has metadata for latest stable
204
+ latest_version = data[0]["latest"]
205
+ return Version(latest_version)
206
+
207
+ @classmethod
208
+ def get_latest_possible_python_version(cls) -> Version:
139
209
  """Get the latest possible python version."""
140
210
  constraint = cls.load()["project"]["requires-python"]
141
211
  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"
212
+ version = version_constraint.get_upper_inclusive()
213
+ if version is None:
214
+ version = cls.fetch_latest_python_version()
215
+ return version
154
216
 
155
217
  @classmethod
156
- def get_first_supported_python_version(cls) -> str:
218
+ def get_first_supported_python_version(cls) -> Version:
157
219
  """Get the first supported python version."""
158
220
  constraint = cls.load()["project"]["requires-python"]
159
221
  version_constraint = VersionConstraint(constraint)
@@ -161,7 +223,28 @@ class PyprojectConfigFile(TomlConfigFile):
161
223
  if lower is None:
162
224
  msg = "Need a lower bound for python version"
163
225
  raise ValueError(msg)
164
- return str(lower)
226
+ return lower
227
+
228
+ @classmethod
229
+ def get_supported_python_versions(cls) -> list[Version]:
230
+ """Get all supported python versions."""
231
+ constraint = cls.load()["project"]["requires-python"]
232
+ version_constraint = VersionConstraint(constraint)
233
+ return version_constraint.get_version_range(
234
+ level="minor", upper_default=cls.fetch_latest_python_version()
235
+ )
236
+
237
+ @classmethod
238
+ def update_poetry(cls) -> None:
239
+ """Update poetry."""
240
+ args = [POETRY_ARG, "self", "update"]
241
+ run_subprocess(args)
242
+
243
+ @classmethod
244
+ def update_with_dev(cls) -> None:
245
+ """Install all dependencies with dev."""
246
+ args = [POETRY_ARG, "update", "--with", "dev"]
247
+ run_subprocess(args)
165
248
 
166
249
 
167
250
  class TypedConfigFile(ConfigFile):
@@ -223,7 +306,9 @@ class DotPythonVersionConfigFile(ConfigFile):
223
306
  def get_configs(cls) -> dict[str, Any]:
224
307
  """Get the config."""
225
308
  return {
226
- cls.VERSION_KEY: PyprojectConfigFile.get_first_supported_python_version()
309
+ cls.VERSION_KEY: str(
310
+ PyprojectConfigFile.get_first_supported_python_version()
311
+ )
227
312
  }
228
313
 
229
314
  @classmethod
@@ -0,0 +1,21 @@
1
+ """Contains a dict with the dev dependencies.
2
+
3
+ For poetry when winipedia_utils is a dependency.
4
+ winipedia_utils will add these automatically to the pyproject.toml file.
5
+ winipedia utils PyprojectConfigFile will auto dump the config here so it can access it
6
+ when being a dependency in another project.
7
+ """
8
+
9
+ DEV_DEPENDENCIES: set[str] = {
10
+ "ruff",
11
+ "types-networkx",
12
+ "types-defusedxml",
13
+ "types-pyyaml",
14
+ "pytest",
15
+ "types-setuptools",
16
+ "pytest-mock",
17
+ "bandit",
18
+ "pre-commit",
19
+ "mypy",
20
+ "types-tqdm",
21
+ }
@@ -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)]
@@ -11,7 +11,6 @@ from winipedia_utils.text.config import (
11
11
 
12
12
  def create_project_root() -> None:
13
13
  """Create the project root."""
14
- ConfigFile.init_config_files()
15
-
16
14
  src_package_name = PyprojectConfigFile.get_package_name()
17
15
  create_module(src_package_name, is_package=True)
16
+ ConfigFile.init_config_files()
winipedia_utils/setup.py CHANGED
@@ -12,13 +12,17 @@ from typing import Any
12
12
  from winipedia_utils.git.gitignore.config import GitIgnoreConfigFile
13
13
  from winipedia_utils.git.pre_commit.run_hooks import run_hooks
14
14
  from winipedia_utils.logging.logger import get_logger
15
+ from winipedia_utils.projects.poetry.config import PyprojectConfigFile
15
16
  from winipedia_utils.projects.project import create_project_root
16
17
 
17
18
  logger = get_logger(__name__)
18
19
 
19
20
 
20
21
  SETUP_STEPS: list[Callable[..., Any]] = [
21
- GitIgnoreConfigFile, # must be first
22
+ GitIgnoreConfigFile, # must be before create_project_root
23
+ PyprojectConfigFile, # must be before create_project_root
24
+ PyprojectConfigFile.update_poetry, # must be before create_project_root
25
+ PyprojectConfigFile.update_with_dev, # must be before create_project_root
22
26
  create_project_root,
23
27
  run_hooks,
24
28
  ]
@@ -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):
@@ -7,7 +7,10 @@ These utilities simplify common string manipulation tasks throughout the applica
7
7
 
8
8
  import hashlib
9
9
  import textwrap
10
+ from collections.abc import Callable
10
11
  from io import StringIO
12
+ from types import ModuleType
13
+ from typing import Any
11
14
 
12
15
  from defusedxml import ElementTree as DefusedElementTree
13
16
 
@@ -124,3 +127,33 @@ def split_on_uppercase(string: str) -> list[str]:
124
127
  current_part += letter
125
128
  parts.append(current_part)
126
129
  return parts
130
+
131
+
132
+ def make_name_from_obj(
133
+ package: ModuleType | Callable[..., Any] | type | str,
134
+ split_on: str = "_",
135
+ join_on: str = "-",
136
+ *,
137
+ capitalize: bool = True,
138
+ ) -> str:
139
+ """Make a name from a package.
140
+
141
+ takes a package and makes a name from it that is readable by humans.
142
+
143
+ Args:
144
+ package (ModuleType): The package to make a name from
145
+ split_on (str, optional): what to split the package name on. Defaults to "_".
146
+ join_on (str, optional): what to join the package name with. Defaults to "-".
147
+ capitalize (bool, optional): Whether to capitalize each part. Defaults to True.
148
+
149
+ Returns:
150
+ str: _description_
151
+ """
152
+ if not isinstance(package, str):
153
+ package_name = package.__name__.split(".")[-1]
154
+ else:
155
+ package_name = package
156
+ parts = package_name.split(split_on)
157
+ if capitalize:
158
+ parts = [part.capitalize() for part in parts]
159
+ return join_on.join(parts)