|
35 | 35 | from types import CodeType, ModuleType, MethodType
|
36 | 36 | from unittest.util import safe_repr
|
37 | 37 | from functools import wraps, partial
|
| 38 | +from threading import RLock |
38 | 39 |
|
39 | 40 |
|
40 | 41 | class InvalidSpecError(Exception):
|
@@ -402,6 +403,14 @@ def __init__(self, /, *args, **kwargs):
|
402 | 403 | class NonCallableMock(Base):
|
403 | 404 | """A non-callable version of `Mock`"""
|
404 | 405 |
|
| 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 | + |
405 | 414 | def __new__(cls, /, *args, **kw):
|
406 | 415 | # every instance has its own class
|
407 | 416 | # so we can create magic methods on the
|
@@ -644,35 +653,36 @@ def __getattr__(self, name):
|
644 | 653 | f"{name!r} is not a valid assertion. Use a spec "
|
645 | 654 | f"for the mock if {name!r} is meant to be an attribute.")
|
646 | 655 |
|
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 |
668 | 670 | )
|
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 |
676 | 686 |
|
677 | 687 | return result
|
678 | 688 |
|
|
0 commit comments