gh-75988: Fix issues with autospec ignoring wrapped object (#115223)

* set default return value of functional types as _mock_return_value

* added test of wrapping child attributes

* added backward compatibility with explicit return

* added docs on the order of precedence

* added test to check default return_value
This commit is contained in:
infohash 2024-03-09 00:44:32 +05:30 committed by GitHub
parent 7db871e4fa
commit 735fc2cbbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 199 additions and 2 deletions

View File

@ -2831,3 +2831,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

View File

@ -245,6 +245,65 @@ class MockTest(unittest.TestCase):
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_explicit_parent(self):
parent = Mock()
mock1 = Mock(parent=parent, return_value=None)
@ -622,6 +681,14 @@ class MockTest(unittest.TestCase):
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()

View File

@ -573,7 +573,7 @@ class NonCallableMock(Base):
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='()'
)
@ -1234,6 +1234,9 @@ class CallableMixin(Base):
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)
@ -2785,9 +2788,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):
@ -2809,6 +2815,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}

View File

@ -0,0 +1 @@
Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.