Merge branch 'master' into py39
This commit is contained in:
commit
35f28f7a64
|
@ -1,3 +1,28 @@
|
|||
Version 20.9.1
|
||||
===============
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
|
||||
*
|
||||
`#1954 <https://github.com/huge-success/sanic/pull/1954>`_
|
||||
Fix static route registration on blueprints
|
||||
*
|
||||
`#1957 <https://github.com/huge-success/sanic/pull/1957>`_
|
||||
Removes duplicate headers in ASGI streaming body
|
||||
|
||||
|
||||
Version 19.12.3
|
||||
===============
|
||||
|
||||
Bugfixes
|
||||
********
|
||||
|
||||
*
|
||||
`#1959 <https://github.com/huge-success/sanic/pull/1959>`_
|
||||
Removes duplicate headers in ASGI streaming body
|
||||
|
||||
|
||||
Version 20.9.0
|
||||
===============
|
||||
|
||||
|
|
|
@ -26,8 +26,8 @@ Sanic | Build fast. Run fast.
|
|||
:target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Codecov| image:: https://codecov.io/gh/huge-success/sanic/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/huge-success/sanic
|
||||
.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master
|
||||
:target: https://travis-ci.org/huge-success/sanic
|
||||
.. |Build Status| image:: https://travis-ci.com/huge-success/sanic.svg?branch=master
|
||||
:target: https://travis-ci.com/huge-success/sanic
|
||||
.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/huge-success/sanic
|
||||
.. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest
|
||||
|
|
1
changelogs/1970.misc.rst
Normal file
1
changelogs/1970.misc.rst
Normal file
|
@ -0,0 +1 @@
|
|||
Adds py.typed file to expose type information to other packages.
|
|
@ -88,5 +88,5 @@ When `stream_large_files` is `True`, Sanic will use `file_stream()` instead of `
|
|||
|
||||
app = Sanic(__name__)
|
||||
|
||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8KB
|
||||
chunk_size = 1024 * 1024 * 8 # Set chunk size to 8MiB
|
||||
app.static('/large_video.mp4', '/home/ubuntu/large_video.mp4', stream_large_files=chunk_size)
|
||||
|
|
|
@ -1,28 +1,83 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from argparse import ArgumentParser, RawDescriptionHelpFormatter
|
||||
from importlib import import_module
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sanic import __version__
|
||||
from sanic.app import Sanic
|
||||
from sanic.config import BASE_LOGO
|
||||
from sanic.log import logger
|
||||
|
||||
|
||||
class SanicArgumentParser(ArgumentParser):
|
||||
def add_bool_arguments(self, *args, **kwargs):
|
||||
group = self.add_mutually_exclusive_group()
|
||||
group.add_argument(*args, action="store_true", **kwargs)
|
||||
kwargs["help"] = "no " + kwargs["help"]
|
||||
group.add_argument(
|
||||
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="sanic")
|
||||
parser.add_argument("--host", dest="host", type=str, default="127.0.0.1")
|
||||
parser.add_argument("--port", dest="port", type=int, default=8000)
|
||||
parser.add_argument("--unix", dest="unix", type=str, default="")
|
||||
parser = SanicArgumentParser(
|
||||
prog="sanic",
|
||||
description=BASE_LOGO,
|
||||
formatter_class=RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-H",
|
||||
"--host",
|
||||
dest="host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
help="host address [default 127.0.0.1]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--port",
|
||||
dest="port",
|
||||
type=int,
|
||||
default=8000,
|
||||
help="port to serve on [default 8000]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-u",
|
||||
"--unix",
|
||||
dest="unix",
|
||||
type=str,
|
||||
default="",
|
||||
help="location of unix socket",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--cert", dest="cert", type=str, help="location of certificate for SSL"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key", dest="key", type=str, help="location of keyfile for SSL."
|
||||
)
|
||||
parser.add_argument("--workers", dest="workers", type=int, default=1)
|
||||
parser.add_argument(
|
||||
"-w",
|
||||
"--workers",
|
||||
dest="workers",
|
||||
type=int,
|
||||
default=1,
|
||||
help="number of worker processes [default 1]",
|
||||
)
|
||||
parser.add_argument("--debug", dest="debug", action="store_true")
|
||||
parser.add_argument("module")
|
||||
parser.add_bool_arguments(
|
||||
"--access-logs", dest="access_log", help="display access logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"Sanic {__version__}",
|
||||
)
|
||||
parser.add_argument(
|
||||
"module", help="path to your Sanic app. Example: path.to.server:app"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
|
@ -30,9 +85,12 @@ def main():
|
|||
if module_path not in sys.path:
|
||||
sys.path.append(module_path)
|
||||
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
if ":" in args.module:
|
||||
module_name, app_name = args.module.rsplit(":", 1)
|
||||
else:
|
||||
module_parts = args.module.split(".")
|
||||
module_name = ".".join(module_parts[:-1])
|
||||
app_name = module_parts[-1]
|
||||
|
||||
module = import_module(module_name)
|
||||
app = getattr(module, app_name, None)
|
||||
|
@ -57,6 +115,7 @@ def main():
|
|||
unix=args.unix,
|
||||
workers=args.workers,
|
||||
debug=args.debug,
|
||||
access_log=args.access_log,
|
||||
ssl=ssl,
|
||||
)
|
||||
except ImportError as e:
|
||||
|
|
20
sanic/app.py
20
sanic/app.py
|
@ -68,7 +68,7 @@ class Sanic:
|
|||
|
||||
self.name = name
|
||||
self.asgi = False
|
||||
self.router = router or Router()
|
||||
self.router = router or Router(self)
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config(load_env=load_env)
|
||||
|
@ -900,7 +900,9 @@ class Sanic:
|
|||
name = None
|
||||
try:
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs, uri, name = self.router.get(request)
|
||||
handler, args, kwargs, uri, name, endpoint = self.router.get(
|
||||
request
|
||||
)
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Request Middleware
|
||||
|
@ -922,16 +924,8 @@ class Sanic:
|
|||
"handler from the router"
|
||||
)
|
||||
)
|
||||
else:
|
||||
if not getattr(handler, "__blueprintname__", False):
|
||||
request.endpoint = self._build_endpoint_name(
|
||||
handler.__name__
|
||||
)
|
||||
else:
|
||||
request.endpoint = self._build_endpoint_name(
|
||||
getattr(handler, "__blueprintname__", ""),
|
||||
handler.__name__,
|
||||
)
|
||||
|
||||
request.endpoint = endpoint
|
||||
|
||||
# Run response handler
|
||||
response = handler(request, *args, **kwargs)
|
||||
|
@ -1454,6 +1448,8 @@ class Sanic:
|
|||
asgi_app = await ASGIApp.create(self, scope, receive, send)
|
||||
await asgi_app()
|
||||
|
||||
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Configuration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
|
|
@ -312,13 +312,19 @@ class ASGIApp:
|
|||
callback = None if self.ws else self.stream_callback
|
||||
await handler(self.request, None, callback)
|
||||
|
||||
async def stream_callback(self, response: HTTPResponse) -> None:
|
||||
_asgi_single_callable = True # We conform to ASGI 3.0 single-callable
|
||||
|
||||
async def stream_callback(
|
||||
self, response: Union[HTTPResponse, StreamingHTTPResponse]
|
||||
) -> 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(
|
||||
|
@ -351,12 +357,22 @@ class ASGIApp:
|
|||
]
|
||||
|
||||
response.asgi = True
|
||||
|
||||
if "content-length" not in response.headers and not isinstance(
|
||||
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 += [
|
||||
(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:
|
||||
|
|
|
@ -179,8 +179,8 @@ def abort(status_code, message=None):
|
|||
message appropriate for the given status code, unless provided.
|
||||
|
||||
:param status_code: The HTTP status code to return.
|
||||
:param message: The HTTP response body. Defaults to the messages
|
||||
in response.py for the given status code.
|
||||
:param message: The HTTP response body. Defaults to the messages in
|
||||
STATUS_CODES from sanic.helpers for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
message = STATUS_CODES.get(status_code)
|
||||
|
|
0
sanic/py.typed
Normal file
0
sanic/py.typed
Normal file
|
@ -100,6 +100,8 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||
"""
|
||||
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:
|
||||
|
|
|
@ -11,7 +11,16 @@ from sanic.views import CompositionView
|
|||
|
||||
|
||||
Route = namedtuple(
|
||||
"Route", ["handler", "methods", "pattern", "parameters", "name", "uri"]
|
||||
"Route",
|
||||
[
|
||||
"handler",
|
||||
"methods",
|
||||
"pattern",
|
||||
"parameters",
|
||||
"name",
|
||||
"uri",
|
||||
"endpoint",
|
||||
],
|
||||
)
|
||||
Parameter = namedtuple("Parameter", ["name", "cast"])
|
||||
|
||||
|
@ -79,7 +88,8 @@ class Router:
|
|||
routes_always_check = None
|
||||
parameter_pattern = re.compile(r"<(.+?)>")
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.routes_all = {}
|
||||
self.routes_names = {}
|
||||
self.routes_static_files = {}
|
||||
|
@ -299,11 +309,15 @@ class Router:
|
|||
|
||||
handler_name = f"{bp_name}.{name or handler.__name__}"
|
||||
else:
|
||||
handler_name = name or getattr(handler, "__name__", None)
|
||||
handler_name = name or getattr(
|
||||
handler, "__name__", handler.__class__.__name__
|
||||
)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
endpoint = self.app._build_endpoint_name(handler_name)
|
||||
|
||||
route = Route(
|
||||
handler=handler,
|
||||
methods=methods,
|
||||
|
@ -311,6 +325,7 @@ class Router:
|
|||
parameters=parameters,
|
||||
name=handler_name,
|
||||
uri=uri,
|
||||
endpoint=endpoint,
|
||||
)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
|
@ -449,7 +464,8 @@ class Router:
|
|||
route_handler = route.handler
|
||||
if hasattr(route_handler, "handlers"):
|
||||
route_handler = route_handler.handlers[method]
|
||||
return route_handler, [], kwargs, route.uri, route.name
|
||||
|
||||
return route_handler, [], kwargs, route.uri, route.name, route.endpoint
|
||||
|
||||
def is_stream_handler(self, request):
|
||||
"""Handler for request is stream or not.
|
||||
|
|
|
@ -90,6 +90,7 @@ class CompositionView:
|
|||
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
def add(self, methods, handler, stream=False):
|
||||
if stream:
|
||||
|
|
8
setup.py
8
setup.py
|
@ -57,6 +57,7 @@ setup_kwargs = {
|
|||
),
|
||||
"long_description": long_description,
|
||||
"packages": ["sanic"],
|
||||
"package_data": {"sanic": ["py.typed"]},
|
||||
"platforms": "any",
|
||||
"python_requires": ">=3.6",
|
||||
"classifiers": [
|
||||
|
@ -79,15 +80,15 @@ requirements = [
|
|||
"httptools>=0.0.10",
|
||||
uvloop,
|
||||
ujson,
|
||||
"aiofiles>=0.3.0",
|
||||
"aiofiles>=0.6.0",
|
||||
"websockets>=8.1,<9.0",
|
||||
"multidict==5.0.0",
|
||||
"multidict>=5.0,<6.0",
|
||||
"httpx==0.15.4",
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
"pytest==5.2.1",
|
||||
"multidict==5.0.0",
|
||||
"multidict>=5.0,<6.0",
|
||||
"gunicorn",
|
||||
"pytest-cov",
|
||||
"httpcore==0.3.0",
|
||||
|
@ -97,6 +98,7 @@ tests_require = [
|
|||
"pytest-sanic",
|
||||
"pytest-sugar",
|
||||
"pytest-benchmark",
|
||||
"pytest-dependency",
|
||||
]
|
||||
|
||||
docs_require = [
|
||||
|
|
|
@ -95,10 +95,10 @@ class RouteStringGenerator:
|
|||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def sanic_router():
|
||||
def sanic_router(app):
|
||||
# noinspection PyProtectedMember
|
||||
def _setup(route_details: tuple) -> (Router, tuple):
|
||||
router = Router()
|
||||
router = Router(app)
|
||||
added_router = []
|
||||
for method, route in route_details:
|
||||
try:
|
||||
|
|
|
@ -117,7 +117,7 @@ def test_app_route_raise_value_error(app):
|
|||
|
||||
def test_app_handle_request_handler_is_none(app, monkeypatch):
|
||||
def mockreturn(*args, **kwargs):
|
||||
return None, [], {}, "", ""
|
||||
return None, [], {}, "", "", None
|
||||
|
||||
# Not sure how to make app.router.get() return None, so use mock here.
|
||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||
|
|
|
@ -735,6 +735,7 @@ def test_static_blueprint_name(app: Sanic, static_file_directory, file_name):
|
|||
_, response = app.test_client.get("/static/test.file/")
|
||||
assert response.status == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["test.file"])
|
||||
def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
||||
current_file = inspect.getfile(inspect.currentframe())
|
||||
|
@ -745,7 +746,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
|||
|
||||
bp = Blueprint(name="test_mw", url_prefix="")
|
||||
|
||||
@bp.middleware('request')
|
||||
@bp.middleware("request")
|
||||
def bp_mw1(request):
|
||||
nonlocal triggered
|
||||
triggered = True
|
||||
|
@ -754,7 +755,7 @@ def test_static_blueprintp_mw(app: Sanic, static_file_directory, file_name):
|
|||
"/test.file",
|
||||
get_file_path(static_file_directory, file_name),
|
||||
strict_slashes=True,
|
||||
name="static"
|
||||
name="static",
|
||||
)
|
||||
|
||||
app.blueprint(bp)
|
||||
|
|
|
@ -20,7 +20,9 @@ def test_load_module_from_file_location(loaded_module_from_file_location):
|
|||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_load_module_from_file_location"])
|
||||
def test_loaded_module_from_file_location_name(loaded_module_from_file_location,):
|
||||
def test_loaded_module_from_file_location_name(
|
||||
loaded_module_from_file_location,
|
||||
):
|
||||
name = loaded_module_from_file_location.__name__
|
||||
if "C:\\" in name:
|
||||
name = name.split("\\")[-1]
|
||||
|
|
|
@ -238,7 +238,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
|
|||
@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 == "4\r\nfoo,\r\n3\r\nbar\r\n0\r\n\r\n"
|
||||
assert response.text == "foo,bar"
|
||||
|
||||
|
||||
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||
|
|
Loading…
Reference in New Issue
Block a user