Simpler CLI targets (#2700)

Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com>
This commit is contained in:
Adam Hopkins 2023-03-21 20:50:25 +02:00 committed by GitHub
parent 932088e37e
commit 6e1c787e5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 83 deletions

View File

@ -24,17 +24,22 @@ class SanicCLI:
{get_logo(True)} {get_logo(True)}
To start running a Sanic application, provide a path to the module, where To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance: app is a Sanic() instance in the global scope:
$ sanic path.to.server:app $ sanic path.to.server:app
If the Sanic instance variable is called 'app', you can leave off the last
part, and only provide a path to the module where the instance is:
$ sanic path.to.server
Or, a path to a callable that returns a Sanic() instance: Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory $ sanic path.to.factory:create_app
Or, a path to a directory to run as a simple HTTP server: Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple $ sanic ./path/to/static
""", """,
prefix=" ", prefix=" ",
) )
@ -95,7 +100,7 @@ Or, a path to a directory to run as a simple HTTP server:
self.args = self.parser.parse_args(args=parse_args) self.args = self.parser.parse_args(args=parse_args)
self._precheck() self._precheck()
app_loader = AppLoader( app_loader = AppLoader(
self.args.module, self.args.factory, self.args.simple, self.args self.args.target, self.args.factory, self.args.simple, self.args
) )
if self.args.inspect or self.args.inspect_raw or self.args.trigger: if self.args.inspect or self.args.inspect_raw or self.args.trigger:
@ -120,9 +125,9 @@ Or, a path to a directory to run as a simple HTTP server:
def _inspector_legacy(self, app_loader: AppLoader): def _inspector_legacy(self, app_loader: AppLoader):
host = port = None host = port = None
module = cast(str, self.args.module) target = cast(str, self.args.target)
if ":" in module: if ":" in target:
maybe_host, maybe_port = module.rsplit(":", 1) maybe_host, maybe_port = target.rsplit(":", 1)
if maybe_port.isnumeric(): if maybe_port.isnumeric():
host, port = maybe_host, int(maybe_port) host, port = maybe_host, int(maybe_port)
if not host: if not host:

View File

@ -57,11 +57,15 @@ class GeneralGroup(Group):
) )
self.container.add_argument( self.container.add_argument(
"module", "target",
help=( help=(
"Path to your Sanic app. Example: path.to.server:app\n" "Path to your Sanic app instance.\n"
"If running a Simple Server, path to directory to serve. " "\tExample: path.to.server:app\n"
"Example: ./\n" "If running a Simple Server, path to directory to serve.\n"
"\tExample: ./\n"
"Additionally, this can be a path to a factory function\n"
"that returns a Sanic app instance.\n"
"\tExample: path.to.server:create_app\n"
), ),
) )

View File

