Skip to content

Commit

Permalink
Merge branch 'master' into 794
Browse files Browse the repository at this point in the history
  • Loading branch information
rochacbruno committed Sep 2, 2022
2 parents 3183c69 + eae0773 commit a928113
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 25 deletions.
43 changes: 35 additions & 8 deletions dynaconf/utils/__init__.py
Expand Up @@ -55,23 +55,33 @@ def object_merge(
existing_value = recursive_get(old, full_path) # doesnt handle None
# Need to make every `None` on `_store` to be an wrapped `LazyNone`

for key, value in old.items():

# data coming from source, in `new` can be mix case: KEY4|key4|Key4
# data existing on `old` object has the correct case: key4|KEY4|Key4
# So we need to ensure that new keys matches the existing keys
for new_key in list(new.keys()):
correct_case_key = find_the_correct_casing(new_key, old)
if correct_case_key:
new[correct_case_key] = new.pop(new_key)

for old_key, value in old.items():

# This is for when the dict exists internally
# but the new value on the end of full path is the same
if (
existing_value is not None
and key.lower() == full_path[-1].lower()
and old_key.lower() == full_path[-1].lower()
and existing_value is value
):
# Here Be The Dragons
# This comparison needs to be smarter
continue

if key not in new:
new[key] = value
if old_key not in new:
new[old_key] = value
else:
object_merge(
value,
new[key],
new[old_key],
full_path=full_path[1:] if full_path else None,
)

Expand All @@ -89,7 +99,7 @@ def recursive_get(
"""
if not names:
return
head, tail = names[0], names[1:]
head, *tail = names
result = getattr(obj, head, None)
if not tail:
return result
Expand All @@ -106,7 +116,6 @@ def handle_metavalues(
# MetaValue instances
if getattr(new[key], "_dynaconf_reset", False): # pragma: no cover
# a Reset on `new` triggers reasign of existing data
# @reset is deprecated on v3.0.0
new[key] = new[key].unwrap()
elif getattr(new[key], "_dynaconf_del", False):
# a Del on `new` triggers deletion of existing data
Expand Down Expand Up @@ -424,3 +433,21 @@ def isnamedtupleinstance(value):
if not isinstance(f, tuple):
return False
return all(type(n) == str for n in f)


def find_the_correct_casing(key: str, data: dict[str, Any]) -> str | None:
"""Given a key, find the proper casing in data
Arguments:
key {str} -- A key to be searched in data
data {dict} -- A dict to be searched
Returns:
str -- The proper casing of the key in data
"""
if key in data:
return key
for k in data.keys():
if k.lower() == key.lower():
return k
return None
23 changes: 6 additions & 17 deletions dynaconf/utils/boxing.py
Expand Up @@ -3,8 +3,8 @@
import inspect
from functools import wraps

from dynaconf.utils import find_the_correct_casing
from dynaconf.utils import recursively_evaluate_lazy_format
from dynaconf.utils import upperfy
from dynaconf.utils.functional import empty
from dynaconf.vendor.box import Box

Expand Down Expand Up @@ -37,15 +37,15 @@ def __getattr__(self, item, *args, **kwargs):
try:
return super().__getattr__(item, *args, **kwargs)
except (AttributeError, KeyError):
n_item = item.lower() if item.isupper() else upperfy(item)
n_item = find_the_correct_casing(item, self) or item
return super().__getattr__(n_item, *args, **kwargs)

@evaluate_lazy_format
def __getitem__(self, item, *args, **kwargs):
try:
return super().__getitem__(item, *args, **kwargs)
except (AttributeError, KeyError):
n_item = item.lower() if item.isupper() else upperfy(item)
n_item = find_the_correct_casing(item, self) or item
return super().__getitem__(n_item, *args, **kwargs)

def __copy__(self):
Expand All @@ -60,22 +60,11 @@ def copy(self):
box_settings=self._box_config.get("box_settings"),
)

def _case_insensitive_get(self, item, default=None):
"""adds a bit of overhead but allows case insensitive get
See issue: #486
"""
lower_self = {k.casefold(): v for k, v in self.items()}
return lower_self.get(item.casefold(), default)

@evaluate_lazy_format
def get(self, item, default=None, *args, **kwargs):
if item not in self: # toggle case
item = item.lower() if item.isupper() else upperfy(item)
value = super().get(item, empty, *args, **kwargs)
if value is empty:
# see Issue: #486
return self._case_insensitive_get(item, default)
return value
n_item = find_the_correct_casing(item, self) or item
value = super().get(n_item, empty, *args, **kwargs)
return value if value is not empty else default

def __dir__(self):
keys = list(self.keys())
Expand Down
41 changes: 41 additions & 0 deletions tests/test_yaml_loader.py
Expand Up @@ -493,3 +493,44 @@ def test_should_NOT_duplicate_when_explicit_set(tmpdir):
"script1.sh",
# merge_unique does not duplicate, but overrides the order
]


def test_empty_yaml_key_overriding(tmpdir):
new_key_value = "new_key_value"
os.environ["DYNACONF_LEVEL1__KEY"] = new_key_value
os.environ["DYNACONF_LEVEL1__KEY2"] = new_key_value
os.environ["DYNACONF_LEVEL1__key3"] = new_key_value
os.environ["DYNACONF_LEVEL1__KEY4"] = new_key_value
os.environ["DYNACONF_LEVEL1__KEY5"] = new_key_value

tmpdir.join("test.yml").write(
"""
level1:
key: key_value
KEY2:
key3:
keY4:
"""
)

for merge_state in (True, False):
_settings = LazySettings(
settings_files=["test.yml"], merge_enabled=merge_state
)
assert _settings.level1.key == new_key_value
assert _settings.level1.key2 == new_key_value
assert _settings.level1.key3 == new_key_value
assert _settings.level1.get("KEY4") == new_key_value
assert _settings.level1.get("key4") == new_key_value
assert _settings.level1.get("keY4") == new_key_value
assert _settings.level1.get("keY6", "foo") == "foo"
assert _settings.level1.get("KEY6", "bar") == "bar"
assert _settings.level1["Key4"] == new_key_value
assert _settings.level1.Key4 == new_key_value
assert _settings.level1.KEy4 == new_key_value
assert _settings.level1.KEY4 == new_key_value
assert _settings.level1.key4 == new_key_value
with pytest.raises(AttributeError):
_settings.level1.key6
_settings.level1.key7
_settings.level1.KEY8

0 comments on commit a928113

Please sign in to comment.