Merge branch 'main' into request-contextvars
This commit is contained in:
commit
f64e917746
@ -1,2 +0,0 @@
|
|||||||
[tool.black]
|
|
||||||
line-length = 79
|
|
6
Makefile
6
Makefile
@ -66,15 +66,15 @@ ifdef include_tests
|
|||||||
isort -rc sanic tests
|
isort -rc sanic tests
|
||||||
else
|
else
|
||||||
$(info Sorting Imports)
|
$(info Sorting Imports)
|
||||||
isort -rc sanic tests --profile=black
|
isort -rc sanic tests
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
black:
|
black:
|
||||||
black --config ./.black.toml sanic tests
|
black sanic tests
|
||||||
|
|
||||||
isort:
|
isort:
|
||||||
isort sanic tests --profile=black
|
isort sanic tests
|
||||||
|
|
||||||
pretty: black isort
|
pretty: black isort
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ Hello World Example
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
app = Sanic("My Hello, world app")
|
app = Sanic("my-hello-world-app")
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
async def test(request):
|
async def test(request):
|
||||||
|
@ -1,3 +1,18 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools<60.0", "wheel"]
|
requires = ["setuptools<60.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
atomic = true
|
||||||
|
default_section = "THIRDPARTY"
|
||||||
|
include_trailing_comma = true
|
||||||
|
known_first_party = "sanic"
|
||||||
|
known_third_party = "pytest"
|
||||||
|
line_length = 79
|
||||||
|
lines_after_imports = 2
|
||||||
|
lines_between_types = 1
|
||||||
|
multi_line_output = 3
|
||||||
|
profile = "black"
|
||||||
|
11
sanic/app.py
11
sanic/app.py
@ -97,7 +97,7 @@ if TYPE_CHECKING: # no cov
|
|||||||
from sanic_ext import Extend # type: ignore
|
from sanic_ext import Extend # type: ignore
|
||||||
from sanic_ext.extensions.base import Extension # type: ignore
|
from sanic_ext.extensions.base import Extension # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Extend = TypeVar("Extend") # type: ignore
|
Extend = TypeVar("Extend", Type) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if OS_IS_WINDOWS: # no cov
|
if OS_IS_WINDOWS: # no cov
|
||||||
@ -992,10 +992,10 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
cancelled = False
|
cancelled = False
|
||||||
try:
|
try:
|
||||||
await fut
|
await fut
|
||||||
except Exception as e:
|
|
||||||
self.error_handler.log(request, e)
|
|
||||||
except (CancelledError, ConnectionClosed):
|
except (CancelledError, ConnectionClosed):
|
||||||
cancelled = True
|
cancelled = True
|
||||||
|
except Exception as e:
|
||||||
|
self.error_handler.log(request, e)
|
||||||
finally:
|
finally:
|
||||||
self.websocket_tasks.remove(fut)
|
self.websocket_tasks.remove(fut)
|
||||||
if cancelled:
|
if cancelled:
|
||||||
@ -1573,8 +1573,9 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
|
|||||||
"shutdown",
|
"shutdown",
|
||||||
):
|
):
|
||||||
raise SanicException(f"Invalid server event: {event}")
|
raise SanicException(f"Invalid server event: {event}")
|
||||||
if self.state.verbosity >= 1:
|
logger.debug(
|
||||||
logger.debug(f"Triggering server events: {event}")
|
f"Triggering server events: {event}", extra={"verbosity": 1}
|
||||||
|
)
|
||||||
reverse = concern == "shutdown"
|
reverse = concern == "shutdown"
|
||||||
if loop is None:
|
if loop is None:
|
||||||
loop = self.loop
|
loop = self.loop
|
||||||
|
@ -9,7 +9,7 @@ from socket import socket
|
|||||||
from ssl import SSLContext
|
from ssl import SSLContext
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
from sanic.log import logger
|
from sanic.log import VerbosityFilter, logger
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
|
|
||||||
|
|
||||||
@ -91,6 +91,9 @@ class ApplicationState:
|
|||||||
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
if getattr(self.app, "configure_logging", False) and self.app.debug:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
def set_verbosity(self, value: int):
|
||||||
|
VerbosityFilter.verbosity = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_debug(self):
|
def is_debug(self):
|
||||||
return self.mode is Mode.DEBUG
|
return self.mode is Mode.DEBUG
|
||||||
|
@ -25,27 +25,28 @@ class Lifespan:
|
|||||||
def __init__(self, asgi_app: ASGIApp) -> None:
|
def __init__(self, asgi_app: ASGIApp) -> None:
|
||||||
self.asgi_app = asgi_app
|
self.asgi_app = asgi_app
|
||||||
|
|
||||||
if self.asgi_app.sanic_app.state.verbosity > 0:
|
if (
|
||||||
if (
|
"server.init.before"
|
||||||
"server.init.before"
|
in self.asgi_app.sanic_app.signal_router.name_index
|
||||||
in self.asgi_app.sanic_app.signal_router.name_index
|
):
|
||||||
):
|
logger.debug(
|
||||||
logger.debug(
|
'You have set a listener for "before_server_start" '
|
||||||
'You have set a listener for "before_server_start" '
|
"in ASGI mode. "
|
||||||
"in ASGI mode. "
|
"It will be executed as early as possible, but not before "
|
||||||
"It will be executed as early as possible, but not before "
|
"the ASGI server is started.",
|
||||||
"the ASGI server is started."
|
extra={"verbosity": 1},
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
"server.shutdown.after"
|
"server.shutdown.after"
|
||||||
in self.asgi_app.sanic_app.signal_router.name_index
|
in self.asgi_app.sanic_app.signal_router.name_index
|
||||||
):
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'You have set a listener for "after_server_stop" '
|
'You have set a listener for "after_server_stop" '
|
||||||
"in ASGI mode. "
|
"in ASGI mode. "
|
||||||
"It will be executed as late as possible, but not after "
|
"It will be executed as late as possible, but not after "
|
||||||
"the ASGI server is stopped."
|
"the ASGI server is stopped.",
|
||||||
)
|
extra={"verbosity": 1},
|
||||||
|
)
|
||||||
|
|
||||||
async def startup(self) -> None:
|
async def startup(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -163,6 +164,13 @@ class ASGIApp:
|
|||||||
instance.request_body = True
|
instance.request_body = True
|
||||||
instance.request.conn_info = ConnInfo(instance.transport)
|
instance.request.conn_info = ConnInfo(instance.transport)
|
||||||
|
|
||||||
|
await sanic_app.dispatch(
|
||||||
|
"http.lifecycle.request",
|
||||||
|
inline=True,
|
||||||
|
context={"request": instance.request},
|
||||||
|
fail_not_found=False,
|
||||||
|
)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
async def read(self) -> Optional[bytes]:
|
async def read(self) -> Optional[bytes]:
|
||||||
|
17
sanic/log.py
17
sanic/log.py
@ -61,21 +61,36 @@ class Colors(str, Enum): # no cov
|
|||||||
END = "\033[0m"
|
END = "\033[0m"
|
||||||
BLUE = "\033[01;34m"
|
BLUE = "\033[01;34m"
|
||||||
GREEN = "\033[01;32m"
|
GREEN = "\033[01;32m"
|
||||||
YELLOW = "\033[01;33m"
|
PURPLE = "\033[01;35m"
|
||||||
RED = "\033[01;31m"
|
RED = "\033[01;31m"
|
||||||
|
SANIC = "\033[38;2;255;13;104m"
|
||||||
|
YELLOW = "\033[01;33m"
|
||||||
|
|
||||||
|
|
||||||
|
class VerbosityFilter(logging.Filter):
|
||||||
|
verbosity: int = 0
|
||||||
|
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
verbosity = getattr(record, "verbosity", 0)
|
||||||
|
return verbosity <= self.verbosity
|
||||||
|
|
||||||
|
|
||||||
|
_verbosity_filter = VerbosityFilter()
|
||||||
|
|
||||||
logger = logging.getLogger("sanic.root") # no cov
|
logger = logging.getLogger("sanic.root") # no cov
|
||||||
|
logger.addFilter(_verbosity_filter)
|
||||||
"""
|
"""
|
||||||
General Sanic logger
|
General Sanic logger
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error_logger = logging.getLogger("sanic.error") # no cov
|
error_logger = logging.getLogger("sanic.error") # no cov
|
||||||
|
error_logger.addFilter(_verbosity_filter)
|
||||||
"""
|
"""
|
||||||
Logger used by Sanic for error logging
|
Logger used by Sanic for error logging
|
||||||
"""
|
"""
|
||||||
|
|
||||||
access_logger = logging.getLogger("sanic.access") # no cov
|
access_logger = logging.getLogger("sanic.access") # no cov
|
||||||
|
access_logger.addFilter(_verbosity_filter)
|
||||||
"""
|
"""
|
||||||
Logger used by Sanic for access logging
|
Logger used by Sanic for access logging
|
||||||
"""
|
"""
|
||||||
|
@ -8,7 +8,17 @@ from pathlib import PurePath
|
|||||||
from re import sub
|
from re import sub
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from sanic_routing.route import Route # type: ignore
|
from sanic_routing.route import Route # type: ignore
|
||||||
@ -283,7 +293,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **GET** *HTTP* method
|
Add an API URL under the **GET** *HTTP* method
|
||||||
|
|
||||||
@ -299,17 +309,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"GET"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"GET"}),
|
||||||
version=version,
|
host=host,
|
||||||
name=name,
|
strict_slashes=strict_slashes,
|
||||||
ignore_body=ignore_body,
|
version=version,
|
||||||
version_prefix=version_prefix,
|
name=name,
|
||||||
error_format=error_format,
|
ignore_body=ignore_body,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def post(
|
def post(
|
||||||
@ -323,7 +336,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **POST** *HTTP* method
|
Add an API URL under the **POST** *HTTP* method
|
||||||
|
|
||||||
@ -339,17 +352,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"POST"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"POST"}),
|
||||||
stream=stream,
|
host=host,
|
||||||
version=version,
|
strict_slashes=strict_slashes,
|
||||||
name=name,
|
stream=stream,
|
||||||
version_prefix=version_prefix,
|
version=version,
|
||||||
error_format=error_format,
|
name=name,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def put(
|
def put(
|
||||||
@ -363,7 +379,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PUT** *HTTP* method
|
Add an API URL under the **PUT** *HTTP* method
|
||||||
|
|
||||||
@ -379,17 +395,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"PUT"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"PUT"}),
|
||||||
stream=stream,
|
host=host,
|
||||||
version=version,
|
strict_slashes=strict_slashes,
|
||||||
name=name,
|
stream=stream,
|
||||||
version_prefix=version_prefix,
|
version=version,
|
||||||
error_format=error_format,
|
name=name,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def head(
|
def head(
|
||||||
@ -403,7 +422,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **HEAD** *HTTP* method
|
Add an API URL under the **HEAD** *HTTP* method
|
||||||
|
|
||||||
@ -427,17 +446,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"HEAD"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"HEAD"}),
|
||||||
version=version,
|
host=host,
|
||||||
name=name,
|
strict_slashes=strict_slashes,
|
||||||
ignore_body=ignore_body,
|
version=version,
|
||||||
version_prefix=version_prefix,
|
name=name,
|
||||||
error_format=error_format,
|
ignore_body=ignore_body,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def options(
|
def options(
|
||||||
@ -451,7 +473,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **OPTIONS** *HTTP* method
|
Add an API URL under the **OPTIONS** *HTTP* method
|
||||||
|
|
||||||
@ -475,17 +497,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"OPTIONS"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"OPTIONS"}),
|
||||||
version=version,
|
host=host,
|
||||||
name=name,
|
strict_slashes=strict_slashes,
|
||||||
ignore_body=ignore_body,
|
version=version,
|
||||||
version_prefix=version_prefix,
|
name=name,
|
||||||
error_format=error_format,
|
ignore_body=ignore_body,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def patch(
|
def patch(
|
||||||
@ -499,7 +524,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **PATCH** *HTTP* method
|
Add an API URL under the **PATCH** *HTTP* method
|
||||||
|
|
||||||
@ -525,17 +550,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"PATCH"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"PATCH"}),
|
||||||
stream=stream,
|
host=host,
|
||||||
version=version,
|
strict_slashes=strict_slashes,
|
||||||
name=name,
|
stream=stream,
|
||||||
version_prefix=version_prefix,
|
version=version,
|
||||||
error_format=error_format,
|
name=name,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@ -549,7 +577,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
version_prefix: str = "/v",
|
version_prefix: str = "/v",
|
||||||
error_format: Optional[str] = None,
|
error_format: Optional[str] = None,
|
||||||
**ctx_kwargs,
|
**ctx_kwargs,
|
||||||
) -> RouteWrapper:
|
) -> RouteHandler:
|
||||||
"""
|
"""
|
||||||
Add an API URL under the **DELETE** *HTTP* method
|
Add an API URL under the **DELETE** *HTTP* method
|
||||||
|
|
||||||
@ -565,17 +593,20 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
will be appended to the route context (``route.ctx``)
|
will be appended to the route context (``route.ctx``)
|
||||||
:return: Object decorated with :func:`route` method
|
:return: Object decorated with :func:`route` method
|
||||||
"""
|
"""
|
||||||
return self.route(
|
return cast(
|
||||||
uri,
|
RouteHandler,
|
||||||
methods=frozenset({"DELETE"}),
|
self.route(
|
||||||
host=host,
|
uri,
|
||||||
strict_slashes=strict_slashes,
|
methods=frozenset({"DELETE"}),
|
||||||
version=version,
|
host=host,
|
||||||
name=name,
|
strict_slashes=strict_slashes,
|
||||||
ignore_body=ignore_body,
|
version=version,
|
||||||
version_prefix=version_prefix,
|
name=name,
|
||||||
error_format=error_format,
|
ignore_body=ignore_body,
|
||||||
**ctx_kwargs,
|
version_prefix=version_prefix,
|
||||||
|
error_format=error_format,
|
||||||
|
**ctx_kwargs,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def websocket(
|
def websocket(
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from email.utils import formatdate
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import PurePath
|
from pathlib import Path, PurePath
|
||||||
|
from time import time
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
@ -23,7 +26,12 @@ from sanic.compat import Header, open_async
|
|||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
|
||||||
from sanic.cookies import CookieJar
|
from sanic.cookies import CookieJar
|
||||||
from sanic.exceptions import SanicException, ServerError
|
from sanic.exceptions import SanicException, ServerError
|
||||||
from sanic.helpers import has_message_body, remove_entity_headers
|
from sanic.helpers import (
|
||||||
|
Default,
|
||||||
|
_default,
|
||||||
|
has_message_body,
|
||||||
|
remove_entity_headers,
|
||||||
|
)
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.models.protocol_types import HTMLProtocol, Range
|
from sanic.models.protocol_types import HTMLProtocol, Range
|
||||||
|
|
||||||
@ -309,6 +317,9 @@ async def file(
|
|||||||
mime_type: Optional[str] = None,
|
mime_type: Optional[str] = None,
|
||||||
headers: Optional[Dict[str, str]] = None,
|
headers: Optional[Dict[str, str]] = None,
|
||||||
filename: Optional[str] = None,
|
filename: Optional[str] = None,
|
||||||
|
last_modified: Optional[Union[datetime, float, int, Default]] = _default,
|
||||||
|
max_age: Optional[Union[float, int]] = None,
|
||||||
|
no_store: Optional[bool] = None,
|
||||||
_range: Optional[Range] = None,
|
_range: Optional[Range] = None,
|
||||||
) -> HTTPResponse:
|
) -> HTTPResponse:
|
||||||
"""Return a response object with file data.
|
"""Return a response object with file data.
|
||||||
@ -317,6 +328,9 @@ async def file(
|
|||||||
:param mime_type: Specific mime_type.
|
:param mime_type: Specific mime_type.
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param filename: Override filename.
|
:param filename: Override filename.
|
||||||
|
:param last_modified: The last modified date and time of the file.
|
||||||
|
:param max_age: Max age for cache control.
|
||||||
|
:param no_store: Any cache should not store this response.
|
||||||
:param _range:
|
:param _range:
|
||||||
"""
|
"""
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
@ -324,6 +338,33 @@ async def file(
|
|||||||
headers.setdefault(
|
headers.setdefault(
|
||||||
"Content-Disposition", f'attachment; filename="{filename}"'
|
"Content-Disposition", f'attachment; filename="{filename}"'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if isinstance(last_modified, datetime):
|
||||||
|
last_modified = last_modified.timestamp()
|
||||||
|
elif isinstance(last_modified, Default):
|
||||||
|
last_modified = Path(location).stat().st_mtime
|
||||||
|
|
||||||
|
if last_modified:
|
||||||
|
headers.setdefault(
|
||||||
|
"last-modified", formatdate(last_modified, usegmt=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if no_store:
|
||||||
|
cache_control = "no-store"
|
||||||
|
elif max_age:
|
||||||
|
cache_control = f"public, max-age={max_age}"
|
||||||
|
headers.setdefault(
|
||||||
|
"expires",
|
||||||
|
formatdate(
|
||||||
|
time() + max_age,
|
||||||
|
usegmt=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cache_control = "no-cache"
|
||||||
|
|
||||||
|
headers.setdefault("cache-control", cache_control)
|
||||||
|
|
||||||
filename = filename or path.split(location)[-1]
|
filename = filename or path.split(location)[-1]
|
||||||
|
|
||||||
async with await open_async(location, mode="rb") as f:
|
async with await open_async(location, mode="rb") as f:
|
||||||
|
@ -8,6 +8,8 @@ from sanic.touchup.meta import TouchUpMeta
|
|||||||
if TYPE_CHECKING: # no cov
|
if TYPE_CHECKING: # no cov
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
from asyncio import CancelledError
|
from asyncio import CancelledError
|
||||||
from time import monotonic as current_time
|
from time import monotonic as current_time
|
||||||
|
|
||||||
@ -169,7 +171,10 @@ class HttpProtocol(SanicProtocol, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
self.loop.call_later(max(0.1, interval), self.check_timeouts)
|
self.loop.call_later(max(0.1, interval), self.check_timeouts)
|
||||||
return
|
return
|
||||||
self._task.cancel()
|
cancel_msg_args = ()
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
cancel_msg_args = ("Cancel connection task with a timeout",)
|
||||||
|
self._task.cancel(*cancel_msg_args)
|
||||||
except Exception:
|
except Exception:
|
||||||
error_logger.exception("protocol.check_timeouts")
|
error_logger.exception("protocol.check_timeouts")
|
||||||
|
|
||||||
|
@ -24,9 +24,7 @@ class OptionalDispatchEvent(BaseScheme):
|
|||||||
raw_source = getsource(method)
|
raw_source = getsource(method)
|
||||||
src = dedent(raw_source)
|
src = dedent(raw_source)
|
||||||
tree = parse(src)
|
tree = parse(src)
|
||||||
node = RemoveDispatch(
|
node = RemoveDispatch(self._registered_events).visit(tree)
|
||||||
self._registered_events, self.app.state.verbosity
|
|
||||||
).visit(tree)
|
|
||||||
compiled_src = compile(node, method.__name__, "exec")
|
compiled_src = compile(node, method.__name__, "exec")
|
||||||
exec_locals: Dict[str, Any] = {}
|
exec_locals: Dict[str, Any] = {}
|
||||||
exec(compiled_src, module_globals, exec_locals) # nosec
|
exec(compiled_src, module_globals, exec_locals) # nosec
|
||||||
@ -64,9 +62,8 @@ class OptionalDispatchEvent(BaseScheme):
|
|||||||
|
|
||||||
|
|
||||||
class RemoveDispatch(NodeTransformer):
|
class RemoveDispatch(NodeTransformer):
|
||||||
def __init__(self, registered_events, verbosity: int = 0) -> None:
|
def __init__(self, registered_events) -> None:
|
||||||
self._registered_events = registered_events
|
self._registered_events = registered_events
|
||||||
self._verbosity = verbosity
|
|
||||||
|
|
||||||
def visit_Expr(self, node: Expr) -> Any:
|
def visit_Expr(self, node: Expr) -> Any:
|
||||||
call = node.value
|
call = node.value
|
||||||
@ -83,8 +80,10 @@ class RemoveDispatch(NodeTransformer):
|
|||||||
if hasattr(event, "s"):
|
if hasattr(event, "s"):
|
||||||
event_name = getattr(event, "value", event.s)
|
event_name = getattr(event, "value", event.s)
|
||||||
if self._not_registered(event_name):
|
if self._not_registered(event_name):
|
||||||
if self._verbosity >= 2:
|
logger.debug(
|
||||||
logger.debug(f"Disabling event: {event_name}")
|
f"Disabling event: {event_name}",
|
||||||
|
extra={"verbosity": 2},
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
11
setup.cfg
11
setup.cfg
@ -1,13 +1,2 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
ignore = E203, W503
|
ignore = E203, W503
|
||||||
|
|
||||||
[isort]
|
|
||||||
atomic = true
|
|
||||||
default_section = THIRDPARTY
|
|
||||||
include_trailing_comma = true
|
|
||||||
known_first_party = sanic
|
|
||||||
known_third_party = pytest
|
|
||||||
line_length = 79
|
|
||||||
lines_after_imports = 2
|
|
||||||
lines_between_types = 1
|
|
||||||
multi_line_output = 3
|
|
||||||
|
@ -9,10 +9,11 @@ import uvicorn
|
|||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.application.state import Mode
|
from sanic.application.state import Mode
|
||||||
from sanic.asgi import MockTransport
|
from sanic.asgi import MockTransport
|
||||||
from sanic.exceptions import Forbidden, BadRequest, ServiceUnavailable
|
from sanic.exceptions import BadRequest, Forbidden, ServiceUnavailable
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.server.websockets.connection import WebSocketConnection
|
from sanic.server.websockets.connection import WebSocketConnection
|
||||||
|
from sanic.signals import RESERVED_NAMESPACES
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -221,6 +222,7 @@ def test_listeners_triggered_async(app, caplog):
|
|||||||
assert after_server_stop
|
assert after_server_stop
|
||||||
|
|
||||||
app.state.mode = Mode.DEBUG
|
app.state.mode = Mode.DEBUG
|
||||||
|
app.state.verbosity = 0
|
||||||
with caplog.at_level(logging.DEBUG):
|
with caplog.at_level(logging.DEBUG):
|
||||||
server.run()
|
server.run()
|
||||||
|
|
||||||
@ -513,3 +515,34 @@ async def test_request_exception_suppressed_by_middleware(app):
|
|||||||
|
|
||||||
_, response = await app.asgi_client.get("/error-prone")
|
_, response = await app.asgi_client.get("/error-prone")
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_signals_triggered(app):
|
||||||
|
@app.get("/test_signals_triggered")
|
||||||
|
async def _request(request):
|
||||||
|
return text("test_signals_triggered")
|
||||||
|
|
||||||
|
signals_triggered = []
|
||||||
|
signals_expected = [
|
||||||
|
# "http.lifecycle.begin",
|
||||||
|
# "http.lifecycle.read_head",
|
||||||
|
"http.lifecycle.request",
|
||||||
|
"http.lifecycle.handle",
|
||||||
|
"http.routing.before",
|
||||||
|
"http.routing.after",
|
||||||
|
"http.lifecycle.response",
|
||||||
|
# "http.lifecycle.send",
|
||||||
|
# "http.lifecycle.complete",
|
||||||
|
]
|
||||||
|
|
||||||
|
def signal_handler(signal):
|
||||||
|
return lambda *a, **kw: signals_triggered.append(signal)
|
||||||
|
|
||||||
|
for signal in RESERVED_NAMESPACES["http"]:
|
||||||
|
app.signal(signal)(signal_handler(signal))
|
||||||
|
|
||||||
|
_, response = await app.asgi_client.get("/test_signals_triggered")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.text == "test_signals_triggered"
|
||||||
|
assert signals_triggered == signals_expected
|
||||||
|
@ -3,12 +3,7 @@ from pytest import raises
|
|||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import BadRequest, Forbidden, SanicException, ServerError
|
||||||
Forbidden,
|
|
||||||
BadRequest,
|
|
||||||
SanicException,
|
|
||||||
ServerError,
|
|
||||||
)
|
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import HTTPResponse, text
|
from sanic.response import HTTPResponse, text
|
||||||
|
|
||||||
|
@ -7,12 +7,7 @@ import pytest
|
|||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
from sanic.blueprints import Blueprint
|
from sanic.blueprints import Blueprint
|
||||||
from sanic.constants import HTTP_METHODS
|
from sanic.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import BadRequest, NotFound, SanicException, ServerError
|
||||||
BadRequest,
|
|
||||||
NotFound,
|
|
||||||
SanicException,
|
|
||||||
ServerError,
|
|
||||||
)
|
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ from bs4 import BeautifulSoup
|
|||||||
from pytest import LogCaptureFixture, MonkeyPatch
|
from pytest import LogCaptureFixture, MonkeyPatch
|
||||||
|
|
||||||
from sanic import Sanic, handlers
|
from sanic import Sanic, handlers
|
||||||
from sanic.exceptions import Forbidden, BadRequest, NotFound, ServerError
|
from sanic.exceptions import BadRequest, Forbidden, NotFound, ServerError
|
||||||
from sanic.handlers import ErrorHandler
|
from sanic.handlers import ErrorHandler
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.response import stream, text
|
from sanic.response import stream, text
|
||||||
|
@ -209,3 +209,42 @@ def test_access_log_client_ip_reqip(monkeypatch):
|
|||||||
"request": f"GET {request.scheme}://{request.host}/",
|
"request": f"GET {request.scheme}://{request.host}/",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"app_verbosity,log_verbosity,exists",
|
||||||
|
(
|
||||||
|
(0, 0, True),
|
||||||
|
(0, 1, False),
|
||||||
|
(0, 2, False),
|
||||||
|
(1, 0, True),
|
||||||
|
(1, 1, True),
|
||||||
|
(1, 2, False),
|
||||||
|
(2, 0, True),
|
||||||
|
(2, 1, True),
|
||||||
|
(2, 2, True),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_verbosity(app, caplog, app_verbosity, log_verbosity, exists):
|
||||||
|
rand_string = str(uuid.uuid4())
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def log_info(request):
|
||||||
|
logger.info("DEFAULT")
|
||||||
|
logger.info(rand_string, extra={"verbosity": log_verbosity})
|
||||||
|
return text("hello")
|
||||||
|
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
_ = app.test_client.get(
|
||||||
|
"/", server_kwargs={"verbosity": app_verbosity}
|
||||||
|
)
|
||||||
|
|
||||||
|
record = ("sanic.root", logging.INFO, rand_string)
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
assert record in caplog.record_tuples
|
||||||
|
else:
|
||||||
|
assert record not in caplog.record_tuples
|
||||||
|
|
||||||
|
if app_verbosity == 0:
|
||||||
|
assert ("sanic.root", logging.INFO, "DEFAULT") in caplog.record_tuples
|
||||||
|
@ -3,10 +3,13 @@ import inspect
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from datetime import datetime
|
||||||
|
from email.utils import formatdate
|
||||||
from logging import ERROR, LogRecord
|
from logging import ERROR, LogRecord
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
|
from pathlib import Path
|
||||||
from random import choice
|
from random import choice
|
||||||
from typing import Callable, List
|
from typing import Callable, List, Union
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -328,12 +331,27 @@ def static_file_directory():
|
|||||||
return static_directory
|
return static_directory
|
||||||
|
|
||||||
|
|
||||||
def get_file_content(static_file_directory, file_name):
|
def path_str_to_path_obj(static_file_directory: Union[Path, str]):
|
||||||
|
if isinstance(static_file_directory, str):
|
||||||
|
static_file_directory = Path(static_file_directory)
|
||||||
|
return static_file_directory
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_content(static_file_directory: Union[Path, str], file_name: str):
|
||||||
"""The content of the static file to check"""
|
"""The content of the static file to check"""
|
||||||
with open(os.path.join(static_file_directory, file_name), "rb") as file:
|
static_file_directory = path_str_to_path_obj(static_file_directory)
|
||||||
|
with open(static_file_directory / file_name, "rb") as file:
|
||||||
return file.read()
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_last_modified_timestamp(
|
||||||
|
static_file_directory: Union[Path, str], file_name: str
|
||||||
|
):
|
||||||
|
"""The content of the static file to check"""
|
||||||
|
static_file_directory = path_str_to_path_obj(static_file_directory)
|
||||||
|
return (static_file_directory / file_name).stat().st_mtime
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"file_name", ["test.file", "decode me.txt", "python.png"]
|
"file_name", ["test.file", "decode me.txt", "python.png"]
|
||||||
)
|
)
|
||||||
@ -711,3 +729,84 @@ def send_response_after_eof_should_fail(
|
|||||||
assert "foo, " in response.text
|
assert "foo, " in response.text
|
||||||
assert message_in_records(caplog.records, error_msg1)
|
assert message_in_records(caplog.records, error_msg1)
|
||||||
assert message_in_records(caplog.records, error_msg2)
|
assert message_in_records(caplog.records, error_msg2)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name", ["test.file", "decode me.txt", "python.png"]
|
||||||
|
)
|
||||||
|
def test_file_response_headers(
|
||||||
|
app: Sanic, file_name: str, static_file_directory: str
|
||||||
|
):
|
||||||
|
test_last_modified = datetime.now()
|
||||||
|
test_max_age = 10
|
||||||
|
test_expires = test_last_modified.timestamp() + test_max_age
|
||||||
|
|
||||||
|
@app.route("/files/cached/<filename>", methods=["GET"])
|
||||||
|
def file_route_cache(request, filename):
|
||||||
|
file_path = (Path(static_file_directory) / file_name).absolute()
|
||||||
|
return file(
|
||||||
|
file_path, max_age=test_max_age, last_modified=test_last_modified
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route(
|
||||||
|
"/files/cached_default_last_modified/<filename>", methods=["GET"]
|
||||||
|
)
|
||||||
|
def file_route_cache_default_last_modified(request, filename):
|
||||||
|
file_path = (Path(static_file_directory) / file_name).absolute()
|
||||||
|
return file(file_path, max_age=test_max_age)
|
||||||
|
|
||||||
|
@app.route("/files/no_cache/<filename>", methods=["GET"])
|
||||||
|
def file_route_no_cache(request, filename):
|
||||||
|
file_path = (Path(static_file_directory) / file_name).absolute()
|
||||||
|
return file(file_path)
|
||||||
|
|
||||||
|
@app.route("/files/no_store/<filename>", methods=["GET"])
|
||||||
|
def file_route_no_store(request, filename):
|
||||||
|
file_path = (Path(static_file_directory) / file_name).absolute()
|
||||||
|
return file(file_path, no_store=True)
|
||||||
|
|
||||||
|
_, response = app.test_client.get(f"/files/cached/{file_name}")
|
||||||
|
assert response.body == get_file_content(static_file_directory, file_name)
|
||||||
|
headers = response.headers
|
||||||
|
assert (
|
||||||
|
"cache-control" in headers
|
||||||
|
and f"max-age={test_max_age}" in headers.get("cache-control")
|
||||||
|
and f"public" in headers.get("cache-control")
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
"expires" in headers
|
||||||
|
and headers.get("expires")[:-6]
|
||||||
|
== formatdate(test_expires, usegmt=True)[:-6]
|
||||||
|
# [:-6] to allow at most 1 min difference
|
||||||
|
# It's minimal for cases like:
|
||||||
|
# Thu, 26 May 2022 05:36:49 GMT
|
||||||
|
# AND
|
||||||
|
# Thu, 26 May 2022 05:36:50 GMT
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "last-modified" in headers and headers.get(
|
||||||
|
"last-modified"
|
||||||
|
) == formatdate(test_last_modified.timestamp(), usegmt=True)
|
||||||
|
|
||||||
|
_, response = app.test_client.get(
|
||||||
|
f"/files/cached_default_last_modified/{file_name}"
|
||||||
|
)
|
||||||
|
file_last_modified = get_file_last_modified_timestamp(
|
||||||
|
static_file_directory, file_name
|
||||||
|
)
|
||||||
|
headers = response.headers
|
||||||
|
assert "last-modified" in headers and headers.get(
|
||||||
|
"last-modified"
|
||||||
|
) == formatdate(file_last_modified, usegmt=True)
|
||||||
|
|
||||||
|
_, response = app.test_client.get(f"/files/no_cache/{file_name}")
|
||||||
|
headers = response.headers
|
||||||
|
assert "cache-control" in headers and f"no-cache" == headers.get(
|
||||||
|
"cache-control"
|
||||||
|
)
|
||||||
|
|
||||||
|
_, response = app.test_client.get(f"/files/no_store/{file_name}")
|
||||||
|
headers = response.headers
|
||||||
|
assert "cache-control" in headers and f"no-store" == headers.get(
|
||||||
|
"cache-control"
|
||||||
|
)
|
||||||
|
4
tox.ini
4
tox.ini
@ -19,8 +19,8 @@ commands =
|
|||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
commands =
|
commands =
|
||||||
flake8 sanic
|
flake8 sanic
|
||||||
black --config ./.black.toml --check --verbose sanic/
|
black --check --verbose sanic/
|
||||||
isort --check-only sanic --profile=black
|
isort --check-only sanic
|
||||||
slotscheck --verbose -m sanic
|
slotscheck --verbose -m sanic
|
||||||
|
|
||||||
[testenv:type-checking]
|
[testenv:type-checking]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user