Uh oh!
There was an error while loading. Please reload this page.
Unify timeout behaviour - #463
Conversation
| pass | ||
| UNSET = UnsetType() |
There was a problem hiding this comment.
Had to put this here instead of the model package due to a circular dependency.
There was a problem hiding this comment.
config.py is probably a nicer home for this constant anyway. :)
| from .utils import get_ca_bundle_from_env, get_logger | ||
| class UnsetType: |
There was a problem hiding this comment.
Unfortunately can't use this pattern
UnsetType=NewType('UnsetType', object)
UNSET=UnsetType(object())because NewType does not allow isinstance() on it, and it was needed otherwise mypy was complaining in TimeoutConfig.
There was a problem hiding this comment.
Yup, well done here.
From Python 3.8, typing.Literal would allow to solve this quite elegantly:
importtypingUNSET=object()
MyCustomType=typing.Union[bool, typing.Literal[UNSET]]Not sure that there'll be a backport to 3.6 and 3.7 unfortunately, but just thought it was interesting to share. :)
There was a problem hiding this comment.
It's available in the typing_extensions package https://github.com/python/typing/tree/master/typing_extensions
But I guess it's not worth adding a new dependency just for this (since we have the class workaround anyway).
There was a problem hiding this comment.
@florimondmanca I think typing.Literal is not valid in this usage. UNSET is not a Literal.
| timeout = timeout if timeout is not UNSET else self.timeout | ||
| connection = await self.acquire_connection( | ||
| origin=request.url.origin, timeout=timeout |
There was a problem hiding this comment.
Slight change from the previous code, now we use the provided timeout for acquiring the connection too.
florimondmanca
left a comment
There was a problem hiding this comment.
Looking good!
Need to accept that we'll now be having UNSETs floating around everywhere, but overall I think this is on a good path.
I left a couple of comments and suggestions. Would be good to have others' opinions too. :)
| self.connect_timeout = timeout.connect_timeout | ||
| self.read_timeout = timeout.read_timeout | ||
| self.write_timeout = timeout.write_timeout | ||
| elif isinstance(timeout, UnsetType): |
There was a problem hiding this comment.
Is there any reason (maybe type hints-related?) we can't use elif timeout is UNSET here?
There was a problem hiding this comment.
I tried that first but mypy was complaining, I will reproduce the actual error once I get home, maybe there is something I missed.
There was a problem hiding this comment.
This is the mypy error:
httpx/config.py:262: error: Incompatible types in assignment (expression has type "Union[float, UnsetType]", variable has type "Optional[float]")
httpx/config.py:263: error: Incompatible types in assignment (expression has type "Union[float, UnsetType]", variable has type "Optional[float]")
httpx/config.py:264: error: Incompatible types in assignment (expression has type "Union[float, UnsetType]", variable has type "Optional[float]")
The weird thing is that it complains about these lines:
else:
self.connect_timeout = timeout
self.read_timeout = timeout
self.write_timeout = timeout
There was a problem hiding this comment.
emm, this is because you declare the type of timeout is TimeoutTypes(in __init__ method). It's a Union which includes UnsetType. And you declare the type of self.connect_timeout is typing.Optional[float] in line 241. So if you use elif timeout is UNSET, any instance of UnsetType will enter the else branch.
Ref: https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| async def test_async_client_timeout_default(server, backend): | ||
| async with httpx.AsyncClient(backend=backend) as client: | ||
| assert client.dispatch.timeout == httpx.config.UNSET |
There was a problem hiding this comment.
So, maybe this is an odd behavior. Shouldn't we expect accessing the timeout config at any point in the chain to return a definite TimeoutConfig object? cc @tomchristie
| async def test_async_client_timeout_disabled(server, backend): | ||
| async with httpx.AsyncClient(backend=backend, timeout=None) as client: | ||
| assert client.dispatch.timeout is None |
There was a problem hiding this comment.
Ideally, to fully test this we should make a request to the /slow_response endpoint of the server with a long delay (e.g. 100ms), surround the call with a "raise a timeout after x seconds" (where e.g. x = 50ms) instruction, and ensure that the timeout error is raised.
The "raise timeout after x seconds" instruction depends on the underlying async library, but we can add an util in tests/concurrency.py (single-dispatch stuff not listed here):
importasyncioasyncdeftimeout_after(delay, coroutine):
try:
awaitasyncio.wait(coroutine, timeout=delay)
exceptasyncio.TimeoutError:
raiseTimeoutErrorimporttrioasyncdeftimeout_after(delay, coroutine):
asyncwithtrio.move_on_after(delay) ascancel_scope:
awaitcoroutineifcancel_scope.cancelled_caught:
raiseTimeoutErrorAnd use it like this:
asyncdeftest_async_client_timeout_disabled(server, backend):
url=server.url.replace_with(path="/slow_response/100")
asyncwithhttpx.AsyncClient(backend=backend, timeout=None) asclient:
assertclient.dispatch.timeoutisNonewithpytest.raises(TimeoutError):
awaittimeout_after(50e-3, client.get(url))Or would this be too much?
There was a problem hiding this comment.
I was struggling a bit to change the default timeout of the client (didn't want to have a test that took the full 5s of the default timeout), but I really like this approach. I'll add a few tests with this pattern!
lovelydinosaur
commented
Oct 10, 2019
Having seen this going all the way through the stack like this I'm a whole lot more cautious about it. |
jcugat
commented
Oct 10, 2019
@tomchristie your concern is that now we have an |
jcugat
commented
Oct 16, 2019
Is there something else I can do to help push this forward? |
florimondmanca
commented
Oct 20, 2019
lovelydinosaur
commented
Dec 1, 2019
Thanks for having put a bunch of time into this! It's bit out of sync now, but a good basis for figure out what we need.
|
lovelydinosaur
commented
Dec 4, 2019
Superseeded by #592 - Thanks so much for your time on this, it's been really helpful in getting it squared away! |
Fixes #433
Took a first stab at it. There are a few tests missing, but wanted to push it early to start getting some feedback.