Skip to content

Commit

Permalink
Fix #976 from envvars parse True/False as booleans (#983)
Browse files Browse the repository at this point in the history
Only when reading from envvars True and False will be transformed to lowercase to allow toml parser.
  • Loading branch information
rochacbruno committed Aug 22, 2023
1 parent 297eefc commit 97366d6
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 9 deletions.
6 changes: 6 additions & 0 deletions docs/envvars.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export DYNACONF_STRING_NUM="'76'" # str: "76"
export DYNACONF_PERSON__IS_ADMIN=true # bool: True (nested)
```

!!! info
For envvars Dynaconf will automatically transform `True` and `False` to `true` and `false` respectively,
this transformation allows TOML to parse the value as a boolean.
If you want to keep the original value in this case use `@str` or wrap it in quotes twice like `FOO='"True"'`
Note that this applies only for strictly string equal to `True|False` doesn't apply to same string found
inside toml data structures.

With the above it is now possible to read the settings in your `program.py` using:

Expand Down
10 changes: 7 additions & 3 deletions dynaconf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from dynaconf.utils.functional import empty
from dynaconf.utils.functional import LazyObject
from dynaconf.utils.parse_conf import apply_converter
from dynaconf.utils.parse_conf import boolean_fix
from dynaconf.utils.parse_conf import converters
from dynaconf.utils.parse_conf import Lazy
from dynaconf.utils.parse_conf import parse_conf_data
Expand Down Expand Up @@ -157,7 +158,8 @@ def _should_load_dotenv(self):
"""Chicken and egg problem, we must manually check envvar
before deciding if we are loading envvars :)"""
_environ_load_dotenv = parse_conf_data(
os.environ.get("LOAD_DOTENV_FOR_DYNACONF"), tomlfy=True
boolean_fix(os.environ.get("LOAD_DOTENV_FOR_DYNACONF")),
tomlfy=True,
)
return self._kwargs.get("load_dotenv", _environ_load_dotenv)

Expand Down Expand Up @@ -498,7 +500,7 @@ def get(
sysenv_fallback, list
) and key in [upperfy(k) for k in sysenv_fallback]
if sysenv_fallback is True or key_in_sysenv_fallback_list:
default = self.environ.get(key)
default = self.get_environ(key, cast=True)

# default values should behave exactly Dynaconf parsed values
if default is not None:
Expand Down Expand Up @@ -561,7 +563,9 @@ def get_environ(self, key, default=None, cast=None):
if cast in converters:
data = apply_converter(cast, data, box_settings=self)
elif cast is True:
data = parse_conf_data(data, tomlfy=True, box_settings=self)
data = parse_conf_data(
boolean_fix(data), tomlfy=True, box_settings=self
)
return data

def exists_in_environ(self, key):
Expand Down
3 changes: 2 additions & 1 deletion dynaconf/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dynaconf.utils import upperfy
from dynaconf.utils import warn_deprecations
from dynaconf.utils.files import find_file
from dynaconf.utils.parse_conf import boolean_fix
from dynaconf.utils.parse_conf import parse_conf_data
from dynaconf.vendor.dotenv import load_dotenv

Expand All @@ -33,7 +34,7 @@ def get(key, default=None):
value = try_renamed(key, value, old, new)

return (
parse_conf_data(value, tomlfy=True, box_settings={})
parse_conf_data(boolean_fix(value), tomlfy=True, box_settings={})
if value is not None
else default
)
Expand Down
9 changes: 5 additions & 4 deletions dynaconf/loaders/env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from dynaconf.loaders.base import SourceMetadata
from dynaconf.utils import missing
from dynaconf.utils import upperfy
from dynaconf.utils.parse_conf import boolean_fix
from dynaconf.utils.parse_conf import parse_conf_data

DOTENV_IMPORTED = False
Expand Down Expand Up @@ -75,14 +76,14 @@ def load_from_env(
try: # obj is a Settings
obj.set(
key,
value,
boolean_fix(value),
loader_identifier=source_metadata,
tomlfy=True,
validate=validate,
)
except AttributeError: # obj is a dict
obj[key] = parse_conf_data(
value, tomlfy=True, box_settings=obj
boolean_fix(value), tomlfy=True, box_settings=obj
)

# Load environment variables in bulk (when matching).
Expand All @@ -94,9 +95,9 @@ def load_from_env(
trim_len = len(env_)
data = {
key[trim_len:]: parse_conf_data(
data, tomlfy=True, box_settings=obj
boolean_fix(value), tomlfy=True, box_settings=obj
)
for key, data in environ.items()
for key, value in environ.items()
if key.startswith(env_)
and not (
# Ignore environment variables that haven't been
Expand Down
14 changes: 14 additions & 0 deletions dynaconf/utils/parse_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,17 @@ def unparse_conf_data(value):
return "@none "

return value


def boolean_fix(value: str | None):
"""Gets a value like `True/False` and turns to `true/false`
This function exists because of issue #976
Toml parser casts booleans from true/false lower case
however envvars are usually exportes as True/False capitalized
by mistake, this helper fixes it for envvars only.
Assume envvars are always str.
"""
if value and value.strip() in ("True", "False"):
return value.lower()
return value
2 changes: 1 addition & 1 deletion netlify.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build]
# if commit is not tagged don't build
# ignore = "git describe --exact-match && exit 1 || exit 0"
ignore = "git describe --exact-match --tags HEAD && exit 1 || exit 0"
publish = "site"
command = "pip install -r requirements.txt && mkdocs build --clean"

Expand Down
16 changes: 16 additions & 0 deletions tests/test_env_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,3 +505,19 @@ def test_filtering_unknown_variables_with_prefix():
assert not settings.get("IGNOREME")
# Smoke test.
assert settings.get("MYCONFIG") == "ham"


def test_boolean_fix():
environ["BOOLFIX_CAPITALTRUE"] = "True"
environ["BOOLFIX_CAPITALFALSE"] = "False"
settings.IGNORE_UNKNOWN_ENVVARS_FOR_DYNACONF = False
load_from_env(
obj=settings,
prefix="BOOLFIX",
key=None,
silent=True,
identifier="env_global",
env=False,
)
assert settings.CAPITALTRUE is True
assert settings.CAPITALFALSE is False
11 changes: 11 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from dynaconf.utils import upperfy
from dynaconf.utils.files import find_file
from dynaconf.utils.files import get_local_filename
from dynaconf.utils.parse_conf import boolean_fix
from dynaconf.utils.parse_conf import evaluate_lazy_format
from dynaconf.utils.parse_conf import Formatters
from dynaconf.utils.parse_conf import Lazy
Expand Down Expand Up @@ -508,3 +509,13 @@ def test_add_converter_path_example(tmp_path):
)
settings = Dynaconf(settings_file=fn)
assert isinstance(settings.my_path, Path)


def test_boolean_fix():
"""Assert boolean fix works"""
assert boolean_fix("True") == "true"
assert boolean_fix("False") == "false"
assert boolean_fix("NotOnlyTrue") == "NotOnlyTrue"
assert boolean_fix("TrueNotOnly") == "TrueNotOnly"
assert boolean_fix("FalseNotOnly") == "FalseNotOnly"
assert boolean_fix("NotOnlyFalse") == "NotOnlyFalse"
4 changes: 4 additions & 0 deletions tests_functional/issues/976_True_False/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DYNACONF_UPPERTRUE=True
DYNACONF_UPPERFALSE=False
DYNACONF_LOWERTRUE=true
DYNACONF_LOWERFALSE=false
23 changes: 23 additions & 0 deletions tests_functional/issues/976_True_False/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from dynaconf import Dynaconf

settings = Dynaconf(
settings_file="settings.toml",
load_dotenv=True,
)


# TOML must rely on its own type system
assert settings.TOMLUPPERTRUE == "True"
assert settings.TOMLUPPERFALSE == "False"
assert settings.TOMLLOWERTRUE is True
assert settings.TOMLLOWERFALSE is False

# ENV vars must transform `True` to `true` and `False` to `false`
assert settings.UPPERTRUE is True, settings.UPPERTRUE
assert settings.UPPERFALSE is False, settings.UPPERFALSE
assert settings.LOWERTRUE is True, settings.LOWERTRUE
assert settings.LOWERFALSE is False, settings.LOWERFALSE

print("BOOLEAN FIX FOR ENV VARS WORKS")
4 changes: 4 additions & 0 deletions tests_functional/issues/976_True_False/settings.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
tomllowertrue = true
tomllowerfalse = false
tomluppertrue = "True"
tomlupperfalse = "False"

0 comments on commit 97366d6

Please sign in to comment.