Skip to content

Commit

Permalink
Allow loading of environment variables into the config
Browse files Browse the repository at this point in the history
This new method will pick out any environment variables with a certain
prefix and place them into the config named without the prefix. This
makes it easy to use environment variables to configure the app as is
now more popular than when Flask started.

The prefix should ensure that the environment isn't polluted and the
config isn't polluted by environment variables.

I've followed the dynaconf convention of trying to parse the
environment variable and then falling back to the raw value if parsing
fails.
  • Loading branch information
pgjones committed Mar 21, 2022
1 parent 7abcf57 commit bc46eca
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -52,6 +52,8 @@ Unreleased
:issue:`4095, 4295, 4297`
- Fix typing for ``__exit__`` methods for better compatibility with
``ExitStack``. :issue:`4474`
- Allow loading of prefixed environment variables into the Config.
:pr:`4479`


Version 2.0.3
Expand Down
47 changes: 24 additions & 23 deletions docs/config.rst
Expand Up @@ -515,9 +515,11 @@ Or from a JSON file:
Configuring from Environment Variables
--------------------------------------

In addition to pointing to configuration files using environment variables, you
may find it useful (or necessary) to control your configuration values directly
from the environment.
In addition to pointing to configuration files using environment
variables, you may find it useful (or necessary) to control your
configuration values directly from the environment. Flask can be
instructed to load all environment variables starting with a specific
prefix into the config using :meth:`~flask.Config.from_prefixed_env`.

Environment variables can be set in the shell before starting the server:

Expand All @@ -527,54 +529,53 @@ Environment variables can be set in the shell before starting the server:

.. code-block:: text
$ export SECRET_KEY="5f352379324c22463451387a0aec5d2f"
$ export MAIL_ENABLED=false
$ export FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f"
$ export FLASK_MAIL_ENABLED=false
$ flask run
* Running on http://127.0.0.1:5000/
.. group-tab:: Fish

.. code-block:: text
$ set -x SECRET_KEY "5f352379324c22463451387a0aec5d2f"
$ set -x MAIL_ENABLED false
$ set -x FLASK_SECRET_KEY "5f352379324c22463451387a0aec5d2f"
$ set -x FLASK_MAIL_ENABLED false
$ flask run
* Running on http://127.0.0.1:5000/
.. group-tab:: CMD

.. code-block:: text
> set SECRET_KEY="5f352379324c22463451387a0aec5d2f"
> set MAIL_ENABLED=false
> set FLASK_SECRET_KEY="5f352379324c22463451387a0aec5d2f"
> set FLASK_MAIL_ENABLED=false
> flask run
* Running on http://127.0.0.1:5000/
.. group-tab:: Powershell

.. code-block:: text
> $env:SECRET_KEY = "5f352379324c22463451387a0aec5d2f"
> $env:MAIL_ENABLED = "false"
> $env:FLASK_SECRET_KEY = "5f352379324c22463451387a0aec5d2f"
> $env:FLASK_MAIL_ENABLED = "false"
> flask run
* Running on http://127.0.0.1:5000/
While this approach is straightforward to use, it is important to remember that
environment variables are strings -- they are not automatically deserialized
into Python types.
The variables can then be loaded and accessed via the config with a
key equal to the environment variable name without the prefix i.e.

Here is an example of a configuration file that uses environment variables::

import os

_mail_enabled = os.environ.get("MAIL_ENABLED", default="true")
MAIL_ENABLED = _mail_enabled.lower() in {"1", "t", "true"}
.. code-block:: python
SECRET_KEY = os.environ.get("SECRET_KEY")
app.config.from_prefixed_env()
app.config["SECRET_KEY"] # Is "5f352379324c22463451387a0aec5d2f"
if not SECRET_KEY:
raise ValueError("No SECRET_KEY set for Flask application")
The prefix is ``FLASK_`` by default, however it is an configurable via
the ``prefix`` argument of :meth:`~flask.Config.from_prefixed_env`.

Whilst the value of any environment variable is a string, it will be
parsed before being placed into the flask config. By default the
parsing is done by json.loads, however this is configurable via the
``loads`` argument of :meth:`~flask.Config.from_prefixed_env`.

Notice that any value besides an empty string will be interpreted as a boolean
``True`` value in Python, which requires care if an environment explicitly sets
Expand Down
46 changes: 46 additions & 0 deletions src/flask/config.py
@@ -1,11 +1,19 @@
import errno
import json
import os
import types
import typing as t

from werkzeug.utils import import_string


def _json_loads(raw: t.Union[str, bytes]) -> t.Any:
try:
return json.loads(raw)
except json.JSONDecodeError:
return raw


class ConfigAttribute:
"""Makes an attribute forward to the config"""

Expand Down Expand Up @@ -97,6 +105,44 @@ def from_envvar(self, variable_name: str, silent: bool = False) -> bool:
)
return self.from_pyfile(rv, silent=silent)

def from_prefixed_env(
self,
prefix: str = "FLASK_",
*,
loads: t.Callable[[t.Union[str, bytes]], t.Any] = _json_loads,
) -> bool:
"""Updates the config from environment variables with the prefix.
Calling this method will result in every environment variable
starting with **prefix** being placed into the configuration
without the **prefix**. The prefix is configurable as an
argument. Note that this method updates the existing config.
For example if there is an environment variable
``FLASK_SECRET_KEY`` with value ``secretly`` and the prefix is
``FLASK_`` the config will contain the key ``SECRET_KEY`` with
the value ``secretly`` after calling this method.
The value of the environment variable will be passed to the
**loads** parameter before being placed into the config. By
default **loads** utilises the stdlib json.loads to parse the
value, falling back to the value itself on parsing error.
:param loads: A callable that takes a str (or bytes) returns
the parsed value.
:return: Always returns ``True``.
.. versionadded:: 2.1.0
"""
mapping = {}
for raw_key, value in os.environ.items():
if raw_key.startswith(prefix):
key = raw_key[len(prefix) :] # Use removeprefix with Python 3.9
mapping[key] = loads(value)

return self.from_mapping(mapping)

def from_pyfile(self, filename: str, silent: bool = False) -> bool:
"""Updates the values in the config from a Python file. This function
behaves as if the file was imported as module with the
Expand Down
24 changes: 24 additions & 0 deletions tests/test_config.py
Expand Up @@ -38,6 +38,30 @@ def test_config_from_file():
common_object_test(app)


def test_config_from_prefixed_env(monkeypatch):
app = flask.Flask(__name__)
monkeypatch.setenv("FLASK_A", "A value")
monkeypatch.setenv("FLASK_B", "true")
monkeypatch.setenv("FLASK_C", "1")
monkeypatch.setenv("FLASK_D", "1.2")
monkeypatch.setenv("NOT_FLASK_A", "Another value")
app.config.from_prefixed_env()
assert app.config["A"] == "A value"
assert app.config["B"] is True
assert app.config["C"] == 1
assert app.config["D"] == 1.2
assert "Another value" not in app.config.items()


def test_config_from_custom_prefixed_env(monkeypatch):
app = flask.Flask(__name__)
monkeypatch.setenv("FLASK_A", "A value")
monkeypatch.setenv("NOT_FLASK_A", "Another value")
app.config.from_prefixed_env("NOT_FLASK_")
assert app.config["A"] == "Another value"
assert "A value" not in app.config.items()


def test_config_from_mapping():
app = flask.Flask(__name__)
app.config.from_mapping({"SECRET_KEY": "config", "TEST_KEY": "foo"})
Expand Down

0 comments on commit bc46eca

Please sign in to comment.