Reputation: 15165
I need to fill a small string template. My requirements are the following:
$(...)
str.format
does, for instance in 'hey {obj.name}'.format(obj=some_object)
I thought the Template
class in the module string
would do the trick: it fulfills point 1 (placeholder) and 3 (error tolerant), but unfortunately it does not support the "dot-access".
>>> from string import Template
>>> t = Template('$(obj.get)')
>>> t.substitute(obj=dict)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "C:\Development\CDBServer_active\lib\string.py", line 172, in substitute
return self.pattern.sub(convert, self.template)
File "C:\Development\CDBServer_active\lib\string.py", line 169, in convert
self._invalid(mo)
File "C:\Development\CDBServer_active\lib\string.py", line 146, in _invalid
(lineno, colno))
ValueError: Invalid placeholder in string: line 1, col 1
Is there any way to do this without 3rd-party libraries or without writing my own code?
Upvotes: 2
Views: 1010
Reputation: 250961
You can override the Template
class and define your own pattern and safe_substitute
method to get the desired behavior:
from string import Template
class MyTemplate(Template):
pattern = r"""
\$(?:
(?P<escaped>\$)| # Escape sequence of two delimiters
(?P<named>[_a-z][_.a-z0-9]*)| # delimiter and a Python identifier
\((?P<braced>[_a-z][_.a-z0-9]*)\)| # delimiter and a braced identifier
(?P<invalid>) # Other ill-formed delimiter exprs
)
"""
def safe_substitute(*args, **kws):
if not args:
raise TypeError("descriptor 'safe_substitute' of 'Template' object "
"needs an argument")
self, args = args[0], args[1:] # allow the "self" keyword be passed
if len(args) > 1:
raise TypeError('Too many positional arguments')
if not args:
mapping = kws
elif kws:
mapping = _multimap(kws, args[0])
else:
mapping = args[0]
# Helper function for .sub()
def convert(mo):
named = mo.group('braced')
if named is not None:
try:
if '.' not in named:
return '%s' % (mapping[named],)
else:
attrs = named.split('.')
named, attrs = attrs[0], attrs[1:]
if not named.strip() or not all(attr.strip() for attr in attrs):
# handle cases like foo. foo.bar..spam
raise Exception()
return '%s' % reduce(lambda x, y: getattr(x, y), attrs, mapping[named])
except Exception as e:
return mo.group()
if mo.group('escaped') is not None:
return self.delimiter
if mo.group('invalid') is not None:
return mo.group()
raise ValueError('Unrecognized named group in pattern',
self.pattern)
return self.pattern.sub(convert, self.template)
Demo:
class A(object):
def __init__(self, val):
self.val = val
def __getattr__(self, attr):
return A(self.val * 2)
def __repr__(self):
return str(self.val)
>>> t = MyTemplate('$(obj.get) $(foo) $(spam.a.b.c.d) $(A.__getattr__)')
>>> t.safe_substitute(obj=dict, foo=1, spam=A(10), A=A)
"<method 'get' of 'dict' objects> 1 160 <unbound method A.__getattr__>"
>>> t.safe_substitute(obj=A(100), foo=1, spam=A(10), A=A)
'200 1 160 <unbound method A.__getattr__>'
The changes in safe_subsitute
method is that if .
is present in the identifier then try to calculate its value using reduce
with getattr
:
attrs = named.split('.')
named, attrs = attrs[0], attrs[1:]
if not named.strip() or not all(attr.strip() for attr in attrs):
# handle cases like foo. foo.bar..spam
raise Exception()
return '%s' % reduce(lambda x, y: getattr(x, y), attrs, mapping[named])
Note that currently the code will also replace the values of named groups like $obj.get
, if you don't want that behavior remove the named
group from the regex and change the first line of function convert
to:
named = mo.group('braced')
Upvotes: 1
Reputation: 97591
Just translate format strings?
def translate(fmt):
# escape their markers
fmt = fmt.replace('{', '{{').replace('}', '}}')
# translate our markers
fmt = re.sub(r'\$\((.+?)\)', r'{\1}', fmt)
# unescape out markers
fmt = fmt.replace('$$', '$')
return fmt
obj = lambda: this_is_a_hack
obj.it = 123
translate("$(testing.it)").format(testing=obj)
Upvotes: 1