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] [--strict] [--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
--strict Fail (non-zero exit) on a requested symbol that is absent from the source module instead of skipping it with a warning. Default (--no-strict) skips an absent symbol and records a warning
--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 usual end-of-file placement regardless
--include-helpers / --no-include-helpers Whether to copy transitively-referenced local helpers and constants into the target. --include-helpers (default) copies private helper symbols alongside the moved symbol. --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 (see Python API). 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 anvil_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 anvil_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 ``anvil_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 "anvil_move"

    @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} 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
            ],
            "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),
                *(entry.file for entry in plan.callers_updated),
            ],
        }
        if reexport:
            data["reexport"] = True
        if check:
            data["check"] = True
        return data

    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,
        strict: 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.
        strict:
            When ``True``, a requested symbol absent from the source module
            raises (surfaced as ``success=False``) instead of being silently
            skipped with a warning. When ``False`` (default) the current
            skip-and-warn behaviour is preserved.
        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 = 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,
                strict=strict,
                insert_after=insert_after,
                include_helpers=include_helpers,
                side_effect_decorators=extra_decorators,
            )
        except Exception as exc:  # noqa: BLE001
            return 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"anvil_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, strict=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. strict: When True, a requested symbol absent from the source module raises (surfaced as success=False) instead of being silently skipped with a warning. When False (default) the current skip-and-warn behaviour is preserved. 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,
    strict: 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.
    strict:
        When ``True``, a requested symbol absent from the source module
        raises (surfaced as ``success=False``) instead of being silently
        skipped with a warning. When ``False`` (default) the current
        skip-and-warn behaviour is preserved.
    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 = 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,
            strict=strict,
            insert_after=insert_after,
            include_helpers=include_helpers,
            side_effect_decorators=extra_decorators,
        )
    except Exception as exc:  # noqa: BLE001
        return 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)

ExtractTool

Registered as anvil_extract via the axm.tools entry point (reachable as axm anvil_extract on the CLI and via MCP). Extracts top-level symbols from from_file into a new to_file (created on disk, parent directories included), copying the same transitive dependencies (imports, local helpers, constants) as anvil_move and rewriting every cross-file caller (from old import X to from new import X). It is a thin specialisation of MoveTool where the target module does not yet exist: extracting into a pre-existing module that already defines one of the requested symbols fails with success=False (no silent overwrite). With dry_run=True the plan is computed without leaving any file on disk. reexport and check are intentionally not exposed (meaningless against a freshly created module). The returned ToolResult carries the same shape as anvil_move (moved, dependencies_copied, callers_updated, warnings, shared_helpers_detected, files_modified).

ExtractTool

Bases: AXMTool

Extract top-level symbols from a module into a brand-new module.

Registered as anvil_extract via the axm.tools entry point. Delegates to :func:axm_anvil.core.extract.extract_symbols (itself a thin adapter over the move pipeline) and adapts exceptions into ToolResult(success=False). The result shape matches anvil_move.

