mirror of https://github.com/pallets/flask.git
update env file precedence
This commit is contained in:
parent
ce08bc704e
commit
2c31603042
|
@ -17,6 +17,9 @@ Unreleased
|
|||
about resource limits to the security page. :issue:`5625`
|
||||
- Add support for the ``Partitioned`` cookie attribute (CHIPS), with the
|
||||
``SESSION_COOKIE_PARTITIONED`` config. :issue`5472`
|
||||
- ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
|
||||
``load_dotenv`` loads default files in addition to a path unless
|
||||
``load_defaults=False`` is passed. :issue:`5628`
|
||||
|
||||
|
||||
Version 3.0.3
|
||||
|
|
110
src/flask/cli.py
110
src/flask/cli.py
|
@ -297,6 +297,9 @@ class ScriptInfo:
|
|||
a bigger role. Typically it's created automatically by the
|
||||
:class:`FlaskGroup` but you can also manually create it and pass it
|
||||
onwards as click object.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
Added the ``load_dotenv_defaults`` parameter and attribute.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -304,6 +307,7 @@ class ScriptInfo:
|
|||
app_import_path: str | None = None,
|
||||
create_app: t.Callable[..., Flask] | None = None,
|
||||
set_debug_flag: bool = True,
|
||||
load_dotenv_defaults: bool = True,
|
||||
) -> None:
|
||||
#: Optionally the import path for the Flask application.
|
||||
self.app_import_path = app_import_path
|
||||
|
@ -314,6 +318,16 @@ class ScriptInfo:
|
|||
#: this script info.
|
||||
self.data: dict[t.Any, t.Any] = {}
|
||||
self.set_debug_flag = set_debug_flag
|
||||
|
||||
self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults)
|
||||
"""Whether default ``.flaskenv`` and ``.env`` files should be loaded.
|
||||
|
||||
``ScriptInfo`` doesn't load anything, this is for reference when doing
|
||||
the load elsewhere during processing.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
"""
|
||||
|
||||
self._loaded_app: Flask | None = None
|
||||
|
||||
def load_app(self) -> Flask:
|
||||
|
@ -479,23 +493,22 @@ _debug_option = click.Option(
|
|||
def _env_file_callback(
|
||||
ctx: click.Context, param: click.Option, value: str | None
|
||||
) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
import importlib
|
||||
|
||||
try:
|
||||
importlib.import_module("dotenv")
|
||||
import dotenv # noqa: F401
|
||||
except ImportError:
|
||||
raise click.BadParameter(
|
||||
"python-dotenv must be installed to load an env file.",
|
||||
ctx=ctx,
|
||||
param=param,
|
||||
) from None
|
||||
# Only show an error if a value was passed, otherwise we still want to
|
||||
# call load_dotenv and show a message without exiting.
|
||||
if value is not None:
|
||||
raise click.BadParameter(
|
||||
"python-dotenv must be installed to load an env file.",
|
||||
ctx=ctx,
|
||||
param=param,
|
||||
) from None
|
||||
|
||||
# Load if a value was passed, or we want to load default files, or both.
|
||||
if value is not None or ctx.obj.load_dotenv_defaults:
|
||||
load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults)
|
||||
|
||||
# Don't check FLASK_SKIP_DOTENV, that only disables automatically
|
||||
# loading .env and .flaskenv files.
|
||||
load_dotenv(value)
|
||||
return value
|
||||
|
||||
|
||||
|
@ -504,7 +517,11 @@ def _env_file_callback(
|
|||
_env_file_option = click.Option(
|
||||
["-e", "--env-file"],
|
||||
type=click.Path(exists=True, dir_okay=False),
|
||||
help="Load environment variables from this file. python-dotenv must be installed.",
|
||||
help=(
|
||||
"Load environment variables from this file, taking precedence over"
|
||||
" those set by '.env' and '.flaskenv'. Variables set directly in the"
|
||||
" environment take highest precedence. python-dotenv must be installed."
|
||||
),
|
||||
is_eager=True,
|
||||
expose_value=False,
|
||||
callback=_env_file_callback,
|
||||
|
@ -528,6 +545,9 @@ class FlaskGroup(AppGroup):
|
|||
directory to the directory containing the first file found.
|
||||
:param set_debug_flag: Set the app's debug flag.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files.
|
||||
|
||||
.. versionchanged:: 2.2
|
||||
Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options.
|
||||
|
||||
|
@ -654,14 +674,11 @@ class FlaskGroup(AppGroup):
|
|||
# when importing, blocking whatever command is being called.
|
||||
os.environ["FLASK_RUN_FROM_CLI"] = "true"
|
||||
|
||||
# Attempt to load .env and .flask env files. The --env-file
|
||||
# option can cause another file to be loaded.
|
||||
if get_load_dotenv(self.load_dotenv):
|
||||
load_dotenv()
|
||||
|
||||
if "obj" not in extra and "obj" not in self.context_settings:
|
||||
extra["obj"] = ScriptInfo(
|
||||
create_app=self.create_app, set_debug_flag=self.set_debug_flag
|
||||
create_app=self.create_app,
|
||||
set_debug_flag=self.set_debug_flag,
|
||||
load_dotenv_defaults=self.load_dotenv,
|
||||
)
|
||||
|
||||
return super().make_context(info_name, args, parent=parent, **extra)
|
||||
|
@ -684,18 +701,26 @@ def _path_is_ancestor(path: str, other: str) -> bool:
|
|||
return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other
|
||||
|
||||
|
||||
def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool:
|
||||
"""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.
|
||||
def load_dotenv(
|
||||
path: str | os.PathLike[str] | None = None, load_defaults: bool = True
|
||||
) -> bool:
|
||||
"""Load "dotenv" files to set environment variables. A given path takes
|
||||
precedence over ``.env``, which takes precedence over ``.flaskenv``. After
|
||||
loading and combining these files, values are only set if the key is not
|
||||
already set in ``os.environ``.
|
||||
|
||||
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.
|
||||
:param path: Load the file at this location.
|
||||
:param load_defaults: Search for and load the default ``.flaskenv`` and
|
||||
``.env`` files.
|
||||
:return: ``True`` if at least one env var was loaded.
|
||||
|
||||
.. versionchanged:: 3.1
|
||||
Added the ``load_defaults`` parameter. A given path takes precedence
|
||||
over default files.
|
||||
|
||||
.. versionchanged:: 2.0
|
||||
The current directory is not changed to the location of the
|
||||
|
@ -715,34 +740,33 @@ def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool:
|
|||
except ImportError:
|
||||
if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
|
||||
click.secho(
|
||||
" * Tip: There are .env or .flaskenv files present."
|
||||
' Do "pip install python-dotenv" to use them.',
|
||||
" * Tip: There are .env files present. Install python-dotenv"
|
||||
" to use them.",
|
||||
fg="yellow",
|
||||
err=True,
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
# Always return after attempting to load a given path, don't load
|
||||
# the default files.
|
||||
if path is not None:
|
||||
if os.path.isfile(path):
|
||||
return dotenv.load_dotenv(path, encoding="utf-8")
|
||||
data: dict[str, str | None] = {}
|
||||
|
||||
return False
|
||||
if load_defaults:
|
||||
for default_name in (".flaskenv", ".env"):
|
||||
if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)):
|
||||
continue
|
||||
|
||||
loaded = False
|
||||
data |= dotenv.dotenv_values(default_path, encoding="utf-8")
|
||||
|
||||
for name in (".env", ".flaskenv"):
|
||||
path = dotenv.find_dotenv(name, usecwd=True)
|
||||
if path is not None and os.path.isfile(path):
|
||||
data |= dotenv.dotenv_values(path, encoding="utf-8")
|
||||
|
||||
if not path:
|
||||
for key, value in data.items():
|
||||
if key in os.environ or value is None:
|
||||
continue
|
||||
|
||||
dotenv.load_dotenv(path, encoding="utf-8")
|
||||
loaded = True
|
||||
os.environ[key] = value
|
||||
|
||||
return loaded # True if at least one file was located and loaded.
|
||||
return bool(data) # True if at least one env var was loaded.
|
||||
|
||||
|
||||
def show_server_banner(debug: bool, app_import_path: str | None) -> None:
|
||||
|
|
|
@ -398,7 +398,12 @@ def test_flaskgroup_nested(app, runner):
|
|||
def test_no_command_echo_loading_error():
|
||||
from flask.cli import cli
|
||||
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
try:
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
except (DeprecationWarning, TypeError):
|
||||
# Click >= 8.2
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ["missing"])
|
||||
assert result.exit_code == 2
|
||||
assert "FLASK_APP" in result.stderr
|
||||
|
@ -408,7 +413,12 @@ def test_no_command_echo_loading_error():
|
|||
def test_help_echo_loading_error():
|
||||
from flask.cli import cli
|
||||
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
try:
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
except (DeprecationWarning, TypeError):
|
||||
# Click >= 8.2
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "FLASK_APP" in result.stderr
|
||||
|
@ -420,7 +430,13 @@ def test_help_echo_exception():
|
|||
raise Exception("oh no")
|
||||
|
||||
cli = FlaskGroup(create_app=create_app)
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
|
||||
try:
|
||||
runner = CliRunner(mix_stderr=False)
|
||||
except (DeprecationWarning, TypeError):
|
||||
# Click >= 8.2
|
||||
runner = CliRunner()
|
||||
|
||||
result = runner.invoke(cli, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Exception: oh no" in result.stderr
|
||||
|
@ -537,7 +553,7 @@ def test_load_dotenv(monkeypatch):
|
|||
# test env file encoding
|
||||
assert os.environ["HAM"] == "火腿"
|
||||
# Non existent file should not load
|
||||
assert not load_dotenv("non-existent-file")
|
||||
assert not load_dotenv("non-existent-file", load_defaults=False)
|
||||
|
||||
|
||||
@need_dotenv
|
||||
|
|
Loading…
Reference in New Issue