For the benefit of others, the problem is that 
`unittest.mock.call.__wrapped__` generates a new object, which in turn 
has a dynamic `__wrapped__` attribute, which does the same, thus 
generating an infinite chain of *distinct* proxies.

Being distinct proxy objects defeats the loop detection algorithm of 
`inspect.unwrap`, which raises ValueError when the recursion limit is 
reached, and that breaks doctest.

(I am explicitly stating that here because I spent an embarassingly long 
time misunderstanding the nature of the bug, then more time digging into 
the PR and bug track issues to understand what it was, rather than what 
it isn't. Maybe I can save anyone else from my misunderstanding.)

I'm not convinced that this should be fixed by catching the ValueError 
inside doctest. Or at least, not *just* by doing so. There's a deeper 
problem that should be fixed, outside of doctest. Looking at the 
similar issue here:

https://bugs.python.org/issue25532

`mock.call` has broken other functions in the past, and will probably 
continue to do so in the future.

I don't think this infinite chain is intentional, I think it just 
happens by accident, which makes this a bug in `call`. I think.

Michael Foord (creator of mock, if I recall correctly) suggested 
blacklisting `__wrapped__` from the proxying:

https://bugs.python.org/issue25532#msg254726

which I think is the right solution, rather than touching doctest.

Michael also said he wasn't happy with an arbitrary limit on the depth 
of proxies, but I would say that limiting the depth to 
sys.getrecursionlimit() is not arbitrary and should avoid or at least 
mitigate the risk of infinite loops and/or memory exhaustion in the 
general case of arbitrary attribute lookups:


    py> a = unittest.mock.call
    py> for i in range(5):  # for arbitrary large values of 5
    ...     a = a.wibble
    ... 
    py> a
    wibble.wibble.wibble.wibble.wibble


I'm not a mock expert, but I guess such mock dynamic lookups should be 
limited to the recursion limit. Currently they will loop forever or 
until you run out of memory.

Setting `call.__wrapped__` to None seems to directly fix the problem 
with doctest:

    [steve@susan test]$ cat demo2.py 
    """
    Run doctest on this module.
    """
    
    from unittest.mock import call
    call.__wrapped__ = None

    [steve@susan test]$ python3.9 -m doctest -v demo2.py
    1 items had no tests:
        demo2
    0 tests in 1 items.
    0 passed and 0 failed.
    Test passed.


but I don't know if that will break any uses of `mock.call`.

Another fix (untested) would be to explicitly test for mocked call:

    inspect.unwrap(val, stop=lambda obj: isinstance(obj, unittest.mock._Call))

but I don't like that much.


-- 
Steve
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/2PBZDDVIPW63NJDVATRJ2Y32RQQC3JWL/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to