{"id":26251362,"url":"https://github.com/foorenxiang/patchymcpatchface","last_synced_at":"2025-03-13T16:52:27.016Z","repository":{"id":48081565,"uuid":"392801436","full_name":"foorenxiang/patchymcpatchface","owner":"foorenxiang","description":"Monkey patching package using only standard Python library","archived":false,"fork":false,"pushed_at":"2022-01-18T09:00:16.000Z","size":219,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-08-09T22:09:49.645Z","etag":null,"topics":["monkey-patching","python","testing"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/patchymcpatchface/0.1.15/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/foorenxiang.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-08-04T19:13:31.000Z","updated_at":"2021-08-12T16:59:33.000Z","dependencies_parsed_at":"2022-08-12T18:10:47.581Z","dependency_job_id":null,"html_url":"https://github.com/foorenxiang/patchymcpatchface","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foorenxiang%2Fpatchymcpatchface","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foorenxiang%2Fpatchymcpatchface/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foorenxiang%2Fpatchymcpatchface/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/foorenxiang%2Fpatchymcpatchface/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/foorenxiang","download_url":"https://codeload.github.com/foorenxiang/patchymcpatchface/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243447540,"owners_count":20292449,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["monkey-patching","python","testing"],"created_at":"2025-03-13T16:52:26.436Z","updated_at":"2025-03-13T16:52:27.009Z","avatar_url":"https://github.com/foorenxiang.png","language":"Python","readme":"# Monkey Patching Reliably: PatchyMcPatchFace\n\n## Description\n\nWant to mock objects for unit testing? Want to automate application of your monkey patches? This is the package you are looking for\n\n## Setup\n\n\u003e pip install patchymcpatchface  \n\u003e import patchymcpatchface as pf\n\n## How to use\n\nThere are 2 modes to use this package\n\n1. Directly patch an object with pf.patch_apply\n   - Useful for mocking in unit tests\n2. For normal script execution, using patch hooks that automate patch application\n\n## Mocking for unit tests (Directly patching an object)\n\n### Simple Usage Example\n\n#### Install libraries\n\n\u003e pip install patchymcpatchface  \n\u003e pip install pytest (not required if not unit testing)\n\n#### Your app file\n\n- `main.py`\n\n  ```python\n  def hello_world():\n      return \"Hello World\"\n\n\n  def get_text():\n      return hello_world()\n\n\n  if __name__ == \"__main__\":\n      print(get_text())\n  ```\n\n- run file\n  \u003e python3 main.py\n  - result\n\n    ```python\n    Hello World\n    ```\n\n#### Your test file\n\n- `test_main.py`\n\n  ```python\n  import patchymcpatchface as pf\n  from main import get_text\n\n\n  mock_hello_world = lambda *args, **kwargs: \"hi world\"\n\n\n  def test_get_text():\n      pf.patch_apply(\n          \"main.hello_world\", mock_hello_world\n      )\n\n      result = get_text()\n      assert result == \"hi world\"\n  ```\n\n- run test\n  \u003e pytest .\n  - Test should pass because the `hello_world` function has been mocked with `hi world` return value.\n\n### Real World Usage Example\n\n#### Install libraries\n\n\u003e pip install patchymcpatchface pytest requests\n\n#### Your app file\n\n- `main.py`\n\n  ```python\n  from requests import request\n\n\n  url = \"https://jsonplaceholder.typicode.com/posts\"\n  body_request = {\n      \"title\": \"foo\",\n      \"body\": \"bar\",\n      \"userId\": 1,\n  }\n\n\n  def http_request(method, url, request_body):\n      response = request(method, url, json=request_body)\n      return response\n\n\n  if __name__ == \"__main__\":\n      print(http_request(\"POST\", url, body_request).json())\n  ```\n\n- run file\n  \u003e python3 main.py\n  - result\n\n    ```python\n    {'title': 'foo', 'body': 'bar', 'userId': 1, 'id': 101}\n    ```\n\n#### Your test file\n\n- `test_main.py`\n\n  ```python\n  import patchymcpatchface as pf\n  from main import http_request\n\n  url = \"https://jsonplaceholder.typicode.com/posts\"\n  body_request = {\n      \"title\": \"foo\",\n      \"body\": \"bar\",\n      \"userId\": 1,\n  }\n\n\n  def mock_request(*args, **kwargs):\n      mock = type(\"mock_request\", (), {})()\n      mock.status_code = 201\n      mock.json = lambda: {\n          \"title\": \"foo\",\n          \"body\": \"bar\",\n          \"userId\": 1,\n          \"id\": 123,\n      }\n      return mock\n\n\n  def test_http_request():\n      pf.patch_apply(\n          \"main.request\", mock_request\n      )\n\n      response = http_request(\"POST\", url, body_request)\n      assert response.status_code == mock_request().status_code\n      assert response.json() == mock_request().json()\n  ```\n\n- run test\n  \u003e pytest .\n  - 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.\n\n## Automating patch application with patch hooks\n\n### Simple Usage Example\n\n#### Install library\n\n\u003e pip install patchymcpatchface\n\n#### Your app file\n\n- `main.py`\n\n  ```python\n  import patchymcpatchface as pf\n\n  def hello_world():\n      return \"Hello World\"\n  \n  def foo_bar():\n      return \"foo bar\"\n\n\n  if __name__ == \"__main__\":\n      pf.invoke_patch_hooks(PATCH_MODULES)\n      print(hello_world())\n      print(foo_bar())\n  ```\n\n#### Your monkey patch files\n\n- `hello_world_patch.py`\n\n  ```python\n  import patchymcpatchface as pf\n\n\n  patched_hello_world = lambda *args, **kwargs: \"hi world\"\n\n\n  def patch_hook():\n      pf.patch_apply(\n          \"main.hello_world\", patched_hello_world\n      )\n      print(\"Applied hello world patch\")\n  ```\n\n`patch_hook` is a reserved function name to be placed at the module global level  \npf will look for this function and invoke it  \n\n#### Your patch manifest file\n\n- `patch_manifest.py` (placed at project root)\n\n  ```python\n  import hello_world_patch\n\n  PATCH_MODULES = [\n    hello_world_patch,\n  ]\n  ```\n\n`patch_manifest.py` contains the list of patches that pf will apply\n\n- run main\n  \u003e python3 main.py\n  - result\n\n    ```python\n    Applied hello world patch\n    hi world\n    ```\n\n### Real World Usage Example\n\n#### Your monkey patch files\n\n- `hello_world_patch.py`\n\n  ```python\n  import patchymcpatchface as pf\n\n\n  patched_hello_world = lambda *args, **kwargs: \"hi world\"\n\n\n  def patch_hook():\n      pf.patch_apply(\n          \"main.hello_world\", patched_hello_world\n      )\n      print(\"Applied hello world patch\")\n  ```\n\n- `foo_bar_patch.py`\n\n  ```python\n  import patchymcpatchface as pf\n\n\n  patched_foo_bar = lambda *args, **kwargs: \"bar foo\"\n\n\n  def patch_hook():\n      pf.patch_apply(\n          \"main.foo_bar\", patched_foo_bar\n      )\n      print(\"Applied foo bar patch\")\n  ```\n\n`patch_hook` is a reserved function name to be placed at the module global level  \npf will look for this function and invoke it  \n#### Your patch manifest file\n\n- `patch_manifest.py` (placed in hello_package)\n\n  ```python\n  import hello_world_patch\n  from typing import List\n  from types import ModuleType\n\n  PATCH_MODULES: List[ModuleType] = [\n    hello_world_patch,\n    # you can list other modules containing monkey patches and patch_hook here\n  ]\n  ```\n\n- `patch_manifest.py` (placed in foo_package)\n\n  ```python\n  import foo_bar_patch\n  from typing import List\n  from types import ModuleType\n\n  PATCH_MODULES: List[ModuleType] = [\n    foo_bar_patch,\n    # you can list other modules containing monkey patches and patch_hook here\n  ]\n  ```\n\n`patch_manifest.py` contains the list of patches that pf will apply\n\n_Invoking automatic patching_  \n\nUse `pf.invoke_patch_hooks` to register and invoke the patches. See below for example:\n\n#### Your app file\n\n- `main.py`\n\n  ```python\n  import patchymcpatchface as pf\n  from hello_package.patch_manifest_hello import PATCH_MODULES_HELLO\n  from foo_package.patch_manifest_foo import PATCH_MODULES as PATCH_MODULES_FOO\n\n  def hello_world():\n      return \"Hello World\"\n  \n  def foo_bar():\n      return \"foo bar\"\n\n\n  if __name__ == \"__main__\":\n      # apply patches at start of program\n      pf.invoke_patch_hooks(PATCH_MODULES_HELLO)\n      \n      # run the patched function registered by PATCH_MODULES\n      print(hello_world())\n      \n      # call the original function\n      print(foo_bar()) \n      \n      # delayed patch invocation for foo_bar\n      pf.invoke_patch_hooks(PATCH_MODULES_FOO)\n      \n      # run the patched function registered by PATCH_MODULES_FOO\n      print(foo_bar())\n  ```\n\n- run main\n  \u003e python3 main.py\n  - result\n\n    ```python\n    Applied hello world patch\n    hi world\n    foo bar\n    Applied foo bar patch\n    bar foo\n    ```\n\n## How this works\n\n\u003chttps://realpython.com/python-import/#import-internals\u003e\n\nTo quote real python:\n\n```text\nThe 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:  \n\n- Searched for\n- Loaded\n- Bound to a namespace\n\nFor 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.  \n```\n\nAfter 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!\n\nAfterwards, the patch is robust against how the other modules import this function!\n\n```python\nfilter_sys_modules(\"package_to_be_patched\"): {'package_to_be_patched': \u003cmodule 'package_to_be_patched' (namespace)\u003e,\n                                      'package_to_be_patched.foo': \u003cmodule 'package_to_be_patched.foo' from '/Users/foorx/Developer/python_patching_experiment/package_to_be_patched/foo.py'\u003e}\n```\n\nTesting various methods of importing the target function to be patched in module foo yields a consistent result:\n\n```text\n__main__\nRunning target_function_direct()\nI'm the patched function\n\n__main__\nRunning package_to_be_patched.foo.target_function()\nI'm the patched function\n\nrunning_package.foo\nfrom package_to_be_patched.foo import target_function\nRunning target_function()\nI'm the patched function\n\nrunning_package.bar\nimport package_to_be_patched\nRunning package_to_be_patched.foo.target_function()\nI'm the patched function\n\nrunning_package.baz\nimport package_to_be_patched.foo\nRunning package_to_be_patched.foo.target_function()\nI'm the patched function\n\n\nrunning_package.foobar\nfrom package_to_be_patched.foo import *\nRunning target_function()\nI'm the patched function\n\nrunning_package.bazbar\nimport package_to_be_patched.foo\nRunning package_to_be_patched.foo.target_function()\nI'm the other patched function\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffoorenxiang%2Fpatchymcpatchface","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffoorenxiang%2Fpatchymcpatchface","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffoorenxiang%2Fpatchymcpatchface/lists"}