Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simpler CLI targets #2700

Merged
merged 18 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions sanic/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,22 @@ class SanicCLI:
{get_logo(True)}

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

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:

$ 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:

$ sanic ./path/to/static --simple
$ sanic ./path/to/static
""",
prefix=" ",
)
Expand Down Expand Up @@ -95,7 +100,7 @@ def run(self, parse_args=None):
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
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:
Expand All @@ -120,9 +125,9 @@ def run(self, parse_args=None):

def _inspector_legacy(self, app_loader: AppLoader):
host = port = None
module = cast(str, self.args.module)
if ":" in module:
maybe_host, maybe_port = module.rsplit(":", 1)
target = cast(str, self.args.target)
if ":" in target:
maybe_host, maybe_port = target.rsplit(":", 1)
if maybe_port.isnumeric():
host, port = maybe_host, int(maybe_port)
if not host:
Expand Down
12 changes: 8 additions & 4 deletions sanic/cli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,15 @@ def attach(self):
)

self.container.add_argument(
"module",
"target",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
"Path to your Sanic app instance.\n"
"\tExample: path.to.server:app\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"
),
)

Expand Down
66 changes: 40 additions & 26 deletions sanic/worker/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import os
import sys

from contextlib import suppress
from importlib import import_module
from inspect import isfunction
from pathlib import Path
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Union, cast
Expand All @@ -15,6 +17,8 @@
if TYPE_CHECKING:
from sanic import Sanic as SanicApp

DEFAULT_APP_NAME = "app"


class AppLoader:
def __init__(
Expand All @@ -36,7 +40,11 @@ def __init__(

if module_input:
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)
self.module_name = module_name
self.app_name = app_name
Expand All @@ -55,21 +63,30 @@ def load(self) -> SanicApp:
from sanic.app import Sanic
from sanic.simple import create_simple_server

if self.as_simple:
path = Path(self.module_input)
app = create_simple_server(path)
maybe_path = Path(self.module_input)
if self.as_simple or (
maybe_path.is_dir()
and ("\\" in self.module_input or "/" in self.module_input)
):
app = create_simple_server(maybe_path)
else:
if self.module_name == "" and os.path.isdir(self.module_input):
raise ValueError(
"App not found.\n"
" Please use --simple if you are passing a "
"directory to sanic.\n"
f" eg. sanic {self.module_input} --simple"
)

implied_app_name = False
if not self.module_name and not self.app_name:
self.module_name = self.module_input
self.app_name = DEFAULT_APP_NAME
implied_app_name = True
module = import_module(self.module_name)
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:
app = app(self.args)
except TypeError:
Expand All @@ -80,21 +97,18 @@ def load(self) -> SanicApp:
if (
not isinstance(app, Sanic)
and self.args
and hasattr(self.args, "module")
and hasattr(self.args, "target")
):
if callable(app):
solution = f"sanic {self.args.module} --factory"
raise ValueError(
"Module is not a Sanic app, it is a "
f"{app_type_name}\n"
" If this callable returns a "
f"Sanic instance try: \n{solution}"
with suppress(ModuleNotFoundError):
maybe_module = import_module(self.module_input)
app = getattr(maybe_module, "app", None)
if not app:
message = (
"Module is not a Sanic app, "
f"it is a {app_type_name}\n"
f" Perhaps you meant {self.args.target}:app?"
)

raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}:app?"
)
raise ValueError(message)
return app


Expand Down
2 changes: 1 addition & 1 deletion tests/fake/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ def create_app_with_args(args):
try:
logger.info(f"foo={args.foo}")
except AttributeError:
logger.info(f"module={args.module}")
logger.info(f"target={args.target}")

return app
36 changes: 11 additions & 25 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ def read_app_info(lines: List[str]):
"appname,extra",
(
("fake.server.app", None),
("fake.server", None),
("fake.server:create_app", "--factory"),
("fake.server.create_app()", None),
("fake.server.create_app", None),
),
)
def test_server_run(
Expand All @@ -60,14 +62,17 @@ def test_server_run(
assert "Goin' Fast @ http://127.0.0.1:8000" in lines


def test_server_run_factory_with_args(caplog):
command = [
"fake.server.create_app_with_args",
"--factory",
]
@pytest.mark.parametrize(
"command",
(
["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)

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):
Expand All @@ -81,25 +86,6 @@ def test_server_run_factory_with_args_arbitrary(caplog):
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(
"cmd",
(
Expand Down
29 changes: 9 additions & 20 deletions tests/worker/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,23 @@ def test_cwd_in_path():

def test_input_is_dir():
loader = AppLoader(str(STATIC))
message = (
"App not found.\n Please use --simple if you are passing a "
f"directory to sanic.\n eg. sanic {str(STATIC)} --simple"
)
with pytest.raises(ValueError, match=message):
loader.load()
app = loader.load()
assert isinstance(app, Sanic)


def test_input_is_factory():
ns = SimpleNamespace(module="foo")
ns = SimpleNamespace(target="foo")
loader = AppLoader("tests.fake.server:create_app", args=ns)
message = (
"Module is not a Sanic app, it is a function\n If this callable "
"returns a Sanic instance try: \nsanic foo --factory"
)
with pytest.raises(ValueError, match=message):
loader.load()
app = loader.load()
assert isinstance(app, Sanic)


def test_input_is_module():
ns = SimpleNamespace(module="foo")
ns = SimpleNamespace(target="foo")
loader = AppLoader("tests.fake.server", args=ns)
message = (
"Module is not a Sanic app, it is a module\n "
"Perhaps you meant foo:app?"
)
with pytest.raises(ValueError, match=message):
loader.load()

app = loader.load()
assert isinstance(app, Sanic)


@pytest.mark.parametrize("creator", ("mkcert", "trustme"))
Expand Down