Merge branch 'master' into master

This commit is contained in:
Adam Hopkins 2020-09-29 00:41:22 +03:00 committed by GitHub
commit 65a7060d3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 120 additions and 147 deletions

View File

@ -58,6 +58,8 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy. Sanic is a **Python 3.6+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
`Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_. `Source code on GitHub <https://github.com/huge-success/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_.
The project is maintained by the community, for the community. **Contributions are welcome!** The project is maintained by the community, for the community. **Contributions are welcome!**
@ -104,7 +106,7 @@ Hello World Example
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000) app.run(host='0.0.0.0', port=8000)
Sanic can now be easily run using ``python3 hello.py``. Sanic can now be easily run using ``sanic hello.app``.
.. code:: .. code::

View File

@ -418,6 +418,7 @@ class HttpProtocol(asyncio.Protocol):
async def stream_append(self): async def stream_append(self):
while self._body_chunks: while self._body_chunks:
body = self._body_chunks.popleft() body = self._body_chunks.popleft()
if self.request:
if self.request.stream.is_full(): if self.request.stream.is_full():
self.transport.pause_reading() self.transport.pause_reading()
await self.request.stream.put(body) await self.request.stream.put(body)

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
@ -195,24 +197,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

@ -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,7 +231,6 @@ 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
) )
@ -279,11 +239,7 @@ def test_keep_alive_client_timeout():
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)
except ValueError as e: assert ReusableSanicConnectionPool.last_reused_connection is None
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,7 +254,6 @@ 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
) )
@ -307,13 +262,6 @@ def test_keep_alive_server_timeout():
loop.run_until_complete(aio_sleep(3)) loop.run_until_complete(aio_sleep(3))
exception = None exception = None
request, response = client.get("/1", request_keepalive=60) request, response = client.get("/1", request_keepalive=60)
except ValueError as e: assert ReusableSanicConnectionPool.last_reused_connection is None
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]
)
finally: finally:
client.kill_server() client.kill_server()

View File

@ -1,9 +1,12 @@
import asyncio
import pytest 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
@ -337,6 +340,22 @@ def test_request_stream_handle_exception(app):
assert "Method GET not allowed for URL /post/random_id" in response.text assert "Method GET not allowed for URL /post/random_id" in response.text
@pytest.mark.asyncio
async def test_request_stream_unread(app):
"""ensure no error is raised when leaving unread bytes in byte-buffer"""
err = None
protocol = HttpProtocol(loop=asyncio.get_event_loop(), app=app)
try:
protocol.request = None
protocol._body_chunks.append("this is a test")
await protocol.stream_append()
except AttributeError as e:
err = e
assert err is None and not protocol._body_chunks
def test_request_stream_blueprint(app): def test_request_stream_blueprint(app):
"""for self.is_request_stream = True""" """for self.is_request_stream = True"""
bp = Blueprint("test_blueprint_request_stream_blueprint") bp = Blueprint("test_blueprint_request_stream_blueprint")

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
@ -55,6 +55,9 @@ commands =
[pytest] [pytest]
filterwarnings = filterwarnings =
ignore:.*async with lock.* instead:DeprecationWarning ignore:.*async with lock.* instead:DeprecationWarning
addopts = --strict-markers
markers =
asyncio
[testenv:security] [testenv:security]
deps = deps =