Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libcst-codemod

A Claude Code skill that teaches an AI agent to refactor Python across a whole repository with LibCST, without shipping a half-migration.

Regex does not know what a name means. ast throws away comments and formatting, so a three-line migration comes back as a whole-file diff nobody wants to review. LibCST parses to a concrete syntax tree: untouched code stays byte-identical, and the diff shows only what changed.

That part is well known. The part this skill exists for is what happens next.

The problem it actually solves

A codemod that looks correct can still emit code that imports one name and calls another. Every failure below was reproduced against libcst 1.9.0 while building this skill, not taken from documentation:

Aliases and shadowing. node.value == "requests" matches a local variable named requests and misses import requests as r. QualifiedNameProvider resolves both, and the skill bans the string comparison outright.

The official import helper fails silently on aliases. RemoveImportsVisitor.remove_unused_import(ctx, "requests") removes nothing from import requests as r and reports nothing. Pair it with an add and the file ends up carrying both imports, under a summary line reading 0 warnings were generated.

Import rewriting and call-site rewriting are coupled. Handle the alias correctly in the import and you get import httpx as r alongside httpx.get(u). httpx is unbound. The diff looks clean; the code raises NameError. Pick one strategy, keep-alias or normalize, and never mix them.

From-imports hide the module. In from requests import get, the ImportAlias is get. The module lives in ImportFrom.module, which leave_ImportAlias never visits. Miss it and every from-import still points at the old package, importing cleanly until the day the old package is uninstalled.

Conditional imports get half-migrated. A rename rewrites the import line and the call sites, but cannot see except ImportError: requests = None. The result raises on exactly the path the fallback existed to protect. This one is not solvable by a name-based rewrite, so the workflow detects it and escalates to a human instead of pretending otherwise.

Install

git clone https://github.com/pouriamrt/libcst-codemod.git

Then point your skills directory at it. On macOS or Linux:

ln -s "$(pwd)/libcst-codemod" ~/.claude/skills/libcst-codemod

On Windows, from PowerShell:

New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.claude\skills\libcst-codemod" -Target "$PWD\libcst-codemod"

Copying the directory works too. A symlink means edits go live without reinstalling.

No pip install is needed. Every command runs LibCST through an ephemeral uv environment.

What is inside

Path What it does
SKILL.md The workflow the agent follows, steps 0 through 5
templates/codemod.py A working module-rename command, keep-alias strategy
templates/test_codemod.py Eight fixtures covering the failure modes above
references/gotchas.md Nine reproduced gotchas, loaded only when something bites

The workflow

  1. Pre-flight. Grep for except ImportError around the target. Hand every hit to a human before writing anything.
  2. Fixtures first. Six mandatory cases, red before green: plain, aliased, shadowed local, comment preservation, unrelated module, from-import.
  3. Resolve names with metadata. QualifiedNameProvider, never a string compare.
  4. Pick one import strategy and hold it for the whole codemod.
  5. Dry-run the repo with --unified-diff=3. Read it. Check Failed and Skipped, which are not lines to skim past.
  6. Apply, then prove it. Run the target repo's own tests and paste the output.

Try it

cd templates
uv run --with libcst --with pytest python -m pytest test_codemod.py -q

Eight tests, covering the alias case, the shadowed local, and the conditional import whose limitation is pinned as behaviour so it cannot regress unnoticed.

To see it run against files:

uv run --with libcst python -m libcst.tool codemod -x --unified-diff=3 codemod.RenameModule src/

Drop --unified-diff to write the changes.

One CLI quirk worth knowing up front: attach every option value. --unified-diff=3 and --jobs=1 work, while -u 3 and -j 1 fail with 3 is not a valid codemod command, because the parser hands the value to the COMMAND positional.

Scope

Use it when the job spans three or more files, or when any rename could collide with an alias or a shadowed local. Below that, a plain edit is faster and this is ceremony.

It resolves names, not types. A migration that needs to know what an expression evaluates to is out of reach.

Requirements

uv, and Python 3.10 or newer. LibCST is fetched on demand.

License

MIT. See LICENSE.

About

A Claude Code skill for scope-aware Python refactoring with LibCST. Handles aliases, shadowing, and from-imports so codemods don't ship half-migrations.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages