Skip to content

Commit 491d331

Browse files
committed
load env vars using python-dotenv
1 parent 77b98a2 commit 491d331

13 files changed

Lines changed: 235 additions & 42 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
.DS_Store
2+
.env
3+
.flaskenv
24
*.pyc
35
*.pyo
46
env

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ script:
3333
cache:
3434
- pip
3535

36+
branches:
37+
only:
38+
- master
39+
- /^.*-maintenance$/
40+
3641
notifications:
3742
email: false
3843
irc:

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ Major release, unreleased
101101
- The ``request.json`` property is no longer deprecated. (`#1421`_)
102102
- Support passing an existing ``EnvironBuilder`` or ``dict`` to
103103
``test_client.open``. (`#2412`_)
104+
- The ``flask`` command and ``app.run`` will load environment variables using
105+
from ``.env`` and ``.flaskenv`` files if python-dotenv is installed.
106+
(`#2416`_)
104107

105108
.. _#1421: https://github.com/pallets/flask/issues/1421
106109
.. _#1489: https://github.com/pallets/flask/pull/1489
@@ -130,6 +133,7 @@ Major release, unreleased
130133
.. _#2385: https://github.com/pallets/flask/issues/2385
131134
.. _#2412: https://github.com/pallets/flask/pull/2412
132135
.. _#2414: https://github.com/pallets/flask/pull/2414
136+
.. _#2416: https://github.com/pallets/flask/pull/2416
133137

134138
Version 0.12.2
135139
--------------

docs/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,8 @@ Command Line Interface
814814
.. autoclass:: ScriptInfo
815815
:members:
816816

817+
.. autofunction:: load_dotenv
818+
817819
.. autofunction:: with_appcontext
818820

819821
.. autofunction:: pass_script_info

docs/cli.rst

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,8 @@ Custom Commands
9797
---------------
9898

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

104103
import click
105104
from flask import Flask
@@ -134,6 +133,35 @@ decorator::
134133
def example():
135134
pass
136135

136+
137+
.. _dotenv:
138+
139+
Loading Environment Variables From ``.env`` Files
140+
-------------------------------------------------
141+
142+
If `python-dotenv`_ is installed, running the :command:`flask` command will set
143+
environment variables defined in the files :file:`.env` and :file:`.flaskenv`.
144+
This can be used to avoid having to set ``FLASK_APP`` manually every time you
145+
open a new terminal, and to set configuration using environment variables
146+
similar to how some deployment services work.
147+
148+
Variables set on the command line are used over those set in :file:`.env`,
149+
which are used over those set in :file:`.flaskenv`. :file:`.flaskenv` should be
150+
used for public variables, such as ``FLASK_APP``, while :file:`.env` should not
151+
be committed to your repository so that it can set private variables.
152+
153+
Directories are scanned upwards from the directory you call :command:`flask`
154+
from to locate the files. The current working directory will be set to the
155+
location of the file, with the assumption that that is the top level project
156+
directory.
157+
158+
The files are only loaded by the :command:`flask` command or calling
159+
:meth:`~flask.Flask.run`. If you would like to load these files when running in
160+
production, you should call :func:`~flask.cli.load_dotenv` manually.
161+
162+
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
163+
164+
137165
Factory Functions
138166
-----------------
139167

docs/installation.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ use them if you install them.
4141
* `SimpleJSON`_ is a fast JSON implementation that is compatible with
4242
Python's ``json`` module. It is preferred for JSON operations if it is
4343
installed.
44+
* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask``
45+
commands.
4446

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

4851
Virtual environments
4952
--------------------

flask/app.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,9 @@ def _reconfigure_for_run_debug(self, debug):
820820
self.debug = debug
821821
self.jinja_env.auto_reload = self.templates_auto_reload
822822

