All refactor scripts MUST use scripts/rope_bootstrap.py. It provides project setup, --diff preview mode, git safety checks, and the RefactorContext with file selection helpers.
import sys
from argparse import ArgumentParser
from pathlib import Path
SKILL_SCRIPTS = "/absolute/path/to/skill/scripts" # use the actual skill path
sys.path.insert(0, SKILL_SCRIPTS)
from rope_bootstrap import RefactorContext, run
from rope.refactor.rename import Rename
def setup_args(parser: ArgumentParser) -> None:
parser.add_argument("source", type=Path, help="Source file")
def refactor(ctx: RefactorContext) -> None:
resource = ctx.get_resource(ctx.args.source)
pymodule = ctx.project.get_pymodule(resource)
source = resource.read()
line_start = pymodule.lines.get_line_start(line_number)
offset = line_start + source[line_start:].index("symbol_name")
changes = Rename(ctx.project, resource, offset).get_changes("new_name")
ctx.do(changes)
if __name__ == "__main__":
run(refactor, description="My refactor script", setup_args=setup_args)IMPORTANT: The sys.path.insert line MUST use the absolute path to this skill's scripts/ directory so rope_bootstrap and other skill modules can be imported. Use the actual resolved path, e.g. /Users/me/.claude/skills/python-refactor/scripts.
The bootstrap provides these flags automatically — no need to add them:
--project-root(optional) — rope project root (defaults to git repository root)--diff— show unified diff without applying changes
Each refactor script adds its own args via setup_args. Access all parsed args through ctx.args.
Always run with --diff first, then apply.
Use ctx.find_files() to select files. It combines glob matching with optional text pre-filtering (via ripgrep, falling back to grep):
# All .py files in the project
files = ctx.find_files()
# Only files containing "cmd" or "send", filtered by globs
files = ctx.find_files(
patterns={"cmd", "send"},
include=["tests/**/*.py"],
exclude=["conftest.py"],
)Searches from the project root. When patterns is provided, only files containing a match are included. The result is always intersected with include/exclude globs. Logs which tool was used, how many files matched, and how many were excluded.
resource = ctx.get_resource(file_path) # Path → rope File resource
source = resource.read() # current source text
# Build a ChangeSet and apply it — changes write to disk immediately via rope's history
from rope.base.change import ChangeSet, ChangeContents
cs = ChangeSet("description")
cs.add_change(ChangeContents(resource, new_source))
ctx.do(cs) # apply changes (writes to disk, tagged for undo)Each ctx.do() call writes to disk immediately through project.do() and tags the change with a state hash for undo/redo support. Use refactor_history.py undo and refactor_history.py redo to reverse or reapply changes after a run.
ctx.project # the rope Project instance
ctx.args # parsed CLI args (bootstrap + script-specific)
ctx.dry_run # True if --diffUse ctx.do() to apply rope's built-in refactorings:
from rope.refactor.rename import Rename
def refactor(ctx: RefactorContext) -> None:
resource = ctx.get_resource(some_path)
# offset = byte position of the symbol to rename
changes = Rename(ctx.project, resource, offset).get_changes("new_name")
ctx.do(changes)Available: Rename, Move, ExtractMethod, ExtractVariable, Inline, ChangeSignature, IntroduceParameter, Restructure, UseFunction, MethodObject, IntroduceFactory, EncapsulateField, LocalToField, ToPackage.
See rope-api.md for full usage details.