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¶
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¶
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¶
Renaming¶
# Rename using regex substitution
results = classes.rename(r"^Old", "New") # OldUser → NewUser
results = funcs.rename(r"_v1$", "_v2") # process_v1 → process_v2
Deleting¶
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¶
Generate Docstrings for Functions Without Them¶
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¶
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¶
- Config Files — Work with TOML, YAML, and JSON
- Error Handling — Handle failures gracefully