[Python-checkins] gh-100690: Raise an AttributeError when the assert_ prefix is forgotten when using Mock (#100691)

cjw296 webhook-mailer at python.org
Fri Jan 6 13:38:58 EST 2023


https://github.com/python/cpython/commit/1d4d677d1c90fcf4886ded0bf04b8f9d5b60b909
commit: 1d4d677d1c90fcf4886ded0bf04b8f9d5b60b909
branch: main
author: Christian Klein <167265+cklein at users.noreply.github.com>
committer: cjw296 <chris at withers.org>
date: 2023-01-06T18:38:50Z
summary:

gh-100690: Raise an AttributeError when the assert_ prefix is forgotten when using Mock (#100691)

Mock objects which are not unsafe will now raise an AttributeError when accessing an
attribute that matches the name of an assertion but without the prefix `assert_`, e.g. accessing `called_once` instead of `assert_called_once`.

This is in addition to this already happening for accessing attributes with prefixes assert, assret, asert, aseert, and assrt.

files:
A Misc/NEWS.d/next/Library/2023-01-02-16-59-49.gh-issue-100690.2EgWPS.rst
M Lib/test/test_unittest/testmock/testmock.py
M Lib/unittest/mock.py

diff --git a/Lib/test/test_unittest/testmock/testmock.py b/Lib/test/test_unittest/testmock/testmock.py
index b224f87fa3e4..97fe66fa4ca2 100644
--- a/Lib/test/test_unittest/testmock/testmock.py
+++ b/Lib/test/test_unittest/testmock/testmock.py
@@ -1645,12 +1645,36 @@ def test_mock_unsafe(self):
             m.aseert_foo_call()
         with self.assertRaisesRegex(AttributeError, msg):
             m.assrt_foo_call()
+        with self.assertRaisesRegex(AttributeError, msg):
+            m.called_once_with()
+        with self.assertRaisesRegex(AttributeError, msg):
+            m.called_once()
+        with self.assertRaisesRegex(AttributeError, msg):
+            m.has_calls()
+
+        class Foo(object):
+            def called_once(self):
+                pass
+
+            def has_calls(self):
+                pass
+
+        m = Mock(spec=Foo)
+        m.called_once()
+        m.has_calls()
+
+        m.called_once.assert_called_once()
+        m.has_calls.assert_called_once()
+
         m = Mock(unsafe=True)
         m.assert_foo_call()
         m.assret_foo_call()
         m.asert_foo_call()
         m.aseert_foo_call()
         m.assrt_foo_call()
+        m.called_once()
+        m.called_once_with()
+        m.has_calls()
 
     # gh-100739
     def test_mock_safe_with_spec(self):
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index 47928e564850..78827d61b69d 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -653,7 +653,7 @@ def __getattr__(self, name):
         elif _is_magic(name):
             raise AttributeError(name)
         if not self._mock_unsafe and (not self._mock_methods or name not in self._mock_methods):
-            if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')):
+            if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')) or name in ATTRIB_DENY_LIST:
                 raise AttributeError(
                     f"{name!r} is not a valid assertion. Use a spec "
                     f"for the mock if {name!r} is meant to be an attribute.")
@@ -1062,6 +1062,10 @@ def _calls_repr(self, prefix="Calls"):
         return f"\n{prefix}: {safe_repr(self.mock_calls)}."
 
 
+# Denylist for forbidden attribute names in safe mode
+ATTRIB_DENY_LIST = {name.removeprefix("assert_") for name in dir(NonCallableMock) if name.startswith("assert_")}
+
+
 class _AnyComparer(list):
     """A list which checks if it contains a call which may have an
     argument of ANY, flipping the components of item and self from
@@ -1231,9 +1235,11 @@ class or instance) that acts as the specification for the mock object. If
       `return_value` attribute.
 
     * `unsafe`: By default, accessing any attribute whose name starts with
-      *assert*, *assret*, *asert*, *aseert* or *assrt* will raise an
-       AttributeError. Passing `unsafe=True` will allow access to
-      these attributes.
+      *assert*, *assret*, *asert*, *aseert*, or *assrt* raises an AttributeError.
+      Additionally, an AttributeError is raised when accessing
+      attributes that match the name of an assertion method without the prefix
+      `assert_`, e.g. accessing `called_once` instead of `assert_called_once`.
+      Passing `unsafe=True` will allow access to these attributes.
 
     * `wraps`: Item for the mock object to wrap. If `wraps` is not None then
       calling the Mock will pass the call through to the wrapped object
diff --git a/Misc/NEWS.d/next/Library/2023-01-02-16-59-49.gh-issue-100690.2EgWPS.rst b/Misc/NEWS.d/next/Library/2023-01-02-16-59-49.gh-issue-100690.2EgWPS.rst
new file mode 100644
index 000000000000..3796772aebdf
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-01-02-16-59-49.gh-issue-100690.2EgWPS.rst
@@ -0,0 +1,7 @@
+``Mock`` objects which are not unsafe will now raise an
+``AttributeError`` when accessing an attribute that matches the name
+of an assertion but without the prefix ``assert_``, e.g. accessing
+``called_once`` instead of ``assert_called_once``.
+This is in addition to this already happening for accessing attributes
+with prefixes ``assert``, ``assret``, ``asert``, ``aseert``,
+and ``assrt``.



More information about the Python-checkins mailing list