]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Restructuring
authorTom Christie <tom@tomchristie.com>
Mon, 29 Apr 2019 10:54:48 +0000 (11:54 +0100)
committerTom Christie <tom@tomchristie.com>
Mon, 29 Apr 2019 10:54:48 +0000 (11:54 +0100)
21 files changed:
httpcore/__init__.py
httpcore/adapters/__init__.py [new file with mode: 0644]
httpcore/adapters/authentication.py [moved from httpcore/auth.py with 78% similarity]
httpcore/adapters/cookies.py [moved from httpcore/cookies.py with 85% similarity]
httpcore/adapters/environment.py [moved from httpcore/environment.py with 90% similarity]
httpcore/adapters/redirects.py [moved from httpcore/redirects.py with 77% similarity]
httpcore/client.py
httpcore/config.py
httpcore/dispatch/__init__.py [new file with mode: 0644]
httpcore/dispatch/connection.py [moved from httpcore/connection.py with 91% similarity]
httpcore/dispatch/connection_pool.py [moved from httpcore/connection_pool.py with 96% similarity]
httpcore/dispatch/http11.py [moved from httpcore/http11.py with 93% similarity]
httpcore/dispatch/http2.py [moved from httpcore/http2.py with 94% similarity]
httpcore/interfaces.py [moved from httpcore/adapters.py with 60% similarity]
httpcore/models.py
httpcore/status_codes.py
httpcore/streams.py
httpcore/structures.py [deleted file]
httpcore/sync.py
httpcore/utils.py
tests/adapters/test_redirects.py [new file with mode: 0644]

index ef8d6a5d449feb5dbc46e035f36e421d33a03e9f..b81c877924683fce17c8d36e51bb1d4c21173664 100644 (file)
@@ -1,8 +1,10 @@
-from .adapters import Adapter
+from .adapters.redirects import RedirectAdapter
 from .client import Client
 from .config import PoolLimits, SSLConfig, TimeoutConfig
