Repository: benoitc/gunicorn

Issue:
--reload flag changed semantics
I just upgraded from the 19.6.0 release to the latest commit c54426c1e4b26b30c5feee2cec4450656ae147a3

I previously ran gunicorn like `gunicorn --reload mymodule:app` during development. However now I get the following error:
```
$ gunicorn --reload mymodule:app
usage: gunicorn [OPTIONS] [APP_MODULE]
gunicorn: error: No application module specified.
```
If I put an option after the reload flag it works.
```
$ gunicorn --reload -w=1 mymodule:app
[2017-01-05 11:02:33 +0000] [102] [INFO] Starting gunicorn 19.6.0
[2017-01-05 11:02:33 +0000] [102] [INFO] Listening at:  (102)
[2017-01-05 11:02:33 +0000] [102] [INFO] Using worker: sync
[2017-01-05 11:02:33 +0000] [105] [INFO] Booting worker with pid: 105
```

Calling it like `gunicorn --reload auto mymodule:app` also works, but then the argument isn't really optional. Perhaps the reload type should be non-optional to avoid confusion?


Discussion:
Thanks for the report. I think this has been fixed in master in https://github.com/benoitc/gunicorn/commit/8dbb2963b299ebbef2bd21270ae203c1eabf9e0e. Could you please try the master branch?
Thanks for replying.

The commit I used (c54426c1e4b26b30c5feee2cec4450656ae147a3) is 2 after the one you point at. I installed it like: 
```
pip3 install --force-reinstall --upgrade git+git://github.com/benoitc/gunicorn@c54426c1e4b26b30c5feee2cec4450656ae147a3
```
Oops, you're right! I think we've hit a bug in argparse: http://bugs.python.org/issue9338

Another workaround:

```sh
$ gunicorn --reload -- mymodule:app
```
Here is a test case:

```diff
diff --git a/tests/test_config.py b/tests/test_config.py
index 9f9a2fe..f00aa60 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -285,3 +285,17 @@ def test_always_use_configured_logger():
     c.set('statsd_host', 'localhost:12345')
     # still uses custom logger over statsd
     assert c.logger_class == MyLogger
+
+
+@pytest.mark.parametrize("options, expected", [
+    (["myapp:app"], "off"),
+    (["--reload=poll", "myapp:app"], "poll"),
+    (["--reload", "myapp:app"], "auto"),
+    (["--reload", "--", "myapp:app"], "auto"),
+])
+def test_reload(options, expected):
+    cmdline = ["prog_name"]
+    cmdline.extend(options)
+    with AltArgs(cmdline):
+        app = NoConfigApp()
+    assert app.cfg.reload == expected
```
> I think we've hit a bug in argparse: http://bugs.python.org/issue9338

I was wrong. Issue 9338 is different than this.

Here is a pure Python snippet:

```py
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("args", nargs="*", help=argparse.SUPPRESS)
parser.add_argument("--reload", nargs="?", const="auto", default="off", help=argparse.SUPPRESS)

tests = [
    ("app:app", 'off'),
    ("--reload app:app", 'auto'),
    ("--reload poll app:app", 'poll'),
    ("--reload -- app:app", 'auto'),
    ("app:app --reload", 'auto'),
]

for args, expected in tests:
    got = parser.parse_args(args.split()).reload
    print("[%s] Test: %25s Expected: %10s Got: %10s" % ("P" if got == expected else "X", args, expected, got))
```

My vote is to document using `--` as a workaround.
If I apply the patch from http://bugs.python.org/issue9338 and replace

```py
parser.add_argument("args", nargs="*", help=argparse.SUPPRESS)
```

with

```py
parser.add_argument("args", nargs="+", help=argparse.SUPPRESS)
```

in the snippet I gave in https://github.com/benoitc/gunicorn/issues/1422#issuecomment-271131630, `--reload app:app` works as expected.

Since the `app:app` part is the only required argument, it might make sense to change `nargs` from `"*"` to `"+"` in line 82 in `gunicorn/config.py`:

