Skip to main content

Rejig

A comprehensive Python library for programmatic code refactoring, analysis, and transformation. The goal of this library is to help you automate common editing, refactoring and optimisations within a python codebase. Rejig is not an AI/LLM, it is an API for making targeted changes to code.

Use-cases

I built this library primarily to automate codebase changes that were too complex for basic tools like sed or patch - however it has been fleshed out to perform a wide variety of tasks I consider useful for python code development. I wanted the power of a library like libCST without the complexity. While it doesn't always make sense to automate changes, sometimes it does - this library is for those times.

Where this library really shines is when you want to automate changes to a codebase but you don't know what other changes have been made. This may be the case where you have multiple projects based on the same/similar template but diverged enough that you can't just use git or patch to ship a set of changes. That was exactly the scenario that led me to write this - dealing with 20+ repos based on the same original code but with no common git history. Git didn't want to know about it because it needs a common parent commit and regular patch would fail due to line number or whitespace changes. One of the things Rejig does well is let you accurately target the thing you want to change even when you don't know exactly where it is.

Here are some other usage suggestions:

  • As an IDE or AI Backend — This framework supports a lot of features you get in an IDE like PyCharm, except it's headless. You could wrap this in a UI or build an MCP server.
  • Improve Code — Use it to find and modernize legacy programming patterns in a older codebase. Find errors and dead code. Add documentation, directives and type hints. Break up long files.
  • Migrate Frameworks — ie, Move your Flask project to Django. Switch from Poetry to UV, etc
  • As an LLM alternative - You want to automate some things with CoPilot/Claude but perhaps you're not allowed due to contract restrictions. This library provides a compromise between AI automation and tedious manual edits. It also means you get deterministic output instead of whatever an LLM thinks is right at the time.

Features

  • Fluent Target API — Chain operations naturally: rj.file("app.py").find_class("User").find_method("save")
  • Batch Operations — Apply changes to multiple targets at once with TargetList
  • Atomic Transactions — Collect changes and apply them atomically with rollback support
  • Dry-run Mode — Preview all changes before applying them
  • Code Analysis — Detect complexity issues, dead code, and patterns
  • Security Scanning — Find hardcoded secrets and vulnerability patterns
  • Optimization Detection — Identify duplicate code and loop improvements
  • Import Management — Organize, detect unused, and fix circular imports
  • Type Hint Operations — Infer, modernize, and generate type hints
  • Docstring Generation — Create and update docstrings in multiple styles
  • Config File Support — Manipulate TOML, YAML, JSON, and INI files
  • Project Management — Manage pyproject.toml, dependencies, and tool configs
  • Framework Support — Django, Flask, FastAPI, and SQLAlchemy integrations
  • Patch to Script - Convert a patch file into a python script and vice-versa.

Installation

pip install rejig

# For framework support
pip install rejig[django]    # Django projects
pip install rejig[flask]     # Flask projects
pip install rejig[fastapi]   # FastAPI projects

# For all features
pip install rejig[all]

Quick Start

from rejig import Rejig

# Initialize with a directory, file, or glob pattern
rj = Rejig("src/")

# Find and modify code
rj.find_class("MyClass").add_attribute("count", "int", "0")
rj.find_class("MyClass").find_method("process").insert_statement("self.validate()")

# Preview changes without modifying files
rj = Rejig("src/", dry_run=True)
result = rj.find_class("MyClass").add_attribute("x", "int", "0")
print(result.message)  # [DRY RUN] Would add attribute...
print(result.diff)     # Shows unified diff

Core API

Finding Code Elements

rj = Rejig("src/")

# Find by name
cls = rj.find_class("MyClass")
func = rj.find_function("process_data")
method = rj.find_class("MyClass").find_method("save")

# Find multiple with patterns
classes = rj.find_classes(pattern="^Test")              # All test classes
methods = rj.find_class("MyClass").find_methods(pattern="^get_")  # Getter methods
funcs = rj.find_functions(pattern=".*_handler$")        # All handlers

# Find in specific files
file_target = rj.file("models.py")
module_target = rj.module("myapp.models")

# Find other elements
todos = rj.find_todos()
imports = rj.file("models.py").find_imports()
strings = rj.find_hardcoded_strings(min_length=10)

Class Operations

