-
-
Notifications
You must be signed in to change notification settings - Fork 856
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
Supporting Sync. Done right. #572
Comments
Ah, actually I guess that might not quite be okay, given that Probably workarounds available, but could be problematic. Anyway's there's:
|
IMO sync should be the second class citizen, not async. Not sticking to this means going backwards wrt discussions and conclusions we’ve drawn from #522. So, another option: keep the same API, but have it live under httpx.sync. In short... >>> import httpx.sync as httpx |
I get that this is not stable software, but this was a bit of a rude shock when all my tests broke. I completely disagree that "sync should be the second class citizen". Imho 90% of use cases will be fetch this page then fetch the next and so on... |
@phillmac I hear you competely. We've had a big pain point here in that our prior approach to sync support needed to be ripped out in order to change how we handle it. Having cleared that out the way, I now absolutely want to support the sync and async cases both equally. We've got a bit of technical stuff to do to get that done, and we've also got the naming decisions here to resolve, but yes it'll be on the way back in. I suggest pinning your |
Cool, my fault for not pinning it earlier, but I wanted bleeding edge stuff. Guess I got what I asked for |
I should be slightly more precise here - one thing that may well differ is that our sync API may not support HTTP/2. (I see you mention that in the thread in #522.) |
Yeah that's the whole point, I have a http/2 api server, there seems not to be any just bog standard http/2 libraries 😞 |
I've been playing around with this a little now...took an other approach. Might be too magic though, but wanted to throw it out for discussion anyways ;-) Idea is to use the same high-level api for both sync and async by reverting the api methods On top of that they could be decorated with a new Tested example: # api.py
def awaitable(func: Callable) -> Callable:
def wrapper(
*args: Any, **kwargs: Any
) -> Union[Response, Coroutine[Any, Any, Response]]:
coroutine = func(*args, **kwargs)
try:
# Detect if we're in async context
sniffio.current_async_library()
except sniffio.AsyncLibraryNotFoundError:
# Not in async context, run coroutine in a new event loop.
return asyncio.run(coroutine)
else:
# We're in async context, return the coroutine for await usage
return coroutine
return wrapper
async def request(...) -> Response:
async with Client(...) as client:
return await client.request(...)
@awaitable
def get(...) -> Union[Response, Coroutine[Any, Any, Response]]:
return request(...)
# test_api.py
def test_sync_get():
response = httpx.get(...)
@pytest.mark.asyncio
def test_async_get():
response = await httpx.get(...) Pros are that it doen't change the current api, meaning it's still awaitable coroutines returned that can be used with asyncio.gather etc. Cons are that it gives a sad extra context-check overhead, if not solved some way, cache?, and also maybe harder to document. To get rid of the context check, a setting could be introduced instead, i.e. Thoughts? |
Aside from |
True, the |
It's a neat approach. Potentially not great for folks using mypy or other type checkers against their codebase. |
@tomchristie do you have any experience/thoughts on |
@lundberg Not something I've looked into, no. Any particular indication that might be an issue? |
I've updated the issue description with my thoughts on how the naming/scoping could look here. |
No other than that it feels wrong that I had to do the check on every api call.
We could supply a typing shorthand for this, not as nice as Response, but still. |
Can confirm: mypy would disallow awaiting the return value of Full snippet: import asyncio
import typing
import httpx
import sniffio
T = typing.TypeVar("T")
def awaitable(
func: typing.Callable[..., typing.Union[T, typing.Awaitable[T]]]
) -> typing.Callable[..., typing.Union[T, typing.Awaitable[T]]]:
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Union[T, typing.Awaitable[T]]:
coroutine = func(*args, **kwargs)
try:
# Detect if we're in async context
sniffio.current_async_library()
except sniffio.AsyncLibraryNotFoundError:
# Not in async context, run coroutine in a new event loop.
coroutine = typing.cast(typing.Awaitable[T], coroutine)
return asyncio.run(coroutine)
else:
# We're in async context, return the coroutine for await usage
return coroutine
return wrapper
async def request(method: str, url: str) -> httpx.Response:
async with httpx.Client() as client:
return await client.request(method, url)
@awaitable
def get(url: str) -> typing.Union[httpx.Response, typing.Coroutine[typing.Any, typing.Any, httpx.Response]]:
return request('GET', url)
async def main() -> None:
_ = await get('https://example.org')
_debug/app.py:42: error: Incompatible types in "await" (actual type "Union[Response, Awaitable[Response]]", expected type "Awaitable[Any]") While this is smart, I think the implicitness induced by having to look at the context around the function call makes it a no-no for me. On the contrary, an API such as |
@tomchristie A few questions regarding the update in the issue description. A) Do we want B) Re: responses, as I understand I'm thinking it might be possible to take advantage of the fact that class StreamingResponse:
def __enter__(self):
assert is_sync_iterator(self.content)
...
def __exit__(self, *args):
...
def __aenter__(self):
assert is_async_iterator(self.content)
...
def __aexit__(self, *args):
... This way:
And we don't have to create sync/async classes just for the streaming case. :-) (Caveat: we'd need to do the same kind of checks for the |
Nice try though @florimondmanca 😅 Also really agree that it's more clear with separate sync/async apis like get/aget etc. But even though my rough idea, read think outside the box, probably is not the way to go, it does keep the core code base untouched, in other words no need for sync/async implementations like before. Still think it would be nice to keep the core async, and "add" sync support/help on top. Maybe just so, a sync-wrapper api, maybe even go all the way with opt-in install |
There's still the "code-gen the sync version from the async one" approach that we've been thinking about seeing what folks from |
Ah, that’s a good point. I’d not considered that those two wouldn’t mirror each other. At that point I’d probably just say “let’s not have the top level API for async.” Different constraints - the top level API is a convenience case, and there’s really no good reason not to always use a client instance in the async case. And yeah neat point about the |
Couldn't let go of my attempt of having one high-level api... 😄 New idea is to skip the sync/async context checking and instead wrap the coroutine from Example: # models.py
class FutureResponse(Awaitable[Response]):
def __init__(self, coroutine):
self.coroutine = coroutine
def __await__(self):
return self.coroutine.__await__()
def send(self):
# Alternative names; result, run, resolve, call, __call__, ?
# Should be more clever here and reuse sync helper loop and support py3.6
return asyncio.run(self.coroutine) # api.py
async def request(...) -> Response:
async with Client(...) as client:
return await client.request(...)
def get(...) -> FutureResponse:
return FutureResponse(request(...)) # test_api.py
@pytest.mark.asyncio
async def test_async_get():
response = await httpx.get(...)
def test_sync_get():
response = httpx.get(...).send() Thoughts? |
That is outrageously clever, @lundberg. 🤯 On trio you'd have to some a weird |
Do you mean that If so, I personally think that it doesn't need to be that clever, since if you have trio installed, you're probably already in an async context, or know what you're doing. Additionally, about the parallel api namespace approach, |
I'mma have to curtail the fun here I think. 😅 Let's not be clever. @florimondmanca made a good point about The |
@tomchristie So, about the |
Also, I'm not too wild on enforcing the usage of a client in the async case. One of the killer features of Requests is the ability to just I'd really like to be able to just In short, I'm in favor of an async top-level API with the rationale that the top-level API is not only convenient for the REPL use cases. |
Why should they be in the same package then? Won't it be easier to have a separate |
@phillmac I'd even say its 99% and |
@lundberg with the
|
So, regarding the implementation of a sync variant, I guess one thing we agree on at this point is that the sync version must be purely sync, right? So, no While there's the question of how to syncify the Option 1) is to re-implement everything in a sync variant, so I had some fun today putting together a sample package that uses The main goal was to see how the development experience looked like, and how to set things up to get mypy, flake8, and code coverage to work with code generation. Getting code coverage to work locally and on CI was a bit involved, but overall besides the compilation step there isn't much that changes. I must say the development experience is less pleasing/straight-forward than for a simple "it's all in the source tree" package, but having the sync version "maintain itself" is really handy. Wondering what applying this approach to HTTPX would involve in practice. My thoughts are this would require the following…
That's quite a lot of upfront work, but it's not clear to me yet which of 1) re-implementing sync from async or 2) setting up code generation would be the best. I guess both have their pro's and con's… Is there anything we can do/try to see whether codegen would fit us well? Maybe only tackle syncifying |
I guess there might be, i.e. a 3rd-p-package limiting to a regular
IMO, that would limit async features, i.e. gather etc, or passing the coroutine around before awaiting. |
Nice to see this alternative as well @florimondmanca 😅 As we know, a lot of libraries and frameworks are struggling with the same stuff, and tackling it in different ways. Django released 3.0, ~ a week ago, with asgi- and some async support. Until Django is fully async, their solution is async_to_sync. |
Yup. Though I think this is the kind of "bridging" approach we had been trying to use here before (only via a custom implementation), i.e. running wrapping an async operation in an |
Totally agree @florimondmanca. Just wanted to highlight the alternative. But having said that, it's still a nice way of keeping the code base tight, either solution and alternative used, to keep the core async and "just" wrap it for sync use. |
Is there any reason for bringing back the sync version at all If it would be the case? How would it differ from the requests? |
Actually turns out that I think our Sync variant will support HTTP/2. (After our latest refactoring we don't actually need background tasks in the HTTP/2 case.)
|
So, with the 0.8 release we dropped our sync interface completely.
The approach of bridging a sync interface onto an underlying async one, had some issues, in particular:
The good news is that I think the
unasync
approach that thehip
team are working with is actually a much better tack onto this, and is something that I thinkhttpx
can adopt.We'll need to decide:
(Essentially the same discussion as python-trio/hip#168)
One option here could be...
httpx.get()
,httpx.post()
and pals. Sync only - they're just a nice convenience. Good for the repl and casual scripting, but no good reason why async code shouldn't use a proper client.httpx.sync.Client
,httpx.async.Client
and pals. Keep the names the same in each sync vs async case, but just scope them into two different submodules. Our__repr__
's for these clases could make sure to be distinct, eg.<async.Response [200 OK]>
So eg...
Sync:
Async:
Update
Here's how the naming/scoping could look...
Top level:
Client usage:
(Unlike our previous approach there's no
Client
case there, and no subclassing. Just a pure async implementation, and a pure sync implementation.)We can just have a single
Request
class, that'd accept whatever types on init and expose both streaming and async streaming interfaces to be used by the dispatcher, that raises errors in invalid cases eg. the sync streaming case is called, but it was passed an async iterable for body data.Most operations would return a plain ol'
Response
class, with no I/O available on it. See #588 (comment)When using
.stream
, either anAsyncResponse
or aSyncResponse
would be returned.There'd be sync and async variants of the dispatcher interface and implementations, but that wouldn't be part of the 1.0 API spec. Neither would the backend API.
The text was updated successfully, but these errors were encountered: