]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-75988: Fix issues with autospec ignoring wrapped object (#115223)
authorinfohash <46137868+infohash@users.noreply.github.com>
Fri, 8 Mar 2024 19:14:32 +0000 (00:44 +0530)
committerGitHub <noreply@github.com>
Fri, 8 Mar 2024 19:14:32 +0000 (19:14 +0000)
* 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

Doc/library/unittest.mock.rst
Lib/test/test_unittest/testmock/testmock.py
Lib/unittest/mock.py
Misc/NEWS.d/next/Library/2024-02-27-13-05-51.gh-issue-75988.In6LlB.rst [new file with mode: 0644]

index 1f25a16f544da83c3728f32d8eab48d40fbe60ee..d1f2a96df667c66791e4fe6ae4157a804eb74052 100644 (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
index 1725406bcfb9e41af757fae8e448e5b2e105deff..b81b3049d56bf82ff434484b608dcce71bc4cc61 100644 (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()
index 93f4d9743ed2fa94b5d08ee980ea1d074fbe3be8..1799e9bbf58592fc4d0a2802580c56682a0e9e09 100644 (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}
 
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 (file)
index 0000000..682b7cf
--- /dev/null
@@ -0,0 +1 @@
+Fixed :func:`unittest.mock.create_autospec` to pass the call through to the wrapped object to return the real result.