cls = rj.find_class("MyClass")

# Attributes
cls.add_attribute("cache", "dict[str, Any] | None", "None")
cls.remove_attribute("old_attr")

# Methods
cls.add_method("validate", "return self.is_valid()")
cls.find_method("process").rename("handle")

# Decorators
cls.add_decorator("dataclass")
cls.remove_decorator("deprecated")

# Structure
cls.rename("NewClassName")
cls.add_base_class("BaseModel")
cls.convert_to_dataclass()
cls.delete()

Method & Function Operations

method = rj.find_class("MyClass").find_method("process")

# Statements
method.insert_statement("self.validate()", position="start")
method.insert_before_match(r"return\s+", "self.log_result(result)")
method.insert_after_match(r"result\s*=", "self.validate_result(result)")

# Parameters
method.add_parameter("timeout", "int", "30")
method.remove_parameter("old_param")
method.rename_parameter("data", "payload")
method.set_parameter_type("value", "str | None")

# Decorators
method.add_decorator("cached_property")
method.remove_decorator("staticmethod")
method.convert_to_classmethod()

# Type hints and docstrings
method.set_return_type("list[str]")
method.infer_type_hints()
method.generate_docstring(style="google")

# Conversions
method.convert_to_async()
method.wrap_with_try_except(["ValueError"], "logger.error(e)")

Batch Operations

# Apply operations to multiple targets
classes = rj.find_classes(pattern="^Test")
classes.add_decorator("pytest.mark.slow")

# Filter and operate
rj.find_functions().in_file("utils.py").add_decorator("timer")
rj.find_class("TestSuite").find_methods(pattern="^test_").add_decorator("skip")

# Type hints for all functions
rj.find_functions().infer_type_hints()
rj.find_functions().modernize_type_hints()

# Generate docstrings
rj.find_functions().without_docstrings().generate_docstrings(style="google")

Line Operations

file = rj.file("config.py")

# Single lines
line = file.line(42)
line.insert_before("# Important:")
line.insert_after("logger.info('done')")
line.rewrite("new_content = True")

# Line ranges
block = file.lines(10, 20)
block.indent(1)
block.delete()

# Code blocks
for_block = file.block_at_line(15)
for_block.insert_after("total += 1")

Import Management

from rejig import Rejig, ImportOrganizer, ImportGraph
from pathlib import Path

rj = Rejig("src/")

# Add imports
file = rj.file("module.py")
file.add_import("from typing import Optional, List")

# Organize imports (isort-like)
file.organize_imports()
# ...or organize a file via the ImportOrganizer directly
organizer = ImportOrganizer(rj)
organizer.organize(Path("src/module.py"))

# Find unused imports
unused = file.find_unused_imports()
unused.delete_all()

# Detect circular imports
graph = ImportGraph(rj)
cycles = graph.find_circular_imports()
for cycle in cycles:
    print(f"Circular: {' -> '.join(cycle.cycle)}")

Type Hints

rj = Rejig("src/")

# Infer from defaults and names
func = rj.find_function("process")
func.infer_type_hints()  # count: int, is_valid: bool, items: list

# Modernize syntax (Python 3.10+)
rj.find_functions().modernize_type_hints()
# List[str] -> list[str]
# Optional[int] -> int | None
# Union[str, int] -> str | int

# Add specific type hints
func.set_parameter_type("data", "dict[str, Any]")
func.set_return_type("list[str]")

# Generate stub files
from rejig.typehints import StubGenerator
from pathlib import Path
StubGenerator(rj).generate_for_package(Path("src/"), Path("stubs/"))

Docstrings

rj = Rejig("src/")

# Generate docstrings from signatures
func = rj.find_function("process")
func.generate_docstring(style="google")  # or "numpy", "sphinx"

# Generate for all functions without docstrings
rj.find_functions().without_docstrings().generate_docstrings()

# Convert between styles
rj.find_functions().convert_docstring_style("google", "numpy")

# Update individual sections of an existing docstring
func.update_docstring_param("timeout", "Maximum wait time in seconds")
func.add_docstring_returns("The processed result")

Code Analysis

from rejig import Rejig, AnalysisType

rj = Rejig("src/")

# Run a full analysis (returns an AnalysisReport)
report = rj.analyze_code()
print(f"Total issues: {report.total_issues}")
print(report)  # human-readable summary

