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:
parent
3a6fac7d59
commit
72a745bfd5
|
@ -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:
|
||||||
error_logger.error(
|
if module_name.startswith(e.name):
|
||||||
f"No module named {e.name} found.\n"
|
error_logger.error(
|
||||||
f" Example File: project/sanic_server.py -> app\n"
|
f"No module named {e.name} found.\n"
|
||||||
f" Example Module: project.sanic_server.app"
|
" Example File: project/sanic_server.py -> app\n"
|
||||||
)
|
" 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")
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
32
tests/fake/server.py
Normal 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
125
tests/test_cli.py
Normal 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")
|
Loading…
Reference in New Issue
Block a user