@ -3,7 +3,9 @@ from __future__ import annotations
import os import os
import sys import sys
from contextlib import suppress
from importlib import import_module from importlib import import_module
from inspect import isfunction
from pathlib import Path from pathlib import Path
from ssl import SSLContext from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
@ -15,6 +17,8 @@ from sanic.http.tls.creators import MkcertCreator, TrustmeCreator
if TYPE_CHECKING: if TYPE_CHECKING:
from sanic import Sanic as SanicApp from sanic import Sanic as SanicApp
DEFAULT_APP_NAME = "app"
class AppLoader: class AppLoader:
def __init__( def __init__(
@ -36,7 +40,11 @@ class AppLoader:
if module_input: if module_input:
delimiter = ":" if ":" in module_input else "." delimiter = ":" if ":" in module_input else "."
if module_input.count(delimiter): if (
delimiter in module_input
and "\\" not in module_input
and "/" not in module_input
):
module_name, app_name = module_input.rsplit(delimiter, 1) module_name, app_name = module_input.rsplit(delimiter, 1)
self.module_name = module_name self.module_name = module_name
self.app_name = app_name self.app_name = app_name
@ -55,21 +63,30 @@ class AppLoader:
from sanic.app import Sanic from sanic.app import Sanic
from sanic.simple import create_simple_server from sanic.simple import create_simple_server
if self.as_simple: maybe_path = Path(self.module_input)
path = Path(self.module_input) if self.as_simple or (
app = create_simple_server(path) maybe_path.is_dir()
and ("\\" in self.module_input or "/" in self.module_input)
):
app = create_simple_server(maybe_path)
else: else:
if self.module_name == "" and os.path.isdir(self.module_input): implied_app_name = False
raise ValueError( if not self.module_name and not self.app_name:
"App not found.\n" self.module_name = self.module_input
" Please use --simple if you are passing a " self.app_name = DEFAULT_APP_NAME
"directory to sanic.\n" implied_app_name = True
f" eg. sanic {self.module_input} --simple"
)
module = import_module(self.module_name) module = import_module(self.module_name)
app = getattr(module, self.app_name, None) app = getattr(module, self.app_name, None)
if self.as_factory: if not app and implied_app_name:
raise ValueError(
"Looks like you only supplied a module name. Sanic "
"tried to locate an application instance named "
f"{self.module_name}:app, but was unable to locate "
"an application instance. Please provide a path "
"to a global instance of Sanic(), or a callable that "
"will return a Sanic() application instance."
)
if self.as_factory or isfunction(app):
try: try:
app = app(self.args) app = app(self.args)
except TypeError: except TypeError:
@ -80,21 +97,18 @@ class AppLoader:
if ( if (
not isinstance(app, Sanic) not isinstance(app, Sanic)
and self.args and self.args
and hasattr(self.args, "module") and hasattr(self.args, "target")
): ):
if callable(app): with suppress(ModuleNotFoundError):
solution = f"sanic {self.args.module} --factory" maybe_module = import_module(self.module_input)
raise ValueError( app = getattr(maybe_module, "app", None)
"Module is not a Sanic app, it is a " if not app:
f"{app_type_name}\n" message = (
" If this callable returns a " "Module is not a Sanic app, "
f"Sanic instance try: \n{solution}" f"it is a {app_type_name}\n"
f" Perhaps you meant {self.args.target}:app?"
) )
raise ValueError(message)
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
)
return app return app

View File

@ -49,6 +49,6 @@ def create_app_with_args(args):
try: try:
logger.info(f"foo={args.foo}") logger.info(f"foo={args.foo}")
except AttributeError: except AttributeError:
logger.info(f"module={args.module}") logger.info(f"target={args.target}")
return app return app

View File

@ -43,8 +43,10 @@ def read_app_info(lines: List[str]):
"appname,extra", "appname,extra",
( (
("fake.server.app", None), ("fake.server.app", None),
("fake.server", None),
("fake.server:create_app", "--factory"), ("fake.server:create_app", "--factory"),
("fake.server.create_app()", None), ("fake.server.create_app()", None),
("fake.server.create_app", None),
), ),
) )
def test_server_run( def test_server_run(
@ -60,14 +62,17 @@ def test_server_run(
assert "Goin' Fast @ http://127.0.0.1:8000" in lines assert "Goin' Fast @ http://127.0.0.1:8000" in lines
def test_server_run_factory_with_args(caplog): @pytest.mark.parametrize(
command = [ "command",
"fake.server.create_app_with_args", (
"--factory", ["fake.server.create_app_with_args", "--factory"],
] ["fake.server.create_app_with_args"],
),
)
def test_server_run_factory_with_args(caplog, command):
lines = capture(command, caplog) lines = capture(command, caplog)
assert "module=fake.server.create_app_with_args" in lines assert "target=fake.server.create_app_with_args" in lines
def test_server_run_factory_with_args_arbitrary(caplog): def test_server_run_factory_with_args_arbitrary(caplog):
@ -81,25 +86,6 @@ def test_server_run_factory_with_args_arbitrary(caplog):
assert "foo=bar" in lines assert "foo=bar" in lines
def test_error_with_function_as_instance_without_factory_arg(caplog):
command = ["fake.server.create_app"]
lines = capture(command, caplog)
assert (
"Failed to run app: Module is not a Sanic app, it is a function\n "
"If this callable returns a Sanic instance try: \n"
"sanic fake.server.create_app --factory"
) in lines
def test_error_with_path_as_instance_without_simple_arg(caplog):
command = ["./fake/"]
lines = capture(command, caplog)
assert (
"Failed to run app: App not found.\n Please use --simple if you "
"are passing a directory to sanic.\n eg. sanic ./fake/ --simple"
) in lines
@pytest.mark.parametrize( @pytest.mark.parametrize(
"cmd", "cmd",
( (

View File

@ -52,34 +52,23 @@ def test_cwd_in_path():
def test_input_is_dir(): def test_input_is_dir():
loader = AppLoader(str(STATIC)) loader = AppLoader(str(STATIC))
message = ( app = loader.load()
"App not found.\n Please use --simple if you are passing a " assert isinstance(app, Sanic)
f"directory to sanic.\n eg. sanic {str(STATIC)} --simple"
)
with pytest.raises(ValueError, match=message):
loader.load()
def test_input_is_factory(): def test_input_is_factory():
ns = SimpleNamespace(module="foo") ns = SimpleNamespace(target="foo")
loader = AppLoader("tests.fake.server:create_app", args=ns) loader = AppLoader("tests.fake.server:create_app", args=ns)
message = ( app = loader.load()
"Module is not a Sanic app, it is a function\n If this callable " assert isinstance(app, Sanic)
"returns a Sanic instance try: \nsanic foo --factory"
)
with pytest.raises(ValueError, match=message):
loader.load()
def test_input_is_module(): def test_input_is_module():
ns = SimpleNamespace(module="foo") ns = SimpleNamespace(target="foo")
loader = AppLoader("tests.fake.server", args=ns) loader = AppLoader("tests.fake.server", args=ns)
message = (
"Module is not a Sanic app, it is a module\n " app = loader.load()
"Perhaps you meant foo:app?" assert isinstance(app, Sanic)
)
with pytest.raises(ValueError, match=message):
loader.load()
@pytest.mark.parametrize("creator", ("mkcert", "trustme")) @pytest.mark.parametrize("creator", ("mkcert", "trustme"))