-
-
Notifications
You must be signed in to change notification settings - Fork 30.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-104745: Limit starting a patcher more than once without stopping it #126649
Conversation
Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object.
Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst
Outdated
Show resolved
Hide resolved
Lib/unittest/mock.py
Outdated
self.temp_original = original | ||
self.is_local = local | ||
self._exit_stack = contextlib.ExitStack() | ||
self._started_context = _PatchStartedContext( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the naming self._context
would be sufficient.
Lib/unittest/mock.py
Outdated
exit_stack=contextlib.ExitStack(), | ||
is_local=is_local, | ||
target=self.getter(), | ||
temp_original=original, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This attribute can be called original
since we are anyway wrapping it in a new object.
Lib/unittest/mock.py
Outdated
@@ -1320,6 +1320,14 @@ def _check_spec_arg_typos(kwargs_to_check): | |||
) | |||
|
|||
|
|||
class _PatchStartedContext(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reduce the memory footprint, how about using a namedtuple? I would also call it only class _PatchContext
because it does not really give more information to have the "Started" IMO.
Lib/unittest/mock.py
Outdated
try: | ||
setattr(self.target, self.attribute, new_attr) | ||
if self.attribute_name is not None: | ||
extra_args = {} | ||
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arg = self._started_context.exit_stack.enter_context(patching) | |
arg = exit_stack.enter_context(patching) |
Lib/unittest/mock.py
Outdated
self.temp_original = original | ||
self.is_local = local | ||
self._exit_stack = contextlib.ExitStack() | ||
self._started_context = _PatchStartedContext( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self._started_context = _PatchStartedContext( | |
exit_stack = contextlib.ExitStack() | |
self._started_context = _PatchStartedContext( |
Lib/unittest/mock.py
Outdated
self.is_local = local | ||
self._exit_stack = contextlib.ExitStack() | ||
self._started_context = _PatchStartedContext( | ||
exit_stack=contextlib.ExitStack(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exit_stack=contextlib.ExitStack(), | |
exit_stack=exit_stack, |
Lib/unittest/mock.py
Outdated
@@ -1469,13 +1478,31 @@ def get_original(self): | |||
) | |||
return original, local | |||
|
|||
@property | |||
def is_started(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, those were writable attributes and now it's no more the case. Could there be some code in the wild assuming so? (for instance pytest which makes quite hacky things, though I don't know if they do hacky things with this specific part of CPython).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess so... I have committed property setters just in case. Will write tests if needed when we figure out what to do with temp_original
: do we preserve it and somehow deprecate or anything else
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed them. A bit ugly but anyway these setters exist not for an intended usecase but for backwards compatibility only
Misc/NEWS.d/next/Library/2024-11-10-18-14-51.gh-issue-104745.zAa5Ke.rst
Outdated
Show resolved
Hide resolved
…Aa5Ke.rst Co-authored-by: Peter Bierma <[email protected]>
Co-authored-by: Bénédikt Tran <[email protected]>
I rechecked and get that `unittest.mock.patch.dict` is not involved. Also deleted a trailing comma
Lib/unittest/mock.py
Outdated
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, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Urgh, I forgot that you cannot change the value of namedtuples. Ok, my suggestion using namedtuples was wrong. To reduce memory footprint, we can use __slots__
in a regular class instead like you had before. That way, we save an from collections import namedtuple
as well and simplify the property's setter. WDYT? (again sorry for this bad suggestion).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain why we need the whole _PatchContext
thing?
What couldn't be achieved by just having a self.is_started
boolean?
Consistency, at the first place. Why add another field for that while we already have all the bunch of others which exist only when |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like we could just add an is_started
attribute, which is set to True
when the patcher is started and False
when it is stopped.
I'd prefer to see that smaller, simpler change, tbh.
But, great to see the new tests in this area!
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
I think that putting these fields in |
What you propose introduces more complexity than is required to fix the bug, and to a piece of code that is already very complex. The change I think you should make amounts to 3 lines, if I understand correctly, and I would prefer that change to the one currently in this PR. |
Well, if we should minimize impact on existing code, I agree with this suggestion |
@@ -1098,7 +1146,7 @@ def test_new_callable_patch(self): | |||
|
|||
self.assertIsNot(m1, m2) | |||
for mock in m1, m2: | |||
self.assertNotCallable(m1) | |||
self.assertNotCallable(mock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice spot!
…ping it (pythonGH-126649) Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- (cherry picked from commit 1e40c5b) Co-authored-by: Red4Ru <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
GH-126772 is a backport of this pull request to the 3.13 branch. |
…ping it (pythonGH-126649) Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- (cherry picked from commit 1e40c5b) Co-authored-by: Red4Ru <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
GH-126773 is a backport of this pull request to the 3.12 branch. |
…pping it (GH-126649) (#126773) gh-104745: Limit starting a patcher more than once without stopping it (GH-126649) Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- (cherry picked from commit 1e40c5b) Co-authored-by: Red4Ru <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
…pping it (GH-126649) (#126772) gh-104745: Limit starting a patcher more than once without stopping it (GH-126649) Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- (cherry picked from commit 1e40c5b) Co-authored-by: Red4Ru <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
Previously, this would cause an
AttributeError
if the patch stopped more than once after this, and would also disrupt the original patched object.