Skip to content

CLI Reference

Commands

axm-anvil move

Move top-level symbols (classes, functions, constants) between Python files atomically. Wraps the MoveTool MCP tool.

Bash
axm-anvil move <from_file> <to_file> <symbols> [--dry-run] [--check] [--path <root>] [--shared-helpers <strategy>] [--reexport] [--rename '<json>'] [--insert-after <symbol>] [--no-include-helpers] [--side-effect-decorators '<csv>']
Argument Description
from_file Source Python file path
to_file Target Python file path
symbols Comma-separated symbol names to move
--dry-run Preview the move without writing files
--check Simulate the move, including import-cycle detection, without writing. Fails with ImportCycleError if the move would introduce a new cycle
--path Workspace root (default: .)
--shared-helpers Strategy when a helper is used by both moved and remaining symbols: duplicate (default, copies the helper and emits a warning) or error (abort with SharedHelpersError)
--reexport Leave callers untouched; inject from new_module import <Symbol> # re-export for backwards compat into the source module for gradual migration
--rename JSON object string mapping old symbol names to new ones (e.g. '{"OldName": "NewName"}'). Renames moved definitions and rewrites all caller references to the new name. Incompatible with --reexport
--insert-after Name of an existing top-level symbol in the target module; moved blocks are spliced immediately after it. Omitted (default) appends the blocks at the end of the target; naming an absent symbol appends at the end and records a warning on MovePlan.warnings. Imports and constants keep their historical placement regardless
--include-helpers / --no-include-helpers Whether to copy transitively-referenced local helpers and constants into the target. --include-helpers (default) preserves the historical copy behaviour. --no-include-helpers leaves the moved code referencing those helpers without copying them, short-circuits the --shared-helpers classification, and records a include_helpers=False: not copied into target: <names> warning on MovePlan.warnings. Imports required by the moved code are always copied regardless
--side-effect-decorators Comma-separated extra side-effect decorator dotted-names (e.g. 'mylib.register') that extend the built-in SIDE_EFFECT_DECORATORS whitelist. When a moved symbol carries a matching decorator, a non-blocking warning is recorded on MovePlan.warnings; the move always proceeds

MCP Tools

MoveTool

Registered as ast_move via the axm.tools entry point. Accepts the same fields as the CLI and returns a ToolResult with the move plan (moved symbols, copied imports/constants, warnings).

MoveTool

Bases: AXMTool

Move top-level symbols between Python files atomically.

Registered as ast_move via the axm.tools entry point. Delegates to :func:axm_anvil.core.move.move_symbols and adapts exceptions into ToolResult(success=False).

