0
# Codemod Framework
1
2
LibCST's codemod framework provides a high-level system for building automated code transformation applications. It handles context management, parallel processing, file discovery, error handling, and testing utilities for large-scale refactoring operations.
3
4
## Capabilities
5
6
### Base Codemod Classes
7
8
Foundation classes for building code transformation commands.
9
10
```python { .api }
11
class CodemodCommand:
12
"""Base class for codemod commands."""
13
14
def transform_module(self, tree: Module) -> Module:
15
"""
16
Transform a module CST.
17
18
Parameters:
19
- tree: Module CST to transform
20
21
Returns:
22
Module: Transformed module CST
23
"""
24
25
def get_description(self) -> str:
26
"""Get human-readable description of the transformation."""
27
28
class VisitorBasedCodemodCommand(CodemodCommand):
29
"""Base class for visitor-based codemod commands."""
30
31
def leave_Module(self, original_node: Module, updated_node: Module) -> Module:
32
"""Transform module using visitor pattern."""
33
34
class MagicArgsCodemodCommand(CodemodCommand):
35
"""Base class for commands with automatic argument handling."""
36
```
37
38
### Context Management
39
40
Manage execution context and state during transformations.
41
42
```python { .api }
43
class CodemodContext:
44
"""Execution context for codemod operations."""
45
46
def __init__(self, metadata_wrapper: MetadataWrapper) -> None:
47
"""
48
Initialize codemod context.
49
50
Parameters:
51
- metadata_wrapper: Metadata wrapper for the module
52
"""
53
54
@property
55
def metadata_wrapper(self) -> MetadataWrapper:
56
"""Access to metadata for the current module."""
57
58
class ContextAwareTransformer(CSTTransformer):
59
"""Transformer with access to codemod context."""
60
61
def __init__(self, context: CodemodContext) -> None:
62
"""
63
Initialize context-aware transformer.
64
65
Parameters:
66
- context: Codemod execution context
67
"""
68
69
@property
70
def context(self) -> CodemodContext:
71
"""Access to codemod context."""
72
73
class ContextAwareVisitor(CSTVisitor):
74
"""Visitor with access to codemod context."""
75
76
def __init__(self, context: CodemodContext) -> None:
77
"""
78
Initialize context-aware visitor.
79
80
Parameters:
81
- context: Codemod execution context
82
"""
83
84
@property
85
def context(self) -> CodemodContext:
86
"""Access to codemod context."""
87
```
88
89
### Transform Results
90
91
Result types for tracking transformation outcomes.
92
93
```python { .api }
94
class TransformResult:
95
"""Base class for transformation results."""
96
code: Optional[str]
97
encoding: str
98
99
class TransformSuccess(TransformResult):
100
"""Successful transformation result."""
101
102
class TransformFailure(TransformResult):
103
"""Failed transformation result."""
104
error: Exception
105
106
class TransformSkip(TransformResult):
107
"""Skipped transformation result."""
108
skip_reason: SkipReason
109
110
class TransformExit(TransformResult):
111
"""Early exit transformation result."""
112
113
class SkipFile(TransformResult):
114
"""File skip result."""
115
116
class SkipReason:
117
"""Enumeration of skip reasons."""
118
BLACKLISTED: ClassVar[str]
119
OTHER: ClassVar[str]
120
121
class ParallelTransformResult:
122
"""Results from parallel transformation execution."""
123
successes: int
124
failures: int
125
skips: int
126
warnings: int
127
```
128
129
### Execution Functions
130
131
High-level functions for running codemod transformations.
132
133
```python { .api }
134
def transform_module(
135
code: str,
136
command: CodemodCommand,
137
*,
138
python_version: str = "3.8"
139
) -> TransformResult:
140
"""
141
Transform a single module with a codemod command.
142
143
Parameters:
144
- code: Source code to transform
145
- command: Codemod command to apply
146
- python_version: Target Python version
147
148
Returns:
149
TransformResult: Result of the transformation
150
"""
151
152
def gather_files(
153
paths: Sequence[str],
154
*,
155
include_generated: bool = False,
156
exclude_patterns: Sequence[str] = ()
157
) -> Sequence[str]:
158
"""
159
Gather Python files for transformation.
160
161
Parameters:
162
- paths: Directories or files to search
163
- include_generated: Include generated files
164
- exclude_patterns: Patterns to exclude
165
166
Returns:
167
Sequence[str]: List of Python file paths
168
"""
169
170
def exec_transform_with_prettyprint(
171
command: CodemodCommand,
172
files: Sequence[str],
173
*,
174
jobs: int = 1,
175
show_diff: bool = True,
176
unified_diff_lines: int = 5,
177
hide_generated: bool = True,
178
hide_blacklisted: bool = True,
179
hide_progress: bool = False
180
) -> int:
181
"""
182
Execute transformation with formatted output.
183
184
Parameters:
185
- command: Codemod command to execute
186
- files: Files to transform
187
- jobs: Number of parallel jobs
188
- show_diff: Show code differences
189
- unified_diff_lines: Context lines in diff
190
- hide_generated: Hide generated files from output
191
- hide_blacklisted: Hide blacklisted files
192
- hide_progress: Hide progress indicators
193
194
Returns:
195
int: Exit code (0 for success)
196
"""
197
198
def parallel_exec_transform_with_prettyprint(
199
command: CodemodCommand,
200
files: Sequence[str],
201
*,
202
jobs: int = 1,
203
**kwargs
204
) -> ParallelTransformResult:
205
"""
206
Execute transformation in parallel with formatted output.
207
208
Parameters:
209
- command: Codemod command to execute
210
- files: Files to transform
211
- jobs: Number of parallel jobs
212
- kwargs: Additional arguments for exec_transform_with_prettyprint
213
214
Returns:
215
ParallelTransformResult: Aggregated results
216
"""
217
218
def diff_code(
219
old_code: str,
220
new_code: str,
221
*,
222
filename: str = "<unknown>",
223
lines: int = 5
224
) -> str:
225
"""
226
Generate unified diff between code versions.
227
228
Parameters:
229
- old_code: Original code
230
- new_code: Transformed code
231
- filename: Filename for diff header
232
- lines: Context lines
233
234
Returns:
235
str: Unified diff output
236
"""
237
```
238
239
### Testing Framework
240
241
Testing utilities for codemod development and validation.
242
243
```python { .api }
244
class CodemodTest:
245
"""Base class for codemod testing."""
246
247
def assert_transform(
248
self,
249
before: str,
250
after: str,
251
*,
252
command: Optional[CodemodCommand] = None,
253
python_version: str = "3.8"
254
) -> None:
255
"""
256
Assert that transformation produces expected result.
257
258
Parameters:
259
- before: Source code before transformation
260
- after: Expected code after transformation
261
- command: Codemod command (defaults to self.command)
262
- python_version: Target Python version
263
"""
264
265
def assert_unchanged(
266
self,
267
code: str,
268
*,
269
command: Optional[CodemodCommand] = None,
270
python_version: str = "3.8"
271
) -> None:
272
"""
273
Assert that transformation leaves code unchanged.
274
275
Parameters:
276
- code: Source code to test
277
- command: Codemod command (defaults to self.command)
278
- python_version: Target Python version
279
"""
280
```
281
282
## Usage Examples
283
284
### Simple Codemod Command
285
286
```python
287
import libcst as cst
288
from libcst.codemod import VisitorBasedCodemodCommand
289
290
class RemovePrintStatements(VisitorBasedCodemodCommand):
291
"""Remove all print() calls from code."""
292
293
def leave_Call(self, original_node, updated_node):
294
# Remove print function calls
295
if isinstance(updated_node.func, cst.Name) and updated_node.func.value == "print":
296
return cst.RemoveFromParent
297
return updated_node
298
299
def get_description(self):
300
return "Remove all print() statements"
301
302
# Usage
303
from libcst.codemod import transform_module
304
305
source = '''
306
def process_data(data):
307
print("Processing data...")
308
result = data * 2
309
print(f"Result: {result}")
310
return result
311
'''
312
313
command = RemovePrintStatements()
314
result = transform_module(source, command)
315
316
if isinstance(result, cst.codemod.TransformSuccess):
317
print("Transformation successful:")
318
print(result.code)
319
else:
320
print(f"Transformation failed: {result.error}")
321
```
322
323
### Context-Aware Codemod
324
325
```python
326
import libcst as cst
327
from libcst.codemod import VisitorBasedCodemodCommand, CodemodContext
328
from libcst.metadata import ScopeProvider
329
330
class SafeVariableRenamer(VisitorBasedCodemodCommand):
331
"""Rename variables while avoiding conflicts."""
332
333
METADATA_DEPENDENCIES = (ScopeProvider,)
334
335
def __init__(self, old_name: str, new_name: str):
336
super().__init__()
337
self.old_name = old_name
338
self.new_name = new_name
339
340
def leave_Name(self, original_node, updated_node):
341
if updated_node.value == self.old_name:
342
# Check if new name conflicts in current scope
343
scope = self.resolve(ScopeProvider)[updated_node]
344
if self.new_name not in scope:
345
return updated_node.with_changes(value=self.new_name)
346
return updated_node
347
348
def get_description(self):
349
return f"Rename '{self.old_name}' to '{self.new_name}' safely"
350
351
# Usage
352
source = '''
353
def example():
354
x = 1
355
y = x + 2
356
return y
357
'''
358
359
command = SafeVariableRenamer("x", "input_value")
360
result = transform_module(source, command)
361
```
362
363
### Batch File Processing
364
365
```python
366
from libcst.codemod import (
367
gather_files,
368
exec_transform_with_prettyprint,
369
parallel_exec_transform_with_prettyprint
370
)
371
372
class UpdateImports(VisitorBasedCodemodCommand):
373
"""Update deprecated import statements."""
374
375
def leave_ImportFrom(self, original_node, updated_node):
376
if isinstance(updated_node.module, cst.Attribute):
377
if updated_node.module.value.value == "oldmodule":
378
new_module = updated_node.module.with_changes(
379
value=cst.Name("newmodule")
380
)
381
return updated_node.with_changes(module=new_module)
382
return updated_node
383
384
# Process multiple files
385
command = UpdateImports()
386
files = gather_files(["src/", "tests/"], exclude_patterns=["**/generated/**"])
387
388
# Sequential processing
389
exit_code = exec_transform_with_prettyprint(
390
command,
391
files,
392
show_diff=True,
393
unified_diff_lines=3
394
)
395
396
# Parallel processing for large codebases
397
result = parallel_exec_transform_with_prettyprint(
398
command,
399
files,
400
jobs=4,
401
show_diff=True
402
)
403
404
print(f"Processed {result.successes} files successfully")
405
print(f"Failed on {result.failures} files")
406
print(f"Skipped {result.skips} files")
407
```
408
409
### Advanced Pattern-Based Codemod
410
411
```python
412
import libcst as cst
413
import libcst.matchers as m
414
from libcst.codemod import VisitorBasedCodemodCommand
415
416
class ModernizeExceptionHandling(VisitorBasedCodemodCommand):
417
"""Modernize exception handling patterns."""
418
419
def leave_ExceptHandler(self, original_node, updated_node):
420
# Transform "except Exception, e:" to "except Exception as e:"
421
if (isinstance(updated_node.type, cst.Name) and
422
isinstance(updated_node.name, cst.AsName) and
423
isinstance(updated_node.name.name, cst.Name)):
424
425
# Check if this is old-style exception syntax
426
if updated_node.name.asname is None:
427
return updated_node.with_changes(
428
name=cst.AsName(
429
name=updated_node.name.name,
430
asname=cst.AsName(name=updated_node.name.name)
431
)
432
)
433
return updated_node
434
435
def get_description(self):
436
return "Modernize exception handling syntax"
437
438
# Usage with matcher-based replacement
439
class ReplaceAssertWithRaise(VisitorBasedCodemodCommand):
440
"""Replace assert False with raise statements."""
441
442
def leave_Assert(self, original_node, updated_node):
443
# Replace "assert False, msg" with "raise AssertionError(msg)"
444
if m.matches(updated_node.test, m.Name(value="False")):
445
if updated_node.msg:
446
return cst.Raise(
447
exc=cst.Call(
448
func=cst.Name("AssertionError"),
449
args=[cst.Arg(value=updated_node.msg)]
450
)
451
)
452
else:
453
return cst.Raise(exc=cst.Call(func=cst.Name("AssertionError")))
454
return updated_node
455
```
456
457
### Codemod Testing
458
459
```python
460
import unittest
461
from libcst.codemod import CodemodTest
462
463
class TestRemovePrintStatements(CodemodTest):
464
command = RemovePrintStatements()
465
466
def test_removes_print_calls(self):
467
before = '''
468
def example():
469
print("hello")
470
x = 42
471
print("world")
472
return x
473
'''
474
after = '''
475
def example():
476
x = 42
477
return x
478
'''
479
self.assert_transform(before, after)
480
481
def test_preserves_other_calls(self):
482
code = '''
483
def example():
484
log("message")
485
return calculate()
486
'''
487
self.assert_unchanged(code)
488
489
def test_removes_nested_prints(self):
490
before = '''
491
if condition:
492
print("debug")
493
process()
494
'''
495
after = '''
496
if condition:
497
process()
498
'''
499
self.assert_transform(before, after)
500
501
if __name__ == "__main__":
502
unittest.main()
503
```
504
505
## Types
506
507
```python { .api }
508
# Base command types
509
class CodemodCommand:
510
"""Base class for codemod commands."""
511
512
class VisitorBasedCodemodCommand(CodemodCommand):
513
"""Visitor-based codemod command."""
514
515
class MagicArgsCodemodCommand(CodemodCommand):
516
"""Command with automatic argument handling."""
517
518
# Context types
519
class CodemodContext:
520
"""Execution context for codemods."""
521
metadata_wrapper: MetadataWrapper
522
523
class ContextAwareTransformer(CSTTransformer):
524
"""Transformer with context access."""
525
context: CodemodContext
526
527
class ContextAwareVisitor(CSTVisitor):
528
"""Visitor with context access."""
529
context: CodemodContext
530
531
# Result types
532
class TransformResult:
533
"""Base transformation result."""
534
code: Optional[str]
535
encoding: str
536
537
class TransformSuccess(TransformResult): ...
538
class TransformFailure(TransformResult):
539
error: Exception
540
class TransformSkip(TransformResult):
541
skip_reason: SkipReason
542
class TransformExit(TransformResult): ...
543
class SkipFile(TransformResult): ...
544
545
class SkipReason:
546
BLACKLISTED: ClassVar[str]
547
OTHER: ClassVar[str]
548
549
class ParallelTransformResult:
550
"""Results from parallel execution."""
551
successes: int
552
failures: int
553
skips: int
554
warnings: int
555
556
# Testing types
557
class CodemodTest:
558
"""Base class for codemod testing."""
559
command: CodemodCommand
560
```