diff --git a/CHANGES.rst b/CHANGES.rst index ec3009a3f6..633e6ea7c4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/docs/config.rst b/docs/config.rst index 0b86674d88..9e66e01e50 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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: @@ -527,8 +529,8 @@ 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/ @@ -536,8 +538,8 @@ Environment variables can be set in the shell before starting the server: .. 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/ @@ -545,8 +547,8 @@ Environment variables can be set in the shell before starting the server: .. 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/ @@ -554,27 +556,26 @@ Environment variables can be set in the shell before starting the server: .. 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 diff --git a/src/flask/config.py b/src/flask/config.py index 9657edc80a..c02db27218 100644 --- a/src/flask/config.py +++ b/src/flask/config.py @@ -1,4 +1,5 @@ import errno +import json import os import types import typing as t @@ -6,6 +7,13 @@ 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""" @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index a3cd3d25bd..e2d5cd632a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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"})