Upgrade httpx

This commit is contained in:
Adam Hopkins 2020-09-27 02:58:36 +03:00
parent 58e15134fd
commit eb8df1fc18
10 changed files with 94 additions and 150 deletions

View File

@ -249,10 +249,7 @@ def raw(
:param content_type: the content type (string) of the response. :param content_type: the content type (string) of the response.
""" """
return HTTPResponse( return HTTPResponse(
body=body, body=body, status=status, headers=headers, content_type=content_type,
status=status,
headers=headers,
content_type=content_type,
) )

View File

@ -11,6 +11,8 @@ from sanic.response import text
ASGI_HOST = "mockserver" ASGI_HOST = "mockserver"
ASGI_PORT = 1234
ASGI_BASE_URL = f"http://{ASGI_HOST}:{ASGI_PORT}"
HOST = "127.0.0.1" HOST = "127.0.0.1"
PORT = None PORT = None
@ -103,9 +105,7 @@ class SanicTestClient:
if self.port: if self.port:
server_kwargs = dict( server_kwargs = dict(
host=host or self.host, host=host or self.host, port=self.port, **server_kwargs,
port=self.port,
**server_kwargs,
) )
host, port = host or self.host, self.port host, port = host or self.host, self.port
else: else:
@ -195,24 +195,19 @@ async def app_call_with_return(self, scope, receive, send):
return await asgi_app() return await asgi_app()
class SanicASGIDispatch(httpx.ASGIDispatch):
pass
class SanicASGITestClient(httpx.AsyncClient): class SanicASGITestClient(httpx.AsyncClient):
def __init__( def __init__(
self, self,
app, app,
base_url: str = f"http://{ASGI_HOST}", base_url: str = ASGI_BASE_URL,
suppress_exceptions: bool = False, suppress_exceptions: bool = False,
) -> None: ) -> None:
app.__class__.__call__ = app_call_with_return app.__class__.__call__ = app_call_with_return
app.asgi = True app.asgi = True
self.app = app self.app = app
transport = httpx.ASGITransport(app=app, client=(ASGI_HOST, ASGI_PORT))
dispatch = SanicASGIDispatch(app=app, client=(ASGI_HOST, PORT or 0)) super().__init__(transport=transport, base_url=base_url)
super().__init__(dispatch=dispatch, base_url=base_url)
self.last_request = None self.last_request = None

View File

@ -81,7 +81,7 @@ requirements = [
"aiofiles>=0.3.0", "aiofiles>=0.3.0",
"websockets>=8.1,<9.0", "websockets>=8.1,<9.0",
"multidict>=4.0,<5.0", "multidict>=4.0,<5.0",
"httpx==0.11.1", "httpx==0.15.4",
] ]
tests_require = [ tests_require = [

View File

@ -1,9 +1,9 @@
import asyncio import asyncio
import logging import logging
import sys import sys
from unittest.mock import patch
from inspect import isawaitable from inspect import isawaitable
from unittest.mock import patch
import pytest import pytest

View File

@ -3,6 +3,7 @@ import asyncio
from asyncio import sleep as aio_sleep from asyncio import sleep as aio_sleep
from json import JSONDecodeError from json import JSONDecodeError
import httpcore
import httpx import httpx
from sanic import Sanic, server from sanic import Sanic, server
@ -12,67 +13,26 @@ from sanic.testing import HOST, SanicTestClient
CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True} CONFIG_FOR_TESTS = {"KEEP_ALIVE_TIMEOUT": 2, "KEEP_ALIVE": True}
old_conn = None
PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port PORT = 42101 # test_keep_alive_timeout_reuse doesn't work with random port
from httpcore._async.base import ConnectionState
from httpcore._async.connection import AsyncHTTPConnection
from httpcore._types import Origin
class ReusableSanicConnectionPool(
httpx.dispatch.connection_pool.ConnectionPool
):
@property
def cert(self):
return self.ssl.cert
@property class ReusableSanicConnectionPool(httpcore.AsyncConnectionPool):
def verify(self): last_reused_connection = None
return self.ssl.verify
@property async def _get_connection_from_pool(self, *args, **kwargs):
def trust_env(self): conn = await super()._get_connection_from_pool(*args, **kwargs)
return self.ssl.trust_env self.__class__.last_reused_connection = conn
return conn
@property
def http2(self):
return self.ssl.http2
async def acquire_connection(self, origin, timeout):
global old_conn
connection = self.pop_connection(origin)
if connection is None:
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
ssl_config = httpx.config.SSLConfig(
cert=self.cert,
verify=self.verify,
trust_env=self.trust_env,
http2=self.http2,
)
connection = httpx.dispatch.connection.HTTPConnection(
origin,
ssl=ssl_config,
backend=self.backend,
release_func=self.release_connection,
uds=self.uds,
)
self.active_connections.add(connection)
if old_conn is not None:
if old_conn != connection:
raise RuntimeError(
"We got a new connection, wanted the same one!"
)
old_conn = connection
return connection
class ResusableSanicSession(httpx.AsyncClient): class ResusableSanicSession(httpx.AsyncClient):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
dispatch = ReusableSanicConnectionPool() transport = ReusableSanicConnectionPool()
super().__init__(dispatch=dispatch, *args, **kwargs) super().__init__(transport=transport, *args, **kwargs)
class ReuseableSanicTestClient(SanicTestClient): class ReuseableSanicTestClient(SanicTestClient):
@ -258,6 +218,7 @@ def test_keep_alive_timeout_reuse():
request, response = client.get("/1") request, response = client.get("/1")
assert response.status == 200 assert response.status == 200
assert response.text == "OK" assert response.text == "OK"
assert ReusableSanicConnectionPool.last_reused_connection
finally: finally:
client.kill_server() client.kill_server()
@ -270,20 +231,15 @@ def test_keep_alive_client_timeout():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop)
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
try: request, response = client.get(
request, response = client.get( "/1", headers=headers, request_keepalive=1
"/1", headers=headers, request_keepalive=1 )
) assert response.status == 200
assert response.status == 200 assert response.text == "OK"
assert response.text == "OK" loop.run_until_complete(aio_sleep(2))
loop.run_until_complete(aio_sleep(2)) exception = None
exception = None request, response = client.get("/1", request_keepalive=1)
request, response = client.get("/1", request_keepalive=1) assert ReusableSanicConnectionPool.last_reused_connection is None
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert "got a new connection" in exception.args[0]
finally: finally:
client.kill_server() client.kill_server()
@ -298,22 +254,14 @@ def test_keep_alive_server_timeout():
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop)
headers = {"Connection": "keep-alive"} headers = {"Connection": "keep-alive"}
try: request, response = client.get(
request, response = client.get( "/1", headers=headers, request_keepalive=60
"/1", headers=headers, request_keepalive=60
)
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
except ValueError as e:
exception = e
assert exception is not None
assert isinstance(exception, ValueError)
assert (
"Connection reset" in exception.args[0]
or "got a new connection" in exception.args[0]
) )
assert response.status == 200
assert response.text == "OK"
loop.run_until_complete(aio_sleep(3))
exception = None
request, response = client.get("/1", request_keepalive=60)
assert ReusableSanicConnectionPool.last_reused_connection is None
finally: finally:
client.kill_server() client.kill_server()