# Drill into the finding lists (each is an AnalysisTargetList)
high_complexity = report.complexity_issues.by_type(AnalysisType.HIGH_CYCLOMATIC_COMPLEXITY)
long_functions = report.complexity_issues.by_type(AnalysisType.LONG_FUNCTION)

# Group by file
by_file = report.pattern_issues.group_by_file()
for file_path, file_issues in by_file.items():
    print(f"{file_path}: {len(file_issues)} issues")

# Find dead code with dedicated finders
unused_functions = rj.find_unused_functions()
unused_classes = rj.find_unused_classes()
unused_variables = rj.find_unused_variables()

Security Scanning

from rejig import Rejig

rj = Rejig("src/")

# Find security issues
security = rj.find_security_issues()

# Filter by severity
critical = security.critical()
high = security.high()

# Filter by category
secrets = security.secrets()
injection = security.injection_risks()

# Get detailed report
for issue in security:
    print(f"{issue.severity}: {issue.message}")
    print(f"  {issue.file_path}:{issue.line_number}")

Optimization Detection

from rejig import Rejig, OptimizeType
from rejig.optimize import DRYAnalyzer, LoopOptimizer

rj = Rejig("src/")

# Duplicate code detection (DRY analysis)
dry = DRYAnalyzer(rj)
duplicates = dry.find_all_issues().by_type(OptimizeType.DUPLICATE_CODE_BLOCK)
for dup in duplicates:
    print(f"Duplicate code at {dup.location}")

# Loop optimization suggestions
loops = LoopOptimizer(rj)
optimizations = loops.find_all_issues()
for loop in optimizations:
    print(f"{loop.message}")
    print(f"Suggestion:\n{loop.suggested_code}")

Config Files

rj = Rejig(".")

# TOML files (pyproject.toml, etc.)
toml = rj.toml("pyproject.toml")
toml.set("tool.black.line-length", 110)
toml.get("project.version")
toml.delete("tool.deprecated")

# YAML files
yaml = rj.yaml("config.yaml")
yaml.set("database.host", "localhost")
yaml.get_section("logging")

# JSON files
json_file = rj.json("package.json")
json_file.set("version", "2.0.0")

# INI files
ini = rj.ini("setup.cfg")
ini.set("metadata", "version", "1.0.0")

Project Management

from rejig import PythonProject

# High-level project management
project = PythonProject(".")

# Metadata
project.project().set_version("2.0.0")
project.project().bump_version("minor")
project.project().add_author("Jane Doe", "jane@example.com")

# Dependencies
project.dependencies().add("requests", "^2.28.0")
project.dependencies().update("django", "^4.2.0")
project.dependencies().remove("deprecated-package")
project.dev_dependencies().add("pytest", "^7.0.0")

# Entry points / scripts
project.scripts().add("mycli", "myapp.cli:main")

# Tool configuration
project.black().set_line_length(110)
project.ruff().select(["E", "F", "W"])
project.mypy().enable_strict()
project.pytest().set_testpaths(["tests/"])

Transactions

rj = Rejig("src/")

# Atomic batch operations
with rj.transaction() as tx:
    rj.find_class("OldName").rename("NewName")
    rj.find_function("old_func").rename("new_func")
    rj.find_class("Service").find_methods(pattern="^_old").rename("^_old", "_new")

    # Preview before commit
    print(tx.preview())

# Changes applied atomically, or rolled back on error

TODO Management

rj = Rejig("src/")

# Find all TODOs
todos = rj.find_todos()

# Filter
fixmes = todos.by_type("FIXME")
high_priority = todos.high_priority()
my_todos = todos.by_author("john")
with_issues = todos.with_issues()

# Operations
for todo in todos.without_issues():
    todo.link_to_issue("GH-123")

# Report
from rejig.todos.reporter import TodoReporter
print(TodoReporter(rj, todos).summary())

Linting Directives

rj = Rejig("src/")

# Find type: ignore comments
type_ignores = rj.find_type_ignores()
bare_ignores = type_ignores.bare()
for ignore in bare_ignores:
    ignore.add_code("type-arg")  # Make specific

# Find noqa comments
noqas = rj.find_noqa_comments()
noqas.with_code("E501").remove_all()  # Remove line-length ignores