Source code in packages/axm-anvil/src/axm_anvil/tools/extract.py
Python
class ExtractTool(AXMTool):
    """Extract top-level symbols from a module into a brand-new module.

    Registered as ``anvil_extract`` via the ``axm.tools`` entry point.
    Delegates to :func:`axm_anvil.core.extract.extract_symbols` (itself a
    thin adapter over the move pipeline) and adapts exceptions into
    ``ToolResult(success=False)``. The result shape matches ``anvil_move``.
    """

    agent_hint: str = (
        "Extract classes, functions, or constants into a NEW module "
        "(created on disk), with their transitive dependencies, and rewrite "
        "cross-file callers. Use dry_run=True to preview changes."
    )

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

    @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,
    ) -> dict[str, object]:
        return {
            "moved": [{"symbol": name} 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
            ],
            "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)],
        }

    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,
        rename: str | None = None,
        strict: bool = False,
        insert_after: str | None = None,
        include_helpers: bool = True,
        side_effect_decorators: str | None = None,
        **kwargs: object,
    ) -> ToolResult:
        """Extract ``symbols`` (CSV) from ``from_file`` into a new ``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 extract. Empty
            entries are ignored.
        from_file:
            Source Python file. Relative paths are resolved against ``path``.
        to_file:
            Target Python file to **create**. Relative paths are resolved
            against ``path``; missing parent directories are created.
        dry_run:
            When ``True``, compute the :class:`MovePlan` without writing (and
            without leaving a scaffolded target on disk).
        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"``.
        rename:
            Optional JSON object string mapping old symbol names to new ones
            (e.g. ``'{"OldName": "NewName"}'``). Invalid JSON yields a
            ``success=False`` result.
        strict:
            When ``True``, a requested symbol absent from the source module
            raises (surfaced as ``success=False``) instead of being skipped
            with a warning.
        insert_after:
            Optional name of a top-level symbol in the target module after
            which extracted blocks are spliced. ``None`` appends at the end.
        include_helpers:
            When ``True`` (default) transitively-referenced local helpers and
            constants are copied into the target.
        side_effect_decorators:
            Optional comma-separated list of extra side-effect decorator
            dotted-names extending the built-in whitelist.

        Returns
        -------
        ToolResult
            ``success=True`` with a plan summary on success; otherwise
            ``success=False`` with a message (missing symbol, collision,
            shared helpers, validation error).
        """
        root, src_path, tgt_path, symbol_list = 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 = extract_symbols(
                src_path,
                tgt_path,
                symbol_list,
                dry_run=dry_run,
                workspace_root=root,
                shared_helpers=shared_helpers,
                shared_helpers_module=shared_helpers_module,
                rename=rename_map,
                strict=strict,
                insert_after=insert_after,
                include_helpers=include_helpers,
                side_effect_decorators=extra_decorators,
            )
        except Exception as exc:  # noqa: BLE001
            return exception_to_result(exc)

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

    def _format_text(
        self,
        plan: MovePlan,
        *,
        from_file: str,
        to_file: str,
    ) -> str:
        """Render the extract plan as compact text (mirrors anvil_move)."""
        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"anvil_extract | {n} symbols | {src_name}{tgt_name} (new)",
            "",
            "Extracted:",
        ]
        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, rename=None, strict=False, insert_after=None, include_helpers=True, side_effect_decorators=None, **kwargs)

Extract symbols (CSV) from from_file into a new 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 extract. Empty entries are ignored. from_file: Source Python file. Relative paths are resolved against path. to_file: Target Python file to create. Relative paths are resolved against path; missing parent directories are created. dry_run: When True, compute the :class:MovePlan without writing (and without leaving a scaffolded target on disk). 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". rename: Optional JSON object string mapping old symbol names to new ones (e.g. '{"OldName": "NewName"}'). Invalid JSON yields a success=False result. strict: When True, a requested symbol absent from the source module raises (surfaced as success=False) instead of being skipped with a warning. insert_after: Optional name of a top-level symbol in the target module after which extracted blocks are spliced. None appends at the end. include_helpers: When True (default) transitively-referenced local helpers and constants are copied into the target. side_effect_decorators: Optional comma-separated list of extra side-effect decorator dotted-names extending the built-in whitelist.

Returns

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

Source code in packages/axm-anvil/src/axm_anvil/tools/extract.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,
    rename: str | None = None,
    strict: bool = False,
    insert_after: str | None = None,
    include_helpers: bool = True,
    side_effect_decorators: str | None = None,
    **kwargs: object,
) -> ToolResult:
    """Extract ``symbols`` (CSV) from ``from_file`` into a new ``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 extract. Empty
        entries are ignored.
    from_file:
        Source Python file. Relative paths are resolved against ``path``.
    to_file:
        Target Python file to **create**. Relative paths are resolved
        against ``path``; missing parent directories are created.
    dry_run:
        When ``True``, compute the :class:`MovePlan` without writing (and
        without leaving a scaffolded target on disk).
    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"``.
    rename:
        Optional JSON object string mapping old symbol names to new ones
        (e.g. ``'{"OldName": "NewName"}'``). Invalid JSON yields a
        ``success=False`` result.
    strict:
        When ``True``, a requested symbol absent from the source module
        raises (surfaced as ``success=False``) instead of being skipped
        with a warning.
    insert_after:
        Optional name of a top-level symbol in the target module after
        which extracted blocks are spliced. ``None`` appends at the end.
    include_helpers:
        When ``True`` (default) transitively-referenced local helpers and
        constants are copied into the target.
    side_effect_decorators:
        Optional comma-separated list of extra side-effect decorator
        dotted-names extending the built-in whitelist.

    Returns
    -------
    ToolResult
        ``success=True`` with a plan summary on success; otherwise
        ``success=False`` with a message (missing symbol, collision,
        shared helpers, validation error).
    """
    root, src_path, tgt_path, symbol_list = 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 = extract_symbols(
            src_path,
            tgt_path,
            symbol_list,
            dry_run=dry_run,
            workspace_root=root,
            shared_helpers=shared_helpers,
            shared_helpers_module=shared_helpers_module,
            rename=rename_map,
            strict=strict,
            insert_after=insert_after,
            include_helpers=include_helpers,
            side_effect_decorators=extra_decorators,
        )
    except Exception as exc:  # noqa: BLE001
        return exception_to_result(exc)

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

RenameTool

Registered as anvil_rename via the axm.tools entry point (so it is reachable as axm anvil_rename on the CLI and via MCP). Renames top-level symbols in place — definition and internal usages — and rewrites every cross-file caller (from mod import Old import alias and usages). Pass a mono-symbol old/new pair, or a mapping JSON object (e.g. '{"OldName": "NewName"}') for batch renames. dry_run previews the plan without writing; strict turns an absent symbol into a success=False result instead of a skipped-with-warning. reexport is intentionally not exposed (incompatible with rename). The returned ToolResult carries renamed, callers_updated, warnings, and files_modified.

RenameTool

Bases: AXMTool

Rename top-level symbols in place, rewriting cross-file callers.

Registered as anvil_rename via the axm.tools entry point. Delegates to :func:axm_anvil.core.rename.rename_symbols and adapts exceptions into ToolResult(success=False). Mono-symbol renames use --old/--new; batch renames pass a --mapping JSON object (symmetric with the rename JSON of :class:MoveTool). reexport is not exposed (incompatible with rename, per MoveTool.execute).

Source code in packages/axm-anvil/src/axm_anvil/tools/rename.py
Python
class RenameTool(AXMTool):
    """Rename top-level symbols in place, rewriting cross-file callers.

    Registered as ``anvil_rename`` via the ``axm.tools`` entry point.
    Delegates to :func:`axm_anvil.core.rename.rename_symbols` and adapts
    exceptions into ``ToolResult(success=False)``. Mono-symbol renames use
    ``--old``/``--new``; batch renames pass a ``--mapping`` JSON object
    (symmetric with the ``rename`` JSON of :class:`MoveTool`). ``reexport``
    is not exposed (incompatible with rename, per ``MoveTool.execute``).
    """

    agent_hint: str = (
        "Rename a top-level symbol in place and rewrite its cross-file "
        "callers atomically. Use dry_run=True to preview changes."
    )

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

    @staticmethod
    def _resolve_mapping(
        mapping: str | None, old: str, new: str
    ) -> dict[str, str] | ToolResult:
        """Build the ``old -> new`` mapping from JSON or ``old``/``new`` args."""
        if mapping is not None:
            try:
                parsed = json.loads(mapping)
            except json.JSONDecodeError as exc:
                return ToolResult(
                    success=False, error=f"invalid JSON in mapping: {exc}"
                )
            if not isinstance(parsed, dict):
                return ToolResult(success=False, error="mapping must be a JSON object")
            return {str(k): str(v) for k, v in parsed.items()}
        if old and new:
            return {old: new}
        return ToolResult(
            success=False,
            error="provide --old and --new, or a --mapping JSON object",
        )

    @staticmethod
    def _build_result_data(plan: RenamePlan) -> dict[str, object]:
        return {
            "renamed": [{"old": old, "new": new} for old, new in plan.renamed.items()],
            "callers_updated": [
                {
                    "file": entry.file,
                    "line": entry.line,
                    "old": entry.old,
                    "new": entry.new,
                }
                for entry in plan.callers_updated
            ],
            "warnings": list(plan.warnings),
            "files_modified": list(plan.files_modified),
        }

    @staticmethod
    def _exception_to_result(exc: Exception) -> ToolResult:
        match exc:
            case SymbolNotFoundError():
                return ToolResult(
                    success=False,
                    error=f"Symbol {exc!s} not found in module",
                )
            case MoveValidationError():
                return ToolResult(success=False, error=str(exc))
            case _:
                return ToolResult(success=False, error=str(exc))

    def execute(  # noqa: PLR0913
        self,
        *,
        path: str = ".",
        file: str = "",
        old: str = "",
        new: str = "",
        mapping: str | None = None,
        dry_run: bool = False,
        strict: bool = False,
        **kwargs: object,
    ) -> ToolResult:
        """Rename symbol(s) in ``file`` and rewrite cross-file callers.

        Parameters
        ----------
        path:
            Workspace root used to resolve a relative ``file`` and to
            constrain caller discovery.
        file:
            Python file defining the symbols. Relative paths resolve against
            ``path``.
        old / new:
            Mono-symbol rename: rename ``old`` to ``new``. Ignored when
            ``mapping`` is provided.
        mapping:
            Optional JSON object string mapping old names to new ones
            (e.g. ``'{"OldName": "NewName"}'``) for batch renames. Invalid
            JSON yields a ``success=False`` result.
        dry_run:
            When ``True``, compute the :class:`RenamePlan` without writing.
        strict:
            When ``True`` an absent symbol raises (surfaced as
            ``success=False``); when ``False`` (default) it is skipped with
            a warning.

        Returns
        -------
        ToolResult
            ``success=True`` with a rename summary (``renamed``,
            ``callers_updated``, ``warnings``, ``files_modified``) on
            success; otherwise ``success=False`` with a failure message.
        """
        resolved = self._resolve_mapping(mapping, old, new)
        if isinstance(resolved, ToolResult):
            return resolved
        root = Path(path).resolve()
        src_path = Path(file)
        if not src_path.is_absolute():
            src_path = root / src_path

        try:
            plan = rename_symbols(
                root,
                src_path,
                resolved,
                dry_run=dry_run,
                workspace_root=root,
                strict=strict,
            )
        except Exception as exc:  # noqa: BLE001
            return self._exception_to_result(exc)

        data = self._build_result_data(plan)
        text = self._format_text(plan, file=str(src_path))
        return ToolResult(success=True, data=data, text=text)

    def _format_text(self, plan: RenamePlan, *, file: str) -> str:
        """Render the rename plan as compact text (mirrors anvil_move)."""
        n = len(plan.renamed)
        name = Path(file).name or file
        lines: list[str] = [f"anvil_rename | {n} symbols | {name}", ""]
        lines.append("Renamed:")
        for old, new in plan.renamed.items():
            lines.append(f"  - {old}{new}")
        lines.append("")
        lines.append(f"Callers Updated: {len(plan.callers_updated)}")
        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='.', file='', old='', new='', mapping=None, dry_run=False, strict=False, **kwargs)

Rename symbol(s) in file and rewrite cross-file callers.

Parameters

path: Workspace root used to resolve a relative file and to constrain caller discovery. file: Python file defining the symbols. Relative paths resolve against path. old / new: Mono-symbol rename: rename old to new. Ignored when mapping is provided. mapping: Optional JSON object string mapping old names to new ones (e.g. '{"OldName": "NewName"}') for batch renames. Invalid JSON yields a success=False result. dry_run: When True, compute the :class:RenamePlan without writing. strict: When True an absent symbol raises (surfaced as success=False); when False (default) it is skipped with a warning.

