Ernest A
Ernest A

Reputation: 7839

How to escape $ on Python string Template class?

Introduction

The string module has a Template class, that lets you make substitutions in a string using a mapping object, for instance:

>>> string.Template('var is $var').substitute({'var': 1})
'var is 1'

The substitute method may raise a KeyError exception, if an attempt is made to substitute an element that is missing from the mapping, for instance

>>> string.Template('var is $var and foo is $foo').substitute({'var': 1})
KeyError: 'foo'

or may raise a ValueError, if the template string is invalid, e.g. it contains a $ character followed by a space:

>>> string.Template('$ var is $var').substitute({'var': 1})
ValueError: Invalid placeholder in string: line 1, col 1

The Problem

Given a template string and a mapping, I want to determine whether all place-holders in the template would be substituted. For this, I would try to make the substitution and catch any KeyError exception:

def check_substitution(template, mapping):
    try:
        string.Template(template).substitute(mapping)
    except KeyError:
        return False
    except ValueError:
        pass
    return True

But this doesn't work, because if the template is invalid and a ValueError is raised, subsequent KeyErrors aren't caught:

>>> check_substitution('var is $var and foo is $foo', {'var': 1})
False
>>> check_substitution('$ var is $var and foo is $foo', {'var': 1})
True

but I don't care about ValueErrors. So, what would be the right approach to this problem?

Upvotes: 20

Views: 33485

Answers (4)

Gna007
Gna007

Reputation: 1

The good form is :

string.Template('**$$** var is $var').substitute({'var': 1})

It is necessary to double $.

Sample:

>>> print(string.Template('**$$** var is $var').substitute({'var': 1}))
$ var is 1

Upvotes: 0

knowingpark
knowingpark

Reputation: 759

Python will not do string substitution over multiple lines

If you have this string

criterion = """
    <criteria>
    <order>{order}</order>
      <body><![CDATA[{code}]]></body>
    </criteria>
"""

criterion.format(dict(order="1",code="Hello")

results in:

KeyError: 'order'

A solution is to use the string.Template module

from string import Template

criterion = """
    <criteria>
    <order>$order</order>
      <body><![CDATA[$code]]></body>
    </criteria>
"""

Template(criterion).substitute(dict(order="1",code="hello")

NOTE: you have to prefix the keywords with a $ not wrap them in {}

output is:

 <criteria>
    <order>1</order>
      <body><![CDATA[hello]]></body>
    </criteria>

Full docs are: https://docs.python.org/2/library/string.html#template-strings

Upvotes: -1

jfs
jfs

Reputation: 414915

The docs say that you can replace the pattern as long as it contains all necessary named groups:

import re
from string import Template


class TemplateIgnoreInvalid(Template):
    # override pattern to make sure `invalid` never matches
    pattern = r"""
    %(delim)s(?:
      (?P<escaped>%(delim)s) |   # Escape sequence of two delimiters
      (?P<named>%(id)s)      |   # delimiter and a Python identifier
      {(?P<braced>%(id)s)}   |   # delimiter and a braced identifier
      (?P<invalid>^$)            # never matches (the regex is not multilined)
    )
    """ % dict(delim=re.escape(Template.delimiter), id=Template.idpattern)


def check_substitution(template, **mapping):
    try:
        TemplateIgnoreInvalid(template).substitute(mapping)
    except KeyError:
        return False
    else:
        return True

Tests

f = check_substitution
assert f('var is $var', var=1)
assert f('$ var is $var', var=1)
assert     f('var is $var and foo is $foo', var=1, foo=2)
assert not f('var is $var and foo is $foo', var=1)
assert     f('$ var is $var and foo is $foo', var=1, foo=2)
assert not f('$ var is $var and foo is $foo', var=1)
# support all invalid patterns
assert f('var is $var and foo is ${foo', var=1)
assert f('var is $var and foo is ${foo', var=1, foo=2) #NOTE: problematic API
assert     f('var is $var and foo is ${foo and ${baz}', var=1, baz=3)
assert not f('var is $var and foo is ${foo and ${baz}', var=1)

It works for all invalid occurences of the delimiter ($).

The examples show that ignoring invalid patterns conceals simple typos in the template so it is not a good API.

Upvotes: 7

MBarsi
MBarsi

Reputation: 2457

This is a Quick Fix (Using recursion):

def check_substitution(tem, m):
    try:
        string.Template(tem).substitute(m)
    except KeyError:
        return False
    except ValueError:
        return check_substitution(tem.replace('$ ', '$'), m) #strip spaces after $
    return True

I Know its take a longer time if there is more than One Space between $ and var , so you may improve it by using Regular Expression.

EDIT

escaping $ into $$ makes more sense [ Thanks @Pedro ] so you can catch ValueError by this statement:

return check_substitution(tem.replace('$ ', '$$ '), m) #escaping $ by $$

Upvotes: 6

Related Questions