Source code in packages/axm-anvil/src/axm_anvil/tools/move.py
Python
class MoveTool(AXMTool):
    """Move top-level symbols between Python files atomically.

    Registered as ``ast_move`` via the ``axm.tools`` entry point.
    Delegates to :func:`axm_anvil.core.move.move_symbols` and adapts
    exceptions into ``ToolResult(success=False)``.
    """

    agent_hint: str = (
        "Move classes, functions, or constants between Python files atomically. "
        "Use dry_run=True to preview changes."
    )

    @property
    def name(self) -> str:
        """Return tool name for registry lookup."""
        return "ast_move"

    @staticmethod
    def _normalize_execute_args(
        path: str,
        symbols: str,
        from_file: str,
        to_file: str,
    ) -> tuple[Path, Path, Path, list[str]]:
        symbol_list = [s.strip() for s in symbols.split(",") if s.strip()]
        root = Path(path).resolve()
        src_path = Path(from_file)
        tgt_path = Path(to_file)
        if not src_path.is_absolute():
            src_path = root / src_path
        if not tgt_path.is_absolute():
            tgt_path = root / tgt_path
        return root, src_path, tgt_path, symbol_list

    @staticmethod
    def _parse_decorators(spec: str | None) -> frozenset[str] | None:
        if spec is None:
            return None
        return frozenset(entry.strip() for entry in spec.split(",") if entry.strip())

    @staticmethod
    def _build_result_data(
        plan: MovePlan,
        src_path: Path,
        tgt_path: Path,
        *,
        reexport: bool,
        check: bool,
    ) -> dict[str, object]:
        data: dict[str, object] = {
            "moved": [
                {"symbol": name, "from_lines": [], "to_lines": []}
                for name in plan.moved_names
            ],
            "dependencies_copied": {
                "imports": list(plan.imports_added),
                "constants": list(plan.constants_added),
            },
            "callers_updated": [
                {
                    "file": entry.file,
                    "line": entry.line,
                    "old": entry.old,
                    "new": entry.new,
                }
                for entry in plan.callers_updated
            ],
            "orphans_removed": [],
            "warnings": list(plan.warnings),
            "shared_helpers_detected": [
                {
                    "name": det.name,
                    "used_by_moved": list(det.used_by_moved),
                    "used_by_remaining": list(det.used_by_remaining),
                }
                for det in plan.shared_helpers_detected
            ],
            "files_modified": [str(src_path), str(tgt_path)],
        }
        if reexport:
            data["reexport"] = True
        if check:
            data["check"] = True
        return data

    @staticmethod
    def _exception_to_result(exc: Exception) -> ToolResult:
        match exc:
            case SymbolNotFoundError():
                return ToolResult(
                    success=False,
                    error=f"Symbol {exc!s} not found in source module",
                )
            case SymbolAlreadyExistsError():
                return ToolResult(
                    success=False,
                    error=f"Symbol {exc!s} already exists in target module",
                )
            case SharedHelpersError():
                joined = ", ".join(exc.shared_helpers)
                return ToolResult(
                    success=False,
                    error=f"Shared helpers detected: {joined}",
                )
            case ImportCycleError():
                return ToolResult(success=False, error=str(exc))
            case _:
                return ToolResult(success=False, error=str(exc))

    def execute(  # noqa: PLR0913
        self,
        *,
        path: str = ".",
        symbols: str = "",
        from_file: str = "",
        to_file: str = "",
        dry_run: bool = False,
        shared_helpers: str = "duplicate",
        shared_helpers_module: str | None = None,
        reexport: bool = False,
        rename: str | None = None,
        check: bool = False,
        insert_after: str | None = None,
        include_helpers: bool = True,
        side_effect_decorators: str | None = None,
        **kwargs: object,
    ) -> ToolResult:
        """Move ``symbols`` (CSV) from ``from_file`` to ``to_file``.

        Parameters
        ----------
        path:
            Workspace root used to resolve relative ``from_file`` / ``to_file``
            and to constrain caller updates.
        symbols:
            Comma-separated list of top-level symbol names to move. Empty
            entries are ignored.
        from_file:
            Source Python file. Relative paths are resolved against ``path``.
        to_file:
            Target Python file. Relative paths are resolved against ``path``.
        dry_run:
            When ``True``, compute the :class:`MovePlan` without writing.
        shared_helpers:
            Policy for helpers used by both moved and remaining symbols:
            ``"duplicate"``, ``"extract"``, or ``"error"``.
        shared_helpers_module:
            Target module path used when ``shared_helpers="extract"``.
        reexport:
            When ``True``, leave callers untouched and inject a re-export in
            the source module. Incompatible with ``rename``.
        rename:
            Optional JSON object string mapping old symbol names to new ones
            (e.g. ``'{"OldName": "NewName"}'``). Parsed to ``dict[str, str]``
            and forwarded to :func:`move_symbols`. Invalid JSON yields a
            ``success=False`` result.
        insert_after:
            Optional name of a top-level symbol in the target module; moved
            blocks are spliced immediately after it. When ``None`` blocks
            append at the end; an absent name appends at the end with a
            warning.
        include_helpers:
            When ``True`` (default) transitively-referenced local helpers and
            constants are copied into the target. When ``False`` they are not
            copied (a warning enumerates the un-copied names); imports are
            still copied regardless.
        side_effect_decorators:
            Optional comma-separated list of extra side-effect decorator
            dotted-names that extend the built-in ``SIDE_EFFECT_DECORATORS``
            whitelist. A moved symbol decorated with a matching decorator
            yields a non-blocking warning on the plan.

        Returns
        -------
        ToolResult
            ``success=True`` with a ``MovePlan`` summary on success; otherwise
            ``success=False`` with a message describing the failure
            (missing symbol, collision, shared helpers, validation error).
        """
        root, src_path, tgt_path, symbol_list = self._normalize_execute_args(
            path, symbols, from_file, to_file
        )

        extra_decorators = self._parse_decorators(side_effect_decorators)

        rename_map: dict[str, str] | None = None
        if rename is not None:
            try:
                rename_map = json.loads(rename)
            except json.JSONDecodeError as exc:
                return ToolResult(success=False, error=f"invalid JSON in rename: {exc}")

        try:
            plan = move_symbols(
                src_path,
                tgt_path,
                symbol_list,
                dry_run=dry_run,
                workspace_root=root,
                shared_helpers=shared_helpers,
                shared_helpers_module=shared_helpers_module,
                reexport=reexport,
                rename=rename_map,
                check=check,
                insert_after=insert_after,
                include_helpers=include_helpers,
                side_effect_decorators=extra_decorators,
            )
        except Exception as exc:  # noqa: BLE001
            return self._exception_to_result(exc)

        data = self._build_result_data(
            plan, src_path, tgt_path, reexport=reexport, check=check
        )
        text = self._format_text(
            plan,
            from_file=str(src_path),
            to_file=str(tgt_path),
            reexport=reexport,
        )
        return ToolResult(success=True, data=data, text=text)

    def _format_text(
        self,
        plan: MovePlan,
        *,
        from_file: str,
        to_file: str,
        reexport: bool = False,
    ) -> str:
        """Render the move plan as compact text per spec §14.2."""
        n = len(plan.moved_names)
        src_name = Path(from_file).name or from_file
        tgt_name = Path(to_file).name or to_file
        lines: list[str] = [
            f"ast_move | {n} symbols | {src_name} \u2192 {tgt_name}",
            "",
        ]
        if reexport:
            lines.append("Mode: reexport")
            lines.append("")
        lines.append("Moved:")
        for name in plan.moved_names:
            lines.append(f"  - {name}")
        lines.append("")
        lines.append("Dependencies:")
        lines.append(f"  imports: {len(plan.imports_added)}")
        lines.append(f"  constants: {len(plan.constants_added)}")
        lines.append("")
        lines.append(f"Callers Updated: {len(plan.callers_updated)}")
        if plan.shared_helpers_detected:
            lines.append("")
            lines.append("Shared Helpers:")
            for det in plan.shared_helpers_detected:
                lines.append(
                    f"  - {det.name} (also used by: {', '.join(det.used_by_remaining)})"
                )
        if plan.warnings:
            lines.append("")
            lines.append("Warnings:")
            for warning in plan.warnings:
                lines.append(f"  - {warning}")
        return "\n".join(lines)

