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

interpolation literals

SGStino opened this issue · comments

consider the following configuration:

config = config_from_dict({
        'something': 'value_of_something',
        'interpolatable': 'say {something} {{literal}}'
    }, interpolate=True)

when accessing config.interpolatable, you'd receive a KeyError that literal can't be found.

I'm using the configuration to pass a formattable string to the application, but the format variables are only known at runtime, and aren't inside the config:

config.interpolatable.format(literal="newvalue")

and i'd expect the output to be 'say value_of_something newvalue'

Am I trying to abuse the feature, or is there a bug somewhere?

it seems that {something} is causing the problem with {{literal}}:
when i only have:

config = config_from_dict({
        'something': 'value_of_something',
        'interpolatable': 'say {{literal}}'
    }, interpolate=True)

then

config.interpolatable.format(literal="newvalue") 

works fine and outputs: 'say newvalue'

Interesting use case, I didn't build for that but seems like a very useful feature. Would you want to specify extra values to interpolate at the config level or the attribute level?
For the former, something like config(... , interpolate={"literal": "newvalue"}) would probably do. The latter is a bit more cumbersome as the attribute needs to be resolved properly before passing it to the format function.

it seems that {something} is causing the problem with {{literal}}

The issue is that the library tries to recursively interpolate the keys until it finds nothing else. In the first case, it first interpolates the {something} and then tries to look for the literal. In the second case, there's nothing to interpolate so the literal interpolation works fine.

I can definitely modify the logic to account for the case when we don't want to interpolate certain strings.

I've been tinkering around it a bit, and couldn't really find a clean solution.

But considering the second example works, i think the recursive looking for interpolatable strings should stop at some point, with the same logic as not starting to look for it in the first place?

Like you said: it tries to look for literal, can't find it, and stops if it's the first iteration, but it doesn't stop when it's the Nth iteration?

But maybe there's something more fundamental that could resolve the issue?

When we have the recursive interpolation config:

{
"var_a": "final",
"var_b": "something {var_a}",
"var_c": "something {var_b}"
}

if I understand the recursion correctly, it would mean this:

  • something {var_b}
  • something something {var_a}
  • something something final

that means the code would be something along the lines of:

"something {var_b}".format(var_b="something {var_a}").format(var_a="final")

wouldn't it be cleaner to change this to:

"something {var_b}".format(var_b="something {var_a}".format(var_a="final")

that'd also mean, that if there are escaped literals, they would just propagate through into the final string?

however, i must be missing something, because I can't seem to construct an example like this that would have a problem with the escaped literal? I guess i'll have to dig into the source code to find out where the interpolation actually happens.

I'll be back after a bit more experimenting

Came up with a very quick recursive interpolator as a test for my theory:

This is quick and dirty, and I suppose when implemented, that Formatter().parse bit and the actual .format should be taken together?

from string import Formatter
class Interpolator:
    def __init__(self, cfg):
        self.cfg = cfg
        self.cache = {}
        
    def keys(self):
        return self.cfg.keys()
    def __getattr__(self, name):      
        
        cached = self.cache.get(name, None)
        if(cached is None):        
            value = self.cfg[name] 
            keys = [n for _, n, _, _ in Formatter().parse(value) if n is not None]
            values = {key:self[key] for key in keys}
            cached = value.format(**values)
            self.cache[name] = cached
        return cached
    def __getitem__(self, name):
        return self.__getattr__(name)

so, i basically use the config as a literal value store and push it through that little class afterwards:

cfg = config.config_from_dict({
    "var_a": "final {{literal}}",
    "var_b": "something {var_a}",
    "var_c": "something something {var_b}",
    "var_d": "combine {var_a} with {var_b}"
}, interpolate=False)
cfg2 = Interpolator(cfg)

then i can do:

print(cfg2.var_d)

and it'll output:
'combine final {literal} with something final {literal}'

on which i can, at runtime, call then .format(literal=current_literal_value)

I just made it as a wrapper class around the config, I'll have to dig into the code to find out how exactly you're doing the interpolation.

And it would seem that the interpolation does a lot more than what i'm doing here, with tracking of the nested level depth (from reading the circular reference issue)

EDIT: i see the difference in approach, i'll try if i can replicate it with the cached recursion.

@SGStino, take a look at #25. It should do what you want. I also added the behavior I mentioned on #24 (comment).

That looks like it's doing exactly what I was trying to accomplish, thanks!