Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 563
Lone Learner Avatar asked Aug 24 '19 09:08

Lone Learner


1 Answers

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.

like image 175
blhsing Avatar answered Nov 01 '22 13:11

blhsing