Browse Source

load env vars using python-dotenv

pull/2416/head
David Lord 7 years ago
parent
commit
491d331e6e
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 2
      .gitignore
  2. 5
      .travis.yml
  3. 4
      CHANGES
  4. 2
      docs/api.rst
  5. 34
      docs/cli.rst
  6. 3
      docs/installation.rst
  7. 42
      flask/app.py
  8. 101
      flask/cli.py
  9. 2
      setup.py
  10. 3
      tests/test_apps/.env
  11. 3
      tests/test_apps/.flaskenv
  12. 73
      tests/test_cli.py
  13. 3
      tox.ini

2
.gitignore vendored

@ -1,4 +1,6 @@
.DS_Store
.env
.flaskenv
*.pyc
*.pyo
env

5
.travis.yml

@ -33,6 +33,11 @@ script:
cache:
- pip
branches:
only:
- master
- /^.*-maintenance$/
notifications:
email: false
irc:

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
--------------

2
docs/api.rst

@ -814,6 +814,8 @@ Command Line Interface
.. autoclass:: ScriptInfo
:members:
.. autofunction:: load_dotenv
.. autofunction:: with_appcontext
.. autofunction:: pass_script_info

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
-----------------

3
docs/installation.rst

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,3 @@
FOO=env
SPAM=1
EGGS=2

3
tests/test_apps/.flaskenv

@ -0,0 +1,3 @@
FOO=flaskenv
BAR=bar
EGGS=0

73
tests/test_cli.py

@ -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

@ -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…
Cancel
Save