# Find all directives
from rejig.directives import DirectiveFinder
directives = DirectiveFinder(rj).find_all()
print(directives.count_by_type())

Framework Support

Django

from rejig.django import DjangoProject

with DjangoProject("/path/to/project") as project:
    # Settings
    project.add_installed_app("myapp", after="django.contrib.auth")
    project.add_middleware("myapp.middleware.Custom", position="first")
    project.add_setting("MY_SETTING", '"value"')
    project.update_setting("DEBUG", "False")

    # URLs
    project.add_url_include("myapp.urls", path_prefix="api/")
    project.add_url_pattern("health/", "HealthView.as_view()", name="health")

    # App discovery
    app = project.find_app_containing_class("MyView")

Flask

from rejig.frameworks.flask import FlaskProject

flask = FlaskProject("src/")
flask.add_route("/users", "get_users", methods=["GET"])
flask.add_blueprint("admin", url_prefix="/admin")
flask.add_error_handler(404, "handle_not_found")

FastAPI

from rejig.frameworks.fastapi import FastAPIProject

api = FastAPIProject("src/")
api.add_endpoint("/items/{id}", "get_item", method="GET")
api.add_dependency("get_db", "Depends(get_database)")
api.add_middleware("CORSMiddleware", allow_origins=["*"])

Result Handling

All operations return a Result object:

result = cls.add_attribute("count", "int", "0")

if result.success:
    print(f"Success: {result.message}")
    print(f"Files changed: {result.files_changed}")
    print(f"Diff:\n{result.diff}")
else:
    print(f"Failed: {result.message}")
    if result.exception:
        print(f"Exception: {result.exception}")

# Results are truthy/falsy
if result:
    print("Operation succeeded!")

Error Handling

Rejig never raises exceptions for missing targets. Instead, ErrorTarget allows safe chaining:

# This won't raise even if class doesn't exist
result = rj.find_class("NonExistent").find_method("foo").rename("bar")

if not result:
    print(result.message)  # "Class 'NonExistent' not found"

Dry Run Mode

Preview all changes without modifying files:

rj = Rejig("src/", dry_run=True)
result = rj.find_class("MyClass").rename("NewClass")
print(result.message)  # [DRY RUN] Would rename class MyClass to NewClass
print(result.diff)     # Shows what would change

Requirements

  • Python 3.10+
  • libcst >= 1.0.0
  • rope >= 1.0.0 (for move operations)

Documentation

Full documentation: docs/

License

MIT

Contributing

A significant portion of this library was generated using Claude Code. That doesn't mean humans aren't welcome to contribute. Contact the author via Github Repository (https://github.com/SpliFF/rejig) or email (spliff@warriorhut.org) if you have feature requests or contributions you think should be included.

Metadata

Release files for rejig 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for rejig 0.1.6
File Size Uploaded Provenance Actions
rejig-0.1.6.tar.gz 648.3 kB With provenance Details for rejig-0.1.6.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for rejig 0.1.6
File Interpreter ABI Platform Actions
rejig-0.1.6-py3-none-any.whl Python 3 none any Details for rejig-0.1.6-py3-none-any.whl

Total release size: 1.1 MB

Release files / rejig-0.1.6.tar.gz

Download URL rejig-0.1.6.tar.gz
Size 648.3 kB
Tags Source
SHA-256 checksum
How to use checksums
48f8269901e810bec1e8a6524733b7b2464c5fb0ff47e017b4acf3778ba2c41a
BLAKE2b-256 checksum
How to use checksums
52834c766c44aa1d6aeba0a8a2ab164dbe0e1711061de7bf7a10e608c8a7485a
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 Aug 21, 2026.

Transparency log

Release files / rejig-0.1.6-py3-none-any.whl

Download URL rejig-0.1.6-py3-none-any.whl
Size 456.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2f8e63e560eb0b96cb73fea0f3e3b2180e3ef7534db7e96bbf427bb050340f2
BLAKE2b-256 checksum
How to use checksums
3a50007ad4b3089880f0761a4db93e4dac8654a41109cc565a325eb97909005a
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 Aug 21, 2026.

Transparency log

Security for rejig 0.1.6

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for rejig-0.1.6.tar.gz

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 Aug 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page