https://github.com/foorenxiang/patchymcpatchface
Monkey patching package using only standard Python library
https://github.com/foorenxiang/patchymcpatchface
monkey-patching python testing
Last synced: 11 months ago
JSON representation
Monkey patching package using only standard Python library
- Host: GitHub
- URL: https://github.com/foorenxiang/patchymcpatchface
- Owner: foorenxiang
- Created: 2021-08-04T19:13:31.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2022-01-18T09:00:16.000Z (about 4 years ago)
- Last Synced: 2024-08-09T22:09:49.645Z (over 1 year ago)
- Topics: monkey-patching, python, testing
- Language: Python
- Homepage: https://pypi.org/project/patchymcpatchface/0.1.15/
- Size: 214 KB
- Stars: 1
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Monkey Patching Reliably: PatchyMcPatchFace
## Description
Want to mock objects for unit testing? Want to automate application of your monkey patches? This is the package you are looking for
## Setup
> pip install patchymcpatchface
> import patchymcpatchface as pf
## How to use
There are 2 modes to use this package
1. Directly patch an object with pf.patch_apply
- Useful for mocking in unit tests
2. For normal script execution, using patch hooks that automate patch application
## Mocking for unit tests (Directly patching an object)
### Simple Usage Example
#### Install libraries
> pip install patchymcpatchface
> pip install pytest (not required if not unit testing)
#### Your app file
- `main.py`
```python
def hello_world():
return "Hello World"
def get_text():
return hello_world()
if __name__ == "__main__":
print(get_text())
```
- run file
> python3 main.py
- result
```python
Hello World
```
#### Your test file
- `test_main.py`
```python
import patchymcpatchface as pf
from main import get_text
mock_hello_world = lambda *args, **kwargs: "hi world"
def test_get_text():
pf.patch_apply(
"main.hello_world", mock_hello_world
)
result = get_text()
assert result == "hi world"
```
- run test
> pytest .
- Test should pass because the `hello_world` function has been mocked with `hi world` return value.
### Real World Usage Example
#### Install libraries
> pip install patchymcpatchface pytest requests
#### Your app file
- `main.py`
```python
from requests import request
url = "https://jsonplaceholder.typicode.com/posts"
body_request = {
"title": "foo",
"body": "bar",
"userId": 1,
}
def http_request(method, url, request_body):
response = request(method, url, json=request_body)
return response
if __name__ == "__main__":
print(http_request("POST", url, body_request).json())
```
- run file
> python3 main.py
- result
```python
{'title': 'foo', 'body': 'bar', 'userId': 1, 'id': 101}
```
#### Your test file
- `test_main.py`
```python
import patchymcpatchface as pf
from main import http_request
url = "https://jsonplaceholder.typicode.com/posts"
body_request = {
"title": "foo",
"body": "bar",
"userId": 1,
}
def mock_request(*args, **kwargs):
mock = type("mock_request", (), {})()
mock.status_code = 201
mock.json = lambda: {
"title": "foo",
"body": "bar",
"userId": 1,
"id": 123,
}
return mock
def test_http_request():
pf.patch_apply(
"main.request", mock_request
)
response = http_request("POST", url, body_request)
assert response.status_code == mock_request().status_code
assert response.json() == mock_request().json()
```
- run test
> pytest .
- Test should pass because the `request` function from the `requests` library has been mocked with `{'title': 'foo', 'body': 'bar', 'userId': 1, 'id': 123}` return value.
## Automating patch application with patch hooks
### Simple Usage Example
#### Install library
> pip install patchymcpatchface
#### Your app file
- `main.py`
```python
import patchymcpatchface as pf
def hello_world():
return "Hello World"
def foo_bar():
return "foo bar"
if __name__ == "__main__":
pf.invoke_patch_hooks(PATCH_MODULES)
print(hello_world())
print(foo_bar())
```
#### Your monkey patch files
- `hello_world_patch.py`
```python
import patchymcpatchface as pf
patched_hello_world = lambda *args, **kwargs: "hi world"
def patch_hook():
pf.patch_apply(
"main.hello_world", patched_hello_world
)
print("Applied hello world patch")
```
`patch_hook` is a reserved function name to be placed at the module global level
pf will look for this function and invoke it
#### Your patch manifest file
- `patch_manifest.py` (placed at project root)
```python
import hello_world_patch
PATCH_MODULES = [
hello_world_patch,
]
```
`patch_manifest.py` contains the list of patches that pf will apply
- run main
> python3 main.py
- result
```python
Applied hello world patch
hi world
```
### Real World Usage Example
#### Your monkey patch files
- `hello_world_patch.py`
```python
import patchymcpatchface as pf
patched_hello_world = lambda *args, **kwargs: "hi world"
def patch_hook():
pf.patch_apply(
"main.hello_world", patched_hello_world
)
print("Applied hello world patch")
```
- `foo_bar_patch.py`
```python
import patchymcpatchface as pf
patched_foo_bar = lambda *args, **kwargs: "bar foo"
def patch_hook():
pf.patch_apply(
"main.foo_bar", patched_foo_bar
)
print("Applied foo bar patch")
```
`patch_hook` is a reserved function name to be placed at the module global level
pf will look for this function and invoke it
#### Your patch manifest file
- `patch_manifest.py` (placed in hello_package)
```python
import hello_world_patch
from typing import List
from types import ModuleType
PATCH_MODULES: List[ModuleType] = [
hello_world_patch,
# you can list other modules containing monkey patches and patch_hook here
]
```
- `patch_manifest.py` (placed in foo_package)
```python
import foo_bar_patch
from typing import List
from types import ModuleType
PATCH_MODULES: List[ModuleType] = [
foo_bar_patch,
# you can list other modules containing monkey patches and patch_hook here
]
```
`patch_manifest.py` contains the list of patches that pf will apply
_Invoking automatic patching_
Use `pf.invoke_patch_hooks` to register and invoke the patches. See below for example:
#### Your app file
- `main.py`
```python
import patchymcpatchface as pf
from hello_package.patch_manifest_hello import PATCH_MODULES_HELLO
from foo_package.patch_manifest_foo import PATCH_MODULES as PATCH_MODULES_FOO
def hello_world():
return "Hello World"
def foo_bar():
return "foo bar"
if __name__ == "__main__":
# apply patches at start of program
pf.invoke_patch_hooks(PATCH_MODULES_HELLO)
# run the patched function registered by PATCH_MODULES
print(hello_world())
# call the original function
print(foo_bar())
# delayed patch invocation for foo_bar
pf.invoke_patch_hooks(PATCH_MODULES_FOO)
# run the patched function registered by PATCH_MODULES_FOO
print(foo_bar())
```
- run main
> python3 main.py
- result
```python
Applied hello world patch
hi world
foo bar
Applied foo bar patch
bar foo
```
## How this works
To quote real python:
```text
The details of the Python import system are described in the official documentation. At a high level, three things happen when you import a module (or package). The module is:
- Searched for
- Loaded
- Bound to a namespace
For the usual imports—those done with the import statement—all three steps happen automatically. When you use importlib, however, only the first two steps are automatic. You need to bind the module to a variable or namespace yourself.
```
After importing package_to_be_patched.foo ___module___ in the patch module, the imported module will be loaded and bounded to the global namespace with the following keys. Yes, multiple keys for a single ___module___ import!
Afterwards, the patch is robust against how the other modules import this function!
```python
filter_sys_modules("package_to_be_patched"): {'package_to_be_patched': ,
'package_to_be_patched.foo': }
```
Testing various methods of importing the target function to be patched in module foo yields a consistent result:
```text
__main__
Running target_function_direct()
I'm the patched function
__main__
Running package_to_be_patched.foo.target_function()
I'm the patched function
running_package.foo
from package_to_be_patched.foo import target_function
Running target_function()
I'm the patched function
running_package.bar
import package_to_be_patched
Running package_to_be_patched.foo.target_function()
I'm the patched function
running_package.baz
import package_to_be_patched.foo
Running package_to_be_patched.foo.target_function()
I'm the patched function
running_package.foobar
from package_to_be_patched.foo import *
Running target_function()
I'm the patched function
running_package.bazbar
import package_to_be_patched.foo
Running package_to_be_patched.foo.target_function()
I'm the other patched function
```