View File

@ -1,13 +1,14 @@
import pytest
import asyncio import asyncio
import pytest
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.exceptions import HeaderExpectationFailed from sanic.exceptions import HeaderExpectationFailed
from sanic.request import StreamBuffer from sanic.request import StreamBuffer
from sanic.response import json, stream, text from sanic.response import json, stream, text
from sanic.server import HttpProtocol
from sanic.views import CompositionView, HTTPMethodView from sanic.views import CompositionView, HTTPMethodView
from sanic.views import stream as stream_decorator from sanic.views import stream as stream_decorator
from sanic.server import HttpProtocol
data = "abc" * 1_000_000 data = "abc" * 1_000_000

View File

@ -1,64 +1,54 @@
import asyncio import asyncio
from typing import cast
import httpcore
import httpx import httpx
from httpcore._async.base import (
AsyncByteStream,
AsyncHTTPTransport,
ConnectionState,
NewConnectionRequired,
)
from httpcore._async.connection import AsyncHTTPConnection
from httpcore._async.connection_pool import ResponseByteStream
from httpcore._exceptions import LocalProtocolError, UnsupportedProtocol
from httpcore._types import TimeoutDict
from httpcore._utils import url_to_origin
from sanic import Sanic from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.testing import SanicTestClient from sanic.testing import SanicTestClient
class DelayableHTTPConnection(httpx.dispatch.connection.HTTPConnection): class DelayableHTTPConnection(httpcore._async.connection.AsyncHTTPConnection):
def __init__(self, *args, **kwargs): async def arequest(self, *args, **kwargs):
self._request_delay = None await asyncio.sleep(2)
if "request_delay" in kwargs: return await super().arequest(*args, **kwargs)
self._request_delay = kwargs.pop("request_delay")
super().__init__(*args, **kwargs)
async def send(self, request, timeout=None):
if self.connection is None:
self.connection = await self.connect(timeout=timeout)
async def _open_socket(self, *args, **kwargs):
retval = await super()._open_socket(*args, **kwargs)
if self._request_delay: if self._request_delay:
await asyncio.sleep(self._request_delay) await asyncio.sleep(self._request_delay)
return retval
response = await self.connection.send(request, timeout=timeout)
return response
class DelayableSanicConnectionPool( class DelayableSanicConnectionPool(httpcore.AsyncConnectionPool):
httpx.dispatch.connection_pool.ConnectionPool
):
def __init__(self, request_delay=None, *args, **kwargs): def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay self._request_delay = request_delay
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
async def acquire_connection(self, origin, timeout=None): async def _add_to_pool(self, connection, timeout):
connection = self.pop_connection(origin) connection.__class__ = DelayableHTTPConnection
connection._request_delay = self._request_delay
if connection is None: await super()._add_to_pool(connection, timeout)
pool_timeout = None if timeout is None else timeout.pool_timeout
await self.max_connections.acquire(timeout=pool_timeout)
connection = DelayableHTTPConnection(
origin,
ssl=self.ssl,
backend=self.backend,
release_func=self.release_connection,
uds=self.uds,
request_delay=self._request_delay,
)
self.active_connections.add(connection)
return connection
class DelayableSanicSession(httpx.AsyncClient): class DelayableSanicSession(httpx.AsyncClient):
def __init__(self, request_delay=None, *args, **kwargs) -> None: def __init__(self, request_delay=None, *args, **kwargs) -> None:
dispatch = DelayableSanicConnectionPool(request_delay=request_delay) transport = DelayableSanicConnectionPool(request_delay=request_delay)
super().__init__(dispatch=dispatch, *args, **kwargs) super().__init__(transport=transport, *args, **kwargs)
class DelayableSanicTestClient(SanicTestClient): class DelayableSanicTestClient(SanicTestClient):

