Skip to content

[3.11] gh-75988: Fix issues with autospec ignoring wrapped object (GH-115223) #117124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions Doc/library/unittest.mock.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2780,3 +2780,123 @@ Sealing mocks
>>> mock.not_submock.attribute2 # This won't raise.

.. versionadded:: 3.7


Order of precedence of :attr:`side_effect`, :attr:`return_value` and *wraps*
----------------------------------------------------------------------------

The order of their precedence is:

1. :attr:`~Mock.side_effect`
2. :attr:`~Mock.return_value`
3. *wraps*

If all three are set, mock will return the value from :attr:`~Mock.side_effect`,
ignoring :attr:`~Mock.return_value` and the wrapped object altogether. If any
two are set, the one with the higher precedence will return the value.
Regardless of the order of which was set first, the order of precedence
remains unchanged.

>>> from unittest.mock import Mock
>>> class Order:
... @staticmethod
... def get_value():
... return "third"
...
>>> order_mock = Mock(spec=Order, wraps=Order)
>>> order_mock.get_value.side_effect = ["first"]
>>> order_mock.get_value.return_value = "second"
>>> order_mock.get_value()
'first'

As ``None`` is the default value of :attr:`~Mock.side_effect`, if you reassign
its value back to ``None``, the order of precedence will be checked between
:attr:`~Mock.return_value` and the wrapped object, ignoring
:attr:`~Mock.side_effect`.

>>> order_mock.get_value.side_effect = None
>>> order_mock.get_value()
'second'

If the value being returned by :attr:`~Mock.side_effect` is :data:`DEFAULT`,
it is ignored and the order of precedence moves to the successor to obtain the
value to return.

>>> from unittest.mock import DEFAULT
>>> order_mock.get_value.side_effect = [DEFAULT]
>>> order_mock.get_value()
'second'

When :class:`Mock` wraps an object, the default value of
:attr:`~Mock.return_value` will be :data:`DEFAULT`.

>>> order_mock = Mock(spec=Order, wraps=Order)
>>> order_mock.return_value
sentinel.DEFAULT
>>> order_mock.get_value.return_value
sentinel.DEFAULT

The order of precedence will ignore this value and it will move to the last
successor which is the wrapped object.

As the real call is being made to the wrapped object, creating an instance of
this mock will return the real instance of the class. The positional arguments,
if any, required by the wrapped object must be passed.

>>> order_mock_instance = order_mock()
>>> isinstance(order_mock_instance, Order)
True
>>> order_mock_instance.get_value()
'third'

>>> order_mock.get_value.return_value = DEFAULT
>>> order_mock.get_value()
'third'

>>> order_mock.get_value.return_value = "second"
>>> order_mock.get_value()
'second'

But if you assign ``None`` to it, this will not be ignored as it is an
explicit assignment. So, the order of precedence will not move to the wrapped
object.

>>> order_mock.get_value.return_value = None
>>> order_mock.get_value() is None
True

Even if you set all three at once when initializing the mock, the order of
precedence remains the same:

>>> order_mock = Mock(spec=Order, wraps=Order,
... **{"get_value.side_effect": ["first"],
... "get_value.return_value": "second"}
... )
...
>>> order_mock.get_value()
'first'
>>> order_mock.get_value.side_effect = None
>>> order_mock.get_value()
'second'
>>> order_mock.get_value.return_value = DEFAULT
>>> order_mock.get_value()
'third'

If :attr:`~Mock.side_effect` is exhausted, the order of precedence will not
cause a value to be obtained from the successors. Instead, ``StopIteration``
exception is raised.

>>> order_mock = Mock(spec=Order, wraps=Order)
>>> order_mock.get_value.side_effect = ["first side effect value",
... "another side effect value"]
>>> order_mock.get_value.return_value = "second"

>>> order_mock.get_value()
'first side effect value'
>>> order_mock.get_value()
'another side effect value'

>>> order_mock.get_value()
Traceback (most recent call last):
...
StopIteration
13 changes: 11 additions & 2 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def __get_return_value(self):
if self._mock_delegate is not None:
ret = self._mock_delegate.return_value