```py
parser.add_argument("args", nargs="+", help=argparse.SUPPRESS)
```
@berkerpeksag but we need the patch in Python for it right?
I can commit the patch (it's not ready to commit btw) to next Python 3.5 and 3.6 releases, but that would only work for 3.5+.

I see two viable solutions:

* Document the current behavior and the `--` workaround
* Since this change is not released yet we can introduce a new setting like `--reloader-backend` and keep the old behavior of `--reload`
@berkerpeksag i'm asking myself if we should keep the old behaviour at all ...
ie. we can just force the user to choose the reload type it needs when using the `--reload` setting. Which would be simpler.
@tilgovi just saw the pacth was yours, what do you think about it also? Should we keep the old behaviour and add another setting to the long list we have? Or force the usage of the new behaviour implying to pass the reload type?
It would be nice to keep the old behavior. Another alternative solution is to remove `--reload=...` options and use the best options available implicitly.
:+1: for just using the best available
Ok, I will open a PR to implement it this week :)

Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -496,20 +496,6 @@ def validate_hostport(val):
         raise TypeError("Value must consist of: hostname:port")
 
 
-def validate_reloader(val):
-    if val is None:
-        val = 'auto'
-
-    choices = ['auto', 'poll', 'inotify', 'off']
-
-    if val not in choices:
-        raise ConfigError(
-            'Invalid reloader type. Must be one of: %s' % choices
-        )
-
-    return val
-
-
 def get_default_config_file():
     config_path = os.path.join(os.path.abspath(os.getcwd()),
             'gunicorn.conf.py')
@@ -827,11 +813,9 @@ class Reload(Setting):
     name = "reload"
     section = 'Debugging'
     cli = ['--reload']
-    validator = validate_reloader
-    const = 'auto'
-    default = 'off'
-    meta = 'RELOADER_TYPE'
-    nargs = '?'
+    validator = validate_bool
+    action = 'store_true'
+    default = False
 
     desc = '''\
         Restart workers when code changes.
@@ -843,14 +827,13 @@ class Reload(Setting):
         paste configuration be sure that the server block does not import any
         application code or the reload will not work as designed.
 
-        When using this option, you can optionally specify whether you would
-        like to use file system polling or the kernel's inotify API to watch
-        for changes. Generally, inotify should be preferred if available
-        because it consumes less system resources. The default behavior (auto)
-        is to attempt inotify with a fallback to FS polling.
+        The default behavior is to attempt inotify with a fallback to file
+        system polling. Generally, inotify should be preferred if available
+        because it consumes less system resources.
 
-        Note: In order to use the inotify reloader, you must have the 'inotify'
-        package installed.
+        .. note::
+           In order to use the inotify reloader, you must have the ``inotify``
+           package installed.
         '''
 
 
diff --git a/gunicorn/workers/base.py b/gunicorn/workers/base.py
--- a/gunicorn/workers/base.py
+++ b/gunicorn/workers/base.py
@@ -15,7 +15,7 @@
 from gunicorn import six
 from gunicorn import util
 from gunicorn.workers.workertmp import WorkerTmp
-from gunicorn.reloader import preferred_reloader, Reloader, InotifyReloader
+from gunicorn.reloader import preferred_reloader
 from gunicorn.http.errors import (
     InvalidHeader, InvalidHeaderName, InvalidRequestLine, InvalidRequestMethod,
     InvalidHTTPVersion, LimitRequestLine, LimitRequestHeaders,
@@ -114,7 +114,7 @@ def init_process(self):
         self.init_signals()
 
         # start the reloader
-        if self.cfg.reload and self.cfg.reload != 'off':
+        if self.cfg.reload:
             def changed(fname):
                 self.log.info("Worker reloading: %s modified", fname)
                 self.alive = False
@@ -122,14 +122,7 @@ def changed(fname):
                 time.sleep(0.1)
                 sys.exit(0)
 
-            if self.cfg.reload == 'poll':
-                reloader_cls = Reloader
-            elif self.cfg.reload == 'inotify':
-                reloader_cls = InotifyReloader
-            else:
-                reloader_cls = preferred_reloader
-
-            self.reloader = reloader_cls(callback=changed)
+            self.reloader = preferred_reloader(callback=changed)
             self.reloader.start()
 
         self.load_wsgi()


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -306,3 +306,17 @@ def test_cli_overrides_enviroment_variables_module(monkeypatch):
     with AltArgs(["prog_name", "-c", cfg_file(), "--workers", "3"]):
         app = NoConfigApp()
     assert app.cfg.workers == 3
+
+
+@pytest.mark.parametrize("options, expected", [
+    (["myapp:app"], False),
+    (["--reload", "myapp:app"], True),
+    (["--reload", "--", "myapp:app"], True),
+    (["--reload", "-w 2", "myapp:app"], True),
+])
+def test_reload(options, expected):
+    cmdline = ["prog_name"]
+    cmdline.extend(options)
+    with AltArgs(cmdline):
+        app = NoConfigApp()
+    assert app.cfg.reload == expected