Simpler CLI targets (#2700)
Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com>
This commit is contained in:
parent
932088e37e
commit
6e1c787e5d
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
(
|
(
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user