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 Version 19.6.3
============== ==============

View File

@@ -71,7 +71,7 @@ black:
black --config ./.black.toml sanic tests black --config ./.black.toml sanic tests
fix-import: black fix-import: black
isort -rc sanic tests isort sanic tests
docs-clean: 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 strict_slashes = self.strict_slashes
def response(handler): 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()) args = list(signature(handler).parameters.keys())
if not args: if not args:
@@ -205,14 +211,16 @@ class Sanic:
if stream: if stream:
handler.is_stream = stream handler.is_stream = stream
routes = self.router.add( routes.extend(
uri=uri, self.router.add(
methods=methods, uri=uri,
handler=handler, methods=methods,
host=host, handler=handler,
strict_slashes=strict_slashes, host=host,
version=version, strict_slashes=strict_slashes,
name=name, version=version,
name=name,
)
) )
return routes, handler return routes, handler
@@ -476,6 +484,13 @@ class Sanic:
strict_slashes = self.strict_slashes strict_slashes = self.strict_slashes
def response(handler): 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): async def websocket_handler(request, *args, **kwargs):
request.app = self request.app = self
if not getattr(handler, "__blueprintname__", False): if not getattr(handler, "__blueprintname__", False):
@@ -516,13 +531,15 @@ class Sanic:
self.websocket_tasks.remove(fut) self.websocket_tasks.remove(fut)
await ws.close() await ws.close()
routes = self.router.add( routes.extend(
uri=uri, self.router.add(
handler=websocket_handler, uri=uri,
methods=frozenset({"GET"}), handler=websocket_handler,
host=host, methods=frozenset({"GET"}),
strict_slashes=strict_slashes, host=host,
name=name, strict_slashes=strict_slashes,
name=name,
)
) )
return routes, handler return routes, handler
@@ -813,6 +830,14 @@ class Sanic:
"Endpoint with name `{}` was not found".format(view_name) "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"): if view_name == "static" or view_name.endswith(".static"):
filename = kwargs.pop("filename", None) filename = kwargs.pop("filename", None)
# it's static folder # it's static folder
@@ -845,7 +870,7 @@ class Sanic:
netloc = kwargs.pop("_server", None) netloc = kwargs.pop("_server", None)
if netloc is None and external: if netloc is None and external:
netloc = self.config.get("SERVER_NAME", "") netloc = host or self.config.get("SERVER_NAME", "")
if external: if external:
if not scheme: if not scheme:
@@ -1449,3 +1474,5 @@ class Sanic:
self.asgi = True self.asgi = True
asgi_app = await ASGIApp.create(self, scope, receive, send) asgi_app = await ASGIApp.create(self, scope, receive, send)
await asgi_app() 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 callback = None if self.ws else self.stream_callback
await handler(self.request, None, 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: async def stream_callback(self, response: HTTPResponse) -> None:
""" """
Write the response. Write the response.
""" """
headers: List[Tuple[bytes, bytes]] = [] headers: List[Tuple[bytes, bytes]] = []
cookies: Dict[str, str] = {} cookies: Dict[str, str] = {}
content_length: List[str] = []
try: try:
content_length = response.headers.popall("content-length", [])
cookies = { cookies = {
v.key: v v.key: v
for _, v in list( for _, v in list(
@@ -347,11 +351,23 @@ class ASGIApp:
if name not in (b"Set-Cookie",) if name not in (b"Set-Cookie",)
] ]
if "content-length" not in response.headers and not isinstance( response.asgi = True
response, StreamingHTTPResponse 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 += [ 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: if "content-type" not in response.headers:

View File

@@ -129,27 +129,27 @@ class Request:
def get(self, key, default=None): def get(self, key, default=None):
""".. deprecated:: 19.9 """.. 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) return self.ctx.__dict__.get(key, default)
def __contains__(self, key): def __contains__(self, key):
""".. deprecated:: 19.9 """.. 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__ return key in self.ctx.__dict__
def __getitem__(self, key): def __getitem__(self, key):
""".. deprecated:: 19.9 """.. 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] return self.ctx.__dict__[key]
def __delitem__(self, key): def __delitem__(self, key):
""".. deprecated:: 19.9 """.. 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] del self.ctx.__dict__[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
""".. deprecated:: 19.9 """.. 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) setattr(self.ctx, key, value)
def body_init(self): def body_init(self):

View File

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

View File

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

View File

@@ -87,6 +87,7 @@ class HttpProtocol(asyncio.Protocol):
"_header_fragment", "_header_fragment",
"state", "state",
"_debug", "_debug",
"_body_chunks",
) )
def __init__( def __init__(
@@ -730,6 +731,26 @@ class AsyncioServer:
task = asyncio.ensure_future(coro, loop=self.loop) task = asyncio.ensure_future(coro, loop=self.loop)
return task 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): def __await__(self):
"""Starts the asyncio server, returns AsyncServerCoro""" """Starts the asyncio server, returns AsyncServerCoro"""
task = asyncio.ensure_future(self.serve_coro) task = asyncio.ensure_future(self.serve_coro)

View File

@@ -174,7 +174,7 @@ class GunicornWorker(base.Worker):
@staticmethod @staticmethod
def _create_ssl_context(cfg): 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. See ssl.SSLSocket.__init__ for more details.
""" """
ctx = ssl.SSLContext(cfg.ssl_version) ctx = ssl.SSLContext(cfg.ssl_version)

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,20 @@ def test_create_asyncio_server(app):
assert srv.is_serving() is True 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( @pytest.mark.skipif(
sys.version_info < (3, 7), reason="requires python3.7 or higher" 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) srv = loop.run_until_complete(asyncio_srv_coro)
assert srv.is_serving() is False 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): def test_app_loop_not_running(app):

View File

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

View File

@@ -15,13 +15,13 @@ from aiofiles import os as async_os
from sanic.response import ( from sanic.response import (
HTTPResponse, HTTPResponse,
StreamingHTTPResponse, StreamingHTTPResponse,
empty,
file, file,
file_stream, file_stream,
json, json,
raw, raw,
stream, stream,
) )
from sanic.response import empty
from sanic.server import HttpProtocol from sanic.server import HttpProtocol
from sanic.testing import HOST, PORT from sanic.testing import HOST, PORT
@@ -232,6 +232,12 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
assert response.text == "foo,bar" 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): def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/") request, response = non_chunked_streaming_app.test_client.get("/")
assert "Transfer-Encoding" not in response.headers 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" 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( 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("/") request, response = non_chunked_streaming_app.test_client.get("/")
assert response.text == "foo,bar" 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]) @pytest.mark.parametrize("keep_alive_timeout", [10, 20, 30])
def test_stream_response_keep_alive_returns_correct_headers( def test_stream_response_keep_alive_returns_correct_headers(
keep_alive_timeout keep_alive_timeout,
): ):
response = StreamingHTTPResponse(sample_streaming_fn) response = StreamingHTTPResponse(sample_streaming_fn)
headers = response.get_headers( 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( def test_stream_response_writes_correct_content_to_transport_when_chunked(
streaming_app streaming_app,
): ):
response = StreamingHTTPResponse(sample_streaming_fn) response = StreamingHTTPResponse(sample_streaming_fn)
response.protocol = MagicMock(HttpProtocol) response.protocol = MagicMock(HttpProtocol)

View File

@@ -551,6 +551,35 @@ def test_route_duplicate(app):
pass 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): def test_method_not_allowed(app):
@app.route("/test", methods=["GET"]) @app.route("/test", methods=["GET"])
async def handler(request): async def handler(request):

View File

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