diff --git a/CHANGELOG.md b/CHANGELOG.md
index b47a18bb..d2abd040 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## unreleased
+
+- The networking backend interface has [been added to the public API](https://www.encode.io/httpcore/network-backends). Some classes which were previously private implementation detail are now part of the top-level public API. (#699)
+
## 0.17.2 (May 23th, 2023)
- Add `socket_options` argument to `ConnectionPool` and `HTTProxy` classes. (#668)
diff --git a/docs/extensions.md b/docs/extensions.md
index 231e73f9..3ac3d082 100644
--- a/docs/extensions.md
+++ b/docs/extensions.md
@@ -200,6 +200,8 @@ The interface provided by the network stream:
This API can be used as the foundation for working with HTTP proxies, WebSocket upgrades, and other advanced use-cases.
+See the [network backends documentation](network-backends.md) for more information on working directly with network streams.
+
##### `CONNECT` requests
A proxy CONNECT request using the network stream:
diff --git a/docs/network-backends.md b/docs/network-backends.md
index b3783e78..fbb6bfdb 100644
--- a/docs/network-backends.md
+++ b/docs/network-backends.md
@@ -1,3 +1,279 @@
# Network Backends
-TODO
+The API layer at which `httpcore` interacts with the network is described as the network backend. Various backend implementations are provided, allowing `httpcore` to handle networking in different runtime contexts.
+
+## Working with network backends
+
+### The default network backend
+
+Typically you won't need to specify a network backend, as a default will automatically be selected. However, understanding how the network backends fit in may be useful if you want to better understand the underlying architecture. Let's start by seeing how we can explicitly select the network backend.
+
+First we're making a standard HTTP request, using a connection pool:
+
+```python
+import httpcore
+
+with httpcore.ConnectionPool() as http:
+ response = http.request('GET', 'https://www.example.com')
+ print(response)
+```
+
+We can also have the same behavior, but be explicit with our selection of the network backend:
+
+```python
+import httpcore
+
+network_backend = httpcore.SyncBackend()
+with httpcore.ConnectionPool(network_backend=network_backend) as http:
+ response = http.request('GET', 'https://www.example.com')
+ print(response)
+```
+
+The `httpcore.SyncBackend()` implementation handles the opening of TCP connections, and operations on the socket stream, such as reading, writing, and closing the connection.
+
+We can get a better understanding of this by using a network backend to send a basic HTTP/1.1 request directly:
+
+```python
+import httpcore
+
+# Create an SSL context using 'certifi' for the certificates.
+ssl_context = httpcore.default_ssl_context()
+
+# A basic HTTP/1.1 request as a plain bytestring.
+request = b'\r\n'.join([
+ b'GET / HTTP/1.1',
+ b'Host: www.example.com',
+ b'Accept: */*',
+ b'Connection: close',
+ b''
+])
+
+# Open a TCP stream and upgrade it to SSL.
+network_backend = httpcore.SyncBackend()
+network_stream = network_backend.connect_tcp("www.example.com", 443)
+network_stream = network_stream.start_tls(ssl_context, server_hostname="www.example.com")
+
+# Send the HTTP request.
+network_stream.write(request)
+
+# Read the HTTP response.
+while True:
+ response = network_stream.read(max_bytes=4096)
+ if response == b'':
+ break
+ print(response)
+
+# The output should look something like this:
+#
+# b'HTTP/1.1 200 OK\r\nAge: 600005\r\n [...] Content-Length: 1256\r\nConnection: close\r\n\r\n'
+# b'\n\n
\n Example Domain [...] \n'
+```
+
+### Async network backends
+
+If we're working with an `async` codebase, then we need to select a different backend.
+
+The `httpcore.AnyIOBackend` is suitable for usage if you're running under `asyncio`. This is a networking backend implemented using [the `anyio` package](https://anyio.readthedocs.io/en/3.x/).
+
+```python
+import httpcore
+import asyncio
+
+async def main():
+ network_backend = httpcore.AnyIOBackend()
+ async with httpcore.AsyncConnectionPool(network_backend=network_backend) as http:
+ response = await http.request('GET', 'https://www.example.com')
+ print(response)
+
+asyncio.run(main())
+```
+
+The `AnyIOBackend` will work when running under either `asyncio` or `trio`. However, if you're working with async using the [`trio` framework](https://trio.readthedocs.io/en/stable/), then we recommend using the `httpcore.TrioBackend`.
+
+This will give you the same kind of networking behavior you'd have using `AnyIOBackend`, but there will be a little less indirection so it will be marginally more efficient and will present cleaner tracebacks in error cases.
+
+```python
+import httpcore
+import trio
+
+async def main():
+ network_backend = httpcore.TrioBackend()
+ async with httpcore.AsyncConnectionPool(network_backend=network_backend) as http:
+ response = await http.request('GET', 'https://www.example.com')
+ print(response)
+
+trio.run(main)
+```
+
+### Mock network backends
+
+There are also mock network backends available that can be useful for testing purposes.
+These backends accept a list of bytes, and return network stream interfaces that return those byte streams.
+
+Here's an example of mocking a simple HTTP/1.1 response...
+
+```python
+import httpcore
+
+network_backend = httpcore.MockBackend([
+ b"HTTP/1.1 200 OK\r\n",
+ b"Content-Type: plain/text\r\n",
+ b"Content-Length: 13\r\n",
+ b"\r\n",
+ b"Hello, world!",
+])
+with httpcore.ConnectionPool(network_backend=network_backend) as http:
+ response = http.request("GET", "https://example.com/")
+ print(response.extensions['http_version'])
+ print(response.status)
+ print(response.content)
+```
+
+Mocking a HTTP/2 response is more complex, since it uses a binary format...
+
+```python
+import hpack
+import hyperframe.frame
+import httpcore
+
+content = [
+ hyperframe.frame.SettingsFrame().serialize(),
+ hyperframe.frame.HeadersFrame(
+ stream_id=1,
+ data=hpack.Encoder().encode(
+ [
+ (b":status", b"200"),
+ (b"content-type", b"plain/text"),
+ ]
+ ),
+ flags=["END_HEADERS"],
+ ).serialize(),
+ hyperframe.frame.DataFrame(
+ stream_id=1, data=b"Hello, world!", flags=["END_STREAM"]
+ ).serialize(),
+]
+# Note that we instantiate the mock backend with an `http2=True` argument.
+# This ensures that the mock network stream acts as if the `h2` ALPN flag has been set,
+# and causes the connection pool to interact with the connection using HTTP/2.
+network_backend = httpcore.MockBackend(content, http2=True)
+with httpcore.ConnectionPool(network_backend=network_backend) as http:
+ response = http.request("GET", "https://example.com/")
+ print(response.extensions['http_version'])
+ print(response.status)
+ print(response.content)
+```
+
+### Custom network backends
+
+The base interface for network backends is provided as public API, allowing you to implement custom networking behavior.
+
+You can use this to provide advanced networking functionality such as:
+
+* Network recording / replay.
+* In-depth debug tooling.
+* Handling non-standard SSL or DNS requirements.
+
+Here's an example that records the network response to a file on disk:
+
+```python
+import httpcore
+
+
+class RecordingNetworkStream(httpcore.NetworkStream):
+ def __init__(self, record_file, stream):
+ self.record_file = record_file
+ self.stream = stream
+
+ def read(self, max_bytes, timeout=None):
+ data = self.stream.read(max_bytes, timeout=timeout)
+ self.record_file.write(data)
+ return data
+
+ def write(self, buffer, timeout=None):
+ self.stream.write(buffer, timeout=timeout)
+
+ def close(self) -> None:
+ self.stream.close()
+
+ def start_tls(
+ self,
+ ssl_context,
+ server_hostname=None,
+ timeout=None,
+ ):
+ self.stream = self.stream.start_tls(
+ ssl_context, server_hostname=server_hostname, timeout=timeout
+ )
+ return self
+
+ def get_extra_info(self, info):
+ return self.stream.get_extra_info(info)
+
+
+class RecordingNetworkBackend(httpcore.NetworkBackend):
+ """
+ A custom network backend that records network responses.
+ """
+ def __init__(self, record_file):
+ self.record_file = record_file
+ self.backend = httpcore.SyncBackend()
+
+ def connect_tcp(
+ self,
+ host,
+ port,
+ timeout=None,
+ local_address=None,
+ socket_options=None,
+ ):
+ # Note that we're only using a single record file here,
+ # so even if multiple connections are opened the network
+ # traffic will all write to the same file.
+
+ # An alternative implementation might automatically use
+ # a new file for each opened connection.
+ stream = self.backend.connect_tcp(
+ host,
+ port,
+ timeout=timeout,
+ local_address=local_address,
+ socket_options=socket_options
+ )
+ return RecordingNetworkStream(self.record_file, stream)
+
+
+# Once you make the request, the raw HTTP/1.1 response will be available
+# in the 'network-recording' file.
+#
+# Try switching to `http2=True` to see the difference when recording HTTP/2 binary network traffic,
+# or add `headers={'Accept-Encoding': 'gzip'}` to see HTTP content compression.
+with open("network-recording", "wb") as record_file:
+ network_backend = RecordingNetworkBackend(record_file)
+ with httpcore.ConnectionPool(network_backend=network_backend) as http:
+ response = http.request("GET", "https://www.example.com/")
+ print(response)
+```
+
+---
+
+## Reference
+
+### Networking Backends
+
+* `httpcore.SyncBackend`
+* `httpcore.AnyIOBackend`
+* `httpcore.TrioBackend`
+
+### Mock Backends
+
+* `httpcore.MockBackend`
+* `httpcore.MockStream`
+* `httpcore.AsyncMockBackend`
+* `httpcore.AsyncMockStream`
+
+### Base Interface
+
+* `httpcore.NetworkBackend`
+* `httpcore.NetworkStream`
+* `httpcore.AsyncNetworkBackend`
+* `httpcore.AsyncNetworkStream`
diff --git a/httpcore/__init__.py b/httpcore/__init__.py
index 0477ec3e..d3e57ad0 100644
--- a/httpcore/__init__.py
+++ b/httpcore/__init__.py
@@ -8,6 +8,15 @@
AsyncHTTPProxy,
AsyncSOCKSProxy,
)
+from ._backends.base import (
+ SOCKET_OPTION,
+ AsyncNetworkBackend,
+ AsyncNetworkStream,
+ NetworkBackend,
+ NetworkStream,
+)
+from ._backends.mock import AsyncMockBackend, AsyncMockStream, MockBackend, MockStream
+from ._backends.sync import SyncBackend
from ._exceptions import (
ConnectError,
ConnectionNotAvailable,
@@ -37,6 +46,30 @@
SOCKSProxy,
)
+# The 'httpcore.AnyIOBackend' class is conditional on 'anyio' being installed.
+try:
+ from ._backends.anyio import AnyIOBackend
+except ImportError: # pragma: nocover
+
+ class AnyIOBackend: # type: ignore
+ def __init__(self, *args, **kwargs): # type: ignore
+ msg = (
+ "Attempted to use 'httpcore.AnyIOBackend' but 'anyio' is not installed."
+ )
+ raise RuntimeError(msg)
+
+
+# The 'httpcore.TrioBackend' class is conditional on 'trio' being installed.
+try:
+ from ._backends.trio import TrioBackend
+except ImportError: # pragma: nocover
+
+ class TrioBackend: # type: ignore
+ def __init__(self, *args, **kwargs): # type: ignore
+ msg = "Attempted to use 'httpcore.TrioBackend' but 'trio' is not installed."
+ raise RuntimeError(msg)
+
+
__all__ = [
# top-level requests
"request",
@@ -62,8 +95,23 @@
"HTTP2Connection",
"ConnectionInterface",
"SOCKSProxy",
+ # network backends, implementations
+ "SyncBackend",
+ "AnyIOBackend",
+ "TrioBackend",
+ # network backends, mock implementations
+ "AsyncMockBackend",
+ "AsyncMockStream",
+ "MockBackend",
+ "MockStream",
+ # network backends, interface
+ "AsyncNetworkStream",
+ "AsyncNetworkBackend",
+ "NetworkStream",
+ "NetworkBackend",
# util
"default_ssl_context",
+ "SOCKET_OPTION",
# exceptions
"ConnectionNotAvailable",
"ProxyError",
diff --git a/httpcore/_async/connection.py b/httpcore/_async/connection.py
index ecf2546b..9014ab95 100644
--- a/httpcore/_async/connection.py
+++ b/httpcore/_async/connection.py
@@ -4,13 +4,13 @@
from types import TracebackType
from typing import Iterable, Iterator, Optional, Type
+from .._backends.auto import AutoBackend
+from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
from .._exceptions import ConnectError, ConnectionNotAvailable, ConnectTimeout
from .._models import Origin, Request, Response
from .._ssl import default_ssl_context
from .._synchronization import AsyncLock
from .._trace import Trace
-from ..backends.auto import AutoBackend
-from ..backends.base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
from .http11 import AsyncHTTP11Connection
from .interfaces import AsyncConnectionInterface
diff --git a/httpcore/_async/connection_pool.py b/httpcore/_async/connection_pool.py
index 37be0c2d..935f34db 100644
--- a/httpcore/_async/connection_pool.py
+++ b/httpcore/_async/connection_pool.py
@@ -3,11 +3,11 @@
from types import TracebackType
from typing import AsyncIterable, AsyncIterator, Iterable, List, Optional, Type
+from .._backends.auto import AutoBackend
+from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
from .._models import Origin, Request, Response
from .._synchronization import AsyncEvent, AsyncLock
-from ..backends.auto import AutoBackend
-from ..backends.base import SOCKET_OPTION, AsyncNetworkBackend
from .connection import AsyncHTTPConnection
from .interfaces import AsyncConnectionInterface, AsyncRequestInterface
diff --git a/httpcore/_async/http11.py b/httpcore/_async/http11.py
index 99b356f4..3ef6c80f 100644
--- a/httpcore/_async/http11.py
+++ b/httpcore/_async/http11.py
@@ -15,6 +15,7 @@
import h11
+from .._backends.base import AsyncNetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
@@ -24,7 +25,6 @@
from .._models import Origin, Request, Response
from .._synchronization import AsyncLock
from .._trace import Trace
-from ..backends.base import AsyncNetworkStream
from .interfaces import AsyncConnectionInterface
logger = logging.getLogger("httpcore.http11")
diff --git a/httpcore/_async/http2.py b/httpcore/_async/http2.py
index 131d932b..7eb3680b 100644
--- a/httpcore/_async/http2.py
+++ b/httpcore/_async/http2.py
@@ -10,6 +10,7 @@
import h2.exceptions
import h2.settings
+from .._backends.base import AsyncNetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
@@ -18,7 +19,6 @@
from .._models import Origin, Request, Response
from .._synchronization import AsyncLock, AsyncSemaphore
from .._trace import Trace
-from ..backends.base import AsyncNetworkStream
from .interfaces import AsyncConnectionInterface
logger = logging.getLogger("httpcore.http2")
diff --git a/httpcore/_async/http_proxy.py b/httpcore/_async/http_proxy.py
index 3dd1cb4f..62f51097 100644
--- a/httpcore/_async/http_proxy.py
+++ b/httpcore/_async/http_proxy.py
@@ -3,6 +3,7 @@
from base64 import b64encode
from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union
+from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
from .._exceptions import ProxyError
from .._models import (
URL,
@@ -16,7 +17,6 @@
from .._ssl import default_ssl_context
from .._synchronization import AsyncLock
from .._trace import Trace
-from ..backends.base import SOCKET_OPTION, AsyncNetworkBackend
from .connection import AsyncHTTPConnection
from .connection_pool import AsyncConnectionPool
from .http11 import AsyncHTTP11Connection
diff --git a/httpcore/_async/socks_proxy.py b/httpcore/_async/socks_proxy.py
index b3d7b762..f12cb373 100644
--- a/httpcore/_async/socks_proxy.py
+++ b/httpcore/_async/socks_proxy.py
@@ -4,13 +4,13 @@
from socksio import socks5
+from .._backends.auto import AutoBackend
+from .._backends.base import AsyncNetworkBackend, AsyncNetworkStream
from .._exceptions import ConnectionNotAvailable, ProxyError
from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url
from .._ssl import default_ssl_context
from .._synchronization import AsyncLock
from .._trace import Trace
-from ..backends.auto import AutoBackend
-from ..backends.base import AsyncNetworkBackend, AsyncNetworkStream
from .connection_pool import AsyncConnectionPool
from .http11 import AsyncHTTP11Connection
from .interfaces import AsyncConnectionInterface
diff --git a/httpcore/backends/__init__.py b/httpcore/_backends/__init__.py
similarity index 100%
rename from httpcore/backends/__init__.py
rename to httpcore/_backends/__init__.py
diff --git a/httpcore/backends/asyncio.py b/httpcore/_backends/anyio.py
similarity index 96%
rename from httpcore/backends/asyncio.py
rename to httpcore/_backends/anyio.py
index 70a22078..1ed5228d 100644
--- a/httpcore/backends/asyncio.py
+++ b/httpcore/_backends/anyio.py
@@ -16,7 +16,7 @@
from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
-class AsyncIOStream(AsyncNetworkStream):
+class AnyIOStream(AsyncNetworkStream):
def __init__(self, stream: anyio.abc.ByteStream) -> None:
self._stream = stream
@@ -76,7 +76,7 @@ async def start_tls(
except Exception as exc: # pragma: nocover
await self.aclose()
raise exc
- return AsyncIOStream(ssl_stream)
+ return AnyIOStream(ssl_stream)
def get_extra_info(self, info: str) -> typing.Any:
if info == "ssl_object":
@@ -93,7 +93,7 @@ def get_extra_info(self, info: str) -> typing.Any:
return None
-class AsyncIOBackend(AsyncNetworkBackend):
+class AnyIOBackend(AsyncNetworkBackend):
async def connect_tcp(
self,
host: str,
@@ -119,7 +119,7 @@ async def connect_tcp(
# By default TCP sockets opened in `asyncio` include TCP_NODELAY.
for option in socket_options:
stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
- return AsyncIOStream(stream)
+ return AnyIOStream(stream)
async def connect_unix_socket(
self,
@@ -139,7 +139,7 @@ async def connect_unix_socket(
stream: anyio.abc.ByteStream = await anyio.connect_unix(path)
for option in socket_options:
stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
- return AsyncIOStream(stream)
+ return AnyIOStream(stream)
async def sleep(self, seconds: float) -> None:
await anyio.sleep(seconds) # pragma: nocover
diff --git a/httpcore/backends/auto.py b/httpcore/_backends/auto.py
similarity index 93%
rename from httpcore/backends/auto.py
rename to httpcore/_backends/auto.py
index f4766ab5..b612ba07 100644
--- a/httpcore/backends/auto.py
+++ b/httpcore/_backends/auto.py
@@ -15,9 +15,9 @@ async def _init_backend(self) -> None:
self._backend: AsyncNetworkBackend = TrioBackend()
else:
- from .asyncio import AsyncIOBackend
+ from .anyio import AnyIOBackend
- self._backend = AsyncIOBackend()
+ self._backend = AnyIOBackend()
async def connect_tcp(
self,
diff --git a/httpcore/backends/base.py b/httpcore/_backends/base.py
similarity index 100%
rename from httpcore/backends/base.py
rename to httpcore/_backends/base.py
diff --git a/httpcore/backends/mock.py b/httpcore/_backends/mock.py
similarity index 100%
rename from httpcore/backends/mock.py
rename to httpcore/_backends/mock.py
diff --git a/httpcore/backends/sync.py b/httpcore/_backends/sync.py
similarity index 100%
rename from httpcore/backends/sync.py
rename to httpcore/_backends/sync.py
diff --git a/httpcore/backends/trio.py b/httpcore/_backends/trio.py
similarity index 100%
rename from httpcore/backends/trio.py
rename to httpcore/_backends/trio.py
diff --git a/httpcore/_sync/connection.py b/httpcore/_sync/connection.py
index d40b155b..39b8b97e 100644
--- a/httpcore/_sync/connection.py
+++ b/httpcore/_sync/connection.py
@@ -4,13 +4,13 @@
from types import TracebackType
from typing import Iterable, Iterator, Optional, Type
+from .._backends.sync import SyncBackend
+from .._backends.base import SOCKET_OPTION, NetworkBackend, NetworkStream
from .._exceptions import ConnectError, ConnectionNotAvailable, ConnectTimeout
from .._models import Origin, Request, Response
from .._ssl import default_ssl_context
from .._synchronization import Lock
from .._trace import Trace
-from ..backends.sync import SyncBackend
-from ..backends.base import SOCKET_OPTION, NetworkBackend, NetworkStream
from .http11 import HTTP11Connection
from .interfaces import ConnectionInterface
diff --git a/httpcore/_sync/connection_pool.py b/httpcore/_sync/connection_pool.py
index d30a1c51..f64334af 100644
--- a/httpcore/_sync/connection_pool.py
+++ b/httpcore/_sync/connection_pool.py
@@ -3,11 +3,11 @@
from types import TracebackType
from typing import Iterable, Iterator, Iterable, List, Optional, Type
+from .._backends.sync import SyncBackend
+from .._backends.base import SOCKET_OPTION, NetworkBackend
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
from .._models import Origin, Request, Response
from .._synchronization import Event, Lock
-from ..backends.sync import SyncBackend
-from ..backends.base import SOCKET_OPTION, NetworkBackend
from .connection import HTTPConnection
from .interfaces import ConnectionInterface, RequestInterface
diff --git a/httpcore/_sync/http11.py b/httpcore/_sync/http11.py
index 3a430bcf..448cf8de 100644
--- a/httpcore/_sync/http11.py
+++ b/httpcore/_sync/http11.py
@@ -15,6 +15,7 @@
import h11
+from .._backends.base import NetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
@@ -24,7 +25,6 @@
from .._models import Origin, Request, Response
from .._synchronization import Lock
from .._trace import Trace
-from ..backends.base import NetworkStream
from .interfaces import ConnectionInterface
logger = logging.getLogger("httpcore.http11")
diff --git a/httpcore/_sync/http2.py b/httpcore/_sync/http2.py
index 68482d26..4dc508ab 100644
--- a/httpcore/_sync/http2.py
+++ b/httpcore/_sync/http2.py
@@ -10,6 +10,7 @@
import h2.exceptions
import h2.settings
+from .._backends.base import NetworkStream
from .._exceptions import (
ConnectionNotAvailable,
LocalProtocolError,
@@ -18,7 +19,6 @@
from .._models import Origin, Request, Response
from .._synchronization import Lock, Semaphore
from .._trace import Trace
-from ..backends.base import NetworkStream
from .interfaces import ConnectionInterface
logger = logging.getLogger("httpcore.http2")
diff --git a/httpcore/_sync/http_proxy.py b/httpcore/_sync/http_proxy.py
index f282f663..bb368dd4 100644
--- a/httpcore/_sync/http_proxy.py
+++ b/httpcore/_sync/http_proxy.py
@@ -3,6 +3,7 @@
from base64 import b64encode
from typing import Iterable, List, Mapping, Optional, Sequence, Tuple, Union
+from .._backends.base import SOCKET_OPTION, NetworkBackend
from .._exceptions import ProxyError
from .._models import (
URL,
@@ -16,7 +17,6 @@
from .._ssl import default_ssl_context
from .._synchronization import Lock
from .._trace import Trace
-from ..backends.base import SOCKET_OPTION, NetworkBackend
from .connection import HTTPConnection
from .connection_pool import ConnectionPool
from .http11 import HTTP11Connection
diff --git a/httpcore/_sync/socks_proxy.py b/httpcore/_sync/socks_proxy.py
index 7387e731..407351d0 100644
--- a/httpcore/_sync/socks_proxy.py
+++ b/httpcore/_sync/socks_proxy.py
@@ -4,13 +4,13 @@
from socksio import socks5
+from .._backends.sync import SyncBackend
+from .._backends.base import NetworkBackend, NetworkStream
from .._exceptions import ConnectionNotAvailable, ProxyError
from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url
from .._ssl import default_ssl_context
from .._synchronization import Lock
from .._trace import Trace
-from ..backends.sync import SyncBackend
-from ..backends.base import NetworkBackend, NetworkStream
from .connection_pool import ConnectionPool
from .http11 import HTTP11Connection
from .interfaces import ConnectionInterface
diff --git a/mkdocs.yml b/mkdocs.yml
index 8fd57903..9f4200ba 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -12,6 +12,7 @@ nav:
- Proxies: 'proxies.md'
- HTTP/2: 'http2.md'
- Async Support: 'async.md'
+ - Network Backends: 'network-backends.md'
- Extensions: 'extensions.md'
- Logging: 'logging.md'
- Exceptions: 'exceptions.md'
diff --git a/tests/_async/test_connection.py b/tests/_async/test_connection.py
index 77ab3db0..a1011c05 100644
--- a/tests/_async/test_connection.py
+++ b/tests/_async/test_connection.py
@@ -6,9 +6,15 @@
import hyperframe.frame
import pytest
-from httpcore import AsyncHTTPConnection, ConnectError, ConnectionNotAvailable, Origin
-from httpcore.backends.base import SOCKET_OPTION, AsyncNetworkStream
-from httpcore.backends.mock import AsyncMockBackend
+from httpcore import (
+ SOCKET_OPTION,
+ AsyncHTTPConnection,
+ AsyncMockBackend,
+ AsyncNetworkStream,
+ ConnectError,
+ ConnectionNotAvailable,
+ Origin,
+)
@pytest.mark.anyio
diff --git a/tests/_async/test_connection_pool.py b/tests/_async/test_connection_pool.py
index 35562833..2ccfaf8c 100644
--- a/tests/_async/test_connection_pool.py
+++ b/tests/_async/test_connection_pool.py
@@ -6,14 +6,15 @@
import trio as concurrency
from httpcore import (
+ SOCKET_OPTION,
AsyncConnectionPool,
+ AsyncMockBackend,
+ AsyncNetworkStream,
ConnectError,
PoolTimeout,
ReadError,
UnsupportedProtocol,
)
-from httpcore.backends.base import SOCKET_OPTION, AsyncNetworkStream
-from httpcore.backends.mock import AsyncMockBackend
@pytest.mark.anyio
diff --git a/tests/_async/test_http11.py b/tests/_async/test_http11.py
index d0a60df7..f1cc1e25 100644
--- a/tests/_async/test_http11.py
+++ b/tests/_async/test_http11.py
@@ -2,12 +2,12 @@
from httpcore import (
AsyncHTTP11Connection,
+ AsyncMockStream,
ConnectionNotAvailable,
LocalProtocolError,
Origin,
RemoteProtocolError,
)
-from httpcore.backends.mock import AsyncMockStream
@pytest.mark.anyio
diff --git a/tests/_async/test_http2.py b/tests/_async/test_http2.py
index f995465f..92ac7efd 100644
--- a/tests/_async/test_http2.py
+++ b/tests/_async/test_http2.py
@@ -4,11 +4,11 @@
from httpcore import (
AsyncHTTP2Connection,
+ AsyncMockStream,
ConnectionNotAvailable,
Origin,
RemoteProtocolError,
)
-from httpcore.backends.mock import AsyncMockStream
@pytest.mark.anyio
diff --git a/tests/_async/test_http_proxy.py b/tests/_async/test_http_proxy.py
index 55ac88e1..b60915e6 100644
--- a/tests/_async/test_http_proxy.py
+++ b/tests/_async/test_http_proxy.py
@@ -6,9 +6,15 @@
import hyperframe.frame
import pytest
-from httpcore import AsyncHTTPProxy, Origin, ProxyError
-from httpcore.backends.base import SOCKET_OPTION, AsyncNetworkStream
-from httpcore.backends.mock import AsyncMockBackend, AsyncMockStream
+from httpcore import (
+ SOCKET_OPTION,
+ AsyncHTTPProxy,
+ AsyncMockBackend,
+ AsyncMockStream,
+ AsyncNetworkStream,
+ Origin,
+ ProxyError,
+)
@pytest.mark.anyio
diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py
index 168bc21e..033c3259 100644
--- a/tests/_async/test_socks_proxy.py
+++ b/tests/_async/test_socks_proxy.py
@@ -1,7 +1,6 @@
import pytest
-from httpcore import AsyncSOCKSProxy, Origin, ProxyError
-from httpcore.backends.mock import AsyncMockBackend
+from httpcore import AsyncMockBackend, AsyncSOCKSProxy, Origin, ProxyError
@pytest.mark.anyio
diff --git a/tests/_sync/test_connection.py b/tests/_sync/test_connection.py
index 40990c30..ad989ee2 100644
--- a/tests/_sync/test_connection.py
+++ b/tests/_sync/test_connection.py
@@ -6,9 +6,15 @@
import hyperframe.frame
import pytest
-from httpcore import HTTPConnection, ConnectError, ConnectionNotAvailable, Origin
-from httpcore.backends.base import SOCKET_OPTION, NetworkStream
-from httpcore.backends.mock import MockBackend
+from httpcore import (
+ SOCKET_OPTION,
+ HTTPConnection,
+ MockBackend,
+ NetworkStream,
+ ConnectError,
+ ConnectionNotAvailable,
+ Origin,
+)
diff --git a/tests/_sync/test_connection_pool.py b/tests/_sync/test_connection_pool.py
index c804a48f..b0fc9704 100644
--- a/tests/_sync/test_connection_pool.py
+++ b/tests/_sync/test_connection_pool.py
@@ -6,14 +6,15 @@
from tests import concurrency
from httpcore import (
+ SOCKET_OPTION,
ConnectionPool,
+ MockBackend,
+ NetworkStream,
ConnectError,
PoolTimeout,
ReadError,
UnsupportedProtocol,
)
-from httpcore.backends.base import SOCKET_OPTION, NetworkStream
-from httpcore.backends.mock import MockBackend
diff --git a/tests/_sync/test_http11.py b/tests/_sync/test_http11.py
index 4965654c..b4842f19 100644
--- a/tests/_sync/test_http11.py
+++ b/tests/_sync/test_http11.py
@@ -2,12 +2,12 @@
from httpcore import (
HTTP11Connection,
+ MockStream,
ConnectionNotAvailable,
LocalProtocolError,
Origin,
RemoteProtocolError,
)
-from httpcore.backends.mock import MockStream
diff --git a/tests/_sync/test_http2.py b/tests/_sync/test_http2.py
index 0adb96ef..f83a3be8 100644
--- a/tests/_sync/test_http2.py
+++ b/tests/_sync/test_http2.py
@@ -4,11 +4,11 @@
from httpcore import (
HTTP2Connection,
+ MockStream,
ConnectionNotAvailable,
Origin,
RemoteProtocolError,
)
-from httpcore.backends.mock import MockStream
diff --git a/tests/_sync/test_http_proxy.py b/tests/_sync/test_http_proxy.py
index f00995e2..98d07e69 100644
--- a/tests/_sync/test_http_proxy.py
+++ b/tests/_sync/test_http_proxy.py
@@ -6,9 +6,15 @@
import hyperframe.frame
import pytest
-from httpcore import HTTPProxy, Origin, ProxyError
-from httpcore.backends.base import SOCKET_OPTION, NetworkStream
-from httpcore.backends.mock import MockBackend, MockStream
+from httpcore import (
+ SOCKET_OPTION,
+ HTTPProxy,
+ MockBackend,
+ MockStream,
+ NetworkStream,
+ Origin,
+ ProxyError,
+)
diff --git a/tests/_sync/test_socks_proxy.py b/tests/_sync/test_socks_proxy.py
index 39108c8f..4c701b0d 100644
--- a/tests/_sync/test_socks_proxy.py
+++ b/tests/_sync/test_socks_proxy.py
@@ -1,7 +1,6 @@
import pytest
-from httpcore import SOCKSProxy, Origin, ProxyError
-from httpcore.backends.mock import MockBackend
+from httpcore import MockBackend, SOCKSProxy, Origin, ProxyError
diff --git a/unasync.py b/unasync.py
index 60888596..d3607cd4 100755
--- a/unasync.py
+++ b/unasync.py
@@ -4,7 +4,7 @@
import sys
SUBS = [
- ('from ..backends.auto import AutoBackend', 'from ..backends.sync import SyncBackend'),
+ ('from .._backends.auto import AutoBackend', 'from .._backends.sync import SyncBackend'),
('import trio as concurrency', 'from tests import concurrency'),
('AsyncByteStream', 'SyncByteStream'),
('AsyncIterator', 'Iterator'),