classification
Title: mock.create_autospec generates incorrect signature for some decorated methods
Type: behavior Stage:
Components: Library (Lib) Versions: Python 3.7
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: breilly_box, cjw296, lisroach, mariocj89, michael.foord, xtreak
Priority: normal Keywords:

Created on 2019-11-06 22:59 by breilly_box, last changed 2019-11-08 17:57 by breilly_box.

Files
File name Uploaded Description Edit
decorator.py breilly_box, 2019-11-06 22:59 Script demonstrating the bug
Messages (3)
msg356159 - (view) Author: Ben Reilly (breilly_box) Date: 2019-11-06 22:59
mock.create_autospec is meant to create a mock that will ensure, among other things, that method calls on the mock correspond to real methods on the original object and that the arguments match the signature of the original method. However, if the original method is decorated with a method that returns a callable object, the signature check may fail.

Attached is a script that demonstrates the error.

The essential part of the script looks like this:
====
def decorator(m):
    return Wrapper(m)

class Wrapper(object):
    def __init__(self, method):
        self.method = method
        update_wrapper(self, method)

    def __call__(self, instance, *args, **kwargs):
        return self.__get__(instance, type(instance))(*args, **kwargs)

    def __get__(self, instance, owner):
        ... # do the wrapping
====

The `decorator` method returns an instance of the `Wrapper` class, which is callable. Mock will calculate the signature of a method wrapped with `decorator` to be equal to that of `Wrapper.__call__`, namely `(instance, *args, **kwargs)`. Consequently, calls to the mocked method...

1. will incorrectly fail if the method usually takes no arguments, and
2. will incorrectly pass if the method takes at least one argument but too many arguments are provided.

This list of incorrect behaviour is not exhaustive, but hopefully you get the point.

If anyone's concerned about real-life examples, this kind of wrapper is used, for example, in the public Box SDK, as shown here: https://github.com/box/box-python-sdk/blob/b7f41d9a3f8be0ff3622a0c417bf31d2fbbee969/boxsdk/util/api_call_decorator.py#L10
msg356226 - (view) Author: Karthikeyan Singaravelan (xtreak) * (Python triager) Date: 2019-11-08 06:30
If I understand this correctly, when an autospec of a class is created the attributes themselves of the given class are autospecced. During autospec of attributes it's modelled upon __call__'s signature for the given attributes [0]. Here you are wrapping the method's inside a class with Wrapper with a custom __call__ implementation. During actual calls the chain goes through __call__ and executes __get__ where bound method is called. But in mock I am not sure of a way where it can figure out __call__ and then look more into the body to see if there are actual calls being made to be mocked with current implementation just going through signature of __call__.

[0] https://github.com/python/cpython/blob/befa032d8869e0fab4732d910f3887642879d644/Lib/unittest/mock.py#L94
msg356253 - (view) Author: Ben Reilly (breilly_box) Date: 2019-11-08 17:57
Yes, your description sounds right, and I had zero-ed in on the same park of the mock code when I was doing my investigation.

I know that this is a peculiar situation, but one thing to note is that `inspect.signature` gets the signature right on these wrapped methods. You can see this if you were to add in an appropriate spot the following code to the script:

====
from inspect import signature

print(signature(a.with_arg))  # prints `(x)`
print(signature(a.no_arg))    # prints `()`
====

Is there a reason why mock calculates the signature on its own rather than relying on `inspect`?
History
Date User Action Args
2019-11-08 17:57:09breilly_boxsetmessages: + msg356253
2019-11-08 06:30:47xtreaksetmessages: + msg356226
2019-11-07 02:04:10xtreaksetnosy: + cjw296, michael.foord, lisroach, mariocj89, xtreak
components: + Library (Lib), - Tests
2019-11-06 23:03:05breilly_boxsetcomponents: + Tests
2019-11-06 22:59:48breilly_boxcreate