All Projects → adamchainz → Patchy

adamchainz / Patchy

Licence: mit
⚓️ Patch the inner source of python functions at runtime.

Programming Languages

python
139335 projects - #7 most used programming language
hack
652 projects

Projects that are alternatives of or similar to Patchy

Hack Tools
hack tools
Stars: ✭ 488 (+480.95%)
Mutual labels:  hacks, injection
Dikit
Dependency Injection Framework for Swift, inspired by KOIN.
Stars: ✭ 77 (-8.33%)
Mutual labels:  injection
Minerinthemiddle
Stars: ✭ 46 (-45.24%)
Mutual labels:  injection
Reflexil
The .NET Assembly Editor
Stars: ✭ 1,117 (+1229.76%)
Mutual labels:  injection
Guide 3ds
A complete guide to 3DS custom firmware, from stock to boot9strap.
Stars: ✭ 1,055 (+1155.95%)
Mutual labels:  hacks
Python Patch
Library to parse and apply unified diffs
Stars: ✭ 65 (-22.62%)
Mutual labels:  patch
Old Slack Emojis
Bring back old emojis to new Slack!
Stars: ✭ 39 (-53.57%)
Mutual labels:  patch
Wheelchair
An introduction to the battle between JavaScript cheats and anti cheats.
Stars: ✭ 84 (+0%)
Mutual labels:  injection
Nightpatch
Enable Night Shift on any old Mac models.
Stars: ✭ 72 (-14.29%)
Mutual labels:  patch
React In Patterns Cn
React in patterns 中文版
Stars: ✭ 1,107 (+1217.86%)
Mutual labels:  injection
Dyci Main
Dynamic Code Injection Tool for Objective-C
Stars: ✭ 1,103 (+1213.1%)
Mutual labels:  injection
Pcsgolh
PCSGOLH - Pointless Counter-Strike: Global Offensive Lua Hooks. A open-source Lua API for CS:GO hacking written in modern C++
Stars: ✭ 56 (-33.33%)
Mutual labels:  injection
Revokemsgpatcher
A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了)
Stars: ✭ 12,482 (+14759.52%)
Mutual labels:  patch
Tr2main
Tomb Raider II Injector Dynamic Library
Stars: ✭ 46 (-45.24%)
Mutual labels:  patch
Query.apex
A dynamic SOQL and SOSL query builder on Salesforce.com platform
Stars: ✭ 78 (-7.14%)
Mutual labels:  injection
Minject
Mono Framework Interaction / Injection Library for .NET (C++/CLI)
Stars: ✭ 42 (-50%)
Mutual labels:  injection
Escapefromtarkov Trainer
Escape from Tarkov Trainer
Stars: ✭ 59 (-29.76%)
Mutual labels:  injection
Rad Studio Xe 10.3 Windows
RADStudio XE 10.3.3 Rio - Activation & Documentation
Stars: ✭ 63 (-25%)
Mutual labels:  patch
Unityandroidhotupdate
(Unity3D热更新) provide a way to hot update Unity app on Android, support code&resources, not need lua js or IL runtime etc..., will not disturb your project development; just loading the new version apk file to achieve.
Stars: ✭ 85 (+1.19%)
Mutual labels:  patch
Qqmessageinvoke
👍QQ 防测回插件 🎁
Stars: ✭ 81 (-3.57%)
Mutual labels:  patch

====== Patchy

.. image:: https://img.shields.io/github/workflow/status/adamchainz/patchy/CI/main?style=for-the-badge :target: https://github.com/adamchainz/patchy/actions?workflow=CI

.. image:: https://img.shields.io/coveralls/github/adamchainz/patchy/main?style=for-the-badge :target: https://app.codecov.io/gh/adamchainz/patchy

.. image:: https://img.shields.io/pypi/v/patchy.svg?style=for-the-badge :target: https://pypi.org/project/patchy/

.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge :target: https://github.com/psf/black

.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge :target: https://github.com/pre-commit/pre-commit :alt: pre-commit

.. figure:: https://raw.github.com/adamchainz/patchy/main/pirate.png :alt: A patchy pirate.

..

Patch the inner source of python functions at runtime.

A quick example, making a function that returns 1 instead return 9001:

.. code-block:: python

>>> def sample():
...    return 1
>>> patchy.patch(sample, """\
...     @@ -1,2 +1,2 @@
...      def sample():
...     -    return 1
...     +    return 9001
...     """)
>>> sample()
9001

Patchy works by replacing the code attribute of the function, leaving the function object itself the same. It's thus more versatile than monkey patching, since if the function has been imported in multiple places they'll also call the new behaviour.

Installation

Use pip:

.. code-block:: bash

python -m pip install patchy

Python 3.6 to 3.9 supported.


Hacking on a Django project? Check out my book Speed Up Your Django Tests <https://gumroad.com/l/suydt>__ which covers loads of best practices so you can write faster, more accurate tests.


Why?

If you’re monkey-patching an external library to add or fix some functionality, you will probably forget to check the monkey patch when you upgrade it. By using a patch against its source code, you can specify some context that you expect to remain the same in the function that will be checked before the source is applied.

I found this with some small but important patches to Django for a project. Since it takes a lot of energy to maintain a fork, writing monkey patches was the chosen quick solution, but then writing actual patches would be better.

