Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 540
Ernest A Avatar asked Oct 07 '12 10:10

Ernest A


1 Answers

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.

like image 154
jfs Avatar answered Sep 18 '22 14:09

jfs