◐ Shell
reader mode source ↗
Skip to content

[WIP] bpo-17013: Implement WaitableMock to create Mock objects that can wait until called#12818

Closed
tirkarthi wants to merge 8 commits into
python:masterfrom
tirkarthi:bpo17013
Closed

[WIP] bpo-17013: Implement WaitableMock to create Mock objects that can wait until called#12818
tirkarthi wants to merge 8 commits into
python:masterfrom
tirkarthi:bpo17013

Conversation

@tirkarthi

@tirkarthi tirkarthi commented Apr 13, 2019

Copy link
Copy Markdown
Member

This is an initial implementation with preliminary docs and tests to see if it's worthy enough of addition.

Some notes :

  • Currently, it doesn't support waiting for calls with keyword arguments.
  • It seems the calls to event object are also recorded in mock_calls . I think these should be filtered out or maybe I am doing something wrong.
  • There is per call event object and per mock object event to track if mock was called with given arguments and to store just mock is called or not. Is there a better way to store this?

cc : @mariocj89

https://bugs.python.org/issue17013

wait_until_called and wait_until_called_with are supported. This
stores a dictionary with args as key and corresponding event object
which is set once there is a call to it. In case of call not present
we only want to know if the function was called and hence a per mock
event object is also present which is set for no args case.

@mariocj89 mariocj89 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hide comment

I think this is going to be really useful!

@tirkarthi

Copy link
Copy Markdown
Member Author

Another problem is that inside wait_until_called_with I create event objects and calling self._event_class and so on seems to record the calls to the Mock object too. But this is not a problem with wait_until_called where not such attribute access is done. I guess there is some flaw in my approach of attribute access with self leading to this.

import threading
import time
from unittest.mock import WaitableMock, patch, call

def call_after_sometime(func, *args, delay=1):
    time.sleep(delay)
    func(*args)

def foo(*args):
    pass

def bar(*args):
    pass

with patch('__main__.foo', WaitableMock(event_class=threading.Event)):
    with patch('__main__.bar', WaitableMock(event_class=threading.Event)):
        threading.Thread(target=call_after_sometime, args=(foo, 1), kwargs={'delay': 1}).start()
        threading.Thread(target=call_after_sometime, args=(bar, 2), kwargs={'delay': 1}).start()
        print("bar called with 1 ", bar.wait_until_called_with(2, timeout=2))
        print(bar.mock_calls)
        bar.assert_called_once_with(2)
        print("foo called with 1 ", foo.wait_until_called_with(timeout=2))
        print(foo.mock_calls)
bar called with 1  <WaitableMock name='mock._event_class().is_set()' id='4461405584'>
[call.expected_calls.__contains__((2,)),
 call._event_class(),
 call._event_class().is_set(),
 call._event_class().is_set().__bool__(),
 call._event_class().is_set().__str__()]
Traceback (most recent call last):
  File "../backups/bpo17013_mock_1.py", line 25, in <module>
    bar.assert_called_once_with(2)
  File "/Users/karthikeyansingaravelan/stuff/python/cpython/Lib/unittest/mock.py", line 852, in assert_called_once_with
    raise AssertionError(msg)
AssertionError: Expected 'mock' to be called once. Called 0 times.
Calls: [call.expected_calls.__contains__((2,)),
 call._event_class(),
 call._event_class().is_set(),
 call._event_class().is_set().__bool__(),
 call._event_class().is_set().__str__()].

For foo it would work fine :

foo called with 1  True
[call(1)]

calls to foo return true and correct call objects. Meanwhile calls to bar with wait_until_called_with cause irrelevant calls to be recorded. Is there something I am missing or some setup in configuring the mock that I have missed?

I think there is a flaw that I have added time.sleep in my examples and calls are made misleading me and perhaps the mock object never waits for wait_until_called_with calls and the assertion error is telling me the same? If I add sleep it works fine.

@tirkarthi

Copy link
Copy Markdown
Member Author

Regarding my above comment on irrelevant calls. I got bitten by mock's flexibility :)

I was using self.expected_calls in the for loop that should be self._expected_class and self._event_class without storing the event class thus for attributes not found mock gave me set of WaitableMock. I have fixed them and now mock_calls only has the actual calls made. I added an assertion for the same too.

@bedevere-bot

Copy link
Copy Markdown

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.

@tirkarthi

Copy link
Copy Markdown
Member Author

IMHO timeout must not have a default, so what about putting the timeout as the first position? I heard that a PEP 570 has been merged and it allows to write "def wait_until_called_with(self, timeout, /, *args, **kwargs):" which accepts timeout in kwargs ;-)

@vstinner This slightly makes the API confusing to me . When I need to check if the mock is called with ('a', 'b') under timeout of 1.0 then I would do mock.wait_until_called_with(1.0, 'a', 'b') . Here 1.0 is timeout passed as positional only argument but it reads like wait until mock is called with (1.0, 'a', 'b') . This should be modeled on API like mock.assert_called_with with *args and **kwargs along with timeout. If the user has a timeout parameter and also wants custom timeout this can lead to more confusion like something.method_1.wait_until_called_with(1, timeout=0.1).

Maybe just skip timeout in this case and ask the user to pass timeout to constructor and use it here? But that would mean that wait_until_called can custom timeout meanwhile this couldn't so remove that too. I don't have a good answer to where the trade-off should be made on passing it per call or in the constructor which I hope was the point @mariocj89 was also making to just make this constructor field despite losing some flexibility.

You must add **kwargs to mimick Mock.assert_called_with().

Yes, currently I have used args as the dictionary key there should be a way to have a unique key combination of args and kwargs to store the corresponding event object.

@tirkarthi tirkarthi changed the title bpo-17013: Implement WaitableMock to create Mock objects that can wait until called Apr 15, 2019
@csabella csabella requested a review from vstinner June 1, 2019 15:17
@mariocj89

mariocj89 commented Sep 13, 2019 via email

Copy link
Copy Markdown
Contributor

@tirkarthi

Copy link
Copy Markdown
Member Author

Closing in favor of #16094 by Mario. Thanks all for the suggestions on the API.

@tirkarthi tirkarthi closed this Oct 19, 2019
@pitrou pitrou mentioned this pull request Apr 10, 2022
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