Last active
October 12, 2017 15:58
-
-
Save akatrevorjay/91d81696ba86b2ca990a46d7deaefdc3 to your computer and use it in GitHub Desktop.
Format each string value of dictionary using values contained within itself, keeping track of dependencies as required.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import re | |
def format_dict_recursively(mapping, raise_unresolvable=True, strip_unresolvable=False, conversions={'True': True, 'False': False}): | |
"""Format each string value of dictionary using values contained within | |
itself, keeping track of dependencies as required. | |
Also converts any formatted values according to conversions dict. | |
Example: | |
>>> c = dict(wat='wat{omg}', omg=True) | |
>>> format_dict_recursively(c) | |
{'omg': True, 'wat': 'watTrue'} | |
Dealing with missing (unresolvable) keys in format strings: | |
>>> c = dict(wat='wat{omg}', omg=True, fail='no{whale}') | |
>>> format_dict_recursively(c) | |
Traceback (most recent call last): | |
... | |
ValueError: Impossible to format dict due to missing elements: {'fail': ['whale']} | |
>>> format_dict_recursively(c, raise_unresolvable=False) | |
{'fail': 'no{whale}', 'omg': True, 'wat': 'watTrue'} | |
>>> format_dict_recursively(c, raise_unresolvable=False, strip_unresolvable=True) | |
{'omg': True, 'wat': 'watTrue'} | |
:param dict mapping: Dict. | |
:param bool raise_unresolvable: Upon True, raises ValueError upon an unresolvable key. | |
:param bool strip_unresolvable: Upon True, strips unresolvable keys. | |
:param dict conversions: Mapping of {from: to}. | |
""" | |
if conversions is None: | |
conversions = {} | |
ret = {} | |
# Create dependency mapping | |
deps = {} | |
for k, v in mapping.items(): | |
# Do not include multiline values in this to avoid KeyErrors on actual | |
# .format below | |
if isinstance(v, six.string_types) and '\n' not in v: | |
# Map key -> [*deps] | |
# This is a bit naive, but it works well. | |
deps[k] = re.findall(r'\{(\w+)\}', v) | |
else: | |
ret[k] = v | |
while len(ret) != len(mapping): | |
ret_key_count_at_start = len(ret) | |
sret = set(ret) | |
keys = set(mapping) - sret | |
for k in keys: | |
needed = (x not in ret for x in deps[k]) | |
if any(needed): | |
continue | |
ret[k] = mapping[k].format(**ret) | |
if ret[k] in conversions: | |
ret[k] = conversions[ret[k]] | |
# We have done all that we can here. | |
if ret_key_count_at_start == len(ret): | |
if not raise_unresolvable: | |
if not strip_unresolvable: | |
# backfill | |
ret.update({k: mapping[k] for k in keys}) | |
break | |
missing = {k: [x for x in deps[k] if x not in ret]} | |
raise ValueError('Impossible to format dict due to missing elements: %r' % missing) | |
return ret |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment