Compare commits

..

10 Commits

Author SHA1 Message Date
Adam Hopkins
0b750593da Version Bump 2022-07-31 13:03:51 +03:00
Adam Hopkins
5b1686ceba Use path.parts instead of match (#2508) 2022-07-31 12:58:21 +03:00
Adam Hopkins
86baaef1ec Use pathlib for path resolution (#2506) 2022-07-31 12:58:18 +03:00
Adam Hopkins
2b4b78da88 Fix dotted test 2022-07-31 12:56:57 +03:00
Néstor Pérez
ee6d8cfe11 Prevent directory traversion with static files (#2495)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
Co-authored-by: Zhiwei Liang <zhi.wei.liang@outlook.com>
2022-07-31 12:56:52 +03:00
Adam Hopkins
c4da66bf1f Update changelog 2022-01-06 12:26:35 +02:00
Adam Hopkins
d50d3b8448 Bump version 2022-01-06 12:21:44 +02:00
Adam Hopkins
313f97ac77 Only display MOTD in ASGI on startup (#2349) 2022-01-06 11:22:57 +02:00
Adam Hopkins
a23547d73b Ignore name argument on Python 3.7 (#2355)
Co-authored-by: Néstor Pérez <25409753+prryplatypus@users.noreply.github.com>
Co-authored-by: Ryu juheon <saidbysolo@gmail.com>
2022-01-06 10:57:24 +02:00
Adam Hopkins
34d1dee407 Add config.update support for setters (#2354) 2022-01-06 09:55:03 +02:00
58 changed files with 1445 additions and 2093 deletions

28
.codeclimate.yml Normal file
View File

@@ -0,0 +1,28 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"
checks:
argument-count:
enabled: false
file-lines:
config:
threshold: 1000
method-count:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,12 +3,13 @@ branch = True
source = sanic
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
[html]
directory = coverage
@@ -20,12 +21,3 @@ exclude_lines =
noqa
NOQA
pragma: no cover
omit =
site-packages
sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
skip_empty = True

View File

@@ -7,11 +7,10 @@ on:
tags:
- "!*" # Do not execute on tags
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -21,6 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
@@ -29,10 +29,9 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run coverage
run: tox -e coverage
continue-on-error: true
- uses: codecov/codecov-action@v2
- uses: paambaati/codeclimate-action@v2.5.3
if: always()
env:
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with:
files: ./coverage.xml
fail_ci_if_error: false
coverageCommand: tox -e coverage

View File

@@ -66,7 +66,7 @@ Sanic | Build fast. Run fast.
Sanic is a **Python 3.7+** web server and web framework that's written to go fast. It allows the usage of the ``async/await`` syntax added in Python 3.5, which makes your code non-blocking and speedy.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanicframework.org/en/guide/deployment/running.html#asgi>`_.
Sanic is also ASGI compliant, so you can deploy it with an `alternative ASGI webserver <https://sanic.readthedocs.io/en/latest/sanic/deploying.html#running-via-asgi>`_.
`Source code on GitHub <https://github.com/sanic-org/sanic/>`_ | `Help and discussion board <https://community.sanicframework.org/>`_ | `User Guide <https://sanicframework.org>`_ | `Chat on Discord <https://discord.gg/FARQzAEMAA>`_

View File

@@ -4,6 +4,7 @@ coverage:
default:
target: auto
threshold: 0.75
informational: true
project:
default:
target: auto

View File

@@ -6,5 +6,5 @@ data = ""
for i in range(1, 250000):
data += str(i)
r = requests.post("http://0.0.0.0:8000/stream", data=data)
r = requests.post('http://0.0.0.0:8000/stream', data=data)
print(r.text)

View File

@@ -1,3 +1,3 @@
[build-system]
requires = ["setuptools<60.0", "wheel"]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

View File

@@ -6,4 +6,4 @@ python:
path: .
extra_requirements:
- docs
system_packages: true
system_packages: true

View File

@@ -1 +1 @@
__version__ = "22.3.0.dev1"
__version__ = "21.12.2"

View File

@@ -3,24 +3,28 @@ from __future__ import annotations
import asyncio
import logging
import logging.config
import os
import platform
import re
import sys
from asyncio import (
AbstractEventLoop,
CancelledError,
Protocol,
Task,
ensure_future,
get_event_loop,
get_running_loop,
wait_for,
)
from asyncio.futures import Future
from collections import defaultdict, deque
from contextlib import suppress
from functools import partial
from importlib import import_module
from inspect import isawaitable
from pathlib import Path
from socket import socket
from ssl import SSLContext
from traceback import format_exc
from types import SimpleNamespace
from typing import (
@@ -50,8 +54,11 @@ from sanic_routing.exceptions import ( # type: ignore
)
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
from sanic.application.ext import setup_ext
from sanic.application.state import ApplicationState, Mode, ServerStage
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationState, Mode
from sanic.asgi import ASGIApp
from sanic.base.root import BaseSanic
from sanic.blueprint_group import BlueprintGroup
@@ -65,15 +72,16 @@ from sanic.exceptions import (
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.helpers import _default
from sanic.http import Stage
from sanic.log import (
LOGGING_CONFIG_DEFAULTS,
Colors,
deprecation,
error_logger,
logger,
)
from sanic.mixins.listeners import ListenerEvent
from sanic.mixins.runner import RunnerMixin
from sanic.models.futures import (
FutureException,
FutureListener,
@@ -88,8 +96,13 @@ from sanic.models.handler_types import Sanic as SanicVar
from sanic.request import Request
from sanic.response import BaseHTTPResponse, HTTPResponse, ResponseStream
from sanic.router import Router
from sanic.server import AsyncioServer, HttpProtocol
from sanic.server import Signal as ServerSignal
from sanic.server import serve, serve_multiple, serve_single, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.websockets.impl import ConnectionClosed
from sanic.signals import Signal, SignalRouter
from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta
@@ -101,13 +114,15 @@ if TYPE_CHECKING: # no cov
Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS: # no cov
if OS_IS_WINDOWS:
enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning)
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
class Sanic(BaseSanic, metaclass=TouchUpMeta):
"""
The main application instance
"""
@@ -206,6 +221,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
self.blueprints: Dict[str, Blueprint] = {}
self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace()
self.debug = False
self.error_handler: ErrorHandler = error_handler or ErrorHandler()
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
@@ -249,7 +265,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
Only supported when using the `app.run` method.
"""
if self.state.stage is ServerStage.STOPPED and self.asgi is False:
if not self.is_running and self.asgi is False:
raise SanicException(
"Loop can only be retrieved after the app has started "
"running. Not supported with `create_server` function"
@@ -1036,6 +1052,283 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
# Execution
# -------------------------------------------------------------------- #
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
self.run(*args, **kwargs)
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: Nothing
"""
self.state.verbosity = verbosity
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
for directory in reload_dir:
direc = Path(directory)
if not direc.is_dir():
logger.warning(
f"Directory {directory} could not be located"
)
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
if auto_reload or auto_reload is None and debug:
auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0, self)
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
if fast:
self.state.fast = True
try:
workers = len(os.sched_getaffinity(0))
except AttributeError:
workers = os.cpu_count() or 1
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
)
if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()
try:
self.is_running = True
self.is_stopping = False
if workers > 1 and os.name != "posix":
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
workers = 1
if workers == 1:
serve_single(server_settings)
else:
serve_multiple(server_settings, workers)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
self.is_running = False
logger.info("Server Stopped")
def stop(self):
"""
This kills the Sanic
"""
if not self.is_stopping:
self.shutdown_tasks(timeout=0)
self.is_stopping = True
get_event_loop().stop()
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server: bool = False,
asyncio_server_kwargs: Dict[str, Any] = None,
noisy_exceptions: Optional[bool] = None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
)
if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
logger.warning(
"Listener events for the main process are not available "
"with create_server()"
)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
async def _run_request_middleware(
self, request, request_name=None
): # no cov
@@ -1119,6 +1412,100 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
break
return response
def _helper(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
workers: int = 1,
loop: AbstractEventLoop = None,
protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
):
"""Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
ssl = process_to_context(ssl)
self.debug = debug
self.state.host = host
self.state.port = port
self.state.workers = workers
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
"host": host,
"port": port,
"sock": sock,
"unix": unix,
"ssl": ssl,
"app": self,
"signal": ServerSignal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
}
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
"Consider using '--debug' or '--dev' while actively "
f"developing your application.{Colors.END}"
)
# Register start/stop events
for event_name, settings_name, reverse in (
("main_process_start", "main_start", False),
("main_process_stop", "main_stop", True),
):
listeners = self.listeners[event_name].copy()
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
return server_settings
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
def _build_endpoint_name(self, *parts):
parts = [self.name, *parts]
return ".".join(parts)
@@ -1167,14 +1554,13 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
) -> Task:
if not isinstance(task, Future):
prepped = cls._prep_task(task, app, loop)
if sys.version_info < (3, 8): # no cov
task = loop.create_task(prepped)
if sys.version_info < (3, 8):
if name:
error_logger.warning(
"Cannot set a name for a task when using Python 3.7. "
"Your task will be created without a name."
)
task.get_name = lambda: name
task = loop.create_task(prepped)
else:
task = loop.create_task(prepped, name=name)
@@ -1212,6 +1598,12 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
:param task: future, couroutine or awaitable
"""
if name and sys.version_info == (3, 7):
name = None
error_logger.warning(
"Cannot set a name for a task when using Python 3.7. Your "
"task will be created without a name."
)
try:
loop = self.loop # Will raise SanicError if loop is not started
return self._loop_add_task(
@@ -1234,6 +1626,12 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
def get_task(
self, name: str, *, raise_exception: bool = True
) -> Optional[Task]:
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (get_task) is only supported on using "
"Python 3.8+."
)
return
try:
return self._task_registry[name]
except KeyError:
@@ -1250,13 +1648,19 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
*,
raise_exception: bool = True,
) -> None:
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (cancel_task) is only supported on using "
"Python 3.8+."
)
return
task = self.get_task(name, raise_exception=raise_exception)
if task and not task.cancelled():
args: Tuple[str, ...] = ()
if msg:
if sys.version_info >= (3, 9):
args = (msg,)
else: # no cov
else:
raise RuntimeError(
"Cancelling a task with a message is only supported "
"on Python 3.9+."
@@ -1268,6 +1672,12 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
...
def purge_tasks(self):
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (purge_tasks) is only supported on using "
"Python 3.8+."
)
return
for task in self.tasks:
if task.done() or task.cancelled():
name = task.get_name()
@@ -1280,22 +1690,31 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
def shutdown_tasks(
self, timeout: Optional[float] = None, increment: float = 0.1
):
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (shutdown_tasks) is only supported on using "
"Python 3.8+."
)
return
for task in self.tasks:
if task.get_name() != "RunServer":
task.cancel()
task.cancel()
if timeout is None:
timeout = self.config.GRACEFUL_SHUTDOWN_TIMEOUT
while len(self._task_registry) and timeout:
with suppress(RuntimeError):
running_loop = get_running_loop()
running_loop.run_until_complete(asyncio.sleep(increment))
self.loop.run_until_complete(asyncio.sleep(increment))
self.purge_tasks()
timeout -= increment
@property
def tasks(self):
if sys.version_info < (3, 8):
error_logger.warning(
"This feature (tasks) is only supported on using "
"Python 3.8+."
)
return
return iter(self._task_registry.values())
# -------------------------------------------------------------------- #
@@ -1345,13 +1764,6 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
@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
@@ -1369,60 +1781,80 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
@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
def reload_dirs(self):
return self.state.reload_dirs
def motd(self, serve_location):
if self.config.MOTD:
mode = [f"{self.state.mode},"]
if self.state.fast:
mode.append("goin' fast")
if self.state.asgi:
mode.append("ASGI")
else:
if self.state.workers == 1:
mode.append("single worker")
else:
mode.append(f"w/ {self.state.workers} workers")
display = {
"mode": " ".join(mode),
"server": self.state.server,
"python": platform.python_version(),
"platform": platform.platform(),
}
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"
if self.state.reload_dirs:
reload_display += ", ".join(
[
"",
*(
str(path.absolute())
for path in self.state.reload_dirs
),
]
)
display["auto-reload"] = reload_display
packages = []
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
logo = (
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
@property
def ext(self) -> Extend:
if not hasattr(self, "_ext"):
@@ -1520,6 +1952,7 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
async def _startup(self):
self._future_registry.clear()
# Startup Sanic Extensions
if not hasattr(self, "_ext"):
setup_ext(self)
if hasattr(self, "_ext"):
@@ -1542,11 +1975,8 @@ class Sanic(BaseSanic, RunnerMixin, metaclass=TouchUpMeta):
self.__class__._uvloop_setting = self.config.USE_UVLOOP
# Startup time optimizations
if self.state.primary:
# TODO:
# - Raise warning if secondary apps have error handler config
ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self)
ErrorHandler.finalize(self.error_handler, config=self.config)
TouchUp.run(self)
self.state.is_started = True

View File

@@ -3,17 +3,16 @@ from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, IntEnum, auto
from enum import Enum, auto
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger
from sanic.server.async_server import AsyncioServer
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic import Sanic
@@ -33,19 +32,6 @@ class Mode(StrEnum):
DEBUG = auto()
class ServerStage(IntEnum):
STOPPED = auto()
PARTIAL = auto()
SERVING = auto()
@dataclass
class ApplicationServerInfo:
settings: Dict[str, Any]
stage: ServerStage = field(default=ServerStage.STOPPED)
server: Optional[AsyncioServer] = field(default=None)
@dataclass
class ApplicationState:
app: Sanic
@@ -59,15 +45,12 @@ class ApplicationState:
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set)
auto_reload: bool = field(default=False)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
primary: bool = field(default=True)
server_info: List[ApplicationServerInfo] = field(default_factory=list)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
@@ -94,17 +77,3 @@ class ApplicationState:
@property
def is_debug(self):
return self.mode is Mode.DEBUG
@property
def stage(self) -> ServerStage:
if not self.server_info:
return ServerStage.STOPPED
if all(info.stage is ServerStage.SERVING for info in self.server_info):
return ServerStage.SERVING
elif any(
info.stage is ServerStage.SERVING for info in self.server_info
):
return ServerStage.PARTIAL
return ServerStage.STOPPED

View File

@@ -1,15 +1,14 @@
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Optional
from typing import Optional
from urllib.parse import quote
import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.log import logger
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request
from sanic.response import BaseHTTPResponse
@@ -17,35 +16,30 @@ from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection
if TYPE_CHECKING: # no cov
from sanic import Sanic
class Lifespan:
def __init__(self, asgi_app: ASGIApp) -> None:
def __init__(self, asgi_app: "ASGIApp") -> None:
self.asgi_app = asgi_app
if self.asgi_app.sanic_app.state.verbosity > 0:
if (
"server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'You have set a listener for "before_server_start" '
"in ASGI mode. "
"It will be executed as early as possible, but not before "
"the ASGI server is started."
)
if (
"server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index
):
logger.debug(
'You have set a listener for "after_server_stop" '
"in ASGI mode. "
"It will be executed as late as possible, but not after "
"the ASGI server is stopped."
)
if (
"server.init.before"
in self.asgi_app.sanic_app.signal_router.name_index
):
warnings.warn(
'You have set a listener for "before_server_start" '
"in ASGI mode. "
"It will be executed as early as possible, but not before "
"the ASGI server is started."
)
if (
"server.shutdown.after"
in self.asgi_app.sanic_app.signal_router.name_index
):
warnings.warn(
'You have set a listener for "after_server_stop" '
"in ASGI mode. "
"It will be executed as late as possible, but not after "
"the ASGI server is stopped."
)
async def startup(self) -> None:
"""
@@ -94,7 +88,7 @@ class Lifespan:
class ASGIApp:
sanic_app: Sanic
sanic_app: "sanic.app.Sanic"
request: Request
transport: MockTransport
lifespan: Lifespan

View File

@@ -79,6 +79,13 @@ Or, a path to a directory to run as a simple HTTP server:
error_logger.exception("Failed to run app")
def _precheck(self):
if self.args.debug and self.main_process:
error_logger.warning(
"Starting in v22.3, --debug will no "
"longer automatically run the auto-reloader.\n Switch to "
"--dev to continue using that functionality."
)
# # Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
@@ -167,11 +174,16 @@ Or, a path to a directory to run as a simple HTTP server:
"workers": self.args.workers,
}
for maybe_arg in ("auto_reload", "dev"):
if getattr(self.args, maybe_arg, False):
kwargs[maybe_arg] = True
if self.args.auto_reload:
kwargs["auto_reload"] = True
if self.args.path:
kwargs["auto_reload"] = True
kwargs["reload_dir"] = self.args.path
if self.args.auto_reload or self.args.debug:
kwargs["reload_dir"] = self.args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
return kwargs

View File

@@ -180,10 +180,18 @@ class DevelopmentGroup(Group):
"--debug",
dest="debug",
action="store_true",
help="Run the server in debug mode",
)
self.container.add_argument(
"-d",
"--dev",
dest="debug",
action="store_true",
help=(
"Run the server in DEBUG mode. It includes DEBUG logging,\n"
"additional context on exceptions, and other settings\n"
"not-safe for PRODUCTION, but helpful for debugging problems."
"Currently is an alias for --debug. But starting in v22.3, \n"
"--debug will no longer automatically trigger auto_restart. \n"
"However, --dev will continue, effectively making it the \n"
"same as debug + auto_reload."
),
)
self.container.add_argument(
@@ -204,13 +212,6 @@ class DevelopmentGroup(Group):
action="append",
help="Extra directories to watch and reload on changes",
)
self.container.add_argument(
"-d",
"--dev",
dest="dev",
action="store_true",
help=("debug + auto reload."),
)
class OutputGroup(Group):

View File

@@ -39,7 +39,7 @@ DEFAULT_CONFIG = {
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
}
@@ -226,12 +226,9 @@ class Config(dict, metaclass=DescriptorMeta):
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
"""
lower_case_var_found = False
for key, value in environ.items():
if not key.startswith(prefix):
continue
if not key.isupper():
lower_case_var_found = True
_, config_key = key.split(prefix, 1)
@@ -241,12 +238,6 @@ class Config(dict, metaclass=DescriptorMeta):
break
except ValueError:
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]):
"""

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type, Union
from sanic.config import Config
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
TextRenderer,
HTMLRenderer,
exception_response,
)
from sanic.exceptions import (
@@ -34,11 +35,13 @@ class ErrorHandler:
"""
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__(
self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = TextRenderer,
base: Type[BaseRenderer] = HTMLRenderer,
):
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {}
@@ -50,14 +53,14 @@ class ErrorHandler:
self._warn_fallback_deprecation()
@property
def fallback(self): # no cov
def fallback(self):
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str): # no cov
def fallback(self, value: str):
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
@@ -92,8 +95,8 @@ class ErrorHandler:
def finalize(
cls,
error_handler: ErrorHandler,
config: Config,
fallback: Optional[str] = None,
config: Optional[Config] = None,
):
if fallback:
deprecation(
@@ -104,10 +107,14 @@ class ErrorHandler:
22.6,
)
if not fallback:
fallback = config.FALLBACK_ERROR_FORMAT
if config is None:
deprecation(
"Starting in v22.3, config will be a required argument "
"for ErrorHandler.finalize().",
22.3,
)
if fallback != DEFAULT_FORMAT:
if fallback and fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
@@ -121,9 +128,27 @@ class ErrorHandler:
f"Error handler is non-conforming: {type(error_handler)}"
)
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
deprecation(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
22.3,
)
legacy_lookup = error_handler._legacy_lookup
error_handler._lookup = legacy_lookup # type: ignore
def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name)
def _legacy_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception)
def add(self, exception, handler, route_names: Optional[List[str]] = None):
"""
Add a new exception handler to an already existing handler object.
@@ -137,6 +162,9 @@ class ErrorHandler:
:return: None
"""
# self.handlers is deprecated and will be removed in version 22.3
self.handlers.append((exception, handler))
if route_names:
for route in route_names:
self.cached_handlers[(exception, route)] = handler
@@ -208,7 +236,7 @@ class ErrorHandler:
except Exception:
try:
url = repr(request.url)
except AttributeError: # no cov
except AttributeError:
url = "unknown"
response_message = (
"Exception raised in exception handler " '"%s" for uri: %s'
@@ -253,7 +281,7 @@ class ErrorHandler:
if quiet is False or noisy is True:
try:
url = repr(request.url)
except AttributeError: # no cov
except AttributeError:
url = "unknown"
error_logger.exception(

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import re
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from urllib.parse import unquote
from sanic.exceptions import InvalidHeader
@@ -18,7 +18,7 @@ Options = Dict[str, Union[int, str]] # key=value fields in various headers
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
_ipv6_re = re.compile(_ipv6)
@@ -394,17 +394,3 @@ def parse_accept(accept: str) -> AcceptContainer:
return AcceptContainer(
sorted(accept_list, key=_sort_accept_value, reverse=True)
)
def parse_credentials(
header: Optional[str],
prefixes: Union[List, Tuple, Set] = None,
) -> Tuple[Optional[str], Optional[str]]:
"""Parses any header with the aim to retrieve any credentials from it."""
if not prefixes or not isinstance(prefixes, (list, tuple, set)):
prefixes = ("Basic", "Bearer", "Token")
if header is not None:
for prefix in prefixes:
if prefix in header:
return prefix, header.partition(prefix)[-1].strip()
return None, header

View File

@@ -6,7 +6,7 @@ from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1,
disable_existing_loggers=False,
loggers={
@@ -57,7 +57,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
)
class Colors(str, Enum): # no cov
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
@@ -65,23 +65,23 @@ class Colors(str, Enum): # no cov
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root") # no cov
logger = logging.getLogger("sanic.root")
"""
General Sanic logger
"""
error_logger = logging.getLogger("sanic.error") # no cov
error_logger = logging.getLogger("sanic.error")
"""
Logger used by Sanic for error logging
"""
access_logger = logging.getLogger("sanic.access") # no cov
access_logger = logging.getLogger("sanic.access")
"""
Logger used by Sanic for access logging
"""
def deprecation(message: str, version: float): # no cov
def deprecation(message: str, version: float):
version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"

View File

@@ -4,8 +4,7 @@ from functools import partial, wraps
from inspect import getsource, signature
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from re import sub
from pathlib import Path, PurePath
from textwrap import dedent
from time import gmtime, strftime
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
@@ -17,12 +16,7 @@ from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
from sanic.exceptions import (
ContentRangeError,
FileNotFound,
HeaderNotFound,
InvalidUsage,
)
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound
from sanic.handlers import ContentRangeHandler
from sanic.log import deprecation, error_logger
from sanic.models.futures import FutureRoute, FutureStatic
@@ -775,32 +769,40 @@ class RouteMixin(metaclass=SanicMeta):
content_type=None,
__file_uri__=None,
):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if __file_uri__ and "../" in __file_uri__:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
root_path = file_path = file_or_directory
if __file_uri__:
file_path = path.join(
file_or_directory, sub("^[/]*", "", __file_uri__)
)
file_path_raw = Path(unquote(file_or_directory))
root_path = file_path = file_path_raw.resolve()
not_found = FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
# URL decode the path sent by the browser otherwise we won't be able to
# match filenames which got encoded (filenames with spaces etc)
file_path = path.abspath(unquote(file_path))
if not file_path.startswith(path.abspath(unquote(root_path))):
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
if __file_uri__:
# Strip all / that in the beginning of the URL to help prevent
# python from herping a derp and treating the uri as an
# absolute path
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
file_path_raw = Path(file_or_directory, unquoted_file_uri)
file_path = file_path_raw.resolve()
if (
file_path < root_path and not file_path_raw.is_symlink()
) or ".." in file_path_raw.parts:
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try:
file_path.relative_to(root_path)
except ValueError:
if not file_path_raw.is_symlink():
error_logger.exception(
f"File not found: path={file_or_directory}, "
f"relative_url={__file_uri__}"
)
raise not_found
try:
headers = {}
# Check if the client has been sent this file before
@@ -868,11 +870,7 @@ class RouteMixin(metaclass=SanicMeta):
except ContentRangeError:
raise
except FileNotFoundError:
raise FileNotFound(
"File not found",
path=file_or_directory,
relative_url=__file_uri__,
)
raise not_found
except Exception:
error_logger.exception(
f"Exception in static request handler: "

View File

@@ -1,695 +0,0 @@
from __future__ import annotations
import os
import platform
import sys
from asyncio import (
AbstractEventLoop,
CancelledError,
Protocol,
all_tasks,
get_event_loop,
get_running_loop,
)
from contextlib import suppress
from functools import partial
from importlib import import_module
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Type, Union
from sanic import reloader_helpers
from sanic.application.logo import get_logo
from sanic.application.motd import MOTD
from sanic.application.state import ApplicationServerInfo, Mode, ServerStage
from sanic.base.meta import SanicMeta
from sanic.compat import OS_IS_WINDOWS
from sanic.helpers import _default
from sanic.log import Colors, error_logger, logger
from sanic.models.handler_types import ListenerType
from sanic.server import Signal as ServerSignal
from sanic.server import try_use_uvloop
from sanic.server.async_server import AsyncioServer
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
from sanic.server.runners import serve, serve_multiple, serve_single
from sanic.tls import process_to_context
if TYPE_CHECKING: # no cov
from sanic import Sanic
from sanic.application.state import ApplicationState
from sanic.config import Config
SANIC_PACKAGES = ("sanic-routing", "sanic-testing", "sanic-ext")
class RunnerMixin(metaclass=SanicMeta):
_app_registry: Dict[str, Sanic]
config: Config
listeners: Dict[str, List[ListenerType[Any]]]
state: ApplicationState
websocket_enabled: bool
def make_coffee(self, *args, **kwargs):
self.state.coffee = True
self.run(*args, **kwargs)
def run(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
"""
Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param auto_reload: Reload app whenever its source code is changed.
Enabled by default in debug mode.
:type auto_relaod: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: str, dict, SSLContext or list
:param sock: Socket for the server to accept connections from
:type sock: socket
:param workers: Number of processes received before it is respected
:type workers: int
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param register_sys_signals: Register SIG* events
:type register_sys_signals: bool
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param unix: Unix socket to listen on instead of TCP port
:type unix: str
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: Nothing
"""
self.prepare(
host=host,
port=port,
dev=dev,
debug=debug,
auto_reload=auto_reload,
ssl=ssl,
sock=sock,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
access_log=access_log,
unix=unix,
loop=loop,
reload_dir=reload_dir,
noisy_exceptions=noisy_exceptions,
motd=motd,
fast=fast,
verbosity=verbosity,
motd_display=motd_display,
)
self.__class__.serve(primary=self) # type: ignore
def prepare(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
dev: bool = False,
debug: bool = False,
auto_reload: Optional[bool] = None,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
workers: int = 1,
protocol: Optional[Type[Protocol]] = None,
backlog: int = 100,
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: AbstractEventLoop = None,
reload_dir: Optional[Union[List[str], str]] = None,
noisy_exceptions: Optional[bool] = None,
motd: bool = True,
fast: bool = False,
verbosity: int = 0,
motd_display: Optional[Dict[str, str]] = None,
) -> None:
if dev:
debug = True
auto_reload = True
self.state.verbosity = verbosity
if not self.state.auto_reload:
self.state.auto_reload = bool(auto_reload)
if fast and workers != 1:
raise RuntimeError("You cannot use both fast=True and workers=X")
if motd_display:
self.config.MOTD_DISPLAY.update(motd_display)
if reload_dir:
if isinstance(reload_dir, str):
reload_dir = [reload_dir]
for directory in reload_dir:
direc = Path(directory)
if not direc.is_dir():
logger.warning(
f"Directory {directory} could not be located"
)
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
"loop is not a valid argument. To use an existing loop, "
"change to create_server().\nSee more: "
"https://sanic.readthedocs.io/en/latest/sanic/deploying.html"
"#asynchronous-support"
)
if (
self.__class__.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
): # no cov
return
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"AUTO_RELOAD": auto_reload,
"MOTD": motd,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
if fast:
self.state.fast = True
try:
workers = len(os.sched_getaffinity(0))
except AttributeError: # no cov
workers = os.cpu_count() or 1
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
workers=workers,
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
)
self.state.server_info.append(
ApplicationServerInfo(settings=server_settings)
)
if self.config.USE_UVLOOP is True or (
self.config.USE_UVLOOP is _default and not OS_IS_WINDOWS
):
try_use_uvloop()
async def create_server(
self,
host: Optional[str] = None,
port: Optional[int] = None,
*,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
protocol: Type[Protocol] = None,
backlog: int = 100,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
return_asyncio_server: bool = False,
asyncio_server_kwargs: Dict[str, Any] = None,
noisy_exceptions: Optional[bool] = None,
) -> Optional[AsyncioServer]:
"""
Asynchronous version of :func:`run`.
This method will take care of the operations necessary to invoke
the *before_start* events via :func:`trigger_events` method invocation
before starting the *sanic* app in Async mode.
.. note::
This does not support multiprocessing and is not the preferred
way to run a :class:`Sanic` application.
:param host: Address to host on
:type host: str
:param port: Port to host on
:type port: int
:param debug: Enables debug output (slows server)
:type debug: bool
:param ssl: SSLContext, or location of certificate and key
for SSL encryption of worker(s)
:type ssl: SSLContext or dict
:param sock: Socket for the server to accept connections from
:type sock: socket
:param protocol: Subclass of asyncio Protocol class
:type protocol: type[Protocol]
:param backlog: a number of unaccepted connections that the system
will allow before refusing new connections
:type backlog: int
:param access_log: Enables writing access logs (slows server)
:type access_log: bool
:param return_asyncio_server: flag that defines whether there's a need
to return asyncio.Server or
start it serving right away
:type return_asyncio_server: bool
:param asyncio_server_kwargs: key-value arguments for
asyncio/uvloop create_server method
:type asyncio_server_kwargs: dict
:param noisy_exceptions: Log exceptions that are normally considered
to be quiet/silent
:type noisy_exceptions: bool
:return: AsyncioServer if return_asyncio_server is true, else Nothing
"""
if sock is None:
host, port = host or "127.0.0.1", port or 8000
if protocol is None:
protocol = (
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# Set explicitly passed configuration values
for attribute, value in {
"ACCESS_LOG": access_log,
"NOISY_EXCEPTIONS": noisy_exceptions,
}.items():
if value is not None:
setattr(self.config, attribute, value)
server_settings = self._helper(
host=host,
port=port,
debug=debug,
ssl=ssl,
sock=sock,
unix=unix,
loop=get_event_loop(),
protocol=protocol,
backlog=backlog,
run_async=return_asyncio_server,
)
if self.config.USE_UVLOOP is not _default:
error_logger.warning(
"You are trying to change the uvloop configuration, but "
"this is only effective when using the run(...) method. "
"When using the create_server(...) method Sanic will use "
"the already existing loop."
)
main_start = server_settings.pop("main_start", None)
main_stop = server_settings.pop("main_stop", None)
if main_start or main_stop:
logger.warning(
"Listener events for the main process are not available "
"with create_server()"
)
return await serve(
asyncio_server_kwargs=asyncio_server_kwargs, **server_settings
)
def stop(self):
"""
This kills the Sanic
"""
if self.state.stage is not ServerStage.STOPPED:
self.shutdown_tasks(timeout=0)
for task in all_tasks():
with suppress(AttributeError):
if task.get_name() == "RunServer":
task.cancel()
get_event_loop().stop()
def _helper(
self,
host: Optional[str] = None,
port: Optional[int] = None,
debug: bool = False,
ssl: Union[None, SSLContext, dict, str, list, tuple] = None,
sock: Optional[socket] = None,
unix: Optional[str] = None,
workers: int = 1,
loop: AbstractEventLoop = None,
protocol: Type[Protocol] = HttpProtocol,
backlog: int = 100,
register_sys_signals: bool = True,
run_async: bool = False,
) -> Dict[str, Any]:
"""Helper function used by `run` and `create_server`."""
if self.config.PROXIES_COUNT and self.config.PROXIES_COUNT < 0:
raise ValueError(
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
ssl = process_to_context(ssl)
if not self.state.is_debug:
self.state.mode = Mode.DEBUG if debug else Mode.PRODUCTION
self.state.host = host or ""
self.state.port = port or 0
self.state.workers = workers
self.state.ssl = ssl
self.state.unix = unix
self.state.sock = sock
server_settings = {
"protocol": protocol,
"host": host,
"port": port,
"sock": sock,
"unix": unix,
"ssl": ssl,
"app": self,
"signal": ServerSignal(),
"loop": loop,
"register_sys_signals": register_sys_signals,
"backlog": backlog,
}
self.motd(self.serve_location)
if sys.stdout.isatty() and not self.state.is_debug:
error_logger.warning(
f"{Colors.YELLOW}Sanic is running in PRODUCTION mode. "
"Consider using '--debug' or '--dev' while actively "
f"developing your application.{Colors.END}"
)
# Register start/stop events
for event_name, settings_name, reverse in (
("main_process_start", "main_start", False),
("main_process_stop", "main_stop", True),
):
listeners = self.listeners[event_name].copy()
if reverse:
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
return server_settings
def motd(self, serve_location):
if self.config.MOTD:
mode = [f"{self.state.mode},"]
if self.state.fast:
mode.append("goin' fast")
if self.state.asgi:
mode.append("ASGI")
else:
if self.state.workers == 1:
mode.append("single worker")
else:
mode.append(f"w/ {self.state.workers} workers")
display = {
"mode": " ".join(mode),
"server": self.state.server,
"python": platform.python_version(),
"platform": platform.platform(),
}
extra = {}
if self.config.AUTO_RELOAD:
reload_display = "enabled"
if self.state.reload_dirs:
reload_display += ", ".join(
[
"",
*(
str(path.absolute())
for path in self.state.reload_dirs
),
]
)
display["auto-reload"] = reload_display
packages = []
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
if packages:
display["packages"] = ", ".join(packages)
if self.config.MOTD_DISPLAY:
extra.update(self.config.MOTD_DISPLAY)
logo = (
get_logo(coffee=self.state.coffee)
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
@property
def serve_location(self) -> str:
serve_location = ""
proto = "http"
if self.state.ssl is not None:
proto = "https"
if self.state.unix:
serve_location = f"{self.state.unix} {proto}://..."
elif self.state.sock:
serve_location = f"{self.state.sock.getsockname()} {proto}://..."
elif self.state.host and self.state.port:
# colon(:) is legal for a host only in an ipv6 address
display_host = (
f"[{self.state.host}]"
if ":" in self.state.host
else self.state.host
)
serve_location = f"{proto}://{display_host}:{self.state.port}"
return serve_location
@classmethod
def should_auto_reload(cls) -> bool:
return any(app.state.auto_reload for app in cls._app_registry.values())
@classmethod
def serve(cls, primary: Optional[Sanic] = None) -> None:
apps = list(cls._app_registry.values())
if not primary:
try:
primary = apps[0]
except IndexError:
raise RuntimeError("Did not find any applications.")
# We want to run auto_reload if ANY of the applications have it enabled
if (
cls.should_auto_reload()
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
reload_dirs: Set[Path] = primary.state.reload_dirs.union(
*(app.state.reload_dirs for app in apps)
)
return reloader_helpers.watchdog(1.0, reload_dirs)
# This exists primarily for unit testing
if not primary.state.server_info: # no cov
for app in apps:
app.state.server_info.clear()
return
primary_server_info = primary.state.server_info[0]
primary.before_server_start(partial(primary._start_servers, apps=apps))
try:
primary_server_info.stage = ServerStage.SERVING
if primary.state.workers > 1 and os.name != "posix": # no cov
logger.warn(
f"Multiprocessing is currently not supported on {os.name},"
" using workers=1 instead"
)
primary.state.workers = 1
if primary.state.workers == 1:
serve_single(primary_server_info.settings)
elif primary.state.workers == 0:
raise RuntimeError("Cannot serve with no workers")
else:
serve_multiple(
primary_server_info.settings, primary.state.workers
)
except BaseException:
error_logger.exception(
"Experienced exception while trying to serve"
)
raise
finally:
primary_server_info.stage = ServerStage.STOPPED
logger.info("Server Stopped")
for app in apps:
app.state.server_info.clear()
app.router.reset()
app.signal_router.reset()
async def _start_servers(
self,
primary: Sanic,
_,
apps: List[Sanic],
) -> None:
for app in apps:
if (
app.name is not primary.name
and app.state.workers != primary.state.workers
and app.state.server_info
):
message = (
f"The primary application {repr(primary)} is running "
f"with {primary.state.workers} worker(s). All "
"application instances will run with the same number. "
f"You requested {repr(app)} to run with "
f"{app.state.workers} worker(s), which will be ignored "
"in favor of the primary application."
)
if sys.stdout.isatty():
message = "".join(
[
Colors.YELLOW,
message,
Colors.END,
]
)
error_logger.warning(message, exc_info=True)
for server_info in app.state.server_info:
if server_info.stage is not ServerStage.SERVING:
app.state.primary = False
handlers = [
*server_info.settings.pop("main_start", []),
*server_info.settings.pop("main_stop", []),
]
if handlers:
error_logger.warning(
f"Sanic found {len(handlers)} listener(s) on "
"secondary applications attached to the main "
"process. These will be ignored since main "
"process listeners can only be attached to your "
"primary application: "
f"{repr(primary)}"
)
if not server_info.settings["loop"]:
server_info.settings["loop"] = get_running_loop()
try:
server_info.server = await serve(
**server_info.settings,
run_async=True,
reuse_port=bool(primary.state.workers - 1),
)
except OSError as e: # no cov
first_message = (
"An OSError was detected on startup. "
"The encountered error was: "
)
second_message = str(e)
if sys.stdout.isatty():
message_parts = [
Colors.YELLOW,
first_message,
Colors.RED,
second_message,
Colors.END,
]
else:
message_parts = [first_message, second_message]
message = "".join(message_parts)
error_logger.warning(message, exc_info=True)
continue
primary.add_task(
self._run_server(app, server_info), name="RunServer"
)
async def _run_server(
self,
app: RunnerMixin,
server_info: ApplicationServerInfo,
) -> None:
try:
# We should never get to this point without a server
# This is primarily to keep mypy happy
if not server_info.server: # no cov
raise RuntimeError("Could not locate AsyncioServer")
if app.state.stage is ServerStage.STOPPED:
server_info.stage = ServerStage.SERVING
await server_info.server.startup()
await server_info.server.before_start()
await server_info.server.after_start()
await server_info.server.serve_forever()
except CancelledError:
# We should never get to this point without a server
# This is primarily to keep mypy happy
if not server_info.server: # no cov
raise RuntimeError("Could not locate AsyncioServer")
await server_info.server.before_stop()
await server_info.server.close()
await server_info.server.after_stop()
finally:
server_info.stage = ServerStage.STOPPED
server_info.server = None

View File

@@ -13,7 +13,7 @@ ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol: # no cov
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped
self.transport = transport
@@ -56,7 +56,7 @@ class MockProtocol: # no cov
await self._not_paused.wait()
class MockTransport: # no cov
class MockTransport:
_protocol: Optional[MockProtocol]
def __init__(

View File

@@ -1,35 +0,0 @@
from __future__ import annotations
from base64 import b64decode
from dataclasses import dataclass, field
from typing import Optional
@dataclass()
class Credentials:
auth_type: Optional[str]
token: Optional[str]
_username: Optional[str] = field(default=None)
_password: Optional[str] = field(default=None)
def __post_init__(self):
if self._auth_is_basic:
self._username, self._password = (
b64decode(self.token.encode("utf-8")).decode().split(":")
)
@property
def username(self):
if not self._auth_is_basic:
raise AttributeError("Username is available for Basic Auth only")
return self._username
@property
def password(self):
if not self._auth_is_basic:
raise AttributeError("Password is available for Basic Auth only")
return self._password
@property
def _auth_is_basic(self) -> bool:
return self.auth_type == "Basic"

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from ssl import SSLObject
from types import SimpleNamespace
from typing import Any, Dict, Optional

View File

@@ -77,7 +77,7 @@ def _check_file(filename, mtimes):
return need_reload
def watchdog(sleep_interval, reload_dirs):
def watchdog(sleep_interval, app):
"""Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second.
@@ -100,7 +100,7 @@ def watchdog(sleep_interval, reload_dirs):
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in reload_dirs),
*(d.glob("**/*") for d in app.reload_dirs),
):
try:
if _check_file(filename, mtimes):

View File

@@ -14,8 +14,6 @@ from typing import (
from sanic_routing.route import Route # type: ignore
from sanic.models.http_types import Credentials
if TYPE_CHECKING: # no cov
from sanic.server import ConnInfo
@@ -39,7 +37,6 @@ from sanic.headers import (
Options,
parse_accept,
parse_content_header,
parse_credentials,
parse_forwarded,
parse_host,
parse_xforwarded,
@@ -101,13 +98,11 @@ class Request:
"method",
"parsed_accept",
"parsed_args",
"parsed_credentials",
"parsed_not_grouped_args",
"parsed_files",
"parsed_form",
"parsed_forwarded",
"parsed_json",
"parsed_not_grouped_args",
"parsed_token",
"parsed_forwarded",
"raw_url",
"responded",
"request_middleware_started",
@@ -127,7 +122,6 @@ class Request:
app: Sanic,
head: bytes = b"",
):
self.raw_url = url_bytes
# TODO: Content-Encoding detection
self._parsed_url = parse_url(url_bytes)
@@ -147,11 +141,9 @@ class Request:
self.ctx = SimpleNamespace()
self.parsed_forwarded: Optional[Options] = None
self.parsed_accept: Optional[AcceptContainer] = None
self.parsed_credentials: Optional[Credentials] = None
self.parsed_json = None
self.parsed_form = None
self.parsed_files = None
self.parsed_token: Optional[str] = None
self.parsed_args: DefaultDict[
Tuple[bool, bool, str, str], RequestParameters
] = defaultdict(RequestParameters)
@@ -340,41 +332,20 @@ class Request:
return self.parsed_accept
@property
def token(self) -> Optional[str]:
def token(self):
"""Attempt to return the auth header token.
:return: token related to request
"""
if self.parsed_token is None:
prefixes = ("Bearer", "Token")
_, token = parse_credentials(
self.headers.getone("authorization", None), prefixes
)
self.parsed_token = token
return self.parsed_token
prefixes = ("Bearer", "Token")
auth_header = self.headers.getone("authorization", None)
@property
def credentials(self) -> Optional[Credentials]:
"""Attempt to return the auth header value.
if auth_header is not None:
for prefix in prefixes:
if prefix in auth_header:
return auth_header.partition(prefix)[-1].strip()
Covers NoAuth, Basic Auth, Bearer Token, Api Token authentication
schemas.
:return: A Credentials object with token, or username and password
related to the request
"""
if self.parsed_credentials is None:
try:
prefix, credentials = parse_credentials(
self.headers.getone("authorization", None)
)
if credentials:
self.parsed_credentials = Credentials(
auth_type=prefix, token=credentials
)
except ValueError:
pass
return self.parsed_credentials
return auth_header
@property
def form(self):

View File

@@ -1,26 +1,12 @@
import asyncio
from distutils.util import strtobool
from os import getenv
from sanic.compat import OS_IS_WINDOWS
from sanic.log import error_logger
def strtobool(query: str) -> bool:
"""
reimplement strtobool per PEP 632 and python 3.12 deprecation
True values are y, yes, t, true, on and 1; false values are n, no, f,
false, off and 0. Raises ValueError if val is anything else.
"""
if query.lower() in ["y", "yes", "t", "true", "on", "1"]:
return True
elif query.lower() in ["n", "no", "f", "false", "off", "0"]:
return False
else:
raise ValueError(f"String value {query} cannot be converted to bool")
def try_use_uvloop() -> None:
"""
Use uvloop instead of the default asyncio loop.

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic.app import Sanic
import asyncio

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Optional
from sanic.touchup.meta import TouchUpMeta
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic.app import Sanic
from asyncio import CancelledError

View File

@@ -5,13 +5,13 @@ from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
from sanic.log import error_logger
from sanic.log import deprecation, error_logger
from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from websockets import http11
@@ -29,6 +29,9 @@ class WebSocketProtocol(HttpProtocol):
*args,
websocket_timeout: float = 10.0,
websocket_max_size: Optional[int] = None,
websocket_max_queue: Optional[int] = None, # max_queue is deprecated
websocket_read_limit: Optional[int] = None, # read_limit is deprecated
websocket_write_limit: Optional[int] = None, # write_limit deprecated
websocket_ping_interval: Optional[float] = 20.0,
websocket_ping_timeout: Optional[float] = 20.0,
**kwargs,
@@ -37,6 +40,27 @@ class WebSocketProtocol(HttpProtocol):
self.websocket: Optional[WebsocketImplProtocol] = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.",
22.3,
)
if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.",
22.3,
)
if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.",
22.3,
)
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout

View File

@@ -132,7 +132,7 @@ def serve(
try:
http_server = loop.run_until_complete(server_coroutine)
except BaseException:
error_logger.exception("Unable to start server", exc_info=True)
error_logger.exception("Unable to start server")
return
# Ignore SIGINT when run_multiple

View File

@@ -9,7 +9,7 @@ from websockets.typing import Data
from sanic.exceptions import ServerError
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from .impl import WebsocketImplProtocol
UTF8Decoder = codecs.getincrementaldecoder("utf-8")
@@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
"get_id",
"put_id",
)
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
protocol: "WebsocketImplProtocol"
read_mutex: asyncio.Lock
write_mutex: asyncio.Lock
@@ -131,7 +131,7 @@ class WebsocketFrameAssembler:
if self.paused:
self.protocol.resume_frames()
self.paused = False
if not self.get_in_progress: # no cov
if not self.get_in_progress:
# This should be guarded against with the read_mutex,
# exception is here as a failsafe
raise ServerError(
@@ -204,7 +204,7 @@ class WebsocketFrameAssembler:
if self.paused:
self.protocol.resume_frames()
self.paused = False
if not self.get_in_progress: # no cov
if not self.get_in_progress:
# This should be guarded against with the read_mutex,
# exception is here as a failsafe
raise ServerError(
@@ -212,7 +212,7 @@ class WebsocketFrameAssembler:
"asynchronous get was in progress."
)
self.get_in_progress = False
if not self.message_complete.is_set(): # no cov
if not self.message_complete.is_set():
# This should be guarded against with the read_mutex,
# exception is here as a failsafe
raise ServerError(
@@ -220,7 +220,7 @@ class WebsocketFrameAssembler:
"message was complete."
)
self.message_complete.clear()
if self.message_fetched.is_set(): # no cov
if self.message_fetched.is_set():
# This should be guarded against with the read_mutex,
# and get_in_progress check, this exception is
# here as a failsafe

View File

@@ -518,12 +518,8 @@ class WebsocketImplProtocol:
)
try:
self.recv_cancel = asyncio.Future()
tasks = (
self.recv_cancel,
asyncio.ensure_future(self.assembler.get(timeout)),
)
done, pending = await asyncio.wait(
tasks,
(self.recv_cancel, self.assembler.get(timeout)),
return_when=asyncio.FIRST_COMPLETED,
)
done_task = next(iter(done))
@@ -574,12 +570,8 @@ class WebsocketImplProtocol:
self.can_pause = False
self.recv_cancel = asyncio.Future()
while True:
tasks = (
self.recv_cancel,
asyncio.ensure_future(self.assembler.get(timeout=0)),
)
done, pending = await asyncio.wait(
tasks,
(self.recv_cancel, self.assembler.get(timeout=0)),
return_when=asyncio.FIRST_COMPLETED,
)
done_task = next(iter(done))

View File

@@ -10,14 +10,12 @@ from .base import BaseScheme
class OptionalDispatchEvent(BaseScheme):
ident = "ODE"
SYNC_SIGNAL_NAMESPACES = "http."
def __init__(self, app) -> None:
super().__init__(app)
self._sync_events()
self._registered_events = [
signal.name for signal in app.signal_router.routes
signal.path for signal in app.signal_router.routes
]
def run(self, method, module_globals):
@@ -33,35 +31,6 @@ class OptionalDispatchEvent(BaseScheme):
return exec_locals[method.__name__]
def _sync_events(self):
all_events = set()
app_events = {}
for app in self.app.__class__._app_registry.values():
if app.state.server_info:
app_events[app] = {
signal.name for signal in app.signal_router.routes
}
all_events.update(app_events[app])
for app, events in app_events.items():
missing = {
x
for x in all_events.difference(events)
if any(x.startswith(y) for y in self.SYNC_SIGNAL_NAMESPACES)
}
if missing:
was_finalized = app.signal_router.finalized
if was_finalized: # no cov
app.signal_router.reset()
for event in missing:
app.signal(event)(self.noop)
if was_finalized: # no cov
app.signal_router.finalize()
@staticmethod
async def noop(**_): # no cov
...
class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events, verbosity: int = 0) -> None:

View File

@@ -11,18 +11,35 @@ from sanic.helpers import import_string
def str_to_bool(val: str) -> bool:
"""
reimplement strtobool per PEP 632 and python 3.12 deprecation
"""Takes string and tries to turn it into bool as human would do.
True values are y, yes, t, true, on and 1; false values are n, no, f,
false, off and 0. Raises ValueError if val is anything else.
"""
if val.lower() in ["y", "yes", "t", "true", "on", "1"]:
If val is in case insensitive (
"y", "yes", "yep", "yup", "t",
"true", "on", "enable", "enabled", "1"
) returns True.
If val is in case insensitive (
"n", "no", "f", "false", "off", "disable", "disabled", "0"
) returns False.
Else Raise ValueError."""
val = val.lower()
if val in {
"y",
"yes",
"yep",
"yup",
"t",
"true",
"on",
"enable",
"enabled",
"1",
}:
return True
elif val.lower() in ["n", "no", "f", "false", "off", "0"]:
elif val in {"n", "no", "f", "false", "off", "disable", "disabled", "0"}:
return False
else:
raise ValueError(f"String value {val} cannot be converted to bool")
raise ValueError(f"Invalid truth value {val}")
def load_module_from_file_location(

243
sanic/worker.py Normal file
View File

@@ -0,0 +1,243 @@
import asyncio
import logging
import os
import signal
import sys
import traceback
from gunicorn.workers import base # type: ignore
from sanic.compat import UVLOOP_INSTALLED
from sanic.log import logger
from sanic.server import HttpProtocol, Signal, serve, try_use_uvloop
from sanic.server.protocols.websocket_protocol import WebSocketProtocol
try:
import ssl # type: ignore
except ImportError: # no cov
ssl = None # type: ignore
if UVLOOP_INSTALLED: # no cov
try_use_uvloop()
class GunicornWorker(base.Worker):
http_protocol = HttpProtocol
websocket_protocol = WebSocketProtocol
def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw)
cfg = self.cfg
if cfg.is_ssl:
self.ssl_context = self._create_ssl_context(cfg)
else:
self.ssl_context = None
self.servers = {}
self.connections = set()
self.exit_code = 0
self.signal = Signal()
def init_process(self):
# create new event_loop after fork
asyncio.get_event_loop().close()
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
super().init_process()
def run(self):
is_debug = self.log.loglevel == logging.DEBUG
protocol = (
self.websocket_protocol
if self.app.callable.websocket_enabled
else self.http_protocol
)
self._server_settings = self.app.callable._helper(
loop=self.loop,
debug=is_debug,
protocol=protocol,
ssl=self.ssl_context,
run_async=True,
)
self._server_settings["signal"] = self.signal
self._server_settings.pop("sock")
self._await(self.app.callable._startup())
self._await(
self.app.callable._server_event("init", "before", loop=self.loop)
)
main_start = self._server_settings.pop("main_start", None)
main_stop = self._server_settings.pop("main_stop", None)
if main_start or main_stop: # noqa
logger.warning(
"Listener events for the main process are not available "
"with GunicornWorker"
)
try:
self._await(self._run())
self.app.callable.is_running = True
self._await(
self.app.callable._server_event(
"init", "after", loop=self.loop
)
)
self.loop.run_until_complete(self._check_alive())
self._await(
self.app.callable._server_event(
"shutdown", "before", loop=self.loop
)
)
self.loop.run_until_complete(self.close())
except BaseException:
traceback.print_exc()
finally:
try:
self._await(
self.app.callable._server_event(
"shutdown", "after", loop=self.loop
)
)
except BaseException:
traceback.print_exc()
finally:
self.loop.close()
sys.exit(self.exit_code)
async def close(self):
if self.servers:
# stop accepting connections
self.log.info(
"Stopping server: %s, connections: %s",
self.pid,
len(self.connections),
)
for server in self.servers:
server.close()
await server.wait_closed()
self.servers.clear()
# prepare connections for closing
self.signal.stopped = True
for conn in self.connections:
conn.close_if_idle()
# gracefully shutdown timeout
start_shutdown = 0
graceful_shutdown_timeout = self.cfg.graceful_timeout
while self.connections and (
start_shutdown < graceful_shutdown_timeout
):
await asyncio.sleep(0.1)
start_shutdown = start_shutdown + 0.1
# Force close non-idle connection after waiting for
# graceful_shutdown_timeout
for conn in self.connections:
if hasattr(conn, "websocket") and conn.websocket:
conn.websocket.fail_connection(code=1001)
else:
conn.abort()
async def _run(self):
for sock in self.sockets:
state = dict(requests_count=0)
self._server_settings["host"] = None
self._server_settings["port"] = None
server = await serve(
sock=sock,
connections=self.connections,
state=state,
**self._server_settings
)
self.servers[server] = state
async def _check_alive(self):
# If our parent changed then we shut down.
pid = os.getpid()
try:
while self.alive:
self.notify()
req_count = sum(
self.servers[srv]["requests_count"] for srv in self.servers
)
if self.max_requests and req_count > self.max_requests:
self.alive = False
self.log.info(
"Max requests exceeded, shutting down: %s", self
)
elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
else:
await asyncio.sleep(1.0, loop=self.loop)
except (Exception, BaseException, GeneratorExit, KeyboardInterrupt):
pass
@staticmethod
def _create_ssl_context(cfg):
"""Creates SSLContext instance for usage in asyncio.create_server.
See ssl.SSLSocket.__init__ for more details.
"""
ctx = ssl.SSLContext(cfg.ssl_version)
ctx.load_cert_chain(cfg.certfile, cfg.keyfile)
ctx.verify_mode = cfg.cert_reqs
if cfg.ca_certs:
ctx.load_verify_locations(cfg.ca_certs)
if cfg.ciphers:
ctx.set_ciphers(cfg.ciphers)
return ctx
def init_signals(self):
# Set up signals through the event loop API.
self.loop.add_signal_handler(
signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None
)
self.loop.add_signal_handler(
signal.SIGTERM, self.handle_exit, signal.SIGTERM, None
)
self.loop.add_signal_handler(
signal.SIGINT, self.handle_quit, signal.SIGINT, None
)
self.loop.add_signal_handler(
signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None
)
self.loop.add_signal_handler(
signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None
)
self.loop.add_signal_handler(
signal.SIGABRT, self.handle_abort, signal.SIGABRT, None
)
# Don't let SIGTERM and SIGUSR1 disturb active requests
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
def handle_quit(self, sig, frame):
self.alive = False
self.app.callable.is_running = False
self.cfg.worker_int(self)
def handle_abort(self, sig, frame):
self.alive = False
self.exit_code = 1
self.cfg.worker_abort(self)
sys.exit(1)
def _await(self, coro):
fut = asyncio.ensure_future(coro, loop=self.loop)
self.loop.run_until_complete(fut)

View File

@@ -6,6 +6,8 @@ import os
import re
import sys
from distutils.util import strtobool
from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand
@@ -59,7 +61,7 @@ setup_kwargs = {
"Build fast. Run fast."
),
"long_description": long_description,
"packages": find_packages(include=[]),
"packages": find_packages(),
"package_data": {"sanic": ["py.typed"]},
"platforms": "any",
"python_requires": ">=3.7",
@@ -130,23 +132,6 @@ dev_require = tests_require + [
all_require = list(set(dev_require + docs_require))
# trying to self-refernce this from within sanic prior to install is
# problematic
def strtobool(val: str) -> bool:
"""
reimplement strtobool per PEP 632 and python 3.12 deprecation
True values are y, yes, t, true, on and 1; false values are n, no, f,
false, off and 0. Raises ValueError if val is anything else.
"""
if val.lower() in ["y", "yes", "t", "true", "on", "1"]:
return True
elif val.lower() in ["n", "no", "f", "false", "off", "0"]:
return False
else:
raise ValueError(f"String value {val} cannot be converted to bool")
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
print("Installing without uJSON")
requirements.remove(ujson)

View File

@@ -1,34 +0,0 @@
"""
For 3.7 compat
"""
from unittest.mock import Mock
class AsyncMock(Mock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.await_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
parent = super(AsyncMock, self)
async def dummy():
self.await_count += 1
return parent.__call__(*args, **kwargs)
return dummy()
def __await__(self):
return self().__await__()
def assert_awaited_once(self):
if not self.await_count == 1:
msg = (
f"Expected to have been awaited once."
f" Awaited {self.await_count} times."
)
raise AssertionError(msg)

View File

@@ -175,21 +175,6 @@ def run_startup(caplog):
return run
@pytest.fixture
def run_multi(caplog):
def run(app, level=logging.DEBUG):
@app.after_server_start
async def stop(app, _):
app.stop()
with caplog.at_level(level):
Sanic.serve()
return caplog.record_tuples
return run
@pytest.fixture(scope="function")
def message_in_records():
def msg_in_log(records: List[LogRecord], msg: str):

View File

@@ -197,7 +197,7 @@ def test_app_enable_websocket(app, websocket_enabled, enable):
assert app.websocket_enabled == True
@patch("sanic.mixins.runner.WebSocketProtocol")
@patch("sanic.app.WebSocketProtocol")
def test_app_websocket_parameters(websocket_protocol_mock, app):
app.config.WEBSOCKET_MAX_SIZE = 44
app.config.WEBSOCKET_PING_TIMEOUT = 48
@@ -473,14 +473,13 @@ def test_custom_context():
assert app.ctx == ctx
@pytest.mark.parametrize("use", (False, True))
def test_uvloop_config(app, monkeypatch, use):
def test_uvloop_config(app, monkeypatch):
@app.get("/test")
def handler(request):
return text("ok")
try_use_uvloop = Mock()
monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
# Default config
app.test_client.get("/test")
@@ -490,13 +489,14 @@ def test_uvloop_config(app, monkeypatch, use):
try_use_uvloop.assert_called_once()
try_use_uvloop.reset_mock()
app.config["USE_UVLOOP"] = use
app.config["USE_UVLOOP"] = False
app.test_client.get("/test")
try_use_uvloop.assert_not_called()
if use:
try_use_uvloop.assert_called_once()
else:
try_use_uvloop.assert_not_called()
try_use_uvloop.reset_mock()
app.config["USE_UVLOOP"] = True
app.test_client.get("/test")
try_use_uvloop.assert_called_once()
def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
@@ -506,7 +506,7 @@ def test_uvloop_cannot_never_called_with_create_server(caplog, monkeypatch):
apps[2].config.USE_UVLOOP = True
try_use_uvloop = Mock()
monkeypatch.setattr(sanic.mixins.runner, "try_use_uvloop", try_use_uvloop)
monkeypatch.setattr(sanic.app, "try_use_uvloop", try_use_uvloop)
loop = asyncio.get_event_loop()
@@ -569,8 +569,3 @@ def test_cannot_run_fast_and_workers(app):
message = "You cannot use both fast=True and workers=X"
with pytest.raises(RuntimeError, match=message):
app.run(fast=True, workers=4)
def test_no_workers(app):
with pytest.raises(RuntimeError, match="Cannot serve with no workers"):
app.run(workers=0)

View File

@@ -1,5 +1,4 @@
import asyncio
import logging
from collections import deque, namedtuple
@@ -7,7 +6,6 @@ import pytest
import uvicorn
from sanic import Sanic
from sanic.application.state import Mode
from sanic.asgi import MockTransport
from sanic.exceptions import Forbidden, InvalidUsage, ServiceUnavailable
from sanic.request import Request
@@ -46,7 +44,7 @@ def protocol(transport):
return transport.get_protocol()
def test_listeners_triggered(caplog):
def test_listeners_triggered():
app = Sanic("app")
before_server_start = False
after_server_start = False
@@ -84,31 +82,9 @@ def test_listeners_triggered(caplog):
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config)
start_message = (
'You have set a listener for "before_server_start" in ASGI mode. '
"It will be executed as early as possible, but not before the ASGI "
"server is started."
)
stop_message = (
'You have set a listener for "after_server_stop" in ASGI mode. '
"It will be executed as late as possible, but not after the ASGI "
"server is stopped."
)
with caplog.at_level(logging.DEBUG):
with pytest.warns(UserWarning):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
for task in all_tasks:
task.cancel()
@@ -118,38 +94,8 @@ def test_listeners_triggered(caplog):
assert before_server_stop
assert after_server_stop
app.state.mode = Mode.DEBUG
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
app.state.verbosity = 2
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) in caplog.record_tuples
def test_listeners_triggered_async(app, caplog):
def test_listeners_triggered_async(app):
before_server_start = False
after_server_start = False
before_server_stop = False
@@ -186,31 +132,9 @@ def test_listeners_triggered_async(app, caplog):
config = uvicorn.Config(app=app, loop="asyncio", limit_max_requests=0)
server = CustomServer(config=config)
start_message = (
'You have set a listener for "before_server_start" in ASGI mode. '
"It will be executed as early as possible, but not before the ASGI "
"server is started."
)
stop_message = (
'You have set a listener for "after_server_stop" in ASGI mode. '
"It will be executed as late as possible, but not after the ASGI "
"server is stopped."
)
with caplog.at_level(logging.DEBUG):
with pytest.warns(UserWarning):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
all_tasks = asyncio.all_tasks(asyncio.get_event_loop())
for task in all_tasks:
task.cancel()
@@ -220,36 +144,6 @@ def test_listeners_triggered_async(app, caplog):
assert before_server_stop
assert after_server_stop
app.state.mode = Mode.DEBUG
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) not in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) not in caplog.record_tuples
app.state.verbosity = 2
with caplog.at_level(logging.DEBUG):
server.run()
assert (
"sanic.root",
logging.DEBUG,
start_message,
) in caplog.record_tuples
assert (
"sanic.root",
logging.DEBUG,
stop_message,
) in caplog.record_tuples
def test_non_default_uvloop_config_raises_warning(app):
app.config.USE_UVLOOP = True