name property

Return tool name for registry lookup.

execute(*, path='.', symbols='', from_file='', to_file='', dry_run=False, shared_helpers='duplicate', shared_helpers_module=None, reexport=False, rename=None, check=False, insert_after=None, include_helpers=True, side_effect_decorators=None, **kwargs)

Move symbols (CSV) from from_file to to_file.

Parameters

path: Workspace root used to resolve relative from_file / to_file and to constrain caller updates. symbols: Comma-separated list of top-level symbol names to move. Empty entries are ignored. from_file: Source Python file. Relative paths are resolved against path. to_file: Target Python file. Relative paths are resolved against path. dry_run: When True, compute the :class:MovePlan without writing. shared_helpers: Policy for helpers used by both moved and remaining symbols: "duplicate", "extract", or "error". shared_helpers_module: Target module path used when shared_helpers="extract". reexport: When True, leave callers untouched and inject a re-export in the source module. Incompatible with rename. rename: Optional JSON object string mapping old symbol names to new ones (e.g. '{"OldName": "NewName"}'). Parsed to dict[str, str] and forwarded to :func:move_symbols. Invalid JSON yields a success=False result. insert_after: Optional name of a top-level symbol in the target module; moved blocks are spliced immediately after it. When None blocks append at the end; an absent name appends at the end with a warning. include_helpers: When True (default) transitively-referenced local helpers and constants are copied into the target. When False they are not copied (a warning enumerates the un-copied names); imports are still copied regardless. side_effect_decorators: Optional comma-separated list of extra side-effect decorator dotted-names that extend the built-in SIDE_EFFECT_DECORATORS whitelist. A moved symbol decorated with a matching decorator yields a non-blocking warning on the plan.

Returns

