Compare commits

...

21 Commits

Author SHA1 Message Date
Adam Hopkins
5930bb67a6 Merge branch '19.12LTS' of github.com:sanic-org/sanic into 19.12LTS 2021-02-16 08:50:47 +02:00
Adam Hopkins
4c360f43fd Merge pull request #2025 from sanic-org/fix-uvloop-1912
Fix uvloop 1912
2021-02-16 08:49:45 +02:00
Adam Hopkins
6387f5ddc9 Bump version 2021-02-16 08:43:03 +02:00
Adam Hopkins
23a0308d40 Merge branch '19.12LTS' into fix-uvloop-1912 2021-02-16 08:36:42 +02:00
Adam Hopkins
a7d563d566 Merge pull request #2027 from ashleysommer/19.12LTS
Fix tox requirement install dependency problems in 19.12LTS
2021-02-16 08:36:14 +02:00
Ashley Sommer
f2d91bd4d2 Fix tox requirement install dependency problems in 19.12LTS
Remove old chardet requirement, add in our hard multidict requirement
2021-02-16 09:58:15 +10:00
Adam Hopkins
8c628c69fb fix uvloop version 2021-02-15 14:23:30 +02:00
Adam Hopkins
9b24fbb2f3 Prepare for release 2020-11-05 09:34:02 +02:00
Adam Hopkins
468f4ac7f1 Merge pull request #1966 from ashleysommer/asgs_chunk_1912
Backport #1965 to 19.12LTS
2020-11-05 09:31:01 +02:00
Adam Hopkins
be1ca93a23 Resolve mypy issue 2020-11-05 09:08:56 +02:00
Adam Hopkins
662c7c7f62 Fix linting and bump to 19.12.4 2020-11-05 08:57:40 +02:00
Ashley Sommer
3e4bec7f2c Fix Chunked Transport-Encoding in ASGI streaming response
In ASGI-mode, don't do sanic-side response chunk encoding, leave that to the ASGI-response-transport
Don't set content-length when using chunked-encoding in ASGI mode, this is incompatible with ASGI Chunked Transport-Encoding.

(cherry picked from commit c0839afdde)
2020-11-05 15:35:50 +10:00
Adam Hopkins
df4970a73d Merge branch '19.12LTS' of github.com:huge-success/sanic into 19.12LTS 2020-10-25 14:32:42 +02:00
Adam Hopkins
c5070bd449 Backport stream header fix (#1959)
Resolve headers as body in ASGI mode

* Bump version to 19.12.3

* Update multidict==5.0.0
2020-10-25 14:32:18 +02:00
Adam Hopkins
eb3d0a3f87 squash 2020-10-25 10:45:22 +02:00
Adam Hopkins
c09129ec63 Resolve headers as body in ASGI mode 2020-10-25 10:40:08 +02:00
Adam Hopkins
2a44a27236 Backport to 1912 (#1900)
* Cherry pick PRs to backport to 19.12LTS

Includes commits from:
https://github.com/huge-success/sanic/pull/1762
https://github.com/huge-success/sanic/pull/1764
https://github.com/huge-success/sanic/pull/1789

* Fix type annotation issue; run black and isort

* Update Makefile

Co-authored-by: Ashley Sommer <ashleysommer@gmail.com>
2020-07-29 13:54:33 +03:00
Adam Hopkins
bb9ff7cec1 Set version
Set version
2020-01-02 23:34:02 +02:00
Adam Hopkins
850b63f642 Merge pull request #1743 from eric-nieuwland/master
Forgotten slot
2020-01-02 23:17:35 +02:00
Eric Nieuwland
a9c669f17b Forgotten slot
Crashes the server at __init__() time
2019-12-28 15:21:27 +01:00
Stephen Sadowski
075affec23 Release v19.12.0 (#1740)
* Bumping up version from 19.9.0 to 19.12.0

* Pipfile crud removed
2019-12-27 07:10:46 -06:00
23 changed files with 220 additions and 69 deletions

View File

@@ -1,3 +1,38 @@
Version 19.12.0
===============
Bugfixes
********
- Fix blueprint middleware application
Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was
being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike.
As part of this change, the blueprint based middleware application is enforced based on where they are
registered.
- If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
- If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
- If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes (`#37 <https://github.com/huge-success/sanic/issues/37>`__)
- Fix `url_for` behavior with missing SERVER_NAME
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/huge-success/sanic/issues/1707>`__)
Improved Documentation
**********************
- Move docs from RST to MD
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
the future to update documentation. (`#1691 <https://github.com/huge-success/sanic/issues/1691>`__)
- Fix documentation for `get` and `getlist` of the `request.args`
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior (`#1704 <https://github.com/huge-success/sanic/issues/1704>`__)
Version 19.6.3
==============

View File

@@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests
fix-import: black
isort -rc sanic tests
isort sanic tests
docs-clean:

View File

@@ -1,4 +0,0 @@
Move docs from RST to MD
Moved all docs from markdown to restructured text like the rest of the docs to unify the scheme and make it easier in
the future to update documentation.

View File

@@ -1,3 +0,0 @@
Fix documentation for `get` and `getlist` of the `request.args`
Add additional example for showing the usage of `getlist` and fix the documentation string for `request.args` behavior

View File

@@ -1,4 +0,0 @@
Fix `url_for` behavior with missing SERVER_NAME
If the `SERVER_NAME` was missing in the `app.config` entity, the `url_for` on the `request` and `app` were failing
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior.

View File

@@ -1,11 +0,0 @@
Fix blueprint middleware application
Currently, any blueprint middleware registered, irrespective of which blueprint was used to do so, was
being applied to all of the routes created by the :code:`@app` and :code:`@blueprint` alike.
As part of this change, the blueprint based middleware application is enforced based on where they are
registered.
- If you register a middleware via :code:`@blueprint.middleware` then it will apply only to the routes defined by the blueprint.
- If you register a middleware via :code:`@blueprint_group.middleware` then it will apply to all blueprint based routes that are part of the group.
- If you define a middleware via :code:`@app.middleware` then it will be applied on all available routes

View File

@@ -1 +1 @@
__version__ = "19.9.0"
__version__ = "19.12.5"

View File

@@ -194,6 +194,12 @@ class Sanic:
strict_slashes = self.strict_slashes
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
args = list(signature(handler).parameters.keys())
if not args:
@@ -205,14 +211,16 @@ class Sanic:
if stream:
handler.is_stream = stream
routes = self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
routes.extend(
self.router.add(
uri=uri,
methods=methods,
handler=handler,
host=host,
strict_slashes=strict_slashes,
version=version,
name=name,
)
)
return routes, handler
@@ -476,6 +484,13 @@ class Sanic:
strict_slashes = self.strict_slashes
def response(handler):
if isinstance(handler, tuple):
# if a handler fn is already wrapped in a route, the handler
# variable will be a tuple of (existing routes, handler fn)
routes, handler = handler
else:
routes = []
async def websocket_handler(request, *args, **kwargs):
request.app = self
if not getattr(handler, "__blueprintname__", False):
@@ -516,13 +531,15 @@ class Sanic:
self.websocket_tasks.remove(fut)
await ws.close()
routes = self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
routes.extend(
self.router.add(
uri=uri,
handler=websocket_handler,
methods=frozenset({"GET"}),
host=host,
strict_slashes=strict_slashes,
name=name,
)
)
return routes, handler
@@ -813,6 +830,14 @@ class Sanic:
"Endpoint with name `{}` was not found".format(view_name)
)
# If the route has host defined, split that off
# TODO: Retain netloc and path separately in Route objects
host = uri.find("/")
if host > 0:
host, uri = uri[:host], uri[host:]
else:
host = None
if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None)
# it's static folder
@@ -845,7 +870,7 @@ class Sanic:
netloc = kwargs.pop("_server", None)
if netloc is None and external:
netloc = self.config.get("SERVER_NAME", "")
netloc = host or self.config.get("SERVER_NAME", "")
if external:
if not scheme:
@@ -1449,3 +1474,5 @@ class Sanic:
self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app()
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable

View File

@@ -309,13 +309,17 @@ class ASGIApp:
callback = None if self.ws else self.stream_callback
await handler(self.request, None, callback)
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
async def stream_callback(self, response: HTTPResponse) -> None:
"""
Write the response.
"""
headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {}
content_length: List[str] = []
try:
content_length = response.headers.popall("content-length", [])
cookies = {
v.key: v
for _, v in list(
@@ -347,11 +351,23 @@ class ASGIApp:
if name not in (b"Set-Cookie",)
]
if "content-length" not in response.headers and not isinstance(
response, StreamingHTTPResponse
):
response.asgi = True
is_streaming = isinstance(response, StreamingHTTPResponse)
if is_streaming and getattr(response, "chunked", False):
# disable sanic chunking, this is done at the ASGI-server level
setattr(response, "chunked", False)
# content-length header is removed to signal to the ASGI-server
# to use automatic-chunking if it supports it
elif len(content_length) > 0:
headers += [
(b"content-length", str(len(response.body)).encode("latin-1"))
(b"content-length", str(content_length[0]).encode("latin-1"))
]
elif not is_streaming:
headers += [
(
b"content-length",
str(len(getattr(response, "body", b""))).encode("latin-1"),
)
]
if "content-type" not in response.headers:

View File

@@ -129,27 +129,27 @@ class Request:
def get(self, key, default=None):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__.get(key, default)
def __contains__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
Custom context is now stored in `request.custom_context.yourkey`"""
return key in self.ctx.__dict__
def __getitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
Custom context is now stored in `request.custom_context.yourkey`"""
return self.ctx.__dict__[key]
def __delitem__(self, key):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
Custom context is now stored in `request.custom_context.yourkey`"""
del self.ctx.__dict__[key]
def __setitem__(self, key, value):
""".. deprecated:: 19.9
Custom context is now stored in `request.custom_context.yourkey`"""
Custom context is now stored in `request.custom_context.yourkey`"""
setattr(self.ctx, key, value)
def body_init(self):

View File

@@ -22,6 +22,9 @@ except ImportError:
class BaseHTTPResponse:
def __init__(self):
self.asgi = False
def _encode_body(self, data):
try:
# Try to encode it regularly
@@ -59,12 +62,15 @@ class StreamingHTTPResponse(BaseHTTPResponse):
content_type="text/plain",
chunked=True,
):
super().__init__()
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = Header(headers or {})
self.chunked = chunked
self._cookies = None
self.protocol = None
async def write(self, data):
"""Writes a chunk of data to the streaming response.
@@ -74,6 +80,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
if type(data) != bytes:
data = self._encode_body(data)
# `chunked` will always be False in ASGI-mode, even if the underlying
# ASGI Transport implements Chunked transport. That does it itself.
if self.chunked:
await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data))
else:
@@ -93,8 +101,9 @@ class StreamingHTTPResponse(BaseHTTPResponse):
keep_alive=keep_alive,
keep_alive_timeout=keep_alive_timeout,
)
await self.protocol.push_data(headers)
await self.protocol.drain()
if not getattr(self, "asgi", False):
await self.protocol.push_data(headers)
await self.protocol.drain()
await self.streaming_fn(self)
if self.chunked:
await self.protocol.push_data(b"0\r\n\r\n")
@@ -144,6 +153,8 @@ class HTTPResponse(BaseHTTPResponse):
content_type=None,
body_bytes=b"",
):
super().__init__()
self.content_type = content_type
if body is not None:
@@ -202,16 +213,14 @@ class HTTPResponse(BaseHTTPResponse):
return self._cookies
def empty(
status=204, headers=None,
):
def empty(status=204, headers=None):
"""
Returns an empty response to the client.
:param status Response code.
:param headers Custom Headers.
"""
return HTTPResponse(body_bytes=b"", status=status, headers=headers,)
return HTTPResponse(body_bytes=b"", status=status, headers=headers)
def json(

View File

@@ -484,7 +484,7 @@ class Router:
return route_handler, [], kwargs, route.uri, route.name
def is_stream_handler(self, request):
""" Handler for request is stream or not.
"""Handler for request is stream or not.
:param request: Request object
:return: bool
"""

View File

@@ -87,6 +87,7 @@ class HttpProtocol(asyncio.Protocol):
"_header_fragment",
"state",
"_debug",
"_body_chunks",
)
def __init__(
@@ -730,6 +731,26 @@ class AsyncioServer:
task = asyncio.ensure_future(coro, loop=self.loop)
return task
def start_serving(self):
if self.server:
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self):
if self.server:
try:
return self.server.serve_forever()
except AttributeError:
raise NotImplementedError(
"server.serve_forever not available in this version "
"of asyncio or uvloop."
)
def __await__(self):
"""Starts the asyncio server, returns AsyncServerCoro"""
task = asyncio.ensure_future(self.serve_coro)

View File

@@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
@staticmethod
def _create_ssl_context(cfg):
""" Creates SSLContext instance for usage in asyncio.create_server.
"""Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)

View File

@@ -1,6 +1,6 @@
[tool.towncrier]
package = "sanic"
package_dir = "."
package_dir = ".."
filename = "../CHANGELOG.rst"
directory = "./changelogs"
underlines = ["=", "*", "~"]

View File

@@ -14,7 +14,7 @@ multi_line_output = 3
not_skip = __init__.py
[version]
current_version = 19.9.0
current_version = 19.12.0
files = sanic/__version__.py
current_version_pattern = __version__ = "{current_version}"
new_version_pattern = __version__ = "{new_version}"

View File

@@ -25,6 +25,7 @@ class PyTest(TestCommand):
def run_tests(self):
import shlex
import pytest
errno = pytest.main(shlex.split(self.pytest_args))
@@ -75,7 +76,7 @@ env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"'
)
ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3" + env_dependency
uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency
requirements = [
"httptools>=0.0.10",
@@ -83,13 +84,13 @@ requirements = [
ujson,
"aiofiles>=0.3.0",
"websockets>=7.0,<9.0",
"multidict>=4.0,<5.0",
"multidict==5.0.0",
"httpx==0.9.3",
]
tests_require = [
"pytest==5.2.1",
"multidict>=4.0,<5.0",
"multidict==5.0.0",
"gunicorn",
"pytest-cov",
"httpcore==0.3.0",

View File

@@ -39,6 +39,7 @@ main = WSGIApplication(
if __name__ == "__main__":
import sys
from wsgiref.simple_server import make_server
try:

View File

@@ -41,6 +41,20 @@ def test_create_asyncio_server(app):
assert srv.is_serving() is True
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
def test_asyncio_server_no_start_serving(app):
if not uvloop_installed():
loop = asyncio.get_event_loop()
asyncio_srv_coro = app.create_server(
return_asyncio_server=True,
asyncio_server_kwargs=dict(start_serving=False),
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher"
)
@@ -53,6 +67,10 @@ def test_asyncio_server_start_serving(app):
)
srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False
loop.run_until_complete(srv.start_serving())
assert srv.is_serving() is True
srv.close()
# Looks like we can't easily test `serve_forever()`
def test_app_loop_not_running(app):

View File

@@ -230,8 +230,8 @@ async def handler3(request):
def test_keep_alive_timeout_reuse():
"""If the server keep-alive timeout and client keep-alive timeout are
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
both longer than the delay, the client _and_ server will successfully
reuse the existing connection."""
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)

View File

@@ -15,13 +15,13 @@ from aiofiles import os as async_os
from sanic.response import (
HTTPResponse,
StreamingHTTPResponse,
empty,
file,
file_stream,
json,
raw,
stream,
)
from sanic.response import empty
from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT
@@ -232,6 +232,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
assert response.text == "foo,bar"
@pytest.mark.asyncio
async def test_chunked_streaming_returns_correct_content_asgi(streaming_app):
request, response = await streaming_app.asgi_client.get("/")
assert response.text == "foo,bar"
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/")
assert "Transfer-Encoding" not in response.headers
@@ -239,8 +245,18 @@ def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
assert response.headers["Content-Length"] == "7"
@pytest.mark.asyncio
async def test_non_chunked_streaming_adds_correct_headers_asgi(
non_chunked_streaming_app,
):
request, response = await non_chunked_streaming_app.asgi_client.get("/")
assert "Transfer-Encoding" not in response.headers
assert response.headers["Content-Type"] == "text/csv"
assert response.headers["Content-Length"] == "7"
def test_non_chunked_streaming_returns_correct_content(
non_chunked_streaming_app
non_chunked_streaming_app,
):
request, response = non_chunked_streaming_app.test_client.get("/")
assert response.text == "foo,bar"
@@ -255,7 +271,7 @@ def test_stream_response_status_returns_correct_headers(status):
@pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30])
def test_stream_response_keep_alive_returns_correct_headers(
keep_alive_timeout
keep_alive_timeout,
):
response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers(
@@ -284,7 +300,7 @@ def test_stream_response_does_not_include_chunked_header_if_disabled():
def test_stream_response_writes_correct_content_to_transport_when_chunked(
streaming_app
streaming_app,
):
response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol)

View File

@@ -551,6 +551,35 @@ def test_route_duplicate(app):
pass
def test_double_stack_route(app):
@app.route("/test/1")
@app.route("/test/2")
async def handler1(request):
return text("OK")
request, response = app.test_client.get("/test/1")
assert response.status == 200
request, response = app.test_client.get("/test/2")
assert response.status == 200
@pytest.mark.asyncio
async def test_websocket_route_asgi(app):
ev = asyncio.Event()
@app.websocket("/test/1")
@app.websocket("/test/2")
async def handler(request, ws):
ev.set()
request, response = await app.asgi_client.websocket("/test/1")
first_set = ev.is_set()
ev.clear()
request, response = await app.asgi_client.websocket("/test/1")
second_set = ev.is_set()
assert first_set and second_set
def test_method_not_allowed(app):
@app.route("/test", methods=["GET"])
async def handler(request):

View File

@@ -14,7 +14,7 @@ deps =
pytest-sugar
httpcore==0.3.0
httpx==0.9.3
chardet<=2.3.0
multidict==5.0.0
beautifulsoup4
gunicorn
pytest-benchmark