diff --git a/.gitignore b/.gitignore index fb9baf3540..0bf80cfde4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.env *.pyc *.pyo env diff --git a/CHANGES b/CHANGES index 62bb200498..ea77828161 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,9 @@ Major release, unreleased - Make `app.run()` into a noop if a Flask application is run from the development server on the command line. This avoids some behavior that was confusing to debug for newcomers. +- The `flask` command line tool now loads a `.env` file when it discovers + one. When it finds one it will change into that folder before starting + and load environment variables from it. Version 0.12.1 -------------- diff --git a/docs/cli.rst b/docs/cli.rst index 2ca0e83ee0..0b710ef4a9 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -28,7 +28,10 @@ register more commands there if they desire so. For the :command:`flask` script to work, an application needs to be discovered. This is achieved by exporting the ``FLASK_APP`` environment variable. It can be either set to an import path or to a filename of a -Python module that contains a Flask application. +Python module that contains a Flask application. Additionally the +:command:`flask` command as of 0.13 will look for a ``.env`` file in the +current folder or any higher folder and automatically load all environment +variables from it. In that imported file the name of the app needs to be called ``app`` or optionally be specified after a colon. For instance @@ -48,6 +51,12 @@ Or with a filename:: export FLASK_APP=/path/to/hello.py flask run +If you use Flask 0.13 or later you can also set the environment variables +in a ``.env`` file. For instance like this:: + + FLASK_APP=/path/to/hello.py + FLASK_DEBUG=1 + Virtualenv Integration ---------------------- diff --git a/flask/app.py b/flask/app.py index 27918d01fa..021844de48 100644 --- a/flask/app.py +++ b/flask/app.py @@ -826,7 +826,8 @@ def run(self, host=None, port=None, debug=None, **options): """ # Change this into a no-op if the server is invoked from the # command line. Have a look at cli.py for more information. - if os.environ.get('FLASK_RUN_FROM_CLI_SERVER') == '1': + # After the first call we unset the variable however. + if os.environ.pop('FLASK_RUN_FROM_CLI', None) == '1': from .debughelpers import explain_ignored_app_run explain_ignored_app_run() return diff --git a/flask/cli.py b/flask/cli.py index bde5a13b55..d8ad07b2c4 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -16,7 +16,7 @@ import click -from ._compat import iteritems, reraise +from ._compat import iteritems, reraise, text_type, PY2 from .helpers import get_debug_flag from . import __version__ @@ -120,6 +120,57 @@ def find_default_import_path(): return app +def find_dotenv(): + here = os.getcwd() + while 1: + path = os.path.join(here, '.env') + if os.path.isfile(path): + return path + there = os.path.dirname(here) + if there == here: + break + here = there + + +def _set_env(key, value): + if PY2: + if isinstance(value, text_type): + value = value.encode('utf-8') + else: + key = key.decode('utf-8', 'surrogateescape') + if isinstance(value, bytes): + value = value.decode('utf-8', 'surrogateescape') + os.environ[key] = value + + +def _unquote(s): + if len(s) >= 2 and s[0] == s[-1] and s[0] in '"\'': + return s[1:-1] \ + .decode('utf-8', 'replace') \ + .encode('utf-8', 'unicode-escape') \ + .decode('unicode-escape') + return s + + +def load_dotenv(): + """Loads a dotenv file for flask.""" + path = find_dotenv() + if path is None: + return + os.chdir(os.path.dirname(path)) + with open(path, 'rb') as f: + for line in f: + line = line.strip() + if not line or line[:1] == b'#' or b'=' not in line: + continue + key, value = line.split(b'=', 1) + value = _unquote(value.strip()) + _set_env(key.strip(), value) + + +_load_dotenv = load_dotenv + + def get_version(ctx, param, value): if not value or ctx.resilient_parsing: return @@ -199,7 +250,17 @@ class ScriptInfo(object): onwards as click object. """ - def __init__(self, app_import_path=None, create_app=None): + def __init__(self, app_import_path=None, create_app=None, + load_dotenv=True): + # Set a global flag that indicates that we were invoked from the + # command line interface. This is detected by Flask.run to make + # the call into a no-op. This is necessary to avoid ugly errors + # when the script that is loaded here also attempts to start a + # server. + os.environ['FLASK_RUN_FROM_CLI'] = '1' + + if load_dotenv: + _load_dotenv() if create_app is None: if app_import_path is None: app_import_path = find_default_import_path() @@ -412,13 +473,6 @@ def run_command(info, host, port, reload, debugger, eager_loading, """ from werkzeug.serving import run_simple - # Set a global flag that indicates that we were invoked from the - # command line interface provided server command. This is detected - # by Flask.run to make the call into a no-op. This is necessary to - # avoid ugly errors when the script that is loaded here also attempts - # to start a server. - os.environ['FLASK_RUN_FROM_CLI_SERVER'] = '1' - debug = get_debug_flag() if reload is None: reload = bool(debug)