|
34 | 34 | from types import CodeType, ModuleType, MethodType
|
35 | 35 | from unittest.util import safe_repr
|
36 | 36 | from functools import wraps, partial
|
| 37 | +from threading import RLock |
37 | 38 |
|
38 | 39 |
|
39 | 40 | class InvalidSpecError(Exception):
|
@@ -401,6 +402,14 @@ def __init__(self, /, *args, **kwargs):
|
401 | 402 | class NonCallableMock(Base):
|
402 | 403 | """A non-callable version of `Mock`"""
|
403 | 404 |
|
| 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 | + |
404 | 413 | def __new__(cls, /, *args, **kw):
|
405 | 414 | # every instance has its own class
|
406 | 415 | # so we can create magic methods on the
|
@@ -640,35 +649,36 @@ def __getattr__(self, name):
|
640 | 649 | f"{name!r} is not a valid assertion. Use a spec "
|
641 | 650 | f"for the mock if {name!r} is meant to be an attribute.")
|
642 | 651 |
|
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 |
664 | 666 | )
|
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 |
672 | 682 |
|
673 | 683 | return result
|
674 | 684 |
|
|
0 commit comments