suggestion: nested interpolation
SGStino opened this issue · comments
While checking pull request #25 I've noticed that python's .format() supports nested objects:
class Test():
pass
test = Test()
test.value = 15
"data.value = {data.value}".format(data=test)
would spit out data.value = 15
however while interpolating, it would raise a KeyError for "data", because the format's kwargs would contain the key "data.value" instead of data.
so applying a simple unflaten
method on the kwargs would convert {"data.value":15}
to {"data":{"value":15}}
And the exception would change to AttributeError: 'dict' object has no attribute 'value'
.
Which is fixable by using Munch's munchify or something like it that converts dictionaries to objects.
So in #25, when just adding two method calls:
interpolated = {v: interpolate(d[v], d, found) for v in variables}
+++ interpolated = munchify(unflatten(interpolated))
return text.format(**interpolated)
we suddenly get support for nested objects.
Unflatten from stackoverflow
def unflatten(dictionary):
resultDict = dict()
for key, value in dictionary.items():
parts = key.split(".")
d = resultDict
for part in parts[:-1]:
if part not in d:
d[part] = dict()
d = d[part]
d[parts[-1]] = value
return resultDict
and munchify is from infinidat/munch
However, a lot of what both methods are doing is exactly what the Configuration
class itself is doing: the attribute notation and nesting is supported out of the box in the Configuration objects.
Except that it's keys()
method returns the full paths instead of only the root levels:
cfg = Configuration({'data.value':15})
cfg.data.value
would successfully output 15.
so "{data.value}".format(data = cfg.data)
should output 15
too.
and it does, but only if I force the keys()
to output ['data']
instead of ['data.value']
:
kwargs = config.Configuration(dict(data=cfg.data.as_dict()))
# hack the keys to make it work:
kwargs.keys = lambda: ['data']
"{data.value}".format(**kwargs)
So i'd suggest letting the Configuration class's keys only output the top level keys if that doesn't break to much, otherwise a simple wrapper would suffice:
from config import Configuration
class ConfigurationWrapper:
def __init__(self, data : Configuration):
self.data = data
# the important modification
def keys(self):
return set(k.split('.',2)[0] for k in self.data.keys())
# just pass through to the Configuration class
def __getattr__(self, name):
value = self.data[name]
if(isinstance(value, Configuration)):
return ConfigurationWrapper(value)
return value
def __getitem__(self, name):
return self.__getattr__(name)
Running the following test:
# test config
cfg = ConfigurationWrapper(config.Configuration({'data.value':15, 'data.nested.value2':16}))
print("{data.value} and {data.nested.value2}".format(**cfg))
would correctly output 15 and 16
.
That would change the #25 interpolation method like this:
interpolated = {v: interpolate(d[v], d, found) for v in variables}
+++ interpolated = ConfigurationWrapper(Configuration(interpolated))
return text.format(**interpolated)
Or without the ConfigurationWrapper
if Configuration
can be changed.
This already kind of works using the levels keyword that configuration instances have for the keys
, items
, values
. Basically, this would work as you want if we default levels=1
, which essentially gives you the root keys.
Implemented in #29. Note that, as suggested, this changes the default behavior of all iterable-related methods (keys, values, items, len, iter).
Closed by #29