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.
- wexample_wex_addon_dev_python/commands/code/rename.py +41 -19
- wexample_wex_addon_dev_python/file/python_pyproject_toml_file.py +11 -11
- wexample_wex_addon_dev_python/python_addon_manager.py +1 -1
- wexample_wex_addon_dev_python/refactor/abstract_python_path_rename_handler.py +104 -0
- wexample_wex_addon_dev_python/refactor/abstract_python_rename_handler.py +89 -0
- wexample_wex_addon_dev_python/refactor/abstract_python_symbol_rename_handler.py +183 -0
- wexample_wex_addon_dev_python/refactor/applier.py +49 -8
- wexample_wex_addon_dev_python/refactor/python_class_rename_handler.py +19 -0
- wexample_wex_addon_dev_python/refactor/python_common.py +426 -0
- wexample_wex_addon_dev_python/refactor/python_constant_rename_handler.py +37 -0
- wexample_wex_addon_dev_python/refactor/python_function_rename_handler.py +21 -0
- wexample_wex_addon_dev_python/refactor/python_module_rename_handler.py +74 -0
- wexample_wex_addon_dev_python/refactor/python_package_rename_handler.py +83 -219
- wexample_wex_addon_dev_python/refactor/rename_plan.py +58 -17
- wexample_wex_addon_dev_python/refactor/symbol_kind.py +21 -6
- wexample_wex_addon_dev_python/services/python/commands/service/setup.py +2 -2
- wexample_wex_addon_dev_python/workdir/python_package_workdir.py +8 -8
- wexample_wex_addon_dev_python/workdir/python_workdir.py +6 -6
- {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/METADATA +10 -10
- {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/RECORD +24 -16
- /wexample_wex_addon_dev_python/{helpers → helper}/__init__.py +0 -0
- /wexample_wex_addon_dev_python/{helpers → helper}/pdm.py +0 -0
- {wexample_wex_addon_dev_python-12.2.0.dist-info → wexample_wex_addon_dev_python-13.0.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
5
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
7
6
|
|
|
8
|
-
from wexample_wex_addon_dev_python.refactor.
|
|
9
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
|
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
|
|
153
|
-
self,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
21
|
-
"""(
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
problems.append(f"
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
37
|
-
from wexample_wex_addon_app.
|
|
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.
|
|
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.
|
|
53
|
-
from wexample_helpers.
|
|
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.
|
|
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.
|
|
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.
|
|
385
|
-
from wexample_helpers_git.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
19
|
-
Requires-Dist: wexample-filestate-python>=
|
|
20
|
-
Requires-Dist: wexample-wex-addon-ai>=
|
|
21
|
-
Requires-Dist: wexample-wex-addon-app>=
|
|
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:
|
|
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.
|
|
116
|
-
- wexample-filestate-python: >=
|
|
117
|
-
- wexample-wex-addon-ai: >=
|
|
118
|
-
- wexample-wex-addon-app: >=
|
|
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
|
|