Mailing List Archive

[3.11] gh-75988: Fix issues with autospec ignoring wrapped object (GH-115223) (#117124)
https://github.com/python/cpython/commit/51da1ddefc74354cfb353db7f61ea7bb6298b332
commit: 51da1ddefc74354cfb353db7f61ea7bb6298b332
branch: 3.11
author: infohash <46137868+infohash@users.noreply.github.com>
committer: cjw296 <chris@withers.org>
date: 2024-03-22T09:48:41Z
summary:

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

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

(cherry picked from commit 735fc2cbbcf875c359021b5b2af7f4c29f4cf66d)

files:
A Misc/NEWS.d/next/Library/2024-02-27-13-05-51.gh-issue-75988.In6LlB.rst
M Doc/library/unittest.mock.rst
M Lib/unittest/mock.py
M Lib/unittest/test/testmock/testmock.py

diff --git a/Doc/library/unittest.mock.rst b/Doc/library/unittest.mock.rst
index b626c0036fca67..2ff9d82f0bdf28 100644
--- a/Doc/library/unittest.mock.rst
+++ b/Doc/library/unittest.mock.rst
@@ -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
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 3c96f1e864932a..d4307edac74126 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -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='()'
)
@@ -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)

@@ -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):
@@ -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}

diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py
index b2b299c14023b9..843c4d0fc203fc 100644
--- a/Lib/unittest/test/testmock/testmock.py
+++ b/Lib/unittest/test/testmock/testmock.py
@@ -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()
@@ -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()
diff --git a/Misc/NEWS.d/next/Library/2024-02-27-13-05-51.gh-issue-75988.In6LlB.rst b/Misc/NEWS.d/next/Library/2024-02-27-13-05-51.gh-issue-75988.In6LlB.rst
new file mode 100644
index 00000000000000..682b7cfa06b868
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-02-27-13-05-51.gh-issue-75988.In6LlB.rst
@@ -0,0 +1 @@
+Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-leave@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: list-python-checkins@lists.gossamer-threads.com