A Python refactoring CLI tool with structured edits and pattern transforms
Project description
emend
A Python refactoring CLI tool built on LibCST, with AST-based commands for handling nested functions and closures.
Built on two complementary systems:
- Structured Edits - Precise changes to symbol metadata using selectors like
file.py::func[params][0] - Pattern Transforms - Code-pattern search and replace with capture variables like
print($X)→logger.info($X)
Installation
# Create virtual environment and install
make venv
# Or manually:
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
Usage
emend <command> [options]
Workflow
- Preview changes: Run with
--dry-run(default) to see what will change - Review the diff output
- Apply changes: Re-run with
--apply - Format code: Run formatters (black/ruff/isort) - emend may not preserve exact formatting
- Verify: Run tests/type checks
Selector Syntax
Three types of selectors:
Symbol Selectors
file.py::Class.method.nested # Nested symbol path
file.py::func # Module-level symbol
Extended Selectors (with components)
file.py::func[params] # Function parameters
file.py::func[params][ctx] # Specific parameter (by name)
file.py::func[params][0] # Specific parameter (by index)
file.py::func[returns] # Return annotation
file.py::func[decorators] # Decorator list
file.py::MyClass[bases] # Base classes
file.py::func[body] # Function body
Pseudo-class Selectors
file.py::func[params]:KEYWORD_ONLY # Keyword-only parameter slot
file.py::func[params]:POSITIONAL_ONLY # Positional-only parameter slot
Line Selectors
file.py:42 # Single line
file.py:42-100 # Line range
Wildcard Selectors
file.py::*[params] # All function parameters
file.py::Test*[decorators] # Test class parameters
file.py::*.*[returns] # All method return types
file.py::Class.*[body] # All method bodies in Class
Wildcards support glob patterns:
*- Match any symbol at this levelTest*- Match symbols starting with Test*.*- Match any method in any classClass.*- Match any method in Class
Selector Grammar (Lark)
start: selector
selector: file_path DOUBLE_COLON symbol_path? component*
file_path: PATH
symbol_path: IDENTIFIER ("." IDENTIFIER)*
component: "[" COMPONENT_NAME "]" accessor? pseudo_class?
accessor: "[" (IDENTIFIER | INT) "]"
pseudo_class: PSEUDO_CLASS
COMPONENT_NAME: "params" | "returns" | "decorators" | "bases" | "body" | "imports"
DOUBLE_COLON: "::"
PATH: /[^:]+/
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
INT: /\d+/
PSEUDO_CLASS: /:KEYWORD_ONLY|:POSITIONAL_ONLY|:POSITIONAL_OR_KEYWORD/
Commands
Read Commands
lookup - Unified read command
Combines get, query, and show with smart mode detection:
- Component extraction (like
get):emend lookup file.py::func[params] - Filtering (like
query):emend lookup file.py --kind function --name test_* - Display source (like
show):emend lookup file.py::func - Wildcard support:
emend lookup 'file.py::*[params]'for bulk extraction - Output modes:
--json,--metadata,--paths-only,--count
edit - Unified write command
Combines set, add, and rm with location control:
- Replace (like
set):emend edit file.py::func[returns] "int" --apply - Insert (like
add):emend edit file.py::func[params] "new_param" --before ctx --apply - Remove (like
rm):emend edit file.py::func[params][old_param] --rm --apply - Position flags:
--before,--after,--atfor precise insertion - Pseudo-class support:
:KEYWORD_ONLY,:POSITIONAL_ONLYfor parameters
Alternate Structured Edit Commands
get- Read component value (alias forlookup)set- Replace component value (alias foredit)add- Insert into list component (alias foredit)rm- Remove symbol or component (alias foredit)
Pattern Transform Commands
find- Find pattern matchesreplace- Simple pattern substitutionbatch- Run multiple operations from JSON
Symbol Management
query- Find symbols with filters → uselookupinsteadshow- Display symbol source code → uselookupinsteadlist-symbols- List symbols in a modulefind-references- Find all references to a symbolrename- Rename a symbol across the projectmove- Move a symbol to another file with import updatescopy-to- Copy a symbol to another file
Module Operations
move-module- Move a module to another packagerename-module- Rename a module file
Utility
batch- Run multiple operations from JSON
Examples
Read and Edit Examples
Using lookup
# Extract function parameters
emend lookup api.py::handler[params]
# Extract all function parameters from a file (wildcard)
emend lookup 'api.py::*[params]'
# Query functions by name pattern
emend lookup src/ --kind function --name test_*
# Show function source code
emend lookup api.py::handler
# Get return types as JSON
emend lookup 'src/**/*.py::*[returns]' --json
Using edit
# Update return type
emend edit api.py::handler[returns] "Response" --apply
# Add parameter with default value
emend edit api.py::handler[params] "timeout: int = 30" --apply
# Add keyword-only parameter using pseudo-class
emend edit "api.py::handler[params]:KEYWORD_ONLY" "debug: bool" --apply
# Insert parameter before specific param
emend edit api.py::handler[params] "ctx: Context" --before user_id --apply
# Remove a specific parameter
emend edit api.py::handler[params][deprecated_arg] --rm --apply
Alternate Structured Edits
# These alternate commands also work:
emend add api.py::handler[params] "timeout: int = 30" --apply
emend set api.py::handler[returns] "Response" --apply
emend rm api.py::handler[params][deprecated_arg] --apply
Pattern Transforms
# Simple find and replace (dry-run by default)
emend replace 'print($X)' 'logger.info($X)' file.py
# Replace within a specific scope
emend replace 'old_var' 'new_var' api.py --in process --apply
# Replace with pattern capture
emend replace 'get_field($N)' 'field$N' api.py --in process --apply
# Find pattern matches
emend find 'print($X)' src/
# Multi-rule batch operations
emend batch rules.json --apply
# Or use shell loop for multiple replacements
for pattern in "pattern1:replacement1" "pattern2:replacement2"; do
IFS=: read -r from to <<< "$pattern"
emend replace "$from" "$to" file.py --apply
done
Advanced Operations
# List symbols with full nesting
emend list-symbols workflow.py --tree-depth 3
# Extract nested function to another file
emend copy-to workflow.py::Builder._build.helper tasks.py --dedent --apply
# Rename a symbol project-wide
emend rename models.py::User --to Account --apply
# Move a symbol to another file (updates imports)
emend move utils.py::parse_date helpers/dates.py --apply
# Find all references to a symbol
emend find-references models.py::User --json
# Copy imports from one file to another (using primitives)
emend get source.py::[imports] | while read import_line; do
emend add dest.py::[imports] "$import_line" --apply
done
Pattern Syntax
Patterns support metavariables for capturing:
# Single expression
emend find 'print($MSG)' src/
# Multiple arguments with capture
emend find 'func($A, $B)' src/
# Variable arguments
emend find 'func($...ARGS)' src/
# Type constraints
emend find 'range($N:int)' src/
# Anonymous metavariables
emend find 'func($_, $ARG)' src/
# Structural constraints
emend find 'print($X)' src/ --inside 'async def'
emend find 'await $X' src/ --not-inside 'if __debug__'
# Supported pattern types:
# Literals: $X, $MSG:str, $N:int, 3.14
# Calls: func($X), obj.method($A, $B)
# Operations: $A + $B, $A and $B, not $X, $X[$Y]
# Collections: ($A, $B), [$X, $Y], {$K: $V}
# Control: return $X, assert $A == $B, raise $EXC
Pattern Grammar (Lark)
start: pattern
pattern: (code_chunk | metavar)+
metavar: DOLLAR (ELLIPSIS)? METAVAR_NAME TYPE_CONSTRAINT?
| DOLLAR UNDERSCORE
DOLLAR: "$"
ELLIPSIS: "..."
UNDERSCORE: "_"
METAVAR_NAME: /[A-Z][A-Z0-9_]*/
TYPE_CONSTRAINT: /:(?:expr|stmt|identifier|int|str|float|call|attr|any)/
code_chunk: /[^$:]+/ | ":"
The code_chunk rule excludes colons (/[^$:]+/) to prevent consuming colons that are part of type constraints (e.g., $MSG:str). A standalone colon is matched by the alternative | ":" for patterns containing colons outside of type constraints.
Diff Patch Format
- pattern_to_find
+ replacement_pattern
- another_pattern
+ another_replacement
Lines prefixed with - are matched; corresponding + lines are the replacement. Blank lines separate rules.
Development
Running Tests
# Run all tests
make test
# Run specific test file
make test TESTS=tests/test_emend/test_add_parameter.py
# Run specific test
make test TESTS="tests/test_emend/test_add_parameter.py::test_add_parameter_with_default"
Project Structure
emend/
├── src/emend/
│ ├── cli.py # CLI entry point, argument parsing
│ ├── transform.py # Transform primitives (get/set/add/remove/find/replace)
│ ├── pattern.py # Pattern parsing and compilation
│ ├── query.py # Symbol querying with filters
│ ├── ast_commands.py # AST-based command implementations
│ ├── ast_utils.py # AST traversal utilities
│ ├── component_selector.py # Extended selector parsing
│ └── grammars/
│ ├── selector.lark # Extended selector grammar
│ └── pattern.lark # Pattern grammar
├── tests/test_emend/ # Test suite
├── Makefile
└── pyproject.toml
License
MPL 2.0
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file emend-0.0.1.tar.gz.
File metadata
- Download URL: emend-0.0.1.tar.gz
- Upload date:
- Size: 167.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b158336005cee044556e7a86a9fed2f63f5562ba367a0d998313c7884b29e32
|
|
| MD5 |
d9d5d7a0d99ced6769303cfb38c22a0b
|
|
| BLAKE2b-256 |
637f5b0bdb501e0be15c95a16d83a3fcd0ea86f88fe1918e5f8cb93593a15ad2
|
Provenance
The following attestation bundles were made for emend-0.0.1.tar.gz:
Publisher:
publish.yml on lucaswiman/emend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
emend-0.0.1.tar.gz -
Subject digest:
6b158336005cee044556e7a86a9fed2f63f5562ba367a0d998313c7884b29e32 - Sigstore transparency entry: 976725923
- Sigstore integration time:
-
Permalink:
lucaswiman/emend@bd113c7fb48fd388308dd9c60e0b89851d28c7d1 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/lucaswiman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bd113c7fb48fd388308dd9c60e0b89851d28c7d1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file emend-0.0.1-py3-none-any.whl.
File metadata
- Download URL: emend-0.0.1-py3-none-any.whl
- Upload date:
- Size: 79.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d58c1e1b489752090d5e36ab1ce25541611e5c62f21268955c9fca0946eb969
|
|
| MD5 |
ebfb0e1490b7bc4e8796c6807c0483e2
|
|
| BLAKE2b-256 |
5bfda1216e8d957fdfab34cbf5313ef75a09093cce1f0f20f507e93c682ec41d
|
Provenance
The following attestation bundles were made for emend-0.0.1-py3-none-any.whl:
Publisher:
publish.yml on lucaswiman/emend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
emend-0.0.1-py3-none-any.whl -
Subject digest:
6d58c1e1b489752090d5e36ab1ce25541611e5c62f21268955c9fca0946eb969 - Sigstore transparency entry: 976725926
- Sigstore integration time:
-
Permalink:
lucaswiman/emend@bd113c7fb48fd388308dd9c60e0b89851d28c7d1 -
Branch / Tag:
refs/tags/v0.0.1 - Owner: https://github.com/lucaswiman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bd113c7fb48fd388308dd9c60e0b89851d28c7d1 -
Trigger Event:
release
-
Statement type: