Repository: encode/httpx
Pull request: Document when to use AsyncClient
Fixes #179, refs (or fixes?) #508.

![Screenshot 2019-11-17 at 11 47 51](https://user-images.githubusercontent.com/15911462/69006515-1abe9800-0930-11ea-8461-53af83169765.png)


Problem statement:
Use the existing runloop for sync calls in async handler
Hi, this library looks awesome and I really want to use it; but I seem to have hit a blocker and can't seem to find a solution in the docs.

TL;DR: Is there any way to provide our own runloop when using the `sync` calls as looking through the source the sync calls are actually using the async request call.

Basically I'm writing a client api that provides 2 interfaces; an async and sync interface.

The issue that i am having is that if I try to use the sync methods with an existing runloop (provided by Sanic in this scenario), it complains with the exception: `Cannot run the event loop while another loop is running`.

Essentially I'm just calling:
```
import httpx
...
class SomeClass(object):
    def get_some_url(self):
        r = httpx.get('some url')
```

note that nothing here is async; apart from the fact that the `.get_some_url` call is made from within an async function like so:

```
import SomeClass

async def handler(request):
    o = SomeClass()
    o.get_some_url()
```

---

This works fine when using the async calls like this:

```
import httpx
...
class SomeClass(object):
    async def get_some_url(self):
        client = httpx.AsyncClient()
        r = await client.get('some url')
```

```
import SomeClass

async def handler(request):
    o = SomeClass()
    await o.get_some_url()
```

So I was just wondering if its possible to provide our own runloop when using the sync calls? Sanic uses uvloop internally, would be nice to be able to just pass that straight in somehow when constructing the `httpx` object, or setting it as a property after import.

Thanks!

Hints from issue discussion:
FWIW; I tried the following in a vein hope that it would do what I wanted:
```
import httpx
client = httpx.Client(backend=<my app uvloop>)
```

But alas, no success.. this the end of the stacktrace.

```
   self.client = httpx.Client(backend=kwargs.get('runloop'))
  File "/usr/local/lib/python3.6/site-packages/httpx/client.py", line 85, in __init__
    backend=backend,
  File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/connection_pool.py", line 97, in __init__
    self.max_connections = self.backend.get_semaphore(pool_limits)
AttributeError: 'Loop' object has no attribute 'get_semaphore'
```
In the meantime; I'll just use `requests` for the sync stuff, it feels odd passing in a runloop when I'm doing `sync` calls; purely because internally you're using async under the hood even for sync requests, or am I mistaken? https://github.com/encode/httpx/blob/66754ad0c58d61d34489294530351aa4b17e217f/httpx/client.py#L625
> FWIW; I tried the following in a vein hope that it would do what I wanted:
> 
> ```
> import httpx
> client = httpx.Client(backend=<my app uvloop>)
> ```
> 
> But alas, no success.. this the end of the stacktrace.
> 
> ```
>    self.client = httpx.Client(backend=kwargs.get('runloop'))
>   File "/usr/local/lib/python3.6/site-packages/httpx/client.py", line 85, in __init__
>     backend=backend,
>   File "/usr/local/lib/python3.6/site-packages/httpx/dispatch/connection_pool.py", line 97, in __init__
>     self.max_connections = self.backend.get_semaphore(pool_limits)
> AttributeError: 'Loop' object has no attribute 'get_semaphore'
> ```

Hi to use backend parameter, it has to be derived by ConcurrencyBackend. 
Yeah, I figured that after I got the exception. I figured it would handle a standard runloop.
you can check sample class from https://github.com/encode/httpx/blob/66754ad0c58d61d34489294530351aa4b17e217f/tests/dispatch/utils.py#L19
Yeah, unfortunately I can't modify my existing runloop to meet the requirements of the `backend` parameter; it is given to me by sanic directly.
Maybe `runloop` would be added with a `loop` params rather `backend`. I'd want to :)
There is no API to provide the event loop yourself, however there is a [`loop` property in the `AsyncioConcurrencyBackend`](https://github.com/encode/httpx/blob/master/httpx/concurrency.py#L190-L197) that checks an existing private `_loop` instance variable you can potentially set to the Sanic event loop.

We should consider adding this as a feature though.
I'm not sure about the context you're working with but there's some subtleties around using the sync API in an async application.

The sync API internally uses the loop's `run_in_executor` which will create a `ThreadPoolExecutor` and run your request there which might hinder performance or just be unnecessary. If possible I'd suggest staying in async land and use the AsyncClient, but, again, it depends on your context.
> The issue that i am having is that if I try to use the sync methods with an existing runloop (provided by Sanic in this scenario), it complains with the exception: Cannot run the event loop while another loop is running.

Okay, that's a problematic thing to be doing.

If you're running in the context of an event loop, but then making *blocking* style network calls you're going to block the event loop. You *don't* want to be doing that.

We've got a relevant issue here about *how* we describe that most clearly in any raised exception, but it's a fundamental property of the differing concurrency model, rather than a bug (at least as I read it here)

Commit message: Document when to use AsyncClient

Patch to review:
diff --git a/docs/async.md b/docs/async.md
index 45620bf8c2..9161669dff 100644
--- a/docs/async.md
+++ b/docs/async.md
@@ -105,3 +105,78 @@ trio.run(main)
 
 !!! important
     `trio` must be installed to import and use the `TrioBackend`.
+
+## FAQ
+
+### When should I use an `AsyncClient`?
+
+You should use an `AsyncClient` whenever you are inside an *async environment*.
+
+In particular, using `httpx.get()` or `httpx.Client()` in an async environment is **not** supported. There are several technical reasons to this, but the rationale is that you shouldn't be doing blocking-style network calls in the context of an event loop (for more discussion, see [#179](https://github.com/encode/httpx/issues/179)).
+
+For example, this won't work:
+
+```python
+import asyncio
+import httpx
+
+async def main():
+    r = httpx.get("https://example.org")
+
+asyncio.run(main())
+```
+
+Instead, you should use an `AsyncClient`:
+
+```python
+import asyncio
+import httpx
+
+async def main():
+    async with httpx.AsyncClient() as client:
+        r = await client.get("https://example.org")
+
+asyncio.run(main())
+```
+
+Note that being in an async environment may not be immediately obvious. For example, you may be in a regular function that is called from an asynchronous function. So, this won't work either:
+
+```python
+import asyncio
+import httpx
+
+class Service:
+    def fetch_data(self):
+        r = httpx.get("https://example.org")
+        return r.text
+
+async def main():
+    service = Service()
+    data = service.fetch_data()
+
+asyncio.run(main())
+```
+
+Here, you should make `.fetch_data()` asynchronous. To use a shared client and benefit from connection pooling, you can also inject the `AsyncClient` as a parameter. This leads to the following code:
+
+```python
+import asyncio
+import httpx
+
+class Service:
+    async def fetch_data(self, client):
+        r = await client.get("https://example.org")
+        return r.text
+
+async def main():
+    service = Service()
+    async with httpx.AsyncClient() as client:
+        data = await service.fetch_data(client)
+        more_data = await service.fetch_data(client)
+
+asyncio.run(main())
+```
+
+If you do not have control over making `.fetch_data()` asynchronous (for example, if it is a synchronous hook provided by a third-party framework), then the only solution would be to make the HTTP request in a separate thread. At this point, you should probably consider using a regular threaded HTTP client such as [Requests](https://github.com/psf/requests).
+
+Lastly, if you are using HTTPX in the Jupyter REPL, you should also use an `AsyncClient`, as the Jupyter REPL secretly runs an event loop and lets you use `await` directly. For related discussion, see [#508](https://github.com/encode/httpx/issues/508)).


Code review comments:
diff_hunk: @@ -105,3 +105,78 @@ trio.run(main)
 
 !!! important
     `trio` must be installed to import and use the `TrioBackend`.
+
+## FAQ
+
+### When should I use an `AsyncClient`?
+
+You should use an `AsyncClient` whenever you are inside an *async environment*.
+
+In particular, using `httpx.get()` or `httpx.Client()` in an async environment is **not** supported. There are several technical reasons to this, but the rationale is that you shouldn't be doing blocking-style network calls in the context of an event loop (for more discussion, see [#179](https://github.com/encode/httpx/issues/179)).
+
+For example, this won't work:
+
+```python
+import asyncio
+import httpx
+
+async def main():
+    r = httpx.get("https://example.org")
+
+asyncio.run(main())
+```
+
+Instead, you should use an `AsyncClient`:
+
+```python
+import asyncio
+import httpx
+
+async def main():
+    async with httpx.AsyncClient() as client:
+        r = await client.get("https://example.org")
+
+asyncio.run(main())
+```
+
+Note that being in an async environment may not be immediately obvious. For example, you may be in a regular function that is called from an asynchronous function. So, this won't work either:
+
+```python
+import asyncio
+import httpx
+
+class Service:
+    def fetch_data(self):
+        r = httpx.get("https://example.org")
+        return r.text
+
+async def main():
+    service = Service()
+    data = service.fetch_data()
+
+asyncio.run(main())
+```
+
+Here, you should make `.fetch_data()` asynchronous. To use a shared client and benefit from connection pooling, you can also inject the `AsyncClient` as a parameter. This leads to the following code:
+
+```python
+import asyncio
+import httpx
+
+class Service:
+    async def fetch_data(self, client):
+        r = await client.get("https://example.org")
+        return r.text
+
+async def main():
+    service = Service()
+    async with httpx.AsyncClient() as client:
+        data = await service.fetch_data(client)
+        more_data = await service.fetch_data(client)
+
+asyncio.run(main())
+```
+
+If you do not have control over making `.fetch_data()` asynchronous (for example, if it is a synchronous hook provided by a third-party framework), then the only solution would be to make the HTTP request in a separate thread. At this point, you should probably consider using a regular threaded HTTP client such as [Requests](https://github.com/psf/requests).
path: docs/async.md
text: @user1:
Agreed. I'd probably suggest scratching everything from "may not be immediately obviously" through to "If you do not have control". I think it dilutes the important stuff too much.

Otherwise great, tho.