-
Notifications
You must be signed in to change notification settings - Fork 41
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
Add the awaited attribute to CoroutineMock. #67
Conversation
312a6a8
to
4321ab8
Compare
@Martiusweb despite the incompatibility with 3.4 (which I will fix) what do you think? Do you think it makes sense to always increase |
test/test_mock.py
Outdated
@@ -283,6 +283,23 @@ def test_called_CoroutineMock_returns_MagicMock(self): | |||
mock = asynctest.mock.CoroutineMock() | |||
self.assertIsInstance(run_coroutine(mock()), asynctest.mock.MagicMock) | |||
|
|||
def test_awaited_CoroutineMock_sets_awaited(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.
Can you add more tests:
- when result is awaitable?
- when awaiting the result raises an exception?
Yes, this is something that should be implemented.
which will always be ok. I think it should be renamed in awaited_event (or both attributes can be provided, awaited as a boolean, awaited_event as an Event object). About the exception: the way you implemented it is right. Since you check that the returned result is awaitable, the exception will be raised as the result of the awaited call, not because the object is not awaitable. Thus, I think that we can safely say that the CoroutineMock has been awaited even if an exception is raised. |
I cannot share the entire project, but the use case is for tests whose behavior require to do something at very specific steps of execution flow. E.g. when a test involves 2 coroutines (client and server) and you want to send some data after server authorizes client. In that test one of server's methods is wrapped with a mock and then test awaits that wrapped mock to be awaited. That is a signal that test can proceed.
What if we replace asyncio.Event with a custom subclass: class Event(asyncio.Event):
def __bool__(self):
return self.is_set()
The reason I did it this way was to make it closer to |
I recently merged autospec support but it seems that I forgot a few things (handling the mock attributes/delegated properties correctly). edit: I'll work on this next to unblock this PR. |
Do you know how to address it? I was going to take a look at this next.
I don't think this is necessary: it seems to be useful only for "public" attributes. |
Your last commit is the right way to fix it. I thought it was caused by how _mock_add_spec is overridden, but I forgot that adding methods/public attributes requires adding it to create_autospec(). I'll add a bit of documentation and tests for the autospec case and merge it. |
Since I don't have the right to edit your PR, I pushed my changes in the branch GreatFruitOmsk-coroutine-wait:
Can you review them so we can merge it all? It will be part of the 0.12 release, that I will release once I'm ready to merge #73 and #74. |
Another thing I just realized is that |
The awaited can be used for boolean comparison. Refs Martiusweb#64
b785776
to
93852fc
Compare
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.
Your porposal is a good idea, but I think that there are things that need to be changed.
return (yield from self.wait_for(predicate)) | ||
|
||
@asyncio.coroutine | ||
def wait_for(self, predicate): |
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.
_AwaitEvent.wait_for() and AwaitEvent.notify() should be private (prefixed with "")
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 github's parser ate the prefix, what did you mean _
?
I wanted to keep wait_for
public for a user to introduce more complex conditions (e.g. mock is awaited after being called with certain arguments).
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.
Interresting use case. I didn't think of something like this.
I think that we can add this example in the documentation, and tell that the predicate is only checked when the mock is awaited. I don't want users to have the feeling that it can magically awake after a random event.
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.
In fact, I was thinking about this:
def predicate(mock):
return mock.await_count > 0 and m.call_args == (("foo", ), {})
t1 = asyncio.ensure_future(mock("foo"))
t2 = mock("bar") # not yet scheduled
mock.awaited.wait_for(predicate) # will hang forever: mock.call_args is now based on the last call.
The problem is that the predicate is based on the mock (ie coroutine function), not the coroutine that will be awaited itself.
I'm affraid that this will be confusing as it's not possible to wait_for() a given instance of the coroutine function to be awaited. Maybe we should keep this private and let users write their own logic when they want to do complex things (one can use the wraps
argument to customize th behavior of the 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.
You should use call_args_list
.
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 won’t be 100% accurate (you can await in different order) but could be sufficient for some tests.
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.
In other hand for those tests skip
would be sufficient as well. Perhaps we should add await_args
/ await_args_list
?
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 that the use case for checking which exact call has been awaited makes sense, but I'm not conviced with generic/public wait_for() method yet.
I think that we should merge this PR with a private _wait_for() as it's already very useful like this. Then we can work on a new PR to implement wait_call() and wait_next_call() or add optional parameters matching in wait() and wait_next().
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.
When I was working on that, ability to await a specific call was only one of use cases I thought of. Another was an ability to await an external consition like setting mock’s attribute to a specific value. Or more general: any modification of mock’s state related to await. I also thought of ability to await external conditions, but couldn’t think of a practical example.
I would opt for having wait_for
public as it seems in line with the naming of async primitives. People who would confuse it would probably have problems with wait
and wait_next
as well. This an advanced method, but it is possible to mark it as such with docs.
What do you say if we keep it public via naming, but exclude from the documentation for now?
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.
Please check GreatFruitOmsk@3839b15, it introduces an independent list of "await args" and among other addresses the issue in your example for wait_for
.
asynctest/mock.py
Outdated
Wait for await to be called at least ``skip`` number of times. | ||
""" | ||
def predicate(mock): | ||
return mock.await_count > skip |
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 default value of skip should be 1 to be consistent with the meaning of the parameter (and I'd call it "min_await_count" which is more meaningful).
mock = CoroutineMock()
await mock.wait(skip=0) # should not block according to doc because it has been called /at least/ 0 times
await mock.wait(skip=1) # should block until awaited once
Thus, the condition must be mock.await_count >= min_await_count
(greater or equal is consistent with "at least").
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.
There is inconsistency indeed, but I like the skip
argument as it appears more to the point: you wait for a call skipping a given number of other calls. Perhaps I could rephrase the doc better.
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.
Yes, it sounds consistent like this:
mock.awaited.wait() # wait the first await
mock.awaited.wait(skip=1) # don't resume for the first await
cool :)
await_count = self._mock.await_count | ||
|
||
def predicate(mock): | ||
return mock.await_count > await_count + skip |
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.
Same as for wait(), to be consistent I'd call the argument min_next_wait_count
, set 1 as default value, and use >=
in the predicate.
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 find the skip
name more to the point: wait for await skipping # awaits.
Please take a look at the rephrased docstrings. If they are still hard to comprehend, I'll change the name.
asynctest/mock.py
Outdated
@@ -462,16 +510,16 @@ def proxy(): | |||
try: | |||
return (yield from result) | |||
finally: | |||
yield from _mock_self.awaited.notify() |
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.
calling notify() before incrementing await_count is incorrect with this implementation or the changes I suggest:
mock = CoroutineMock() # await_count is 0
t = ensure_future(mock())
await mock.awaited.wait() # concurrently, notify is called, but await_count is 0 when the , thus the previous condition is not met, the value is incremented, but never notified again.
notify() must be called after the counter has been incremented.
asynctest/mock.py
Outdated
else: | ||
@asyncio.coroutine | ||
def proxy(): | ||
try: | ||
return result | ||
finally: | ||
yield from _mock_self.awaited.notify() |
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.
same as above.
test/test_mock.py
Outdated
self.assertEqual(1, mock.a_coroutine.await_count) | ||
|
||
def test_awaited_wait(self): | ||
loop = asyncio.new_event_loop() | ||
with replace_loop(loop): |
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 don't think it is worth changing run_coroutine, which is just a very small helper.
I'd simply use the normal APIs of asyncio and unittest:
loop = asyncio.new_event_loop()
self.addCleanup(loop.close)
mock = asynctest.mock.CoroutineMock()
t = asyncio.ensure_future(mock.awaited.wait())
loop.run_until_complete(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.
That's not enough. The new loop is also need to be set and unset (like run_coroutine
does) before mock is created, otherwise asyncio.Condition
will capture the current loop, not the loop that is running it.
Don't you think this utility function might be convenient as more tests are added? Perhaps it can be further extended (by another PR) into function shared between asynctest and utils?
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.
You're right. We can keep it that way for now, or move these tests in a class extending asynctest.TestCase
, since one of its purpose is to handle the loop for each test case.
Allow to skip a given number of awaits and wait for the next await. Refs Martiusweb#64
When condition is created it automatically captures current event loop. Since user should be able to create mock before setting the event loop, creation of the Condition should be delayed before it's needed: - One of wait methods is called - CoroutineMock result is being awaited Tests are simplified to reuse asynctest.TestCase. Addition of replace_loop is reverted being unnecessary. Refs Martiusweb#64
Hi, Sorry for the delay. I was thinking of something else: Basically, it would be something like this:
|
Please take a look at the commit I linked. It is not part of this PR, but I’d like to include it.
In that case you likely don’t care about executing the coroutine and plain side_effect would do. |
I added a bunch of comments, but this is very cool, let's add this commit in the PR! I changed a few cosmetic things and rebased the PR on top of master (I pushed a change to fix something in python 3.7). Once the PR is updated with the changes of https://github.com/Martiusweb/asynctest/tree/GreatFruitOmsk-coroutine-wait and the commit for await args is ready, we can merge. Thank you very much! |
There still work to do to fix Python 3.7 with PYTHONASYNCIODEBUG=1, I'll ignore this for this PR and work on it later. It's a bug in the nightly: |
Finally done! Thank you very much for this awesome contribution and your patience. |
Just pushed a follow up ;) Please let me know if anything needs to be changed for other PRs I opened. |
Refs #64.