axm-anvil
Deterministic CST-based refactoring toolkit for Python.
Overview
Moving symbols (classes, functions, constants) between Python files is error-prone
when done by hand or via LLM text generation. axm-anvil replaces those rewrites
with deterministic, CST-based transformations that preserve formatting,
comments, and semantics exactly.
Built on libcst for lossless round-trip, it exposes a set of MCP tools for agent-driven refactoring.
Features
- 🔨
anvil_move— Move classes, functions, or constants between files with transitive dependency resolution (imports, constants, helpers) - ✏️
anvil_rename— Rename top-level symbols in place and rewrite cross-file callers atomically (definition + internal usages +from mod import Oldimports and usages in every caller) - 🔁 Smart import merging — Uses
AddImportsVisitorto combine imports from the same module instead of duplicating - 🧹 Scope-aware orphan cleanup — Source file's unused imports are removed via
ruff check --select F401 --fix - 📐 Topological constant ordering — Dependencies are always inserted before their dependents
- 🛡️ Atomic writes — All modifications computed in memory, validated via
cst.parse_module(), then applied via a singlebatch_editcall (all-or-nothing) - 🔗 Attribute-style caller rewrites — Rewrites
old_module.Symbolchains (includingimport ... asaliases) to the new module, preserving method/subscript chains and skipping shadowed names viaScopeProvider - 🪢
--reexportmode — Leaves callers untouched and injects afrom new_module import <Symbol> # re-export for backwards compatshim into the source module for gradual migration - 🎯 Overload-aware — Detects
@overloadcompanions and moves them together as an indivisible group - 📦 Lossless formatting — Comments, whitespace, and trailing commas preserved exactly via libcst round-trip
- 🤝 Complementary to
axm-ast— Builds the workspace-wide module graph viaanalyze_workspace+build_workspace_module_graphto detect newly-introduced cross-package import cycles - ✏️
--renameon move — Rename moved definitions in flight (JSON{"Old": "New"}); references,__all__entries, and string forward-references are all rewritten to the new name - 📍
--insert-after— Splice moved blocks after a named target symbol instead of appending at end-of-file - 🚫
--no-include-helpers— Skip auto-copying local helpers/constants into the target (imports are still copied); emits a warning listing the un-copied names - 🧭 Edge-case awareness — Syncs
__all__(never created spontaneously), preservestry/exceptconditional imports verbatim, converts relative imports to absolute on cross-package moves, and warns on side-effect decorators (@app.route,@pytest.fixture…), string forward-references, and pytest fixture-scope breaks
Roadmap
Planned tools
anvil_move, anvil_rename, and anvil_extract are the shipped
operations. The tools below are not yet implemented — they are listed
here to convey the intended direction.
| Tool | Description | Status |
|---|---|---|
anvil_move |
Move symbols between files | shipped |
anvil_rename |
Rename a top-level symbol in place; rewrite cross-file callers (imports + usages) | shipped |
anvil_extract |
Extract symbols into a new module (created on disk) with their transitive dependencies | shipped |
anvil_split |
Split a module into N sub-modules | planned |
anvil_merge |
Merge N modules into one | planned |
anvil_promote |
_foo → foo + add __all__ + update imports |
planned |
anvil_seal |
foo → _foo + verify zero external callers |
planned |
All share the same pipeline: identify (libcst block extraction) → blast radius (libcst caller discovery + the workspace module graph from axm-ast) → transform (libcst) → validate (cst.parse_module() + ruff check --fix) → write atomically (batch_edit) → rollback on error.
Installation
uv add axm-anvil
Or as a workspace dependency in pyproject.toml:
[project]
dependencies = ["axm-anvil"]
[tool.uv.sources]
axm-anvil = { workspace = true }
Quick Start
CLI — preview a move (positional or flag form, both accepted):
axm-anvil move src/mylib/core/models.py src/mylib/core/services.py \
UserService,_validate_input --dry-run
Rename while moving, and place the result after an existing symbol:
axm-anvil move \
--from-file src/mylib/core/models.py \
--to-file src/mylib/core/services.py \
--symbols UserService \
--rename '{"UserService": "AccountService"}' \
--insert-after existing_service
Move without dragging local helpers along (imports are still copied):
axm-anvil move src/mylib/a.py src/mylib/b.py Widget --no-include-helpers
Python / MCP:
from axm_anvil import MoveTool
result = MoveTool().execute(
path=".",
symbols="UserService,_validate_input",
from_file="src/mylib/core/models.py",
to_file="src/mylib/core/services.py",
dry_run=True,
)
print(result.data["moved"])
print(result.data["warnings"]) # __all__ sync, conditional imports, decorators, …
See the CLI Reference for every flag and the warnings each one can emit.
Development
This package is part of the axm-forge uv workspace.
# Run tests for this package
uv run --package axm-anvil --directory packages/axm-anvil pytest
# From workspace root
make test-anvil
License
Apache-2.0 — © 2026 AXM Protocols
Metadata
Release files for axm-anvil 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| axm_anvil-0.4.0.tar.gz | 118.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| axm_anvil-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 189.8 kB
Release files / axm_anvil-0.4.0.tar.gz
| Download URL | axm_anvil-0.4.0.tar.gz |
|---|---|
| Size | 118.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b269812a050e841c562c4baa59355ffe7344863bbd0aab0aea426f137df521ee
|
|
BLAKE2b-256 checksum How to use checksums |
620226f56d13fe2d7c44089c31628e6229043194275074814522362e85ad70f7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / axm_anvil-0.4.0-py3-none-any.whl
| Download URL | axm_anvil-0.4.0-py3-none-any.whl |
|---|---|
| Size | 71.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ec20538c4885742af4ab2304b747b91463f432a3525d29d3088513410bac07b
|
|
BLAKE2b-256 checksum How to use checksums |
30c1f612dd58d6d624008ddd8949f7cd66818c4b614ada9e10650b126cdeb718
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log