View File

@@ -103,7 +103,7 @@ def test_tls_wrong_options(cmd):
assert not out
lines = err.decode().split("\n")
errmsg = lines[6]
errmsg = lines[8]
assert errmsg == "TLS certificates must be specified by either of:"
@@ -118,10 +118,10 @@ def test_host_port_localhost(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
expected = b"Goin' Fast @ http://localhost:9999"
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
assert firstline == b"Goin' Fast @ http://localhost:9999"
@pytest.mark.parametrize(
@@ -135,10 +135,10 @@ def test_host_port_ipv4(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
expected = b"Goin' Fast @ http://127.0.0.127:9999"
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999"
@pytest.mark.parametrize(
@@ -152,10 +152,10 @@ def test_host_port_ipv6_any(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
expected = b"Goin' Fast @ http://[::]:9999"
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
assert firstline == b"Goin' Fast @ http://[::]:9999"
@pytest.mark.parametrize(
@@ -169,10 +169,10 @@ def test_host_port_ipv6_loopback(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
expected = b"Goin' Fast @ http://[::1]:9999"
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert expected in lines, f"Lines found: {lines}\nErr output: {err}"
assert firstline == b"Goin' Fast @ http://[::1]:9999"
@pytest.mark.parametrize(
@@ -191,40 +191,24 @@ def test_num_workers(num, cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
if num == 1:
expected = b"mode: production, single worker"
else:
expected = (f"mode: production, w/ {num} workers").encode()
worker_lines = [
line
for line in lines
if b"Starting worker" in line or b"Stopping worker" in line
]
assert exitcode != 1
assert expected in lines, f"Expected {expected}\nLines found: {lines}"
assert len(worker_lines) == num * 2, f"Lines found: {lines}"
@pytest.mark.parametrize("cmd", ("--debug",))
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
def test_debug(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
info = read_app_info(lines)
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
assert (
info["auto_reload"] is False
), f"Lines found: {lines}\nErr output: {err}"
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
@pytest.mark.parametrize("cmd", ("--dev", "-d"))
def test_dev(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
info = read_app_info(lines)
assert info["debug"] is True, f"Lines found: {lines}\nErr output: {err}"
assert (
info["auto_reload"] is True
), f"Lines found: {lines}\nErr output: {err}"
assert info["debug"] is True
assert info["auto_reload"] is True
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
@@ -234,11 +218,8 @@ def test_auto_reload(cmd):
lines = out.split(b"\n")
info = read_app_info(lines)
assert info["debug"] is False, f"Lines found: {lines}\nErr output: {err}"
assert (
info["auto_reload"] is True
), f"Lines found: {lines}\nErr output: {err}"
assert "dev" not in info, f"Lines found: {lines}\nErr output: {err}"
assert info["debug"] is False
assert info["auto_reload"] is True
@pytest.mark.parametrize(
@@ -250,9 +231,7 @@ def test_access_logs(cmd, expected):
lines = out.split(b"\n")
info = read_app_info(lines)
assert (
info["access_log"] is expected
), f"Lines found: {lines}\nErr output: {err}"
assert info["access_log"] is expected
@pytest.mark.parametrize("cmd", ("--version", "-v"))
@@ -277,6 +256,4 @@ def test_noisy_exceptions(cmd, expected):
lines = out.split(b"\n")
info = read_app_info(lines)
assert (
info["noisy_exceptions"] is expected
), f"Lines found: {lines}\nErr output: {err}"
assert info["noisy_exceptions"] is expected

View File

@@ -9,8 +9,6 @@ from unittest.mock import Mock, call
import pytest
from pytest import MonkeyPatch
from sanic import Sanic
from sanic.config import DEFAULT_CONFIG, Config
from sanic.exceptions import PyFileError
@@ -41,21 +39,21 @@ class UltimateAnswer:
self.answer = int(answer)
def test_load_from_object(app: Sanic):
def test_load_from_object(app):
app.config.load(ConfigTest)
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_object_string(app: Sanic):
def test_load_from_object_string(app):
app.config.load("test_config.ConfigTest")
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
assert "not_for_config" not in app.config
def test_load_from_instance(app: Sanic):
def test_load_from_instance(app):
app.config.load(ConfigTest())
assert "CONFIG_VALUE" in app.config
assert app.config.CONFIG_VALUE == "should be used"
@@ -64,7 +62,7 @@ def test_load_from_instance(app: Sanic):
assert "another_not_for_config" not in app.config
def test_load_from_object_string_exception(app: Sanic):
def test_load_from_object_string_exception(app):
with pytest.raises(ImportError):
app.config.load("test_config.Config.test")
@@ -122,18 +120,6 @@ def test_env_w_custom_converter():
del environ["SANIC_TEST_ANSWER"]
def test_env_lowercase():
with pytest.warns(None) as record:
environ["SANIC_test_answer"] = "42"
app = Sanic(name=__name__)
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"]
def test_add_converter_multiple_times(caplog):
def converter():
...
@@ -150,7 +136,7 @@ def test_add_converter_multiple_times(caplog):
assert len(config._converters) == 5
def test_load_from_file(app: Sanic):
def test_load_from_file(app):
config = dedent(
"""
VALUE = 'some value'
@@ -169,12 +155,12 @@ def test_load_from_file(app: Sanic):
assert "condition" not in app.config
def test_load_from_missing_file(app: Sanic):
def test_load_from_missing_file(app):
with pytest.raises(IOError):
app.config.load("non-existent file")
def test_load_from_envvar(app: Sanic):
def test_load_from_envvar(app):
config = "VALUE = 'some value'"
with temp_path() as config_path:
config_path.write_text(config)
@@ -184,7 +170,7 @@ def test_load_from_envvar(app: Sanic):
assert app.config.VALUE == "some value"
def test_load_from_missing_envvar(app: Sanic):
def test_load_from_missing_envvar(app):
with pytest.raises(IOError) as e:
app.config.load("non-existent variable")
assert str(e.value) == (
@@ -194,7 +180,7 @@ def test_load_from_missing_envvar(app: Sanic):
)
def test_load_config_from_file_invalid_syntax(app: Sanic):
def test_load_config_from_file_invalid_syntax(app):
config = "VALUE = some value"
with temp_path() as config_path:
config_path.write_text(config)
@@ -203,7 +189,7 @@ def test_load_config_from_file_invalid_syntax(app: Sanic):
app.config.load(config_path)
def test_overwrite_exisiting_config(app: Sanic):
def test_overwrite_exisiting_config(app):
app.config.DEFAULT = 1
class Config:
@@ -213,7 +199,7 @@ def test_overwrite_exisiting_config(app: Sanic):
assert app.config.DEFAULT == 2
def test_overwrite_exisiting_config_ignore_lowercase(app: Sanic):
def test_overwrite_exisiting_config_ignore_lowercase(app):
app.config.default = 1
class Config:
@@ -223,7 +209,7 @@ def test_overwrite_exisiting_config_ignore_lowercase(app: Sanic):
assert app.config.default == 1
def test_missing_config(app: Sanic):
def test_missing_config(app):
with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"):
_ = app.config.NON_EXISTENT
@@ -291,7 +277,7 @@ def test_config_custom_defaults_with_env():
del environ[key]
def test_config_access_log_passing_in_run(app: Sanic):
def test_config_access_log_passing_in_run(app):
assert app.config.ACCESS_LOG is True
@app.listener("after_server_start")
@@ -301,15 +287,12 @@ def test_config_access_log_passing_in_run(app: Sanic):
app.run(port=1340, access_log=False)
assert app.config.ACCESS_LOG is False
app.router.reset()
app.signal_router.reset()
app.run(port=1340, access_log=True)
assert app.config.ACCESS_LOG is True
@pytest.mark.asyncio
async def test_config_access_log_passing_in_create_server(app: Sanic):
async def test_config_access_log_passing_in_create_server(app):
assert app.config.ACCESS_LOG is True
@app.listener("after_server_start")
@@ -358,18 +341,18 @@ _test_setting_as_module = str(
],
ids=["from_dict", "from_class", "from_file"],
)
def test_update(app: Sanic, conf_object):
def test_update(app, conf_object):
app.update_config(conf_object)
assert app.config["TEST_SETTING_VALUE"] == 1
def test_update_from_lowercase_key(app: Sanic):
def test_update_from_lowercase_key(app):
d = {"test_setting_value": 1}
app.update_config(d)
assert "test_setting_value" not in app.config
def test_deprecation_notice_when_setting_logo(app: Sanic):
def test_deprecation_notice_when_setting_logo(app):
message = (
"Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6."
@@ -378,7 +361,7 @@ def test_deprecation_notice_when_setting_logo(app: Sanic):
app.config.LOGO = "My Custom Logo"
def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
def test_config_set_methods(app, monkeypatch):
post_set = Mock()
monkeypatch.setattr(Config, "_post_set", post_set)
@@ -423,15 +406,3 @@ def test_config_set_methods(app: Sanic, monkeypatch: MonkeyPatch):
app.config.update_config({"FOO": 10})
post_set.assert_called_once_with("FOO", 10)
def test_negative_proxy_count(app: Sanic):
app.config.PROXIES_COUNT = -1
message = (
"PROXIES_COUNT cannot be negative. "
"https://sanic.readthedocs.io/en/latest/sanic/config.html"
"#proxy-configuration"
)
with pytest.raises(ValueError, match=message):
app.prepare()

View File

@@ -67,7 +67,7 @@ def test_auto_fallback_with_data(app):
_, response = app.test_client.get("/error")
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
_, response = app.test_client.post("/error", json={"foo": "bar"})
assert response.status == 500
@@ -75,7 +75,7 @@ def test_auto_fallback_with_data(app):
_, response = app.test_client.post("/error", data={"foo": "bar"})
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
def test_auto_fallback_with_content_type(app):
@@ -91,7 +91,7 @@ def test_auto_fallback_with_content_type(app):
"/error", headers={"content-type": "foo/bar", "accept": "*/*"}
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
def test_route_error_format_set_on_auto(app):
@@ -174,17 +174,6 @@ def test_route_error_format_unknown(app):
...
def test_fallback_with_content_type_html(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
_, response = app.test_client.get(
"/error",
headers={"content-type": "application/json", "accept": "text/html"},
)
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
def test_fallback_with_content_type_mismatch_accept(app):
app.config.FALLBACK_ERROR_FORMAT = "auto"
@@ -197,10 +186,10 @@ def test_fallback_with_content_type_mismatch_accept(app):
_, response = app.test_client.get(
"/error",
headers={"content-type": "text/html", "accept": "foo/bar"},
headers={"content-type": "text/plain", "accept": "foo/bar"},
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
app.router.reset()
@@ -219,7 +208,7 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"},
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
_, response = app.test_client.get(
"/alt1",
headers={"accept": "foo/bar,*/*"},
@@ -232,7 +221,7 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"},
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
assert response.content_type == "text/html; charset=utf-8"
_, response = app.test_client.get(
"/alt2",
headers={"accept": "foo/bar,*/*"},
@@ -245,13 +234,6 @@ def test_fallback_with_content_type_mismatch_accept(app):
headers={"accept": "foo/bar"},
)
assert response.status == 500
assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.get(
"/alt3",
headers={"accept": "foo/bar,text/html"},
)
assert response.status == 500
assert response.content_type == "text/html; charset=utf-8"
@@ -306,10 +288,6 @@ def test_allow_fallback_error_format_set_main_process_start(app):
def test_setting_fallback_on_config_changes_as_expected(app):
app.error_handler = ErrorHandler()
_, response = app.test_client.get("/error")
assert response.content_type == "text/plain; charset=utf-8"
app.config.FALLBACK_ERROR_FORMAT = "html"
_, response = app.test_client.get("/error")
assert response.content_type == "text/html; charset=utf-8"

View File

@@ -1,4 +1,5 @@
import logging
import warnings
import pytest
@@ -33,7 +34,6 @@ class SanicExceptionTestException(Exception):
@pytest.fixture(scope="module")
def exception_app():
app = Sanic("test_exceptions")
app.config.FALLBACK_ERROR_FORMAT = "html"
@app.route("/")
def handler(request):

View File

@@ -216,6 +216,31 @@ def test_exception_handler_processed_request_middleware(
assert response.text == "Done."
def test_single_arg_exception_handler_notice(
exception_handler_app: Sanic, caplog: LogCaptureFixture
):
class CustomErrorHandler(ErrorHandler):
def lookup(self, exception):
return super().lookup(exception, None)
exception_handler_app.error_handler = CustomErrorHandler()
message = (
"[DEPRECATION v22.3] You are using a deprecated error handler. The "
"lookup method should accept two positional parameters: (exception, "
"route_name: Optional[str]). Until you upgrade your "
"ErrorHandler.lookup, Blueprint specific exceptions will not work "
"properly. Beginning in v22.3, the legacy style lookup method will "
"not work at all."
)
with pytest.warns(DeprecationWarning) as record:
_, response = exception_handler_app.test_client.get("/1")
assert len(record) == 1
assert record[0].message.args[0] == message
assert response.status == 400
def test_error_handler_noisy_log(
exception_handler_app: Sanic, monkeypatch: MonkeyPatch
):
@@ -254,7 +279,7 @@ def test_exception_handler_response_was_sent(
@app.route("/2")
async def handler2(request: Request):
await request.respond()
response = await request.respond()
raise ServerError("Exception")
with caplog.at_level(logging.WARNING):

View File

@@ -1,15 +1,11 @@
import logging
import os
import platform
import sys
from unittest.mock import Mock
import pytest
from sanic import Sanic, __version__
from sanic import __version__
from sanic.application.logo import BASE_LOGO
from sanic.application.motd import MOTD, MOTDTTY
from sanic.application.motd import MOTDTTY
def test_logo_base(app, run_startup):
@@ -87,25 +83,3 @@ def test_motd_display(caplog):
└───────────────────────┴────────┘
"""
)
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not on 3.7")
def test_reload_dirs(app):
app.config.LOGO = None
app.config.AUTO_RELOAD = True
app.prepare(reload_dir="./", auto_reload=True, motd_display={"foo": "bar"})
existing = MOTD.output
MOTD.output = Mock()
app.motd("foo")
MOTD.output.assert_called_once()
assert (
MOTD.output.call_args.args[2]["auto-reload"]
== f"enabled, {os.getcwd()}"
)
assert MOTD.output.call_args.args[3] == {"foo": "bar"}
MOTD.output = existing
Sanic._app_registry = {}

View File

@@ -1,207 +0,0 @@
import logging
from unittest.mock import Mock
import pytest
from sanic import Sanic
from sanic.response import text
from sanic.server.async_server import AsyncioServer
from sanic.signals import Event
from sanic.touchup.schemes.ode import OptionalDispatchEvent
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
@pytest.fixture
def app_one():
app = Sanic("One")
@app.get("/one")
async def one(request):
return text("one")
return app
@pytest.fixture
def app_two():
app = Sanic("Two")
@app.get("/two")
async def two(request):
return text("two")
return app
@pytest.fixture(autouse=True)
def clean():
Sanic._app_registry = {}
yield
def test_serve_same_app_multiple_tuples(app_one, run_multi):
app_one.prepare(port=23456)
app_one.prepare(port=23457)
logs = run_multi(app_one)
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23456",
) in logs
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23457",
) in logs
def test_serve_multiple_apps(app_one, app_two, run_multi):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
logs = run_multi(app_one)
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23456",
) in logs
assert (
"sanic.root",
logging.INFO,
"Goin' Fast @ http://127.0.0.1:23457",
) in logs
def test_listeners_on_secondary_app(app_one, app_two, run_multi):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
before_start = AsyncMock()
after_start = AsyncMock()
before_stop = AsyncMock()
after_stop = AsyncMock()
app_two.before_server_start(before_start)
app_two.after_server_start(after_start)
app_two.before_server_stop(before_stop)
app_two.after_server_stop(after_stop)
run_multi(app_one)
before_start.assert_awaited_once()
after_start.assert_awaited_once()
before_stop.assert_awaited_once()
after_stop.assert_awaited_once()
@pytest.mark.parametrize(
"events",
(
(Event.HTTP_LIFECYCLE_BEGIN,),
(Event.HTTP_LIFECYCLE_BEGIN, Event.HTTP_LIFECYCLE_COMPLETE),
(
Event.HTTP_LIFECYCLE_BEGIN,
Event.HTTP_LIFECYCLE_COMPLETE,
Event.HTTP_LIFECYCLE_REQUEST,
),
),
)
def test_signal_synchronization(app_one, app_two, run_multi, events):
app_one.prepare(port=23456)
app_two.prepare(port=23457)
for event in events:
app_one.signal(event)(AsyncMock())
run_multi(app_one)
assert len(app_two.signal_router.routes) == len(events) + 1
signal_handlers = {
signal.handler
for signal in app_two.signal_router.routes
if signal.name.startswith("http")
}
assert len(signal_handlers) == 1
assert list(signal_handlers)[0] is OptionalDispatchEvent.noop
def test_warning_main_process_listeners_on_secondary(
app_one, app_two, run_multi
):
app_two.main_process_start(AsyncMock())
app_two.main_process_stop(AsyncMock())
app_one.prepare(port=23456)
app_two.prepare(port=23457)
log = run_multi(app_one)
message = (
f"Sanic found 2 listener(s) on "
"secondary applications attached to the main "
"process. These will be ignored since main "
"process listeners can only be attached to your "
"primary application: "
f"{repr(app_one)}"
)
assert ("sanic.error", logging.WARNING, message) in log
def test_no_applications():
Sanic._app_registry = {}
message = "Did not find any applications."
with pytest.raises(RuntimeError, match=message):
Sanic.serve()
def test_oserror_warning(app_one, app_two, run_multi, capfd):
orig = AsyncioServer.__await__
AsyncioServer.__await__ = Mock(side_effect=OSError("foo"))
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457, workers=2)
run_multi(app_one)
captured = capfd.readouterr()
assert (
"An OSError was detected on startup. The encountered error was: foo"
) in captured.err
AsyncioServer.__await__ = orig
def test_running_multiple_offset_warning(app_one, app_two, run_multi, capfd):
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457)
run_multi(app_one)
captured = capfd.readouterr()
assert (
f"The primary application {repr(app_one)} is running "
"with 2 worker(s). All "
"application instances will run with the same number. "
f"You requested {repr(app_two)} to run with "
"1 worker(s), which will be ignored "
"in favor of the primary application."
) in captured.err
def test_running_multiple_secondary(app_one, app_two, run_multi, capfd):
app_one.prepare(port=23456, workers=2)
app_two.prepare(port=23457)
before_start = AsyncMock()
app_two.before_server_start(before_start)
run_multi(app_one)
before_start.await_count == 2

View File

@@ -132,11 +132,11 @@ def test_main_process_event(app, caplog):
logger.info("main_process_stop")
@app.main_process_start
def main_process_start2(app, loop):
def main_process_start(app, loop):
logger.info("main_process_start")
@app.main_process_stop
def main_process_stop2(app, loop):
def main_process_stop(app, loop):
logger.info("main_process_stop")
with caplog.at_level(logging.INFO):

View File

@@ -1,71 +0,0 @@
import logging
import os
from pathlib import Path
from unittest.mock import Mock
import pytest
from sanic import Sanic
from sanic.application.state import ApplicationServerInfo
@pytest.fixture(autouse=True)
def no_skip():
should_auto_reload = Sanic.should_auto_reload
Sanic.should_auto_reload = Mock(return_value=False)
yield
Sanic._app_registry = {}
Sanic.should_auto_reload = should_auto_reload
def get_primary(app: Sanic) -> ApplicationServerInfo:
return app.state.server_info[0]
def test_dev(app: Sanic):
app.prepare(dev=True)
assert app.state.is_debug
assert app.state.auto_reload
def test_motd_display(app: Sanic):
app.prepare(motd_display={"foo": "bar"})
assert app.config.MOTD_DISPLAY["foo"] == "bar"
del app.config.MOTD_DISPLAY["foo"]
@pytest.mark.parametrize("dirs", ("./foo", ("./foo", "./bar")))
def test_reload_dir(app: Sanic, dirs, caplog):
messages = []
with caplog.at_level(logging.WARNING):
app.prepare(reload_dir=dirs)
if isinstance(dirs, str):
dirs = (dirs,)
for d in dirs:
assert Path(d) in app.state.reload_dirs
messages.append(
f"Directory {d} could not be located",
)
for message in messages:
assert ("sanic.root", logging.WARNING, message) in caplog.record_tuples
def test_fast(app: Sanic, run_multi):
app.prepare(fast=True)
try:
workers = len(os.sched_getaffinity(0))
except AttributeError:
workers = os.cpu_count() or 1
assert app.state.fast
assert app.state.workers == workers
logs = run_multi(app, logging.INFO)
messages = [m[2] for m in logs]
assert f"mode: production, goin' fast w/ {workers} workers" in messages

View File

@@ -1,4 +1,3 @@
import base64
import logging
from json import dumps as json_dumps
@@ -16,15 +15,11 @@ from sanic_testing.testing import (
)
from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
from sanic.exceptions import SanicException, ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, Request, RequestParameters
from sanic.response import html, json, text
def encode_basic_auth_credentials(username, password):
return base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
# ------------------------------------------------------------ #
# GET
# ------------------------------------------------------------ #
@@ -367,95 +362,93 @@ async def test_uri_template_asgi(app):
assert request.uri_template == "/foo/<id:int>/bar/<name:[A-z]+>"
@pytest.mark.parametrize(
("auth_type", "token"),
[
# uuid4 generated token set in "Authorization" header
(None, "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# uuid4 generated token with API Token authorization
("Token", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# uuid4 generated token with Bearer Token authorization
("Bearer", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"),
# no Authorization header
(None, None),
],
)
def test_token(app, auth_type, token):
def test_token(app):
@app.route("/")
async def handler(request):
return text("OK")
if token:
headers = {
"content-type": "application/json",
"Authorization": f"{auth_type} {token}"
if auth_type
else f"{token}",
}
else:
headers = {"content-type": "application/json"}
# uuid4 generated token.
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"{token}",
}
request, response = app.test_client.get("/", headers=headers)
assert request.token == token
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Token {token}",
}
@pytest.mark.parametrize(
("auth_type", "token", "username", "password"),
[
# uuid4 generated token set in "Authorization" header
(None, "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
# uuid4 generated token with API Token authorization
("Token", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
# uuid4 generated token with Bearer Token authorization
("Bearer", "a1d895e0-553a-421a-8e22-5ff8ecb48cbf", None, None),
# username and password with Basic Auth authorization
(
"Basic",
encode_basic_auth_credentials("some_username", "some_pass"),
"some_username",
"some_pass",
),
# no Authorization header
(None, None, None, None),
],
)
def test_credentials(app, capfd, auth_type, token, username, password):
request, response = app.test_client.get("/", headers=headers)
assert request.token == token
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Bearer {token}",
}
request, response = app.test_client.get("/", headers=headers)
assert request.token == token
# no Authorization headers
headers = {"content-type": "application/json"}
request, response = app.test_client.get("/", headers=headers)
assert request.token is None
@pytest.mark.asyncio
async def test_token_asgi(app):
@app.route("/")
async def handler(request):
return text("OK")
if token:
headers = {
"content-type": "application/json",
"Authorization": f"{auth_type} {token}"
if auth_type
else f"{token}",
}
else:
headers = {"content-type": "application/json"}
# uuid4 generated token.
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"{token}",
}
request, response = app.test_client.get("/", headers=headers)
request, response = await app.asgi_client.get("/", headers=headers)
if auth_type == "Basic":
assert request.credentials.username == username
assert request.credentials.password == password
else:
_, err = capfd.readouterr()
with pytest.raises(AttributeError):
request.credentials.password
assert "Password is available for Basic Auth only" in err
request.credentials.username
assert "Username is available for Basic Auth only" in err
assert request.token == token
if token:
assert request.credentials.token == token
assert request.credentials.auth_type == auth_type
else:
assert request.credentials is None
assert not hasattr(request.credentials, "token")
assert not hasattr(request.credentials, "auth_type")
assert not hasattr(request.credentials, "_username")
assert not hasattr(request.credentials, "_password")
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Token {token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
assert request.token == token
token = "a1d895e0-553a-421a-8e22-5ff8ecb48cbf"
headers = {
"content-type": "application/json",
"Authorization": f"Bearer {token}",
}
request, response = await app.asgi_client.get("/", headers=headers)
assert request.token == token
# no Authorization headers
headers = {"content-type": "application/json"}
request, response = await app.asgi_client.get("/", headers=headers)
assert request.token is None
def test_content_type(app):
@@ -1721,6 +1714,7 @@ async def test_request_query_args_custom_parsing_asgi(app):
def test_request_cookies(app):
cookies = {"test": "OK"}
@app.get("/")
@@ -1735,6 +1729,7 @@ def test_request_cookies(app):
@pytest.mark.asyncio
async def test_request_cookies_asgi(app):
cookies = {"test": "OK"}
@app.get("/")

View File

@@ -35,7 +35,7 @@ def create_listener(listener_name, in_list):
def start_stop_app(random_name_app, **run_kwargs):
def stop_on_alarm(signum, frame):
random_name_app.stop()
raise KeyboardInterrupt("SIGINT for sanic to stop gracefully")
signal.signal(signal.SIGALRM, stop_on_alarm)
signal.alarm(1)
@@ -130,9 +130,6 @@ async def test_trigger_before_events_create_server_missing_event(app):
def test_create_server_trigger_events(app):
"""Test if create_server can trigger server events"""
def stop_on_alarm(signum, frame):
raise KeyboardInterrupt("...")
flag1 = False
flag2 = False
flag3 = False
@@ -140,7 +137,8 @@ def test_create_server_trigger_events(app):
async def stop(app, loop):
nonlocal flag1
flag1 = True
signal.alarm(1)
await asyncio.sleep(0.1)
app.stop()
async def before_stop(app, loop):
nonlocal flag2
@@ -157,8 +155,6 @@ def test_create_server_trigger_events(app):
loop = asyncio.get_event_loop()
# Use random port for tests
signal.signal(signal.SIGALRM, stop_on_alarm)
with closing(socket()) as sock:
sock.bind(("127.0.0.1", 0))

View File

@@ -1,6 +1,7 @@
import inspect
import logging
import os
import sys
from collections import Counter
from pathlib import Path
@@ -8,7 +9,7 @@ from time import gmtime, strftime
import pytest
from sanic import text
from sanic import Sanic, text
from sanic.exceptions import FileNotFound
@@ -21,6 +22,22 @@ def static_file_directory():
return static_directory
@pytest.fixture(scope="module")
def double_dotted_directory_file(static_file_directory: str):
"""Generate double dotted directory and its files"""
if sys.platform == "win32":
raise Exception("Windows doesn't support double dotted directories")
file_path = Path(static_file_directory) / "dotted.." / "dot.txt"
double_dotted_dir = file_path.parent
Path.mkdir(double_dotted_dir, exist_ok=True)
with open(file_path, "w") as f:
f.write("DOT\n")
yield file_path
Path.unlink(file_path)
Path.rmdir(double_dotted_dir)
def get_file_path(static_file_directory, file_name):
return os.path.join(static_file_directory, file_name)
@@ -578,3 +595,43 @@ def test_resource_type_dir(app, static_file_directory):
def test_resource_type_unknown(app, static_file_directory, caplog):
with pytest.raises(ValueError):
app.static("/static", static_file_directory, resource_type="unknown")
@pytest.mark.skipif(
sys.platform == "win32",
reason="Windows does not support double dotted directories",
)
def test_dotted_dir_ok(
app: Sanic, static_file_directory: str, double_dotted_directory_file: Path
):
app.static("/foo", static_file_directory)
dot_relative_path = str(
double_dotted_directory_file.relative_to(static_file_directory)
)
_, response = app.test_client.get("/foo/" + dot_relative_path)
assert response.status == 200
assert response.body == b"DOT\n"
def test_breakout(app: Sanic, static_file_directory: str):
app.static("/foo", static_file_directory)
_, response = app.test_client.get("/foo/..%2Ffake/server.py")
assert response.status == 404
_, response = app.test_client.get("/foo/..%2Fstatic/test.file")
assert response.status == 404
@pytest.mark.skipif(
sys.platform != "win32", reason="Block backslash on Windows only"
)
def test_double_backslash_prohibited_on_win32(
app: Sanic, static_file_directory: str
):
app.static("/foo", static_file_directory)
_, response = app.test_client.get("/foo/static/..\\static/test.file")
assert response.status == 404
_, response = app.test_client.get("/foo/static\\../static/test.file")
assert response.status == 404

View File

@@ -1,4 +1,5 @@
import asyncio
import sys
from asyncio.tasks import Task
from unittest.mock import Mock, call
@@ -6,15 +7,9 @@ from unittest.mock import Mock, call
import pytest
from sanic.app import Sanic
from sanic.application.state import ApplicationServerInfo, ServerStage
from sanic.response import empty
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
pytestmark = pytest.mark.asyncio
@@ -25,14 +20,11 @@ async def dummy(n=0):
@pytest.fixture(autouse=True)
def mark_app_running(app: Sanic):
app.state.server_info.append(
ApplicationServerInfo(
stage=ServerStage.SERVING, settings={}, server=AsyncMock()
)
)
def mark_app_running(app):
app.is_running = True
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_add_task_returns_task(app: Sanic):
task = app.add_task(dummy())
@@ -40,6 +32,7 @@ async def test_add_task_returns_task(app: Sanic):
assert len(app._task_registry) == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_add_task_with_name(app: Sanic):
task = app.add_task(dummy(), name="dummy")
@@ -51,6 +44,7 @@ async def test_add_task_with_name(app: Sanic):
assert task in app._task_registry.values()
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_cancel_task(app: Sanic):
task = app.add_task(dummy(3), name="dummy")
@@ -68,6 +62,7 @@ async def test_cancel_task(app: Sanic):
assert task.cancelled()
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
async def test_purge_tasks(app: Sanic):
app.add_task(dummy(3), name="dummy")
@@ -80,6 +75,7 @@ async def test_purge_tasks(app: Sanic):
assert len(app._task_registry) == 0
@pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7")
def test_shutdown_tasks_on_app_stop():
class TestSanic(Sanic):
shutdown_tasks = Mock()

View File

@@ -72,12 +72,14 @@ def test_unix_socket_creation(caplog):
assert not os.path.exists(SOCKPATH)
@pytest.mark.parametrize("path", (".", "no-such-directory/sanictest.sock"))
def test_invalid_paths(path):
def test_invalid_paths():
app = Sanic(name=__name__)
with pytest.raises((FileExistsError, FileNotFoundError)):
app.run(unix=path)
with pytest.raises(FileExistsError):
app.run(unix=".")
with pytest.raises(FileNotFoundError):
app.run(unix="no-such-directory/sanictest.sock")
def test_dont_replace_file():
@@ -199,7 +201,7 @@ async def test_zero_downtime():
for _ in range(40):
async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200, r.content
assert r.status_code == 200
assert r.text == "Slept 0.1 seconds.\n"
def spawn():
@@ -207,7 +209,6 @@ async def test_zero_downtime():
sys.executable,
"-m",
"sanic",
"--debug",
"--unix",
SOCKPATH,
"examples.delayed_response.app",

View File

@@ -6,7 +6,6 @@ import pytest
from sanic.exceptions import LoadFileException
from sanic.utils import load_module_from_file_location
from sanic.utils import str_to_bool as strtobool
@pytest.mark.parametrize(
@@ -49,20 +48,3 @@ def test_load_module_from_file_location_using_env():
module = load_module_from_file_location(location)
assert isinstance(module, ModuleType)
@pytest.mark.parametrize(
"valid,values",
(
(True, ["y", "yes", "t", "true", "on", "1", "Y", "yEs", "True"]),
(False, ["n", "no", "f", "false", "off", "0", "N", "No", "False"]),
(None, ["yyy", "foo"]),
),
)
def test_strtobool(valid, values):
for value in values:
if valid is None:
with pytest.raises(ValueError):
strtobool(value)
else:
assert strtobool(value) is valid

View File

@@ -1,243 +0,0 @@
import re
from asyncio import Event, Queue, TimeoutError
from unittest.mock import Mock, call
import pytest
from websockets.frames import CTRL_OPCODES, DATA_OPCODES, Frame
from sanic.exceptions import ServerError
from sanic.server.websockets.frame import WebsocketFrameAssembler
try:
from unittest.mock import AsyncMock
except ImportError:
from asyncmock import AsyncMock # type: ignore
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete_timeout_0():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get(0)
assert data is None
assembler.message_complete.is_set.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get() on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
await assembler.get()
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get()
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeout():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeouterror():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.message_complete.wait.side_effect = TimeoutError("...")
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed_start():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
data = await assembler.get(0.1)
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
assembler.paused = True
data = await assembler.get()
assert data is None
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_data():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.chunks = [b"foo", b"bar"]
data = await assembler.get()
assert data == b"foobar"
@pytest.mark.asyncio
async def test_ws_frame_get_iter_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get_iter on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
[x async for x in assembler.get_iter()]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_none_in_queue():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.chunks = [b"foo", b"bar"]
chunks = [x async for x in assembler.get_iter()]
assert chunks == [b"foo", b"bar"]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.paused = True
[x async for x in assembler.get_iter()]
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched.set()
message = re.escape(
"Websocket put() got a new message when the previous message was "
"not yet fetched."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b""))
assembler.message_fetched.wait.assert_awaited_once()
assembler.message_fetched.clear.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_complete(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
message = re.escape(
"Websocket put() got a new message when a message was "
"already in its chamber."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_into_queue(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.chunks_queue = AsyncMock(spec=Queue)
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b"foo"))
assembler.chunks_queue.put.has_calls(
call(b"foo"),
call(None),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fin(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b"foo", fin=False))
assert retval is None
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", CTRL_OPCODES)
async def test_ws_frame_put_skip_ctrl(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b""))
assert retval is None

200
tests/test_worker.py Normal file
View File

@@ -0,0 +1,200 @@
import asyncio
import json
import shlex
import subprocess
import time
import urllib.request
from unittest import mock
import pytest
from sanic_testing.testing import ASGI_PORT as PORT
from sanic.app import Sanic
from sanic.worker import GunicornWorker
@pytest.fixture
def gunicorn_worker():
command = (
"gunicorn "
f"--bind 127.0.0.1:{PORT} "
"--worker-class sanic.worker.GunicornWorker "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command))
time.sleep(2)
yield
worker.kill()
@pytest.fixture
def gunicorn_worker_with_access_logs():
command = (
"gunicorn "
f"--bind 127.0.0.1:{PORT + 1} "
"--worker-class sanic.worker.GunicornWorker "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
time.sleep(2)
return worker
@pytest.fixture
def gunicorn_worker_with_env_var():
command = (
'env SANIC_ACCESS_LOG="False" '
"gunicorn "
f"--bind 127.0.0.1:{PORT + 2} "
"--worker-class sanic.worker.GunicornWorker "
"--log-level info "
"examples.hello_world:app"
)
worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
time.sleep(2)
return worker
def test_gunicorn_worker(gunicorn_worker):
with urllib.request.urlopen(f"http://localhost:{PORT}/") as f:
res = json.loads(f.read(100).decode())
assert res["test"]
def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
"""
if SANIC_ACCESS_LOG was set to False do not show access logs
"""
with urllib.request.urlopen(f"http://localhost:{PORT + 2}/") as _:
gunicorn_worker_with_env_var.kill()
logs = list(
filter(
lambda x: b"sanic.access" in x,
gunicorn_worker_with_env_var.stdout.read().split(b"\n"),
)
)
assert len(logs) == 0
def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs):
"""
default - show access logs
"""
with urllib.request.urlopen(f"http://localhost:{PORT + 1}/") as _:
gunicorn_worker_with_access_logs.kill()
assert (
b"(sanic.access)[INFO][127.0.0.1"
in gunicorn_worker_with_access_logs.stdout.read()
)
class GunicornTestWorker(GunicornWorker):
def __init__(self):
self.app = mock.Mock()
self.app.callable = Sanic("test_gunicorn_worker")
self.servers = {}
self.exit_code = 0
self.cfg = mock.Mock()
self.notify = mock.Mock()
@pytest.fixture
def worker():
return GunicornTestWorker()
def test_worker_init_process(worker):
with mock.patch("sanic.worker.asyncio") as mock_asyncio:
try:
worker.init_process()
except TypeError:
pass
assert mock_asyncio.get_event_loop.return_value.close.called
assert mock_asyncio.new_event_loop.called
assert mock_asyncio.set_event_loop.called
def test_worker_init_signals(worker):
worker.loop = mock.Mock()
worker.init_signals()
assert worker.loop.add_signal_handler.called
def test_handle_abort(worker):
with mock.patch("sanic.worker.sys") as mock_sys:
worker.handle_abort(object(), object())
assert not worker.alive
assert worker.exit_code == 1
mock_sys.exit.assert_called_with(1)
def test_handle_quit(worker):
worker.handle_quit(object(), object())
assert not worker.alive
assert worker.exit_code == 0
async def _a_noop(*a, **kw):
pass
def test_run_max_requests_exceeded(worker):
loop = asyncio.new_event_loop()
worker.ppid = 1
worker.alive = True
sock = mock.Mock()
sock.cfg_addr = ("localhost", 8080)
worker.sockets = [sock]
worker.wsgi = mock.Mock()
worker.connections = set()
worker.log = mock.Mock()
worker.loop = loop
worker.servers = {
"server1": {"requests_count": 14},
"server2": {"requests_count": 15},
}
worker.max_requests = 10
worker._run = mock.Mock(wraps=_a_noop)
# exceeding request count
_runner = asyncio.ensure_future(worker._check_alive(), loop=loop)
loop.run_until_complete(_runner)
assert not worker.alive
worker.notify.assert_called_with()
worker.log.info.assert_called_with(
"Max requests exceeded, shutting " "down: %s", worker
)
def test_worker_close(worker):
loop = asyncio.new_event_loop()
asyncio.sleep = mock.Mock(wraps=_a_noop)
worker.ppid = 1
worker.pid = 2
worker.cfg.graceful_timeout = 1.0
worker.signal = mock.Mock()
worker.signal.stopped = False
worker.wsgi = mock.Mock()
conn = mock.Mock()
conn.websocket = mock.Mock()
conn.websocket.fail_connection = mock.Mock(wraps=_a_noop)
worker.connections = set([conn])
worker.log = mock.Mock()
worker.loop = loop
server = mock.Mock()
server.close = mock.Mock(wraps=lambda *a, **kw: None)
server.wait_closed = mock.Mock(wraps=_a_noop)
worker.servers = {server: {"requests_count": 14}}
worker.max_requests = 10
# close worker
_close = asyncio.ensure_future(worker.close(), loop=loop)
loop.run_until_complete(_close)
assert worker.signal.stopped
assert conn.websocket.fail_connection.called
assert len(worker.servers) == 0

View File

@@ -7,9 +7,6 @@ setenv =
{py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UJSON=1
{py37,py38,py39,py310,pyNightly}-no-ext: SANIC_NO_UVLOOP=1
extras = test
allowlist_externals =
pytest
coverage
commands =
pytest {posargs:tests --cov sanic}
- coverage combine --append
@@ -44,7 +41,7 @@ commands =
[testenv:docs]
platform = linux|linux2|darwin
allowlist_externals = make
whitelist_externals = make
extras = docs
commands =
make docs-test