Small improvements to CLI experience (#2136)

* Small improvements to CLI experience

* Add tests

* Add test server for cli testing

* Add LOGO logging to reloader and some additional context to logging debug

* Cleanup tests
This commit is contained in:
Adam Hopkins 2021-05-20 15:35:19 +03:00 committed by GitHub
parent 3a6fac7d59
commit 72a745bfd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 211 additions and 28 deletions

View File

@ -5,6 +5,8 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter
from importlib import import_module from importlib import import_module
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__ from sanic import __version__
from sanic.app import Sanic from sanic.app import Sanic
from sanic.config import BASE_LOGO from sanic.config import BASE_LOGO
@ -65,15 +67,22 @@ def main():
default=1, default=1,
help="number of worker processes [default 1]", help="number of worker processes [default 1]",
) )
parser.add_argument("--debug", dest="debug", action="store_true") parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_bool_arguments( parser.add_argument(
"--access-logs", dest="access_log", help="display access logs" "-r",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
) )
parser.add_argument( parser.add_argument(
"-v", "-v",
"--version", "--version",
action="version", action="version",
version=f"Sanic {__version__}", version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
) )
parser.add_argument( parser.add_argument(
"module", help="path to your Sanic app. Example: path.to.server:app" "module", help="path to your Sanic app. Example: path.to.server:app"
@ -85,12 +94,8 @@ def main():
if module_path not in sys.path: if module_path not in sys.path:
sys.path.append(module_path) sys.path.append(module_path)
if ":" in args.module: delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(":", 1) module_name, app_name = args.module.rsplit(delimiter, 1)
else:
module_parts = args.module.split(".")
module_name = ".".join(module_parts[:-1])
app_name = module_parts[-1]
module = import_module(module_name) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
@ -102,28 +107,34 @@ def main():
f"Perhaps you meant {args.module}.app?" f"Perhaps you meant {args.module}.app?"
) )
if args.cert is not None or args.key is not None: if args.cert is not None or args.key is not None:
ssl = { ssl: Optional[Dict[str, Any]] = {
"cert": args.cert, "cert": args.cert,
"key": args.key, "key": args.key,
} # type: Optional[Dict[str, Any]] }
else: else:
ssl = None ssl = None
app.run( kwargs = {
host=args.host, "host": args.host,
port=args.port, "port": args.port,
unix=args.unix, "unix": args.unix,
workers=args.workers, "workers": args.workers,
debug=args.debug, "debug": args.debug,
access_log=args.access_log, "access_log": args.access_log,
ssl=ssl, "ssl": ssl,
) }
if args.auto_reload:
kwargs["auto_reload"] = True
app.run(**kwargs)
except ImportError as e: except ImportError as e:
if module_name.startswith(e.name):
error_logger.error( error_logger.error(
f"No module named {e.name} found.\n" f"No module named {e.name} found.\n"
f" Example File: project/sanic_server.py -> app\n" " Example File: project/sanic_server.py -> app\n"
f" Example Module: project.sanic_server.app" " Example Module: project.sanic_server.app"
) )
else:
raise e
except ValueError: except ValueError:
error_logger.exception("Failed to run app") error_logger.exception("Failed to run app")

View File

@ -90,6 +90,7 @@ class Sanic(BaseSanic):
"_future_signals", "_future_signals",
"_test_client", "_test_client",
"_test_manager", "_test_manager",
"auto_reload",
"asgi", "asgi",
"blueprints", "blueprints",
"config", "config",
@ -150,6 +151,7 @@ class Sanic(BaseSanic):
self._test_client = None self._test_client = None
self._test_manager = None self._test_manager = None
self.asgi = False self.asgi = False
self.auto_reload = False
self.blueprints: Dict[str, Blueprint] = {} self.blueprints: Dict[str, Blueprint] = {}
self.config = Config(load_env=load_env, env_prefix=env_prefix) self.config = Config(load_env=load_env, env_prefix=env_prefix)
self.configure_logging = configure_logging self.configure_logging = configure_logging
@ -876,8 +878,9 @@ class Sanic(BaseSanic):
) )
if auto_reload or auto_reload is None and debug: if auto_reload or auto_reload is None and debug:
self.auto_reload = True
if os.environ.get("SANIC_SERVER_RUNNING") != "true": if os.environ.get("SANIC_SERVER_RUNNING") != "true":
return reloader_helpers.watchdog(1.0) return reloader_helpers.watchdog(1.0, self)
if sock is None: if sock is None:
host, port = host or "127.0.0.1", port or 8000 host, port = host or "127.0.0.1", port or 8000
@ -1176,6 +1179,10 @@ class Sanic(BaseSanic):
else: else:
logger.info(f"Goin' Fast @ {proto}://{host}:{port}") logger.info(f"Goin' Fast @ {proto}://{host}:{port}")
debug_mode = "enabled" if self.debug else "disabled"
logger.debug("Sanic auto-reload: enabled")
logger.debug(f"Sanic debug mode: {debug_mode}")
return server_settings return server_settings
def _build_endpoint_name(self, *parts): def _build_endpoint_name(self, *parts):

View File

@ -5,6 +5,9 @@ import sys
from time import sleep from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files(): def _iter_module_files():
"""This iterates over all relevant Python files. """This iterates over all relevant Python files.
@ -56,7 +59,7 @@ def restart_with_reloader():
) )
def watchdog(sleep_interval): def watchdog(sleep_interval, app):
"""Watch project files, restart worker process if a change happened. """Watch project files, restart worker process if a change happened.
:param sleep_interval: interval in second. :param sleep_interval: interval in second.
@ -73,6 +76,11 @@ def watchdog(sleep_interval):
worker_process = restart_with_reloader() worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try: try:
while True: while True:
need_reload = False need_reload = False

32
tests/fake/server.py Normal file
View File

@ -0,0 +1,32 @@
import json
import logging
from sanic import Sanic, text
from sanic.log import LOGGING_CONFIG_DEFAULTS, logger
LOGGING_CONFIG = {**LOGGING_CONFIG_DEFAULTS}
LOGGING_CONFIG["formatters"]["generic"]["format"] = "%(message)s"
LOGGING_CONFIG["loggers"]["sanic.root"]["level"] = "DEBUG"
app = Sanic(__name__, log_config=LOGGING_CONFIG)
@app.get("/")
async def handler(request):
return text(request.ip)
@app.before_server_start
async def app_info_dump(app: Sanic, _):
app_data = {
"access_log": app.config.ACCESS_LOG,
"auto_reload": app.auto_reload,
"debug": app.debug,
}
logger.info(json.dumps(app_data))
@app.after_server_start
async def shutdown(app: Sanic, _):
app.stop()

125
tests/test_cli.py Normal file
View File

@ -0,0 +1,125 @@
import json
import subprocess
from pathlib import Path
import pytest
from sanic_routing import __version__ as __routing_version__
from sanic import __version__
from sanic.config import BASE_LOGO
def capture(command):
proc = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=Path(__file__).parent,
)
try:
out, err = proc.communicate(timeout=0.5)
except subprocess.TimeoutExpired:
proc.kill()
out, err = proc.communicate()
return out, err, proc.returncode
@pytest.mark.parametrize("appname", ("fake.server.app", "fake.server:app"))
def test_server_run(appname):
command = ["sanic", appname]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://127.0.0.1:8000"
@pytest.mark.parametrize(
"cmd",
(
("--host=localhost", "--port=9999"),
("-H", "localhost", "-p", "9999"),
),
)
def test_host_port(cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
firstline = lines[6]
assert exitcode != 1
assert firstline == b"Goin' Fast @ http://localhost:9999"
@pytest.mark.parametrize(
"num,cmd",
(
(1, (f"--workers={1}",)),
(2, (f"--workers={2}",)),
(4, (f"--workers={4}",)),
(1, ("-w", "1")),
(2, ("-w", "2")),
(4, ("-w", "4")),
),
)
def test_num_workers(num, cmd):
command = ["sanic", "fake.server.app", *cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
worker_lines = [line for line in lines if b"worker" in line]
assert exitcode != 1
assert len(worker_lines) == num * 2
@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")
app_info = 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
@pytest.mark.parametrize("cmd", ("--auto-reload", "-r"))
def test_auto_reload(cmd):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[9]
info = json.loads(app_info)
assert info["debug"] is False
assert info["auto_reload"] is True
@pytest.mark.parametrize(
"cmd,expected", (("--access-log", True), ("--no-access-log", False))
)
def test_access_logs(cmd, expected):
command = ["sanic", "fake.server.app", cmd]
out, err, exitcode = capture(command)
lines = out.split(b"\n")
app_info = lines[9]
info = json.loads(app_info)
assert info["access_log"] is expected
@pytest.mark.parametrize("cmd", ("--version", "-v"))
def test_version(cmd):
command = ["sanic", cmd]
out, err, exitcode = capture(command)
version_string = f"Sanic {__version__}; Routing {__routing_version__}\n"
assert out == version_string.encode("utf-8")