Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .env support #2416

Merged
merged 1 commit into from
Jul 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
.env
.flaskenv
*.pyc
*.pyo
env
Expand Down
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ script:
cache:
- pip

branches:
only:
- master
- /^.*-maintenance$/

notifications:
email: false
irc:
Expand Down
4 changes: 4 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ Major release, unreleased
- The ``request.json`` property is no longer deprecated. (`#1421`_)
- Support passing an existing ``EnvironBuilder`` or ``dict`` to
``test_client.open``. (`#2412`_)
- The ``flask`` command and ``app.run`` will load environment variables using
from ``.env`` and ``.flaskenv`` files if python-dotenv is installed.
(`#2416`_)

.. _#1421: https://github.com/pallets/flask/issues/1421
.. _#1489: https://github.com/pallets/flask/pull/1489
Expand Down Expand Up @@ -130,6 +133,7 @@ Major release, unreleased
.. _#2385: https://github.com/pallets/flask/issues/2385
.. _#2412: https://github.com/pallets/flask/pull/2412
.. _#2414: https://github.com/pallets/flask/pull/2414
.. _#2416: https://github.com/pallets/flask/pull/2416

Version 0.12.2
--------------
Expand Down
2 changes: 2 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,8 @@ Command Line Interface
.. autoclass:: ScriptInfo
:members:

.. autofunction:: load_dotenv

.. autofunction:: with_appcontext

.. autofunction:: pass_script_info
Expand Down
34 changes: 31 additions & 3 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@ Custom Commands
---------------

If you want to add more commands to the shell script you can do this
easily. Flask uses `click`_ for the command interface which makes
creating custom commands very easy. For instance if you want a shell
command to initialize the database you can do this::
easily. For instance if you want a shell command to initialize the database you
can do this::

import click
from flask import Flask
Expand Down Expand Up @@ -134,6 +133,35 @@ decorator::
def example():
pass


.. _dotenv:

Loading Environment Variables From ``.env`` Files
-------------------------------------------------

If `python-dotenv`_ is installed, running the :command:`flask` command will set
environment variables defined in the files :file:`.env` and :file:`.flaskenv`.
This can be used to avoid having to set ``FLASK_APP`` manually every time you
open a new terminal, and to set configuration using environment variables
similar to how some deployment services work.

Variables set on the command line are used over those set in :file:`.env`,
which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be
used for public variables, such as ``FLASK_APP``, while :file:`.env` should not
be committed to your repository so that it can set private variables.

Directories are scanned upwards from the directory you call :command:`flask`
from to locate the files. The current working directory will be set to the
location of the file, with the assumption that that is the top level project
directory.

The files are only loaded by the :command:`flask` command or calling
:meth:`~flask.Flask.run`. If you would like to load these files when running in
production, you should call :func:`~flask.cli.load_dotenv` manually.

.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme


Factory Functions
-----------------

Expand Down
3 changes: 3 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ use them if you install them.
* `SimpleJSON`_ is a fast JSON implementation that is compatible with
Python's ``json`` module. It is preferred for JSON operations if it is
installed.
* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask``
commands.

.. _Blinker: https://pythonhosted.org/blinker/
.. _SimpleJSON: https://simplejson.readthedocs.io/
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme

Virtual environments
--------------------
Expand Down
42 changes: 27 additions & 15 deletions flask/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,9 @@ def _reconfigure_for_run_debug(self, debug):
self.debug = debug
self.jinja_env.auto_reload = self.templates_auto_reload

def run(self, host=None, port=None, debug=None, **options):
def run(
self, host=None, port=None, debug=None, load_dotenv=True, **options
):
"""Runs the application on a local development server.

Do not use ``run()`` in a production setting. It is not intended to
Expand Down Expand Up @@ -849,30 +851,40 @@ def run(self, host=None, port=None, debug=None, **options):
won't catch any exceptions because there won't be any to
catch.

:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
have the server available externally as well. Defaults to
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable
if present.
:param port: the port of the webserver. Defaults to ``5000`` or the
port defined in the ``SERVER_NAME`` config variable if present.
:param debug: if given, enable or disable debug mode. See
:attr:`debug`.
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
files to set environment variables. Will also change the working
directory to the directory containing the first file found.
:param options: the options to be forwarded to the underlying Werkzeug
server. See :func:`werkzeug.serving.run_simple` for more
information.

.. versionchanged:: 1.0
If installed, python-dotenv will be used to load environment
variables from :file:`.env` and :file:`.flaskenv` files.

.. versionchanged:: 0.10
The default port is now picked from the ``SERVER_NAME`` variable.

:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
have the server available externally as well. Defaults to
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config
variable if present.
:param port: the port of the webserver. Defaults to ``5000`` or the
port defined in the ``SERVER_NAME`` config variable if
present.
:param debug: if given, enable or disable debug mode.
See :attr:`debug`.
:param options: the options to be forwarded to the underlying
Werkzeug server. See
:func:`werkzeug.serving.run_simple` for more
information.
"""
# 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':
if os.environ.get('FLASK_RUN_FROM_CLI') == 'true':
from .debughelpers import explain_ignored_app_run
explain_ignored_app_run()
return

