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. 40
      flask/app.py
  8. 99
      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 .DS_Store
.env
.flaskenv
*.pyc *.pyc
*.pyo *.pyo
env env

5
.travis.yml

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

4
CHANGES

@ -101,6 +101,9 @@ Major release, unreleased
- The ``request.json`` property is no longer deprecated. (`#1421`_) - The ``request.json`` property is no longer deprecated. (`#1421`_)
- Support passing an existing ``EnvironBuilder`` or ``dict`` to - Support passing an existing ``EnvironBuilder`` or ``dict`` to
``test_client.open``. (`#2412`_) ``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 .. _#1421: https://github.com/pallets/flask/issues/1421
.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1489: https://github.com/pallets/flask/pull/1489
@ -130,6 +133,7 @@ Major release, unreleased
.. _#2385: https://github.com/pallets/flask/issues/2385 .. _#2385: https://github.com/pallets/flask/issues/2385
.. _#2412: https://github.com/pallets/flask/pull/2412 .. _#2412: https://github.com/pallets/flask/pull/2412
.. _#2414: https://github.com/pallets/flask/pull/2414 .. _#2414: https://github.com/pallets/flask/pull/2414
.. _#2416: https://github.com/pallets/flask/pull/2416
Version 0.12.2 Version 0.12.2
-------------- --------------

2
docs/api.rst

@ -814,6 +814,8 @@ Command Line Interface
.. autoclass:: ScriptInfo .. autoclass:: ScriptInfo
:members: :members:
.. autofunction:: load_dotenv
.. autofunction:: with_appcontext .. autofunction:: with_appcontext
.. autofunction:: pass_script_info .. 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 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 easily. For instance if you want a shell command to initialize the database you
creating custom commands very easy. For instance if you want a shell can do this::
command to initialize the database you can do this::
import click import click
from flask import Flask from flask import Flask
@ -134,6 +133,35 @@ decorator::
def example(): def example():
pass 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 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 * `SimpleJSON`_ is a fast JSON implementation that is compatible with
Python's ``json`` module. It is preferred for JSON operations if it is Python's ``json`` module. It is preferred for JSON operations if it is
installed. installed.
* `python-dotenv`_ enables support for :ref:`dotenv` when running ``flask``
commands.
.. _Blinker: https://pythonhosted.org/blinker/ .. _Blinker: https://pythonhosted.org/blinker/
.. _SimpleJSON: https://simplejson.readthedocs.io/ .. _SimpleJSON: https://simplejson.readthedocs.io/
.. _python-dotenv: https://github.com/theskumar/python-dotenv#readme
Virtual environments Virtual environments
-------------------- --------------------

40
flask/app.py

@ -820,7 +820,9 @@ class Flask(_PackageBoundObject):
self.debug = debug self.debug = debug
self.jinja_env.auto_reload = self.templates_auto_reload 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. """Runs the application on a local development server.
Do not use ``run()`` in a production setting. It is not intended to 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 won't catch any exceptions because there won't be any to
catch. catch.
.. 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 :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
have the server available externally as well. Defaults to have the server available externally as well. Defaults to
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable
variable if present. if present.
:param port: the port of the webserver. Defaults to ``5000`` or the :param port: the port of the webserver. Defaults to ``5000`` or the
port defined in the ``SERVER_NAME`` config variable if port defined in the ``SERVER_NAME`` config variable if present.
present. :param debug: if given, enable or disable debug mode. See
:param debug: if given, enable or disable debug mode. :attr:`debug`.
See :attr:`debug`. :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv`
:param options: the options to be forwarded to the underlying files to set environment variables. Will also change the working
Werkzeug server. See directory to the directory containing the first file found.
:func:`werkzeug.serving.run_simple` for more :param options: the options to be forwarded to the underlying Werkzeug
server. See :func:`werkzeug.serving.run_simple` for more
information. 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.
""" """
# Change this into a no-op if the server is invoked from the # Change this into a no-op if the server is invoked from the
# command line. Have a look at cli.py for more information. # 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 from .debughelpers import explain_ignored_app_run
explain_ignored_app_run() explain_ignored_app_run()
return return
if load_dotenv:
from flask.cli import load_dotenv
load_dotenv()
if debug is not None: if debug is not None:
self._reconfigure_for_run_debug(bool(debug)) self._reconfigure_for_run_debug(bool(debug))

99
flask/cli.py

@ -8,6 +8,7 @@
:copyright: (c) 2015 by Armin Ronacher. :copyright: (c) 2015 by Armin Ronacher.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from __future__ import print_function
import ast import ast
import inspect import inspect
@ -22,10 +23,14 @@ from threading import Lock, Thread
import click import click
from . import __version__ from . import __version__
from ._compat import iteritems, reraise from ._compat import getargspec, iteritems, reraise
from .globals import current_app from .globals import current_app
from .helpers import get_debug_flag from .helpers import get_debug_flag
from ._compat import getargspec
try:
import dotenv
except ImportError:
dotenv = None
class NoAppException(click.UsageError): class NoAppException(click.UsageError):
@ -396,12 +401,21 @@ class FlaskGroup(AppGroup):
:param add_default_commands: if this is True then the default run and :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 add_version_option: adds the ``--version`` option.
:param create_app: an optional callback that is passed the script info :param create_app: an optional callback that is passed the script info and
and returns the loaded app. 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, def __init__(
add_version_option=True, **extra): self, add_default_commands=True, create_app=None,
add_version_option=True, load_dotenv=True, **extra
):
params = list(extra.pop('params', None) or ()) params = list(extra.pop('params', None) or ())
if add_version_option: if add_version_option:
@ -409,6 +423,7 @@ class FlaskGroup(AppGroup):
AppGroup.__init__(self, params=params, **extra) AppGroup.__init__(self, params=params, **extra)
self.create_app = create_app self.create_app = create_app
self.load_dotenv = load_dotenv
if add_default_commands: if add_default_commands:
self.add_command(run_command) self.add_command(run_command)
@ -472,12 +487,75 @@ class FlaskGroup(AppGroup):
return sorted(rv) return sorted(rv)
def main(self, *args, **kwargs): 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') obj = kwargs.get('obj')
if obj is None: if obj is None:
obj = ScriptInfo(create_app=self.create_app) obj = ScriptInfo(create_app=self.create_app)
kwargs['obj'] = obj kwargs['obj'] = obj
kwargs.setdefault('auto_envvar_prefix', 'FLASK') 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.') @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 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() debug = get_debug_flag()
if reload is None: if reload is None:
reload = bool(debug) reload = bool(debug)

2
setup.py

@ -75,8 +75,10 @@ setup(
'click>=4.0', 'click>=4.0',
], ],
extras_require={ extras_require={
'dotenv': ['python-dotenv'],
'dev': [ 'dev': [
'blinker', 'blinker',
'python-dotenv',
'greenlet', 'greenlet',
'pytest>=3', 'pytest>=3',
'coverage', '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 click
import pytest import pytest
from _pytest.monkeypatch import notset
from click.testing import CliRunner from click.testing import CliRunner
from flask import Flask, current_app from flask import Flask, current_app
from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv, \
find_best_app, get_version, locate_app, prepare_import, with_appcontext 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 @pytest.fixture
@ -125,12 +145,6 @@ def test_find_best_app(test_apps):
pytest.raises(NoAppException, find_best_app, script_info, Module) 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', ( @pytest.mark.parametrize('value,path,result', (
('test', cwd, 'test'), ('test', cwd, 'test'),
('test.py', cwd, 'test'), ('test.py', cwd, 'test'),
@ -414,3 +428,46 @@ class TestRoutes:
assert 'GET, HEAD, OPTIONS, POST' not in output assert 'GET, HEAD, OPTIONS, POST' not in output
output = invoke(['routes', '--all-methods']).output output = invoke(['routes', '--all-methods']).output
assert 'GET, HEAD, OPTIONS, POST' in 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 coverage
greenlet greenlet
blinker blinker
python-dotenv
lowest: Werkzeug==0.9 lowest: Werkzeug==0.9
lowest: Jinja2==2.4 lowest: Jinja2==2.4
@ -67,4 +68,4 @@ skip_install = true
deps = detox deps = detox
commands = 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 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