Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions desloppify/languages/python/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ def _replace_exact_module(line: str, old_module: str, new_module: str) -> str:
return re.sub(rf"(?<!\w){re.escape(old_module)}(?![\w.])", new_module, line)


def _replace_relative_from_module(
line: str,
old_module: str,
new_module: str,
) -> str:
"""Replace the module portion of a relative ``from`` import line."""
return re.sub(
rf"^from\s+{re.escape(old_module)}(?=\s+import\b)",
f"from {new_module}",
line,
)


def _resolve_py_relative(source_dir: Path, dots: str, remainder: str) -> str | None:
"""Resolve a relative Python import to an absolute file path."""
dot_count = len(dots)
Expand Down Expand Up @@ -154,9 +167,11 @@ def find_replacements(
if resolved and str(Path(resolved).resolve()) == source_abs:
new_rel = _compute_py_relative_import(importer, dest_abs)
if new_rel:
old_from = f"from {dots}{remainder}"
new_from = f"from {new_rel}"
replacements.append((old_from, new_from))
new_line = _replace_relative_from_module(
stripped, f"{dots}{remainder}", new_rel
)
if new_line != stripped:
replacements.append((stripped, new_line))

if replacements:
changes[importer] = _dedup(replacements)
Expand Down Expand Up @@ -199,10 +214,11 @@ def find_self_replacements(
if not new_rel:
continue

old_from = f"from {dots}{remainder}"
new_from = f"from {new_rel}"
if old_from != new_from:
replacements.append((old_from, new_from))
new_line = _replace_relative_from_module(
stripped, f"{dots}{remainder}", new_rel
)
if new_line != stripped:
replacements.append((stripped, new_line))

return _dedup(replacements)

Expand Down
38 changes: 38 additions & 0 deletions desloppify/languages/python/tests/test_py_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path

import desloppify.languages.python.move as py_move
from desloppify.languages.python.detectors.deps import build_dep_graph


def test_move_py_module_imports():
Expand Down Expand Up @@ -70,3 +71,40 @@ def test_resolve_py_relative_package(self, tmp_path):
def test_resolve_py_relative_not_found(self, tmp_path):
result = py_move._resolve_py_relative(tmp_path, ".", "nonexistent")
assert result is None

def test_relative_import_replacement_uses_full_line(self, tmp_path, monkeypatch):
pkg_dir = tmp_path / "pkg"
pkg_dir.mkdir()
(pkg_dir / "__init__.py").write_text("SOME_CONSTANT = 42\n")
(pkg_dir / "utils.py").write_text("def helper():\n return 1\n")

sub_dir = pkg_dir / "sub"
sub_dir.mkdir()
importer = sub_dir / "importer.py"
importer.write_text(
"from .. import SOME_CONSTANT\nfrom ..utils import helper\n"
)

monkeypatch.setenv("DESLOPPIFY_ROOT", str(tmp_path))

graph = build_dep_graph(tmp_path)
source_abs = str((pkg_dir / "__init__.py").resolve())
dest_abs = str((tmp_path / "newpkg" / "__init__.py").resolve())

changes = py_move.find_replacements(source_abs, dest_abs, graph)
importer_abs = str(importer.resolve())

assert changes[importer_abs] == [
(
"from .. import SOME_CONSTANT",
"from ...newpkg import SOME_CONSTANT",
)
]

content = importer.read_text()
for old_str, new_str in changes[importer_abs]:
content = content.replace(old_str, new_str)

assert content == (
"from ...newpkg import SOME_CONSTANT\nfrom ..utils import helper\n"
)