Repository: benoitc/gunicorn

Issue:
"--config python:foo" module search path
What I'm trying to do:

```
# myapp/gunicorn.py
from . import something

def on_starting(...): ...
```

If I try to run it with `gunicorn -c file:myapp/gunicorn.py ...` if fails with "cannot perform relative import" because this file is not loaded as a module. OK, I can accept that.

If I try to run it with `gunicorn -c python:myapp.gunicorn ...` it fails with "Error: no module named 'myapp'".

Digging deeper, I find that when I run `python`, I get following for `sys.path`:
`['', '/usr/local/lib/python35.zip', '/usr/local/lib/python3.5', '/usr/local/lib/python3.5/plat-linux', '/usr/local/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/site-packages']`

However, when I run `gunicorn`, it has following for `sys.path`:
`['/usr/local/bin', '/usr/local/lib/python35.zip', '/usr/local/lib/python3.5', '/usr/local/lib/python3.5/plat-linux', '/usr/local/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/site-packages']`

(nevermind system-wide paths, that's because docker)

What's the canonical way?
- install myapp into system path?
- teach gunicorn to add "." to system path? (is there a command-line argument?)
- ugly hack in my config? (e.g. `-c file:xxx` which will chain-load real config)



Discussion:
Usually I would recommend installing the app and gunicorn into a virtualenv. On docker, the virtualenv isn't necessary because the container provides the isolated environment. Note that PEP8 recommends always using absolute imports.

You could probably also set the PYTHONPATH environment variable if you really don't want to install as a package.

I wonder if Gunicorn should add the working directory to the python path, though. It might make sense.

@tilgovi what do you mean ? :)
OT: I'm not working on that product any more. In the end I worked around the limitation using a bootstrap file (i.e. start `file:bootstrap.py` which in turn sets up `sys.path` and imports real code). Doing that was also convenient for other settings (e.g. gunicorn settings).
@benoitc looking at the paths in the example, only the first one is different.

Python has ''.
Gunicorn has '/usr/bin/python'

The current directory seems more appropriate than the location of the gunicorn script.

I'll have a look at what happens with other entry points style scripts and such. I'll see if there's a simple thing I can recommend.
@tilgovi maybe doing something like we do when the pythonpath setting is given?

https://github.com/benoitc/gunicorn/blob/ab444eed50f34df33755f4e778825de8d1bf9668/gunicorn/app/base.py#L196-L201

though I noticed that we are already adding the current directory to the sys path:

https://github.com/benoitc/gunicorn/blob/2b839ca14437c61780d6eaaa8b24ad31e021f2c6/gunicorn/app/wsgiapp.py#L46

So not sure why it wouldn't be found. Otherwise we could simply add `''` to the sys.path in any case

Resolving patch:
diff --git a/gunicorn/app/base.py b/gunicorn/app/base.py
--- a/gunicorn/app/base.py
+++ b/gunicorn/app/base.py
@@ -75,8 +75,18 @@ def run(self):
             sys.stderr.flush()
             sys.exit(1)
 
+
 class Application(BaseApplication):
 
+    def chdir(self):
+        # chdir to the configured path before loading,
+        # default is the current dir
+        os.chdir(self.cfg.chdir)
+
+        # add the path to sys.path
+        if self.cfg.chdir not in sys.path:
+            sys.path.insert(0, self.cfg.chdir)
+
     def get_config_from_filename(self, filename):
 
         if not os.path.exists(filename):
@@ -142,6 +152,9 @@ def load_config(self):
         # optional settings from apps
         cfg = self.init(parser, args, args.args)
 
+        # set up import paths and follow symlinks
+        self.chdir()
+
         # Load up the any app specific configuration
         if cfg:
             for k, v in cfg.items():
@@ -174,6 +187,10 @@ def load_config(self):
                 continue
             self.cfg.set(k.lower(), v)
 
+        # current directory might be changed by the config now
+        # set up import paths and follow symlinks
+        self.chdir()
+
     def run(self):
         if self.cfg.check_config:
             try:
diff --git a/gunicorn/app/pasterapp.py b/gunicorn/app/pasterapp.py
--- a/gunicorn/app/pasterapp.py
+++ b/gunicorn/app/pasterapp.py
@@ -163,10 +163,6 @@ def __init__(self, app, gcfg=None, host="127.0.0.1", port=None, *args, **kwargs)
                 self.load_config_from_file(default_config)
 
     def load(self):
-        # chdir to the configured path before loading,
-        # default is the current dir
-        os.chdir(self.cfg.chdir)
-
         return self.app
 
 
diff --git a/gunicorn/app/wsgiapp.py b/gunicorn/app/wsgiapp.py
--- a/gunicorn/app/wsgiapp.py
+++ b/gunicorn/app/wsgiapp.py
@@ -4,7 +4,6 @@
 # See the NOTICE for more information.
 
 import os
-import sys
 
 from gunicorn.errors import ConfigError
 from gunicorn.app.base import Application
@@ -37,23 +36,11 @@ def init(self, parser, opts, args):
         self.cfg.set("default_proc_name", args[0])
         self.app_uri = args[0]
 
-    def chdir(self):
-        # chdir to the configured path before loading,
-        # default is the current dir
-        os.chdir(self.cfg.chdir)
-
-        # add the path to sys.path
-        sys.path.insert(0, self.cfg.chdir)
-
     def load_wsgiapp(self):
-        self.chdir()
-
         # load the app
         return util.import_app(self.app_uri)
 
     def load_pasteapp(self):
-        self.chdir()
-
         # load the paste app
         from .pasterapp import load_pasteapp
         return load_pasteapp(self.cfgurl, self.relpath, global_conf=self.cfg.paste_global_conf)


Test patch: