简体   繁体   中英

How to perform assert_has_calls for a __getitem__() call?

Code:

from unittest.mock import MagicMock, call

mm = MagicMock()
mm().foo()['bar']

print(mm.mock_calls)
print()

mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])

Output:

[call(), call().foo(), call().foo().__getitem__('bar')]

Traceback (most recent call last):
  File "foo.py", line 9, in <module>
    mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])
TypeError: tuple indices must be integers or slices, not str

How to fix this assert?

This is a bug, since you should always be able to use the repr output of a call object to re-create a new call object of the same value.

The problem here is that call , an instance of unittest.mock._Call , relies on the __getattr__ method to implement its chained call annotation magic, where another _Call object is returned when a non-existent attribute name is given. But since _Call is a subclass of tuple , which does define the __getitem__ attribue, the _Call.__getattr__ method would simply return tuple.__getitem__ instead of a _Call object when the attribute __getitem__ is asked for. Since tuple.__getitem__ does not accept a string as a parameter, you get the said error as a result.

To fix this, since the determination of whether or not an attribute is defined is done via the call of the __getattribute__ method, which raises AttributeError when a given attribute name is not found, we can override _Call.__getattribute__ so that it would raise such an exception when the given attribute name is '__getitem__' , to effectively make __getitem__ "non-existent" and pass on its resolution to the __getattr__ method, which would then return a _Call object just like it would for any other non-existent attribute:

def __getattribute__(self, attr):
    if attr == '__getitem__':
        raise AttributeError
    return tuple.__getattribute__(self, attr)

call.__class__.__getattribute__ = __getattribute__ # call.__class__ is _Call

so that:

mm = MagicMock()
mm().foo()['bar']
mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('bar')])

would raise no exception, while:

mm.assert_has_calls([call(), call().foo(), call().foo().__getitem__('foo')])

would raise:

AssertionError: Calls not found.
Expected: [call(), call().foo(), call().foo().__getitem__('foo')]
Actual: [call(), call().foo(), call().foo().__getitem__('bar')]

Demo: https://repl.it/repls/StrikingRedundantAngle

Note that I have filed the bug at the Python bug tracker and submitted my fix as a pull request to CPython, so hopefully you will no longer have to do the above in the near future.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM