diff --git a/README.md b/README.md index d5d2148713..9bf6026d17 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,12 @@ Install with pip: $ pip install httpx ``` -Or, to include the optional HTTP/2 support, use: +There are also a number of optional dependancies. + +For example to include asyncio and HTTP/2 support, use: ```shell -$ pip install httpx[http2] +$ pip install 'httpx[asyncio,http2]' ``` HTTPX requires Python 3.8+. @@ -129,15 +131,17 @@ The HTTPX project relies on these excellent libraries: * `h11` - HTTP/1.1 support. * `certifi` - SSL certificates. * `idna` - Internationalized domain name support. -* `sniffio` - Async library autodetection. As well as these optional installs: -* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)* -* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)* -* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)* -* `click` - Command line client support. *(Optional, with `httpx[cli]`)* -* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)* +* `anyio` - Async support for `asyncio`. *(Optional, with `httpx['asyncio']`)* +* `trio` - Async support for `trio`. *(Optional, with `httpx['trio']`)* +* `h2` - HTTP/2 support. *(Optional, with `httpx['http2']`)* +* `socksio` - SOCKS proxy support. *(Optional, with `httpx['socks']`)* +* `click` - Command line client support. *(Optional, with `httpx['cli']`)* +* `rich` - Command line client support. *(Optional, with `httpx['cli']`)* +* `pygments` - Command line client support. *(Optional, with `httpx['cli']`)* +* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx['brotli']`)* * `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)* A huge amount of credit is due to `requests` for the API layout that diff --git a/docs/async.md b/docs/async.md index 089d783191..4b62d85e72 100644 --- a/docs/async.md +++ b/docs/async.md @@ -10,6 +10,24 @@ long-lived network connections such as WebSockets. If you're working with an async web framework then you'll also want to use an async client for sending outgoing HTTP requests. +## Enabling Async support + +To enable async support you'll need to install some additional dependencies: + +If you're using Python's [standard `asyncio` support](https://docs.python.org/3/library/asyncio.html) then: + +```shell +$ pip install "httpx[asyncio]" +``` + +Or, if you're working with the [`trio` third party package](https://trio.readthedocs.io/en/stable/): + +```shell +$ pip install httpx['trio'] +``` + +We highly recommend `trio` for async support. The `trio` project [pioneered the principles of structured concurrency](https://en.wikipedia.org/wiki/Structured_concurrency), and has a more carefully constrained API against which to work from. + ## Making Async requests To make asynchronous requests, you'll need an `AsyncClient`. diff --git a/docs/http2.md b/docs/http2.md index 3cab09d912..a865b55a26 100644 --- a/docs/http2.md +++ b/docs/http2.md @@ -28,7 +28,7 @@ trying out our HTTP/2 support. You can do so by first making sure to install the optional HTTP/2 dependencies... ```shell -$ pip install httpx[http2] +$ pip install 'httpx[http2]' ``` And then instantiating a client with HTTP/2 support enabled: diff --git a/httpx/_transports/asgi.py b/httpx/_transports/asgi.py index 8578d4aeff..17ad2b0a14 100644 --- a/httpx/_transports/asgi.py +++ b/httpx/_transports/asgi.py @@ -2,8 +2,6 @@ import typing -import sniffio - from .._models import Request, Response from .._types import AsyncByteStream from .base import AsyncBaseTransport @@ -28,7 +26,9 @@ __all__ = ["ASGITransport"] -def create_event() -> Event: +def create_event() -> "Event": + import sniffio + if sniffio.current_async_library() == "trio": import trio diff --git a/httpx/_transports/default.py b/httpx/_transports/default.py index 33db416dd1..3ebf210249 100644 --- a/httpx/_transports/default.py +++ b/httpx/_transports/default.py @@ -277,6 +277,14 @@ def __init__( retries: int = 0, socket_options: typing.Iterable[SOCKET_OPTION] | None = None, ) -> None: + try: + import sniffio # noqa: F401 + except ImportError: # pragma: nocover + raise RuntimeError( + "Using httpx in async mode, but neither " + "httpx['asyncio'] or asyncio['trio'] is installed." + ) + ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy diff --git a/httpx/_utils.py b/httpx/_utils.py index 46a4d63b1d..0160d61dba 100644 --- a/httpx/_utils.py +++ b/httpx/_utils.py @@ -11,8 +11,6 @@ from pathlib import Path from urllib.request import getproxies -import sniffio - from ._types import PrimitiveData if typing.TYPE_CHECKING: # pragma: no cover @@ -291,29 +289,18 @@ def peek_filelike_length(stream: typing.Any) -> int | None: class Timer: - async def _get_time(self) -> float: - library = sniffio.current_async_library() - if library == "trio": - import trio - - return trio.current_time() - else: - import asyncio - - return asyncio.get_event_loop().time() - def sync_start(self) -> None: self.started = time.perf_counter() async def async_start(self) -> None: - self.started = await self._get_time() + self.started = time.perf_counter() def sync_elapsed(self) -> float: now = time.perf_counter() return now - self.started async def async_elapsed(self) -> float: - now = await self._get_time() + now = time.perf_counter() return now - self.started diff --git a/pyproject.toml b/pyproject.toml index c4c188052e..484989a1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,8 @@ classifiers = [ ] dependencies = [ "certifi", - "httpcore==1.*", - "anyio", + "httpcore>=1.0.0,<2.0.0", "idna", - "sniffio", ] dynamic = ["readme", "version"] @@ -47,10 +45,16 @@ cli = [ "rich>=10,<14", ] http2 = [ - "h2>=3,<5", + "httpcore[http2]", ] socks = [ - "socksio==1.*", + "httpcore[socks]", +] +asyncio = [ + "httpcore[asyncio]" +] +trio = [ + "httpcore[trio]" ] zstd = [ "zstandard>=0.18.0", diff --git a/requirements.txt b/requirements.txt index 5aab03767c..bb2ddd202c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # On the other hand, we're not pinning package dependencies, because our tests # needs to pass with the latest version of the packages. # Reference: https://github.com/encode/httpx/pull/1721#discussion_r661241588 --e .[brotli,cli,http2,socks,zstd] +-e .[asyncio,trio,brotli,cli,http2,socks,zstd] # Optional charset auto-detection # Used in our test cases @@ -24,6 +24,5 @@ mypy==1.11.2 pytest==8.3.2 ruff==0.6.3 trio==0.26.2 -trio-typing==0.10.0 trustme==1.1.0 uvicorn==0.30.6