From 64601ad00ff5149908a02c67e045da32374d58b0 Mon Sep 17 00:00:00 2001 From: Tiago Requeijo Date: Tue, 4 Aug 2020 23:09:42 -0400 Subject: [PATCH] Added dotted interpolation. Fixes #36. --- config/configuration.py | 4 ++-- config/configuration_set.py | 2 +- config/helpers.py | 23 +++++++++++++++++------ tests/test_interpolation.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/config/configuration.py b/config/configuration.py index 77e0ffd..f3d35eb 100644 --- a/config/configuration.py +++ b/config/configuration.py @@ -152,7 +152,7 @@ def __getitem__(self, item: str) -> Union["Configuration", Any]: # noqa: D105 if isinstance(v, dict): return Configuration(v) elif self._interpolate is not False: - d = self.as_dict() + d = self.as_attrdict() d.update(cast(Dict[str, str], self._interpolate)) return interpolate_object(item, v, [d], self._interpolate_type) else: @@ -173,7 +173,7 @@ def get(self, key: str, default: Any = None) -> Union[dict, Any]: def as_dict(self) -> dict: """Return the representation as a dictionary.""" - return self._config + return deepcopy(self._config) def as_attrdict(self) -> AttributeDict: """Return the representation as an attribute dictionary.""" diff --git a/config/configuration_set.py b/config/configuration_set.py index 677b689..a40e006 100644 --- a/config/configuration_set.py +++ b/config/configuration_set.py @@ -78,7 +78,7 @@ def _from_configs(self, attr: str, *args: Any, **kwargs: dict) -> Any: result.update(v) return Configuration(result) elif self._interpolate is not False: - d = [d.as_dict() for d in self._configs] + d = [c.as_attrdict() for c in self._configs] d[0].update(cast(Dict[str, str], self._interpolate)) return interpolate_object(args[0], values[0], d, self._interpolate_type) else: diff --git a/config/helpers.py b/config/helpers.py index 9592909..f967670 100644 --- a/config/helpers.py +++ b/config/helpers.py @@ -2,7 +2,7 @@ import string from enum import Enum -from typing import Any, Dict, List, Set, Tuple, Union +from typing import Any, Dict, Sequence, Set, Tuple, Union TRUTH_TEXT = frozenset(("t", "true", "y", "yes", "on", "1")) @@ -85,6 +85,13 @@ def clean(key: str, value: Any, mask: str = "******") -> Any: return value +def getattr_dotted(d: dict, item: str) -> Any: + """Return the getattr method but looking through dotted items.""" + for it in item.split("."): + d = d[it] + return d + + def interpolate_standard(text: str, d: dict, found: Set[Tuple[str, ...]]) -> str: """ Return the string interpolated as many times as needed. @@ -93,6 +100,8 @@ def interpolate_standard(text: str, d: dict, found: Set[Tuple[str, ...]]) -> str :param d: dictionary :param found: variables found so far """ + from .configuration import Configuration + if not isinstance(text, str): return text @@ -108,14 +117,16 @@ def interpolate_standard(text: str, d: dict, found: Set[Tuple[str, ...]]) -> str else: found.add(variables) - interpolated = {v: interpolate_standard(d[v], d, found) for v in variables} + interpolated = Configuration( + {v: interpolate_standard(getattr_dotted(d, v), d, found) for v in variables} + ).as_attrdict() # convert to attribute dict to play well with .format return text.format(**interpolated) def interpolate_deep( attr: str, text: str, - d: List[dict], + d: Sequence[dict], resolved: Dict[str, str], levels: Dict[str, int], method: InterpolateEnumType, @@ -154,7 +165,7 @@ def interpolate_deep( levels[variable] = level + 1 new_d = ( - ([{}] * level) + d[level:] + ([{}] * level) + d[level:] # type: ignore if method == InterpolateEnumType.DEEP_NO_BACKTRACK else d ) @@ -165,7 +176,7 @@ def interpolate_deep( return text.format(**resolved) -def flatten(d: List[dict]) -> dict: +def flatten(d: Sequence[dict]) -> dict: """ Flatten a list of dictionaries. @@ -177,7 +188,7 @@ def flatten(d: List[dict]) -> dict: def interpolate_object( - attr: str, obj: Any, d: List[dict], method: InterpolateEnumType + attr: str, obj: Any, d: Sequence[dict], method: InterpolateEnumType ) -> Any: """ Return the interpolated object. diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 65bb18e..c8887b9 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -269,3 +269,19 @@ def test_interpolation_same_variable_4(): # type: ignore ) assert cfg.var2 == "test/a/b" # var2(2) --> var1(2) --> var1(1) --> var2(1) assert cfg.var1 == "test/a" # var1(2) --> var1(1) --> var2(1) + + +def test_interpolation_dotted(): # type: ignore + values_1 = {"a.b": "value"} + values_2 = {"var": "{a.b}"} + + cfg = config(values_2, values_1, lowercase_keys=True, interpolate=True,) + + assert cfg.var == "value" + + values_1 = {"a": {"b": "value"}} + values_2 = {"var": "{a.b}"} + + cfg = config(values_2, values_1, lowercase_keys=True, interpolate=True,) + + assert cfg.var == "value"