ToolResult success=True with a MovePlan summary on success; otherwise success=False with a message describing the failure (missing symbol, collision, shared helpers, validation error).

Source code in packages/axm-anvil/src/axm_anvil/tools/move.py
Python
def execute(  # noqa: PLR0913
    self,
    *,
    path: str = ".",
    symbols: str = "",
    from_file: str = "",
    to_file: str = "",
    dry_run: bool = False,
    shared_helpers: str = "duplicate",
    shared_helpers_module: str | None = None,
    reexport: bool = False,
    rename: str | None = None,
    check: bool = False,
    insert_after: str | None = None,
    include_helpers: bool = True,
    side_effect_decorators: str | None = None,
    **kwargs: object,
) -> ToolResult:
    """Move ``symbols`` (CSV) from ``from_file`` to ``to_file``.

    Parameters
    ----------
    path:
        Workspace root used to resolve relative ``from_file`` / ``to_file``
        and to constrain caller updates.
    symbols:
        Comma-separated list of top-level symbol names to move. Empty
        entries are ignored.
    from_file:
        Source Python file. Relative paths are resolved against ``path``.
    to_file:
        Target Python file. Relative paths are resolved against ``path``.
    dry_run:
        When ``True``, compute the :class:`MovePlan` without writing.
    shared_helpers:
        Policy for helpers used by both moved and remaining symbols:
        ``"duplicate"``, ``"extract"``, or ``"error"``.
    shared_helpers_module:
        Target module path used when ``shared_helpers="extract"``.
    reexport:
        When ``True``, leave callers untouched and inject a re-export in
        the source module. Incompatible with ``rename``.
    rename:
        Optional JSON object string mapping old symbol names to new ones
        (e.g. ``'{"OldName": "NewName"}'``). Parsed to ``dict[str, str]``
        and forwarded to :func:`move_symbols`. Invalid JSON yields a
        ``success=False`` result.
    insert_after:
        Optional name of a top-level symbol in the target module; moved
        blocks are spliced immediately after it. When ``None`` blocks
        append at the end; an absent name appends at the end with a
        warning.
    include_helpers:
        When ``True`` (default) transitively-referenced local helpers and
        constants are copied into the target. When ``False`` they are not
        copied (a warning enumerates the un-copied names); imports are
        still copied regardless.
    side_effect_decorators:
        Optional comma-separated list of extra side-effect decorator
        dotted-names that extend the built-in ``SIDE_EFFECT_DECORATORS``
        whitelist. A moved symbol decorated with a matching decorator
        yields a non-blocking warning on the plan.

    Returns
    -------
    ToolResult
        ``success=True`` with a ``MovePlan`` summary on success; otherwise
        ``success=False`` with a message describing the failure
        (missing symbol, collision, shared helpers, validation error).
    """
    root, src_path, tgt_path, symbol_list = self._normalize_execute_args(
        path, symbols, from_file, to_file
    )

    extra_decorators = self._parse_decorators(side_effect_decorators)

    rename_map: dict[str, str] | None = None
    if rename is not None:
        try:
            rename_map = json.loads(rename)
        except json.JSONDecodeError as exc:
            return ToolResult(success=False, error=f"invalid JSON in rename: {exc}")

    try:
        plan = move_symbols(
            src_path,
            tgt_path,
            symbol_list,
            dry_run=dry_run,
            workspace_root=root,
            shared_helpers=shared_helpers,
            shared_helpers_module=shared_helpers_module,
            reexport=reexport,
            rename=rename_map,
            check=check,
            insert_after=insert_after,
            include_helpers=include_helpers,
            side_effect_decorators=extra_decorators,
        )
    except Exception as exc:  # noqa: BLE001
        return self._exception_to_result(exc)

    data = self._build_result_data(
        plan, src_path, tgt_path, reexport=reexport, check=check
    )
    text = self._format_text(
        plan,
        from_file=str(src_path),
        to_file=str(tgt_path),
        reexport=reexport,
    )
    return ToolResult(success=True, data=data, text=text)

Python API

move_symbols

move_symbols(source_path, target_path, symbol_names, dry_run=False, workspace_root=None, shared_helpers='duplicate', shared_helpers_module=None, reexport=False, rename=None, check=False, strict=False, insert_after=None, include_helpers=True, side_effect_decorators=None)

Move top-level symbols from source_path to target_path.

Pipeline: parse → expand overloads → extract blocks → gather deps → build new target (imports + constants + symbols) → remove from source → classify shared helpers → validate parseability → atomic write via batch_edit → ruff fix.

shared_helpers selects the strategy when a helper is used by both a moved symbol and a remaining source symbol: "duplicate" copies and keeps the helper (emitting a warning); "error" aborts with :class:SharedHelpersError; "extract" is reserved for Phase 3.

When reexport=True, callers are left untouched and a from new_module import <names> # re-export for backwards compat line is appended to the source module. Incompatible with rename=.

When check=True, the move is simulated (no files written) and any newly introduced import cycle raises :class:ImportCycleError. A normal (non-dry_run) write also performs this check; dry_run=True alone preserves its historical "preview without enforcement" contract.

A requested name that is absent from the source module's top-level symbols is skipped with a warning on :attr:MovePlan.warnings rather than aborting the whole plan. Pass strict=True to restore the legacy behaviour of raising :class:SymbolNotFoundError on the first absent name.

insert_after controls where the moved blocks land in the target module body: when it names an existing top-level symbol the blocks are spliced immediately after it; when None (default) the blocks append at the end (unchanged contract); when it names an absent symbol the blocks append at the end and a warning is added to :attr:MovePlan.warnings. Imports and constants keep their historical placement regardless of insert_after.

include_helpers (default True) preserves the historical behaviour of copying transitively-referenced local helpers and constants into the target. When False those helpers/constants are not copied (the moved code is left referencing them), a warning enumerating the un-copied local helper names is added to :attr:MovePlan.warnings, and the shared_helpers classification is short-circuited (nothing is duplicated or extracted). Imports required by the moved code are always copied regardless of this flag.

Source code in packages/axm-anvil/src/axm_anvil/core/move.py
Python
def move_symbols(  # noqa: PLR0913
    source_path: str | Path,
    target_path: str | Path,
    symbol_names: Sequence[str],
    dry_run: bool = False,
    workspace_root: Path | None = None,
    shared_helpers: str = "duplicate",
    shared_helpers_module: str | None = None,
    reexport: bool = False,
    rename: dict[str, str] | None = None,
    check: bool = False,
    strict: bool = False,
    insert_after: str | None = None,
    include_helpers: bool = True,
    side_effect_decorators: frozenset[str] | None = None,
) -> MovePlan:
    """Move top-level symbols from ``source_path`` to ``target_path``.

    Pipeline: parse → expand overloads → extract blocks → gather deps →
    build new target (imports + constants + symbols) → remove from source
    → classify shared helpers → validate parseability → atomic write via
    ``batch_edit`` → ruff fix.

    ``shared_helpers`` selects the strategy when a helper is used by both a
    moved symbol and a remaining source symbol: ``"duplicate"`` copies and
    keeps the helper (emitting a warning); ``"error"`` aborts with
    :class:`SharedHelpersError`; ``"extract"`` is reserved for Phase 3.

    When ``reexport=True``, callers are left untouched and a
    ``from new_module import <names>  # re-export for backwards compat`` line
    is appended to the source module. Incompatible with ``rename=``.

    When ``check=True``, the move is simulated (no files written) and any
    *newly introduced* import cycle raises :class:`ImportCycleError`. A
    normal (non-``dry_run``) write also performs this check; ``dry_run=True``
    alone preserves its historical "preview without enforcement" contract.

    A requested name that is absent from the source module's top-level
    symbols is **skipped** with a warning on :attr:`MovePlan.warnings`
    rather than aborting the whole plan. Pass ``strict=True`` to restore
    the legacy behaviour of raising :class:`SymbolNotFoundError` on the
    first absent name.

    ``insert_after`` controls where the moved *blocks* land in the target
    module body: when it names an existing top-level symbol the blocks are
    spliced immediately after it; when ``None`` (default) the blocks append
    at the end (unchanged contract); when it names an absent symbol the
    blocks append at the end and a warning is added to
    :attr:`MovePlan.warnings`. Imports and constants keep their historical
    placement regardless of ``insert_after``.

    ``include_helpers`` (default ``True``) preserves the historical
    behaviour of copying transitively-referenced local helpers and
    constants into the target. When ``False`` those helpers/constants are
    **not** copied (the moved code is left referencing them), a warning
    enumerating the un-copied local helper names is added to
    :attr:`MovePlan.warnings`, and the ``shared_helpers`` classification is
    short-circuited (nothing is duplicated or extracted). Imports required
    by the moved code are always copied regardless of this flag.
    """
    _validate_options(shared_helpers, shared_helpers_module, reexport, rename)

    source_path = Path(source_path)
    target_path = Path(target_path)

    source_text = source_path.read_text()
    target_text = target_path.read_text()
    source_tree = cst.parse_module(source_text)
    target_tree = cst.parse_module(target_text)

    expanded_names, moved_names, skipped_warnings = _validate_and_expand(
        source_tree, target_tree, symbol_names, strict=strict
    )
    remove_targets, blocks = _extract_moved_blocks(source_tree, expanded_names)
    rename_map = _active_rename(rename, moved_names)
    if rename_map:
        blocks = _apply_rename_to_blocks(blocks, rename_map)

    root = (
        Path(workspace_root)
        if workspace_root is not None
        else _find_workspace_root(source_path)
    )
    import_resolution = _build_import_resolution(source_path, target_path, root)

    (
        new_source_tree,
        new_target_tree,
        imports_added,
        constants_added,
        shared_map,
        redundant_import_warnings,
    ) = _build_trees(
        source_tree,
        target_tree,
        blocks,
        remove_targets,
        shared_helpers,
        insert_after=insert_after,
        include_helpers=include_helpers,
        import_resolution=import_resolution,
        rename_map=rename_map,
    )

    if reexport:
        try:
            new_module_path = _module_path_from_file(target_path, root)
        except ValueError:
            new_module_path = target_path.stem
        new_source_tree = _inject_reexport(
            new_source_tree, new_module_path, moved_names
        )

    source_text_new, target_text_new = _render_and_validate(
        new_source_tree, new_target_tree
    )

    caller_texts, caller_rewrites = _resolve_caller_phase(
        reexport, root, moved_names, source_path, target_path, rename_map
    )

    plan = _build_plan(
        source_text_new,
        target_text_new,
        moved_names,
        imports_added,
        constants_added,
        shared_map,
        callers_updated=caller_rewrites,
        redundant_import_warnings=redundant_import_warnings,
    )
    plan.warnings.extend(skipped_warnings)
    # Forward-refs to *renamed* symbols are rewritten in the moved code
    # (RenameSymbols.leave_Annotation); only moved-but-not-renamed names still
    # warrant the manual-update warning.
    unrenamed_moved = [n for n in moved_names if n not in rename_map]
    plan.warnings.extend(_string_forward_ref_warnings(source_tree, unrenamed_moved))
    deco_whitelist = SIDE_EFFECT_DECORATORS | (side_effect_decorators or frozenset())
    plan.warnings.extend(_side_effect_decorator_warnings(blocks, deco_whitelist))
    plan.warnings.extend(
        _fixture_scope_warnings(blocks, source_tree, source_path, target_path, root)
    )

    cycle = _cycle_check(
        root,
        source_path,
        target_path,
        new_source_tree,
        new_target_tree,
        caller_texts,
        blocks,
        source_tree,
    )
    _enforce_cycle(cycle, check, dry_run)

    if dry_run or check:
        return plan

    _apply_write(
        source_path,
        target_path,
        source_text,
        target_text,
        source_text_new,
        target_text_new,
        workspace_root,
        caller_texts,
    )
    plan.warnings.extend(_ruff_fix(source_path, target_path, reexport=reexport))
    return plan

MovePlan

MovePlan dataclass

Result of a :func:move_symbols call.

Carries the rendered source and target texts, the names that were actually moved, and the direct dependencies (imports, constants) copied into the target. warnings aggregates non-fatal issues such as ruff post-processing errors.

Source code in packages/axm-anvil/src/axm_anvil/core/plan.py
Python
@dataclass
class MovePlan:
    """Result of a :func:`move_symbols` call.

    Carries the rendered source and target texts, the names that were
    actually moved, and the direct dependencies (imports, constants)
    copied into the target. ``warnings`` aggregates non-fatal issues
    such as ruff post-processing errors.
    """

    source_text_new: str
    target_text_new: str
    moved_names: list[str]
    imports_added: list[str] = field(default_factory=list)
    constants_added: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    shared_helpers_detected: list[SharedHelperDetection] = field(default_factory=list)
    callers_updated: list[CallerRewrite] = field(default_factory=list)

