Skip to content

Commit

Permalink
pythongh-98624 Add mutex to unittest.mock.NonCallableMock (python#98688)
Browse files Browse the repository at this point in the history
* Added lock to NonCallableMock in unittest.mock

* Add blurb

* Nitpick blurb

* Edit comment based on @Jason-Y-Z's review

* Add link to GH issue
  • Loading branch information
noah-weingarden authored Oct 28, 2022
1 parent fbcafa6 commit 0346edd
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 28 deletions.
66 changes: 38 additions & 28 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from types import CodeType, ModuleType, MethodType
from unittest.util import safe_repr
from functools import wraps, partial
from threading import RLock


class InvalidSpecError(Exception):
Expand Down Expand Up @@ -402,6 +403,14 @@ def __init__(self, /, *args, **kwargs):
class NonCallableMock(Base):
"""A non-callable version of `Mock`"""

# Store a mutex as a class attribute in order to protect concurrent access
# to mock attributes. Using a class attribute allows all NonCallableMock
# instances to share the mutex for simplicity.
#
# See https://github.com/python/cpython/issues/98624 for why this is
# necessary.
_lock = RLock()

def __new__(cls, /, *args, **kw):
# every instance has its own class
# so we can create magic methods on the
Expand Down Expand Up @@ -644,35 +653,36 @@ def __getattr__(self, name):
f"{name!r} is not a valid assertion. Use a spec "
f"for the mock if {name!r} is meant to be an attribute.")

result = self._mock_children.get(name)
if result is _deleted:
raise AttributeError(name)
elif result is None:
wraps = None
if self._mock_wraps is not None:
# XXXX should we get the attribute without triggering code
# execution?
wraps = getattr(self._mock_wraps, name)

result = self._get_child_mock(
parent=self, name=name, wraps=wraps, _new_name=name,
_new_parent=self
)
self._mock_children[name] = result

elif isinstance(result, _SpecState):
try:
result = create_autospec(
result.spec, result.spec_set, result.instance,
result.parent, result.name
with NonCallableMock._lock:
result = self._mock_children.get(name)
if result is _deleted:
raise AttributeError(name)
elif result is None:
wraps = None
if self._mock_wraps is not None:
# XXXX should we get the attribute without triggering code
# execution?
wraps = getattr(self._mock_wraps, name)

result = self._get_child_mock(
parent=self, name=name, wraps=wraps, _new_name=name,
_new_parent=self
)
except InvalidSpecError:
target_name = self.__dict__['_mock_name'] or self
raise InvalidSpecError(
f'Cannot autospec attr {name!r} from target '
f'{target_name!r} as it has already been mocked out. '
f'[target={self!r}, attr={result.spec!r}]')
self._mock_children[name] = result
self._mock_children[name] = result

elif isinstance(result, _SpecState):
try:
result = create_autospec(
result.spec, result.spec_set, result.instance,
result.parent, result.name
)
except InvalidSpecError:
target_name = self.__dict__['_mock_name'] or self
raise InvalidSpecError(
f'Cannot autospec attr {name!r} from target '
f'{target_name!r} as it has already been mocked out. '
f'[target={self!r}, attr={result.spec!r}]')
self._mock_children[name] = result

return result

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a mutex to unittest.mock.NonCallableMock to protect concurrent access
to mock attributes.

0 comments on commit 0346edd

Please sign in to comment.