Package: patchymcpatchface
Monkey Patching Reliably: PatchyMcPatchFaceDescriptionWant to mock objects for unit testing? Want to automate application of your monkey patches? This is the package you are looking forSetuppip install patchymcpatchfaceimport patchymcpatchface as pfHow to useThere are 2 modes to use this packageDirectly patch an object with pf.patch_applyUseful for mocking in unit testsFor normal script execution, using patch hooks that automate patch applicationMocking for unit tests (Directly patching an object)Simple Usage ExampleInstall librariespip install patchymcpatchfacepip install pytest (not required if not unit testing)Your app filemain.pydefhello_world():return"Hello World"defget_text():returnhello_world()if__name__=="__main__":print(get_text())run filepython3 main.pyresultHelloWorldYour test filetest_main.pyimportpatchymcpatchfaceaspffrommainimportget_textmock_hello_world=lambda*args,**kwargs:"hi world"deftest_get_text():pf.patch_apply(pf.as_module(relative_path="main.py",patch_object_in_module="hello_world"),mock_hello_world)result=get_text()assertresult=="hi world"run testpytest .Test should pass because thehello_worldfunction has been mocked withhi worldreturn value.Real World Usage ExampleInstall librariespip install patchymcpatchface pytest requestsYour app filemain.pyfromrequestsimportrequesturl="https://jsonplaceholder.typicode.com/posts"body_request={"title":"foo","body":"bar","userId":1,}defhttp_request(method,url,request_body):response=request(method,url,json=request_body)returnresponseif__name__=="__main__":print(http_request("POST",url,body_request).json())run filepython3 main.pyresult{'title':'foo','body':'bar','userId':1,'id':101}Your test filetest_main.pyimportpatchymcpatchfaceaspffrommainimporthttp_requesturl="https://jsonplaceholder.typicode.com/posts"body_request={"title":"foo","body":"bar","userId":1,}defmock_request(*args,**kwargs):mock=type("mock_request",(),{})()mock.status_code=201mock.json=lambda:{"title":"foo","body":"bar","userId":1,"id":123,}returnmockdeftest_http_request():pf.patch_apply(pf.as_module(relative_path="main.py",patch_object_in_module="request"),mock_request)response=http_request("POST",url,body_request)assertresponse.status_code==mock_request().status_codeassertresponse.json()==mock_request().json()run testpytest .Test should pass because therequestfunction from therequestslibrary has been mocked with{'title': 'foo', 'body': 'bar', 'userId': 1, 'id': 123}return value.Automating patch application with patch hooksSimple Usage ExampleInstall librarypip install patchymcpatchfaceYour app filemain.pyimportpatchymcpatchfaceaspfdefhello_world():return"Hello World"deffoo_bar():return"foo bar"if__name__=="__main__":pf.invoke_patch_hooks(PATCH_MODULES)print(hello_world())print(foo_bar())Your monkey patch fileshello_world_patch.pyimportpatchymcpatchfaceaspfpatched_hello_world=lambda*args,**kwargs:"hi world"defpatch_hook():pf.patch_apply(pf.as_module(relative_path="main.py",patch_object_in_module="hello_world"),patched_hello_world)print("Applied hello world patch")patch_hookis a reserved function name to be placed at the module global levelpf will look for this function and invoke itYour patch manifest filepatch_manifest.py(placed at project root)importhello_world_patchPATCH_MODULES=[hello_world_patch,]patch_manifest.pycontains the list of patches that pf will applyrun mainpython3 main.pyresultAppliedhelloworldpatchhiworldReal World Usage ExampleYour monkey patch fileshello_world_patch.pyimportpatchymcpatchfaceaspfpatched_hello_world=lambda*args,**kwargs:"hi world"defpatch_hook():pf.patch_apply(pf.as_module(relative_path="main.py",patch_object_in_module="hello_world"),patched_hello_world)print("Applied hello world patch")foo_bar_patch.pyimportpatchymcpatchfaceaspfpatched_foo_bar=lambda*args,**kwargs:"bar foo"defpatch_hook():pf.patch_apply(pf.as_module(relative_path="main.py",patch_object_in_module="foo_bar"),patched_foo_bar)print("Applied foo bar patch")patch_hookis a reserved function name to be placed at the module global levelpf will look for this function and invoke itYour patch manifest filepatch_manifest.py(placed in hello_package)importhello_world_patchfromtypingimportListfromtypesimportModuleTypePATCH_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)importfoo_bar_patchfromtypingimportListfromtypesimportModuleTypePATCH_MODULES:List[ModuleType]=[foo_bar_patch,# you can list other modules containing monkey patches and patch_hook here]patch_manifest.pycontains the list of patches that pf will applyInvoking automatic patchingUsepf.invoke_patch_hooksto register and invoke the patches. See below for example:Your app filemain.pyimportpatchymcpatchfaceaspffromhello_package.patch_manifest_helloimportPATCH_MODULES_HELLOfromfoo_package.patch_manifest_fooimportPATCH_MODULESasPATCH_MODULES_FOOdefhello_world():return"Hello World"deffoo_bar():return"foo bar"if__name__=="__main__":# apply patches at start of programpf.invoke_patch_hooks(PATCH_MODULES_HELLO)# run the patched function registered by PATCH_MODULESprint(hello_world())# call the original functionprint(foo_bar())# delayed patch invocation for foo_barpf.invoke_patch_hooks(PATCH_MODULES_FOO)# run the patched function registered by PATCH_MODULES_FOOprint(foo_bar())run mainpython3 main.pyresultAppliedhelloworldpatchhiworldfoobarAppliedfoobarpatchbarfooHow this workshttps://realpython.com/python-import/#import-internalsTo quote real python: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.foomodulein the patch module, the imported module will be loaded and bounded to the global namespace with the following keys. Yes, multiple keys for a singlemoduleimport!Afterwards, the patch is robust against how the other modules import this function!filter_sys_modules("package_to_be_patched"):{'package_to_be_patched':<module'package_to_be_patched'(namespace)>,'package_to_be_patched.foo':<module'package_to_be_patched.foo'from'/Users/foorx/Developer/python_patching_experiment/package_to_be_patched/foo.py'>}Testing various methods of importing the target function to be patched in module foo yields a consistent result:__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