Skip to content

Batch Operations

Apply changes to multiple targets at once.

TargetList

When you find multiple items, you get a TargetList:

classes = rj.find_classes(pattern="^Test")  # TargetList[ClassTarget]
funcs = rj.find_functions()                  # TargetList[FunctionTarget]

Iteration

for cls in classes:
    print(f"{cls.name} in {cls.file_path}")

Accessing Items

classes.first()    # First target or None
classes.last()     # Last target or None
classes[0]         # First target (raises IndexError if empty)
classes[2:5]       # Slice (returns list)
len(classes)       # Number of targets
bool(classes)      # True if not empty
classes.to_list()  # Convert to plain list

Filtering

By Predicate

# Keep only classes with docstrings (has_docstring is a property)
classes.filter(lambda c: c.has_docstring)

# Keep only targets that exist
targets.filter(lambda t: t.exists())

There are also convenience filters for docstrings:

classes.with_docstrings()     # only targets that have a docstring
classes.without_docstrings()  # only targets that lack one

By File

classes.in_file(Path("models.py"))
classes.in_file("models.py")  # String also works

By Name Pattern

classes.matching("User")       # Name contains "User"
classes.matching("^Base")      # Name starts with "Base"
classes.matching("Handler$")   # Name ends with "Handler"

Chaining Filters

targets = (
    rj.find_classes()
    .filter(lambda c: not c.has_docstring)
    .in_file("models.py")
    .matching("^User")
)

Batch Operations

Operations on TargetList apply to all targets and return a BatchResult:

Decorators

results = classes.add_decorator("dataclass")
results = classes.remove_decorator("deprecated")

Renaming

# Rename using regex substitution
results = classes.rename(r"^Old", "New")  # OldUser → NewUser
results = funcs.rename(r"_v1$", "_v2")    # process_v1 → process_v2

Deleting

results = classes.delete()
results = funcs.delete()

Inserting Statements

methods = cls.find_methods()
results = methods.insert_statement("self.log_call()", position="start")

Text Replacement Across Files

replace_all performs a literal in-place text replacement on every file in the list, making whole-tree find-and-replace a one-liner:

# Drop the `not` from every is_token_expired caller under django-root/*/api.py
result = rj.find_files("django-root/*/api.py").replace_all(
    "not is_token_expired(", "is_token_expired("
)
for path in result.files_changed:
    print(f"updated {path}")

The match is literal — the ( above is taken verbatim, not as a regex group. For regex (with backreferences, flags, or count), call file.replace_pattern(...) on individual FileTargets, or file.replace(old, new) for a single-file literal replacement.

Aliases

Some batch methods have _all aliases for clarity:

classes.delete_all()           # Same as delete()
classes.add_decorator_all()    # Same as add_decorator()
classes.rename_all()           # Same as rename()

BatchResult

Batch operations return a BatchResult:

results = classes.add_decorator("@slow")

# Check overall status
results.success          # True if ALL operations succeeded
results.partial_success  # True if ANY operation succeeded
results.all_failed       # True if ALL operations failed

# Access individual results
results.succeeded        # list[Result] - successful operations
results.failed           # list[Result] - failed operations

# Aggregate information
results.files_changed    # list[Path] - all files modified
len(results)             # Total number of operations

# Iterate
for result in results:
    print(result.message)

Handling Partial Failures

results = classes.add_decorator("@pytest.mark.slow")

if results.success:
    print(f"Added decorator to all {len(results)} classes")
elif results.partial_success:
    print(f"Succeeded: {len(results.succeeded)}, Failed: {len(results.failed)}")
    for r in results.failed:
        print(f"  Failed: {r.message}")
else:
    print("All operations failed")

Common Patterns

Add Decorator to All Test Classes

results = rj.find_classes(pattern="^Test").add_decorator("pytest.mark.integration")

Generate Docstrings for Functions Without Them

results = (
    rj.find_functions()
    .without_docstrings()
    .generate_docstrings(style="google")
)

Add Type Hints to All Public Functions

results = (
    rj.find_functions_without_type_hints()
    .filter(lambda f: not f.name.startswith("_"))
    .infer_type_hints()
)

Rename Methods Across Multiple Classes

for cls in rj.find_classes(pattern=".*Repository"):
    cls.find_method("get_all").rename("list_all")

Delete All Deprecated Methods

for cls in rj.find_classes():
    deprecated = cls.find_methods().filter(
        lambda m: "@deprecated" in str(m.get_content().data or "")
    )
    deprecated.delete()

Bulk Add Type Ignore

# Find all lines with mypy errors and add ignores
for file in rj.find_files():
    for line_num in get_mypy_error_lines(file.path):  # Your function
        file.line(line_num).add_type_ignore()

Performance Considerations

Batch operations are generally more efficient than looping:

# Preferred - single batch operation
rj.find_classes("^Test").add_decorator("@slow")

# Less efficient - individual operations
for cls in rj.find_classes("^Test"):
    cls.add_decorator("@slow")

The batch approach: - Parses each file once - Applies all changes in a single pass - Writes each file once

Next Steps