pythonpython-unittestpatchmonkeypatchingpython-unittest.mock

Why does unittest's `mock.patch.start` re-run the function in which the patcher is started?


Let's say we have two files:

to_patch.py

from unittest.mock import patch

def patch_a_function():
    print("Patching!")
    patcher = patch("to_be_patched.function")
    patcher.start()
    print("Done patching!")

to_be_patched.py

from to_patch import patch_a_function

def function():
    pass

patch_a_function()
function()

And we run python -m to_be_patched. This will output:

Patching!
Patching!
  1. Why isn't Done patching! ever printed?
  2. Why is Patching! printed twice?

I've narrowed the answer to (2) down; the call to patch.start seems to trigger patch_a_function again. I suspect this is because it's imported in to_be_patched.py, but am not sure why the function itself would run for a second time. Similarly, I'm not sure why the Done patching! line is not reached in either of the calls to patch_a_function. patcher.start() can't be blocking, because the program exits nicely instead of hanging there... right?

Edit: Huh. It looks like no one can reproduce Done patching! not being printed (which was honestly the main difficulty)—so I guess that's just a my-side problem


Solution

    1. Why isn't Done patching! ever printed?

    Can not reproduce.

    $ python -m to_be_patched
    Patching!
    Patching!
    Done patching!
    Done patching!
    
    1. Why is Patching! printed twice?

    Your module gets imported twice. If you add print(__name__) into the file to_be_patched.py it will be clear:

    from to_patch import patch_a_function
    
    print(f"{__name__=}")
    
    def function():
        pass
    
    patch_a_function()
    function()  # note: this line doesn't actually do anything, and could be commented out
    

    Result:

    $ python -m to_be_patched
    __name__='__main__'
    Patching!
    __name__='to_be_patched'
    Patching!
    Done patching!
    Done patching!
    

    When you use python -m to_be_patched your module to_be_patched will be loaded as top-level code, i.e. the module __name__ will be "__main__".

    When mock.patch is used, mock will first import the patch target. When given a patch target as a string like "to_be_patched.function" mock will use importlib, via pkgutil.resolve_name, to find the correct namespace in which to patch. This method loads the target module with __name__ as "to_be_patched", it's not the top-level code environment. Although it's the same underlying .py file being loaded, there is a cache miss in sys.modules, because of the name mismatch: "__main__" != "to_be_patched".

    The function patch_a_function now has dual identities and exists in the module __main__ as well as the module to_be_patched, so what you're seeing is each one getting called. The first call triggers the second call, by the double-import mechanism described.