Skip to content

Commit 0346edd

Browse files
gh-98624 Add mutex to unittest.mock.NonCallableMock (#98688)
* 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
1 parent fbcafa6 commit 0346edd

File tree

2 files changed

+40
-28
lines changed

2 files changed

+40
-28
lines changed

Lib/unittest/mock.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from types import CodeType, ModuleType, MethodType
3636
from unittest.util import safe_repr
3737
from functools import wraps, partial
38+
from threading import RLock
3839

3940

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

406+
# Store a mutex as a class attribute in order to protect concurrent access
407+
# to mock attributes. Using a class attribute allows all NonCallableMock
408+
# instances to share the mutex for simplicity.
409+
#
410+
# See https://github.com/python/cpython/issues/98624 for why this is
411+
# necessary.
412+
_lock = RLock()
413+
405414
def __new__(cls, /, *args, **kw):
406415
# every instance has its own class
407416
# so we can create magic methods on the
@@ -644,35 +653,36 @@ def __getattr__(self, name):
644653
f"{name!r} is not a valid assertion. Use a spec "
645654
f"for the mock if {name!r} is meant to be an attribute.")
646655

647-
result = self._mock_children.get(name)
648-
if result is _deleted:
649-
raise AttributeError(name)
650-
elif result is None:
651-
wraps = None
652-
if self._mock_wraps is not None:
653-
# XXXX should we get the attribute without triggering code
654-
# execution?
655-
wraps = getattr(self._mock_wraps, name)
656-
657-
result = self._get_child_mock(
658-
parent=self, name=name, wraps=wraps, _new_name=name,
659-
_new_parent=self
660-
)
661-
self._mock_children[name] = result
662-
663-
elif isinstance(result, _SpecState):
664-
try:
665-
result = create_autospec(
666-
result.spec, result.spec_set, result.instance,
667-
result.parent, result.name
656+
with NonCallableMock._lock:
657+
result = self._mock_children.get(name)
658+
if result is _deleted:
659+
raise AttributeError(name)
660+
elif result is None:
661+
wraps = None
662+
if self._mock_wraps is not None:
663+
# XXXX should we get the attribute without triggering code
664+
# execution?
665+
wraps = getattr(self._mock_wraps, name)
666+
667+
result = self._get_child_mock(
668+
parent=self, name=name, wraps=wraps, _new_name=name,
669+
_new_parent=self
668670
)
669-
except InvalidSpecError:
670-
target_name = self.__dict__['_mock_name'] or self
671-
raise InvalidSpecError(
672-
f'Cannot autospec attr {name!r} from target '
673-
f'{target_name!r} as it has already been mocked out. '
674-
f'[target={self!r}, attr={result.spec!r}]')
675-
self._mock_children[name] = result
671+
self._mock_children[name] = result
672+
673+
elif isinstance(result, _SpecState):
674+
try:
675+
result = create_autospec(
676+
result.spec, result.spec_set, result.instance,
677+
result.parent, result.name
678+
)
679+
except InvalidSpecError:
680+
target_name = self.__dict__['_mock_name'] or self
681+
raise InvalidSpecError(
682+
f'Cannot autospec attr {name!r} from target '
683+
f'{target_name!r} as it has already been mocked out. '
684+
f'[target={self!r}, attr={result.spec!r}]')
685+
self._mock_children[name] = result
676686

677687
return result
678688

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add a mutex to unittest.mock.NonCallableMock to protect concurrent access
2+
to mock attributes.

0 commit comments

Comments
 (0)