diff --git a/docs/source/run.rst b/docs/source/run.rst index 6e4be8ada..108956b83 100644 --- a/docs/source/run.rst +++ b/docs/source/run.rst @@ -21,12 +21,15 @@ gunicorn Basic usage:: - $ gunicorn [OPTIONS] APP_MODULE + $ gunicorn [OPTIONS] [WSGI_APP] -Where ``APP_MODULE`` is of the pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. The +Where ``WSGI_APP`` is of the pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. The module name can be a full dotted path. The variable name refers to a WSGI callable that should be found in the specified module. +.. versionchanged:: 20.1.0 + ``WSGI_APP`` is optional if it is defined in a :ref:`config` file. + Example with the test app: .. code-block:: python diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 8ebe04841..2da71c73e 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -42,6 +42,17 @@ application specific configuration. Loading the config from a Python module requires the ``python:`` prefix. +.. _wsgi-app: + +wsgi_app +~~~~~~~~ + +* ``None`` + +A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. + +.. versionadded:: 20.1.0 + Debugging --------- diff --git a/gunicorn/app/wsgiapp.py b/gunicorn/app/wsgiapp.py index c8501e5fe..36cfba9d8 100644 --- a/gunicorn/app/wsgiapp.py +++ b/gunicorn/app/wsgiapp.py @@ -12,6 +12,8 @@ class WSGIApplication(Application): def init(self, parser, opts, args): + self.app_uri = None + if opts.paste: from .pasterapp import has_logging_config @@ -29,11 +31,18 @@ def init(self, parser, opts, args): return - if not args: - parser.error("No application module specified.") + if len(args) > 0: + self.cfg.set("default_proc_name", args[0]) + self.app_uri = args[0] + + def load_config(self): + super().load_config() - self.cfg.set("default_proc_name", args[0]) - self.app_uri = args[0] + if self.app_uri is None: + if self.cfg.wsgi_app is not None: + self.app_uri = self.cfg.wsgi_app + else: + raise ConfigError("No application module specified.") def load_wsgiapp(self): return util.import_app(self.app_uri) diff --git a/gunicorn/config.py b/gunicorn/config.py index 17e492799..0a0c9a3ef 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -560,6 +560,17 @@ class ConfigFile(Setting): prefix. """ +class WSGIApp(Setting): + name = "wsgi_app" + section = "Config File" + meta = "STRING" + validator = validate_string + default = None + desc = """\ + A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. + + .. versionadded:: 20.1.0 + """ class Bind(Setting): name = "bind" diff --git a/tests/config/test_cfg_with_wsgi_app.py b/tests/config/test_cfg_with_wsgi_app.py new file mode 100644 index 000000000..980a92b52 --- /dev/null +++ b/tests/config/test_cfg_with_wsgi_app.py @@ -0,0 +1 @@ +wsgi_app = "app1:app1" diff --git a/tests/test_config.py b/tests/test_config.py index c176b66b9..92cb73c37 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -11,6 +11,7 @@ from gunicorn import config from gunicorn.app.base import Application +from gunicorn.app.wsgiapp import WSGIApplication from gunicorn.errors import ConfigError from gunicorn.workers.sync import SyncWorker from gunicorn import glogging @@ -25,6 +26,8 @@ def cfg_file(): return os.path.join(dirname, "config", "test_cfg.py") def alt_cfg_file(): return os.path.join(dirname, "config", "test_cfg_alt.py") +def cfg_file_with_wsgi_app(): + return os.path.join(dirname, "config", "test_cfg_with_wsgi_app.py") def paster_ini(): return os.path.join(dirname, "..", "examples", "frameworks", "pylonstest", "nose.ini") @@ -52,6 +55,14 @@ def load(self): pass +class WSGIApp(WSGIApplication): + def __init__(self): + super().__init__("no_usage", prog="gunicorn_test") + + def load(self): + pass + + def test_defaults(): c = config.Config() for s in config.KNOWN_SETTINGS: @@ -353,6 +364,7 @@ def test_invalid_enviroment_variables_config(monkeypatch, capsys): _, err = capsys.readouterr() assert "error: unrecognized arguments: --foo" in err + def test_cli_overrides_enviroment_variables_module(monkeypatch): monkeypatch.setenv("GUNICORN_CMD_ARGS", "--workers=4") with AltArgs(["prog_name", "-c", cfg_file(), "--workers", "3"]): @@ -360,6 +372,34 @@ def test_cli_overrides_enviroment_variables_module(monkeypatch): assert app.cfg.workers == 3 +@pytest.mark.parametrize("options, expected", [ + (["app:app"], 'app:app'), + (["-c", cfg_file(), "app:app"], 'app:app'), + (["-c", cfg_file_with_wsgi_app(), "app:app"], 'app:app'), + (["-c", cfg_file_with_wsgi_app()], 'app1:app1'), +]) +def test_wsgi_app_config(options, expected): + cmdline = ["prog_name"] + cmdline.extend(options) + with AltArgs(cmdline): + app = WSGIApp() + assert app.app_uri == expected + + +@pytest.mark.parametrize("options", [ + ([]), + (["-c", cfg_file()]), +]) +def test_non_wsgi_app(options, capsys): + cmdline = ["prog_name"] + cmdline.extend(options) + with AltArgs(cmdline): + with pytest.raises(SystemExit): + WSGIApp() + _, err = capsys.readouterr() + assert "Error: No application module specified." in err + + @pytest.mark.parametrize("options, expected", [ (["myapp:app"], False), (["--reload", "myapp:app"], True),