-from .connection import HTTPConnection
-from .connection_pool import ConnectionPool
+from .dispatch.connection import HTTPConnection
+from .dispatch.connection_pool import ConnectionPool
+from .dispatch.http2 import HTTP2Connection
+from .dispatch.http11 import HTTP11Connection
 from .exceptions import (
     ConnectTimeout,
     PoolTimeout,
@@ -12,9 +14,9 @@ from .exceptions import (
     StreamConsumed,
     Timeout,
 )
-from .http2 import HTTP2Connection
-from .http11 import HTTP11Connection
+from .interfaces import Adapter
 from .models import URL, Headers, Origin, Request, Response
+from .status_codes import codes
 from .streams import BaseReader, BaseWriter, Protocol, Reader, Writer, connect
 from .sync import SyncClient, SyncConnectionPool
 
diff --git a/httpcore/adapters/__init__.py b/httpcore/adapters/__init__.py
new file mode 100644 (file)
index 0000000..8d4629f
--- /dev/null
@@ -0,0 +1,4 @@
+"""
+Adapter classes layer additional behavior over the raw dispatching of the
+HTTP request/response.
+"""
similarity index 78%
rename from httpcore/auth.py
rename to httpcore/adapters/authentication.py
index 38c434a241433133f7f21fe72b84a850d0c94207..cb5ae99ac15d32195ae9e4c6ef771c7181e19995 100644 (file)
@@ -1,10 +1,10 @@
 import typing
 
-from .adapters import Adapter
-from .models import Request, Response
+from ..interfaces import Adapter
+from ..models import Request, Response
 
 
-class AuthAdapter(Adapter):
+class AuthenticationAdapter(Adapter):
     def __init__(self, dispatch: Adapter):
         self.dispatch = dispatch
 
similarity index 85%
rename from httpcore/cookies.py
rename to httpcore/adapters/cookies.py
index ed9e97c4f97463b0ded987bd04310a2cb7aaf188..1105152149ce1f2d2faa1dc6db4df3b6379b8ef7 100644 (file)
@@ -1,7 +1,7 @@
 import typing
 
-from .adapters import Adapter
-from .models import Request, Response
+from ..interfaces import Adapter
+from ..models import Request, Response
 
 
 class CookieAdapter(Adapter):
similarity index 90%
rename from httpcore/environment.py
rename to httpcore/adapters/environment.py
index 5065eed884b1b5ab3f4a9213d0d87007f1529b6e..9840a926783b19c789a76500b43c9c5594692528 100644 (file)
@@ -1,7 +1,7 @@
 import typing
 
-from .adapters import Adapter
-from .models import Request, Response
+from ..interfaces import Adapter
+from ..models import Request, Response
 
 
 class EnvironmentAdapter(Adapter):
similarity index 77%
rename from httpcore/redirects.py
rename to httpcore/adapters/redirects.py
index 557b7468a608fc8c76b5b1971dfcd78d7fd24667..0cf3f9fbb740eb741a7863c763b54aad4b12cad7 100644 (file)
@@ -1,15 +1,16 @@
 import typing
 from urllib.parse import urljoin, urlparse
 
-from .adapters import Adapter
-from .exceptions import TooManyRedirects
-from .models import URL, Request, Response
-from .status_codes import codes
-from .utils import requote_uri
+from ..config import DEFAULT_MAX_REDIRECTS
+from ..exceptions import TooManyRedirects
+from ..interfaces import Adapter
+from ..models import URL, Request, Response
+from ..status_codes import codes
+from ..utils import requote_uri
 
 
 class RedirectAdapter(Adapter):
-    def __init__(self, dispatch: Adapter, max_redirects: int):
+    def __init__(self, dispatch: Adapter, max_redirects: int = DEFAULT_MAX_REDIRECTS):
         self.dispatch = dispatch
         self.max_redirects = max_redirects
 
@@ -37,7 +38,7 @@ class RedirectAdapter(Adapter):
     def build_redirect_request(self, request: Request, response: Response) -> Request:
         method = self.redirect_method(request, response)
         url = self.redirect_url(request, response)
-        raise NotImplementedError()
+        return Request(method=method, url=url)
 
     def redirect_method(self, request: Request, response: Response) -> str:
         """
@@ -47,17 +48,17 @@ class RedirectAdapter(Adapter):
         method = request.method
 
         # https://tools.ietf.org/html/rfc7231#section-6.4.4
-        if response.status_code == codes["see_other"] and method != "HEAD":
+        if response.status_code == codes.see_other and method != "HEAD":
             method = "GET"
 
         # Do what the browsers do, despite standards...
-        # First, turn 302s into GETs.
-        if response.status_code == codes["found"] and method != "HEAD":
+        # Turn 302s into GETs.
+        if response.status_code == codes.found and method != "HEAD":
             method = "GET"
 
-        # Second, if a POST is responded to with a 301, turn it into a GET.
-        # This bizarre behaviour is explained in Issue 1704.
-        if response.status_code == codes["moved"] and method == "POST":
+        # If a POST is responded to with a 301, turn it into a GET.
+        # This bizarre behaviour is explained in 'requests' issue 1704.
+        if response.status_code == codes.moved_permanently and method == "POST":
             method = "GET"
 
         return method
index 2d98ba30784da4551b815d4af28dd27ef086950d..139a56867997f1ea6afb4ae79e6a94cf969f6c81 100644 (file)
@@ -1,7 +1,10 @@
 import typing
 from types import TracebackType
 
-from .auth import AuthAdapter
+from .adapters.authentication import AuthenticationAdapter
+from .adapters.cookies import CookieAdapter
+from .adapters.environment import EnvironmentAdapter
+from .adapters.redirects import RedirectAdapter
 from .config import (
     DEFAULT_MAX_REDIRECTS,
     DEFAULT_POOL_LIMITS,
@@ -11,11 +14,8 @@ from .config import (
     SSLConfig,
     TimeoutConfig,
 )
-from .connection_pool import ConnectionPool
-from .cookies import CookieAdapter
-from .environment import EnvironmentAdapter
+from .dispatch.connection_pool import ConnectionPool
 from .models import URL, Request, Response
-from .redirects import RedirectAdapter
 
 
 class Client:
@@ -28,7 +28,7 @@ class Client:
     ):
         connection_pool = ConnectionPool(ssl=ssl, timeout=timeout, limits=limits)
         cookie_adapter = CookieAdapter(dispatch=connection_pool)
-        auth_adapter = AuthAdapter(dispatch=cookie_adapter)
+        auth_adapter = AuthenticationAdapter(dispatch=cookie_adapter)
         redirect_adapter = RedirectAdapter(
             dispatch=auth_adapter, max_redirects=max_redirects
         )
index 5ce24707c3bf8a0bcdd5e11733f7c0c51ec481b2..ef24a8b1a0046893140e1195d247fbe09fd79e7c 100644 (file)
@@ -175,4 +175,4 @@ DEFAULT_SSL_CONFIG = SSLConfig(cert=None, verify=True)
 DEFAULT_TIMEOUT_CONFIG = TimeoutConfig(timeout=5.0)
 DEFAULT_POOL_LIMITS = PoolLimits(soft_limit=10, hard_limit=100, pool_timeout=5.0)
 DEFAULT_CA_BUNDLE_PATH = certifi.where()
-DEFAULT_MAX_REDIRECTS = 30
+DEFAULT_MAX_REDIRECTS = 20
diff --git a/httpcore/dispatch/__init__.py b/httpcore/dispatch/__init__.py
new file mode 100644 (file)
index 0000000..4057d6e
--- /dev/null
@@ -0,0 +1,4 @@
+"""
+Dispatch classes handle the raw network connections and the implementation
+details of making the HTTP request and receiving the response.
+"""
similarity index 91%
rename from httpcore/connection.py
rename to httpcore/dispatch/connection.py
index 1662018a59067f719c9b7f31162d78dfc3faba51..45134456b9ea2468225b29c587b9aa6dcecdff75 100644 (file)
@@ -4,13 +4,18 @@ import typing
 import h2.connection
 import h11
 
-from .adapters import Adapter
-from .config import DEFAULT_SSL_CONFIG, DEFAULT_TIMEOUT_CONFIG, SSLConfig, TimeoutConfig
-from .exceptions import ConnectTimeout
+from ..config import (
+    DEFAULT_SSL_CONFIG,
+    DEFAULT_TIMEOUT_CONFIG,
+    SSLConfig,
+    TimeoutConfig,
+)
+from ..exceptions import ConnectTimeout
+from ..interfaces import Adapter
+from ..models import Origin, Request, Response
+from ..streams import Protocol, connect
 from .http2 import HTTP2Connection
 from .http11 import HTTP11Connection
-from .models import Origin, Request, Response
-from .streams import Protocol, connect
 
 # Callback signature: async def callback(conn: HTTPConnection) -> None
 ReleaseCallback = typing.Callable[["HTTPConnection"], typing.Awaitable[None]]
similarity index 96%
rename from httpcore/connection_pool.py
rename to httpcore/dispatch/connection_pool.py
index ab394a19630cfd5168c507c2160cc242db547ded..a536d5f38d45f3b87204c53fdce794bbf14f79fa 100644 (file)
@@ -1,8 +1,7 @@
 import collections.abc
 import typing
 
-from .adapters import Adapter
-from .config import (
+from ..config import (
     DEFAULT_CA_BUNDLE_PATH,
     DEFAULT_POOL_LIMITS,
     DEFAULT_SSL_CONFIG,
@@ -11,10 +10,11 @@ from .config import (
     SSLConfig,
     TimeoutConfig,
 )
+from ..exceptions import PoolTimeout
+from ..interfaces import Adapter
+from ..models import Origin, Request, Response
+from ..streams import PoolSemaphore
 from .connection import HTTPConnection
-from .exceptions import PoolTimeout
-from .models import Origin, Request, Response
-from .streams import PoolSemaphore
 
 CONNECTIONS_DICT = typing.Dict[Origin, typing.List[HTTPConnection]]
 
similarity index 93%
rename from httpcore/http11.py
rename to httpcore/dispatch/http11.py
index 97e8b6a82a41dc4788795ee4e223b2fd6f4e7375..e4a5c83a565bf6309c953105ac718951e2b0aac2 100644 (file)
@@ -2,11 +2,16 @@ import typing
 
 import h11
 
-from .adapters import Adapter
-from .config import DEFAULT_SSL_CONFIG, DEFAULT_TIMEOUT_CONFIG, SSLConfig, TimeoutConfig
-from .exceptions import ConnectTimeout, ReadTimeout
-from .models import Request, Response
-from .streams import BaseReader, BaseWriter
+from ..config import (
+    DEFAULT_SSL_CONFIG,
+    DEFAULT_TIMEOUT_CONFIG,
+    SSLConfig,
+    TimeoutConfig,
+)
+from ..exceptions import ConnectTimeout, ReadTimeout
+from ..interfaces import Adapter
+from ..models import Request, Response
+from ..streams import BaseReader, BaseWriter
 
 H11Event = typing.Union[
     h11.Request,
similarity index 94%
rename from httpcore/http2.py
rename to httpcore/dispatch/http2.py
index 40448176230f39e6d20cf4bd0eaf8e76c0c54cae..7bd124b768bf099d3e0c04a7a9fda804294557fd 100644 (file)
@@ -4,11 +4,16 @@ import typing
 import h2.connection
 import h2.events
 
-from .adapters import Adapter
-from .config import DEFAULT_SSL_CONFIG, DEFAULT_TIMEOUT_CONFIG, SSLConfig, TimeoutConfig
-from .exceptions import ConnectTimeout, ReadTimeout
-from .models import Request, Response
-from .streams import BaseReader, BaseWriter
+from ..config import (
+    DEFAULT_SSL_CONFIG,
+    DEFAULT_TIMEOUT_CONFIG,
+    SSLConfig,
+    TimeoutConfig,
+)
+from ..exceptions import ConnectTimeout, ReadTimeout
+from ..interfaces import Adapter
+from ..models import Request, Response
+from ..streams import BaseReader, BaseWriter
 
 OptionalTimeout = typing.Optional[TimeoutConfig]
 
similarity index 60%
rename from httpcore/adapters.py
rename to httpcore/interfaces.py
index 72d5880f0de6cbead08bb6dcdb147f490de52c7c..d3cdca47dcd22929b2276a42167ab0005c0ee36c 100644 (file)
@@ -1,8 +1,11 @@
 import typing
 from types import TracebackType
 
+from .config import TimeoutConfig
 from .models import URL, Request, Response
 
+OptionalTimeout = typing.Optional[TimeoutConfig]
+
 
 class Adapter:
     async def request(
@@ -38,3 +41,27 @@ class Adapter:
         traceback: TracebackType = None,
     ) -> None:
         await self.close()
+
+
+class BaseReader:
+    async def read(self, n: int, timeout: OptionalTimeout = None) -> bytes:
+        raise NotImplementedError()  # pragma: no cover
+
+
+class BaseWriter:
+    def write_no_block(self, data: bytes) -> None:
+        raise NotImplementedError()  # pragma: no cover
+
+    async def write(self, data: bytes, timeout: OptionalTimeout = None) -> None:
+        raise NotImplementedError()  # pragma: no cover
+
+    async def close(self) -> None:
+        raise NotImplementedError()  # pragma: no cover
+
+
+class BasePoolSemaphore:
+    async def acquire(self) -> None:
+        raise NotImplementedError()  # pragma: no cover
+
+    def release(self) -> None:
+        raise NotImplementedError()  # pragma: no cover
index 791bbc906fc5a1310d2de85a5ac147aaed8ea710..b08da934a346387e5e5c0b44f204e55f3339649b 100644 (file)
@@ -70,6 +70,9 @@ class URL:
     def origin(self) -> "Origin":
         return Origin(self)
 
+    def __str__(self) -> str:
+        return self.components.geturl()
+
 
 class Origin:
     def __init__(self, url: typing.Union[str, URL]) -> None:
index 9265a71ba11dfb1d3b29cc31ef48536b7804de95..19b849f045119a18505344389afd65b2bdc02c94 100644 (file)
-"""
-The ``codes`` object defines a mapping from common names for HTTP statuses
-to their numerical codes, accessible either as attributes or as dictionary
-items.
->>> requests.codes['temporary_redirect']
-307
->>> requests.codes.teapot
-418
-Some codes have multiple names, and both upper- and lower-case versions of
-the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
-``codes.okay`` all correspond to the HTTP status code 200.
-"""
-
-import typing
-
-from .structures import LookupDict
-
-_codes = {
-    # Informational.
-    100: ("continue",),
-    101: ("switching_protocols",),
-    102: ("processing",),
-    103: ("checkpoint",),
-    122: ("uri_too_long", "request_uri_too_long"),
-    200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
-    201: ("created",),
-    202: ("accepted",),
-    203: ("non_authoritative_info", "non_authoritative_information"),
-    204: ("no_content",),
-    205: ("reset_content", "reset"),
-    206: ("partial_content", "partial"),
-    207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"),
-    208: ("already_reported",),
-    226: ("im_used",),
-    # Redirection.
-    300: ("multiple_choices",),
-    301: ("moved_permanently", "moved", "\\o-"),
-    302: ("found",),
-    303: ("see_other", "other"),
-    304: ("not_modified",),
-    305: ("use_proxy",),
-    306: ("switch_proxy",),
-    307: ("temporary_redirect", "temporary_moved", "temporary"),
-    308: (
-        "permanent_redirect",
-        "resume_incomplete",
-        "resume",
-    ),  # These 2 to be removed in 3.0
-    # Client Error.
-    400: ("bad_request", "bad"),
-    401: ("unauthorized",),
-    402: ("payment_required", "payment"),
-    403: ("forbidden",),
-    404: ("not_found", "-o-"),
-    405: ("method_not_allowed", "not_allowed"),
-    406: ("not_acceptable",),
-    407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"),
-    408: ("request_timeout", "timeout"),
-    409: ("conflict",),
-    410: ("gone",),
-    411: ("length_required",),
-    412: ("precondition_failed", "precondition"),
-    413: ("request_entity_too_large",),
-    414: ("request_uri_too_large",),
-    415: ("unsupported_media_type", "unsupported_media", "media_type"),
-    416: (
-        "requested_range_not_satisfiable",
-        "requested_range",
-        "range_not_satisfiable",
-    ),
-    417: ("expectation_failed",),
-    418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
-    421: ("misdirected_request",),
-    422: ("unprocessable_entity", "unprocessable"),
-    423: ("locked",),
-    424: ("failed_dependency", "dependency"),
-    425: ("unordered_collection", "unordered"),
-    426: ("upgrade_required", "upgrade"),
-    428: ("precondition_required", "precondition"),
-    429: ("too_many_requests", "too_many"),
-    431: ("header_fields_too_large", "fields_too_large"),
-    444: ("no_response", "none"),
-    449: ("retry_with", "retry"),
-    450: ("blocked_by_windows_parental_controls", "parental_controls"),
-    451: ("unavailable_for_legal_reasons", "legal_reasons"),
-    499: ("client_closed_request",),
-    # Server Error.
-    500: ("internal_server_error", "server_error", "/o\\", "✗"),
-    501: ("not_implemented",),
-    502: ("bad_gateway",),
-    503: ("service_unavailable", "unavailable"),
-    504: ("gateway_timeout",),
-    505: ("http_version_not_supported", "http_version"),
-    506: ("variant_also_negotiates",),
-    507: ("insufficient_storage",),
-    509: ("bandwidth_limit_exceeded", "bandwidth"),
-    510: ("not_extended",),
-    511: ("network_authentication_required", "network_auth", "network_authentication"),
-}  # type: typing.Dict[int, typing.Sequence[str]]
-
-codes = LookupDict(name="status_codes")
-
-
-def _init() -> None:
-    for code, titles in _codes.items():
-        for title in titles:
-            setattr(codes, title, code)
-            if not title.startswith(("\\", "/")):
-                setattr(codes, title.upper(), code)
-
-    def doc(code: int) -> str:
-        names = ", ".join("``%s``" % n for n in _codes[code])
-        return "* %d: %s" % (code, names)
-
-    global __doc__
-    __doc__ = (
-        __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes))
-        if __doc__ is not None
-        else None
-    )
-
-
-_init()
+import enum
+
+codes = enum.IntEnum(
+    "StatusCode",
+    [
+        ("continue", 100),
+        ("switching_protocols", 101),
+        ("ok", 200),
+        ("created", 201),
+        ("accepted", 202),
+        ("non_authoritative_information", 203),
+        ("no_content", 204),
+        ("reset_content", 205),
+        ("partial_content", 206),
+        ("multi_status", 207),
+        ("multiple_choices", 300),
+        ("moved_permanently", 301),
+        ("found", 302),
+        ("see_other", 303),
+        ("not_modified", 304),
+        ("use_proxy", 305),
+        ("reserved", 306),
+        ("temporary_redirect", 307),
+        ("bad_request", 400),
+        ("unauthorized", 401),
+        ("payment_required", 402),
+        ("forbidden", 403),
+        ("not_found", 404),
+        ("method_not_allowed", 405),
+        ("not_acceptable", 406),
+        ("proxy_authentication_required", 407),
+        ("request_timeout", 408),
+        ("conflict", 409),
+        ("gone", 410),
+        ("length_required", 411),
+        ("precondition_failed", 412),
+        ("request_entity_too_large", 413),
+        ("request_uri_too_long", 414),
+        ("unsupported_media_type", 415),
+        ("requested_range_not_satisfiable", 416),
+        ("expectation_failed", 417),
+        ("unprocessable_entity", 422),
+        ("locked", 423),
+        ("failed_dependency", 424),
+        ("precondition_required", 428),
+        ("too_many_requests", 429),
+        ("request_header_fields_too_large", 431),
+        ("unavailable_for_legal_reasons", 451),
+        ("internal_server_error", 500),
+        ("not_implemented", 501),
+        ("bad_gateway", 502),
+        ("service_unavailable", 503),
+        ("gateway_timeout", 504),
+        ("http_version_not_supported", 505),
+        ("insufficient_storage", 507),
+        ("network_authentication_required", 511),
+    ],
+)
index 03bba17ca6719ac74a9b2d0e8b47129b16bb0b3a..a8590db42099b7c39832f67a47761254f0259595 100644 (file)
@@ -15,6 +15,7 @@ import typing
 
 from .config import DEFAULT_TIMEOUT_CONFIG, PoolLimits, TimeoutConfig
 from .exceptions import ConnectTimeout, PoolTimeout, ReadTimeout, WriteTimeout
+from .interfaces import BasePoolSemaphore, BaseReader, BaseWriter
 
 OptionalTimeout = typing.Optional[TimeoutConfig]
 
@@ -24,30 +25,6 @@ class Protocol(enum.Enum):
     HTTP_2 = 2
 
 
-class BaseReader:
-    async def read(self, n: int, timeout: OptionalTimeout = None) -> bytes:
-        raise NotImplementedError()  # pragma: no cover
-
-
-class BaseWriter:
-    def write_no_block(self, data: bytes) -> None:
-        raise NotImplementedError()  # pragma: no cover
-
-    async def write(self, data: bytes, timeout: OptionalTimeout = None) -> None:
-        raise NotImplementedError()  # pragma: no cover
-
-    async def close(self) -> None:
-        raise NotImplementedError()  # pragma: no cover
-
-
-class BasePoolSemaphore:
-    async def acquire(self) -> None:
-        raise NotImplementedError()  # pragma: no cover
-
-    def release(self) -> None:
-        raise NotImplementedError()  # pragma: no cover
-
-
 class Reader(BaseReader):
     def __init__(
         self, stream_reader: asyncio.StreamReader, timeout: TimeoutConfig
diff --git a/httpcore/structures.py b/httpcore/structures.py
deleted file mode 100644 (file)
index 127af5c..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import typing
-
-
-class LookupDict(dict):
-    """Dictionary lookup object."""
-
-    def __init__(self, name: str = None) -> None:
-        self.name = name
-        super(LookupDict, self).__init__()
-
-    def __repr__(self) -> str:
-        return "<lookup '%s'>" % (self.name)
-
-    def __getitem__(self, key: typing.Any) -> typing.Any:
-        # We allow fall-through here, so values default to None
-
-        return self.__dict__.get(key, None)
-
-    def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
-        return self.__dict__.get(key, default)
index 89b586287a2c57d377c07bccc9cff635efb9fdd5..2d58f9a1874b53c279ba7bd190691244d00c8dd9 100644 (file)
@@ -2,9 +2,9 @@ import asyncio
 import typing
 from types import TracebackType
 
-from .adapters import Adapter
 from .config import SSLConfig, TimeoutConfig
-from .connection_pool import ConnectionPool
+from .dispatch.connection_pool import ConnectionPool
+from .interfaces import Adapter
 from .models import URL, Headers, Response
 
 
index dc7f1d282c039f5db7ae41690fc74db3dd0dc873..cd11858a8a8aab08bf1ab52883c3afd7fc03ec92 100644 (file)
@@ -9,9 +9,9 @@ UNRESERVED_SET = frozenset(
 
 
 def unquote_unreserved(uri: str) -> str:
-    """Un-escape any percent-escape sequences in a URI that are unreserved
+    """
+    Un-escape any percent-escape sequences in a URI that are unreserved
     characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
-    :rtype: str
     """
     parts = uri.split("%")
     for i in range(1, len(parts)):
diff --git a/tests/adapters/test_redirects.py b/tests/adapters/test_redirects.py
new file mode 100644 (file)
index 0000000..f4a9810
--- /dev/null
@@ -0,0 +1,31 @@
+import pytest
+
+from httpcore import Adapter, RedirectAdapter, Request, Response, codes
+
+
+class MockDispatch(Adapter):
+    def prepare_request(self, request: Request) -> None:
+        pass
+
+    async def send(self, request: Request, **options) -> Response:
+        if request.url.path == "/redirect_303":
+            return Response(
+                codes.see_other, headers=[(b"location", b"https://example.org/")]
+            )
+        elif request.url.path == "/relative_redirect":
+            return Response(codes.see_other, headers=[(b"location", b"/")])
+        return Response(codes.ok, body=b"Hello, world!")
+
+
+@pytest.mark.asyncio
+async def test_redirect_303():
+    client = RedirectAdapter(MockDispatch())
+    response = await client.request("GET", "https://example.org/redirect_303")
+    assert response.status_code == codes.ok
+
+
+@pytest.mark.asyncio
+async def test_relative_redirect():
+    client = RedirectAdapter(MockDispatch())
+    response = await client.request("GET", "https://example.org/relative_redirect")
+    assert response.status_code == codes.ok