Skip to content

Commit 1d4d677

Browse files
authored
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.
1 parent 9ffbc58 commit 1d4d677

File tree

3 files changed

+41
-4
lines changed

3 files changed

+41
-4
lines changed

Lib/test/test_unittest/testmock/testmock.py

+24
Original file line numberDiff line numberDiff line change
@@ -1645,12 +1645,36 @@ def test_mock_unsafe(self):
16451645
m.aseert_foo_call()
16461646
with self.assertRaisesRegex(AttributeError, msg):
16471647
m.assrt_foo_call()
1648+
with self.assertRaisesRegex(AttributeError, msg):
1649+
m.called_once_with()
1650+
with self.assertRaisesRegex(AttributeError, msg):
1651+
m.called_once()
1652+
with self.assertRaisesRegex(AttributeError, msg):
1653+
m.has_calls()
1654+
1655+
class Foo(object):
1656+
def called_once(self):
1657+
pass
1658+
1659+
def has_calls(self):
1660+
pass
1661+
1662+
m = Mock(spec=Foo)
1663+
m.called_once()
1664+
m.has_calls()
1665+
1666+
m.called_once.assert_called_once()
1667+
m.has_calls.assert_called_once()
1668+
16481669
m = Mock(unsafe=True)
16491670
m.assert_foo_call()
16501671
m.assret_foo_call()
16511672
m.asert_foo_call()
16521673
m.aseert_foo_call()
16531674
m.assrt_foo_call()
1675+
m.called_once()
1676+
m.called_once_with()
1677+
m.has_calls()
16541678

16551679
# gh-100739
16561680
def test_mock_safe_with_spec(self):

Lib/unittest/mock.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ def __getattr__(self, name):
653653
elif _is_magic(name):
654654
raise AttributeError(name)
655655
if not self._mock_unsafe and (not self._mock_methods or name not in self._mock_methods):
656-
if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')):
656+
if name.startswith(('assert', 'assret', 'asert', 'aseert', 'assrt')) or name in ATTRIB_DENY_LIST:
657657
raise AttributeError(
658658
f"{name!r} is not a valid assertion. Use a spec "
659659
f"for the mock if {name!r} is meant to be an attribute.")
@@ -1062,6 +1062,10 @@ def _calls_repr(self, prefix="Calls"):
10621062
return f"\n{prefix}: {safe_repr(self.mock_calls)}."
10631063

10641064

1065+
# Denylist for forbidden attribute names in safe mode
1066+
ATTRIB_DENY_LIST = {name.removeprefix("assert_") for name in dir(NonCallableMock) if name.startswith("assert_")}
1067+
1068+
10651069
class _AnyComparer(list):
10661070
"""A list which checks if it contains a call which may have an
10671071
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
12311235
`return_value` attribute.
12321236
12331237
* `unsafe`: By default, accessing any attribute whose name starts with
1234-
*assert*, *assret*, *asert*, *aseert* or *assrt* will raise an
1235-
AttributeError. Passing `unsafe=True` will allow access to
1236-
these attributes.
1238+
*assert*, *assret*, *asert*, *aseert*, or *assrt* raises an AttributeError.
1239+
Additionally, an AttributeError is raised when accessing
1240+
attributes that match the name of an assertion method without the prefix
1241+
`assert_`, e.g. accessing `called_once` instead of `assert_called_once`.
1242+
Passing `unsafe=True` will allow access to these attributes.
12371243
12381244
* `wraps`: Item for the mock object to wrap. If `wraps` is not None then
12391245
calling the Mock will pass the call through to the wrapped object
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
``Mock`` objects which are not unsafe will now raise an
2+
``AttributeError`` when accessing an attribute that matches the name
3+
of an assertion but without the prefix ``assert_``, e.g. accessing
4+
``called_once`` instead of ``assert_called_once``.
5+
This is in addition to this already happening for accessing attributes
6+
with prefixes ``assert``, ``assret``, ``asert``, ``aseert``,
7+
and ``assrt``.

0 commit comments

Comments
 (0)