823-
def run(self, host=None, port=None, debug=None, **options):
823+
def run(
824+
self, host=None, port=None, debug=None, load_dotenv=True, **options
825+
):
824826
"""Runs the application on a local development server.
825827
826828
Do not use ``run()`` in a production setting. It is not intended to
@@ -849,30 +851,40 @@ def run(self, host=None, port=None, debug=None, **options):
849851
won't catch any exceptions because there won't be any to
850852
catch.
851853
854+
:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
855+
have the server available externally as well. Defaults to
856+
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable
857+
if present.
858+
:param port: the port of the webserver. Defaults to ``5000`` or the
859+
port defined in the ``SERVER_NAME`` config variable if present.
860+
:param debug: if given, enable or disable debug mode. See
861+
:attr:`debug`.
862+
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
863+
files to set environment variables. Will also change the working
864+
directory to the directory containing the first file found.
865+
:param options: the options to be forwarded to the underlying Werkzeug
866+
server. See :func:`werkzeug.serving.run_simple` for more
867+
information.
868+
869+
.. versionchanged:: 1.0
870+
If installed, python-dotenv will be used to load environment
871+
variables from :file:`.env` and :file:`.flaskenv` files.
872+
852873
.. versionchanged:: 0.10
853874
The default port is now picked from the ``SERVER_NAME`` variable.
854875
855-
:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
856-
have the server available externally as well. Defaults to
857-
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config
858-
variable if present.
859-
:param port: the port of the webserver. Defaults to ``5000`` or the
860-
port defined in the ``SERVER_NAME`` config variable if
861-
present.
862-
:param debug: if given, enable or disable debug mode.
863-
See :attr:`debug`.
864-
:param options: the options to be forwarded to the underlying
865-
Werkzeug server. See
866-
:func:`werkzeug.serving.run_simple` for more
867-
information.
868876
"""
869877
# Change this into a no-op if the server is invoked from the
870878
# command line. Have a look at cli.py for more information.
871-
if os.environ.get('FLASK_RUN_FROM_CLI_SERVER') == '1':
879+
if os.environ.get('FLASK_RUN_FROM_CLI') == 'true':
872880
from .debughelpers import explain_ignored_app_run
873881
explain_ignored_app_run()
874882
return
875883

884+
if load_dotenv:
885+
from flask.cli import load_dotenv
886+
load_dotenv()
887+
876888
if debug is not None:
877889
self._reconfigure_for_run_debug(bool(debug))
878890

flask/cli.py

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:copyright: (c) 2015 by Armin Ronacher.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from __future__ import print_function
1112

1213
import ast
1314
import inspect
@@ -22,10 +23,14 @@
2223
import click
2324

2425
from . import __version__
25-
from ._compat import iteritems, reraise
26+
from ._compat import getargspec, iteritems, reraise
2627
from .globals import current_app
2728
from .helpers import get_debug_flag
28-
from ._compat import getargspec
29+
30+
try:
31+
import dotenv
32+
except ImportError:
33+
dotenv = None
2934

3035

