Skip to content

Commit

Permalink
gh-100690: Raise an AttributeError when the assert_ prefix is forgott…
Browse files Browse the repository at this point in the history
…en 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.

Backports: 1d4d677d1c90fcf4886ded0bf04b8f9d5b60b909
Signed-off-by: Chris Withers <chris@simplistix.co.uk>
  • Loading branch information
cklein authored and cjw296 committed Jan 9, 2023
1 parent aeca482 commit 21787a9
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 4 deletions.
7 changes: 7 additions & 0 deletions NEWS.d/2023-01-02-16-59-49.gh-issue-100690.2EgWPS.rst
Original file line number Diff line number Diff line change
@@ -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``.
14 changes: 10 additions & 4 deletions mock/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,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.")
Expand Down Expand Up @@ -1070,6 +1070,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
Expand Down Expand Up @@ -1241,9 +1245,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
Expand Down
24 changes: 24 additions & 0 deletions mock/tests/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,12 +1648,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):
Expand Down

0 comments on commit 21787a9

Please sign in to comment.