Skip to content
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

Merged
merged 11 commits into from
Nov 13, 2024

Conversation

Red4Ru
Copy link
Contributor

@Red4Ru Red4Ru commented Nov 10, 2024

Previously, this would cause an AttributeError if the patch stopped more than once after this, and would also disrupt the original patched object.

Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object.
@Red4Ru Red4Ru requested a review from cjw296 as a code owner November 10, 2024 16:32
Copy link

cpython-cla-bot bot commented Nov 10, 2024

All commit authors signed the Contributor License Agreement.
CLA signed

self.temp_original = original
self.is_local = local
self._exit_stack = contextlib.ExitStack()
self._started_context = _PatchStartedContext(
Copy link
Contributor

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.

exit_stack=contextlib.ExitStack(),
is_local=is_local,
target=self.getter(),
temp_original=original,
Copy link
Contributor

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.

@@ -1320,6 +1320,14 @@ def _check_spec_arg_typos(kwargs_to_check):
)


class _PatchStartedContext(object):
Copy link
Contributor

@picnixz picnixz Nov 10, 2024

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 Show resolved Hide resolved
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
arg = self._started_context.exit_stack.enter_context(patching)
arg = exit_stack.enter_context(patching)

self.temp_original = original
self.is_local = local
self._exit_stack = contextlib.ExitStack()
self._started_context = _PatchStartedContext(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._started_context = _PatchStartedContext(
exit_stack = contextlib.ExitStack()
self._started_context = _PatchStartedContext(

self.is_local = local
self._exit_stack = contextlib.ExitStack()
self._started_context = _PatchStartedContext(
exit_stack=contextlib.ExitStack(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
exit_stack=contextlib.ExitStack(),
exit_stack=exit_stack,

@@ -1469,13 +1478,31 @@ def get_original(self):
)
return original, local

@property
def is_started(self):
Copy link
Contributor

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).

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Lib/unittest/mock.py Outdated Show resolved Hide resolved
Lib/unittest/mock.py Outdated Show resolved Hide resolved
Red4Ru and others added 4 commits November 10, 2024 23:59
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
Comment on lines 1494 to 1500
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,
)
Copy link
Contributor

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).

Copy link
Contributor

@cjw296 cjw296 left a 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?

@Red4Ru
Copy link
Contributor Author

Red4Ru commented Nov 11, 2024

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 _patch is started? (They literally were deleted in __exit__)
At first, I even called it _PatchStartedContext to make this whole purpose more obvious

Copy link
Contributor

@cjw296 cjw296 left a 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!

@bedevere-app
Copy link

bedevere-app bot commented Nov 11, 2024

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 have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@Red4Ru
Copy link
Contributor Author

Red4Ru commented Nov 11, 2024

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.

I think that putting these fields in _context and calculating is_started based on it makes the code easier to maintain
What are the advantages of leaving it as it is by adding the is_started field to _patch?

@cjw296
Copy link
Contributor

cjw296 commented Nov 11, 2024

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.

@Red4Ru
Copy link
Contributor Author

Red4Ru commented Nov 12, 2024

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
But if someone wants to refactor this later, penultimate commit might be useful

@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice spot!

Lib/unittest/mock.py Outdated Show resolved Hide resolved
Lib/unittest/mock.py Outdated Show resolved Hide resolved
Lib/unittest/mock.py Show resolved Hide resolved
@cjw296 cjw296 added needs backport to 3.12 bug and security fixes needs backport to 3.13 bugs and security fixes labels Nov 13, 2024
@cjw296 cjw296 merged commit 1e40c5b into python:main Nov 13, 2024
43 of 44 checks passed
@miss-islington-app
Copy link

Thanks @Red4Ru for the PR, and @cjw296 for merging it 🌮🎉.. I'm working now to backport this PR to: 3.12, 3.13.
🐍🍒⛏🤖

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Nov 13, 2024
…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]>
@bedevere-app
Copy link

bedevere-app bot commented Nov 13, 2024

GH-126772 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.13 bugs and security fixes label Nov 13, 2024
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Nov 13, 2024
…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]>
@bedevere-app
Copy link

bedevere-app bot commented Nov 13, 2024

GH-126773 is a backport of this pull request to the 3.12 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.12 bug and security fixes label Nov 13, 2024
cjw296 pushed a commit that referenced this pull request Nov 13, 2024
…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]>
cjw296 pushed a commit that referenced this pull request Nov 13, 2024
…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]>
@Red4Ru Red4Ru deleted the fix-issue-104745 branch November 13, 2024 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants