Skip to content

gh-94478: Fix bug unsafe not set when patching #94479

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

LakmalCaldera
Copy link

@LakmalCaldera LakmalCaldera commented Jul 1, 2022

Even though mocked objects are created by setting unsafe=True the
internal attribute _mock_unsafe on the actual mocked instances were
not properly set.
The test case has been updated to capture this logic.

Even though mocked objects are created by setting `unsafe=True` the
internal attribute `_mock_unsafe` on the actual mocked instances were
not properly set. The test case has been updated to capture this logic.
@LakmalCaldera LakmalCaldera requested a review from cjw296 as a code owner July 1, 2022 09:31
@ghost
Copy link

ghost commented Jul 1, 2022

All commit authors signed the Contributor License Agreement.
CLA signed

@bedevere-bot
Copy link

Most changes to Python require a NEWS entry.

Please add it using the blurb_it web app or the blurb command-line tool.

@LakmalCaldera
Copy link
Author

Hi @vabr-g
could you help me review this PR.
Thanks.

@vabr-g
Copy link
Contributor

vabr-g commented Jul 1, 2022

Thanks for investigating and fixing this bug!
I don't have write access to this repo, so you probably need a review from someone who does. Also one of the automated checks seems red, but I'm not sure what it means.

@arhadthedev
Copy link
Member

you probably need a review from someone who does

Triagers and core developers sort out the unread notification queue in order of arrival so there is no need to explicitly call anybody. However, they do it in their free time so here is a natural feedback gathering period up to a month to give everybody a chance to participate in the PR.

Also one of the automated checks seems red, but I'm not sure what it means.

It's a known problem in a process of fixing: #94447 (comment)

@bedevere-bot
Copy link

Most changes to Python require a NEWS entry.

Please add it using the blurb_it web app or the blurb command-line tool.

@@ -1282,6 +1282,8 @@ def __init__(
f'Cannot spec attr {attribute!r} as the spec_set '
f'target has already been mocked out. [spec_set={spec_set!r}]')

if unsafe:
kwargs.update({'unsafe': unsafe})
Copy link
Contributor

Choose a reason for hiding this comment

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

why not

kwargs['unsafe'] = unsafe

?

Copy link
Author

Choose a reason for hiding this comment

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

Hi @gst,
This was done to emulate how passing unsafe via kwargs was originally designed.
That is, if unsafe is not explicitly set, kwargs don't default unsafe to False, it's just not present in kwargs.
There are also several test cases already written where, if unsafe is not set then kwargs is expected not to contain the unsafe key.
Particularly in this bug fix, I didn't want to introduce any behavioral changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

well I think both of us got their points actually :)

def __init__(
            self, getter, attribute, new, spec, create,
            spec_set, autospec, new_callable, kwargs, *, unsafe=False
        ):

given there is none annotation to the kwargs we can, eventually, do anything with it.
might be it seems to be a regular dict/mapping though.

and using regular dict[index] = value looks the way to do it ?

Copy link
Author

Choose a reason for hiding this comment

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

I agree, the problem as you are pointing out is in the initializer where unsafe is pulled out and made into a named argument.
My original thought was to just change the initializer and remove that unsafe from it. That is probably the best solution and how it was originally.

But then I realized all the public/exposed APIs like for example:

def patch(
        target, new=DEFAULT, spec=None, create=False,
        spec_set=None, autospec=None, new_callable=None, *, unsafe=False, **kwargs
):
def _patch_object(
        target, attribute, new=DEFAULT, spec=None,
        create=False, spec_set=None, autospec=None,
        new_callable=None, *, unsafe=False, **kwargs
    ):

have unsafe also made into an independent named argument. Since it is these public/exposed APIs that get called **kwargs at this point is always a dict and that get passed internally everywhere in the mock module.

It is strange why kwargs is not checking for None in the _patch initializer(so one can test _patch class independently), and I see that some initializer calls go into great extend to ensure that an empty dict is passed into the initializer. Like the following piece of code that can be found in the _patch_multiple function.

this_patcher = _patch(
    getter, attribute, new, spec, create, spec_set,
    autospec, new_callable, {}
)

So as it is right now, the initializer for _patch class is some what coupled with the calling public API in the sense that whoever calls it has to always pass a dict for kwargs. It's a little unsafe but could just have been the design from the beginning.

So if we always get a dict for kwargs then why not use,
kwargs['unsafe'] = unsafe instead of kwargs.update({'unsafe': unsafe})?
You are right, it's probably more efficient to use dict[index] = value style here.
However the if conditional statement that checks if unsafe is set to True will need to stay. Otherwise we will change the behavior of kwargs and that break some testcases written in other modules.
Would that be okay?

with patch(
f'{__name__}.Something.meth', unsafe=True, autospect=True
) as patched_object:
self.assertEqual(patched_object._mock_unsafe, True)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of asserting that the internal attribute is set I guess it's good to test the actual behavior. The other tests above check for AttributeError. Maybe this can just call the patched object and try using an incorrect assert helper like assret_called_once.

from unittest.mock import patch

class Something:

    def meth(self):
        pass


with patch(f"{__name__}.Something.meth", unsafe=True, autospect=True) as patched_object:
    patched_object()
    patched_object.assret_called_once()
  • In master branch
./python bpo94497.py
Traceback (most recent call last):
  File "/home/karthikeyan/stuff/python/cpython/bpo94497.py", line 11, in <module>
    patched_object.assret_called_once()
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/karthikeyan/stuff/python/cpython/Lib/unittest/mock.py", line 643, in __getattr__
    raise AttributeError(
AttributeError: 'assret_called_once' is not a valid assertion. Use a spec for the mock if 'assret_called_once' is meant to be an attribute.. Did you mean: 'assert_called_once'?
  • With patch no AttributeError is raised since like the behavior before Python 3.10

Copy link
Author

Choose a reason for hiding this comment

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

Thanks @tirkarthi.
You are absolutely right.
I was so focused on the bug, I forgot the testcase was to test the behavior for spelling mistakes, which is rightfully what I should have been testing from the beginning.
I uploaded a new commit fix it.

I feel like this commit doesn't require an entry in Misc/NEWS.d/next/, but I'm not sure how to set a "skip news" label on the commit.

Updated testcase to actually test the behavior of the unsafe logic
instead of just checking if the private attribute `_mock_unsafe` was
set.
@bedevere-bot
Copy link

Most changes to Python require a NEWS entry.

Please add it using the blurb_it web app or the blurb command-line tool.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants