scripts/tests/reproduction_utils/test_move_symbol.py
scripts/tests/reproduction_utils/test_move_symbol.pyBrowse 41 files
4,610 tokens
16,123 bytes
Token encoding: o200k_base
Snapshot a9fb1c3
← Back to SKILL.md
1import subprocess2import sys3from pathlib import Path4 5import pytest6 7sys.path.insert(0, str(Path(__file__).resolve().parents[2]))8 9import mechanical_refactor_reproduction_utils as rr10from mechanical_refactor_reproduction_utils import (11 Repro,12 _def_span,13 _find_class,14 _find_def,15 _replace_span,16 _slice_span,17 dedent,18 exec_command,19 git_add_and_commit,20 verify_mechanical_refactor,21)22from reproduction_testlib import _apply, _commit, _git, _write # noqa: F40123 24 25def test_move_symbol_drops_self_annotation_into_class(tmp_path: Path) -> None:26 """Moving a `def foo(self: Target)` into Target drops the now-redundant annotation."""27 (tmp_path / "src.py").write_text(28 "class M:\n"29 " @staticmethod\n"30 " def foo(self: Target, x):\n"31 " return self.y + x\n"32 )33 (tmp_path / "dst.py").write_text(34 "class Target:\n def keep(self):\n return 1\n"35 )36 r = Repro("b", "t").move_symbol(37 "foo",38 src="src.py",39 dst="dst.py",40 into_class="Target",41 dedent=0,42 drop_self_annotation=True,43 )44 _apply(r, tmp_path)45 text = (tmp_path / "dst.py").read_text()46 assert "def foo(self, x):" in text47 assert "self: Target" not in text48 49 50# --- adversarial audit: import primitives ----------------------------------------51 52 53# --- move_symbol ---------------------------------------------------------------54 55 56def test_move_symbol_into_class_drops_decorator_and_appends(tmp_path: Path) -> None:57 """The def leaves the source, its @staticmethod is dropped, and it lands at the end of58 the destination class with its body verbatim."""59 (tmp_path / "src.py").write_text(60 "class Old:\n"61 " @staticmethod\n"62 " def foo(x):\n"63 " return x + 1\n"64 "\n"65 " def keep(self):\n"66 " return 0\n"67 )68 (tmp_path / "dst.py").write_text(69 "class New:\n def existing(self):\n return 1\n"70 )71 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")72 _apply(r, tmp_path)73 src_out = (tmp_path / "src.py").read_text()74 dst_out = (tmp_path / "dst.py").read_text()75 assert "def foo" not in src_out and "def keep" in src_out76 assert "@staticmethod" not in dst_out77 assert dst_out.index("def existing") < dst_out.index("def foo")78 assert " return x + 1\n" in dst_out79 80 81def test_move_symbol_to_module_level_with_dedent(tmp_path: Path) -> None:82 """With into_class=None and a dedent, the def lands at module level, dedented."""83 (tmp_path / "src.py").write_text(84 "class Old:\n @staticmethod\n def helper(x):\n return x * 2\n"85 )86 (tmp_path / "dst.py").write_text("import os\n")87 r = Repro("b", "t").move_symbol(88 "helper", src="src.py", dst="dst.py", into_class=None, dedent=489 )90 _apply(r, tmp_path)91 assert "def helper(x):\n return x * 2\n" in (tmp_path / "dst.py").read_text()92 assert "def helper" not in (tmp_path / "src.py").read_text()93 94 95def test_move_symbol_before_inserts_above_named_sibling(tmp_path: Path) -> None:96 """With before=, the relocated def lands immediately above that sibling, not at the end."""97 (tmp_path / "src.py").write_text(98 "class Old:\n @staticmethod\n def moved(self):\n return 1\n"99 )100 (tmp_path / "dst.py").write_text(101 "class New:\n"102 " def first(self):\n return 0\n"103 "\n"104 " def last(self):\n return 2\n"105 )106 r = Repro("b", "t").move_symbol(107 "moved", src="src.py", dst="dst.py", into_class="New", before="last"108 )109 _apply(r, tmp_path)110 dst_out = (tmp_path / "dst.py").read_text()111 assert (112 dst_out.index("def first")113 < dst_out.index("def moved")114 < dst_out.index("def last")115 )116 117 118# --- adversarial audit: move_symbol edge cases -----------------------------------119 120 121def test_move_symbol_moves_an_async_def_verbatim(tmp_path: Path) -> None:122 """An async def relocates with its `async` keyword and body byte-identical."""123 (tmp_path / "src.py").write_text(124 "class Old:\n"125 " async def foo(self):\n"126 " return 1\n"127 "\n"128 " def keep(self):\n"129 " return 0\n"130 )131 (tmp_path / "dst.py").write_text("class New:\n def e(self):\n return 1\n")132 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")133 _apply(r, tmp_path)134 assert (tmp_path / "src.py").read_text() == (135 "class Old:\n\n def keep(self):\n return 0\n"136 )137 assert (tmp_path / "dst.py").read_text() == (138 "class New:\n"139 " def e(self):\n"140 " return 1\n"141 "\n"142 " async def foo(self):\n"143 " return 1\n"144 )145 146 147def test_move_symbol_moves_a_def_within_the_same_file(tmp_path: Path) -> None:148 """With src == dst the def is cut and re-inserted above its sibling in one file."""149 (tmp_path / "m.py").write_text(150 "def a():\n return 1\n\n\ndef b():\n return 2\n"151 )152 r = Repro("b", "t").move_symbol(153 "b", src="m.py", dst="m.py", into_class=None, before="a"154 )155 _apply(r, tmp_path)156 assert (tmp_path / "m.py").read_text() == (157 "def b():\n return 2\n\ndef a():\n return 1\n\n\n"158 )159 160 161def test_move_symbol_prefers_a_module_level_def_over_an_earlier_class_method(162 tmp_path: Path,163) -> None:164 """When a class method and a module-level def share a name, the module-level def moves."""165 (tmp_path / "src.py").write_text(166 "class C:\n"167 " def foo(self):\n"168 " return 'method'\n"169 "\n"170 "\n"171 "def foo():\n"172 " return 'module'\n"173 )174 (tmp_path / "dst.py").write_text("x = 1\n")175 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)176 _apply(r, tmp_path)177 assert (tmp_path / "src.py").read_text() == (178 "class C:\n def foo(self):\n return 'method'\n\n\n"179 )180 assert (tmp_path / "dst.py").read_text() == (181 "x = 1\n\ndef foo():\n return 'module'\n"182 )183 184 185def test_move_symbol_keeps_a_real_decorator_while_dropping_classmethod(186 tmp_path: Path,187) -> None:188 """@classmethod is shed on the move but any other decorator travels verbatim."""189 (tmp_path / "src.py").write_text(190 "import functools\n"191 "\n"192 "\n"193 "class Old:\n"194 " @classmethod\n"195 " @functools.lru_cache(maxsize=None)\n"196 " def foo(cls, x):\n"197 " return x + 1\n"198 )199 (tmp_path / "dst.py").write_text("def z():\n return 0\n")200 r = Repro("b", "t").move_symbol(201 "foo", src="src.py", dst="dst.py", into_class=None, dedent=4202 )203 _apply(r, tmp_path)204 assert (tmp_path / "dst.py").read_text() == (205 "def z():\n"206 " return 0\n"207 "\n"208 "@functools.lru_cache(maxsize=None)\n"209 "def foo(cls, x):\n"210 " return x + 1\n"211 )212 213 214def test_move_symbol_leaves_a_comment_above_the_def_in_the_source(215 tmp_path: Path,216) -> None:217 """A comment above the def is not part of its span, so it stays behind in the source."""218 (tmp_path / "src.py").write_text(219 "# explains foo\ndef foo():\n return 1\n\n\ndef keep():\n return 2\n"220 )221 (tmp_path / "dst.py").write_text("x = 1\n")222 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)223 _apply(r, tmp_path)224 assert (tmp_path / "src.py").read_text() == (225 "# explains foo\n\n\ndef keep():\n return 2\n"226 )227 assert (tmp_path / "dst.py").read_text() == "x = 1\n\ndef foo():\n return 1\n"228 229 230def test_move_symbol_without_trailing_newlines_keeps_moved_bytes(231 tmp_path: Path,232) -> None:233 """Files lacking a final newline lose no bytes of the moved def or the remainder."""234 (tmp_path / "src.py").write_text(235 "def keep():\n return 0\n\n\ndef foo():\n return 1"236 )237 (tmp_path / "dst.py").write_text("x = 1")238 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class=None)239 _apply(r, tmp_path)240 assert (tmp_path / "src.py").read_text() == "def keep():\n return 0\n\n\n"241 assert (tmp_path / "dst.py").read_text() == "x = 1\ndef foo():\n return 1"242 243 244def test_move_symbol_dedent_leaves_string_literal_interior_lines(245 tmp_path: Path,246) -> None:247 """Dedent only strips lines with exactly n leading spaces, so string interiors survive."""248 (tmp_path / "src.py").write_text(249 "class Old:\n"250 " class Deep:\n"251 " def foo(self):\n"252 " s = '''raw\n"253 " partial\n"254 "'''\n"255 " return s\n"256 )257 (tmp_path / "dst.py").write_text("import os\n")258 r = Repro("b", "t").move_symbol(259 "foo", src="src.py", dst="dst.py", into_class=None, dedent=8260 )261 _apply(r, tmp_path)262 assert (tmp_path / "dst.py").read_text() == (263 "import os\n\ndef foo(self):\n s = '''raw\n partial\n'''\n return s\n"264 )265 266 267def test_move_symbol_asserts_when_destination_class_missing(tmp_path: Path) -> None:268 """Naming an into_class absent from the destination fails loudly."""269 (tmp_path / "src.py").write_text("def foo():\n return 1\n")270 (tmp_path / "dst.py").write_text("x = 1\n")271 r = Repro("b", "t").move_symbol(272 "foo", src="src.py", dst="dst.py", into_class="Nope"273 )274 with pytest.raises(AssertionError):275 _apply(r, tmp_path)276 277 278def test_move_symbol_preserves_staticmethod_inside_moved_body(tmp_path: Path) -> None:279 """A @staticmethod on a nested def inside the moved body must survive the move."""280 (tmp_path / "src.py").write_text(281 "class Old:\n"282 " @staticmethod\n"283 " def foo(x):\n"284 " class Inner:\n"285 " @staticmethod\n"286 " def helper(y):\n"287 " return y\n"288 " return Inner.helper(x)\n"289 )290 (tmp_path / "dst.py").write_text(291 "class New:\n def keep(self):\n return 0\n"292 )293 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")294 _apply(r, tmp_path)295 dst_out = (tmp_path / "dst.py").read_text()296 assert " @staticmethod\n def helper(y):\n" in dst_out297 298 299def test_move_symbol_rejects_ambiguous_duplicate_names(tmp_path: Path) -> None:300 """Two same-named defs at equal depth must raise instead of silently picking one."""301 (tmp_path / "src.py").write_text(302 "class A:\n"303 " def foo(self):\n"304 " return 'A'\n"305 "\n"306 "class B:\n"307 " def foo(self):\n"308 " return 'B'\n"309 )310 (tmp_path / "dst.py").write_text(311 "class New:\n def keep(self):\n return 0\n"312 )313 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")314 with pytest.raises(AssertionError):315 _apply(r, tmp_path)316 317 318def test_move_symbol_after_inserts_below_named_function(tmp_path: Path) -> None:319 """With after=, the relocated def lands immediately below that sibling def."""320 (tmp_path / "src.py").write_text("def moved():\n return 1\n")321 (tmp_path / "dst.py").write_text(322 "def first():\n return 0\n\n\ndef last():\n return 2\n"323 )324 r = Repro("b", "t").move_symbol(325 "moved", src="src.py", dst="dst.py", into_class=None, after="first"326 )327 _apply(r, tmp_path)328 dst_out = (tmp_path / "dst.py").read_text()329 assert (330 dst_out.index("def first")331 < dst_out.index("def moved")332 < dst_out.index("def last")333 )334 335 336def test_move_symbol_after_assign_lands_before_typechecking_guard(337 tmp_path: Path,338) -> None:339 """after= anchors on a module-level assignment target, landing the def just below it and340 above a following ``if TYPE_CHECKING:`` guard (which is not a nameable anchor)."""341 (tmp_path / "src.py").write_text("def helper(x):\n return x + 1\n")342 (tmp_path / "dst.py").write_text(343 "from u import is_hip\n"344 "\n"345 "_is_hip = is_hip()\n"346 "\n"347 "if TYPE_CHECKING:\n"348 " from m import Thing\n"349 )350 r = Repro("b", "t").move_symbol(351 "helper", src="src.py", dst="dst.py", into_class=None, after="_is_hip"352 )353 _apply(r, tmp_path)354 dst_out = (tmp_path / "dst.py").read_text()355 assert (356 dst_out.index("_is_hip = is_hip()")357 < dst_out.index("def helper")358 < dst_out.index("if TYPE_CHECKING:")359 )360 361 362def test_move_symbol_before_and_after_are_mutually_exclusive(tmp_path: Path) -> None:363 """Passing both before= and after= is rejected up front."""364 (tmp_path / "src.py").write_text("def moved():\n return 1\n")365 (tmp_path / "dst.py").write_text("def z():\n return 0\n")366 with pytest.raises(AssertionError):367 Repro("b", "t").move_symbol(368 "moved",369 src="src.py",370 dst="dst.py",371 into_class=None,372 before="z",373 after="z",374 )375 376 377def test_move_symbol_asserts_when_after_symbol_missing(tmp_path: Path) -> None:378 """An after= anchor absent from the destination must raise, not fall back to append."""379 (tmp_path / "src.py").write_text("def moved():\n return 1\n")380 (tmp_path / "dst.py").write_text("def z():\n return 0\n")381 r = Repro("b", "t").move_symbol(382 "moved", src="src.py", dst="dst.py", into_class=None, after="NO_SUCH_SYMBOL"383 )384 with pytest.raises(AssertionError):385 _apply(r, tmp_path)386 387 388def test_move_symbol_asserts_when_before_sibling_missing(tmp_path: Path) -> None:389 """A before= anchor absent from the destination must raise, not fall back to append."""390 (tmp_path / "src.py").write_text("def moved():\n return 1\n")391 (tmp_path / "dst.py").write_text("def z():\n return 0\n")392 r = Repro("b", "t").move_symbol(393 "moved", src="src.py", dst="dst.py", into_class=None, before="NO_SUCH_DEF"394 )395 with pytest.raises(AssertionError):396 _apply(r, tmp_path)397 398 399def test_move_symbol_preserves_crlf_line_endings(tmp_path: Path) -> None:400 """Moving a def in a CRLF file must keep every line ending CRLF."""401 (tmp_path / "src.py").write_bytes(402 b"class Old:\r\n def foo(self):\r\n return 1\r\n"403 )404 (tmp_path / "dst.py").write_bytes(405 b"class New:\r\n def keep(self):\r\n return 0\r\n"406 )407 r = Repro("b", "t").move_symbol("foo", src="src.py", dst="dst.py", into_class="New")408 _apply(r, tmp_path)409 dst_bytes = (tmp_path / "dst.py").read_bytes()410 assert dst_bytes.count(b"\n") == dst_bytes.count(b"\r\n")411 412 413def test_move_symbol_negative_dedent_indents_into_the_class(tmp_path: Path) -> None:414 """Moving a module-level def into a class with dedent=-4 must indent it as a method."""415 (tmp_path / "src.py").write_text("def helper(x):\n return x\n")416 (tmp_path / "dst.py").write_text("class New:\n def e(self):\n return 0\n")417 r = Repro("b", "t").move_symbol(418 "helper", src="src.py", dst="dst.py", into_class="New", dedent=-4419 )420 _apply(r, tmp_path)421 assert " def helper(x):\n return x\n" in (tmp_path / "dst.py").read_text()422 423 424def test_move_symbol_relocates_a_top_level_class(tmp_path: Path) -> None:425 """move_symbol relocates a whole top-level class (with its methods) verbatim."""426 (tmp_path / "src.py").write_text(427 "x = 1\n\n\nclass Widget:\n def get(self, rank):\n return rank\n"428 )429 (tmp_path / "dst.py").write_text("y = 2\n")430 r = Repro("b", "t").move_symbol(431 "Widget", src="src.py", dst="dst.py", into_class=None432 )433 _apply(r, tmp_path)434 assert "class Widget:" not in (tmp_path / "src.py").read_text()435 assert (436 "class Widget:\n def get(self, rank):\n return rank\n"437 in (tmp_path / "dst.py").read_text()438 )439 440 441# --- adversarial audit: leave_delegate stubs -------------------------------------442