Transactions¶
Rejig supports atomic transactions for batch operations. Changes are collected and applied together, with automatic rollback on failure.
Basic Usage¶
from rejig import Rejig
rj = Rejig("src/")
# Use the context manager to collect changes, then commit explicitly
with rj.transaction() as tx:
rj.find_class("OldClass").rename("NewClass")
rj.find_function("old_func").rename("new_func")
rj.find_class("Job").find_method("process").add_parameter("timeout", "int", "30")
# Apply all changes atomically
tx.commit()
How Transactions Work¶
- Collection Phase: All modifications are collected but not written
- Commit Phase: Calling
tx.commit()writes all changes atomically - Rollback Phase: If the context exits without committing (e.g. on an
exception, or if you simply never call
commit()), the transaction is automatically rolled back and no changes are written
rj = Rejig("src/")
with rj.transaction() as tx:
# These changes are collected, not immediately written
rj.find_class("A").rename("B")
rj.find_class("C").rename("D")
# If an exception occurs here, nothing is written
if some_condition:
raise ValueError("Abort!")
# More changes...
rj.find_function("x").rename("y")
# Write all changes together
tx.commit()
Manual Transaction Control¶
For more control, decide whether to commit or roll back inside the context:
with rj.transaction() as tx:
rj.find_class("A").rename("B")
rj.find_class("C").rename("D")
# Validate changes before committing
if validation_passes():
tx.commit()
else:
tx.rollback()
# If neither commit() nor rollback() is called (e.g. an exception
# propagates), the transaction is auto-rolled back on exit.
Preview Changes¶
See what would be modified before committing:
with rj.transaction() as tx:
rj.find_class("OldName").rename("NewName")
rj.find_function("helper").add_decorator("cache")
# preview() returns a combined unified diff string
print(f"Files to modify: {tx.pending_files}")
print(f"Diff:\n{tx.preview()}")
# Optionally abort
if not confirm("Apply changes?"):
tx.rollback()
return
tx.commit()
Transaction Status¶
Check the current transaction state:
rj = Rejig("src/")
# Check if in transaction
print(rj.in_transaction) # False
with rj.transaction() as tx:
print(rj.in_transaction) # True
# Access current transaction
current = rj.current_transaction
print(current is tx) # True
# Transaction has pending changes
rj.find_class("A").rename("B")
print(tx.pending_count) # 1 (files with pending changes)
print(tx.pending_files) # [Path("models.py")]
Nested Transactions¶
Transactions cannot be nested. Starting a new transaction inside an existing one will raise an error:
with rj.transaction():
# This raises RuntimeError: Nested transactions are not supported
with rj.transaction():
pass
Batch Result from Transaction¶
tx.commit() returns a BatchResult covering every applied change:
with rj.transaction() as tx:
rj.find_class("A").rename("B")
rj.find_function("x").rename("y")
rj.find_class("Worker").find_method("run").add_decorator("cache")
# commit() returns the combined result
batch_result = tx.commit()
print(f"Success: {batch_result.success}")
print(f"Files changed: {batch_result.files_changed}")
print(f"Operations: {len(batch_result)}")
# Access individual results
for result in batch_result:
print(f" {result.message}")
Error Handling¶
Automatic Rollback¶
try:
with rj.transaction():
rj.find_class("A").rename("B")
raise ValueError("Something went wrong")
rj.find_class("C").rename("D") # Never reached
except ValueError:
# Transaction was automatically rolled back
# No files were modified
pass
Handling Partial Failures¶
Operations that fail return ErrorResult but don't abort the transaction:
with rj.transaction() as tx:
# This succeeds
result1 = rj.find_class("Existing").rename("NewName")
# This fails (class doesn't exist) but doesn't abort
result2 = rj.find_class("NonExistent").rename("Something")
# This still runs
result3 = rj.find_function("helper").rename("utility")
# Commit applies only the successful operations
tx.commit()
To abort on any failure:
with rj.transaction() as tx:
results = []
results.append(rj.find_class("A").rename("B"))
results.append(rj.find_class("C").rename("D"))
results.append(rj.find_function("x").rename("y"))
# Check all succeeded
if not all(results):
failed = [r for r in results if not r.success]
for r in failed:
print(f"Failed: {r.message}")
tx.rollback()
else:
tx.commit()
Use Cases¶
Coordinated Renames¶
with rj.transaction() as tx:
# Rename class and update all method references
cls = rj.find_class("UserManager")
cls.rename("UserService")
# Update factory function
rj.find_function("get_user_manager").rename("get_user_service")
# Update remaining textual references (e.g. type hints) across files
for file in rj.find_files():
file.replace_pattern(r"\bUserManager\b", "UserService")
tx.commit()
Safe Migration¶
def migrate_api_version():
rj = Rejig("src/")
with rj.transaction() as tx:
# Update version constant
rj.file("myapp/version.py").replace_pattern(
r"API_VERSION = '1\.0'", "API_VERSION = '2.0'"
)
# Update the api_version argument wherever it appears
for file in rj.find_files():
file.replace_pattern(r"api_version='1\.0'", "api_version='2.0'")
# Preview and confirm (preview() returns a unified diff string)
print(tx.preview())
if not confirm("Apply migration?"):
tx.rollback()
return False
tx.commit()
return True
Batch Refactoring¶
with rj.transaction() as tx:
# Add logging to all API endpoints
for cls in rj.find_classes(pattern=".*View$"):
for method in cls.find_methods():
if method.name in ["get", "post", "put", "delete"]:
method.insert_statement("logger.info(f'API call: {request.path}')")
# Add timing decorator to functions under src/api/
api_funcs = rj.find_functions().filter(
lambda f: "src/api/" in str(f.file_path)
)
for func in api_funcs:
func.add_decorator("timing")
tx.commit()
Conditional Commit¶
with rj.transaction() as tx:
# Collect all changes
rj.find_classes().add_decorator("dataclass")
# Inspect the combined diff before deciding
diff = tx.preview() # unified diff string
if not changes_look_safe(diff): # your validation function
print("Changes failed validation, rolling back")
tx.rollback()
else:
tx.commit()
Dry Run with Transactions¶
Combine dry run mode with transactions:
rj = Rejig("src/", dry_run=True)
with rj.transaction() as tx:
rj.find_class("A").rename("B")
rj.find_function("x").rename("y")
# Even in dry run, preview shows what would happen
print(tx.preview())
# Nothing is written (dry run mode)
# But you can see the complete diff of all changes
Best Practices¶
- Keep transactions focused: Don't mix unrelated changes
- Preview before commit: Use
tx.preview()for complex changes - Handle errors: Decide whether to abort or continue on failures
- Use dry run for testing: Test your refactoring logic safely
- Avoid side effects: Don't perform I/O during collection phase