scripts/tests/reproduction_utils/test_move_symbol_delegate.py
scripts/tests/reproduction_utils/test_move_symbol_delegate.pyBrowse 41 files
2,858 tokens
10,351 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_leave_delegate_keeps_forwarding_stub(tmp_path: Path) -> None:26 """With leave_delegate, the source keeps a forwarding stub through the named field and the27 destination gets the full method body."""28 (tmp_path / "src.py").write_text(29 "class Mixin:\n"30 " def compute(self, n: int) -> int:\n"31 " return n + self.cfg.base\n"32 )33 (tmp_path / "dst.py").write_text(34 "class Cfg:\n def existing(self):\n return 0\n"35 )36 r = Repro("b", "t").move_symbol(37 "compute",38 src="src.py",39 dst="dst.py",40 into_class="Cfg",41 leave_delegate="cfg",42 )43 _apply(r, tmp_path)44 src_out = (tmp_path / "src.py").read_text()45 dst_out = (tmp_path / "dst.py").read_text()46 assert "def compute(self, n: int) -> int:" in src_out47 assert "return self.cfg.compute(n)" in src_out48 assert "return n + self.cfg.base" not in src_out49 assert "return n + self.cfg.base" in dst_out50 51 52def test_move_symbol_leave_delegate_does_not_absorb_leading_comments(53 tmp_path: Path,54) -> None:55 """A leading comment before the first statement is not an AST node, so it must not be56 pulled into the forwarding stub -- the delegate is just the header plus the return.57 """58 (tmp_path / "src.py").write_text(59 "class Mixin:\n"60 " def compute(self, n: int) -> int:\n"61 " # explain the maths\n"62 " # second comment line\n"63 " return n + self.cfg.base\n"64 )65 (tmp_path / "dst.py").write_text(66 "class Cfg:\n def existing(self):\n return 0\n"67 )68 r = Repro("b", "t").move_symbol(69 "compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg"70 )71 _apply(r, tmp_path)72 src_out = (tmp_path / "src.py").read_text()73 dst_out = (tmp_path / "dst.py").read_text()74 assert "# explain the maths" not in src_out75 assert (76 src_out == "class Mixin:\n"77 " def compute(self, n: int) -> int:\n"78 " return self.cfg.compute(n)\n"79 )80 assert "# explain the maths" in dst_out81 82 83# --- adversarial audit: move_symbol edge cases -----------------------------------84 85 86# --- adversarial audit: leave_delegate stubs -------------------------------------87 88 89def test_move_symbol_leave_delegate_keeps_a_multiline_signature_verbatim(90 tmp_path: Path,91) -> None:92 """A multi-line header is carried into the stub byte-for-byte via the bracket scan."""93 (tmp_path / "src.py").write_text(94 "class Mixin:\n"95 " def compute(\n"96 " self,\n"97 " n: int,\n"98 " *,\n"99 " scale: float = 1.0,\n"100 " ) -> int:\n"101 " return int(n * scale) + self.cfg.base\n"102 )103 (tmp_path / "dst.py").write_text("class Cfg:\n def e(self):\n return 0\n")104 r = Repro("b", "t").move_symbol(105 "compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg"106 )107 _apply(r, tmp_path)108 assert (tmp_path / "src.py").read_text() == (109 "class Mixin:\n"110 " def compute(\n"111 " self,\n"112 " n: int,\n"113 " *,\n"114 " scale: float = 1.0,\n"115 " ) -> int:\n"116 " return self.cfg.compute(n, scale=scale)\n"117 )118 assert (tmp_path / "dst.py").read_text() == (119 "class Cfg:\n"120 " def e(self):\n"121 " return 0\n"122 "\n"123 " def compute(\n"124 " self,\n"125 " n: int,\n"126 " *,\n"127 " scale: float = 1.0,\n"128 " ) -> int:\n"129 " return int(n * scale) + self.cfg.base\n"130 )131 132 133def test_move_symbol_leave_delegate_forwards_posonly_vararg_kwonly_kwargs(134 tmp_path: Path,135) -> None:136 """Every parameter kind is forwarded correctly in the delegate's return call."""137 (tmp_path / "src.py").write_text(138 "class Mixin:\n"139 " def compute(self, a, /, b, *args, c, d=3, **kw):\n"140 " return a\n"141 )142 (tmp_path / "dst.py").write_text(143 "class Cfg:\n def keep(self):\n return 0\n"144 )145 r = Repro("b", "t").move_symbol(146 "compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg"147 )148 _apply(r, tmp_path)149 assert (tmp_path / "src.py").read_text() == (150 "class Mixin:\n"151 " def compute(self, a, /, b, *args, c, d=3, **kw):\n"152 " return self.cfg.compute(a, b, *args, c=c, d=d, **kw)\n"153 )154 155 156def test_move_symbol_leave_delegate_survives_paren_in_string_default(157 tmp_path: Path,158) -> None:159 """A string default containing '(' must not break the delegate's header scan."""160 (tmp_path / "src.py").write_text(161 "class Mixin:\n"162 " def compute(\n"163 " self,\n"164 ' sep: str = "(",\n'165 " n: int = 0,\n"166 " ) -> int:\n"167 " return n + self.cfg.base\n"168 )169 (tmp_path / "dst.py").write_text(170 "class Cfg:\n def keep(self):\n return 0\n"171 )172 r = Repro("b", "t").move_symbol(173 "compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg"174 )175 _apply(r, tmp_path)176 src_out = (tmp_path / "src.py").read_text()177 compile(src_out, "src.py", "exec")178 assert "return self.cfg.compute(sep, n)" in src_out179 180 181def test_move_symbol_async_leave_delegate_awaits_the_forwarded_call(182 tmp_path: Path,183) -> None:184 """An async method's delegate stub must await the forwarded coroutine."""185 (tmp_path / "src.py").write_text(186 "class Mixin:\n"187 " async def compute(self, n):\n"188 " return n + self.cfg.base\n"189 )190 (tmp_path / "dst.py").write_text("class Cfg:\n def e(self):\n return 0\n")191 r = Repro("b", "t").move_symbol(192 "compute", src="src.py", dst="dst.py", into_class="Cfg", leave_delegate="cfg"193 )194 _apply(r, tmp_path)195 assert "return await self.cfg.compute(n)" in (tmp_path / "src.py").read_text()196 197 198def test_move_symbol_leave_delegate_on_self_annotated_staticmethod(199 tmp_path: Path,200) -> None:201 """A de-self'd staticmethod (self: Target) moves into Target; the stub drops the202 decorator and the self annotation."""203 (tmp_path / "src.py").write_text(204 "class Runner:\n"205 " @staticmethod\n"206 " def work(self: Comp, n: int) -> int:\n"207 " return n + self.base\n"208 )209 (tmp_path / "dst.py").write_text(210 "class Comp:\n def existing(self):\n return 0\n"211 )212 r = Repro("b", "t").move_symbol(213 "work",214 src="src.py",215 dst="dst.py",216 into_class="Comp",217 from_class="Runner",218 drop_self_annotation=True,219 leave_delegate="comp",220 )221 _apply(r, tmp_path)222 assert (tmp_path / "src.py").read_text() == (223 "class Runner:\n"224 " def work(self, n: int) -> int:\n"225 " return self.comp.work(n)\n"226 )227 assert (tmp_path / "dst.py").read_text() == (228 "class Comp:\n"229 " def existing(self):\n"230 " return 0\n"231 "\n"232 " def work(self, n: int) -> int:\n"233 " return n + self.base\n"234 )235 236 237def test_move_symbol_leave_delegate_keeps_unrelated_self_annotation(238 tmp_path: Path,239) -> None:240 """A self annotation naming a class other than the destination survives in the stub."""241 (tmp_path / "src.py").write_text(242 "class Mixin:\n"243 " def work(self: Runner, n: int) -> int:\n"244 " return n + self.base\n"245 )246 (tmp_path / "dst.py").write_text(247 "class Comp:\n def existing(self):\n return 0\n"248 )249 r = Repro("b", "t").move_symbol(250 "work",251 src="src.py",252 dst="dst.py",253 into_class="Comp",254 from_class="Mixin",255 drop_self_annotation=True,256 leave_delegate="comp",257 )258 _apply(r, tmp_path)259 assert (tmp_path / "src.py").read_text() == (260 "class Mixin:\n"261 " def work(self: Runner, n: int) -> int:\n"262 " return self.comp.work(n)\n"263 )264 265 266def test_move_symbol_delegate_name_forwards_to_the_renamed_collaborator_method(267 tmp_path: Path,268) -> None:269 """delegate_name makes the stub call a differently-named method on the collaborator."""270 (tmp_path / "src.py").write_text(271 "class Mixin:\n"272 " def compute(self, n: int) -> int:\n"273 " return n + self.cfg.base\n"274 )275 (tmp_path / "dst.py").write_text(276 "class Cfg:\n def existing(self):\n return 0\n"277 )278 r = Repro("b", "t").move_symbol(279 "compute",280 src="src.py",281 dst="dst.py",282 into_class="Cfg",283 leave_delegate="cfg",284 delegate_name="compute_impl",285 )286 _apply(r, tmp_path)287 assert "return self.cfg.compute_impl(n)" in (tmp_path / "src.py").read_text()288 assert "def compute(self, n: int) -> int:" in (tmp_path / "dst.py").read_text()289 290 291def test_move_symbol_leave_delegate_on_unannotated_staticmethod_raises(292 tmp_path: Path,293) -> None:294 """A staticmethod with no self: Target annotation has no receiver to forward through, so295 leave_delegate refuses rather than author a bogus self.<field>.<name>(...) stub."""296 (tmp_path / "src.py").write_text(297 "class Runner:\n"298 " @staticmethod\n"299 " def work(x: int) -> int:\n"300 " return x + 1\n"301 )302 (tmp_path / "dst.py").write_text(303 "class Comp:\n def existing(self):\n return 0\n"304 )305 r = Repro("b", "t").move_symbol(306 "work",307 src="src.py",308 dst="dst.py",309 into_class="Comp",310 leave_delegate="comp",311 )312 with pytest.raises(AssertionError):313 _apply(r, tmp_path)314