migration: preserve market data and search slice
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MARKER = " # PRESERVATION_METHODS\n"
|
||||
|
||||
|
||||
def method_span(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[int, int]:
|
||||
start = min((decorator.lineno for decorator in node.decorator_list), default=node.lineno)
|
||||
if node.end_lineno is None:
|
||||
raise ValueError(f"Missing end position for {node.name}")
|
||||
return start - 1, node.end_lineno
|
||||
|
||||
|
||||
def move_methods(
|
||||
source_path: Path,
|
||||
class_name: str,
|
||||
target_path: Path,
|
||||
method_names: list[str],
|
||||
) -> None:
|
||||
source = source_path.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(source_path))
|
||||
owner = next(
|
||||
(
|
||||
node
|
||||
for node in tree.body
|
||||
if isinstance(node, ast.ClassDef) and node.name == class_name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if owner is None:
|
||||
raise ValueError(f"Class not found: {class_name}")
|
||||
|
||||
methods = {
|
||||
node.name: node
|
||||
for node in owner.body
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
}
|
||||
missing = [name for name in method_names if name not in methods]
|
||||
if missing:
|
||||
raise ValueError(f"Methods not found in {class_name}: {', '.join(missing)}")
|
||||
|
||||
lines = source.splitlines(keepends=True)
|
||||
ordered = sorted((methods[name] for name in method_names), key=lambda node: node.lineno)
|
||||
blocks = ["".join(lines[start:end]).rstrip() for start, end in map(method_span, ordered)]
|
||||
|
||||
for start, end in sorted(map(method_span, ordered), reverse=True):
|
||||
del lines[start:end]
|
||||
while start < len(lines) - 1 and lines[start] == "\n" and lines[start + 1] == "\n":
|
||||
del lines[start]
|
||||
|
||||
target = target_path.read_text(encoding="utf-8")
|
||||
if target.count(MARKER) != 1:
|
||||
raise ValueError(f"Target must contain exactly one method marker: {target_path}")
|
||||
target = target.replace(MARKER, "\n\n".join(blocks) + "\n")
|
||||
|
||||
source_path.write_text("".join(lines), encoding="utf-8")
|
||||
target_path.write_text(target, encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Mechanically move class methods between modules")
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--class-name", required=True)
|
||||
parser.add_argument("--target", type=Path, required=True)
|
||||
parser.add_argument("methods", nargs="+")
|
||||
args = parser.parse_args()
|
||||
move_methods(args.source, args.class_name, args.target, args.methods)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user