Is it possible to do partial string formatting with the advanced string formatting methods, similar to the string template safe_substitute() function?
F
Reading @Sam Bourne comment, I modified @SvenMarnach's code
to work properly with coercion (like {a!s:>2s}) without writing a custom parser.
The basic idea is not to convert to strings but concatenate missing keys with coercion tags.
import string
class MissingKey(object):
def __init__(self, key):
self.key = key
def __str__(self): # Supports {key!s}
return MissingKeyStr("".join([self.key, "!s"]))
def __repr__(self): # Supports {key!r}
return MissingKeyStr("".join([self.key, "!r"]))
def __format__(self, spec): # Supports {key:spec}
if spec:
return "".join(["{", self.key, ":", spec, "}"])
return "".join(["{", self.key, "}"])
def __getitem__(self, i): # Supports {key[i]}
return MissingKey("".join([self.key, "[", str(i), "]"]))
def __getattr__(self, name): # Supports {key.name}
return MissingKey("".join([self.key, ".", name]))
class MissingKeyStr(MissingKey, str):
def __init__(self, key):
if isinstance(key, MissingKey):
self.key = "".join([key.key, "!s"])
else:
self.key = key
class SafeFormatter(string.Formatter):
def __init__(self, default=lambda k: MissingKey(k)):
self.default=default
def get_value(self, key, args, kwds):
if isinstance(key, str):
return kwds.get(key, self.default(key))
else:
return super().get_value(key, args, kwds)
Use (for example) like this
SafeFormatter().format("{a:<5} {b:<10}", a=10)
The following tests (inspired by tests from @norok2) check the output for the traditional format_map and a safe_format_map based on the class above in two cases: providing correct keywords or without them.
def safe_format_map(text, source):
return SafeFormatter().format(text, **source)
test_texts = (
'{a} ', # simple nothing useful in source
'{a:5d}', # formatting
'{a!s}', # coercion
'{a!s:>{a}s}', # formatting and coercion
'{a:0{a}d}', # nesting
'{d[x]}', # indexing
'{d.values}', # member
)
source = dict(a=10,d=dict(x='FOO'))
funcs = [safe_format_map,
str.format_map
#safe_format_alt # Version based on parsing (See @norok2)
]
n = 18
for text in test_texts:
# full_source = {**dict(b='---', f=dict(g='Oh yes!')), **source}
# print('{:>{n}s} : OK : '.format('str.format_map', n=n) + text.format_map(full_source))
print("Testing:", text)
for func in funcs:
try:
print(f'{func.__name__:>{n}s} : OK\t\t\t: ' + func(text, dict()))
except:
print(f'{func.__name__:>{n}s} : FAILED')
try:
print(f'{func.__name__:>{n}s} : OK\t\t\t: ' + func(text, source))
except:
print(f'{func.__name__:>{n}s} : FAILED')
Which outputs
Testing: {a}
safe_format_map : OK : {a}
safe_format_map : OK : 10
format_map : FAILED
format_map : OK : 10
Testing: {a:5d}
safe_format_map : OK : {a:5d}
safe_format_map : OK : 10
format_map : FAILED
format_map : OK : 10
Testing: {a!s}
safe_format_map : OK : {a!s}
safe_format_map : OK : 10
format_map : FAILED
format_map : OK : 10
Testing: {a!s:>{a}s}
safe_format_map : OK : {a!s:>{a}s}
safe_format_map : OK : 10
format_map : FAILED
format_map : OK : 10
Testing: {a:0{a}d}
safe_format_map : OK : {a:0{a}d}
safe_format_map : OK : 0000000010
format_map : FAILED
format_map : OK : 0000000010
Testing: {d[x]}
safe_format_map : OK : {d[x]}
safe_format_map : OK : FOO
format_map : FAILED
format_map : OK : FOO
Testing: {d.values}
safe_format_map : OK : {d.values}
safe_format_map : OK :
format_map : FAILED
format_map : OK :