Restructure of CLI and application state (#2295)
* Initial work on restructure of application state * Updated MOTD with more flexible input and add basic version * Remove unnecessary type ignores * Add wrapping and smarter output per process type * Add support for ASGI MOTD * Add Windows color support ernable * Refactor __main__ into submodule * Renest arguments * Passing unit tests * Passing unit tests * Typing * Fix num worker test * Add context to assert failure * Add some type annotations * Some linting * Line aware searching in test * Test abstractions * Fix some flappy tests * Bump up timeout on CLI tests * Change test for no access logs on gunicornworker * Add some basic test converage * Some new tests, and disallow workers and fast on app.run
This commit is contained in:
0
sanic/cli/__init__.py
Normal file
0
sanic/cli/__init__.py
Normal file
189
sanic/cli/app.py
Normal file
189
sanic/cli/app.py
Normal 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
237
sanic/cli/arguments.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user