diff --git a/sanic/cli/app.py b/sanic/cli/app.py index 30d6abe8..7f71d45d 100644 --- a/sanic/cli/app.py +++ b/sanic/cli/app.py @@ -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=" ", ) @@ -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._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: @@ -120,9 +125,9 @@ Or, a path to a directory to run as a simple HTTP server: 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: diff --git a/sanic/cli/arguments.py b/sanic/cli/arguments.py index e1fe905a..e7fadb1d 100644 --- a/sanic/cli/arguments.py +++ b/sanic/cli/arguments.py @@ -57,11 +57,15 @@ class GeneralGroup(Group): ) 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" ), ) diff --git a/sanic/worker/loader.py b/sanic/worker/loader.py index d29f4c68..3e33521a 100644 --- a/sanic/worker/loader.py +++ b/sanic/worker/loader.py @@ -3,7 +3,9 @@ from __future__ import annotations 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 @@ -15,6 +17,8 @@ from sanic.http.tls.creators import MkcertCreator, TrustmeCreator if TYPE_CHECKING: from sanic import Sanic as SanicApp +DEFAULT_APP_NAME = "app" + class AppLoader: def __init__( @@ -36,7 +40,11 @@ class AppLoader: 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 @@ -55,21 +63,30 @@ class AppLoader: 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: @@ -80,21 +97,18 @@ class AppLoader: 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 diff --git a/tests/fake/server.py b/tests/fake/server.py index e219b953..577b70bf 100644 --- a/tests/fake/server.py +++ b/tests/fake/server.py @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index cb3842e7..2e980a2c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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( @@ -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): @@ -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", ( diff --git a/tests/worker/test_loader.py b/tests/worker/test_loader.py index d0d04e9a..c70a2346 100644 --- a/tests/worker/test_loader.py +++ b/tests/worker/test_loader.py @@ -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"))