[formatter] implement 'M' format specifier
Map a list of objects to a list of values
[
{"name": "John Doe" , "age": 42},
{"name": "Jane Smith" , "age": 24},
{"name": "Max Mustermann", "age": null}
]
== :Mname =>
["John Doe", "Jane Smith", "Max Mustermann"]
This commit is contained in:
@@ -266,6 +266,12 @@ Format specifiers can be used for advanced formatting by using the options provi
|
|||||||
<td><code>{tags:J - /}</code></td>
|
<td><code>{tags:J - /}</code></td>
|
||||||
<td><code>sun - tree - water</code></td>
|
<td><code>sun - tree - water</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>M<key>/</code></td>
|
||||||
|
<td>Maps a list of objects to a list of corresponding values by looking up <code><key></code> in each object</td>
|
||||||
|
<td><code>{users:Mname/}</code></td>
|
||||||
|
<td><code>["John", "David", "Max"]</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>R<old>/<new>/</code></td>
|
<td><code>R<old>/<new>/</code></td>
|
||||||
<td>Replaces all occurrences of <code><old></code> with <code><new></code> using <a href="https://docs.python.org/3/library/stdtypes.html#str.replace" rel="nofollow"><code>str.replace()</code></a></td>
|
<td>Replaces all occurrences of <code><old></code> with <code><new></code> using <a href="https://docs.python.org/3/library/stdtypes.html#str.replace" rel="nofollow"><code>str.replace()</code></a></td>
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class FStringFormatter():
|
|||||||
"""Generate text by evaluating an f-string literal"""
|
"""Generate text by evaluating an f-string literal"""
|
||||||
|
|
||||||
def __init__(self, fstring, default=NONE, fmt=None):
|
def __init__(self, fstring, default=NONE, fmt=None):
|
||||||
self.format_map = util.compile_expression('f"""' + fstring + '"""')
|
self.format_map = util.compile_expression(f'f"""{fstring}"""')
|
||||||
|
|
||||||
|
|
||||||
class TemplateFormatter(StringFormatter):
|
class TemplateFormatter(StringFormatter):
|
||||||
@@ -302,7 +302,7 @@ def _parse_optional(format_spec, default):
|
|||||||
fmt = _build_format_func(format_spec, default)
|
fmt = _build_format_func(format_spec, default)
|
||||||
|
|
||||||
def optional(obj):
|
def optional(obj):
|
||||||
return before + fmt(obj) + after if obj else ""
|
return f"{before}{fmt(obj)}{after}" if obj else ""
|
||||||
return optional
|
return optional
|
||||||
|
|
||||||
|
|
||||||
@@ -385,6 +385,27 @@ def _parse_join(format_spec, default):
|
|||||||
return apply_join
|
return apply_join
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_map(format_spec, default):
|
||||||
|
key, _, format_spec = format_spec.partition(_SEPARATOR)
|
||||||
|
key = key[1:]
|
||||||
|
fmt = _build_format_func(format_spec, default)
|
||||||
|
|
||||||
|
def map_(obj):
|
||||||
|
if not obj or isinstance(obj, str):
|
||||||
|
return fmt(obj)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for item in obj:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get(key, ...)
|
||||||
|
results.append(default if value is ... else value)
|
||||||
|
else:
|
||||||
|
results.append(item)
|
||||||
|
return fmt(results)
|
||||||
|
|
||||||
|
return map_
|
||||||
|
|
||||||
|
|
||||||
def _parse_replace(format_spec, default):
|
def _parse_replace(format_spec, default):
|
||||||
old, new, format_spec = format_spec.split(_SEPARATOR, 2)
|
old, new, format_spec = format_spec.split(_SEPARATOR, 2)
|
||||||
old = old[1:]
|
old = old[1:]
|
||||||
@@ -507,6 +528,7 @@ _FORMAT_SPECIFIERS = {
|
|||||||
"D": _parse_datetime,
|
"D": _parse_datetime,
|
||||||
"J": _parse_join,
|
"J": _parse_join,
|
||||||
"L": _parse_maxlen,
|
"L": _parse_maxlen,
|
||||||
|
"M": _parse_map,
|
||||||
"O": _parse_offset,
|
"O": _parse_offset,
|
||||||
"R": _parse_replace,
|
"R": _parse_replace,
|
||||||
"S": _parse_sort,
|
"S": _parse_sort,
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ class TestFormatter(unittest.TestCase):
|
|||||||
"d": {"a": "foo", "b": 0, "c": None},
|
"d": {"a": "foo", "b": 0, "c": None},
|
||||||
"i": 2,
|
"i": 2,
|
||||||
"l": ["a", "b", "c"],
|
"l": ["a", "b", "c"],
|
||||||
|
"L": [
|
||||||
|
{"name": "John Doe" , "age": 42, "email": "jd@example.org"},
|
||||||
|
{"name": "Jane Smith" , "age": 24, "email": None},
|
||||||
|
{"name": "Max Mustermann", "age": False},
|
||||||
|
],
|
||||||
"n": None,
|
"n": None,
|
||||||
"s": " \n\r\tSPACE ",
|
"s": " \n\r\tSPACE ",
|
||||||
"h": "<p>foo </p> & bar <p> </p>",
|
"h": "<p>foo </p> & bar <p> </p>",
|
||||||
@@ -209,7 +214,7 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{j:[b:]}" , v)
|
self._run_test("{j:[b:]}" , v)
|
||||||
self._run_test("{j:[b::]}" , v)
|
self._run_test("{j:[b::]}" , v)
|
||||||
|
|
||||||
def test_maxlen(self):
|
def test_specifier_maxlen(self):
|
||||||
v = self.kwdict["a"]
|
v = self.kwdict["a"]
|
||||||
self._run_test("{a:L5/foo/}" , "foo")
|
self._run_test("{a:L5/foo/}" , "foo")
|
||||||
self._run_test("{a:L50/foo/}", v)
|
self._run_test("{a:L50/foo/}", v)
|
||||||
@@ -217,7 +222,7 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{a:L50/foo/>51}", "foo")
|
self._run_test("{a:L50/foo/>51}", "foo")
|
||||||
self._run_test("{a:Lab/foo/}", "foo")
|
self._run_test("{a:Lab/foo/}", "foo")
|
||||||
|
|
||||||
def test_join(self):
|
def test_specifier_join(self):
|
||||||
self._run_test("{l:J}" , "abc")
|
self._run_test("{l:J}" , "abc")
|
||||||
self._run_test("{l:J,}" , "a,b,c")
|
self._run_test("{l:J,}" , "a,b,c")
|
||||||
self._run_test("{l:J,/}" , "a,b,c")
|
self._run_test("{l:J,/}" , "a,b,c")
|
||||||
@@ -229,7 +234,7 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{a:J/}" , self.kwdict["a"])
|
self._run_test("{a:J/}" , self.kwdict["a"])
|
||||||
self._run_test("{a:J, /}" , self.kwdict["a"])
|
self._run_test("{a:J, /}" , self.kwdict["a"])
|
||||||
|
|
||||||
def test_replace(self):
|
def test_specifier_replace(self):
|
||||||
self._run_test("{a:Rh/C/}" , "CElLo wOrLd")
|
self._run_test("{a:Rh/C/}" , "CElLo wOrLd")
|
||||||
self._run_test("{a!l:Rh/C/}", "Cello world")
|
self._run_test("{a!l:Rh/C/}", "Cello world")
|
||||||
self._run_test("{a!u:Rh/C/}", "HELLO WORLD")
|
self._run_test("{a!u:Rh/C/}", "HELLO WORLD")
|
||||||
@@ -238,12 +243,12 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{a!l:Rl//}" , "heo word")
|
self._run_test("{a!l:Rl//}" , "heo word")
|
||||||
self._run_test("{name:Rame/othing/}", "Nothing")
|
self._run_test("{name:Rame/othing/}", "Nothing")
|
||||||
|
|
||||||
def test_datetime(self):
|
def test_specifier_datetime(self):
|
||||||
self._run_test("{ds:D%Y-%m-%dT%H:%M:%S%z}", "2010-01-01 00:00:00")
|
self._run_test("{ds:D%Y-%m-%dT%H:%M:%S%z}", "2010-01-01 00:00:00")
|
||||||
self._run_test("{ds:D%Y}", "2010-01-01T01:00:00+01:00")
|
self._run_test("{ds:D%Y}", "2010-01-01T01:00:00+01:00")
|
||||||
self._run_test("{l:D%Y}", "None")
|
self._run_test("{l:D%Y}", "None")
|
||||||
|
|
||||||
def test_offset(self):
|
def test_specifier_offset(self):
|
||||||
self._run_test("{dt:O 01:00}", "2010-01-01 01:00:00")
|
self._run_test("{dt:O 01:00}", "2010-01-01 01:00:00")
|
||||||
self._run_test("{dt:O+02:00}", "2010-01-01 02:00:00")
|
self._run_test("{dt:O+02:00}", "2010-01-01 02:00:00")
|
||||||
self._run_test("{dt:O-03:45}", "2009-12-31 20:15:00")
|
self._run_test("{dt:O-03:45}", "2009-12-31 20:15:00")
|
||||||
@@ -254,7 +259,7 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{ds:D%Y-%m-%dT%H:%M:%S%z/O1}", "2010-01-01 01:00:00")
|
self._run_test("{ds:D%Y-%m-%dT%H:%M:%S%z/O1}", "2010-01-01 01:00:00")
|
||||||
self._run_test("{t!d:O2}", "2010-01-01 02:00:00")
|
self._run_test("{t!d:O2}", "2010-01-01 02:00:00")
|
||||||
|
|
||||||
def test_offset_local(self):
|
def test_specifier_offset_local(self):
|
||||||
ts = self.kwdict["dt"].replace(
|
ts = self.kwdict["dt"].replace(
|
||||||
tzinfo=datetime.timezone.utc).timestamp()
|
tzinfo=datetime.timezone.utc).timestamp()
|
||||||
offset = time.localtime(ts).tm_gmtoff
|
offset = time.localtime(ts).tm_gmtoff
|
||||||
@@ -269,7 +274,7 @@ class TestFormatter(unittest.TestCase):
|
|||||||
self._run_test("{dt_dst:O}", str(dt))
|
self._run_test("{dt_dst:O}", str(dt))
|
||||||
self._run_test("{dt_dst:Olocal}", str(dt))
|
self._run_test("{dt_dst:Olocal}", str(dt))
|
||||||
|
|
||||||
def test_sort(self):
|
def test_specifier_sort(self):
|
||||||
self._run_test("{l:S}" , "['a', 'b', 'c']")
|
self._run_test("{l:S}" , "['a', 'b', 'c']")
|
||||||
self._run_test("{l:Sa}", "['a', 'b', 'c']")
|
self._run_test("{l:Sa}", "['a', 'b', 'c']")
|
||||||
self._run_test("{l:Sd}", "['c', 'b', 'a']")
|
self._run_test("{l:Sd}", "['c', 'b', 'a']")
|
||||||
@@ -301,6 +306,19 @@ class TestFormatter(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
self._run_test("{a:Xfoo/ */}", "hello wo *")
|
self._run_test("{a:Xfoo/ */}", "hello wo *")
|
||||||
|
|
||||||
|
def test_specifier_map(self):
|
||||||
|
self._run_test("{L:Mname/}" ,
|
||||||
|
"['John Doe', 'Jane Smith', 'Max Mustermann']")
|
||||||
|
self._run_test("{L:Mage/}" ,
|
||||||
|
"[42, 24, False]")
|
||||||
|
|
||||||
|
self._run_test("{a:Mname}", self.kwdict["a"])
|
||||||
|
self._run_test("{n:Mname}", "None")
|
||||||
|
self._run_test("{title4:Mname}", "0")
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
self._run_test("{t:Mname", "")
|
||||||
|
|
||||||
def test_chain_special(self):
|
def test_chain_special(self):
|
||||||
# multiple replacements
|
# multiple replacements
|
||||||
self._run_test("{a:Rh/C/RE/e/RL/l/}", "Cello wOrld")
|
self._run_test("{a:Rh/C/RE/e/RL/l/}", "Cello wOrld")
|
||||||
@@ -322,6 +340,9 @@ class TestFormatter(unittest.TestCase):
|
|||||||
# sort and join
|
# sort and join
|
||||||
self._run_test("{a:S/J}", " ELLOdhlorw")
|
self._run_test("{a:S/J}", " ELLOdhlorw")
|
||||||
|
|
||||||
|
# map and join
|
||||||
|
self._run_test("{L:Mname/J-}", "John Doe-Jane Smith-Max Mustermann")
|
||||||
|
|
||||||
def test_separator(self):
|
def test_separator(self):
|
||||||
orig_separator = formatter._SEPARATOR
|
orig_separator = formatter._SEPARATOR
|
||||||
try:
|
try:
|
||||||
@@ -494,10 +515,10 @@ def noarg():
|
|||||||
fmt4 = formatter.parse("\fM " + path + ":lengths")
|
fmt4 = formatter.parse("\fM " + path + ":lengths")
|
||||||
|
|
||||||
self.assertEqual(fmt1.format_map(self.kwdict), "'Title' by Name")
|
self.assertEqual(fmt1.format_map(self.kwdict), "'Title' by Name")
|
||||||
self.assertEqual(fmt2.format_map(self.kwdict), "139")
|
self.assertEqual(fmt2.format_map(self.kwdict), "142")
|
||||||
|
|
||||||
self.assertEqual(fmt3.format_map(self.kwdict), "'Title' by Name")
|
self.assertEqual(fmt3.format_map(self.kwdict), "'Title' by Name")
|
||||||
self.assertEqual(fmt4.format_map(self.kwdict), "139")
|
self.assertEqual(fmt4.format_map(self.kwdict), "142")
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
self.assertEqual(fmt0.format_map(self.kwdict), "")
|
self.assertEqual(fmt0.format_map(self.kwdict), "")
|
||||||
|
|||||||
Reference in New Issue
Block a user