Compare commits
7 Commits
middleware
...
v22.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5052321801 | ||
|
|
23ce4eaaa4 | ||
|
|
23a430c4ad | ||
|
|
ec158ffa69 | ||
|
|
6e32270036 | ||
|
|
43ba381e7b | ||
|
|
16503319e5 |
@@ -1 +1 @@
|
|||||||
__version__ = "22.9.1"
|
__version__ = "22.9.0"
|
||||||
|
|||||||
272
sanic/app.py
272
sanic/app.py
@@ -47,7 +47,7 @@ from sanic_routing.exceptions import FinalizationError, NotFound
|
|||||||
from sanic_routing.route import Route
|
from sanic_routing.route import Route
|
||||||
|
|
||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.state import ApplicationState, Mode, ServerStage
|
from sanic.application.state import ApplicationState, ServerStage
|
||||||
from sanic.asgi import ASGIApp
|
from sanic.asgi import ASGIApp
|
||||||
from sanic.base.root import BaseSanic
|
from sanic.base.root import BaseSanic
|
||||||
from sanic.blueprint_group import BlueprintGroup
|
from sanic.blueprint_group import BlueprintGroup
|
||||||
@@ -158,7 +158,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
_app_registry: Dict[str, "Sanic"] = {}
|
_app_registry: Dict[str, "Sanic"] = {}
|
||||||
_uvloop_setting = None # TODO: Remove in v22.6
|
|
||||||
test_mode = False
|
test_mode = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -394,8 +393,8 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
routes = [routes]
|
routes = [routes]
|
||||||
|
|
||||||
for r in routes:
|
for r in routes:
|
||||||
r.ctx.websocket = websocket
|
r.extra.websocket = websocket
|
||||||
r.ctx.static = params.get("static", False)
|
r.extra.static = params.get("static", False)
|
||||||
r.ctx.__dict__.update(ctx)
|
r.ctx.__dict__.update(ctx)
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
@@ -589,7 +588,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
uri = route.path
|
uri = route.path
|
||||||
|
|
||||||
if getattr(route.ctx, "static", None):
|
if getattr(route.extra, "static", None):
|
||||||
filename = kwargs.pop("filename", "")
|
filename = kwargs.pop("filename", "")
|
||||||
# it's static folder
|
# it's static folder
|
||||||
if "__file_uri__" in uri:
|
if "__file_uri__" in uri:
|
||||||
@@ -622,18 +621,18 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
host = kwargs.pop("_host", None)
|
host = kwargs.pop("_host", None)
|
||||||
external = kwargs.pop("_external", False) or bool(host)
|
external = kwargs.pop("_external", False) or bool(host)
|
||||||
scheme = kwargs.pop("_scheme", "")
|
scheme = kwargs.pop("_scheme", "")
|
||||||
if route.ctx.hosts and external:
|
if route.extra.hosts and external:
|
||||||
if not host and len(route.ctx.hosts) > 1:
|
if not host and len(route.extra.hosts) > 1:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Host is ambiguous: {', '.join(route.ctx.hosts)}"
|
f"Host is ambiguous: {', '.join(route.extra.hosts)}"
|
||||||
)
|
)
|
||||||
elif host and host not in route.ctx.hosts:
|
elif host and host not in route.extra.hosts:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Requested host ({host}) is not available for this "
|
f"Requested host ({host}) is not available for this "
|
||||||
f"route: {route.ctx.hosts}"
|
f"route: {route.extra.hosts}"
|
||||||
)
|
)
|
||||||
elif not host:
|
elif not host:
|
||||||
host = list(route.ctx.hosts)[0]
|
host = list(route.extra.hosts)[0]
|
||||||
|
|
||||||
if scheme and not external:
|
if scheme and not external:
|
||||||
raise ValueError("When specifying _scheme, _external must be True")
|
raise ValueError("When specifying _scheme, _external must be True")
|
||||||
@@ -709,7 +708,10 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
async def handle_exception(
|
async def handle_exception(
|
||||||
self, request: Request, exception: BaseException
|
self,
|
||||||
|
request: Request,
|
||||||
|
exception: BaseException,
|
||||||
|
run_middleware: bool = True,
|
||||||
): # no cov
|
): # no cov
|
||||||
"""
|
"""
|
||||||
A handler that catches specific exceptions and outputs a response.
|
A handler that catches specific exceptions and outputs a response.
|
||||||
@@ -718,6 +720,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
:param exception: The exception that was raised
|
:param exception: The exception that was raised
|
||||||
:raises ServerError: response 500
|
:raises ServerError: response 500
|
||||||
"""
|
"""
|
||||||
|
response = None
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.exception",
|
"http.lifecycle.exception",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -758,9 +761,11 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
response = await self._run_request_middleware(
|
if run_middleware:
|
||||||
request, request_name=None
|
middleware = (
|
||||||
)
|
request.route and request.route.extra.request_middleware
|
||||||
|
) or self.request_middleware
|
||||||
|
response = await self._run_request_middleware(request, middleware)
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
try:
|
try:
|
||||||
@@ -840,7 +845,13 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
|
|
||||||
# Define `response` var here to remove warnings about
|
# Define `response` var here to remove warnings about
|
||||||
# allocation before assignment below.
|
# allocation before assignment below.
|
||||||
response = None
|
response: Optional[
|
||||||
|
Union[
|
||||||
|
BaseHTTPResponse,
|
||||||
|
Coroutine[Any, Any, Optional[BaseHTTPResponse]],
|
||||||
|
]
|
||||||
|
] = None
|
||||||
|
run_middleware = True
|
||||||
try:
|
try:
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
@@ -872,7 +883,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
if (
|
if (
|
||||||
request.stream
|
request.stream
|
||||||
and request.stream.request_body
|
and request.stream.request_body
|
||||||
and not route.ctx.ignore_body
|
and not route.extra.ignore_body
|
||||||
):
|
):
|
||||||
|
|
||||||
if hasattr(handler, "is_stream"):
|
if hasattr(handler, "is_stream"):
|
||||||
@@ -885,9 +896,11 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Request Middleware
|
# Request Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
response = await self._run_request_middleware(
|
run_middleware = False
|
||||||
request, request_name=route.name
|
if request.route.extra.request_middleware:
|
||||||
)
|
response = await self._run_request_middleware(
|
||||||
|
request, request.route.extra.request_middleware
|
||||||
|
)
|
||||||
|
|
||||||
# No middleware results
|
# No middleware results
|
||||||
if not response:
|
if not response:
|
||||||
@@ -928,7 +941,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
if request.stream is not None:
|
if request.stream is not None:
|
||||||
response = request.stream.response
|
response = request.stream.response
|
||||||
elif response is not None:
|
elif response is not None:
|
||||||
response = await request.respond(response)
|
response = await request.respond(response) # type: ignore
|
||||||
elif not hasattr(handler, "is_websocket"):
|
elif not hasattr(handler, "is_websocket"):
|
||||||
response = request.stream.response # type: ignore
|
response = request.stream.response # type: ignore
|
||||||
|
|
||||||
@@ -946,7 +959,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
...
|
...
|
||||||
await response.send(end_stream=True)
|
await response.send(end_stream=True)
|
||||||
elif isinstance(response, ResponseStream):
|
elif isinstance(response, ResponseStream):
|
||||||
resp = await response(request)
|
resp = await response(request) # type: ignore
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.lifecycle.response",
|
"http.lifecycle.response",
|
||||||
inline=True,
|
inline=True,
|
||||||
@@ -955,7 +968,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
"response": resp,
|
"response": resp,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await response.eof()
|
await response.eof() # type: ignore
|
||||||
else:
|
else:
|
||||||
if not hasattr(handler, "is_websocket"):
|
if not hasattr(handler, "is_websocket"):
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
@@ -967,7 +980,9 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Response Generation Failed
|
# Response Generation Failed
|
||||||
await self.handle_exception(request, e)
|
await self.handle_exception(
|
||||||
|
request, e, run_middleware=run_middleware
|
||||||
|
)
|
||||||
|
|
||||||
async def _websocket_handler(
|
async def _websocket_handler(
|
||||||
self, handler, request, *args, subprotocols=None, **kwargs
|
self, handler, request, *args, subprotocols=None, **kwargs
|
||||||
@@ -1036,86 +1051,72 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
async def _run_request_middleware(
|
async def _run_request_middleware(
|
||||||
self, request, request_name=None
|
self, request, middleware_collection
|
||||||
): # no cov
|
): # no cov
|
||||||
# The if improves speed. I don't know why
|
request._request_middleware_started = True
|
||||||
named_middleware = self.named_request_middleware.get(
|
|
||||||
request_name, deque()
|
|
||||||
)
|
|
||||||
applicable_middleware = self.request_middleware + named_middleware
|
|
||||||
|
|
||||||
# request.request_middleware_started is meant as a stop-gap solution
|
for middleware in middleware_collection:
|
||||||
# until RFC 1630 is adopted
|
await self.dispatch(
|
||||||
if applicable_middleware and not request.request_middleware_started:
|
"http.middleware.before",
|
||||||
request.request_middleware_started = True
|
inline=True,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"response": None,
|
||||||
|
},
|
||||||
|
condition={"attach_to": "request"},
|
||||||
|
)
|
||||||
|
|
||||||
for middleware in applicable_middleware:
|
response = middleware(request)
|
||||||
await self.dispatch(
|
if isawaitable(response):
|
||||||
"http.middleware.before",
|
response = await response
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = middleware(request)
|
await self.dispatch(
|
||||||
if isawaitable(response):
|
"http.middleware.after",
|
||||||
response = await response
|
inline=True,
|
||||||
|
context={
|
||||||
|
"request": request,
|
||||||
|
"response": None,
|
||||||
|
},
|
||||||
|
condition={"attach_to": "request"},
|
||||||
|
)
|
||||||
|
|
||||||
await self.dispatch(
|
if response:
|
||||||
"http.middleware.after",
|
return response
|
||||||
inline=True,
|
|
||||||
context={
|
|
||||||
"request": request,
|
|
||||||
"response": None,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "request"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if response:
|
|
||||||
return response
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def _run_response_middleware(
|
async def _run_response_middleware(
|
||||||
self, request, response, request_name=None
|
self, request, response, middleware_collection
|
||||||
): # no cov
|
): # no cov
|
||||||
named_middleware = self.named_response_middleware.get(
|
for middleware in middleware_collection:
|
||||||
request_name, deque()
|
await self.dispatch(
|
||||||
)
|
"http.middleware.before",
|
||||||
applicable_middleware = self.response_middleware + named_middleware
|
inline=True,
|
||||||
if applicable_middleware:
|
context={
|
||||||
for middleware in applicable_middleware:
|
"request": request,
|
||||||
await self.dispatch(
|
"response": response,
|
||||||
"http.middleware.before",
|
},
|
||||||
inline=True,
|
condition={"attach_to": "response"},
|
||||||
context={
|
)
|
||||||
"request": request,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
condition={"attach_to": "response"},
|
|
||||||
)
|
|
||||||
|
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
|
|
||||||
await self.dispatch(
|
await self.dispatch(
|
||||||
"http.middleware.after",
|
"http.middleware.after",
|
||||||
inline=True,
|
inline=True,
|
||||||
context={
|
context={
|
||||||
"request": request,
|
"request": request,
|
||||||
"response": _response if _response else response,
|
"response": _response if _response else response,
|
||||||
},
|
},
|
||||||
condition={"attach_to": "response"},
|
condition={"attach_to": "response"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if _response:
|
if _response:
|
||||||
response = _response
|
response = _response
|
||||||
if isinstance(response, BaseHTTPResponse):
|
if isinstance(response, BaseHTTPResponse):
|
||||||
response = request.stream.respond(response)
|
response = request.stream.respond(response)
|
||||||
break
|
break
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _build_endpoint_name(self, *parts):
|
def _build_endpoint_name(self, *parts):
|
||||||
@@ -1344,18 +1345,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
def debug(self):
|
def debug(self):
|
||||||
return self.state.is_debug
|
return self.state.is_debug
|
||||||
|
|
||||||
@debug.setter
|
|
||||||
def debug(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Setting the value of a Sanic application's debug value directly "
|
|
||||||
"is deprecated and will be removed in v22.9. Please set it using "
|
|
||||||
"the CLI, app.run, app.prepare, or directly set "
|
|
||||||
"app.state.mode to Mode.DEBUG.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
mode = Mode.DEBUG if value else Mode.PRODUCTION
|
|
||||||
self.state.mode = mode
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_reload(self):
|
def auto_reload(self):
|
||||||
return self.config.AUTO_RELOAD
|
return self.config.AUTO_RELOAD
|
||||||
@@ -1372,58 +1361,6 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
"""
|
"""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
|
||||||
def is_running(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_running
|
|
||||||
|
|
||||||
@is_running.setter
|
|
||||||
def is_running(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_running property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_running = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_stopping(self):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
return self.state.is_stopping
|
|
||||||
|
|
||||||
@is_stopping.setter
|
|
||||||
def is_stopping(self, value: bool):
|
|
||||||
deprecation(
|
|
||||||
"Use of the is_stopping property is no longer used by Sanic "
|
|
||||||
"internally. The property is now deprecated and will be removed "
|
|
||||||
"in version 22.9. You may continue to set the property for your "
|
|
||||||
"own needs until that time. If you would like to check whether "
|
|
||||||
"the application is operational, please use app.state.stage. More "
|
|
||||||
"information is available at ___.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
self.state.is_stopping = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reload_dirs(self):
|
def reload_dirs(self):
|
||||||
return self.state.reload_dirs
|
return self.state.reload_dirs
|
||||||
@@ -1518,6 +1455,16 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
return cls(name)
|
return cls(name)
|
||||||
raise SanicException(f'Sanic app name "{name}" not found.')
|
raise SanicException(f'Sanic app name "{name}" not found.')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_uvloop_conflict(cls) -> None:
|
||||||
|
values = {app.config.USE_UVLOOP for app in cls._app_registry.values()}
|
||||||
|
if len(values) > 1:
|
||||||
|
error_logger.warning(
|
||||||
|
"It looks like you're running several apps with different "
|
||||||
|
"uvloop settings. This is not supported and may lead to "
|
||||||
|
"unintended behaviour."
|
||||||
|
)
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
@@ -1528,6 +1475,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
except FinalizationError as e:
|
except FinalizationError as e:
|
||||||
if not Sanic.test_mode:
|
if not Sanic.test_mode:
|
||||||
raise e
|
raise e
|
||||||
|
self.finalize_middleware()
|
||||||
|
|
||||||
def signalize(self, allow_fail_builtin=True):
|
def signalize(self, allow_fail_builtin=True):
|
||||||
self.signal_router.allow_fail_builtin = allow_fail_builtin
|
self.signal_router.allow_fail_builtin = allow_fail_builtin
|
||||||
@@ -1566,17 +1514,7 @@ class Sanic(BaseSanic, StartupMixin, metaclass=TouchUpMeta):
|
|||||||
23.3,
|
23.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Replace in v22.6 to check against apps in app registry
|
Sanic._check_uvloop_conflict()
|
||||||
if (
|
|
||||||
self.__class__._uvloop_setting is not None
|
|
||||||
and self.__class__._uvloop_setting != self.config.USE_UVLOOP
|
|
||||||
):
|
|
||||||
error_logger.warning(
|
|
||||||
"It looks like you're running several apps with different "
|
|
||||||
"uvloop settings. This is not supported and may lead to "
|
|
||||||
"unintended behaviour."
|
|
||||||
)
|
|
||||||
self.__class__._uvloop_setting = self.config.USE_UVLOOP
|
|
||||||
|
|
||||||
# Startup time optimizations
|
# Startup time optimizations
|
||||||
if self.state.primary:
|
if self.state.primary:
|
||||||
|
|||||||
@@ -234,4 +234,7 @@ class ASGIApp:
|
|||||||
self.stage = Stage.HANDLER
|
self.stage = Stage.HANDLER
|
||||||
await self.sanic_app.handle_request(self.request)
|
await self.sanic_app.handle_request(self.request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await self.sanic_app.handle_exception(self.request, e)
|
try:
|
||||||
|
await self.sanic_app.handle_exception(self.request, e)
|
||||||
|
except Exception as exc:
|
||||||
|
await self.sanic_app.handle_exception(self.request, exc, False)
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ class Blueprint(BaseSanic):
|
|||||||
|
|
||||||
self.routes += [route for route in routes if isinstance(route, Route)]
|
self.routes += [route for route in routes if isinstance(route, Route)]
|
||||||
self.websocket_routes += [
|
self.websocket_routes += [
|
||||||
route for route in self.routes if route.ctx.websocket
|
route for route in self.routes if route.extra.websocket
|
||||||
]
|
]
|
||||||
self.middlewares += middleware
|
self.middlewares += middleware
|
||||||
self.exceptions += exception_handlers
|
self.exceptions += exception_handlers
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from sanic.constants import LocalCertCreator
|
|||||||
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
|
||||||
from sanic.helpers import Default, _default
|
from sanic.helpers import Default, _default
|
||||||
from sanic.http import Http
|
from sanic.http import Http
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.utils import load_module_from_file_location, str_to_bool
|
from sanic.utils import load_module_from_file_location, str_to_bool
|
||||||
|
|
||||||
|
|
||||||
@@ -71,10 +71,6 @@ DEFAULT_CONFIG = {
|
|||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
# These values will be removed from the Config object in v22.6 and moved
|
|
||||||
# to the application state
|
|
||||||
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptorMeta(type):
|
class DescriptorMeta(type):
|
||||||
def __init__(cls, *_):
|
def __init__(cls, *_):
|
||||||
@@ -132,6 +128,7 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
):
|
):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
super().__init__({**DEFAULT_CONFIG, **defaults})
|
super().__init__({**DEFAULT_CONFIG, **defaults})
|
||||||
|
self._configure_warnings()
|
||||||
|
|
||||||
self._converters = [str, str_to_bool, float, int]
|
self._converters = [str, str_to_bool, float, int]
|
||||||
|
|
||||||
@@ -149,7 +146,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
self.load_environment_vars(SANIC_PREFIX)
|
self.load_environment_vars(SANIC_PREFIX)
|
||||||
|
|
||||||
self._configure_header_size()
|
self._configure_header_size()
|
||||||
self._configure_warnings()
|
|
||||||
self._check_error_format()
|
self._check_error_format()
|
||||||
self._init = True
|
self._init = True
|
||||||
|
|
||||||
@@ -241,7 +237,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
"""
|
"""
|
||||||
Looks for prefixed environment variables and applies them to the
|
Looks for prefixed environment variables and applies them to the
|
||||||
configuration if present. This is called automatically when Sanic
|
configuration if present. This is called automatically when Sanic
|
||||||
starts up to load environment variables into config.
|
starts up to load environment variables into config. Environment
|
||||||
|
variables should start with the defined prefix and should only
|
||||||
|
contain uppercase letters.
|
||||||
|
|
||||||
It will automatically hydrate the following types:
|
It will automatically hydrate the following types:
|
||||||
|
|
||||||
@@ -267,12 +265,9 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
`See user guide re: config
|
`See user guide re: config
|
||||||
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
<https://sanicframework.org/guide/deployment/configuration.html>`__
|
||||||
"""
|
"""
|
||||||
lower_case_var_found = False
|
|
||||||
for key, value in environ.items():
|
for key, value in environ.items():
|
||||||
if not key.startswith(prefix):
|
if not key.startswith(prefix) or not key.isupper():
|
||||||
continue
|
continue
|
||||||
if not key.isupper():
|
|
||||||
lower_case_var_found = True
|
|
||||||
|
|
||||||
_, config_key = key.split(prefix, 1)
|
_, config_key = key.split(prefix, 1)
|
||||||
|
|
||||||
@@ -282,12 +277,6 @@ class Config(dict, metaclass=DescriptorMeta):
|
|||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
if lower_case_var_found:
|
|
||||||
deprecation(
|
|
||||||
"Lowercase environment variables will not be "
|
|
||||||
"loaded into Sanic config beginning in v22.9.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_config(self, config: Union[bytes, str, dict, Any]):
|
def update_config(self, config: Union[bytes, str, dict, Any]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -448,8 +448,8 @@ def exception_response(
|
|||||||
# from the route
|
# from the route
|
||||||
if request.route:
|
if request.route:
|
||||||
try:
|
try:
|
||||||
if request.route.ctx.error_format:
|
if request.route.extra.error_format:
|
||||||
render_format = request.route.ctx.error_format
|
render_format = request.route.extra.error_format
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -428,7 +428,10 @@ class Http(Stream, metaclass=TouchUpMeta):
|
|||||||
if self.request is None:
|
if self.request is None:
|
||||||
self.create_empty_request()
|
self.create_empty_request()
|
||||||
|
|
||||||
await app.handle_exception(self.request, exception)
|
try:
|
||||||
|
await app.handle_exception(self.request, exception)
|
||||||
|
except Exception as e:
|
||||||
|
await app.handle_exception(self.request, e, False)
|
||||||
|
|
||||||
def create_empty_request(self) -> None:
|
def create_empty_request(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
12
sanic/log.py
12
sanic/log.py
@@ -25,6 +25,12 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
|
|||||||
"propagate": True,
|
"propagate": True,
|
||||||
"qualname": "sanic.access",
|
"qualname": "sanic.access",
|
||||||
},
|
},
|
||||||
|
"sanic.server": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": True,
|
||||||
|
"qualname": "sanic.server",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
handlers={
|
handlers={
|
||||||
"console": {
|
"console": {
|
||||||
@@ -101,6 +107,12 @@ Logger used by Sanic for access logging
|
|||||||
"""
|
"""
|
||||||
access_logger.addFilter(_verbosity_filter)
|
access_logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
server_logger = logging.getLogger("sanic.server") # no cov
|
||||||
|
"""
|
||||||
|
Logger used by Sanic for server related messages
|
||||||
|
"""
|
||||||
|
logger.addFilter(_verbosity_filter)
|
||||||
|
|
||||||
|
|
||||||
def deprecation(message: str, version: float): # no cov
|
def deprecation(message: str, version: float): # no cov
|
||||||
version_info = f"[DEPRECATION v{version}] "
|
version_info = f"[DEPRECATION v{version}] "
|
||||||
|
|||||||
66
sanic/middleware.py
Normal file
66
sanic/middleware.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
|
from enum import IntEnum, auto
|
||||||
|
from itertools import count
|
||||||
|
from typing import Deque, Sequence, Union
|
||||||
|
|
||||||
|
from sanic.models.handler_types import MiddlewareType
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareLocation(IntEnum):
|
||||||
|
REQUEST = auto()
|
||||||
|
RESPONSE = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class Middleware:
|
||||||
|
_counter = count()
|
||||||
|
|
||||||
|
__slots__ = ("func", "priority", "location", "definition")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
func: MiddlewareType,
|
||||||
|
location: MiddlewareLocation,
|
||||||
|
priority: int = 0,
|
||||||
|
) -> None:
|
||||||
|
self.func = func
|
||||||
|
self.priority = priority
|
||||||
|
self.location = location
|
||||||
|
self.definition = next(Middleware._counter)
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return self.func(*args, **kwargs)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"{self.__class__.__name__}("
|
||||||
|
f"func=<function {self.func.__name__}>, "
|
||||||
|
f"priority={self.priority}, "
|
||||||
|
f"location={self.location.name})"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order(self):
|
||||||
|
return (self.priority, -self.definition)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert(
|
||||||
|
cls,
|
||||||
|
*middleware_collections: Sequence[Union[Middleware, MiddlewareType]],
|
||||||
|
location: MiddlewareLocation,
|
||||||
|
) -> Deque[Middleware]:
|
||||||
|
return deque(
|
||||||
|
[
|
||||||
|
middleware
|
||||||
|
if isinstance(middleware, Middleware)
|
||||||
|
else Middleware(middleware, location)
|
||||||
|
for collection in middleware_collections
|
||||||
|
for middleware in collection
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_count(cls):
|
||||||
|
cls._counter = count()
|
||||||
|
cls.count = next(cls._counter)
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from operator import attrgetter
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from sanic.base.meta import SanicMeta
|
from sanic.base.meta import SanicMeta
|
||||||
|
from sanic.middleware import Middleware, MiddlewareLocation
|
||||||
from sanic.models.futures import FutureMiddleware
|
from sanic.models.futures import FutureMiddleware
|
||||||
|
from sanic.router import Router
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareMixin(metaclass=SanicMeta):
|
class MiddlewareMixin(metaclass=SanicMeta):
|
||||||
|
router: Router
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self._future_middleware: List[FutureMiddleware] = []
|
self._future_middleware: List[FutureMiddleware] = []
|
||||||
|
|
||||||
@@ -13,7 +19,12 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
raise NotImplementedError # noqa
|
raise NotImplementedError # noqa
|
||||||
|
|
||||||
def middleware(
|
def middleware(
|
||||||
self, middleware_or_request, attach_to="request", apply=True
|
self,
|
||||||
|
middleware_or_request,
|
||||||
|
attach_to="request",
|
||||||
|
apply=True,
|
||||||
|
*,
|
||||||
|
priority=0
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Decorate and register middleware to be called before a request
|
Decorate and register middleware to be called before a request
|
||||||
@@ -30,6 +41,12 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
def register_middleware(middleware, attach_to="request"):
|
def register_middleware(middleware, attach_to="request"):
|
||||||
nonlocal apply
|
nonlocal apply
|
||||||
|
|
||||||
|
location = (
|
||||||
|
MiddlewareLocation.REQUEST
|
||||||
|
if attach_to == "request"
|
||||||
|
else MiddlewareLocation.RESPONSE
|
||||||
|
)
|
||||||
|
middleware = Middleware(middleware, location, priority=priority)
|
||||||
future_middleware = FutureMiddleware(middleware, attach_to)
|
future_middleware = FutureMiddleware(middleware, attach_to)
|
||||||
self._future_middleware.append(future_middleware)
|
self._future_middleware.append(future_middleware)
|
||||||
if apply:
|
if apply:
|
||||||
@@ -46,7 +63,7 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
register_middleware, attach_to=middleware_or_request
|
register_middleware, attach_to=middleware_or_request
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_request(self, middleware=None):
|
def on_request(self, middleware=None, *, priority=0):
|
||||||
"""Register a middleware to be called before a request is handled.
|
"""Register a middleware to be called before a request is handled.
|
||||||
|
|
||||||
This is the same as *@app.middleware('request')*.
|
This is the same as *@app.middleware('request')*.
|
||||||
@@ -54,11 +71,13 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
:param: middleware: A callable that takes in request.
|
:param: middleware: A callable that takes in request.
|
||||||
"""
|
"""
|
||||||
if callable(middleware):
|
if callable(middleware):
|
||||||
return self.middleware(middleware, "request")
|
return self.middleware(middleware, "request", priority=priority)
|
||||||
else:
|
else:
|
||||||
return partial(self.middleware, attach_to="request")
|
return partial(
|
||||||
|
self.middleware, attach_to="request", priority=priority
|
||||||
|
)
|
||||||
|
|
||||||
def on_response(self, middleware=None):
|
def on_response(self, middleware=None, *, priority=0):
|
||||||
"""Register a middleware to be called after a response is created.
|
"""Register a middleware to be called after a response is created.
|
||||||
|
|
||||||
This is the same as *@app.middleware('response')*.
|
This is the same as *@app.middleware('response')*.
|
||||||
@@ -67,6 +86,57 @@ class MiddlewareMixin(metaclass=SanicMeta):
|
|||||||
A callable that takes in a request and its response.
|
A callable that takes in a request and its response.
|
||||||
"""
|
"""
|
||||||
if callable(middleware):
|
if callable(middleware):
|
||||||
return self.middleware(middleware, "response")
|
return self.middleware(middleware, "response", priority=priority)
|
||||||
else:
|
else:
|
||||||
return partial(self.middleware, attach_to="response")
|
return partial(
|
||||||
|
self.middleware, attach_to="response", priority=priority
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize_middleware(self):
|
||||||
|
for route in self.router.routes:
|
||||||
|
request_middleware = Middleware.convert(
|
||||||
|
self.request_middleware,
|
||||||
|
self.named_request_middleware.get(route.name, deque()),
|
||||||
|
location=MiddlewareLocation.REQUEST,
|
||||||
|
)
|
||||||
|
response_middleware = Middleware.convert(
|
||||||
|
self.response_middleware,
|
||||||
|
self.named_response_middleware.get(route.name, deque()),
|
||||||
|
location=MiddlewareLocation.RESPONSE,
|
||||||
|
)
|
||||||
|
route.extra.request_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
request_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
route.extra.response_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
response_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)[::-1]
|
||||||
|
)
|
||||||
|
request_middleware = Middleware.convert(
|
||||||
|
self.request_middleware,
|
||||||
|
location=MiddlewareLocation.REQUEST,
|
||||||
|
)
|
||||||
|
response_middleware = Middleware.convert(
|
||||||
|
self.response_middleware,
|
||||||
|
location=MiddlewareLocation.RESPONSE,
|
||||||
|
)
|
||||||
|
self.request_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
request_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.response_middleware = deque(
|
||||||
|
sorted(
|
||||||
|
response_middleware,
|
||||||
|
key=attrgetter("order"),
|
||||||
|
reverse=True,
|
||||||
|
)[::-1]
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from ast import NodeVisitor, Return, parse
|
from ast import NodeVisitor, Return, parse
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from email.utils import formatdate
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -31,21 +32,13 @@ from sanic.handlers import ContentRangeHandler
|
|||||||
from sanic.log import error_logger
|
from sanic.log import error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
from sanic.models.handler_types import RouteHandler
|
from sanic.models.handler_types import RouteHandler
|
||||||
from sanic.response import HTTPResponse, file, file_stream
|
from sanic.response import HTTPResponse, file, file_stream, validate_file
|
||||||
from sanic.types import HashableDict
|
from sanic.types import HashableDict
|
||||||
|
|
||||||
|
|
||||||
RouteWrapper = Callable[
|
RouteWrapper = Callable[
|
||||||
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
|
||||||
]
|
]
|
||||||
RESTRICTED_ROUTE_CONTEXT = (
|
|
||||||
"ignore_body",
|
|
||||||
"stream",
|
|
||||||
"hosts",
|
|
||||||
"static",
|
|
||||||
"error_format",
|
|
||||||
"websocket",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RouteMixin(metaclass=SanicMeta):
|
class RouteMixin(metaclass=SanicMeta):
|
||||||
@@ -790,24 +783,9 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
async def _static_request_handler(
|
async def _get_file_path(self, file_or_directory, __file_uri__, not_found):
|
||||||
self,
|
|
||||||
file_or_directory,
|
|
||||||
use_modified_since,
|
|
||||||
use_content_range,
|
|
||||||
stream_large_files,
|
|
||||||
request,
|
|
||||||
content_type=None,
|
|
||||||
__file_uri__=None,
|
|
||||||
):
|
|
||||||
# Merge served directory and requested file if provided
|
|
||||||
file_path_raw = Path(unquote(file_or_directory))
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
root_path = file_path = file_path_raw.resolve()
|
root_path = file_path = file_path_raw.resolve()
|
||||||
not_found = FileNotFound(
|
|
||||||
"File not found",
|
|
||||||
path=file_or_directory,
|
|
||||||
relative_url=__file_uri__,
|
|
||||||
)
|
|
||||||
|
|
||||||
if __file_uri__:
|
if __file_uri__:
|
||||||
# Strip all / that in the beginning of the URL to help prevent
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
@@ -834,6 +812,29 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
f"relative_url={__file_uri__}"
|
f"relative_url={__file_uri__}"
|
||||||
)
|
)
|
||||||
raise not_found
|
raise not_found
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
async def _static_request_handler(
|
||||||
|
self,
|
||||||
|
file_or_directory,
|
||||||
|
use_modified_since,
|
||||||
|
use_content_range,
|
||||||
|
stream_large_files,
|
||||||
|
request,
|
||||||
|
content_type=None,
|
||||||
|
__file_uri__=None,
|
||||||
|
):
|
||||||
|
not_found = FileNotFound(
|
||||||
|
"File not found",
|
||||||
|
path=file_or_directory,
|
||||||
|
relative_url=__file_uri__,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge served directory and requested file if provided
|
||||||
|
file_path = await self._get_file_path(
|
||||||
|
file_or_directory, __file_uri__, not_found
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
@@ -841,15 +842,13 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
stats = None
|
stats = None
|
||||||
if use_modified_since:
|
if use_modified_since:
|
||||||
stats = await stat_async(file_path)
|
stats = await stat_async(file_path)
|
||||||
modified_since = strftime(
|
modified_since = stats.st_mtime
|
||||||
"%a, %d %b %Y %H:%M:%S GMT", gmtime(stats.st_mtime)
|
response = await validate_file(request.headers, modified_since)
|
||||||
|
if response:
|
||||||
|
return response
|
||||||
|
headers["Last-Modified"] = formatdate(
|
||||||
|
modified_since, usegmt=True
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
request.headers.getone("if-modified-since", None)
|
|
||||||
== modified_since
|
|
||||||
):
|
|
||||||
return HTTPResponse(status=304)
|
|
||||||
headers["Last-Modified"] = modified_since
|
|
||||||
_range = None
|
_range = None
|
||||||
if use_content_range:
|
if use_content_range:
|
||||||
_range = None
|
_range = None
|
||||||
@@ -864,8 +863,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
del headers["Content-Length"]
|
del headers["Content-Length"]
|
||||||
for key, value in _range.headers.items():
|
headers.update(_range.headers)
|
||||||
headers[key] = value
|
|
||||||
|
|
||||||
if "content-type" not in headers:
|
if "content-type" not in headers:
|
||||||
content_type = (
|
content_type = (
|
||||||
@@ -1041,24 +1039,12 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def _build_route_context(self, raw):
|
def _build_route_context(self, raw: Dict[str, Any]) -> HashableDict:
|
||||||
ctx_kwargs = {
|
ctx_kwargs = {
|
||||||
key.replace("ctx_", ""): raw.pop(key)
|
key.replace("ctx_", ""): raw.pop(key)
|
||||||
for key in {**raw}.keys()
|
for key in {**raw}.keys()
|
||||||
if key.startswith("ctx_")
|
if key.startswith("ctx_")
|
||||||
}
|
}
|
||||||
restricted = [
|
|
||||||
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
|
|
||||||
]
|
|
||||||
if restricted:
|
|
||||||
restricted_arguments = ", ".join(restricted)
|
|
||||||
raise AttributeError(
|
|
||||||
"Cannot use restricted route context: "
|
|
||||||
f"{restricted_arguments}. This limitation is only in place "
|
|
||||||
"until v22.9 when the restricted names will no longer be in"
|
|
||||||
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
|
|
||||||
"for more information."
|
|
||||||
)
|
|
||||||
if raw:
|
if raw:
|
||||||
unexpected_arguments = ", ".join(raw.keys())
|
unexpected_arguments = ", ".join(raw.keys())
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from typing import (
|
|||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.application.logo import get_logo
|
from sanic.application.logo import get_logo
|
||||||
from sanic.application.motd import MOTD
|
from sanic.application.motd import MOTD
|
||||||
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
|
||||||
@@ -558,7 +559,6 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
def motd(
|
def motd(
|
||||||
self,
|
self,
|
||||||
serve_location: str = "",
|
|
||||||
server_settings: Optional[Dict[str, Any]] = None,
|
server_settings: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
if (
|
if (
|
||||||
@@ -568,14 +568,7 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
or os.environ.get("SANIC_SERVER_RUNNING")
|
or os.environ.get("SANIC_SERVER_RUNNING")
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if serve_location:
|
serve_location = self.get_server_location(server_settings)
|
||||||
deprecation(
|
|
||||||
"Specifying a serve_location in the MOTD is deprecated and "
|
|
||||||
"will be removed.",
|
|
||||||
22.9,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
serve_location = self.get_server_location(server_settings)
|
|
||||||
if self.config.MOTD:
|
if self.config.MOTD:
|
||||||
logo = get_logo(coffee=self.state.coffee)
|
logo = get_logo(coffee=self.state.coffee)
|
||||||
display, extra = self.get_motd_data(server_settings)
|
display, extra = self.get_motd_data(server_settings)
|
||||||
@@ -746,9 +739,12 @@ class StartupMixin(metaclass=SanicMeta):
|
|||||||
|
|
||||||
socks = []
|
socks = []
|
||||||
sync_manager = Manager()
|
sync_manager = Manager()
|
||||||
|
setup_ext(primary)
|
||||||
try:
|
try:
|
||||||
main_start = primary_server_info.settings.pop("main_start", None)
|
primary_server_info.settings.pop("main_start", None)
|
||||||
main_stop = primary_server_info.settings.pop("main_stop", None)
|
primary_server_info.settings.pop("main_stop", None)
|
||||||
|
main_start = primary.listeners.get("main_process_start")
|
||||||
|
main_stop = primary.listeners.get("main_process_stop")
|
||||||
app = primary_server_info.settings.pop("app")
|
app = primary_server_info.settings.pop("app")
|
||||||
app.setup_loop()
|
app.setup_loop()
|
||||||
loop = new_event_loop()
|
loop = new_event_loop()
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ from sanic.headers import (
|
|||||||
parse_xforwarded,
|
parse_xforwarded,
|
||||||
)
|
)
|
||||||
from sanic.http import Stage
|
from sanic.http import Stage
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import deprecation, error_logger, logger
|
||||||
from sanic.models.protocol_types import TransportProtocol
|
from sanic.models.protocol_types import TransportProtocol
|
||||||
from sanic.response import BaseHTTPResponse, HTTPResponse
|
from sanic.response import BaseHTTPResponse, HTTPResponse
|
||||||
|
|
||||||
@@ -103,6 +103,7 @@ class Request:
|
|||||||
"_port",
|
"_port",
|
||||||
"_protocol",
|
"_protocol",
|
||||||
"_remote_addr",
|
"_remote_addr",
|
||||||
|
"_request_middleware_started",
|
||||||
"_scheme",
|
"_scheme",
|
||||||
"_socket",
|
"_socket",
|
||||||
"_stream_id",
|
"_stream_id",
|
||||||
@@ -126,7 +127,6 @@ class Request:
|
|||||||
"parsed_token",
|
"parsed_token",
|
||||||
"raw_url",
|
"raw_url",
|
||||||
"responded",
|
"responded",
|
||||||
"request_middleware_started",
|
|
||||||
"route",
|
"route",
|
||||||
"stream",
|
"stream",
|
||||||
"transport",
|
"transport",
|
||||||
@@ -178,7 +178,7 @@ class Request:
|
|||||||
self.parsed_not_grouped_args: DefaultDict[
|
self.parsed_not_grouped_args: DefaultDict[
|
||||||
Tuple[bool, bool, str, str], List[Tuple[str, str]]
|
Tuple[bool, bool, str, str], List[Tuple[str, str]]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
self.request_middleware_started = False
|
self._request_middleware_started = False
|
||||||
self.responded: bool = False
|
self.responded: bool = False
|
||||||
self.route: Optional[Route] = None
|
self.route: Optional[Route] = None
|
||||||
self.stream: Optional[Stream] = None
|
self.stream: Optional[Stream] = None
|
||||||
@@ -219,6 +219,16 @@ class Request:
|
|||||||
def generate_id(*_):
|
def generate_id(*_):
|
||||||
return uuid.uuid4()
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def request_middleware_started(self):
|
||||||
|
deprecation(
|
||||||
|
"Request.request_middleware_started has been deprecated and will"
|
||||||
|
"be removed. You should set a flag on the request context using"
|
||||||
|
"either middleware or signals if you need this feature.",
|
||||||
|
23.3,
|
||||||
|
)
|
||||||
|
return self._request_middleware_started
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stream_id(self):
|
def stream_id(self):
|
||||||
"""
|
"""
|
||||||
@@ -324,9 +334,13 @@ class Request:
|
|||||||
response = await response # type: ignore
|
response = await response # type: ignore
|
||||||
# Run response middleware
|
# Run response middleware
|
||||||
try:
|
try:
|
||||||
response = await self.app._run_response_middleware(
|
middleware = (
|
||||||
self, response, request_name=self.name
|
self.route and self.route.extra.response_middleware
|
||||||
)
|
) or self.app.response_middleware
|
||||||
|
if middleware:
|
||||||
|
response = await self.app._run_response_middleware(
|
||||||
|
self, response, middleware
|
||||||
|
)
|
||||||
except CancelledErrors:
|
except CancelledErrors:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -133,14 +133,14 @@ class Router(BaseRouter):
|
|||||||
params.update({"requirements": {"host": host}})
|
params.update({"requirements": {"host": host}})
|
||||||
|
|
||||||
route = super().add(**params) # type: ignore
|
route = super().add(**params) # type: ignore
|
||||||
route.ctx.ignore_body = ignore_body
|
route.extra.ignore_body = ignore_body
|
||||||
route.ctx.stream = stream
|
route.extra.stream = stream
|
||||||
route.ctx.hosts = hosts
|
route.extra.hosts = hosts
|
||||||
route.ctx.static = static
|
route.extra.static = static
|
||||||
route.ctx.error_format = error_format
|
route.extra.error_format = error_format
|
||||||
|
|
||||||
if error_format:
|
if error_format:
|
||||||
check_error_format(route.ctx.error_format)
|
check_error_format(route.extra.error_format)
|
||||||
|
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from signal import signal as signal_func
|
|||||||
from sanic.application.ext import setup_ext
|
from sanic.application.ext import setup_ext
|
||||||
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
|
||||||
from sanic.http.http3 import SessionTicketStore, get_config
|
from sanic.http.http3 import SessionTicketStore, get_config
|
||||||
from sanic.log import error_logger, logger
|
from sanic.log import error_logger, server_logger
|
||||||
from sanic.models.server_types import Signal
|
from sanic.models.server_types import Signal
|
||||||
from sanic.server.async_server import AsyncioServer
|
from sanic.server.async_server import AsyncioServer
|
||||||
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
from sanic.server.protocols.http_protocol import Http3Protocol, HttpProtocol
|
||||||
@@ -149,12 +149,12 @@ def _setup_system_signals(
|
|||||||
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
def _run_server_forever(loop, before_stop, after_stop, cleanup, unix):
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
logger.info("Starting worker [%s]", pid)
|
server_logger.info("Starting worker [%s]", pid)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
logger.info("Stopping worker [%s]", pid)
|
server_logger.info("Stopping worker [%s]", pid)
|
||||||
|
|
||||||
loop.run_until_complete(before_stop())
|
loop.run_until_complete(before_stop())
|
||||||
|
|
||||||
@@ -372,7 +372,9 @@ def serve_multiple(server_settings, workers):
|
|||||||
processes = []
|
processes = []
|
||||||
|
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
server_logger.info(
|
||||||
|
"Received signal %s. Shutting down.", Signals(signal).name
|
||||||
|
)
|
||||||
for process in processes:
|
for process in processes:
|
||||||
os.kill(process.pid, SIGTERM)
|
os.kill(process.pid, SIGTERM)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
from websockets.connection import CLOSED, CLOSING, OPEN, Event
|
||||||
from websockets.exceptions import ConnectionClosed, ConnectionClosedError
|
from websockets.exceptions import (
|
||||||
|
ConnectionClosed,
|
||||||
|
ConnectionClosedError,
|
||||||
|
ConnectionClosedOK,
|
||||||
|
)
|
||||||
from websockets.frames import Frame, Opcode
|
from websockets.frames import Frame, Opcode
|
||||||
from websockets.server import ServerConnection
|
from websockets.server import ServerConnection
|
||||||
from websockets.typing import Data
|
from websockets.typing import Data
|
||||||
@@ -840,3 +844,10 @@ class WebsocketImplProtocol:
|
|||||||
self.abort_pings()
|
self.abort_pings()
|
||||||
if self.connection_lost_waiter:
|
if self.connection_lost_waiter:
|
||||||
self.connection_lost_waiter.set_result(None)
|
self.connection_lost_waiter.set_result(None)
|
||||||
|
|
||||||
|
async def __aiter__(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
yield await self.recv()
|
||||||
|
except ConnectionClosedOK:
|
||||||
|
return
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class Inspector:
|
|||||||
|
|
||||||
def state_to_json(self):
|
def state_to_json(self):
|
||||||
output = {"info": self.app_info}
|
output = {"info": self.app_info}
|
||||||
output["workers"] = self._make_safe(dict(self.worker_state))
|
output["workers"] = self.make_safe(dict(self.worker_state))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
@@ -84,10 +84,11 @@ class Inspector:
|
|||||||
message = "__TERMINATE__"
|
message = "__TERMINATE__"
|
||||||
self._publisher.send(message)
|
self._publisher.send(message)
|
||||||
|
|
||||||
def _make_safe(self, obj: Dict[str, Any]) -> Dict[str, Any]:
|
@staticmethod
|
||||||
|
def make_safe(obj: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for key, value in obj.items():
|
for key, value in obj.items():
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
obj[key] = self._make_safe(value)
|
obj[key] = Inspector.make_safe(value)
|
||||||
elif isinstance(value, datetime):
|
elif isinstance(value, datetime):
|
||||||
obj[key] = value.isoformat()
|
obj[key] = value.isoformat()
|
||||||
return obj
|
return obj
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -84,7 +84,7 @@ ujson = "ujson>=1.35" + env_dependency
|
|||||||
uvloop = "uvloop>=0.5.3" + env_dependency
|
uvloop = "uvloop>=0.5.3" + env_dependency
|
||||||
types_ujson = "types-ujson" + env_dependency
|
types_ujson = "types-ujson" + env_dependency
|
||||||
requirements = [
|
requirements = [
|
||||||
"sanic-routing>=22.3.0,<22.6.0",
|
"sanic-routing>=22.8.0",
|
||||||
"httptools>=0.0.10",
|
"httptools>=0.0.10",
|
||||||
uvloop,
|
uvloop,
|
||||||
ujson,
|
ujson,
|
||||||
@@ -94,7 +94,7 @@ requirements = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
"sanic-testing>=22.9.0b1",
|
"sanic-testing>=22.9.0b2",
|
||||||
"pytest",
|
"pytest",
|
||||||
"coverage",
|
"coverage",
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from sanic.exceptions import SanicException
|
|||||||
from sanic.helpers import _default
|
from sanic.helpers import _default
|
||||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.router import Route
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
@@ -152,8 +153,13 @@ def test_app_route_raise_value_error(app: Sanic):
|
|||||||
|
|
||||||
|
|
||||||
def test_app_handle_request_handler_is_none(app: Sanic, monkeypatch):
|
def test_app_handle_request_handler_is_none(app: Sanic, monkeypatch):
|
||||||
|
app.config.TOUCHUP = False
|
||||||
|
route = Mock(spec=Route)
|
||||||
|
route.extra.request_middleware = []
|
||||||
|
route.extra.response_middleware = []
|
||||||
|
|
||||||
def mockreturn(*args, **kwargs):
|
def mockreturn(*args, **kwargs):
|
||||||
return Mock(), None, {}
|
return route, None, {}
|
||||||
|
|
||||||
monkeypatch.setattr(app.router, "get", mockreturn)
|
monkeypatch.setattr(app.router, "get", mockreturn)
|
||||||
|
|
||||||
@@ -519,7 +525,7 @@ def test_multiple_uvloop_configs_display_warning(caplog):
|
|||||||
|
|
||||||
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
counter = Counter([(r[1], r[2]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert counter[(logging.WARNING, message)] == 2
|
assert counter[(logging.WARNING, message)] == 3
|
||||||
|
|
||||||
|
|
||||||
def test_cannot_run_fast_and_workers(app: Sanic):
|
def test_cannot_run_fast_and_workers(app: Sanic):
|
||||||
|
|||||||
@@ -125,14 +125,9 @@ def test_env_w_custom_converter():
|
|||||||
|
|
||||||
|
|
||||||
def test_env_lowercase():
|
def test_env_lowercase():
|
||||||
with pytest.warns(None) as record:
|
environ["SANIC_test_answer"] = "42"
|
||||||
environ["SANIC_test_answer"] = "42"
|
app = Sanic(name="Test")
|
||||||
app = Sanic(name="Test")
|
assert "test_answer" not in app.config
|
||||||
assert app.config.test_answer == 42
|
|
||||||
assert str(record[0].message) == (
|
|
||||||
"[DEPRECATION v22.9] Lowercase environment variables will not be "
|
|
||||||
"loaded into Sanic config beginning in v22.9."
|
|
||||||
)
|
|
||||||
del environ["SANIC_test_answer"]
|
del environ["SANIC_test_answer"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,15 +97,15 @@ def test_auto_fallback_with_content_type(app):
|
|||||||
def test_route_error_format_set_on_auto(app):
|
def test_route_error_format_set_on_auto(app):
|
||||||
@app.get("/text")
|
@app.get("/text")
|
||||||
def text_response(request):
|
def text_response(request):
|
||||||
return text(request.route.ctx.error_format)
|
return text(request.route.extra.error_format)
|
||||||
|
|
||||||
@app.get("/json")
|
@app.get("/json")
|
||||||
def json_response(request):
|
def json_response(request):
|
||||||
return json({"format": request.route.ctx.error_format})
|
return json({"format": request.route.extra.error_format})
|
||||||
|
|
||||||
@app.get("/html")
|
@app.get("/html")
|
||||||
def html_response(request):
|
def html_response(request):
|
||||||
return html(request.route.ctx.error_format)
|
return html(request.route.extra.error_format)
|
||||||
|
|
||||||
_, response = app.test_client.get("/text")
|
_, response = app.test_client.get("/text")
|
||||||
assert response.text == "text"
|
assert response.text == "text"
|
||||||
|
|||||||
90
tests/test_middleware_priority.py
Normal file
90
tests/test_middleware_priority.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.middleware import Middleware
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
|
||||||
|
PRIORITY_TEST_CASES = (
|
||||||
|
([0, 1, 2], [1, 1, 1]),
|
||||||
|
([0, 1, 2], [1, 1, None]),
|
||||||
|
([0, 1, 2], [1, None, None]),
|
||||||
|
([0, 1, 2], [2, 1, None]),
|
||||||
|
([0, 1, 2], [2, 2, None]),
|
||||||
|
([0, 1, 2], [3, 2, 1]),
|
||||||
|
([0, 1, 2], [None, None, None]),
|
||||||
|
([0, 2, 1], [1, None, 1]),
|
||||||
|
([0, 2, 1], [2, None, 1]),
|
||||||
|
([0, 2, 1], [2, None, 2]),
|
||||||
|
([0, 2, 1], [3, 1, 2]),
|
||||||
|
([1, 0, 2], [1, 2, None]),
|
||||||
|
([1, 0, 2], [2, 3, 1]),
|
||||||
|
([1, 0, 2], [None, 1, None]),
|
||||||
|
([1, 2, 0], [1, 3, 2]),
|
||||||
|
([1, 2, 0], [None, 1, 1]),
|
||||||
|
([1, 2, 0], [None, 2, 1]),
|
||||||
|
([1, 2, 0], [None, 2, 2]),
|
||||||
|
([2, 0, 1], [1, None, 2]),
|
||||||
|
([2, 0, 1], [2, 1, 3]),
|
||||||
|
([2, 0, 1], [None, None, 1]),
|
||||||
|
([2, 1, 0], [1, 2, 3]),
|
||||||
|
([2, 1, 0], [None, 1, 2]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def reset_middleware():
|
||||||
|
yield
|
||||||
|
Middleware.reset_count()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"expected,priorities",
|
||||||
|
PRIORITY_TEST_CASES,
|
||||||
|
)
|
||||||
|
def test_request_middleware_order_priority(app: Sanic, expected, priorities):
|
||||||
|
order = []
|
||||||
|
|
||||||
|
def add_ident(request, ident):
|
||||||
|
order.append(ident)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return json(None)
|
||||||
|
|
||||||
|
for ident, priority in enumerate(priorities):
|
||||||
|
kwargs = {}
|
||||||
|
if priority is not None:
|
||||||
|
kwargs["priority"] = priority
|
||||||
|
app.on_request(partial(add_ident, ident=ident), **kwargs)
|
||||||
|
|
||||||
|
app.test_client.get("/")
|
||||||
|
|
||||||
|
assert order == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"expected,priorities",
|
||||||
|
PRIORITY_TEST_CASES,
|
||||||
|
)
|
||||||
|
def test_response_middleware_order_priority(app: Sanic, expected, priorities):
|
||||||
|
order = []
|
||||||
|
|
||||||
|
def add_ident(request, response, ident):
|
||||||
|
order.append(ident)
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def handler(request):
|
||||||
|
return json(None)
|
||||||
|
|
||||||
|
for ident, priority in enumerate(priorities):
|
||||||
|
kwargs = {}
|
||||||
|
if priority is not None:
|
||||||
|
kwargs["priority"] = priority
|
||||||
|
app.on_response(partial(add_ident, ident=ident), **kwargs)
|
||||||
|
|
||||||
|
app.test_client.get("/")
|
||||||
|
|
||||||
|
assert order[::-1] == expected
|
||||||
@@ -4,8 +4,8 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate, parsedate_to_datetime
|
||||||
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 pathlib import Path
|
||||||
@@ -665,13 +665,11 @@ def test_multiple_responses(
|
|||||||
|
|
||||||
with caplog.at_level(ERROR):
|
with caplog.at_level(ERROR):
|
||||||
_, response = app.test_client.get("/4")
|
_, response = app.test_client.get("/4")
|
||||||
print(response.json)
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert "foo" not in response.text
|
assert "foo" not in response.text
|
||||||
assert "one" in response.headers
|
assert "one" in response.headers
|
||||||
assert response.headers["one"] == "one"
|
assert response.headers["one"] == "one"
|
||||||
|
|
||||||
print(response.headers)
|
|
||||||
assert message_in_records(caplog.records, error_msg2)
|
assert message_in_records(caplog.records, error_msg2)
|
||||||
|
|
||||||
with caplog.at_level(ERROR):
|
with caplog.at_level(ERROR):
|
||||||
@@ -841,10 +839,10 @@ def test_file_validate(app: Sanic, static_file_directory: str):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
with open(file_path, "a") as f:
|
with open(file_path, "a") as f:
|
||||||
f.write("bar\n")
|
f.write("bar\n")
|
||||||
|
|
||||||
_, response = app.test_client.get(
|
_, response = app.test_client.get(
|
||||||
"/validate", headers={"If-Modified-Since": last_modified}
|
"/validate", headers={"If-Modified-Since": last_modified}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.body == b"foo\nbar\n"
|
assert response.body == b"foo\nbar\n"
|
||||||
|
|
||||||
@@ -921,3 +919,28 @@ def test_file_validating_304_response(
|
|||||||
)
|
)
|
||||||
assert response.status == 304
|
assert response.status == 304
|
||||||
assert response.body == b""
|
assert response.body == b""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"file_name", ["test.file", "decode me.txt", "python.png"]
|
||||||
|
)
|
||||||
|
def test_file_validating_304_response(
|
||||||
|
app: Sanic, file_name: str, static_file_directory: str
|
||||||
|
):
|
||||||
|
app.static("static", Path(static_file_directory) / file_name)
|
||||||
|
|
||||||
|
_, response = app.test_client.get("/static")
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.body == get_file_content(static_file_directory, file_name)
|
||||||
|
last_modified = parsedate_to_datetime(response.headers["Last-Modified"])
|
||||||
|
last_modified += timedelta(seconds=1)
|
||||||
|
_, response = app.test_client.get(
|
||||||
|
"/static",
|
||||||
|
headers={
|
||||||
|
"if-modified-since": formatdate(
|
||||||
|
last_modified.timestamp(), usegmt=True
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status == 304
|
||||||
|
assert response.body == b""
|
||||||
|
|||||||
@@ -503,9 +503,10 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
|
|||||||
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert counter[("sanic.root", logging.INFO)] == 11
|
assert counter[("sanic.root", logging.INFO)] == 9
|
||||||
assert counter[("sanic.root", logging.ERROR)] == 0
|
assert counter[("sanic.root", logging.ERROR)] == 0
|
||||||
assert counter[("sanic.error", logging.ERROR)] == 0
|
assert counter[("sanic.error", logging.ERROR)] == 0
|
||||||
|
assert counter[("sanic.server", logging.INFO)] == 2
|
||||||
|
|
||||||
|
|
||||||
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
||||||
@@ -521,9 +522,10 @@ def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
|
|||||||
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
|
||||||
|
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
assert counter[("sanic.root", logging.INFO)] == 11
|
assert counter[("sanic.root", logging.INFO)] == 9
|
||||||
assert counter[("sanic.root", logging.ERROR)] == 0
|
assert counter[("sanic.root", logging.ERROR)] == 0
|
||||||
assert counter[("sanic.error", logging.ERROR)] == 0
|
assert counter[("sanic.error", logging.ERROR)] == 0
|
||||||
|
assert counter[("sanic.server", logging.INFO)] == 2
|
||||||
assert response.text == "No file: /static/non_existing_file.file"
|
assert response.text == "No file: /static/non_existing_file.file"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
56
tests/test_ws_handlers.py
Normal file
56
tests/test_ws_handlers.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from typing import Any, Callable, Coroutine
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from websockets.client import WebSocketClientProtocol
|
||||||
|
|
||||||
|
from sanic import Request, Sanic, Websocket
|
||||||
|
|
||||||
|
|
||||||
|
MimicClientType = Callable[
|
||||||
|
[WebSocketClientProtocol], Coroutine[None, None, Any]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def simple_ws_mimic_client():
|
||||||
|
async def client_mimic(ws: WebSocketClientProtocol):
|
||||||
|
await ws.send("test 1")
|
||||||
|
await ws.recv()
|
||||||
|
await ws.send("test 2")
|
||||||
|
await ws.recv()
|
||||||
|
|
||||||
|
return client_mimic
|
||||||
|
|
||||||
|
|
||||||
|
def test_ws_handler(
|
||||||
|
app: Sanic,
|
||||||
|
simple_ws_mimic_client: MimicClientType,
|
||||||
|
):
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_echo_handler(request: Request, ws: Websocket):
|
||||||
|
while True:
|
||||||
|
msg = await ws.recv()
|
||||||
|
await ws.send(msg)
|
||||||
|
|
||||||
|
_, ws_proxy = app.test_client.websocket(
|
||||||
|
"/ws", mimic=simple_ws_mimic_client
|
||||||
|
)
|
||||||
|
assert ws_proxy.client_sent == ["test 1", "test 2", ""]
|
||||||
|
assert ws_proxy.client_received == ["test 1", "test 2"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ws_handler_async_for(
|
||||||
|
app: Sanic,
|
||||||
|
simple_ws_mimic_client: MimicClientType,
|
||||||
|
):
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def ws_echo_handler(request: Request, ws: Websocket):
|
||||||
|
async for msg in ws:
|
||||||
|
await ws.send(msg)
|
||||||
|
|
||||||
|
_, ws_proxy = app.test_client.websocket(
|
||||||
|
"/ws", mimic=simple_ws_mimic_client
|
||||||
|
)
|
||||||
|
assert ws_proxy.client_sent == ["test 1", "test 2", ""]
|
||||||
|
assert ws_proxy.client_received == ["test 1", "test 2"]
|
||||||
Reference in New Issue
Block a user