if ret is DEFAULT:
if ret is DEFAULT and self._mock_wraps is None:
ret = self._get_child_mock(
_new_parent=self, _new_name='()'
)
Expand Down Expand Up @@ -1194,6 +1194,9 @@ def _execute_mock_call(self, /, *args, **kwargs):
if self._mock_return_value is not DEFAULT:
return self.return_value

if self._mock_delegate and self._mock_delegate.return_value is not DEFAULT:
return self.return_value

if self._mock_wraps is not None:
return self._mock_wraps(*args, **kwargs)

Expand Down Expand Up @@ -2732,9 +2735,12 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
if _parent is not None and not instance:
_parent._mock_children[_name] = mock

wrapped = kwargs.get('wraps')

if is_type and not instance and 'return_value' not in kwargs:
mock.return_value = create_autospec(spec, spec_set, instance=True,
_name='()', _parent=mock)
_name='()', _parent=mock,
wraps=wrapped)

for entry in dir(spec):
if _is_magic(entry):
Expand All @@ -2756,6 +2762,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
continue

kwargs = {'spec': original}
# Wrap child attributes also.
if wrapped and hasattr(wrapped, entry):
kwargs.update(wraps=original)
if spec_set:
kwargs = {'spec_set': original}

Expand Down
66 changes: 66 additions & 0 deletions Lib/unittest/test/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,64 @@ class B(object):
with mock.patch('builtins.open', mock.mock_open()):
mock.mock_open() # should still be valid with open() mocked

def test_create_autospec_wraps_class(self):
"""Autospec a class with wraps & test if the call is passed to the
wrapped object."""
result = "real result"

class Result:
def get_result(self):
return result
class_mock = create_autospec(spec=Result, wraps=Result)
# Have to reassign the return_value to DEFAULT to return the real
# result (actual instance of "Result") when the mock is called.
class_mock.return_value = mock.DEFAULT
self.assertEqual(class_mock().get_result(), result)
# Autospec should also wrap child attributes of parent.
self.assertEqual(class_mock.get_result._mock_wraps, Result.get_result)

def test_create_autospec_instance_wraps_class(self):
"""Autospec a class instance with wraps & test if the call is passed
to the wrapped object."""
result = "real result"

class Result:
@staticmethod
def get_result():
"""This is a static method because when the mocked instance of
'Result' will call this method, it won't be able to consume
'self' argument."""
return result
instance_mock = create_autospec(spec=Result, instance=True, wraps=Result)
# Have to reassign the return_value to DEFAULT to return the real
# result from "Result.get_result" when the mocked instance of "Result"
# calls "get_result".
instance_mock.get_result.return_value = mock.DEFAULT
self.assertEqual(instance_mock.get_result(), result)
# Autospec should also wrap child attributes of the instance.
self.assertEqual(instance_mock.get_result._mock_wraps, Result.get_result)

def test_create_autospec_wraps_function_type(self):
"""Autospec a function or a method with wraps & test if the call is
passed to the wrapped object."""
result = "real result"

class Result:
def get_result(self):
return result
func_mock = create_autospec(spec=Result.get_result, wraps=Result.get_result)
self.assertEqual(func_mock(Result()), result)

def test_explicit_return_value_even_if_mock_wraps_object(self):
"""If the mock has an explicit return_value set then calls are not
passed to the wrapped object and the return_value is returned instead.
"""
def my_func():
return None
func_mock = create_autospec(spec=my_func, wraps=my_func)
return_value = "explicit return value"
func_mock.return_value = return_value
self.assertEqual(func_mock(), return_value)

def test_reset_mock(self):
parent = Mock()
Expand Down Expand Up @@ -603,6 +661,14 @@ def test_wraps_calls(self):
real = Mock()

mock = Mock(wraps=real)
# If "Mock" wraps an object, just accessing its
# "return_value" ("NonCallableMock.__get_return_value") should not
# trigger its descriptor ("NonCallableMock.__set_return_value") so
# the default "return_value" should always be "sentinel.DEFAULT".
self.assertEqual(mock.return_value, DEFAULT)
# It will not be "sentinel.DEFAULT" if the mock is not wrapping any
# object.
self.assertNotEqual(real.return_value, DEFAULT)
self.assertEqual(mock(), real())

real.reset_mock()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.