Skip to main content

A Python library for runtime function patching using AST manipulation

Project description

awepatch

Awesome Patch - A Python library for runtime function patching using AST manipulation.

Build Status Python Version License

Overview

awepatch is a powerful Python library that allows you to dynamically patch callable objects at runtime by manipulating their Abstract Syntax Tree (AST). Unlike traditional monkey patching, awepatch modifies the actual code object of functions, providing a cleaner and more maintainable approach to runtime code modification.

Features

  • 🔧 Runtime Function Patching: Modify function behavior without changing source code
  • 🎯 AST-Based Manipulation: Clean and precise code modifications using AST
  • 🔄 Automatic Restoration: Context manager support for temporary patches
  • 🎭 Multiple Patch Modes: Insert code before, after, or replace existing statements
  • 📦 Batch Patching: Apply multiple patches to a function in a single call
  • 🧩 Pattern Matching: Use string or regex patterns to locate code to patch
  • 🔗 Decorator Support: Works with decorated functions, class methods, and static methods
  • Type-Safe: Full type hints support with strict type checking

Installation

pip install awepatch

Or using uv:

uv pip install awepatch

Quick Start

Basic Function Patching

from awepatch import patch_callable, Patch

def greet(name: str) -> str:
    message = f"Hello, {name}!"
    return message

# Temporarily patch the function
with patch_callable(
    greet,
    [Patch('message = f"Hello, {name}!"', 'message = f"Hi there, {name}!"', "replace")]
):
    print(greet("World"))  # Output: Hi there, World!

# Function is automatically restored after context
print(greet("World"))  # Output: Hello, World!

Patch Modes

awepatch supports three different patching modes:

Replace Mode

Replace existing code with new code:

from awepatch import patch_callable, Patch

def calculate(x: int) -> int:
    x = x * 2
    return x

with patch_callable(calculate, [Patch("x = x * 2", "x = x * 3", "replace")]):
    print(calculate(5))  # Output: 15

Before Mode

Insert code before the matched statement:

from awepatch import patch_callable, Patch

def process() -> list[int]:
    items: list[int] = []
    items.append(3)
    return items

with patch_callable(process, [Patch("items.append(3)", "items.append(1)", "before")]):
    print(process())  # Output: [1, 3]

After Mode

Insert code after the matched statement:

from awepatch import patch_callable, Patch

def process() -> list[int]:
    items: list[int] = []
    items.append(3)
    return items

with patch_callable(process, [Patch("items.append(3)", "items.append(5)", "after")]):
    print(process())  # Output: [3, 5]

Patching Methods

Instance Methods

from awepatch import patch_callable, Patch

class Calculator:
    def add(self, x: int, y: int) -> int:
        result = x + y
        return result

calc = Calculator()
with patch_callable(calc.add, [Patch("result = x + y", "result = x + y + 1", "replace")]):
    print(calc.add(2, 3))  # Output: 6

Class Methods

from awepatch import patch_callable, Patch

class MathUtils:
    @classmethod
    def multiply(cls, x: int, y: int) -> int:
        result = x * y
        return result

with patch_callable(MathUtils.multiply, [Patch("result = x * y", "result = x * y * 2", "replace")]):
    print(MathUtils.multiply(3, 4))  # Output: 24

Static Methods

from awepatch import patch_callable, Patch

class Helper:
    @staticmethod
    def format_name(name: str) -> str:
        result = name.upper()
        return result

with patch_callable(Helper.format_name, [Patch("result = name.upper()", "result = name.lower()", "replace")]):
    print(Helper.format_name("HELLO"))  # Output: hello

Pattern Matching

You can use both string literals and regular expressions for pattern matching:

import re
from awepatch import patch_callable, Patch

def process_data(value: int) -> int:
    value = value + 10
    return value

# Using string pattern
with patch_callable(process_data, [Patch("value = value + 10", "value = value + 20", "replace")]):
    print(process_data(5))  # Output: 25

# Using regex pattern
with patch_callable(process_data, [Patch(re.compile(r"value = value \+ \d+"), "value = value + 30", "replace")]):
    print(process_data(5))  # Output: 35

Advanced Usage: AST Statements

For more complex modifications, you can provide AST statements directly:

import ast
from awepatch import patch_callable, Patch

def complex_function(x: int) -> int:
    x = x * 2
    return x

# Parse replacement code as AST
new_statements = ast.parse("x = x * 5").body

with patch_callable(complex_function, [Patch("x = x * 2", new_statements, "replace")]):
    print(complex_function(3))  # Output: 15

Multiple Patches

Apply multiple patches to a function in a single call:

from awepatch import patch_callable, Patch

def calculate(x: int, y: int) -> int:
    x = x + 10
    y = y * 2
    result = x + y
    return result

# Apply multiple patches at once
with patch_callable(
    calculate,
    [
        Patch("x = x + 10", "print('processing x')", "before"),
        Patch("y = y * 2", "y = y * 3", "replace"),
        Patch("result = x + y", "print(f'result: {result}')", "after"),
    ]
):
    print(calculate(5, 10))
    # Output:
    # processing x
    # result: 45
    # 45

# You can also apply multiple patches to the same line (before/after modes)
def process(x: int) -> int:
    x = x + 10
    return x

with patch_callable(
    process,
    [
        Patch("x = x + 10", "print('before')", "before"),
        Patch("x = x + 10", "print('after')", "after"),
    ]
):
    result = process(5)
    # Output:
    # before
    # after

Use Cases

  • Testing: Mock function behavior without complex mocking frameworks
  • Debugging: Inject logging or debugging code at runtime
  • Hot-patching: Apply fixes or modifications without restarting applications
  • Experimentation: Test code changes quickly without modifying source files
  • Instrumentation: Add monitoring or profiling code dynamically

Limitations

  • Lambda functions cannot be patched (they lack proper source code information)
  • Functions must have accessible source code via inspect.getsourcelines()
  • Pattern matching must uniquely identify a single statement in the function
  • Only single function definitions are supported in the AST

API Reference

Patch

class Patch(NamedTuple):
    """A single patch operation.
    
    Attributes:
        pattern: The pattern to search for in the source code
        repl: The replacement code or AST statements
        mode: The mode of patching (before/after/replace), defaults to "before"
    """
    pattern: str | re.Pattern[str]
    repl: str | list[ast.stmt]
    mode: Literal["before", "after", "replace"] = "before"

patch_callable

@contextmanager
def patch_callable(
    func: Callable[..., Any],
    patches: list[Patch],
) -> Iterator[None]:
    """
    Context manager to patch a callable's code object using AST manipulation.

    Args:
        func: The function to patch
        patches: List of Patch objects for applying multiple patches

    Raises:
        TypeError: If func is not callable or is a lambda function
        ValueError: If pattern is not found, matches multiple lines, or patches conflict
    """

Development

Setup Development Environment

# Clone the repository
git clone https://github.com/fanck0605/awepatch.git
cd awepatch

# Install development dependencies
uv sync

Running Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=awepatch --cov-report=html

# Run specific test file
pytest tests/test_patch_callable.py

Code Quality

# Format code
ruff format

# Lint code
ruff check

# Fix auto-fixable issues
ruff check --fix

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Author

Chuck Fan - fanck0605@qq.com

Acknowledgments

  • Inspired by the need for cleaner runtime code modification in Python
  • Built with modern Python tooling and best practices

Note: This library modifies function code objects at runtime. Use with caution in production environments and always test thoroughly.

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

awepatch-0.0.8.tar.gz (47.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

awepatch-0.0.8-py3-none-any.whl (10.8 kB view details)

Uploaded Python 3

File details

Details for the file awepatch-0.0.8.tar.gz.

File metadata

  • Download URL: awepatch-0.0.8.tar.gz
  • Upload date:
  • Size: 47.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for awepatch-0.0.8.tar.gz
Algorithm Hash digest
SHA256 7b1c27ce93bd66bb7a90a0286b9515c8f05c1318c468ea5459a0af178c67d928
MD5 53f7e4cb38d4b45ead4372ebd572d11b
BLAKE2b-256 867206b838c86b613212fe31daba7a1f485f1d3f98cfe2bb58334b7395e1e215

See more details on using hashes here.

Provenance

The following attestation bundles were made for awepatch-0.0.8.tar.gz:

Publisher: build.yml on fanck0605/awepatch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file awepatch-0.0.8-py3-none-any.whl.

File metadata

  • Download URL: awepatch-0.0.8-py3-none-any.whl
  • Upload date:
  • Size: 10.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for awepatch-0.0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 b5af716b27a559bfeb86f32ad2ac25ca601c0a62cd71cc6f6202005b23db748b
MD5 ef4586c7d495468a6ee61f6779ddc23d
BLAKE2b-256 2443829f3e0108e8b3b471273dac59a2c7218d108b76811d9b06e2de0968bfda

See more details on using hashes here.

Provenance

The following attestation bundles were made for awepatch-0.0.8-py3-none-any.whl:

Publisher: build.yml on fanck0605/awepatch

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page