if load_dotenv:
from flask.cli import load_dotenv
load_dotenv()

if debug is not None:
self._reconfigure_for_run_debug(bool(debug))

Expand Down
101 changes: 86 additions & 15 deletions flask/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function

import ast
import inspect
Expand All @@ -22,10 +23,14 @@
import click

from . import __version__
from ._compat import iteritems, reraise
from ._compat import getargspec, iteritems, reraise
from .globals import current_app
from .helpers import get_debug_flag
from ._compat import getargspec

try:
import dotenv
except ImportError:
dotenv = None


class NoAppException(click.UsageError):
Expand Down Expand Up @@ -394,21 +399,31 @@ class FlaskGroup(AppGroup):
For information as of why this is useful see :ref:`custom-scripts`.

:param add_default_commands: if this is True then the default run and
shell commands wil be added.
shell commands wil be added.
:param add_version_option: adds the ``--version`` option.
:param create_app: an optional callback that is passed the script info
and returns the loaded app.
:param create_app: an optional callback that is passed the script info and
returns the loaded app.
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
files to set environment variables. Will also change the working
directory to the directory containing the first file found.

.. versionchanged:: 1.0
If installed, python-dotenv will be used to load environment variables
from :file:`.env` and :file:`.flaskenv` files.
"""

def __init__(self, add_default_commands=True, create_app=None,
add_version_option=True, **extra):
def __init__(
self, add_default_commands=True, create_app=None,
add_version_option=True, load_dotenv=True, **extra
):
params = list(extra.pop('params', None) or ())

if add_version_option:
params.append(version_option)

AppGroup.__init__(self, params=params, **extra)
self.create_app = create_app
self.load_dotenv = load_dotenv

if add_default_commands:
self.add_command(run_command)
Expand Down Expand Up @@ -472,12 +487,75 @@ def list_commands(self, ctx):
return sorted(rv)

def main(self, *args, **kwargs):
# 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'] = 'true'

if self.load_dotenv:
load_dotenv()

obj = kwargs.get('obj')

if obj is None:
obj = ScriptInfo(create_app=self.create_app)

kwargs['obj'] = obj
kwargs.setdefault('auto_envvar_prefix', 'FLASK')
return AppGroup.main(self, *args, **kwargs)
return super(FlaskGroup, self).main(*args, **kwargs)


def _path_is_ancestor(path, other):
"""Take ``other`` and remove the length of ``path`` from it. Then join it
to ``path``. If it is the original value, ``path`` is an ancestor of
``other``."""
return os.path.join(path, other[len(path):].lstrip(os.sep)) == other


def load_dotenv(path=None):
"""Load "dotenv" files in order of precedence to set environment variables.

If an env var is already set it is not overwritten, so earlier files in the
list are preferred over later files.

Changes the current working directory to the location of the first file
found, with the assumption that it is in the top level project directory
and will be where the Python path should import local packages from.

This is a no-op if `python-dotenv`_ is not installed.

.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme

:param path: Load the file at this location instead of searching.
:return: ``True`` if a file was loaded.

.. versionadded:: 1.0
"""

if dotenv is None:
return

if path is not None:
return dotenv.load_dotenv(path)

new_dir = None

for name in ('.env', '.flaskenv'):
path = dotenv.find_dotenv(name, usecwd=True)

if not path:
continue

if new_dir is None:
new_dir = os.path.dirname(path)

dotenv.load_dotenv(path)

if new_dir and os.getcwd() != new_dir:
os.chdir(new_dir)

return new_dir is not None # at least one file was located and loaded


@click.command('run', short_help='Runs a development server.')
Expand Down Expand Up @@ -512,13 +590,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)
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ def hello():
'click>=4.0',
],
extras_require={
'dotenv': ['python-dotenv'],
'dev': [
'blinker',
'python-dotenv',
'greenlet',
'pytest>=3',
'coverage',
Expand Down
3 changes: 3 additions & 0 deletions tests/test_apps/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FOO=env
SPAM=1
EGGS=2
3 changes: 3 additions & 0 deletions tests/test_apps/.flaskenv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FOO=flaskenv
BAR=bar
EGGS=0
Loading