View File

@ -12,7 +12,14 @@ from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters
from sanic.response import html, json, text from sanic.response import html, json, text
from sanic.testing import ASGI_HOST, HOST, PORT, SanicTestClient from sanic.testing import (
ASGI_BASE_URL,
ASGI_HOST,
ASGI_PORT,
HOST,
PORT,
SanicTestClient,
)
# ------------------------------------------------------------ # # ------------------------------------------------------------ #
@ -59,7 +66,10 @@ async def test_ip_asgi(app):
request, response = await app.asgi_client.get("/") request, response = await app.asgi_client.get("/")
assert response.text == "http://mockserver/" if response.text.endswith("/") and not ASGI_BASE_URL.endswith("/"):
response.text[:-1] == ASGI_BASE_URL
else:
assert response.text == ASGI_BASE_URL
def test_text(app): def test_text(app):
@ -573,7 +583,7 @@ async def test_standard_forwarded_asgi(app):
assert response.json() == {"for": "127.0.0.2", "proto": "ws"} assert response.json() == {"for": "127.0.0.2", "proto": "ws"}
assert request.remote_addr == "127.0.0.2" assert request.remote_addr == "127.0.0.2"
assert request.scheme == "ws" assert request.scheme == "ws"
assert request.server_port == 80 assert request.server_port == ASGI_PORT
app.config.FORWARDED_SECRET = "mySecret" app.config.FORWARDED_SECRET = "mySecret"
request, response = await app.asgi_client.get("/", headers=headers) request, response = await app.asgi_client.get("/", headers=headers)
@ -1044,9 +1054,9 @@ def test_url_attributes_no_ssl(app, path, query, expected_url):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path,query,expected_url", "path,query,expected_url",
[ [
("/foo", "", "http://{}/foo"), ("/foo", "", "{}/foo"),
("/bar/baz", "", "http://{}/bar/baz"), ("/bar/baz", "", "{}/bar/baz"),
("/moo/boo", "arg1=val1", "http://{}/moo/boo?arg1=val1"), ("/moo/boo", "arg1=val1", "{}/moo/boo?arg1=val1"),
], ],
) )
@pytest.mark.asyncio @pytest.mark.asyncio
@ -1057,7 +1067,7 @@ async def test_url_attributes_no_ssl_asgi(app, path, query, expected_url):
app.add_route(handler, path) app.add_route(handler, path)
request, response = await app.asgi_client.get(path + f"?{query}") request, response = await app.asgi_client.get(path + f"?{query}")
assert request.url == expected_url.format(ASGI_HOST) assert request.url == expected_url.format(ASGI_BASE_URL)
parsed = urlparse(request.url) parsed = urlparse(request.url)

View File

@ -4,6 +4,7 @@ import os
import subprocess import subprocess
import sys import sys
import httpcore
import httpx import httpx
import pytest import pytest
@ -139,8 +140,9 @@ def test_unix_connection():
@app.listener("after_server_start") @app.listener("after_server_start")
async def client(app, loop): async def client(app, loop):
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
try: try:
async with httpx.AsyncClient(uds=SOCKPATH) as client: async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://myhost.invalid/") r = await client.get("http://myhost.invalid/")
assert r.status_code == 200 assert r.status_code == 200
assert r.text == os.path.abspath(SOCKPATH) assert r.text == os.path.abspath(SOCKPATH)
@ -179,8 +181,9 @@ async def test_zero_downtime():
from time import monotonic as current_time from time import monotonic as current_time
async def client(): async def client():
transport = httpcore.AsyncConnectionPool(uds=SOCKPATH)
for _ in range(40): for _ in range(40):
async with httpx.AsyncClient(uds=SOCKPATH) as client: async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1") r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200 assert r.status_code == 200
assert r.text == f"Slept 0.1 seconds.\n" assert r.text == f"Slept 0.1 seconds.\n"

View File

@ -13,7 +13,7 @@ deps =
pytest-sanic pytest-sanic
pytest-sugar pytest-sugar
httpcore==0.3.0 httpcore==0.3.0
httpx==0.11.1 httpx==0.15.4
chardet<=2.3.0 chardet<=2.3.0
beautifulsoup4 beautifulsoup4
gunicorn gunicorn