diff options
author | noah-weingarden <33741795+noah-weingarden@users.noreply.github.com> | 2022-10-28 07:51:18 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-28 07:51:18 (GMT) |
commit | 0346eddbe933b5f1f56151bdebf5bd49392bc275 (patch) | |
tree | a7213ba11ed95f3640ec85832ebf1c378b621cee /Lib/unittest | |
parent | fbcafa6eeeb106c8d9e9bb4c3b44e34e070f79c9 (diff) | |
download | cpython-0346eddbe933b5f1f56151bdebf5bd49392bc275.zip cpython-0346eddbe933b5f1f56151bdebf5bd49392bc275.tar.gz cpython-0346eddbe933b5f1f56151bdebf5bd49392bc275.tar.bz2 |
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
Diffstat (limited to 'Lib/unittest')
-rw-r--r-- | Lib/unittest/mock.py | 66 |
1 files changed, 38 insertions, 28 deletions
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index cd46fea..b8f4e57 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -35,6 +35,7 @@ from asyncio import iscoroutinefunction from types import CodeType, ModuleType, MethodType from unittest.util import safe_repr from functools import wraps, partial +from threading import RLock class InvalidSpecError(Exception): @@ -402,6 +403,14 @@ class Base(object): 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 @@ -644,35 +653,36 @@ class NonCallableMock(Base): 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 |