-
-
Notifications
You must be signed in to change notification settings - Fork 34.8k
bpo-35226: Fix equality for nested unittest.mock.call objects. #10555
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -977,46 +977,51 @@ def _mock_call(_mock_self, *args, **kwargs): | |
| self = _mock_self | ||
| self.called = True | ||
| self.call_count += 1 | ||
| _new_name = self._mock_new_name | ||
| _new_parent = self._mock_new_parent | ||
|
|
||
| # handle call_args | ||
| _call = _Call((args, kwargs), two=True) | ||
| self.call_args = _call | ||
| self.call_args_list.append(_call) | ||
| self.mock_calls.append(_Call(('', args, kwargs))) | ||
|
|
||
| seen = set() | ||
| skip_next_dot = _new_name == '()' | ||
|
|
||
| # initial stuff for method_calls: | ||
| do_method_calls = self._mock_parent is not None | ||
| name = self._mock_name | ||
| while _new_parent is not None: | ||
| this_mock_call = _Call((_new_name, args, kwargs)) | ||
| if _new_parent._mock_new_name: | ||
| dot = '.' | ||
| if skip_next_dot: | ||
| dot = '' | ||
| method_call_name = self._mock_name | ||
|
|
||
| skip_next_dot = False | ||
| if _new_parent._mock_new_name == '()': | ||
| skip_next_dot = True | ||
| # initial stuff for mock_calls: | ||
| mock_call_name = self._mock_new_name | ||
| is_a_call = mock_call_name == '()' | ||
| self.mock_calls.append(_Call(('', args, kwargs))) | ||
|
|
||
| _new_name = _new_parent._mock_new_name + dot + _new_name | ||
| # follow up the chain of mocks: | ||
| _new_parent = self._mock_new_parent | ||
| while _new_parent is not None: | ||
|
|
||
| # handle method_calls: | ||
| if do_method_calls: | ||
| if _new_name == name: | ||
| this_method_call = this_mock_call | ||
| else: | ||
| this_method_call = _Call((name, args, kwargs)) | ||
| _new_parent.method_calls.append(this_method_call) | ||
|
|
||
| _new_parent.method_calls.append(_Call((method_call_name, args, kwargs))) | ||
| do_method_calls = _new_parent._mock_parent is not None | ||
| if do_method_calls: | ||
| name = _new_parent._mock_name + '.' + name | ||
| method_call_name = _new_parent._mock_name + '.' + method_call_name | ||
|
|
||
| # handle mock_calls: | ||
| this_mock_call = _Call((mock_call_name, args, kwargs)) | ||
| _new_parent.mock_calls.append(this_mock_call) | ||
|
|
||
| if _new_parent._mock_new_name: | ||
| if is_a_call: | ||
| dot = '' | ||
| else: | ||
| dot = '.' | ||
| is_a_call = _new_parent._mock_new_name == '()' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this var used?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. indeed, I quickly overview it and it is used within the loop. |
||
| mock_call_name = _new_parent._mock_new_name + dot + mock_call_name | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. |
||
|
|
||
| # follow the parental chain: | ||
| _new_parent = _new_parent._mock_new_parent | ||
|
|
||
| # use ids here so as not to call __hash__ on the mocks | ||
| # check we're not in an infinite loop: | ||
| # ( use ids here so as not to call __hash__ on the mocks) | ||
| _new_parent_id = id(_new_parent) | ||
| if _new_parent_id in seen: | ||
| break | ||
|
|
@@ -2054,6 +2059,10 @@ def __eq__(self, other): | |
| else: | ||
| self_name, self_args, self_kwargs = self | ||
|
|
||
| if (getattr(self, 'parent', None) and getattr(other, 'parent', None) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You are using gettattr to ignore the value and then compare it. What if it has a parent but it evaluates Falsy? Might be better to just use if hasattr(other, 'parent') and self.parent != other.parent:
return FalseI think it reads easier
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The implementation was suggested in the bug discussion, @voidspace can make a call here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think @mariocj89 has a point. On
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Surprising indeed! Look at this: >>> from unittest.mock import Mock
>>> m = Mock()
>>> m.abc.cde()
<Mock name='mock.abc.cde()' id='139944355306408'>
>>> m.mock_calls
[call.abc.cde()]
>>> print(m.mock_calls[0].parent)
NoneSeems parent is not set at all, which is causing a lot of tests to fail with my proposal :/
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow. That seems like a bug too, let me add some cases and see about a fix.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (just as a note, we can follow this up indeed in another issue as I am not sure about the fix, seems we are not passing the parent here): https://github.com/python/cpython/blob/master/Lib/unittest/mock.py#L986)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid it's intrinsically linked to this issue,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where there's a single call there's no parent call. So having parent not set in that example looks correct. |
||
| and self.parent != other.parent): | ||
| return False | ||
|
|
||
| other_name = '' | ||
| if len_other == 0: | ||
| other_args, other_kwargs = (), {} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,6 +269,22 @@ def test_extended_call(self): | |
| self.assertEqual(mock.mock_calls, last_call.call_list()) | ||
|
|
||
|
|
||
| def test_extended_not_equal(self): | ||
| a = call(x=1).foo | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it works due to the implementation, but I'd also validate this works recursively for future changes to the module with a = call(x=1).foo().bar
b = call(x=2).foo().bar |
||
| b = call(x=2).foo | ||
| self.assertEqual(a, a) | ||
| self.assertEqual(b, b) | ||
| self.assertNotEqual(a, b) | ||
|
|
||
|
|
||
| def test_nested_calls_not_equal(self): | ||
| a = call(x=1).foo().bar | ||
| b = call(x=2).foo().bar | ||
| self.assertEqual(a, a) | ||
| self.assertEqual(b, b) | ||
| self.assertNotEqual(a, b) | ||
|
|
||
|
|
||
| def test_call_list(self): | ||
| mock = MagicMock() | ||
| mock(1) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Recursively check arguments when testing for equality of | ||
| :class:`unittest.mock.call` objects and add note that tracking of parameters | ||
| used to create ancestors of mocks in ``mock_calls`` is not possible. |
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.
They are recorded, just as different calls.. :/ I think this is going to be really confusing for the reader.
Uh oh!
There was an error while loading. Please reload this page.
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.
The parameters are absolutely not recorded in
mock_calls, this is the core problem that isn't solvable.Uh oh!
There was an error while loading. Please reload this page.
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.
They are! just not in the last one (each call records only its own parameters), look:
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'm not sure we're getting anywhere constructive with this, so let's wait for Michael to take a look.
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.
...except when
get_endpointis called more than once.