support app factory patten in CLI (#2157)

* support app factory patten in CLI

* Update sanic/__main__.py

* fix mypy errors

* Update mypy further

* Update sanic/utils.py

* Update sanic/utils.py

* support hypercorn/gunicorn style 'asgi.app:create_app()'

* add test for app factory
This commit is contained in:
Thomas Grainger 2021-06-09 10:05:56 +01:00 committed by GitHub
parent 141be0028d
commit 48f8b37b74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 33 additions and 4 deletions

View File

@ -75,6 +75,14 @@ def main():
action="store_true", action="store_true",
help="Watch source directory for file changes and reload on changes", help="Watch source directory for file changes and reload on changes",
) )
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable."
),
)
parser.add_argument( parser.add_argument(
"-v", "-v",
"--version", "--version",
@ -97,13 +105,20 @@ def main():
delimiter = ":" if ":" in args.module else "." delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1) module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name) module = import_module(module_name)
app = getattr(module, app_name, None) app = getattr(module, app_name, None)
app_name = type(app).__name__ if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic): if not isinstance(app, Sanic):
raise ValueError( raise ValueError(
f"Module is not a Sanic app, it is a {app_name}. " f"Module is not a Sanic app, it is a {app_type_name}. "
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:

View File

@ -105,6 +105,7 @@ def load_module_from_file_location(
_mod_spec = spec_from_file_location( _mod_spec = spec_from_file_location(
name, location, *args, **kwargs name, location, *args, **kwargs
) )
assert _mod_spec is not None # type assertion for mypy
module = module_from_spec(_mod_spec) module = module_from_spec(_mod_spec)
_mod_spec.loader.exec_module(module) # type: ignore _mod_spec.loader.exec_module(module) # type: ignore

View File

@ -30,3 +30,7 @@ async def app_info_dump(app: Sanic, _):
@app.after_server_start @app.after_server_start
async def shutdown(app: Sanic, _): async def shutdown(app: Sanic, _):
app.stop() app.stop()
def create_app():
return app

View File

@ -26,7 +26,15 @@ def capture(command):
return out, err, proc.returncode return out, err, proc.returncode
@pytest.mark.parametrize("appname", ("fake.server.app", "fake.server:app")) @pytest.mark.parametrize(
"appname",
(
"fake.server.app",
"fake.server:app",
"fake.server:create_app()",
"fake.server.create_app()",
)
)
def test_server_run(appname): def test_server_run(appname):
command = ["sanic", appname] command = ["sanic", appname]
out, err, exitcode = capture(command) out, err, exitcode = capture(command)

View File

@ -39,7 +39,8 @@ commands =
[testenv:type-checking] [testenv:type-checking]
deps = deps =
mypy mypy>=0.901
types-ujson
commands = commands =
mypy sanic mypy sanic