feat(cli): add planning, recursive discovery, and confirmation flow
Some checks failed
CI (tests + ruff) and stable tag / unittest (py3.10) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.11) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.13) (push) Has been cancelled
CI (tests + ruff) and stable tag / ruff (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / Tag stable (if version commit) (push) Has been cancelled
Some checks failed
CI (tests + ruff) and stable tag / unittest (py3.10) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.11) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / unittest (py3.13) (push) Has been cancelled
CI (tests + ruff) and stable tag / ruff (py3.12) (push) Has been cancelled
CI (tests + ruff) and stable tag / Tag stable (if version commit) (push) Has been cancelled
- Introduce MigrationPlan and refactor migration into plan/apply phases - Add candidate filtering to avoid migrating package internals - Support recursive directory mode (-R) with discovery + plan preview - Add preview (-p) and force (-f) flags with y/N confirmation defaulting to NO - Improve plan output with repo-root relative paths - Expand unittests to cover non-recursive + recursive flows and prompting https://chatgpt.com/share/69468609-0584-800f-a3e0-9d58210fb0e8
This commit is contained in:
@@ -1,34 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from p2pkg.__main__ import migrate_one
|
||||
|
||||
# Ensure we import THIS repo's src/ implementation, not an installed site-packages one.
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC_DIR = REPO_ROOT / "src"
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
|
||||
from p2pkg.__main__ import main, migrate_one # noqa: E402
|
||||
|
||||
|
||||
class TestMigration(unittest.TestCase):
|
||||
def test_migrate_creates_package_and_exports_public_api(self) -> None:
|
||||
def test_migrate_one_creates_package_and_exports_public_api(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
mod = root / "roles_list.py"
|
||||
mod.write_text(
|
||||
textwrap.dedent("""\
|
||||
__all__ = ["add", "PUBLIC_CONST"]
|
||||
PUBLIC_CONST = 123
|
||||
_PRIVATE_CONST = 999
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
__all__ = ["add", "PUBLIC_CONST"]
|
||||
PUBLIC_CONST = 123
|
||||
_PRIVATE_CONST = 999
|
||||
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
def add(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
def _hidden() -> int:
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("running as script")
|
||||
"""),
|
||||
def _hidden() -> int:
|
||||
return 1
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
@@ -39,7 +47,6 @@ class TestMigration(unittest.TestCase):
|
||||
self.assertTrue((pkg / "__init__.py").exists())
|
||||
self.assertFalse(mod.exists())
|
||||
|
||||
# Import the package and ensure re-exports work
|
||||
sys.path.insert(0, str(root))
|
||||
try:
|
||||
import roles_list # type: ignore
|
||||
@@ -49,45 +56,102 @@ class TestMigration(unittest.TestCase):
|
||||
self.assertTrue(hasattr(roles_list, "PUBLIC_CONST"))
|
||||
self.assertEqual(roles_list.PUBLIC_CONST, 123)
|
||||
|
||||
# __all__ should be preserved
|
||||
self.assertEqual(set(roles_list.__all__), {"add", "PUBLIC_CONST"})
|
||||
self.assertFalse(hasattr(roles_list, "_hidden"))
|
||||
finally:
|
||||
sys.path.remove(str(root))
|
||||
if "roles_list" in sys.modules:
|
||||
del sys.modules["roles_list"]
|
||||
sys.modules.pop("roles_list", None)
|
||||
|
||||
def test_migrate_without_explicit_all_exports_public_names(self) -> None:
|
||||
def test_main_non_recursive_prompts_and_applies_on_yes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
mod = root / "foo.py"
|
||||
mod.write_text(
|
||||
textwrap.dedent("""\
|
||||
VALUE = "ok"
|
||||
mod.write_text("X = 1\n", encoding="utf-8")
|
||||
|
||||
def hello() -> str:
|
||||
return "hi"
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), patch("builtins.input", return_value="y"):
|
||||
rc = main(["--no-git", "--repo-root", str(root), str(mod)])
|
||||
|
||||
def _private() -> str:
|
||||
return "no"
|
||||
"""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
migrate_one(mod, use_git=False, repo_root=root)
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertFalse(mod.exists())
|
||||
self.assertTrue((root / "foo" / "__main__.py").exists())
|
||||
self.assertTrue((root / "foo" / "__init__.py").exists())
|
||||
|
||||
sys.path.insert(0, str(root))
|
||||
try:
|
||||
import foo # type: ignore
|
||||
|
||||
self.assertEqual(foo.hello(), "hi")
|
||||
self.assertEqual(foo.VALUE, "ok")
|
||||
self.assertIn("hello", foo.__all__)
|
||||
self.assertIn("VALUE", foo.__all__)
|
||||
self.assertNotIn("_private", foo.__all__)
|
||||
finally:
|
||||
sys.path.remove(str(root))
|
||||
if "foo" in sys.modules:
|
||||
del sys.modules["foo"]
|
||||
def test_main_recursive_preview_does_not_apply_and_does_not_prompt(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
target = root / "tree"
|
||||
target.mkdir()
|
||||
|
||||
(target / "x.py").write_text("X = 1\n", encoding="utf-8")
|
||||
(target / "y.py").write_text("Y = 2\n", encoding="utf-8")
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), patch("builtins.input") as mocked_input:
|
||||
rc = main(["-R", "-p", "--no-git", "--repo-root", str(root), str(target)])
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
mocked_input.assert_not_called()
|
||||
self.assertTrue((target / "x.py").exists())
|
||||
self.assertTrue((target / "y.py").exists())
|
||||
self.assertFalse((target / "x").exists())
|
||||
self.assertFalse((target / "y").exists())
|
||||
|
||||
def test_main_recursive_force_applies_without_prompt(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
target = root / "tree"
|
||||
target.mkdir()
|
||||
|
||||
(target / "x.py").write_text("X = 1\n", encoding="utf-8")
|
||||
(target / "y.py").write_text("Y = 2\n", encoding="utf-8")
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), patch("builtins.input") as mocked_input:
|
||||
rc = main(["-R", "-f", "--no-git", "--repo-root", str(root), str(target)])
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
mocked_input.assert_not_called()
|
||||
self.assertFalse((target / "x.py").exists())
|
||||
self.assertFalse((target / "y.py").exists())
|
||||
self.assertTrue((target / "x" / "__main__.py").exists())
|
||||
self.assertTrue((target / "y" / "__main__.py").exists())
|
||||
|
||||
def test_main_recursive_prompts_and_aborts_on_no(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
target = root / "tree"
|
||||
target.mkdir()
|
||||
|
||||
(target / "x.py").write_text("X = 1\n", encoding="utf-8")
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), patch("builtins.input", return_value="n"):
|
||||
rc = main(["-R", "--no-git", "--repo-root", str(root), str(target)])
|
||||
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertTrue((target / "x.py").exists())
|
||||
self.assertFalse((target / "x").exists())
|
||||
|
||||
def test_main_recursive_prompts_and_applies_on_yes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
target = root / "tree"
|
||||
target.mkdir()
|
||||
|
||||
(target / "x.py").write_text("X = 1\n", encoding="utf-8")
|
||||
(target / "y.py").write_text("Y = 2\n", encoding="utf-8")
|
||||
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), patch("builtins.input", return_value="y"):
|
||||
rc = main(["-R", "--no-git", "--repo-root", str(root), str(target)])
|
||||
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertFalse((target / "x.py").exists())
|
||||
self.assertFalse((target / "y.py").exists())
|
||||
self.assertTrue((target / "x" / "__main__.py").exists())
|
||||
self.assertTrue((target / "y" / "__main__.py").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user