Skip to content

Commit 3f0f743

Browse files
[3.10] gh-98624 Add mutex to unittest.mock.NonCallableMock (GH-98688) (#98798)
(cherry picked from commit 0346edd) Co-authored-by: noah-weingarden <[email protected]>
1 parent af204e4 commit 3f0f743

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
@@ -34,6 +34,7 @@
3434
from types import CodeType, ModuleType, MethodType
3535
from unittest.util import safe_repr
3636
from functools import wraps, partial
37+
from threading import RLock
3738

3839

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

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

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

673683
return result
674684

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)