SIDE_EFFECT_DECORATORS

SIDE_EFFECT_DECORATORS = frozenset({'app.route', 'app.get', 'app.post', 'app.put', 'app.delete', 'app.patch', 'router.get', 'router.post', 'router.put', 'router.delete', 'router.patch', 'pytest.fixture', 'fixture', 'celery.task', 'app.task', 'shared_task', 'click.command', 'click.group', 'singledispatch', 'functools.singledispatch'}) module-attribute

The default whitelist of decorator dotted-names whose primary purpose is to register the decorated symbol with an external registry as an import-time side effect (e.g. app.route, pytest.fixture / bare fixture, celery.task, click.command). When a moved FunctionDef/ClassDef carries a matching decorator — in bare (@fixture), dotted (@pytest.fixture), or call (@app.route("/x")) form — move_symbols records a non-blocking warning on MovePlan.warnings noting that registration may not run in the new module. The move is never blocked. Callers extend the whitelist via the side_effect_decorators parameter of move_symbols (or --side-effect-decorators on the CLI); supplied entries are unioned with these defaults.

CallerRewrite

CallerRewrite dataclass

A single caller-import rewrite record for :class:MovePlan.

Source code in packages/axm-anvil/src/axm_anvil/core/callers.py
Python
@dataclass
class CallerRewrite:
    """A single caller-import rewrite record for :class:`MovePlan`."""

    file: str
    line: int
    old: str
    new: str

One entry per caller import line rewritten by move_symbols. Populated into MovePlan.callers_updated so downstream tooling (CLI output, MCP response) can report every from old_module import … line that was redirected to the new module.

SharedHelpersError

SharedHelpersError

Bases: Exception

Raised in error mode when shared helpers would be duplicated.

Source code in packages/axm-anvil/src/axm_anvil/core/plan.py
Python
class SharedHelpersError(Exception):
    """Raised in ``error`` mode when shared helpers would be duplicated."""

    def __init__(self, shared_helpers: list[str]) -> None:
        self.shared_helpers = list(shared_helpers)
        joined = ", ".join(self.shared_helpers)
        super().__init__(
            f"Shared helpers detected (also used by remaining symbols): {joined}"
        )

Raised by move_symbols when shared_helpers="error" and at least one helper is transitively referenced by both a moved block and a remaining source symbol. The exception's shared_helpers attribute lists the offending helper names.

ImportCycleError

ImportCycleError

Bases: Exception

Raised when a move would introduce a new import cycle.

Source code in packages/axm-anvil/src/axm_anvil/core/plan.py
Python
class ImportCycleError(Exception):
    """Raised when a move would introduce a new import cycle."""

    def __init__(self, cycle: list[str]) -> None:
        self.cycle = list(cycle)
        chain = " \u2192 ".join([*self.cycle, self.cycle[0]])
        super().__init__(f"Import cycle: {chain}")

Raised by move_symbols when the requested move (or the associated caller rewrites) would introduce a new import cycle into the containing package. Pre-existing cycles are ignored. The exception is raised when check=True or during a normal (non-dry-run) write. Pure dry_run=True calls skip the raise to preserve the existing preview contract.

SymbolNotFoundError

SymbolNotFoundError

Bases: Exception

Raised when a requested symbol does not exist in the source module.

Source code in packages/axm-anvil/src/axm_anvil/core/plan.py
Python
class SymbolNotFoundError(Exception):
    """Raised when a requested symbol does not exist in the source module."""

A requested name that is absent from the source module's top-level symbols (for example a test_basic method declared inside a Test* class, or a name that simply does not exist) is skipped by default: move_symbols drops it from the move and records a skipped '<name>': not a top-level symbol in source entry on MovePlan.warnings. The CLI and the ast_move MCP tool surface that warning and still exit successfully. Pass strict=True to restore the legacy behaviour of raising SymbolNotFoundError on the first absent name. Names that are present continue to move exactly as before.

Auto-generated API reference is available under Python API.