Skip to content

Commit

Permalink
TODO: Fix lookup for the root of the node
Browse files Browse the repository at this point in the history
  • Loading branch information
rochacbruno committed Sep 11, 2023
1 parent e4dc61e commit 9143590
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 52 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ dynaconf/vendor/source

# asdf (version manager)
.tool-versions
.ropeproject
151 changes: 102 additions & 49 deletions dynaconf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
from dynaconf.validator import ValidatorList
from dynaconf.vendor.box.box_list import BoxList

notfound = object()


class LazySettings(LazyObject):
"""Loads settings lazily from multiple sources:
Expand Down Expand Up @@ -243,13 +245,21 @@ def __init__(self, settings_module=None, **kwargs): # pragma: no cover
self._validate_only_current_env = kwargs.pop(
"validate_only_current_env", False
)

_validators = kwargs.pop("validators", None)
self.validators = ValidatorList(self)
self._validators_defaults = defaultdict(dict)
self._validators_casts = defaultdict(dict)
_validators = kwargs.pop("validators", [])
self._post_hooks: list[Callable] = ensure_a_list(
kwargs.get("post_hooks", [])
)

compat_kwargs(kwargs)
# execute loaders only after setting defaults got from kwargs
self._defaults = kwargs

# Set init kwargs and SETTINGS_MODULE
# below this line Settings must have all its internal
# attributes set, otherwise set will fail.
if settings_module:
self.set(
"SETTINGS_FILE_FOR_DYNACONF",
Expand All @@ -258,8 +268,6 @@ def __init__(self, settings_module=None, **kwargs): # pragma: no cover
)
for key, value in kwargs.items():
self.set(key, value, loader_identifier="init_kwargs")
# execute loaders only after setting defaults got from kwargs
self._defaults = kwargs

# The following flags are used for when copying of settings is done
skip_loaders = kwargs.get("dynaconf_skip_loaders", False)
Expand All @@ -268,9 +276,7 @@ def __init__(self, settings_module=None, **kwargs): # pragma: no cover
if not skip_loaders:
self.execute_loaders()

self._validators_defaults = defaultdict(dict)
self._validators_casts = defaultdict(dict)
self.validators = ValidatorList(self, validators=_validators)
self.validators.register(*_validators)
if not skip_validators:
self.validators.validate(
only=self._validate_only,
Expand Down Expand Up @@ -423,7 +429,13 @@ def as_dict(self, env=None, internal=False):
to_dict = as_dict # backwards compatibility

def _dotted_get(
self, dotted_key, default=None, parent=None, cast=None, **kwargs
self,
dotted_key,
default=None,
parent=None,
cast=None,
apply_default_on_none=False,
**kwargs,
):
"""
Perform dotted key lookups and keep track of where we are.
Expand All @@ -433,19 +445,33 @@ def _dotted_get(
"""
split_key = dotted_key.split(".")
name, keys = split_key[0], split_key[1:]
result = self.get(name, default=default, parent=parent, **kwargs)
result = self.get(
name,
default=default,
parent=parent,
lookup_validator_metadata=False,
apply_default_on_none=apply_default_on_none,
**kwargs,
)

# If we've reached the end, or parent key not found, then return result
if not keys or result == default:
if cast and cast in converters:
return apply_converter(cast, result, box_settings=self)
elif cast is True:
return parse_conf_data(result, tomlfy=True, box_settings=self)
if name.lower() == "key1":
print("BBBBBB", result, keys, cast)
return result

# If we've still got key elements to traverse, let's do that.
return self._dotted_get(
".".join(keys), default=default, parent=result, cast=cast, **kwargs
".".join(keys),
default=default,
parent=result,
cast=cast,
apply_default_on_none=apply_default_on_none,
**kwargs,
)

def get(
Expand All @@ -457,6 +483,8 @@ def get(
dotted_lookup=empty,
parent=None,
sysenv_fallback=None,
lookup_validator_metadata=True,
apply_default_on_none=False,
):
"""
Get a value from settings store, this is the preferred way to access::
Expand Down Expand Up @@ -484,28 +512,87 @@ def get(
if dotted_lookup is empty:
dotted_lookup = self._store.get("DOTTED_LOOKUP_FOR_DYNACONF")

key = upperfy(key)
# internal attributes are returned as is on store
if key in UPPER_DEFAULT_SETTINGS + RESERVED_ATTRS:
return self._store.get(key, default)

if lookup_validator_metadata:
# get default and cast registered from validator
validator_metadata = self._get_validator_metadata_for_key(key)
v_default, v_cast, v_adn = validator_metadata

if cast is None and v_cast is not empty:
cast = v_cast

if v_default is not empty:
default = v_default

if v_adn is not empty:
apply_default_on_none = v_adn

# handles system environment fallback
if v_default is empty and default is None:
key_in_sysenv_fallback_list = isinstance(
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.get_environ(key, cast=True)

# default values should behave exactly Dynaconf parsed values
if default is not None:
if isinstance(default, list):
default = BoxList(default)
elif isinstance(default, dict):
default = DynaBox(default)

if key in self._deleted:
return default

if (
fresh
or self._fresh
or key in getattr(self, "FRESH_VARS_FOR_DYNACONF", ())
) and key not in UPPER_DEFAULT_SETTINGS:
self.unset(key)
self.execute_loaders(key=key)

if "." in key and dotted_lookup:
return self._dotted_get(
dotted_key=key,
default=default,
cast=cast,
fresh=fresh,
parent=parent,
apply_default_on_none=apply_default_on_none,
)

key = upperfy(key)
value = (parent or self.store).get(key, default)
if value is None and apply_default_on_none:
value = default

if cast:
value = apply_converter(cast, value, box_settings=self)

if key.lower() == "hasemptyvalues":
print("AAAAAAAAA", value, default, apply_default_on_none)

return value

def _get_validator_metadata_for_key(self, key):
# handle default and cast from validators
apply_default_on_none = False
fallback_value = empty
cast = empty
if key not in UPPER_DEFAULT_SETTINGS + RESERVED_ATTRS:
with suppress(AttributeError):
# Handle default from v;alidators
# Handle default from validators
validators_defaults = self._validators_defaults[
self.current_env.lower()
]
validator_with_default = validators_defaults.get(key, empty)
if validator_with_default is not empty:
default = (
fallback_value = (
validator_with_default.default(
self, validator_with_default
)
Expand All @@ -520,42 +607,8 @@ def get(
validators_casts = self._validators_casts[
self.current_env.lower()
]
cast = validators_casts.get(key, cast)

# handles system environment fallback
if default is None:
key_in_sysenv_fallback_list = isinstance(
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.get_environ(key, cast=True)

# default values should behave exactly Dynaconf parsed values
if default is not None:
if isinstance(default, list):
default = BoxList(default)
elif isinstance(default, dict):
default = DynaBox(default)

if key in self._deleted:
return default

if (
fresh
or self._fresh
or key in getattr(self, "FRESH_VARS_FOR_DYNACONF", ())
) and key not in UPPER_DEFAULT_SETTINGS:
self.unset(key)
self.execute_loaders(key=key)

value = (parent or self.store).get(key, default)
if value is None and apply_default_on_none:
value = default

if cast:
value = apply_converter(cast, value, box_settings=self)

return value
cast = validators_casts.get(key, empty)
return fallback_value, cast, apply_default_on_none

def exists(self, key, fresh=False):
"""Check if key exists
Expand Down
9 changes: 9 additions & 0 deletions dynaconf/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from dynaconf import validator_conditions
from dynaconf.utils import ensure_a_list
from dynaconf.utils import upperfy
from dynaconf.utils.boxing import DynaBox
from dynaconf.utils.functional import empty
from dynaconf.vendor.box import BoxList

if TYPE_CHECKING:
from dynaconf.base import LazySettings, Settings
Expand Down Expand Up @@ -139,7 +141,13 @@ def __init__(
self.when = when
self.cast = cast
self.operations = operations

if isinstance(default, list):
default = BoxList(default)
elif isinstance(default, dict):
default = DynaBox(default)
self.default = default

self.description = description
self.envs: Sequence[str] | None = None
self.apply_default_on_none = apply_default_on_none
Expand Down Expand Up @@ -480,6 +488,7 @@ def register_casts(self, validator):
casts = self.settings._validators_casts[env.lower()]
for name in validator.names:
casts[upperfy(name)] = validator.cast
casts[upperfy(name).split(".")[0]]

def descriptions(self, flat: bool = False) -> dict[str, str | list[str]]:

Expand Down
8 changes: 5 additions & 3 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,10 +844,12 @@ def test_use_default_value_when_yaml_is_empty_and_explicitly_marked(tmpdir):
Validator("hasemptyvalues.key4", default="value4"),
],
)
settings.validators.validate()
__import__("ipdb").set_trace()
assert settings.hasemptyvalues.key1 == "value1"
assert settings.hasemptyvalues.key2 is None
assert settings.hasemptyvalues.key3 is None
assert settings.hasemptyvalues.key4 == "value4"
# assert settings.hasemptyvalues.key2 is None
# assert settings.hasemptyvalues.key3 is None
# assert settings.hasemptyvalues.key4 == "value4"


def test_ensure_cast_happens_after_must_exist(tmpdir):
Expand Down

0 comments on commit 9143590

Please sign in to comment.