3136
class NoAppException(click.UsageError):
@@ -394,21 +399,31 @@ class FlaskGroup(AppGroup):
394399
For information as of why this is useful see :ref:`custom-scripts`.
395400
396401
:param add_default_commands: if this is True then the default run and
397-
shell commands wil be added.
402+
shell commands wil be added.
398403
:param add_version_option: adds the ``--version`` option.
399-
:param create_app: an optional callback that is passed the script info
400-
and returns the loaded app.
404+
:param create_app: an optional callback that is passed the script info and
405+
returns the loaded app.
406+
:param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
407+
files to set environment variables. Will also change the working
408+
directory to the directory containing the first file found.
409+
410+
.. versionchanged:: 1.0
411+
If installed, python-dotenv will be used to load environment variables
412+
from :file:`.env` and :file:`.flaskenv` files.
401413
"""
402414

403-
def __init__(self, add_default_commands=True, create_app=None,
404-
add_version_option=True, **extra):
415+
def __init__(
416+
self, add_default_commands=True, create_app=None,
417+
add_version_option=True, load_dotenv=True, **extra
418+
):
405419
params = list(extra.pop('params', None) or ())
406420

407421
if add_version_option:
408422
params.append(version_option)
409423

410424
AppGroup.__init__(self, params=params, **extra)
411425
self.create_app = create_app
426+
self.load_dotenv = load_dotenv
412427

413428
if add_default_commands:
414429
self.add_command(run_command)
@@ -472,12 +487,75 @@ def list_commands(self, ctx):
472487
return sorted(rv)
473488

474489
def main(self, *args, **kwargs):
490+
# Set a global flag that indicates that we were invoked from the
491+
# command line interface. This is detected by Flask.run to make the
492+
# call into a no-op. This is necessary to avoid ugly errors when the
493+
# script that is loaded here also attempts to start a server.
494+
os.environ['FLASK_RUN_FROM_CLI'] = 'true'
495+
496+
if self.load_dotenv:
497+
load_dotenv()
498+
475499
obj = kwargs.get('obj')
500+
476501
if obj is None:
477502
obj = ScriptInfo(create_app=self.create_app)
503+
478504
kwargs['obj'] = obj
479505
kwargs.setdefault('auto_envvar_prefix', 'FLASK')
480-
return AppGroup.main(self, *args, **kwargs)
506+
return super(FlaskGroup, self).main(*args, **kwargs)
507+
508+
509+
def _path_is_ancestor(path, other):
510+
"""Take ``other`` and remove the length of ``path`` from it. Then join it
511+
to ``path``. If it is the original value, ``path`` is an ancestor of
512+
``other``."""
513+
return os.path.join(path, other[len(path):].lstrip(os.sep)) == other
514+
515+
516+
def load_dotenv(path=None):
517+
"""Load "dotenv" files in order of precedence to set environment variables.
518+
519+
If an env var is already set it is not overwritten, so earlier files in the
520+
list are preferred over later files.
521+
522+
Changes the current working directory to the location of the first file
523+
found, with the assumption that it is in the top level project directory
524+
and will be where the Python path should import local packages from.
525+
526+
This is a no-op if `python-dotenv`_ is not installed.
527+
528+
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
529+
530+
:param path: Load the file at this location instead of searching.
531+
:return: ``True`` if a file was loaded.
532+
533+
.. versionadded:: 1.0
534+
"""
535+
536+
if dotenv is None:
537+
return
538+
539+
if path is not None:
540+
return dotenv.load_dotenv(path)
541+
542+
new_dir = None
543+
544+
for name in ('.env', '.flaskenv'):
545+
path = dotenv.find_dotenv(name, usecwd=True)
546+
547+
if not path:
548+
continue
549+
550+
if new_dir is None:
551+
new_dir = os.path.dirname(path)
552+
553+
dotenv.load_dotenv(path)
554+
555+
if new_dir and os.getcwd() != new_dir:
556+
os.chdir(new_dir)
557+
558+
return new_dir is not None # at least one file was located and loaded
481559

482560

483561
@click.command('run', short_help='Runs a development server.')
@@ -512,13 +590,6 @@ def run_command(info, host, port, reload, debugger, eager_loading,
512590
"""
513591
from werkzeug.serving import run_simple
514592

515-
# Set a global flag that indicates that we were invoked from the
516-
# command line interface provided server command. This is detected
517-
# by Flask.run to make the call into a no-op. This is necessary to
518-
# avoid ugly errors when the script that is loaded here also attempts
519-
# to start a server.
520-
os.environ['FLASK_RUN_FROM_CLI_SERVER'] = '1'
521-
522593
debug = get_debug_flag()
523594
if reload is None:
524595
reload = bool(debug)

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,10 @@ def hello():
7575
'click>=4.0',
7676
],
7777
extras_require={
78+
'dotenv': ['python-dotenv'],
7879
'dev': [
7980
'blinker',
81+
'python-dotenv',
8082
'greenlet',
8183
'pytest>=3',
8284
'coverage',

tests/test_apps/.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FOO=env
2+
SPAM=1
3+
EGGS=2

0 commit comments

Comments
 (0)