Merge branch 'main' into feat/optional-uvloop-use

This commit is contained in:
Néstor Pérez 2021-11-07 21:05:49 +01:00 committed by GitHub
commit a7a8624245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1212 additions and 459 deletions

View File

@ -1,5 +1,7 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
@ -8,7 +10,6 @@ exclude_patterns:
- "docker/"
- "docs/"
- "examples/"
- "hack/"
- "scripts/"
- "tests/"
checks:

View File

@ -3,6 +3,9 @@ branch = True
source = sanic
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py

View File

@ -1,6 +0,0 @@
FROM catthehacker/ubuntu:act-latest
SHELL [ "/bin/bash", "-c" ]
ENTRYPOINT []
RUN apt-get update
RUN apt-get install gcc -y
RUN apt-get install -y --no-install-recommends g++

View File

@ -1,248 +1,15 @@
import os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import error_logger
from sanic.simple import create_simple_server
from sanic.cli.app import SanicCLI
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
class SanicArgumentParser(ArgumentParser):
def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help']}\n "
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
if OS_IS_WINDOWS:
enable_windows_color_support()
def main():
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
formatter_class=lambda prog: RawTextHelpFormatter(
prog, max_help_position=33
),
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
parser.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket\n ",
)
parser.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
parser.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
parser.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help="TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose of multiple certificates",
)
parser.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs\n ",
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]\n ",
)
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="print stack traces for all exceptions",
)
parser.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
)
parser.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes\n ",
)
parser.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
args = parser.parse_args()
# Custom TLS mismatch handling for better diagnostics
if (
# one of cert/key missing
bool(args.cert) != bool(args.key)
# new and old style args used together
or args.tls
and args.cert
# strict host checking without certs would always fail
or args.tlshost
and not args.tls
and not args.cert
):
parser.print_usage(sys.stderr)
error_logger.error(
"sanic: error: TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
sys.exit(1)
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if args.simple:
path = Path(args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
ssl: Union[None, dict, str, list] = []
if args.tlshost:
ssl.append(None)
if args.cert is not None or args.key is not None:
ssl.append(dict(cert=args.cert, key=args.key))
if args.tls:
ssl += args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"host": args.host,
"port": args.port,
"unix": args.unix,
"workers": args.workers,
"debug": args.debug,
"access_log": args.access_log,
"ssl": ssl,
"noisy_exceptions": args.noisy_exceptions,
}
if args.auto_reload:
kwargs["auto_reload"] = True
if args.path:
if args.auto_reload or args.debug:
kwargs["reload_dir"] = 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."
)
app.run(**kwargs)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
except ValueError:
error_logger.exception("Failed to run app")
cli = SanicCLI()
cli.attach()
cli.run()
if __name__ == "__main__":

View File

@ -1 +1 @@
__version__ = "21.9.1"
__version__ = "21.12.0dev"

View File

@ -3,7 +3,9 @@ from __future__ import annotations
import logging
import logging.config
import os
import platform
import re
import sys
from asyncio import (
AbstractEventLoop,
@ -16,6 +18,7 @@ from asyncio import (
from asyncio.futures import Future
from collections import defaultdict, deque
from functools import partial
from importlib import import_module
from inspect import isawaitable
from pathlib import Path
from socket import socket
@ -40,16 +43,22 @@ from typing import (
)
from urllib.parse import urlencode, urlunparse
from sanic_routing.exceptions import FinalizationError # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.exceptions import ( # type: ignore
FinalizationError,
NotFound,
)
from sanic_routing.route import Route # type: ignore
from sanic import reloader_helpers
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 import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.blueprints import Blueprint
from sanic.config import BASE_LOGO, SANIC_PREFIX, Config
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
from sanic.config import SANIC_PREFIX, Config
from sanic.exceptions import (
InvalidUsage,
SanicException,
@ -57,7 +66,7 @@ from sanic.exceptions import (
URLBuildError,
)
from sanic.handlers import ErrorHandler
from sanic.log import LOGGING_CONFIG_DEFAULTS, error_logger, logger
from sanic.log import LOGGING_CONFIG_DEFAULTS, Colors, error_logger, logger
from sanic.mixins.listeners import ListenerEvent
from sanic.models.futures import (
FutureException,
@ -82,6 +91,10 @@ from sanic.tls import process_to_context
from sanic.touchup import TouchUp, TouchUpMeta
if OS_IS_WINDOWS:
enable_windows_color_support()
class Sanic(BaseSanic, metaclass=TouchUpMeta):
"""
The main application instance
@ -94,21 +107,23 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"_run_request_middleware",
)
__fake_slots__ = (
"_asgi_app",
"_app_registry",
"_asgi_app",
"_asgi_client",
"_blueprint_order",
"_delayed_tasks",
"_future_routes",
"_future_statics",
"_future_middleware",
"_future_listeners",
"_future_exceptions",
"_future_listeners",
"_future_middleware",
"_future_routes",
"_future_signals",
"_future_statics",
"_state",
"_test_client",
"_test_manager",
"auto_reload",
"asgi",
"auto_reload",
"auto_reload",
"blueprints",
"config",
"configure_logging",
@ -122,7 +137,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"name",
"named_request_middleware",
"named_response_middleware",
"reload_dirs",
"request_class",
"request_middleware",
"response_middleware",
@ -159,7 +173,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
# logging
if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
dict_config = log_config or LOGGING_CONFIG_DEFAULTS
logging.config.dictConfig(dict_config) # type: ignore
if config and (load_env is not True or env_prefix != SANIC_PREFIX):
raise SanicException(
@ -167,37 +182,33 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"load_env or env_prefix"
)
self._asgi_client = None
self._asgi_client: Any = None
self._test_client: Any = None
self._test_manager: Any = None
self._blueprint_order: List[Blueprint] = []
self._delayed_tasks: List[str] = []
self._test_client = None
self._test_manager = None
self.asgi = False
self.auto_reload = False
self._state: ApplicationState = ApplicationState(app=self)
self.blueprints: Dict[str, Blueprint] = {}
self.config = config or Config(
self.config: Config = config or Config(
load_env=load_env, env_prefix=env_prefix
)
self.configure_logging = configure_logging
self.ctx = ctx or SimpleNamespace()
self.debug = None
self.error_handler = error_handler or ErrorHandler(
self.configure_logging: bool = configure_logging
self.ctx: Any = ctx or SimpleNamespace()
self.debug = False
self.error_handler: ErrorHandler = error_handler or ErrorHandler(
fallback=self.config.FALLBACK_ERROR_FORMAT,
)
self.is_running = False
self.is_stopping = False
self.listeners: Dict[str, List[ListenerType[Any]]] = defaultdict(list)
self.named_request_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.named_response_middleware: Dict[str, Deque[MiddlewareType]] = {}
self.reload_dirs: Set[Path] = set()
self.request_class = request_class
self.request_class: Type[Request] = request_class or Request
self.request_middleware: Deque[MiddlewareType] = deque()
self.response_middleware: Deque[MiddlewareType] = deque()
self.router = router or Router()
self.signal_router = signal_router or SignalRouter()
self.sock = None
self.strict_slashes = strict_slashes
self.websocket_enabled = False
self.router: Router = router or Router()
self.signal_router: SignalRouter = signal_router or SignalRouter()
self.sock: Optional[socket] = None
self.strict_slashes: bool = strict_slashes
self.websocket_enabled: bool = False
self.websocket_tasks: Set[Future[Any]] = set()
# Register alternative method names
@ -961,9 +972,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
register_sys_signals: bool = True,
access_log: Optional[bool] = None,
unix: Optional[str] = None,
loop: None = 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
@ -1001,6 +1016,14 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
: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]
@ -1011,7 +1034,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
logger.warning(
f"Directory {directory} could not be located"
)
self.reload_dirs.add(Path(directory))
self.state.reload_dirs.add(Path(directory))
if loop is not None:
raise TypeError(
@ -1022,7 +1045,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
)
if auto_reload or auto_reload is None and debug:
self.auto_reload = True
auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0, self)
@ -1034,12 +1057,22 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
# 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 noisy_exceptions is not None:
self.config.NOISY_EXCEPTIONS = noisy_exceptions
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,
@ -1052,7 +1085,6 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
protocol=protocol,
backlog=backlog,
register_sys_signals=register_sys_signals,
auto_reload=auto_reload,
)
try:
@ -1151,12 +1183,13 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
WebSocketProtocol if self.websocket_enabled else HttpProtocol
)
# if access_log is passed explicitly change config.ACCESS_LOG
if access_log is not None:
self.config.ACCESS_LOG = access_log
if noisy_exceptions is not None:
self.config.NOISY_EXCEPTIONS = noisy_exceptions
# 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,
@ -1268,19 +1301,18 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def _helper(
self,
host=None,
port=None,
debug=False,
ssl=None,
sock=None,
unix=None,
workers=1,
loop: Optional[Union[bool, AbstractEventLoop]] = None,
protocol=HttpProtocol,
backlog=100,
register_sys_signals=True,
run_async=False,
auto_reload=False,
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: Union[AbstractEventLoop, bool] = 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:
@ -1290,36 +1322,24 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"#proxy-configuration"
)
self.error_handler.debug = debug
self.debug = debug
if self.configure_logging and debug:
logger.setLevel(logging.DEBUG)
if (
self.config.LOGO
and os.environ.get("SANIC_SERVER_RUNNING") != "true"
):
logger.debug(
self.config.LOGO
if isinstance(self.config.LOGO, str)
else BASE_LOGO
)
self.state.host = host
self.state.port = port
self.state.workers = workers
# Serve
if host and port:
serve_location = ""
proto = "http"
if ssl is not None:
proto = "https"
if unix:
logger.info(f"Goin' Fast @ {unix} {proto}://...")
else:
serve_location = f"{unix} {proto}://..."
elif sock:
serve_location = f"{sock.getsockname()} {proto}://..."
elif host and port:
# colon(:) is legal for a host only in an ipv6 address
display_host = f"[{host}]" if ":" in host else host
logger.info(f"Goin' Fast @ {proto}://{display_host}:{port}")
debug_mode = "enabled" if self.debug else "disabled"
reload_mode = "enabled" if auto_reload else "disabled"
logger.debug(f"Sanic auto-reload: {reload_mode}")
logger.debug(f"Sanic debug mode: {debug_mode}")
serve_location = f"{proto}://{display_host}:{port}"
if not isinstance(loop, AbstractEventLoop):
if self.config.USE_UVLOOP:
@ -1344,8 +1364,16 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"backlog": backlog,
}
# Register start/stop events
self.motd(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),
@ -1355,7 +1383,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
listeners.reverse()
# Prepend sanic to the arguments when listeners are triggered
listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners
server_settings[settings_name] = listeners # type: ignore
if run_async:
server_settings["run_async"] = True
@ -1418,6 +1446,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.asgi = True
if self.config.USE_UVLOOP:
use_uvloop()
self.motd("")
self._asgi_app = await ASGIApp.create(self, scope, receive, send)
asgi_app = self._asgi_app
await asgi_app()
@ -1438,6 +1467,114 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
self.config.update_config(config)
@property
def asgi(self):
return self.state.asgi
@asgi.setter
def asgi(self, value: bool):
self.state.asgi = value
@property
def debug(self):
return self.state.is_debug
@debug.setter
def debug(self, value: bool):
mode = Mode.DEBUG if value else Mode.PRODUCTION
self.state.mode = mode
@property
def auto_reload(self):
return self.config.AUTO_RELOAD
@auto_reload.setter
def auto_reload(self, value: bool):
self.config.AUTO_RELOAD = value
@property
def state(self):
return self._state
@property
def is_running(self):
return self.state.is_running
@is_running.setter
def is_running(self, value: bool):
self.state.is_running = value
@property
def is_stopping(self):
return self.state.is_stopping
@is_stopping.setter
def is_stopping(self, value: bool):
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, module_name in {
"sanic-routing": "sanic_routing",
"sanic-testing": "sanic_testing",
"sanic-ext": "sanic_ext",
}.items():
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()
if self.config.LOGO == "" or self.config.LOGO is True
else self.config.LOGO
)
MOTD.output(logo, serve_location, display, extra)
# -------------------------------------------------------------------- #
# Class methods
# -------------------------------------------------------------------- #
@ -1515,6 +1652,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
"shutdown",
):
raise SanicException(f"Invalid server event: {event}")
if self.state.verbosity >= 1:
logger.debug(f"Triggering server events: {event}")
reverse = concern == "shutdown"
if loop is None:

View File

48
sanic/application/logo.py Normal file
View File

@ -0,0 +1,48 @@
import re
import sys
from os import environ
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m \033[0m
\033[48;2;255;13;104m \033[0m
Build Fast. Run Fast."""
FULL_COLOR_LOGO = """
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
\033[38;2;255;13;104m \033[0m
""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False):
logo = (
(FULL_COLOR_LOGO if full else COLOR_LOGO)
if sys.stdout.isatty()
else BASE_LOGO
)
if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo)
return logo

144
sanic/application/motd.py Normal file
View File

@ -0,0 +1,144 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.log import logger
class MOTD(ABC):
def __init__(
self,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
self.logo = logo
self.serve_location = serve_location
self.data = data
self.extra = extra
self.key_width = 0
self.value_width = 0
@abstractmethod
def display(self):
... # noqa
@classmethod
def output(
cls,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
def display(self):
if self.logo:
logger.debug(self.logo)
lines = [f"Sanic v{__version__}"]
if self.serve_location:
lines.append(f"Goin' Fast @ {self.serve_location}")
lines += [
*(f"{key}: {value}" for key, value in self.data.items()),
*(f"{key}: {value}" for key, value in self.extra.items()),
]
for line in lines:
logger.info(line)
class MOTDTTY(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.set_variables()
def set_variables(self): # no cov
fallback = (80, 24)
terminal_width = min(get_terminal_size(fallback=fallback).columns, 108)
self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4
self.value_width = self.max_value_width
if self.data:
self.key_width = max(map(len, self.data.keys()))
self.value_width = min(
max(map(len, self.data.values())), self.max_value_width
)
self.logo_lines = self.logo.split("\n") if self.logo else []
self.logo_line_length = 24
self.centering_length = (
self.key_width + self.value_width + 2 + self.logo_line_length
)
self.display_length = self.key_width + self.value_width + 2
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
self._render_data(lines, self.data, 0)
if self.extra:
logo_part = self._get_logo_part(len(lines) - 4)
lines.append(f"| {logo_part}{display_filler}")
self._render_data(lines, self.extra, len(lines) - 4)
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0
for idx, (key, value) in enumerate(data.items(), start=start):
key = key.rjust(self.key_width)
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
for wrap_index, part in enumerate(wrapped):
part = part.ljust(self.value_width)
logo_part = self._get_logo_part(idx + offset + wrap_index)
display = (
f"{key}: {part}"
if wrap_index == 0
else (" " * len(key) + f" {part}")
)
lines.append(f"{logo_part}{display}")
if wrap_index:
offset += 1
def _render_fill(self, lines):
filler = " " * self.display_length
idx = len(lines) - 5
for i in range(1, len(self.logo_lines) - idx):
logo_part = self.logo_lines[idx + i]
lines.append(f"{logo_part}{filler}")
def _get_logo_part(self, idx):
try:
logo_part = self.logo_lines[idx]
except IndexError:
logo_part = " " * (self.logo_line_length - 3)
return logo_part

View File

@ -0,0 +1,72 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from typing import TYPE_CHECKING, Any, Set, Union
from sanic.log import logger
if TYPE_CHECKING:
from sanic import Sanic
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass
class ApplicationState:
app: Sanic
asgi: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
mode: Mode = field(default=Mode.PRODUCTION)
port: int = field(default=0)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
_init: bool = field(default=False)
def __post_init__(self) -> None:
self._init = True
def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init":
raise RuntimeError(
"Cannot change the value of _init after instantiation"
)
super().__setattr__(name, value)
if self._init and hasattr(self, f"set_{name}"):
getattr(self, f"set_{name}")(value)
def set_mode(self, value: Union[str, Mode]):
if hasattr(self.app, "error_handler"):
self.app.error_handler.debug = self.app.debug
if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG)
@property
def is_debug(self):
return self.mode is Mode.DEBUG

0
sanic/cli/__init__.py Normal file
View File

189
sanic/cli/app.py Normal file
View File

@ -0,0 +1,189 @@
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DESCRIPTION = indent(
f"""
{get_logo(True)}
To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance:
$ sanic path.to.server:app
Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory
Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple
""",
prefix=" ",
)
def __init__(self) -> None:
width = shutil.get_terminal_size().columns
self.parser = SanicArgumentParser(
prog="sanic",
description=self.DESCRIPTION,
formatter_class=lambda prog: RawTextHelpFormatter(
prog,
max_help_position=36 if width > 96 else 24,
indent_increment=4,
width=None,
),
)
self.parser._positionals.title = "Required\n========\n Positional"
self.parser._optionals.title = "Optional\n========\n General"
self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: List[Any] = []
def attach(self):
for group in Group._registry:
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
try:
app = self._get_app()
kwargs = self._build_run_kwargs()
app.run(**kwargs)
except ValueError:
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
bool(self.args.cert) != bool(self.args.key)
# new and old style self.args used together
or self.args.tls
and self.args.cert
# strict host checking without certs would always fail
or self.args.tlshost
and not self.args.tls
and not self.args.cert
):
self.parser.print_usage(sys.stderr)
message = (
"TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
error_logger.error(message)
sys.exit(1)
def _get_app(self):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
return app
def _build_run_kwargs(self):
ssl: Union[None, dict, str, list] = []
if self.args.tlshost:
ssl.append(None)
if self.args.cert is not None or self.args.key is not None:
ssl.append(dict(cert=self.args.cert, key=self.args.key))
if self.args.tls:
ssl += self.args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
"motd": self.args.motd,
"noisy_exceptions": self.args.noisy_exceptions,
"port": self.args.port,
"ssl": ssl,
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
}
if self.args.auto_reload:
kwargs["auto_reload"] = True
if 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

237
sanic/cli/arguments.py Normal file
View File

@ -0,0 +1,237 @@
from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
class Group:
name: Optional[str]
container: Union[ArgumentParser, _ArgumentGroup]
_registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None:
Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser
if title:
self.container = self.parser.add_argument_group(title=f" {title}")
else:
self.container = self.parser
@classmethod
def create(cls, parser: ArgumentParser):
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
class GeneralGroup(Group):
name = None
def attach(self):
self.container.add_argument(
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
self.container.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help=(
"Run Sanic as a Simple Server, and serve the contents of "
"a directory\n(module arg should be a path)"
),
)
class SocketGroup(Group):
name = "Socket binding"
def attach(self):
self.container.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
class TLSGroup(Group):
name = "TLS certificate"
def attach(self):
self.container.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
self.container.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
self.container.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help=(
"TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose multiple "
"certificates"
),
)
self.container.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs",
)
class WorkerGroup(Group):
name = "Worker"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="Number of worker processes [default 1]",
)
group.add_argument(
"--fast",
dest="fast",
action="store_true",
help="Set the number of workers to max allowed",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
class DevelopmentGroup(Group):
name = "Development"
def attach(self):
self.container.add_argument(
"--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=(
"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(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help=(
"Watch source directory for file changes and reload on "
"changes"
),
)
self.container.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes",
)
class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--motd",
dest="motd",
default=True,
help="Show the startup display",
)
self.container.add_argument(
"-v",
"--verbosity",
action="count",
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
)
self.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="Output stack traces for all exceptions",
)

View File

@ -18,6 +18,13 @@ except ImportError:
pass
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict):
"""
Container used for both request and response headers. It is a subclass of

View File

@ -6,20 +6,15 @@ from warnings import warn
from sanic.errorpages import check_error_format
from sanic.http import Http
from .utils import load_module_from_file_location, str_to_bool
from sanic.utils import load_module_from_file_location, str_to_bool
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"ACCESS_LOG": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
@ -27,6 +22,8 @@ DEFAULT_CONFIG = {
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
@ -46,6 +43,7 @@ DEFAULT_CONFIG = {
class Config(dict):
ACCESS_LOG: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
@ -54,6 +52,8 @@ class Config(dict):
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
@ -79,7 +79,7 @@ class Config(dict):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self.LOGO = BASE_LOGO
self._LOGO = ""
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
@ -118,6 +118,17 @@ class Config(dict):
self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
elif attr == "LOGO":
self._LOGO = value
warn(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
DeprecationWarning,
)
@property
def LOGO(self):
return self._LOGO
def _configure_header_size(self):
Http.set_header_max_size(

View File

@ -1,8 +1,11 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
LOGGING_CONFIG_DEFAULTS = dict(
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
version=1,
disable_existing_loggers=False,
loggers={
@ -53,6 +56,14 @@ LOGGING_CONFIG_DEFAULTS = dict(
)
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root")
"""
General Sanic logger

View File

@ -1,6 +1,6 @@
from ssl import SSLObject
from types import SimpleNamespace
from typing import Optional
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol
@ -37,14 +37,14 @@ class ConnInfo:
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl = False
self.server_name = ""
self.cert = {}
self.cert: Dict[str, Any] = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = getattr(sslobj.context, "sanic", {})
self.cert = dict(getattr(sslobj.context, "sanic", {}))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return

View File

@ -6,9 +6,6 @@ import sys
from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files():
"""This iterates over all relevant Python files.
@ -56,7 +53,11 @@ def restart_with_reloader():
"""
return subprocess.Popen(
_get_args_for_reloading(),
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
env={
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
},
)
@ -91,11 +92,6 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try:
while True:
need_reload = False

View File

@ -760,9 +760,10 @@ def parse_multipart_form(body, boundary):
break
colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header(
form_line[colon_index + 2 :]
form_line[idx:]
)
if form_header_field == "content-disposition":

View File

@ -134,6 +134,7 @@ def serve(
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
@ -181,7 +182,6 @@ def serve(
else:
conn.abort()
loop.run_until_complete(app._server_event("shutdown", "after"))
remove_unix_socket(unix)
@ -249,7 +249,10 @@ def serve_multiple(server_settings, workers):
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(target=serve, kwargs=server_settings)
process = mp.Process(
target=serve,
kwargs=server_settings,
)
process.daemon = True
process.start()
processes.append(process)

View File

@ -113,7 +113,7 @@ class SignalRouter(BaseRouter):
if fail_not_found:
raise e
else:
if self.ctx.app.debug:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
error_logger.warning(str(e))
return None

View File

@ -124,7 +124,7 @@ class CertSelector(ssl.SSLContext):
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = getattr(ctx, "sanic", {}).get("names", [])
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
@ -161,7 +161,7 @@ def match_hostname(
"""Match names from CertSelector against a received hostname."""
# Local certs are considered trusted, so this can be less pedantic
# and thus faster than the deprecated ssl.match_hostname function is.
names = getattr(ctx, "sanic", {}).get("names", [])
names = dict(getattr(ctx, "sanic", {})).get("names", [])
hostname = hostname.lower()
for name in names:
if name.startswith("*."):

View File

@ -22,7 +22,9 @@ class OptionalDispatchEvent(BaseScheme):
raw_source = getsource(method)
src = dedent(raw_source)
tree = parse(src)
node = RemoveDispatch(self._registered_events).visit(tree)
node = RemoveDispatch(
self._registered_events, self.app.state.verbosity
).visit(tree)
compiled_src = compile(node, method.__name__, "exec")
exec_locals: Dict[str, Any] = {}
exec(compiled_src, module_globals, exec_locals) # nosec
@ -31,8 +33,9 @@ class OptionalDispatchEvent(BaseScheme):
class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events) -> None:
def __init__(self, registered_events, verbosity: int = 0) -> None:
self._registered_events = registered_events
self._verbosity = verbosity
def visit_Expr(self, node: Expr) -> Any:
call = node.value
@ -49,6 +52,7 @@ class RemoveDispatch(NodeTransformer):
if hasattr(event, "s"):
event_name = getattr(event, "value", event.s)
if self._not_registered(event_name):
if self._verbosity >= 2:
logger.debug(f"Disabling event: {event_name}")
return None
return node

View File

@ -108,7 +108,7 @@ tests_require = [
"black",
"isort>=5.0.0",
"bandit",
"mypy>=0.901",
"mypy>=0.901,<0.910",
"docutils",
"pygments",
"uvicorn<0.15.0",

View File

@ -2,10 +2,12 @@ import asyncio
import logging
import re
from email import message
from inspect import isawaitable
from os import environ
from unittest.mock import Mock, patch
import py
import pytest
import sanic
@ -438,7 +440,6 @@ def test_custom_context():
assert app.ctx == ctx
def test_uvloop_usage(app, monkeypatch):
@app.get("/test")
def handler(request):
@ -481,3 +482,8 @@ def test_uvloop_usage_with_create_server(app, monkeypatch):
)
loop.run_until_complete(asyncio_srv_coro)
use_uvloop.assert_called_once()
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)

View File

@ -8,7 +8,6 @@ import pytest
from sanic_routing import __version__ as __routing_version__
from sanic import __version__
from sanic.config import BASE_LOGO
def capture(command):
@ -19,13 +18,20 @@ def capture(command):
cwd=Path(__file__).parent,
)
try:
out, err = proc.communicate(timeout=0.5)
out, err = proc.communicate(timeout=1)
except subprocess.TimeoutExpired:
proc.kill()
out, err = proc.communicate()
return out, err, proc.returncode
def starting_line(lines):
for idx, line in enumerate(lines):
if line.strip().startswith(b"Sanic v"):
return idx
return 0
@pytest.mark.parametrize(
"appname",
(
@ -39,7 +45,7 @@ def test_server_run(appname):
command = ["sanic", appname]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
@ -68,24 +74,20 @@ def test_tls_options(cmd):
out, err, exitcode = capture(command)
assert exitcode != 1
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert firstline == b"Goin' Fast @ https://127.0.0.1:9999"
@pytest.mark.parametrize(
"cmd",
(
(
"--cert=certs/sanic.example/fullchain.pem",
),
("--cert=certs/sanic.example/fullchain.pem",),
(
"--cert=certs/sanic.example/fullchain.pem",
"--key=certs/sanic.example/privkey.pem",
"--tls=certs/localhost/",
),
(
"--tls-strict-host",
),
("--tls-strict-host",),
),
)
def test_tls_wrong_options(cmd):
@ -93,7 +95,9 @@ def test_tls_wrong_options(cmd):
out, err, exitcode = capture(command)
assert exitcode == 1
assert not out
errmsg = err.decode().split("sanic: error: ")[1].split("\n")[0]
lines = err.decode().split("\n")
errmsg = lines[8]
assert errmsg == "TLS certificates must be specified by either of:"
@ -108,7 +112,7 @@ def test_host_port_localhost(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://localhost:9999"
@ -125,7 +129,7 @@ def test_host_port_ipv4(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.127:9999"
@ -142,7 +146,7 @@ def test_host_port_ipv6_any(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::]:9999"
@ -159,7 +163,7 @@ def test_host_port_ipv6_loopback(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
firstline = lines[starting_line(lines) + 1]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://[::1]:9999"
@ -181,9 +185,13 @@ def test_num_workers(num, cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
worker_lines = [line for line in lines if b"worker" in line]
worker_lines = [
line
for line in lines
if b"Starting worker" in line or b"Stopping worker" in line
]
assert exitcode != 1
assert len(worker_lines) == num * 2
assert len(worker_lines) == num * 2, f"Lines found: {lines}"
@pytest.mark.parametrize("cmd", ("--debug", "-d"))
@ -192,10 +200,9 @@ def test_debug(cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert (b"\n".join(lines[:6])).decode("utf-8") == BASE_LOGO
assert info["debug"] is True
assert info["auto_reload"] is True
@ -206,7 +213,7 @@ def test_auto_reload(cmd):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 9]
info = json.loads(app_info)
assert info["debug"] is False
@ -221,7 +228,7 @@ def test_access_logs(cmd, expected):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["access_log"] is expected
@ -248,7 +255,7 @@ def test_noisy_exceptions(cmd, expected):
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[26]
app_info = lines[starting_line(lines) + 8]
info = json.loads(app_info)
assert info["noisy_exceptions"] is expected

View File

@ -1,4 +1,5 @@
from contextlib import contextmanager
from email import message
from os import environ
from pathlib import Path
from tempfile import TemporaryDirectory
@ -350,3 +351,12 @@ 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):
message = (
"Setting the config.LOGO is deprecated and will no longer be "
"supported starting in v22.6."
)
with pytest.warns(DeprecationWarning, match=message):
app.config.LOGO = "My Custom Logo"

View File

@ -4,7 +4,6 @@ import warnings
import pytest
from bs4 import BeautifulSoup
from websockets.version import version as websockets_version
from sanic import Sanic
from sanic.exceptions import (
@ -260,10 +259,8 @@ def test_exception_in_ws_logged(caplog):
raise Exception("...")
with caplog.at_level(logging.INFO):
req, _ = app.test_client.websocket("/feed")
app.test_client.websocket("/feed")
assert (
"sanic.error",
logging.ERROR,
"Exception occurred while handling uri: %s" % repr(req.url),
) in caplog.record_tuples
error_logs = [r for r in caplog.record_tuples if r[0] == "sanic.error"]
assert error_logs[1][1] == logging.ERROR
assert "Exception occurred while handling uri:" in error_logs[1][2]

View File

@ -1,9 +1,10 @@
import asyncio
import logging
import pytest
from unittest.mock import Mock
import pytest
from bs4 import BeautifulSoup
from sanic import Sanic, handlers
@ -220,9 +221,11 @@ def test_single_arg_exception_handler_notice(exception_handler_app, caplog):
with caplog.at_level(logging.WARNING):
_, response = exception_handler_app.test_client.get("/1")
assert (
"sanic.error",
logging.WARNING,
for record in caplog.records:
if record.message.startswith("You are"):
break
assert record.message == (
"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, "

View File

@ -38,9 +38,9 @@ def test_no_exceptions_when_cancel_pending_request(app, caplog):
counter = Counter([r[1] for r in caplog.record_tuples])
assert counter[logging.INFO] == 5
assert counter[logging.INFO] == 11
assert logging.ERROR not in counter
assert (
caplog.record_tuples[3][2]
caplog.record_tuples[9][2]
== "Request: GET http://127.0.0.1:8000/ stopped. Transport is closed."
)

View File

@ -1,42 +1,38 @@
import asyncio
import logging
import os
import sys
from sanic_testing.testing import PORT
from unittest.mock import patch
from sanic.config import BASE_LOGO
import pytest
from sanic.application.logo import (
BASE_LOGO,
COLOR_LOGO,
FULL_COLOR_LOGO,
get_logo,
)
def test_logo_base(app, run_startup):
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == BASE_LOGO
@pytest.mark.parametrize(
"tty,full,expected",
(
(True, False, COLOR_LOGO),
(True, True, FULL_COLOR_LOGO),
(False, False, BASE_LOGO),
(False, True, BASE_LOGO),
),
)
def test_get_logo_returns_expected_logo(tty, full, expected):
with patch("sys.stdout.isatty") as isatty:
isatty.return_value = tty
logo = get_logo(full=full)
assert logo is expected
def test_logo_false(app, caplog, run_startup):
app.config.LOGO = False
logs = run_startup(app)
banner, port = logs[0][2].rsplit(":", 1)
assert logs[0][1] == logging.INFO
assert banner == "Goin' Fast @ http://127.0.0.1"
assert int(port) > 0
def test_logo_true(app, run_startup):
app.config.LOGO = True
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == BASE_LOGO
def test_logo_custom(app, run_startup):
app.config.LOGO = "My Custom Logo"
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == "My Custom Logo"
def test_get_logo_returns_no_colors_on_apple_terminal():
with patch("sys.stdout.isatty") as isatty:
isatty.return_value = False
sys.platform = "darwin"
os.environ["TERM_PROGRAM"] = "Apple_Terminal"
logo = get_logo()
assert "\033" not in logo

85
tests/test_motd.py Normal file
View File

@ -0,0 +1,85 @@
import logging
import platform
from unittest.mock import Mock
from sanic import __version__
from sanic.application.logo import BASE_LOGO
from sanic.application.motd import MOTDTTY
def test_logo_base(app, run_startup):
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == BASE_LOGO
def test_logo_false(app, run_startup):
app.config.LOGO = False
logs = run_startup(app)
banner, port = logs[1][2].rsplit(":", 1)
assert logs[0][1] == logging.INFO
assert banner == "Goin' Fast @ http://127.0.0.1"
assert int(port) > 0
def test_logo_true(app, run_startup):
app.config.LOGO = True
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == BASE_LOGO
def test_logo_custom(app, run_startup):
app.config.LOGO = "My Custom Logo"
logs = run_startup(app)
assert logs[0][1] == logging.DEBUG
assert logs[0][2] == "My Custom Logo"
def test_motd_with_expected_info(app, run_startup):
logs = run_startup(app)
assert logs[1][2] == f"Sanic v{__version__}"
assert logs[3][2] == "mode: debug, single worker"
assert logs[4][2] == "server: sanic"
assert logs[5][2] == f"python: {platform.python_version()}"
assert logs[6][2] == f"platform: {platform.platform()}"
def test_motd_init():
_orig = MOTDTTY.set_variables
MOTDTTY.set_variables = Mock()
motd = MOTDTTY(None, "", {}, {})
motd.set_variables.assert_called_once()
MOTDTTY.set_variables = _orig
def test_motd_display(caplog):
motd = MOTDTTY(" foobar ", "", {"one": "1"}, {"two": "2"})
with caplog.at_level(logging.INFO):
motd.display()
version_line = f"Sanic v{__version__}".center(motd.centering_length)
assert (
"".join(caplog.messages)
== f"""
{version_line}
foobar one: 1
|
two: 2
"""
)

View File

@ -483,11 +483,12 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog):
with caplog.at_level(logging.INFO):
_, response = app.test_client.get("/static/non_existing_file.file")
counter = Counter([r[1] for r in caplog.record_tuples])
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
assert response.status == 404
assert counter[logging.INFO] == 5
assert counter[logging.ERROR] == 0
assert counter[("sanic.root", logging.INFO)] == 11
assert counter[("sanic.root", logging.ERROR)] == 0
assert counter[("sanic.error", logging.ERROR)] == 0
def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
@ -500,11 +501,12 @@ def test_no_stack_trace_on_not_found(app, static_file_directory, caplog):
with caplog.at_level(logging.INFO):
_, response = app.test_client.get("/static/non_existing_file.file")
counter = Counter([r[1] for r in caplog.record_tuples])
counter = Counter([(r[0], r[1]) for r in caplog.record_tuples])
assert response.status == 404
assert counter[logging.INFO] == 5
assert logging.ERROR not in counter
assert counter[("sanic.root", logging.INFO)] == 11
assert counter[("sanic.root", logging.ERROR)] == 0
assert counter[("sanic.error", logging.ERROR)] == 0
assert response.text == "No file: /static/non_existing_file.file"

View File

@ -1,5 +1,7 @@
import logging
import pytest
from sanic.signals import RESERVED_NAMESPACES
from sanic.touchup import TouchUp
@ -8,14 +10,21 @@ def test_touchup_methods(app):
assert len(TouchUp._registry) == 9
async def test_ode_removes_dispatch_events(app, caplog):
@pytest.mark.parametrize(
"verbosity,result", ((0, False), (1, False), (2, True), (3, True))
)
async def test_ode_removes_dispatch_events(app, caplog, verbosity, result):
with caplog.at_level(logging.DEBUG, logger="sanic.root"):
app.state.verbosity = verbosity
await app._startup()
logs = caplog.record_tuples
for signal in RESERVED_NAMESPACES["http"]:
assert (
(
"sanic.root",
logging.DEBUG,
f"Disabling event: {signal}",
) in logs
)
in logs
) is result

View File

@ -191,7 +191,7 @@ async def test_zero_downtime():
async with httpx.AsyncClient(transport=transport) as client:
r = await client.get("http://localhost/sleep/0.1")
assert r.status_code == 200
assert r.text == f"Slept 0.1 seconds.\n"
assert r.text == "Slept 0.1 seconds.\n"
def spawn():
command = [
@ -238,6 +238,12 @@ async def test_zero_downtime():
for worker in processes:
worker.kill()
# Test for clean run and termination
return_codes = [worker.poll() for worker in processes]
# Removing last process which seems to be flappy
return_codes.pop()
assert len(processes) > 5
assert [worker.poll() for worker in processes] == len(processes) * [0]
assert not os.path.exists(SOCKPATH)
assert all(code == 0 for code in return_codes)
# Removing this check that seems to be flappy
# assert not os.path.exists(SOCKPATH)

View File

@ -15,7 +15,7 @@ from sanic.app import Sanic
from sanic.worker import GunicornWorker
@pytest.fixture(scope="module")
@pytest.fixture
def gunicorn_worker():
command = (
"gunicorn "
@ -24,12 +24,12 @@ def gunicorn_worker():
"examples.simple_server:app"
)
worker = subprocess.Popen(shlex.split(command))
time.sleep(3)
time.sleep(2)
yield
worker.kill()
@pytest.fixture(scope="module")
@pytest.fixture
def gunicorn_worker_with_access_logs():
command = (
"gunicorn "
@ -42,7 +42,7 @@ def gunicorn_worker_with_access_logs():
return worker
@pytest.fixture(scope="module")
@pytest.fixture
def gunicorn_worker_with_env_var():
command = (
'env SANIC_ACCESS_LOG="False" '
@ -69,7 +69,13 @@ def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var):
"""
with urllib.request.urlopen(f"http://localhost:{PORT + 2}/") as _:
gunicorn_worker_with_env_var.kill()
assert not gunicorn_worker_with_env_var.stdout.read()
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):