Returns

ToolResult success=True with a rename summary (renamed, callers_updated, warnings, files_modified) on success; otherwise success=False with a failure message.

Source code in packages/axm-anvil/src/axm_anvil/tools/rename.py
Python
def execute(  # noqa: PLR0913
    self,
    *,
    path: str = ".",
    file: str = "",
    old: str = "",
    new: str = "",
    mapping: str | None = None,
    dry_run: bool = False,
    strict: bool = False,
    **kwargs: object,
) -> ToolResult:
    """Rename symbol(s) in ``file`` and rewrite cross-file callers.

    Parameters
    ----------
    path:
        Workspace root used to resolve a relative ``file`` and to
        constrain caller discovery.
    file:
        Python file defining the symbols. Relative paths resolve against
        ``path``.
    old / new:
        Mono-symbol rename: rename ``old`` to ``new``. Ignored when
        ``mapping`` is provided.
    mapping:
        Optional JSON object string mapping old names to new ones
        (e.g. ``'{"OldName": "NewName"}'``) for batch renames. Invalid
        JSON yields a ``success=False`` result.
    dry_run:
        When ``True``, compute the :class:`RenamePlan` without writing.
    strict:
        When ``True`` an absent symbol raises (surfaced as
        ``success=False``); when ``False`` (default) it is skipped with
        a warning.

    Returns
    -------
    ToolResult
        ``success=True`` with a rename summary (``renamed``,
        ``callers_updated``, ``warnings``, ``files_modified``) on
        success; otherwise ``success=False`` with a failure message.
    """
    resolved = self._resolve_mapping(mapping, old, new)
    if isinstance(resolved, ToolResult):
        return resolved
    root = Path(path).resolve()
    src_path = Path(file)
    if not src_path.is_absolute():
        src_path = root / src_path

    try:
        plan = rename_symbols(
            root,
            src_path,
            resolved,
            dry_run=dry_run,
            workspace_root=root,
            strict=strict,
        )
    except Exception as exc:  # noqa: BLE001
        return self._exception_to_result(exc)

    data = self._build_result_data(plan)
    text = self._format_text(plan, file=str(src_path))
    return ToolResult(success=True, data=data, text=text)

Python API

The full Python API — every public function, model, and exception with its signature and docstring — is rendered from source under Python API. This section captures only the cross-cutting semantics that span several symbols.

extract_symbols — thin adapter over move_symbols for the extract case: the target module is created rather than amended. When target_path does not exist it is scaffolded as an empty module so the move pipeline can fill it; a pre-existing target already defining a requested symbol raises SymbolAlreadyExistsError (no silent overwrite). A dry_run=True call removes any scaffolded target — and any directories it created — before returning, leaving disk state byte-identical. All other parameters mirror move_symbols and are forwarded verbatim; reexport and check are not exposed.

rename_symbols — renames the top-level symbols in mapping in place in file and rewrites every cross-file caller discovered under the workspace root. A rename onto a name that already exists in the module is refused with SymbolAlreadyExistsError (no duplicate definition). Caller rewriting is pattern-based on the import statement; shadowing, alias chains, and re-exports/star imports are deferred to a later tier (see the function and module docstrings).

SIDE_EFFECT_DECORATORS — 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. 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).

SymbolNotFoundError — a requested name absent from the source module's top-level symbols is skipped by default: move_symbols drops it and records a skipped '<name>': not a top-level symbol in source entry on MovePlan.warnings. The CLI and the anvil_move MCP tool surface that warning and still exit successfully. Pass strict=True (or --strict) to raise on the first absent name instead.

ImportCycleError — raised by move_symbols when the requested move (or its caller rewrites) would introduce a new import cycle. Pre-existing cycles are ignored. Raised when check=True or during a normal (non-dry-run) write; a pure dry_run=True call skips the raise to preserve the preview contract.