Codemod Recipes¶
Ready-to-use scripts for common code migrations.
Django Migrations¶
Rename Model¶
from rejig import Rejig
def rename_django_model(rj: Rejig, old_name: str, new_name: str):
"""Rename a Django model and update references."""
# Rename the model class
model = rj.find_classes(old_name).first()
if not model:
print(f"Model {old_name} not found")
return
model.rename(new_name)
# Update ForeignKey references in other models
for cls in rj.find_classes():
content = cls.get_content()
if content.success and content.data and old_name in str(content.data):
# This is simplified - real impl would use CST
pass
print(f"Renamed {old_name} to {new_name}")
rj = Rejig("myapp/")
rename_django_model(rj, "UserProfile", "Profile")
Add Created/Updated Timestamps¶
def add_timestamps_to_models(rj: Rejig):
"""Add created_at and updated_at to all models."""
for cls in rj.find_classes():
# Check if it's a Django model
content = cls.get_content()
if not content or "models.Model" not in str(content.data):
continue
# Skip if already has timestamps
if "created_at" in str(content.data):
continue
# Add the fields
cls.add_attribute(
"created_at",
type_hint="models.DateTimeField",
default="models.DateTimeField(auto_now_add=True)"
)
cls.add_attribute(
"updated_at",
type_hint="models.DateTimeField",
default="models.DateTimeField(auto_now=True)"
)
print(f"Added timestamps to {cls.name}")
rj = Rejig("myapp/models/")
add_timestamps_to_models(rj)
Testing Migrations¶
Convert unittest to pytest¶
def convert_unittest_to_pytest(rj: Rejig):
"""Convert unittest test classes to pytest functions."""
for cls in rj.find_classes(pattern="^Test"):
# Skip if not a unittest class
content = cls.get_content()
if not content or "unittest.TestCase" not in str(content.data):
continue
# Remove the base class
cls.remove_base_class("unittest.TestCase")
# Convert setUp to fixture
setup = cls.find_method("setUp")
if setup.exists():
setup.rename("setup_method")
setup.add_decorator("pytest.fixture(autouse=True)")
# Convert assertion methods
for method in cls.find_methods(pattern="^test_"):
# This would need more sophisticated replacement
# assertEqual → assert x == y
# assertTrue → assert x
pass
print(f"Converted {cls.name}")
rj = Rejig("tests/")
convert_unittest_to_pytest(rj)
Add pytest Markers¶
def add_integration_markers(rj: Rejig):
"""Mark tests that use database or network as integration tests."""
integration_indicators = ["database", "db", "requests", "httpx", "aiohttp"]
for cls in rj.find_classes(pattern="^Test"):
content = cls.get_content()
if not content:
continue
content_str = str(content.data).lower()
# Check for integration test indicators
is_integration = any(ind in content_str for ind in integration_indicators)
if is_integration:
cls.add_decorator("pytest.mark.integration")
print(f"Marked {cls.name} as integration")
rj = Rejig("tests/")
add_integration_markers(rj)
Type Hint Migrations¶
Add Type Hints from Defaults¶
def add_type_hints_from_defaults(rj: Rejig):
"""Infer type hints from default parameter values."""
# infer_type_hints() inspects default values to add annotations.
# Pass overwrite=False (the default) to leave existing hints untouched.
for func in rj.find_functions():
result = func.infer_type_hints(overwrite=False)
if result.success and result.files_changed:
print(f"Added hints to {func.name}")
rj = Rejig("src/")
add_type_hints_from_defaults(rj)
Modernize Type Hints¶
def modernize_all_type_hints(rj: Rejig):
"""Update to Python 3.10+ type hint syntax."""
for file in rj.find_files():
result = file.modernize_type_hints()
if result.files_changed:
print(f"Modernized {file.path}")
rj = Rejig("src/")
modernize_all_type_hints(rj)
API Migrations¶
Rename Function Across Codebase¶
def rename_api_function(rj: Rejig, old_name: str, new_name: str):
"""Rename a function and update all call sites."""
# Find and rename the function definition
func = rj.find_functions(old_name).first()
if func:
func.rename(new_name)
print(f"Renamed definition: {old_name} → {new_name}")
# Update imports
for file in rj.find_files():
content = file.get_content()
if not content:
continue
if f"from .* import.*{old_name}" in str(content.data):
# Update import statement
file.replace_pattern(
rf"(from .* import.*)(\b{old_name}\b)",
rf"\1{new_name}"
)
# Update call sites
file.replace_pattern(rf"\b{old_name}\s*\(", f"{new_name}(")
rj = Rejig("src/")
rename_api_function(rj, "process_data", "transform_data")
Deprecate and Redirect¶
def deprecate_function(rj: Rejig, old_name: str, new_name: str):
"""Add deprecation wrapper that redirects to new function."""
func = rj.find_functions(old_name).first()
if not func:
print(f"Function {old_name} not found")
return
# Get the file
file = rj.file(func.file_path)
# Add deprecation wrapper
wrapper = f'''
def {old_name}(*args, **kwargs):
"""Deprecated: Use {new_name} instead."""
import warnings
warnings.warn(
"{old_name} is deprecated, use {new_name}",
DeprecationWarning,
stacklevel=2
)
return {new_name}(*args, **kwargs)
'''
# Rename original
func.rename(new_name)
# Add wrapper after the renamed function
# (This would need proper positioning logic)
print(f"Deprecated {old_name} → {new_name}")
rj = Rejig("src/")
deprecate_function(rj, "old_api", "new_api")
Cleanup Codemods¶
Remove Debug Statements¶
def remove_debug_statements(rj: Rejig):
"""Remove print statements and debugger calls."""
debug_patterns = [
r"^\s*print\s*\(",
r"^\s*import pdb",
r"^\s*pdb\.set_trace\(\)",
r"^\s*breakpoint\(\)",
r"^\s*import ipdb",
r"^\s*ipdb\.set_trace\(\)",
]
for file in rj.find_files():
for pattern in debug_patterns:
file.replace_pattern(pattern, "")
print(f"Cleaned {file.path}")
rj = Rejig("src/")
remove_debug_statements(rj)
Remove Type Ignores¶
def cleanup_type_ignores(rj: Rejig):
"""Remove all type: ignore comments from the codebase."""
result = rj.remove_all_type_ignores()
if result.files_changed:
print(f"Cleaned {len(result.files_changed)} files")
for f in result.files_changed:
print(f" - {f}")
rj = Rejig("src/")
cleanup_type_ignores(rj)
Tip: to audit before removing, use
rj.find_type_ignores()(orrj.find_bare_type_ignores()for those missing an error code).
Standardize String Quotes¶
def standardize_quotes(rj: Rejig):
"""Convert all strings to double quotes (let Black handle it)."""
# This is better handled by Black, but as an example:
for file in rj.find_files():
file.replace_pattern(r"'([^']*)'", r'"\1"')
# Better approach: just run Black
import subprocess
subprocess.run(["black", "src/"])
Project Setup Codemods¶
Initialize pyproject.toml¶
def init_pyproject(rj: Rejig, project_name: str):
"""Create a modern pyproject.toml."""
toml = rj.toml("pyproject.toml")
# Project metadata
toml.set("project.name", project_name)
toml.set("project.version", "0.1.0")
toml.set("project.requires-python", ">=3.10")
toml.set("project.dependencies", [])
toml.set("project.optional-dependencies.dev", [
"pytest>=7.0",
"black>=23.0",
"ruff>=0.1.0",
"mypy>=1.0",
])
# Tool configuration
toml.set("tool.black.line-length", 110)
toml.set("tool.ruff.select", ["E", "F", "W", "I"])
toml.set("tool.ruff.line-length", 110)
toml.set("tool.mypy.python_version", "3.10")
toml.set("tool.mypy.strict", True)
toml.set("tool.pytest.ini_options.testpaths", ["tests"])
print(f"Created pyproject.toml for {project_name}")
rj = Rejig(".")
init_pyproject(rj, "my-project")
Running Codemods¶
As a Script¶
#!/usr/bin/env python3
"""Codemod: Add type hints to all functions."""
from rejig import Rejig
import sys
def main():
path = sys.argv[1] if len(sys.argv) > 1 else "src/"
rj = Rejig(path)
# infer_type_hints() is a batch operation on the TargetList and returns a
# BatchResult. With overwrite=False, already-annotated functions are skipped.
results = rj.find_functions().infer_type_hints(overwrite=False)
print(f"Updated {len(results.succeeded)} functions")
if results.failed:
print(f"Failed: {len(results.failed)}")
if __name__ == "__main__":
main()
With Dry Run¶
def run_codemod(rj: Rejig, dry_run: bool = True):
"""Run codemod with optional dry run."""
if dry_run:
rj = Rejig(rj.root, dry_run=True)
print("DRY RUN - no files will be modified")
# Your codemod logic here
result = rj.find_classes("^Test").add_decorator("@slow")
if dry_run:
print(f"Would modify {len(result)} classes")
else:
print(f"Modified {len(result.files_changed)} files")
rj = Rejig("src/")
run_codemod(rj, dry_run=True) # Preview
run_codemod(rj, dry_run=False) # Apply