Autospec mock does not correctly preserve signature?

Viewed 528

According to the mock guide:

Auto-speccing creates mock objects that have the same attributes and methods as the objects they are replacing, and any functions and methods (including constructors) have the same call signature as the real object.

But that doesn't seem to be true. Stdlib inspect still sees a generic *args, **kwargs signature on the mock:

>>> from unittest.mock import patch
>>> def foo(arg1, arg2, arg3):
...     pass
...     
>>> print(*inspect.signature(foo).parameters)
arg1 arg2 arg3
>>> with patch("__main__.foo", autospec=True) as mock:
...     print(*inspect.signature(mock).parameters)
...     
args kwargs

The autospec does work, in that mock(1,2,3,4) will correctly raise TypeError: too many positional arguments, but it seems this is implemented via some code deeper in the call stack. It's not done via the call signature.

In code where you actually rely on the signature itself (and need the correct signature preserved when mocking in tests), how to autospec a mock in a way that correctly preserves signature?

1 Answers

This was actually considered a bug in Python, and has been fixed in Python 3.8. A patch was also backported to Python 3.7.3. The relevant issue and pull request:

I had a similar issue, testing some code that inspects the signature of a callable being mocked. I solved it by setting the __signature__ attribute of the mock to the original one:

from inspect import signature
from unittest.mock import patch

def foo(a, b, c):
    pass

foo_sig = signature(foo)  # Need to cache the signature before mocking foo

with patch("__main__.foo") as mock:
    mock.__signature__ = foo_sig
    print(signature(mock))  # Prints '(a, b, c)'

This works because the signature() function follows the __signature__ attribute before trying other methods, as stated in PEP 362:

If the object has a __signature__ attribute and if it is not None - return it

Unfortunately this is not mentioned in the documentation for the signature() function or the inspect module.

Related