Skip to content

Commit 3cf97b9

Browse files
Add documentation page for monkey patching features.
Introduces docs/monkey.rst covering wrap_function_wrapper, patch_function_wrapper, function_wrapper, wrap_object, wrap_object_attribute, resolve_path, apply_patch, the module? deferred form, post import hooks (register_post_import_hook, when_imported, discover_post_import_hooks), transient_function_wrapper, and the main pitfalls. Adds the new page to the documentation toctree and appends a Scoped Test Patches example to docs/examples.rst showing how transient_function_wrapper is used in tests.
1 parent cbe863f commit 3cf97b9

3 files changed

Lines changed: 479 additions & 0 deletions

File tree

docs/examples.rst

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,3 +673,87 @@ where ``vars()`` returns a read-only ``mappingproxy``, along with a dual
673673
role as both decorator and context manager, and transparent support for
674674
``async def`` functions and ``async with`` blocks backed by an
675675
``asyncio.Lock``. See :doc:`bundled` for the full description.
676+
677+
Scoped Test Patches
678+
-------------------
679+
680+
``@wrapt.transient_function_wrapper`` installs a monkey patch for the
681+
duration of a single call and removes it afterwards, which makes it a
682+
convenient building block for tests that need to observe or override a
683+
collaborator without leaking that change into neighbouring tests. The
684+
introductory API description is covered in :doc:`monkey`; this section
685+
shows how it composes with the rest of a test's fixtures.
686+
687+
The example below exercises a small function that uses the standard
688+
library ``tempfile`` module to create a temporary directory. The test
689+
wants to verify that the function asks for a specific ``prefix``, without
690+
actually creating a directory on disk.
691+
692+
::
693+
694+
import tempfile
695+
import wrapt
696+
697+
def build_workspace():
698+
return tempfile.mkdtemp(prefix="workspace-")
699+
700+
def test_build_workspace_uses_prefix():
701+
seen = []
702+
703+
@wrapt.transient_function_wrapper("tempfile", "mkdtemp")
704+
def capture(wrapped, instance, args, kwargs):
705+
seen.append((args, kwargs))
706+
return "/fake/path"
707+
708+
@capture
709+
def run():
710+
return build_workspace()
711+
712+
assert run() == "/fake/path"
713+
assert seen == [((), {"prefix": "workspace-"})]
714+
715+
Two things are happening here. The outer ``@wrapt.transient_function_wrapper``
716+
declaration *describes* the patch: it says "when the wrapped function runs,
717+
replace ``tempfile.mkdtemp`` with ``capture``". No patch is in effect at this
718+
point; ``capture`` is a decorator that has not yet been applied to anything.
719+
The inner ``@capture`` then applies that decorator to ``run``, so ``run``
720+
becomes the scope within which the patch is active. Calling ``run()``
721+
installs the patch, executes ``build_workspace()``, and uninstalls the patch
722+
before returning, regardless of whether ``build_workspace`` returned
723+
normally or raised.
724+
725+
This pattern composes naturally with test functions themselves. The wrapper
726+
can be applied directly as a decorator on the test, in which case the patch
727+
is in force for the duration of that test.
728+
729+
::
730+
731+
@wrapt.transient_function_wrapper("tempfile", "mkdtemp")
732+
def stub_mkdtemp(wrapped, instance, args, kwargs):
733+
return "/fake/path"
734+
735+
@stub_mkdtemp
736+
def test_build_workspace_returns_path():
737+
assert build_workspace() == "/fake/path"
738+
739+
Unlike a fixture that installs and tears down a patch at the module level,
740+
the patch applied by ``transient_function_wrapper`` cannot outlive the
741+
decorated call. There is no per-test cleanup step to forget, and no risk
742+
that a failing test leaves an earlier test's patch in place.
743+
744+
Because the wrapper function receives ``wrapped`` as the original, still
745+
usable attribute, a test is free to call it from inside the wrapper when
746+
it wants to record an interaction without changing behaviour.
747+
748+
::
749+
750+
@wrapt.transient_function_wrapper("tempfile", "mkdtemp")
751+
def record_mkdtemp(wrapped, instance, args, kwargs):
752+
result = wrapped(*args, **kwargs)
753+
created.append(result)
754+
return result
755+
756+
Here the real ``mkdtemp`` still runs, so the test can assert on the return
757+
value the production code saw while also recording it for inspection. This
758+
is often enough to replace a bespoke stub object in tests that are really
759+
asking "was the right collaborator called with the right arguments".

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Documentation
5151
quick-start
5252
decorators
5353
wrappers
54+
monkey
5455
typing
5556
bundled
5657
examples

0 commit comments

Comments
 (0)