@@ -673,3 +673,87 @@ where ``vars()`` returns a read-only ``mappingproxy``, along with a dual
673673role 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".
0 commit comments