Skip to main content

A Python library for programmatic code refactoring and transformation.

Reason this release was yanked:

Missing secrets.py

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rejig-0.1.0.tar.gz (631.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rejig-0.1.0-py3-none-any.whl (446.3 kB view details)

Uploaded Python 3

File details

Details for the file rejig-0.1.0.tar.gz.

File metadata

  • Download URL: rejig-0.1.0.tar.gz
  • Upload date:
  • Size: 631.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rejig-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a1af23697c131634d66368a86d0c430c372fc44726ac60ac1ff1f54a3657967e
MD5 b5cbc75d80b5ce915bf2a439d9caa948
BLAKE2b-256 a12ac0bba3da34c5b1b65613e21b256aa2d5e5387cab18c9b03061da87c239c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for rejig-0.1.0.tar.gz:

Publisher: publish.yml on SpliFF/rejig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rejig-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: rejig-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 446.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rejig-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 49e7adb738fa611d0f686304dc7dbe77e26e07ccacc7193057a2574502a5603e
MD5 a1bbd110a039e23705667a98e636f9dd
BLAKE2b-256 0586a7c23ccb5121c8889e26e90f22eecdd6033dd3602acfb411c3bb18cb990c

See more details on using hashes here.

Provenance

The following attestation bundles were made for rejig-0.1.0-py3-none-any.whl:

Publisher: publish.yml on SpliFF/rejig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page