wexample-wex-addon-dev-python 12.2.0__py3-none-any.whl → 13.0.0__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.
Files changed (24) hide show
  1. wexample_wex_addon_dev_python/commands/code/rename.py +41 -19
  2. wexample_wex_addon_dev_python/file/python_pyproject_toml_file.py +11 -11
  3. wexample_wex_addon_dev_python/python_addon_manager.py +1 -1
  4. wexample_wex_addon_dev_python/refactor/abstract_python_path_rename_handler.py +104 -0
  5. wexample_wex_addon_dev_python/refactor/abstract_python_rename_handler.py +89 -0
  6. wexample_wex_addon_dev_python/refactor/abstract_python_symbol_rename_handler.py +183 -0
  7. wexample_wex_addon_dev_python/refactor/applier.py +49 -8
  8. wexample_wex_addon_dev_python/refactor/python_class_rename_handler.py +19 -0
  9. wexample_wex_addon_dev_python/refactor/python_common.py +426 -0
  10. wexample_wex_addon_dev_python/refactor/python_constant_rename_handler.py +37 -0
  11. wexample_wex_addon_dev_python/refactor/python_function_rename_handler.py +21 -0
  12. wexample_wex_addon_dev_python/refactor/python_module_rename_handler.py +74 -0
  13. wexample_wex_addon_dev_python/refactor/python_package_rename_handler.py +83 -219
  14. wexample_wex_addon_dev_python/refactor/rename_plan.py +58 -17
  15. wexample_wex_addon_dev_python/refactor/symbol_kind.py +21 -6
  16. wexample_wex_addon_dev_python/services/python/commands/service/setup.py +2 -2
  17. wexample_wex_addon_dev_python/workdir/python_package_workdir.py +8 -8
  18. wexample_wex_addon_dev_python/workdir/python_workdir.py +6 -6
  19. {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/METADATA +10 -10
  20. {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/RECORD +24 -16
  21. /wexample_wex_addon_dev_python/{helpers → helper}/__init__.py +0 -0
  22. /wexample_wex_addon_dev_python/{helpers → helper}/pdm.py +0 -0
  23. {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/WHEEL +0 -0
  24. {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,132 +1,41 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass, field
4
3
  from pathlib import Path
5
4
 
6
- import libcst as cst
5
+ from wexample_helpers.decorator.base_class import base_class
7
6
 
8
- from wexample_wex_addon_dev_python.refactor.applier import (
9
- NaiveRenameApplier,
10
- RenameApplier,
7
+ from wexample_wex_addon_dev_python.refactor.abstract_python_path_rename_handler import (
8
+ AbstractPythonPathRenameHandler,
11
9
  )
10
+ from wexample_wex_addon_dev_python.refactor.python_common import SKIP_DIR_NAMES
12
11
  from wexample_wex_addon_dev_python.refactor.rename_plan import RenamePlan
13
12
 
14
- # Directories never scanned for source files (build/venv artefacts, VCS).
15
- _SKIP_DIR_NAMES: frozenset[str] = frozenset({
16
- "__pycache__",
17
- ".venv",
18
- "venv",
19
- "env",
20
- ".git",
21
- "node_modules",
22
- ".pytest_cache",
23
- ".mypy_cache",
24
- ".ruff_cache",
25
- ".pdm-build",
26
- "dist",
27
- "build",
28
- })
29
13
 
14
+ @base_class
15
+ class PythonPackageRenameHandler(AbstractPythonPathRenameHandler):
16
+ """Rename a Python package (a directory containing `__init__.py`) and
17
+ propagate the import-rewrite across every scope root.
30
18
 
31
- def _dotted_to_parts(node) -> tuple[str, ...] | None:
32
- """Flatten a chained `cst.Attribute` / `cst.Name` into ('a', 'b', 'c')."""
33
- parts: list[str] = []
34
- cur = node
35
- while isinstance(cur, cst.Attribute):
36
- if not isinstance(cur.attr, cst.Name):
37
- return None
38
- parts.append(cur.attr.value)
39
- cur = cur.value
40
- if not isinstance(cur, cst.Name):
41
- return None
42
- parts.append(cur.value)
43
- parts.reverse()
44
- return tuple(parts)
45
-
46
-
47
- def _parts_to_dotted(parts: tuple[str, ...]):
48
- node: cst.BaseExpression = cst.Name(parts[0])
49
- for part in parts[1:]:
50
- node = cst.Attribute(value=node, attr=cst.Name(part))
51
- return node
52
-
53
-
54
- @dataclass
55
- class RenameReport:
56
- """Outcome of a package rename run.
57
-
58
- `package_dir_old`/`new` describe the move (or proposed move in dry-run).
59
- `files_changed` lists every .py whose imports were rewritten.
60
- """
61
-
62
- files_changed: list[Path] = field(default_factory=list)
63
- files_scanned: int = 0
64
- package_dir_new: Path | None = None
65
- package_dir_old: Path | None = None
66
-
67
-
68
- class PythonPackageRenameHandler:
69
- """Rename a Python package and propagate the change to importers.
70
-
71
- Three phases, fully separated:
72
- 1. compute_plan() — read-only scan, builds a `RenamePlan` in memory.
73
- 2. plan.preflight() — validation (permissions, target existence).
74
- 3. applier.apply(plan) — single batch of writes + the directory rename.
75
-
76
- Splitting plan/apply is the durability strategy: a permission error mid-run
77
- can't leave the repo half-rewritten. Splitting from the applier lets us
78
- swap the write backend (naive → filestate-backed) without touching this
79
- handler.
80
-
81
- `scope_roots` = the set of roots to sweep for both the source package
82
- directory AND import rewrites. Caller passes the sibling-package roots
83
- when running suite-wide (typically obtained via the framework's
84
- `FrameworkPackageSuiteWorkdir.get_ordered_packages()`), or a single
85
- workdir when running standalone.
19
+ When the target directory already exists (typically because the user has
20
+ been migrating modules one at a time before flipping the rest in bulk),
21
+ the handler switches to a merge mode: each child of the source dir is
22
+ moved into the target, identical/empty `__init__.py` duplicates are
23
+ dropped, and the now-empty source dir is removed. Any non-identical,
24
+ non-empty file collision is surfaced as a clean error instead of being
25
+ silently overwritten.
86
26
  """
87
27
 
88
- def __init__(
89
- self,
90
- scope_roots: list[Path],
91
- source: str,
92
- target: str,
93
- *,
94
- dry_run: bool = True,
95
- ) -> None:
96
- if not scope_roots:
97
- raise ValueError("scope_roots must contain at least one path")
98
- self.scope_roots = scope_roots
99
- self.source = source
100
- self.target = target
101
- self.dry_run = dry_run
102
- self._source_parts: tuple[str, ...] = tuple(source.split("."))
103
- self._target_parts: tuple[str, ...] = tuple(target.split("."))
104
- if not all(self._source_parts) or not all(self._target_parts):
105
- raise ValueError("source and target must be dotted names without empty parts")
106
-
107
- def compute_plan(self) -> RenamePlan:
108
- """Phase 1: scan and build the plan. No write yet."""
109
- plan = RenamePlan()
110
- package_dir = self.find_package_dir()
111
- plan.dir_renames.append(
112
- (package_dir, package_dir.parent / self._target_parts[-1])
113
- )
114
-
115
- for py_file in self._iter_python_files():
116
- plan.files_scanned += 1
117
- new_source = self._rewrite_imports(py_file)
118
- if new_source is not None:
119
- plan.rewrites[py_file] = new_source
120
-
121
- return plan
28
+ def _derive_target_path(self, source_path: Path) -> Path:
29
+ # A package rename is a sibling-directory move: same parent, last part
30
+ # swapped for the target's last part.
31
+ return source_path.parent / self._target_parts[-1]
122
32
 
123
- def find_package_dir(self) -> Path:
124
- """Return the unique directory matching `source` across all scope roots."""
33
+ def _find_source_path(self) -> Path:
125
34
  n = len(self._source_parts)
126
35
  matches: list[Path] = []
127
36
  for root in self.scope_roots:
128
37
  for init_file in root.rglob("__init__.py"):
129
- if any(part in _SKIP_DIR_NAMES for part in init_file.parent.parts):
38
+ if any(part in SKIP_DIR_NAMES for part in init_file.parent.parts):
130
39
  continue
131
40
  pkg_dir = init_file.parent
132
41
  if len(pkg_dir.parts) < n:
@@ -149,113 +58,68 @@ class PythonPackageRenameHandler:
149
58
  )
150
59
  return unique[0]
151
60
 
152
- def run(
153
- self,
154
- applier: RenameApplier | None = None,
155
- ) -> RenameReport:
156
- """Compute plan → preflight → apply (or stop after compute in dry-run)."""
157
- plan = self.compute_plan()
158
- old_dir, new_dir = plan.dir_renames[0]
159
- report = RenameReport(
160
- files_changed=list(plan.rewrites.keys()),
161
- files_scanned=plan.files_scanned,
162
- package_dir_new=new_dir,
163
- package_dir_old=old_dir,
164
- )
165
-
166
- if self.dry_run:
167
- return report
168
-
169
- problems = plan.preflight()
170
- if problems:
171
- joined = "\n - ".join(problems)
172
- raise RuntimeError(f"Plan preflight failed:\n - {joined}")
173
-
174
- (applier or NaiveRenameApplier()).apply(plan)
175
- return report
176
-
177
- def _iter_python_files(self):
178
- """Iterate every .py file under any scope root, skipping build/VCS dirs."""
179
- seen: set[str] = set()
180
- for root in self.scope_roots:
181
- stack: list[Path] = [root]
182
- while stack:
183
- current = stack.pop()
184
- try:
185
- for entry in current.iterdir():
186
- if entry.is_dir():
187
- if entry.name not in _SKIP_DIR_NAMES:
188
- stack.append(entry)
189
- elif entry.suffix == ".py":
190
- key = str(entry.resolve())
191
- if key in seen:
192
- continue
193
- seen.add(key)
194
- yield entry
195
- except (PermissionError, FileNotFoundError):
196
- continue
197
-
198
- def _rewrite_imports(self, py_file: Path) -> str | None:
199
- """Return rewritten source, or None when no import touched this file."""
200
- source = py_file.read_text(encoding="utf-8")
201
- try:
202
- module = cst.parse_module(source)
203
- except cst.ParserSyntaxError:
204
- return None
205
-
206
- transformer = _ImportRenameTransformer(
207
- old_parts=self._source_parts,
208
- new_parts=self._target_parts,
209
- )
210
- new_module = module.visit(transformer)
211
- if not transformer.changed:
212
- return None
213
- return new_module.code
214
-
215
-
216
- class _ImportRenameTransformer(cst.CSTTransformer):
217
- """Rewrites import statements whose dotted name starts with `old_parts`."""
218
-
219
- def __init__(self, old_parts: tuple[str, ...], new_parts: tuple[str, ...]) -> None:
220
- super().__init__()
221
- self.old_parts = old_parts
222
- self.new_parts = new_parts
223
- self._n = len(old_parts)
224
- self.changed = False
225
-
226
- def leave_Import(
227
- self,
228
- original_node: cst.Import,
229
- updated_node: cst.Import,
230
- ) -> cst.Import:
231
- new_names = []
232
- touched = False
233
- for alias in updated_node.names:
234
- parts = _dotted_to_parts(alias.name)
235
- if parts is None or not self._starts_with(parts):
236
- new_names.append(alias)
61
+ def _plan_merge_into_existing(
62
+ self, plan: RenamePlan, source_dir: Path, target_dir: Path
63
+ ) -> None:
64
+ """Merge every child of `source_dir` into `target_dir`, then remove
65
+ the now-empty `source_dir`.
66
+
67
+ Conflict resolution per child:
68
+ - target missing → move source child in
69
+ - both directories → unsupported in this palier; refuse
70
+ - both files, equal → drop source's copy (it's a duplicate)
71
+ - source file empty → drop source's copy (target wins)
72
+ - non-empty mismatch → refuse with a clear error
73
+ """
74
+ for entry in sorted(source_dir.iterdir()):
75
+ # Build/VCS artefacts (`__pycache__`, `.pytest_cache`, …) block
76
+ # the non-recursive rmdir but they're regenerable, so we purge
77
+ # them instead of refusing the whole merge.
78
+ if entry.is_dir() and entry.name in SKIP_DIR_NAMES:
79
+ plan.directories_to_purge.append(entry)
237
80
  continue
238
- new_parts = self.new_parts + parts[self._n:]
239
- new_names.append(alias.with_changes(name=_parts_to_dotted(new_parts)))
240
- touched = True
241
- if not touched:
242
- return updated_node
243
- self.changed = True
244
- return updated_node.with_changes(names=new_names)
245
81
 
246
- def leave_ImportFrom(
247
- self,
248
- original_node: cst.ImportFrom,
249
- updated_node: cst.ImportFrom,
250
- ) -> cst.ImportFrom:
251
- if updated_node.module is None:
252
- return updated_node
253
- parts = _dotted_to_parts(updated_node.module)
254
- if parts is None or not self._starts_with(parts):
255
- return updated_node
256
- new_parts = self.new_parts + parts[self._n:]
257
- self.changed = True
258
- return updated_node.with_changes(module=_parts_to_dotted(new_parts))
82
+ target_entry = target_dir / entry.name
83
+ if not target_entry.exists():
84
+ plan.path_renames.append((entry, target_entry))
85
+ continue
259
86
 
260
- def _starts_with(self, parts: tuple[str, ...]) -> bool:
261
- return len(parts) >= self._n and parts[: self._n] == self.old_parts
87
+ if entry.is_dir() or target_entry.is_dir():
88
+ raise RuntimeError(
89
+ "Nested directory merge is not supported in this palier "
90
+ f"(conflict on {entry.name}). Move that subtree manually "
91
+ "before retrying the package rename."
92
+ )
93
+
94
+ try:
95
+ src_content = entry.read_text(encoding="utf-8")
96
+ tgt_content = target_entry.read_text(encoding="utf-8")
97
+ except OSError as e:
98
+ raise RuntimeError(
99
+ f"Cannot read content while planning merge: {e}"
100
+ )
101
+
102
+ if src_content == tgt_content or src_content == "":
103
+ plan.files_to_delete.append(entry)
104
+ else:
105
+ raise RuntimeError(
106
+ "Merge conflict — different non-empty file contents:\n"
107
+ f" source: {entry}\n"
108
+ f" target: {target_entry}\n"
109
+ "Reconcile manually before retrying."
110
+ )
111
+
112
+ plan.directories_to_remove.append(source_dir)
113
+
114
+ def _plan_path_move(
115
+ self, plan: RenamePlan, source_path: Path, target_path: Path
116
+ ) -> None:
117
+ if not target_path.exists():
118
+ super()._plan_path_move(plan, source_path, target_path)
119
+ return
120
+ if not (source_path.is_dir() and target_path.is_dir()):
121
+ raise RuntimeError(
122
+ f"Cannot merge: source or target is not a directory "
123
+ f"({source_path} → {target_path})"
124
+ )
125
+ self._plan_merge_into_existing(plan, source_path, target_path)
@@ -15,29 +15,70 @@ class RenamePlan:
15
15
  """
16
16
 
17
17
  rewrites: dict[Path, str] = field(default_factory=dict)
18
- """Per-file new source code. Keys = absolute file paths."""
18
+ """Per-file new source code. Keys = absolute file paths. May include
19
+ `__init__.py` files for directories listed in `directories_to_create`."""
19
20
 
20
- dir_renames: list[tuple[Path, Path]] = field(default_factory=list)
21
- """(old_dir, new_dir) pairs. Ordered: applied last, after rewrites."""
21
+ path_renames: list[tuple[Path, Path]] = field(default_factory=list)
22
+ """(old, new) pairs files or directories. Applied after rewrites and
23
+ directory creation, before file/dir deletions."""
24
+
25
+ directories_to_create: list[Path] = field(default_factory=list)
26
+ """Directories that don't exist yet and must be created before any
27
+ rewrite or path rename runs. Ordered outermost-first so `parent` is in
28
+ place by the time a deeper child is created."""
29
+
30
+ files_to_delete: list[Path] = field(default_factory=list)
31
+ """Files to unlink after the path-renames step. Typically used by the
32
+ package-merge logic to drop duplicate/empty `__init__.py` once the rest
33
+ of the source dir has been merged into the target."""
34
+
35
+ directories_to_remove: list[Path] = field(default_factory=list)
36
+ """Directories to `rmdir` once their contents have been moved or
37
+ deleted. `rmdir` is non-recursive: anything left over is a bug in the
38
+ plan and surfaces as a clean OSError at apply time."""
39
+
40
+ directories_to_purge: list[Path] = field(default_factory=list)
41
+ """Directories to remove recursively. Reserved for build/VCS artefacts
42
+ (`__pycache__`, `.pytest_cache`, …) encountered during a merge: they
43
+ block the non-recursive `rmdir` but they're regenerated automatically,
44
+ so nuking them is safe and keeps the merge clean."""
22
45
 
23
46
  files_scanned: int = 0
24
47
  """Total .py files inspected (for reporting)."""
25
48
 
26
49
  def preflight(self) -> list[str]:
27
- """Return a list of human-readable problems, empty if plan can run.
28
-
29
- Checks (cheap, no writes):
30
- - each file in `rewrites` is writable
31
- - each `dir_rename` target doesn't already exist
32
- - each `dir_rename` source exists
33
- """
50
+ """Return a list of human-readable problems, empty if plan can run."""
34
51
  problems: list[str] = []
52
+ pending_dirs = {d.resolve() for d in self.directories_to_create}
53
+
35
54
  for path in self.rewrites:
36
- if not os.access(path, os.W_OK):
37
- problems.append(f"Not writable: {path}")
38
- for old_dir, new_dir in self.dir_renames:
39
- if not old_dir.exists():
40
- problems.append(f"Source directory missing: {old_dir}")
41
- if new_dir.exists():
42
- problems.append(f"Target directory already exists: {new_dir}")
55
+ if path.exists():
56
+ if not os.access(path, os.W_OK):
57
+ problems.append(f"Not writable: {path}")
58
+ elif not self._parent_will_exist(path, pending_dirs):
59
+ problems.append(f"Target parent missing for rewrite: {path}")
60
+
61
+ for old_path, new_path in self.path_renames:
62
+ if not old_path.exists():
63
+ problems.append(f"Source path missing: {old_path}")
64
+ if new_path.exists():
65
+ problems.append(f"Target path already exists: {new_path}")
66
+ if not self._parent_will_exist(new_path, pending_dirs):
67
+ problems.append(f"Target parent directory missing: {new_path.parent}")
68
+
69
+ for directory in self.directories_to_create:
70
+ if directory.exists():
71
+ problems.append(
72
+ f"Directory already exists (cannot create): {directory}"
73
+ )
74
+
43
75
  return problems
76
+
77
+ def _parent_will_exist(self, path: Path, pending: set[Path]) -> bool:
78
+ parent = path.parent
79
+ if parent.exists():
80
+ return True
81
+ try:
82
+ return parent.resolve() in pending
83
+ except OSError:
84
+ return False
@@ -4,16 +4,31 @@ from enum import Enum
4
4
 
5
5
 
6
6
  class SymbolKind(str, Enum):
7
- """The kind of symbol targeted by a rename.
8
-
9
- Step 1 only implements PACKAGE; the other members are listed so callers can
10
- already pass `--kind class` and get a clear `NotImplementedError` instead
11
- of a parsing error.
12
- """
7
+ """The kind of symbol targeted by a rename."""
13
8
 
14
9
  PACKAGE = "package"
15
10
  MODULE = "module"
16
11
  CLASS = "class"
17
12
  FUNCTION = "function"
13
+ # METHOD is intentionally NOT implemented yet.
14
+ #
15
+ # Renaming a method `Color.brighten` safely means knowing, at every call
16
+ # site `something.brighten()`, whether `something` is actually a `Color`
17
+ # instance. Python is dynamic: that type can only be inferred reliably by
18
+ # a full static-analysis pipeline (mypy / pyright), and even those tools
19
+ # are routinely wrong on untyped or partially-typed codebases. A naive
20
+ # rewrite would silently rename every `.brighten()` in sight, breaking
21
+ # every unrelated class that happens to share the method name.
22
+ #
23
+ # We defer this until:
24
+ # - The other kinds (PACKAGE / MODULE / CLASS / FUNCTION / CONSTANT)
25
+ # have been used at scale on real refactors and proven robust.
26
+ # - Strict typing and stable syntax conventions are the norm across
27
+ # the codebases we refactor, giving a static type-checker enough
28
+ # signal to disambiguate receivers reliably.
29
+ # - A dedicated validation budget is available: large synthetic
30
+ # fixtures plus dry-runs across the monorepo, broad test coverage,
31
+ # and code-quality agents reviewing every proposed rewrite before
32
+ # it touches a real tree.
18
33
  METHOD = "method"
19
34
  CONSTANT = "constant"
@@ -33,8 +33,8 @@ def python__service__setup(
33
33
  import subprocess
34
34
 
35
35
  from wexample_app.const.globals import WORKDIR_LOCAL_DIR_NAME, WORKDIR_SETUP_DIR
36
- from wexample_helpers.helpers.file import file_mkdir_as_real_user
37
- from wexample_wex_addon_app.helpers.image_builds import (
36
+ from wexample_helpers.helper.file import file_mkdir_as_real_user
37
+ from wexample_wex_addon_app.helper.image_builds import (
38
38
  load_builds,
39
39
  resolve_build_order,
40
40
  )
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
5
5
 
6
6
  from wexample_filestate.const.disk import DiskItemType
7
7
  from wexample_helpers.decorator.base_class import base_class
8
- from wexample_wex_addon_app.helpers.python import (
8
+ from wexample_wex_addon_app.helper.python import (
9
9
  python_install_dependency_in_venv,
10
10
  python_is_package_installed_editable_in_venv,
11
11
  )
@@ -49,8 +49,8 @@ class PythonPackageWorkdir(PythonWorkdir):
49
49
  )
50
50
 
51
51
  def prepare_value(self, raw_value: DictConfig | None = None) -> DictConfig:
52
- from wexample_helpers.helpers.file import file_read
53
- from wexample_helpers.helpers.module import module_get_path
52
+ from wexample_helpers.helper.file import file_read
53
+ from wexample_helpers.helper.module import module_get_path
54
54
 
55
55
  import wexample_wex_addon_dev_python
56
56
 
@@ -190,7 +190,7 @@ class PythonPackageWorkdir(PythonWorkdir):
190
190
  UPGRADE_TYPE_MAJOR,
191
191
  UPGRADE_TYPE_MINOR,
192
192
  )
193
- from wexample_helpers_git.helpers.git import git_has_changes_since_tag
193
+ from wexample_helpers_git.helper.git import git_has_changes_since_tag
194
194
 
195
195
  if not git_has_changes_since_tag(last_tag, "src", cwd=self.get_path()):
196
196
  return UPGRADE_TYPE_MINOR
@@ -283,7 +283,7 @@ class PythonPackageWorkdir(PythonWorkdir):
283
283
  self, venv_path: Path, env: str | None = None, force: bool = False
284
284
  ) -> None:
285
285
  from wexample_app.const.env import ENV_NAME_LOCAL
286
- from wexample_wex_addon_app.helpers.python import (
286
+ from wexample_wex_addon_app.helper.python import (
287
287
  python_install_dependencies_in_venv,
288
288
  )
289
289
 
@@ -381,8 +381,8 @@ class PythonPackageWorkdir(PythonWorkdir):
381
381
 
382
382
  def _publish(self, force: bool = False) -> None:
383
383
  from wexample_filestate_python.common.pipy_gateway import PipyGateway
384
- from wexample_helpers.helpers.shell import shell_run
385
- from wexample_helpers_git.helpers.git import (
384
+ from wexample_helpers.helper.shell import shell_run
385
+ from wexample_helpers_git.helper.git import (
386
386
  git_push_tag,
387
387
  git_tag_annotated,
388
388
  git_tag_exists,
@@ -445,7 +445,7 @@ class PythonPackageWorkdir(PythonWorkdir):
445
445
  shell_run(publish_cmd, inherit_stdio=True, cwd=self.get_path())
446
446
 
447
447
  def _wait_for_registry(self) -> None:
448
- from wexample_helpers.helpers.polling_callback_manager import (
448
+ from wexample_helpers.helper.polling_callback_manager import (
449
449
  PollingCallbackManager,
450
450
  )
451
451
 
@@ -59,7 +59,7 @@ class PythonWorkdir(
59
59
  WithAiWorkdirMixin, WithProfilingPythonWorkdirMixin, CodeBaseWorkdir
60
60
  ):
61
61
  def app_install(self, env: str | None = None, force: bool = False) -> Path:
62
- from wexample_wex_addon_app.helpers.python import (
62
+ from wexample_wex_addon_app.helper.python import (
63
63
  python_ensure_pip_or_fail,
64
64
  python_install_environment,
65
65
  )
@@ -127,7 +127,7 @@ class PythonWorkdir(
127
127
  return f"{self.get_vendor_name()}_{self.get_project_name()}"
128
128
 
129
129
  def get_package_name(self) -> str:
130
- from wexample_helpers.helpers.string import string_to_kebab_case
130
+ from wexample_helpers.helper.string import string_to_kebab_case
131
131
 
132
132
  return string_to_kebab_case(self.get_package_import_name())
133
133
 
@@ -387,7 +387,7 @@ class PythonWorkdir(
387
387
  json_file = JsonFile.create_from_path(path=json_path)
388
388
  totals = json_file.read_config().search("totals", default={}).get_dict()
389
389
 
390
- from wexample_helpers_git.helpers.git import git_get_current_commit_hash
390
+ from wexample_helpers_git.helper.git import git_get_current_commit_hash
391
391
 
392
392
  # Derived state, not configuration: lives in .wex/local/ (untracked)
393
393
  # so test runs never dirty the repository.
@@ -412,7 +412,7 @@ class PythonWorkdir(
412
412
  def update_dependencies(self, dependencies_map: dict[str, str]) -> None:
413
413
  import subprocess
414
414
 
415
- from wexample_helpers.helpers.retryable_callback_manager import (
415
+ from wexample_helpers.helper.retryable_callback_manager import (
416
416
  RetryableCallbackManager,
417
417
  )
418
418
 
@@ -493,7 +493,7 @@ class PythonWorkdir(
493
493
  )
494
494
 
495
495
  def _create_package_name_snake(self, option: ItemTreeConfigOptionMixin) -> str:
496
- from wexample_helpers.helpers.string import string_to_snake_case
496
+ from wexample_helpers.helper.string import string_to_snake_case
497
497
 
498
498
  vendor_prefix = self.get_vendor_name()
499
499
  return (
@@ -646,7 +646,7 @@ class PythonWorkdir(
646
646
  def _install_dependencies_in_venv(
647
647
  self, venv_path: Path, env: str | None = None, force: bool = False
648
648
  ) -> None:
649
- from wexample_wex_addon_app.helpers.python import (
649
+ from wexample_wex_addon_app.helper.python import (
650
650
  python_install_dependency_in_venv,
651
651
  )
652
652
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wexample-wex-addon-dev-python
3
- Version: 12.2.0
3
+ Version: 13.0.0
4
4
  Summary: Python dev addon for wex
5
5
  Author-Email: weeger <contact@wexample.com>
6
6
  License: MIT
@@ -15,10 +15,10 @@ Requires-Dist: griffe>=2.0.2
15
15
  Requires-Dist: networkx
16
16
  Requires-Dist: pylint
17
17
  Requires-Dist: pyright
18
- Requires-Dist: wexample-api>=6.7.0
19
- Requires-Dist: wexample-filestate-python>=8.2.0
20
- Requires-Dist: wexample-wex-addon-ai>=11.3.0
21
- Requires-Dist: wexample-wex-addon-app>=27.0.0
18
+ Requires-Dist: wexample-api>=6.8.0
19
+ Requires-Dist: wexample-filestate-python>=9.0.0
20
+ Requires-Dist: wexample-wex-addon-ai>=12.0.0
21
+ Requires-Dist: wexample-wex-addon-app>=28.0.0
22
22
  Provides-Extra: dev
23
23
  Requires-Dist: pytest; extra == "dev"
24
24
  Requires-Dist: pytest-cov; extra == "dev"
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # wex_addon_dev_python
28
28
 
29
- Version: 12.2.0
29
+ Version: 13.0.0
30
30
 
31
31
  Python dev addon for wex
32
32
 
@@ -112,10 +112,10 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
112
112
  - networkx:
113
113
  - pylint:
114
114
  - pyright:
115
- - wexample-api: >=6.7.0
116
- - wexample-filestate-python: >=8.2.0
117
- - wexample-wex-addon-ai: >=11.3.0
118
- - wexample-wex-addon-app: >=27.0.0
115
+ - wexample-api: >=6.8.0
116
+ - wexample-filestate-python: >=9.0.0
117
+ - wexample-wex-addon-ai: >=12.0.0
118
+ - wexample-wex-addon-app: >=28.0.0
119
119
 
120
120
  ## Versioning & Compatibility Policy
121
121