From f78011f3fd340049dd6ba787670e7699c8be55ee Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Sun, 10 Nov 2024 19:26:12 +0300 Subject: [PATCH 01/11] Limit starting a patcher more than once without stopping it Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --- Lib/test/test_unittest/testmock/testpatch.py | 33 +++++++++- Lib/unittest/mock.py | 60 ++++++++++++++----- ...-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst | 5 ++ 3 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index f26e74ce0bc1ba..ddd7f2866f1d31 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -745,6 +745,35 @@ def test_stop_idempotent(self): self.assertIsNone(patcher.stop()) + def test_exit_idempotent(self): + patcher = patch(foo_name, 'bar', 3) + with patcher: + patcher.stop() + + + def test_second_start_failure(self): + patcher = patch(foo_name, 'bar', 3) + patcher.start() + try: + self.assertRaises(RuntimeError, patcher.start) + finally: + patcher.stop() + + + def test_second_enter_failure(self): + patcher = patch(foo_name, 'bar', 3) + with patcher: + self.assertRaises(RuntimeError, patcher.start) + + + def test_second_start_after_stop(self): + patcher = patch(foo_name, 'bar', 3) + patcher.start() + patcher.stop() + patcher.start() + patcher.stop() + + def test_patchobject_start_stop(self): original = something patcher = patch.object(PTModule, 'something', 'foo') @@ -1098,7 +1127,7 @@ def test_new_callable_patch(self): self.assertIsNot(m1, m2) for mock in m1, m2: - self.assertNotCallable(m1) + self.assertNotCallable(mock) def test_new_callable_patch_object(self): @@ -1111,7 +1140,7 @@ def test_new_callable_patch_object(self): self.assertIsNot(m1, m2) for mock in m1, m2: - self.assertNotCallable(m1) + self.assertNotCallable(mock) def test_new_callable_keyword_arguments(self): diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 21ca061a77c26f..bb596591d804b0 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1320,6 +1320,14 @@ def _check_spec_arg_typos(kwargs_to_check): ) +class _PatchStartedContext(object): + def __init__(self, exit_stack, is_local, target, temp_original): + self.exit_stack = exit_stack + self.is_local = is_local + self.target = target + self.temp_original = temp_original + + class _patch(object): attribute_name = None @@ -1360,6 +1368,7 @@ def __init__( self.autospec = autospec self.kwargs = kwargs self.additional_patchers = [] + self._started_context = None def copy(self): @@ -1469,13 +1478,31 @@ def get_original(self): ) return original, local + @property + def is_started(self): + return self._started_context is not None + + @property + def is_local(self): + return self._started_context.is_local + + @property + def target(self): + return self._started_context.target + + @property + def temp_original(self): + return self._started_context.temp_original def __enter__(self): """Perform the patch.""" + if self.is_started: + raise RuntimeError("Patch is already started") + new, spec, spec_set = self.new, self.spec, self.spec_set autospec, kwargs = self.autospec, self.kwargs new_callable = self.new_callable - self.target = self.getter() + target = self.getter() # normalise False to None if spec is False: @@ -1491,7 +1518,7 @@ def __enter__(self): spec_set not in (True, None)): raise TypeError("Can't provide explicit spec_set *and* spec or autospec") - original, local = self.get_original() + original, is_local = self.get_original() if new is DEFAULT and autospec is None: inherit = False @@ -1579,17 +1606,17 @@ def __enter__(self): if autospec is True: autospec = original - if _is_instance_mock(self.target): + if _is_instance_mock(target): raise InvalidSpecError( f'Cannot autospec attr {self.attribute!r} as the patch ' f'target has already been mocked out. ' - f'[target={self.target!r}, attr={autospec!r}]') + f'[target={target!r}, attr={autospec!r}]') if _is_instance_mock(autospec): - target_name = getattr(self.target, '__name__', self.target) + target_name = getattr(target, '__name__', target) raise InvalidSpecError( f'Cannot autospec attr {self.attribute!r} from target ' f'{target_name!r} as it has already been mocked out. ' - f'[target={self.target!r}, attr={autospec!r}]') + f'[target={target!r}, attr={autospec!r}]') new = create_autospec(autospec, spec_set=spec_set, _name=self.attribute, **kwargs) @@ -1600,9 +1627,12 @@ def __enter__(self): new_attr = new - self.temp_original = original - self.is_local = local - self._exit_stack = contextlib.ExitStack() + self._started_context = _PatchStartedContext( + exit_stack=contextlib.ExitStack(), + is_local=is_local, + target=self.getter(), + temp_original=original, + ) try: setattr(self.target, self.attribute, new_attr) if self.attribute_name is not None: @@ -1610,7 +1640,7 @@ def __enter__(self): if self.new is DEFAULT: extra_args[self.attribute_name] = new for patching in self.additional_patchers: - arg = self._exit_stack.enter_context(patching) + arg = self._started_context.exit_stack.enter_context(patching) if patching.new is DEFAULT: extra_args.update(arg) return extra_args @@ -1622,6 +1652,9 @@ def __enter__(self): def __exit__(self, *exc_info): """Undo the patch.""" + if not self.is_started: + return + if self.is_local and self.temp_original is not DEFAULT: setattr(self.target, self.attribute, self.temp_original) else: @@ -1633,11 +1666,8 @@ def __exit__(self, *exc_info): # needed for proxy objects like django settings setattr(self.target, self.attribute, self.temp_original) - del self.temp_original - del self.is_local - del self.target - exit_stack = self._exit_stack - del self._exit_stack + exit_stack = self._started_context.exit_stack + self._started_context = None return exit_stack.__exit__(*exc_info) diff --git a/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst new file mode 100644 index 00000000000000..132ca5e087e2cf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst @@ -0,0 +1,5 @@ +Limit starting a patcher (from :func:`unittest.mock.patch`, +:func:`unittest.mock.patch.object` or :func:`unittest.mock.dict`) more than +once without stopping it. Previously, this would cause an +:exc:`AttributeError` if the patch stopped more than once after this, and +would also disrupt the original patched object. From 1b9c6d434a7afbe6da7a3f34b3c197a3dc4a0b77 Mon Sep 17 00:00:00 2001 From: Red4Ru <39802734+Red4Ru@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:52:08 +0300 Subject: [PATCH 02/11] Update Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst Co-authored-by: Peter Bierma --- .../Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst index 132ca5e087e2cf..dca4adad0e8a0f 100644 --- a/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst +++ b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst @@ -1,5 +1,3 @@ Limit starting a patcher (from :func:`unittest.mock.patch`, -:func:`unittest.mock.patch.object` or :func:`unittest.mock.dict`) more than -once without stopping it. Previously, this would cause an -:exc:`AttributeError` if the patch stopped more than once after this, and -would also disrupt the original patched object. +:func:`unittest.mock.patch.object` or :func:`unittest.mock.patch.dict`) more than +once without stopping it. From 399bb66feb72adebb5f9f2dcd42b48d2957eb602 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Sun, 10 Nov 2024 21:28:54 +0300 Subject: [PATCH 03/11] Refactor patch context --- Lib/unittest/mock.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index bb596591d804b0..33923f0f181299 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -25,6 +25,7 @@ import asyncio +from collections import namedtuple import contextlib import io import inspect @@ -1320,12 +1321,7 @@ def _check_spec_arg_typos(kwargs_to_check): ) -class _PatchStartedContext(object): - def __init__(self, exit_stack, is_local, target, temp_original): - self.exit_stack = exit_stack - self.is_local = is_local - self.target = target - self.temp_original = temp_original +_PatchContext = namedtuple("_PatchContext", "exit_stack is_local original target") class _patch(object): @@ -1368,7 +1364,7 @@ def __init__( self.autospec = autospec self.kwargs = kwargs self.additional_patchers = [] - self._started_context = None + self._context = None def copy(self): @@ -1480,19 +1476,23 @@ def get_original(self): @property def is_started(self): - return self._started_context is not None + return self._context is not None @property def is_local(self): - return self._started_context.is_local + return self._context.is_local + + @property + def original(self): + return self._context.original @property def target(self): - return self._started_context.target + return self._context.target @property - def temp_original(self): - return self._started_context.temp_original + def temp_original(self): # backwards compatibility + return self.original def __enter__(self): """Perform the patch.""" @@ -1627,11 +1627,12 @@ def __enter__(self): new_attr = new - self._started_context = _PatchStartedContext( - exit_stack=contextlib.ExitStack(), + exit_stack = contextlib.ExitStack() + self._context = _PatchContext( + exit_stack=exit_stack, is_local=is_local, + original=original, target=self.getter(), - temp_original=original, ) try: setattr(self.target, self.attribute, new_attr) @@ -1640,7 +1641,7 @@ def __enter__(self): if self.new is DEFAULT: extra_args[self.attribute_name] = new for patching in self.additional_patchers: - arg = self._started_context.exit_stack.enter_context(patching) + arg = exit_stack.enter_context(patching) if patching.new is DEFAULT: extra_args.update(arg) return extra_args @@ -1655,8 +1656,8 @@ def __exit__(self, *exc_info): if not self.is_started: return - if self.is_local and self.temp_original is not DEFAULT: - setattr(self.target, self.attribute, self.temp_original) + if self.is_local and self.original is not DEFAULT: + setattr(self.target, self.attribute, self.original) else: delattr(self.target, self.attribute) if not self.create and (not hasattr(self.target, self.attribute) or @@ -1664,10 +1665,10 @@ def __exit__(self, *exc_info): '__defaults__', '__annotations__', '__kwdefaults__')): # needed for proxy objects like django settings - setattr(self.target, self.attribute, self.temp_original) + setattr(self.target, self.attribute, self.original) - exit_stack = self._started_context.exit_stack - self._started_context = None + exit_stack = self._context.exit_stack + self._context = None return exit_stack.__exit__(*exc_info) From e28c3fb709da34062a48331299bf0fc1ecf61a92 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Sun, 10 Nov 2024 21:31:55 +0300 Subject: [PATCH 04/11] Add property setters (untested) --- Lib/unittest/mock.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 33923f0f181299..1ddef778e665e0 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1482,18 +1482,34 @@ def is_started(self): def is_local(self): return self._context.is_local + @is_local.setter + def is_local(self, value): + self._context.is_local = value + @property def original(self): return self._context.original + @original.setter + def original(self, value): + self._context.original = value + @property def target(self): return self._context.target + @target.setter + def target(self, value): + self._context.target = value + @property def temp_original(self): # backwards compatibility return self.original + @temp_original.setter + def temp_original(self, value): # backwards compatibility + self.original = value + def __enter__(self): """Perform the patch.""" if self.is_started: From a07d3b5a9c30d3b79749fe59b716adca40d96c2f Mon Sep 17 00:00:00 2001 From: Red4Ru <39802734+Red4Ru@users.noreply.github.com> Date: Sun, 10 Nov 2024 23:59:10 +0300 Subject: [PATCH 05/11] Avoid re-call of self.getter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 1ddef778e665e0..b36c86bc4063c0 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1648,7 +1648,7 @@ def __enter__(self): exit_stack=exit_stack, is_local=is_local, original=original, - target=self.getter(), + target=target, ) try: setattr(self.target, self.attribute, new_attr) From 3c4edf2c71b0058b621a363a1dd6807f04599a03 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Mon, 11 Nov 2024 00:19:06 +0300 Subject: [PATCH 06/11] Preserve only _patch.temp_original instead of _patch.original --- Lib/unittest/mock.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index b36c86bc4063c0..000da71694aa5b 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1486,14 +1486,6 @@ def is_local(self): def is_local(self, value): self._context.is_local = value - @property - def original(self): - return self._context.original - - @original.setter - def original(self, value): - self._context.original = value - @property def target(self): return self._context.target @@ -1503,12 +1495,12 @@ def target(self, value): self._context.target = value @property - def temp_original(self): # backwards compatibility - return self.original + def temp_original(self): + return self._context.original @temp_original.setter - def temp_original(self, value): # backwards compatibility - self.original = value + def temp_original(self, value): + self._context.original = value def __enter__(self): """Perform the patch.""" @@ -1672,8 +1664,8 @@ def __exit__(self, *exc_info): if not self.is_started: return - if self.is_local and self.original is not DEFAULT: - setattr(self.target, self.attribute, self.original) + if self.is_local and self.temp_original is not DEFAULT: + setattr(self.target, self.attribute, self.temp_original) else: delattr(self.target, self.attribute) if not self.create and (not hasattr(self.target, self.attribute) or @@ -1681,7 +1673,7 @@ def __exit__(self, *exc_info): '__defaults__', '__annotations__', '__kwdefaults__')): # needed for proxy objects like django settings - setattr(self.target, self.attribute, self.original) + setattr(self.target, self.attribute, self.temp_original) exit_stack = self._context.exit_stack self._context = None From 916ac69e50b442fc5404a613245e00c17ca27bd6 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Mon, 11 Nov 2024 00:23:00 +0300 Subject: [PATCH 07/11] Change NEWS file I rechecked and get that `unittest.mock.patch.dict` is not involved. Also deleted a trailing comma --- .../Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst index dca4adad0e8a0f..c83a10769820cf 100644 --- a/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst +++ b/Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst @@ -1,3 +1,3 @@ -Limit starting a patcher (from :func:`unittest.mock.patch`, -:func:`unittest.mock.patch.object` or :func:`unittest.mock.patch.dict`) more than -once without stopping it. +Limit starting a patcher (from :func:`unittest.mock.patch` or +:func:`unittest.mock.patch.object`) more than +once without stopping it From 862d5a0390f4659054ed7da12d2bf9ae377b78a7 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Mon, 11 Nov 2024 01:13:28 +0300 Subject: [PATCH 08/11] Fix of property setters along with a test for them --- Lib/test/test_unittest/testmock/testpatch.py | 19 +++++++++++ Lib/unittest/mock.py | 33 ++++++++++++++------ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index ddd7f2866f1d31..037c021e6eafcf 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -774,6 +774,25 @@ def test_second_start_after_stop(self): patcher.stop() + def test_property_setters(self): + mock_object = Mock() + mock_bar = mock_object.bar + patcher = patch.object(mock_object, 'bar', 'x') + with patcher: + self.assertEqual(patcher.is_local, False) + self.assertIs(patcher.target, mock_object) + self.assertEqual(patcher.temp_original, mock_bar) + patcher.is_local = True + patcher.target = mock_bar + patcher.temp_original = mock_object + self.assertEqual(patcher.is_local, True) + self.assertIs(patcher.target, mock_bar) + self.assertEqual(patcher.temp_original, mock_object) + # if changes are left intact, they may lead to disruption as shown below (it might be what someone needs though) + self.assertEqual(mock_bar.bar, mock_object) + self.assertEqual(mock_object.bar, 'x') + + def test_patchobject_start_stop(self): original = something patcher = patch.object(PTModule, 'something', 'foo') diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 000da71694aa5b..79f2c5f9ef2190 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1482,25 +1482,40 @@ def is_started(self): def is_local(self): return self._context.is_local - @is_local.setter - def is_local(self, value): - self._context.is_local = value - @property def target(self): return self._context.target - @target.setter - def target(self, value): - self._context.target = value - @property def temp_original(self): return self._context.original + @is_local.setter + def is_local(self, value): + self._context = _PatchContext( + exit_stack=self._context.exit_stack, + is_local=value, + original=self._context.original, + target=self._context.target, + ) + + @target.setter + def target(self, value): + self._context = _PatchContext( + exit_stack=self._context.exit_stack, + is_local=self._context.is_local, + original=self._context.original, + target=value, + ) + @temp_original.setter def temp_original(self, value): - self._context.original = value + self._context = _PatchContext( + exit_stack=self._context.exit_stack, + is_local=self._context.is_local, + original=value, + target=self._context.target, + ) def __enter__(self): """Perform the patch.""" From 771989414f3b51a35944bdfc3ced6b9c3689141d Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Mon, 11 Nov 2024 10:36:35 +0300 Subject: [PATCH 09/11] Refactor _PatchContext from namedtuple to plain class with __slots__ --- Lib/unittest/mock.py | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 79f2c5f9ef2190..dc9acf977114fb 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -25,7 +25,6 @@ import asyncio -from collections import namedtuple import contextlib import io import inspect @@ -1321,7 +1320,14 @@ def _check_spec_arg_typos(kwargs_to_check): ) -_PatchContext = namedtuple("_PatchContext", "exit_stack is_local original target") +class _PatchContext: + __slots__ = ('exit_stack', 'is_local', 'original', 'target') + + def __init__(self, exit_stack, is_local, original, target): + self.exit_stack = exit_stack + self.is_local = is_local + self.original = original + self.target = target class _patch(object): @@ -1482,40 +1488,25 @@ def is_started(self): def is_local(self): return self._context.is_local + @is_local.setter + def is_local(self, value): + self._context.is_local = value + @property def target(self): return self._context.target + @target.setter + def target(self, value): + self._context.target = value + @property def temp_original(self): return self._context.original - @is_local.setter - def is_local(self, value): - self._context = _PatchContext( - exit_stack=self._context.exit_stack, - is_local=value, - original=self._context.original, - target=self._context.target, - ) - - @target.setter - def target(self, value): - self._context = _PatchContext( - exit_stack=self._context.exit_stack, - is_local=self._context.is_local, - original=self._context.original, - target=value, - ) - @temp_original.setter def temp_original(self, value): - self._context = _PatchContext( - exit_stack=self._context.exit_stack, - is_local=self._context.is_local, - original=value, - target=self._context.target, - ) + self._context.original = value def __enter__(self): """Perform the patch.""" From 1180b632d3896cc08cd425ab2e9155df88c40eb6 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Tue, 12 Nov 2024 11:33:08 +0300 Subject: [PATCH 10/11] Minimize changes in mock.py --- Lib/unittest/mock.py | 71 +++++++++++--------------------------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index dc9acf977114fb..2ef0d309d36052 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1320,16 +1320,6 @@ def _check_spec_arg_typos(kwargs_to_check): ) -class _PatchContext: - __slots__ = ('exit_stack', 'is_local', 'original', 'target') - - def __init__(self, exit_stack, is_local, original, target): - self.exit_stack = exit_stack - self.is_local = is_local - self.original = original - self.target = target - - class _patch(object): attribute_name = None @@ -1370,7 +1360,7 @@ def __init__( self.autospec = autospec self.kwargs = kwargs self.additional_patchers = [] - self._context = None + self.is_started = False def copy(self): @@ -1480,34 +1470,6 @@ def get_original(self): ) return original, local - @property - def is_started(self): - return self._context is not None - - @property - def is_local(self): - return self._context.is_local - - @is_local.setter - def is_local(self, value): - self._context.is_local = value - - @property - def target(self): - return self._context.target - - @target.setter - def target(self, value): - self._context.target = value - - @property - def temp_original(self): - return self._context.original - - @temp_original.setter - def temp_original(self, value): - self._context.original = value - def __enter__(self): """Perform the patch.""" if self.is_started: @@ -1516,7 +1478,7 @@ def __enter__(self): new, spec, spec_set = self.new, self.spec, self.spec_set autospec, kwargs = self.autospec, self.kwargs new_callable = self.new_callable - target = self.getter() + self.target = self.getter() # normalise False to None if spec is False: @@ -1620,17 +1582,17 @@ def __enter__(self): if autospec is True: autospec = original - if _is_instance_mock(target): + if _is_instance_mock(self.target): raise InvalidSpecError( f'Cannot autospec attr {self.attribute!r} as the patch ' f'target has already been mocked out. ' - f'[target={target!r}, attr={autospec!r}]') + f'[target={self.target!r}, attr={autospec!r}]') if _is_instance_mock(autospec): - target_name = getattr(target, '__name__', target) + target_name = getattr(self.target, '__name__', self.target) raise InvalidSpecError( f'Cannot autospec attr {self.attribute!r} from target ' f'{target_name!r} as it has already been mocked out. ' - f'[target={target!r}, attr={autospec!r}]') + f'[target={self.target!r}, attr={autospec!r}]') new = create_autospec(autospec, spec_set=spec_set, _name=self.attribute, **kwargs) @@ -1641,13 +1603,10 @@ def __enter__(self): new_attr = new - exit_stack = contextlib.ExitStack() - self._context = _PatchContext( - exit_stack=exit_stack, - is_local=is_local, - original=original, - target=target, - ) + self.temp_original = original + self.is_local = is_local + self._exit_stack = contextlib.ExitStack() + self.is_started = True try: setattr(self.target, self.attribute, new_attr) if self.attribute_name is not None: @@ -1655,7 +1614,7 @@ def __enter__(self): if self.new is DEFAULT: extra_args[self.attribute_name] = new for patching in self.additional_patchers: - arg = exit_stack.enter_context(patching) + arg = self._exit_stack.enter_context(patching) if patching.new is DEFAULT: extra_args.update(arg) return extra_args @@ -1681,8 +1640,12 @@ def __exit__(self, *exc_info): # needed for proxy objects like django settings setattr(self.target, self.attribute, self.temp_original) - exit_stack = self._context.exit_stack - self._context = None + self.is_started = False + exit_stack = self._exit_stack + del self._exit_stack + del self.is_local + del self.temp_original + del self.target return exit_stack.__exit__(*exc_info) From 7d73933cfc286029242435cdabb1e81864222fb1 Mon Sep 17 00:00:00 2001 From: Red4Ru Date: Tue, 12 Nov 2024 22:48:03 +0300 Subject: [PATCH 11/11] Leave as little changes in mock.py as possible --- Lib/unittest/mock.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 2ef0d309d36052..55cb4b1f6aff90 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1470,6 +1470,7 @@ def get_original(self): ) return original, local + def __enter__(self): """Perform the patch.""" if self.is_started: @@ -1494,7 +1495,7 @@ def __enter__(self): spec_set not in (True, None)): raise TypeError("Can't provide explicit spec_set *and* spec or autospec") - original, is_local = self.get_original() + original, local = self.get_original() if new is DEFAULT and autospec is None: inherit = False @@ -1604,7 +1605,7 @@ def __enter__(self): new_attr = new self.temp_original = original - self.is_local = is_local + self.is_local = local self._exit_stack = contextlib.ExitStack() self.is_started = True try: @@ -1640,12 +1641,12 @@ def __exit__(self, *exc_info): # needed for proxy objects like django settings setattr(self.target, self.attribute, self.temp_original) - self.is_started = False - exit_stack = self._exit_stack - del self._exit_stack - del self.is_local del self.temp_original + del self.is_local del self.target + exit_stack = self._exit_stack + del self._exit_stack + self.is_started = False return exit_stack.__exit__(*exc_info)