Patch functions live
Patch the source of python functions at runtime.
A quick example:
>>> def sample(): ... return 1 >>> patchy.patch(sample, """\ ... @@ -1,2 +1,2 @@ ... def sample(): ... - return 1 ... + return 9001 ... """) >>> sample() 9001
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.
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 you end up with, rather than having a the changes being done at runtime
All are valid arguments. However once in a while this might be the right solution.
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.
replace(func, find, replace, count=None)
Perform a simple find and replace on source of the function func’s source - for when you don’t want to have to write a patch. find and replace should both be strings that will be passed to str.replace.
If count is specified, it will be checked that exactly count occurrences of find exist, and ValueError will be raised if not.
>>> def sample(): ... return "Hi" * 5 ... >>> patchy.replace("Hi", "Hello") >>> patchy.replace("5", "1") >>> sample() "Hello"
Apply the patch patch_text to the source of function func.
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.
>>> def sample(): ... return 1 >>> patchy.patch(sample, """\ ... @@ -2,2 +2,2 @@ ... - return 1 ... + return 2""") >>> sample() 2
How to Create a Patch
Save the source of the function of interest in a .py file, e.g. before.py. Make sure you dedent it so there is no whitespace before the def:
def foo(): print("Change me")
Copy that .py file, to e.g. after.py, and make the changes you want:
def foo(): print("Changed")
Run diff, e.g. diff before.py after.py. You will get output like:
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")
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():
patchy.patch(foo, """\ @@ -1,2 +1,2 @@ def foo(): - print("Change me") + print("Changed") """)
- First release on PyPI.
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
|Filename, size & hash SHA256 hash help||File type||Python version||Upload date|
|patchy-1.0.0-py2.py3-none-any.whl (7.4 kB) Copy SHA256 hash SHA256||Wheel||py2.py3|
|patchy-1.0.0.tar.gz (15.4 kB) Copy SHA256 hash SHA256||Source||None|