blhsing
blhsing

Reputation: 106618

RecursionError when trying to mock an iterable with an __iter__ method that returns self

For the purpose of a unit test, I made a class whose instance is an iterable that would yield a certain sequence and then raise an exception:

class Iter:
    def __init__(self, seq):
        self.seq = seq
        self.pos = 0

    def __next__(self):
        if self.pos == len(self.seq):
            raise Exception
        value = self.seq[self.pos]
        self.pos += 1
        return value

    def __iter__(self):
        return self

so that:

for value in Iter((1, 2, 3)):
    print(value)

would output:

1
2
3
Traceback (most recent call last):
  File "test.py", line 25, in <module>
    for value in mocked_iterable:
  File "test.py", line 11, in __next__
    raise Exception
Exception

But why reinvent the wheel when MagicMock already has a side_effect attribute that should do the same? Per the documentation, the side_effect attribute can be an iterable that yields either a value to be returned from the call to the mock, or an exception to raise, so it suits the purpose of mimicking the aforementioned class perfectly. I therefore created a MagicMock object and made its __iter__ method return the object itself, and made its __next__ method to have a side effect of the desired sequence and the exception:

from unittest.mock import MagicMock
mocked_iterable = MagicMock()
mocked_iterable.__iter__.return_value = mocked_iterable
mocked_iterable.__next__.side_effect = [1, 2, 3, Exception]
for value in mocked_iterable:
    print(value)

However, this outputs:

...
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1005, in _mock_call
    ret_val = effect(*args, **kwargs)
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1793, in __iter__
    return iter(ret_val)
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 939, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 944, in _mock_call
    self.called = True
RecursionError: maximum recursion depth exceeded

But the question is, why is there any recursion?

I found that I can work around this "bug" by putting the self reference in __iter__'s side_effect attribute instead:

mocked_iterable = MagicMock()
mocked_iterable.__iter__.side_effect = [mocked_iterable]
mocked_iterable.__next__.side_effect = [1, 2, 3, Exception]
for value in mocked_iterable:
    print(value)

This correctly outputs:

1
2
3
Traceback (most recent call last):
  File "test.py", line 6, in <module>
    for value in mocked_iterable:
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 939, in __call__
    return _mock_self._mock_call(*args, **kwargs)
  File "C:\Program Files (x86)\Python36-32\lib\unittest\mock.py", line 1000, in _mock_call
    raise result
Exception

But is the recursion error indeed a bug, or a feature of mock with an unintended consequence?

Upvotes: 3

Views: 875

Answers (1)

Sraw
Sraw

Reputation: 20224

I agree that this is indeed a bug. Although this is an edge case.

As we can see in the source code. mock module expects that iter(ret_val) will return the unchanged iterator if ret_val has already been an iterator.

Well, it actually does but still needs to call ret_val's __iter__ method.

Upvotes: 2

Related Questions