tr11 / python-configuration

A Python library to load configuration parameters

Home Page:https://tr11.github.io/python-configuration/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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