Repository: benoitc/gunicorn

Issue:
import error in 19.1.0
I've the following error:

```
Error: relative imports require the 'package' argument
```

No such error on previous installed version (19.0.0).



Discussion:
@SaulTigh hi, thanks for the ticket. Can you give more details? Do you have a way to reproduce it?

I've got this error with upgrade to 19.1.0 from 19.0.0. 
I don't know if it helps, but I'm using supervisor for my django-project. Its configuration is:

```
[program:myproject]
command = /srv/myproject/env/bin/gunicorn myproject.wsgi:application -c ../conf/gunicorn/myproject.py
directory = /srv/myproject/myproject/
autostart = true
autorestart = true
```

And gunicorn configuration is:

```
NAME = 'myproject'
reload = False
bind = 'unix:/tmp/{basename}.sock'.format(basename=NAME)
pidfile = '/tmp/{basename}.pid'.format(basename=NAME)
daemon = False
keepalive = 2
import os
import multiprocessing
workers = 2 * multiprocessing.cpu_count()
# logging
PROJDIR = os.path.abspath(os.path.join(os.path.basename(__file__),  os.pardir, os.pardir))
accesslog = os.path.abspath(os.path.join(PROJDIR, 'logs/gunicorn/access.log'))
errorlog = os.path.abspath(os.path.join(PROJDIR, 'logs/gunicorn/errors.log'))
chdir = os.path.abspath(os.path.join(PROJDIR, 'myproject'))
pythonpath = chdir
```

@SaulTigh is there any traceback? Can you tell which import is causing it?

I think the problem is at https://github.com/benoitc/gunicorn/blob/master/gunicorn/app/base.py#L101

I can reproduce it with the following command:

``` sh
$ gunicorn -c ../c.py app:app

Error: relative imports require the 'package' argument
```

The error message comes from https://github.com/benoitc/gunicorn/blob/master/gunicorn/util.py#L87

And the related commit is https://github.com/benoitc/gunicorn/commit/2bde8eb1034dc65fb460f60d5444b1720e7962f6.

I had asked that we switch the order of these loads, so that relative file paths are tried before standard module imports.

Looks like @benoitc reverted that here: 46afb979e2d0e0b79ace7b8ee3a026eaf1055a30

What was the problem you were seeing, @benoitc?

@tilgovi I think the change you were expected was done in #800. I reverted that changes since it was breaking module loading and forgot to come back on it right after. 

The problem happen here because it try to load a module first for the config and just fail. Imo we rely too much on guess here (if we want to load a module or a patch) and we should probably do it more explicitely. I propose to do the following:
- prefix by `python:` in case of a module. Ex: `python:my.module.name`
- prefix by `file:` for the default. Ex: `file:/path/to/configfile`
- Default case without prefix will be the file.

Thoughts?

What about using the path separator or the file extension to guess:

Okay:

my.module
./my/module.py
./config.py

Bad:
./my/module
config (should be .config or ./config.py)
 On Aug 2, 2014 1:20 AM, "Benoit Chesneau" notifications@github.com wrote:

> @tilgovi https://github.com/tilgovi I think the change you were
> expected was done in #800 https://github.com/benoitc/gunicorn/issues/800.
> I reverted that changes since it was breaking module loading and forgot to
> come back on it right after.
> 
> The problem happen here because it try to load a module first for the
> config and just fail. Imo we rely too much on guess here (if we want to
> load a module or a patch) and we should probably do it more explicitely. I
> propose to do the following:
> - prefix by python: in case of a module. Ex: python:my.module.name
> - prefix by file: for the default. Ex: file:/path/to/configfile
> - Default case without prefix will be the file.
> 
> Thoughts?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/benoitc/gunicorn/issues/836#issuecomment-50957348.

@tilgovi it would break the current behaviour where you can pass config.py and then any graceful update. Imo the python module case should be an exception, so prefixing it looks ok for me.

:+1: @benoitc 

I nominate this for 19.2.

what's the status of this one?

Free to take it. I probably can't this week.

I moved it to 19.3.


Resolving patch:
diff --git a/gunicorn/app/base.py b/gunicorn/app/base.py
--- a/gunicorn/app/base.py
+++ b/gunicorn/app/base.py
@@ -108,10 +108,15 @@ def load_config_from_module_name_or_filename(self, location):
         Exception or stop the process if the configuration file contains a syntax error.
         """
 
-        try:
-            cfg = self.get_config_from_module_name(module_name=location)
-        except ImportError:
-            cfg = self.get_config_from_filename(filename=location)
+        if location.startswith("python:"):
+            module_name = location[len("python:"):]
+            cfg = self.get_config_from_module_name(module_name)
+        else:
+            if location.startswith("file:"):
+                filename = location[len("file:"):]
+            else:
+                filename = location
+            cfg = self.get_config_from_filename(filename)
 
         for k, v in cfg.items():
             # Ignore unknown names
@@ -127,9 +132,7 @@ def load_config_from_module_name_or_filename(self, location):
         return cfg
 
     def load_config_from_file(self, filename):
-        return self.load_config_from_module_name_or_filename(
-            location=filename
-        )
+        return self.load_config_from_module_name_or_filename(location=filename)
 
     def load_config(self):
         # parse console args
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -465,14 +465,20 @@ class ConfigFile(Setting):
     name = "config"
     section = "Config File"
     cli = ["-c", "--config"]
-    meta = "FILE"
+    meta = "CONFIG"
     validator = validate_string
     default = None
     desc = """\
-        The path to a Gunicorn config file, or python module.
+        The Gunicorn config file.
+
+        A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``.
 
         Only has an effect when specified on the command line or as part of an
         application specific configuration.
+
+        .. versionchanged:: 19.4
+           Loading the config from a Python module requires the ``python:``
+           prefix.
         """
 
 class Bind(Setting):


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -183,8 +183,15 @@ def test_load_config():
     assert app.cfg.workers == 3
     assert app.cfg.proc_name == "fooey"
 
+def test_load_config_explicit_file():
+    with AltArgs(["prog_name", "-c", "file:%s" % cfg_file()]):
+        app = NoConfigApp()
+    assert app.cfg.bind == ["unix:/tmp/bar/baz"]
+    assert app.cfg.workers == 3
+    assert app.cfg.proc_name == "fooey"
+
 def test_load_config_module():
-    with AltArgs(["prog_name", "-c", cfg_module()]):
+    with AltArgs(["prog_name", "-c", "python:%s" % cfg_module()]):
         app = NoConfigApp()
     assert app.cfg.bind == ["unix:/tmp/bar/baz"]
     assert app.cfg.workers == 3
@@ -197,7 +204,7 @@ def test_cli_overrides_config():
     assert app.cfg.proc_name == "fooey"
 
 def test_cli_overrides_config_module():
-    with AltArgs(["prog_name", "-c", cfg_module(), "-b", "blarney"]):
+    with AltArgs(["prog_name", "-c", "python:%s" % cfg_module(), "-b", "blarney"]):
         app = NoConfigApp()
     assert app.cfg.bind == ["blarney"]
     assert app.cfg.proc_name == "fooey"