- Python 98.4%
- Nix 1%
- Shell 0.6%
| .forgejo/workflows | ||
| bpatch | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| .python-version | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
bpatch
A library for runtime Python bytecode patching.
Usage
Before patches take effect, you must call manager.apply_all() to apply the registered patches.
Prefix and Postfix (InjectPatcher)
The prefix and postfix decorators provide a high-level way to inject code before or after a target function's execution.
Prefix
A prefix hook runs before the target function. It must return a FlowType indicating whether to continue execution or return early.
from bpatch.patch import Flow, FlowType, manager, prefix
def add(x: int, y: int) -> int:
return x + y
@prefix(add)
def before_add(x: int, y: int) -> FlowType[int]:
if x < 0:
return (Flow.RETURN, -1)
return (Flow.CONTINUE,)
manager.apply_all()
assert add(5, 7) == 12
assert add(-5, 7) == -1
Postfix
A postfix hook runs after the target function and can modify its return value.
from bpatch.patch import manager, postfix
def multiply(x: int, y: int) -> int:
return x * y
@postfix(multiply)
def after_multiply(result: int) -> int:
return result * 2
manager.apply_all()
assert multiply(5, 5) == 50
Transform (BytecodePatcher)
The transform decorator and BytecodePatcher allow for fine-grained manipulation of bytecode instructions. You can use matchers to replace, insert, or remove instructions.
from bpatch.dis import Bytecode, Instruction, Opcode
from bpatch.patch import BytecodePatcher, manager, transform
def target_func() -> int:
x = 42
return x
def find_return(_ctx: Bytecode, inst: Instruction) -> bool:
return inst.opcode == Opcode.RETURN_VALUE
nop = Instruction(Opcode.NOP, 0)
patcher = BytecodePatcher().replace(find_return, nop)
@transform(target_func, patcher=patcher)
def _patch(_p: BytecodePatcher) -> None:
pass
manager.apply_all()
Inline Labels
You can place inline_label inside a function to mark specific points in the bytecode, making it easier to target with BytecodePatcher using strings instead of custom finder functions.
from bpatch.dis import Instruction, Opcode
from bpatch.patch import BytecodePatcher, inline_label, manager, transform
def labeled_func() -> int:
inline_label("my_label")
return 1
nop = Instruction(Opcode.NOP, 0)
patcher = BytecodePatcher().replace("my_label", nop)
@transform(labeled_func, patcher=patcher)
def _patch(_p: BytecodePatcher) -> None:
pass
manager.apply_all()
Executor
Executor allows you to trace and step through the bytecode execution of a function. The underlying implementation automatically selects the optimal backend for your Python version (MonitoringExecutor for 3.12+ or TraceExecutor for older versions).
You can use the executor to manually step through execution or yield instructions as they are executed.
from bpatch.dis import Bytecode
from bpatch.executor import Executor
def simple_func() -> int:
x = 100
return x
bc = Bytecode.from_callable(simple_func)
# Method 1: Iterating over instructions as they execute
for inst in bc.exec():
print(inst.opcode)
# Method 2: Manual stepping
exec_instance = Executor(bc)
while exec_instance.step():
if exec_instance.current_instruction:
print(exec_instance.current_instruction.opcode)