[formatter] implement field name alternatives (#525)

The format string '{a|b|c}' will now try to use the value from 'a' and
fall back to 'b' and 'c' if accessing a field raises an exception or
if its value is None.
This commit is contained in:
Mike Fährmann
2020-02-15 17:58:21 +01:00
parent 67ea80222c
commit 7d1da614d9
2 changed files with 80 additions and 33 deletions

View File

@@ -481,30 +481,54 @@ class Formatter():
self.format_map = self.fields[0][1]
else:
self.format_map = lambda _: format_string
del self.result
del self.fields
del self.result, self.fields
def format_map(self, kwargs):
"""Apply 'kwargs' to the initial format_string and return its result"""
def format_map(self, kwdict):
"""Apply 'kwdict' to the initial format_string and return its result"""
result = self.result
for index, func in self.fields:
self.result[index] = func(kwargs)
return "".join(self.result)
result[index] = func(kwdict)
return "".join(result)
def _field_access(self, field_name, format_spec, conversion):
first, rest = _string.formatter_field_name_split(field_name)
fmt = self._build_format_func(format_spec, conversion)
if "|" in field_name:
return self._apply_list([
self._parse_field_name(fn)
for fn in field_name.split("|")
], fmt)
else:
key, funcs = self._parse_field_name(field_name)
if funcs:
return self._apply(key, funcs, fmt)
return self._apply_simple(key, fmt)
@staticmethod
def _parse_field_name(field_name):
first, rest = _string.formatter_field_name_split(field_name)
funcs = []
for is_attr, key in rest:
if is_attr:
func = operator.attrgetter
elif ":" in key:
func = self._slicegetter
func = operator.itemgetter
start, _, stop = key.partition(":")
stop, _, step = stop.partition(":")
start = int(start) if start else None
stop = int(stop) if stop else None
step = int(step) if step else None
key = slice(start, stop, step)
else:
func = operator.itemgetter
funcs.append(func(key))
return first, funcs
def _build_format_func(self, format_spec, conversion):
if conversion:
funcs.append(self.CONVERSIONS[conversion])
conversion = self.CONVERSIONS[conversion]
if format_spec:
if format_spec[0] == "?":
@@ -518,26 +542,20 @@ class Formatter():
else:
func = self._format_default
fmt = func(format_spec)
if conversion:
def wrap(obj):
return fmt(conversion(obj))
return wrap
return fmt
else:
fmt = str
if funcs:
return self._apply(first, funcs, fmt)
return self._apply_simple(first, fmt)
def _apply_simple(self, key, fmt):
def wrap(obj):
if key in obj:
obj = obj[key]
else:
obj = self.default
return fmt(obj)
return wrap
return conversion or str
def _apply(self, key, funcs, fmt):
def wrap(obj):
def wrap(kwdict):
try:
obj = obj[key]
obj = kwdict[key]
for func in funcs:
obj = func(obj)
except Exception:
@@ -545,14 +563,26 @@ class Formatter():
return fmt(obj)
return wrap
@staticmethod
def _slicegetter(key):
start, _, stop = key.partition(":")
stop, _, step = stop.partition(":")
start = int(start) if start else None
stop = int(stop) if stop else None
step = int(step) if step else None
return operator.itemgetter(slice(start, stop, step))
def _apply_simple(self, key, fmt):
def wrap(kwdict):
return fmt(kwdict[key] if key in kwdict else self.default)
return wrap
def _apply_list(self, lst, fmt):
def wrap(kwdict):
for key, funcs in lst:
try:
obj = kwdict[key]
for func in funcs:
obj = func(obj)
if obj is not None:
break
except Exception:
pass
else:
obj = self.default
return fmt(obj)
return wrap
@staticmethod
def _format_optional(format_spec):

View File

@@ -259,6 +259,7 @@ class TestFormatter(unittest.TestCase):
kwdict = {
"a": "hElLo wOrLd",
"b": "äöü",
"d": {"a": "foo", "b": 0, "c": None},
"l": ["a", "b", "c"],
"n": None,
"u": "%27%3C%20/%20%3E%27",
@@ -323,6 +324,22 @@ class TestFormatter(unittest.TestCase):
self._run_test("{missing[key]}", replacement, default)
self._run_test("{missing:?a//}", "a" + default, default)
def test_alternative(self):
self._run_test("{a|z}" , "hElLo wOrLd")
self._run_test("{z|a}" , "hElLo wOrLd")
self._run_test("{z|y|a}" , "hElLo wOrLd")
self._run_test("{z|y|x|a}", "hElLo wOrLd")
self._run_test("{z|n|a|y}", "hElLo wOrLd")
self._run_test("{z|a!C}" , "Hello World")
self._run_test("{z|a:Rh/C/}" , "CElLo wOrLd")
self._run_test("{z|a!C:RH/C/}", "Cello World")
self._run_test("{z|y|x:?</>/}", "")
self._run_test("{d[c]|d[b]|d[a]}", "0")
self._run_test("{d[a]|d[b]|d[c]}", "foo")
self._run_test("{d[z]|d[y]|d[x]}", "None")
def test_slicing(self):
v = self.kwdict["a"]
self._run_test("{a[1:10]}" , v[1:10])