The patches are applied with the standard patch commandline utility.

Why not?

There are of course a lot of reasons against:

  • It’s (relatively) slow (since it writes the source to disk and calls the patch command)
  • If you have a patch file, why not just fork the library and apply it?
  • At least with monkey-patching you know what end up with, rather than having the changes being done at runtime to source that may have changed.

All are valid arguments. However once in a while this might be the right solution.

How?

The standard library function inspect.getsource() is used to retrieve the source code of the function, the patch is applied with the commandline utility patch, the code is recompiled, and the function’s code object is replaced the new one. Because nothing tends to poke around at code objects apart from dodgy hacks like this, you don’t need to worry about chasing any references that may exist to the function, unlike mock.patch.

A little special treatment is given to instancemethod, classmethod, and staticmethod objects to make sure the underlying function is what gets patched and that you don't have to worry about the details.

API

patch(func, patch_text)

Apply the patch patch_text to the source of function func. func may be either a function, or a string providing the dotted path to import a function.

If the patch is invalid, for example the context lines don’t match, ValueError will be raised, with a message that includes all the output from the patch utility.

Note that patch_text will be textwrap.dedent()’ed, but leading whitespace will not be removed. Therefore the correct way to include the patch is with a triple-quoted string with a backslash - """\ - which starts the string and avoids including the first newline. A final newline is not required and will be automatically added if not present.

Example:

.. code-block:: python

import patchy

def sample():
    return 1

patchy.patch(sample, """\
    @@ -2,2 +2,2 @@
    -    return 1
    +    return 2""")

print(sample())  # prints 2

mc_patchface(func, patch_text)

An alias for patch, so you can meme it up by calling patchy.mc_patchface().

unpatch(func, patch_text)

Unapply the patch patch_text from the source of function func. This is the reverse of patch()\ing it, and calls patch --reverse.

The same error and formatting rules apply as in patch().

Example:

.. code-block:: python

import patchy

def sample():
    return 2

patchy.unpatch(sample, """\
    @@ -2,2 +2,2 @@
    -    return 1
    +    return 2""")

print(sample())  # prints 1

temp_patch(func, patch_text)

Takes the same arguments as patch. Usable as a context manager or function decorator to wrap code with a call to patch before and unpatch after.

Context manager example:

.. code-block:: python

def sample():
    return 1234

patch_text = """\
    @@ -1,2 +1,2 @@
     def sample():
    -    return 1234
    +    return 5678
    """

with patchy.temp_patch(sample, patch_text):
    print(sample())  # prints 5678

Decorator example, using the same sample and patch_text:

.. code-block:: python

@patchy.temp_patch(sample, patch_text)
def my_func():
    return sample() == 5678

print(my_func())  # prints True

replace(func, expected_source, new_source)

Check that function or dotted path to function func has an AST matching `expected_source, then replace its inner code object with source compiled fromnew_source``. If the AST check fails, ``ValueError`` will be raised with current/expected source code in the message. In the author's opinion it's preferable to call ``patch()`` so your call makes it clear to see what is being changed about ``func``, but using ``replace()`` is simpler as you don't have to make a patch and there is no subprocess call to the ``patch`` utility.

Note both expected_source and new_source will be textwrap.dedent()’ed, so the best way to include their source is with a triple quoted string with a backslash escape on the first line, as per the example below.

If you want, you can pass expected_source=None to avoid the guard against your target changing, but this is highly unrecommended as it means if the original function changes, the call to replace() will continue to silently succeed.

Example:

.. code-block:: python

import patchy

def sample():
    return 1

patchy.replace(
    sample,
    """\
    def sample():
        return 1
    """,
    """\
    def sample():
        return 42
    """
)

print(sample())  # prints 42

How to Create a Patch

  1. Save the source of the function of interest (and nothing else) in a .py file, e.g. before.py:

    .. code-block:: python

    def foo():
        print("Change me")
    

    Make sure you dedent it so there is no whitespace before the def, i.e. d is the first character in the file. For example if you wanted to patch the bar() method below:

    .. code-block:: python

    class Foo():
        def bar(self, x):
            return x * 2
    

    ...you would put just the method in a file like so:

    .. code-block:: python

    def bar(self, x):
        return x * 2
    

    However we'll continue with the first example before.py since it's simpler.

  2. Copy that .py file, to e.g. after.py, and make the changes you want, such as:

    .. code-block:: python

    def foo():
        print("Changed")
    
  3. Run diff, e.g. diff -u before.py after.py. You will get output like:

    .. code-block:: diff

    diff --git a/Users/chainz/tmp/before.py b/Users/chainz/tmp/after.py index e6b32c6..31fe8d9 100644 --- a/Users/chainz/tmp/before.py +++ b/Users/chainz/tmp/after.py @@ -1,2 +1,2 @@ def foo():

    • print("Change me")
    • print("Changed")
  4. The filenames are not necessary for patchy to work. Take only from the first @@ line onwards into the multiline string you pass to patchy.patch():

    .. code-block:: python

    patchy.patch(foo, """
    @@ -1,2 +1,2 @@ def foo(): - print("Change me") + print("Changed") """)

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].