load env vars using python-dotenv
This commit is contained in:
parent
77b98a2762
commit
491d331e6e
13 changed files with 235 additions and 42 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,4 +1,6 @@
|
|||
.DS_Store
|
||||
.env
|
||||
.flaskenv
|
||||
*.pyc
|
||||
*.pyo
|
||||
env
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ script:
|
|||
cache:
|
||||
- pip
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^.*-maintenance$/
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
irc:
|
||||
|
|
|
|||
4
CHANGES
4
CHANGES
|
|
@ -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
|
||||
|
|
@ -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
|
||||
--------------
|
||||
|
|
|
|||
|
|
@ -814,6 +814,8 @@ Command Line Interface
|
|||
.. autoclass:: ScriptInfo
|
||||
:members:
|
||||
|
||||
.. autofunction:: load_dotenv
|
||||
|
||||
.. autofunction:: with_appcontext
|
||||
|
||||
.. autofunction:: pass_script_info
|
||||
|
|
|
|||
34
docs/cli.rst
34
docs/cli.rst
|
|
@ -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
|
||||
|
|
@ -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
|
||||
-----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
--------------------
|
||||
|
|
|
|||
42
flask/app.py
42
flask/app.py
|
|
@ -820,7 +820,9 @@ class Flask(_PackageBoundObject):
|
|||
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
|
||||
|
|
@ -849,30 +851,40 @@ class Flask(_PackageBoundObject):
|
|||
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))
|
||||
|
||||
|
|
|
|||
101
flask/cli.py
101
flask/cli.py
|
|
@ -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
|
||||
|
|
@ -22,10 +23,14 @@ from threading import Lock, Thread
|
|||
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):
|
||||
|
|
@ -394,14 +399,23 @@ 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:
|
||||
|
|
@ -409,6 +423,7 @@ class FlaskGroup(AppGroup):
|
|||
|
||||
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)
|
||||
|
|
@ -472,12 +487,75 @@ class FlaskGroup(AppGroup):
|
|||
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.')
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -75,8 +75,10 @@ setup(
|
|||
'click>=4.0',
|
||||
],
|
||||
extras_require={
|
||||
'dotenv': ['python-dotenv'],
|
||||
'dev': [
|
||||
'blinker',
|
||||
'python-dotenv',
|
||||
'greenlet',
|
||||
'pytest>=3',
|
||||
'coverage',
|
||||
|
|
|
|||
3
tests/test_apps/.env
Normal file
3
tests/test_apps/.env
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
FOO=env
|
||||
SPAM=1
|
||||
EGGS=2
|
||||
3
tests/test_apps/.flaskenv
Normal file
3
tests/test_apps/.flaskenv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
FOO=flaskenv
|
||||
BAR=bar
|
||||
EGGS=0
|
||||
|
|
@ -19,11 +19,31 @@ from functools import partial
|
|||
|
||||
import click
|
||||
import pytest
|
||||
from _pytest.monkeypatch import notset
|
||||
from click.testing import CliRunner
|
||||
|
||||
from flask import Flask, current_app
|
||||
from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \
|
||||
find_best_app, get_version, locate_app, prepare_import, with_appcontext
|
||||
from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv, \
|
||||
find_best_app, get_version, load_dotenv, locate_app, prepare_import, \
|
||||
with_appcontext
|
||||
|
||||
cwd = os.getcwd()
|
||||
test_path = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'test_apps'
|
||||
))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def manage_os_environ(monkeypatch):
|
||||
# can't use monkeypatch.delitem since we don't want to restore a value
|
||||
os.environ.pop('FLASK_APP', None)
|
||||
os.environ.pop('FLASK_DEBUG', None)
|
||||
# use monkeypatch internals to force-delete environ keys
|
||||
monkeypatch._setitem.extend((
|
||||
(os.environ, 'FLASK_APP', notset),
|
||||
(os.environ, 'FLASK_DEBUG', notset),
|
||||
(os.environ, 'FLASK_RUN_FROM_CLI', notset),
|
||||
))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -125,12 +145,6 @@ def test_find_best_app(test_apps):
|
|||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
||||
|
||||
|
||||
cwd = os.getcwd()
|
||||
test_path = os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), 'test_apps'
|
||||
))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('value,path,result', (
|
||||
('test', cwd, 'test'),
|
||||
('test.py', cwd, 'test'),
|
||||
|
|
@ -414,3 +428,46 @@ class TestRoutes:
|
|||
assert 'GET, HEAD, OPTIONS, POST' not in output
|
||||
output = invoke(['routes', '--all-methods']).output
|
||||
assert 'GET, HEAD, OPTIONS, POST' in output
|
||||
|
||||
|
||||
need_dotenv = pytest.mark.skipif(
|
||||
dotenv is None, reason='dotenv is not installed'
|
||||
)
|
||||
|
||||
|
||||
@need_dotenv
|
||||
def test_load_dotenv(monkeypatch):
|
||||
# can't use monkeypatch.delitem since the keys don't exist yet
|
||||
for item in ('FOO', 'BAR', 'SPAM'):
|
||||
monkeypatch._setitem.append((os.environ, item, notset))
|
||||
|
||||
monkeypatch.setenv('EGGS', '3')
|
||||
monkeypatch.chdir(os.path.join(test_path, 'cliapp', 'inner1'))
|
||||
load_dotenv()
|
||||
assert os.getcwd() == test_path
|
||||
# .flaskenv doesn't overwrite .env
|
||||
assert os.environ['FOO'] == 'env'
|
||||
# set only in .flaskenv
|
||||
assert os.environ['BAR'] == 'bar'
|
||||
# set only in .env
|
||||
assert os.environ['SPAM'] == '1'
|
||||
# set manually, files don't overwrite
|
||||
assert os.environ['EGGS'] == '3'
|
||||
|
||||
|
||||
@need_dotenv
|
||||
def test_dotenv_path(monkeypatch):
|
||||
for item in ('FOO', 'BAR', 'EGGS'):
|
||||
monkeypatch._setitem.append((os.environ, item, notset))
|
||||
|
||||
cwd = os.getcwd()
|
||||
load_dotenv(os.path.join(test_path, '.flaskenv'))
|
||||
assert os.getcwd() == cwd
|
||||
assert 'FOO' in os.environ
|
||||
|
||||
|
||||
def test_dotenv_optional(monkeypatch):
|
||||
monkeypatch.setattr('flask.cli.dotenv', None)
|
||||
monkeypatch.chdir(test_path)
|
||||
load_dotenv()
|
||||
assert 'FOO' not in os.environ
|
||||
|
|
|
|||
3
tox.ini
3
tox.ini
|
|
@ -15,6 +15,7 @@ deps =
|
|||
coverage
|
||||
greenlet
|
||||
blinker
|
||||
python-dotenv
|
||||
|
||||
lowest: Werkzeug==0.9
|
||||
lowest: Jinja2==2.4
|
||||
|
|
@ -67,4 +68,4 @@ skip_install = true
|
|||
deps = detox
|
||||
commands =
|
||||
detox -e py{36,35,34,33,27,26,py},py{36,27,py}-simplejson,py{36,33,27,26,py}-devel,py{36,33,27,26,py}-lowest
|
||||
tox -e coverage-report
|
||||
tox -e docs-html,coverage-report
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue