Ian Bicking: the old part of his blog

String Interpolation

I was using string.Template in WSGIKit recently, and I started hacking silly things onto it with a dictionary that, for instance, turned d['str_v'] into repr(d['v']). But it got me to thinking about simple templates and string substitution, and the next thing you know I implemented something to do richer interpolation:

>>> name = 'Bob'
>>> e('Hi $name')
'Hi Bob'
>>> e('Hi ${name.lower()}')
'Hi bob'
>>> e('Your name is spelled: ${" ".join(list(name.upper()))}')
'Your name is spelled: B O B'

Maybe I should just use Cheetah and be done with it, but it was interesting to experiment. The source to e is, I think, relatively straight-forward. This has been done before in the guise of PEP 215. If I was to use this for real, I'd probably allow you to pass in a namespace as well -- it still adds value over plain Template by allowing expressions.

Created 13 Apr '05

Comments:

Why the "finally" block with all the "del"s? There's obviously some python black-magic going on that I'm not privvy to ;)
# Richard Jones

Oh, wait, I get it. Euwww :)
# Richard Jones

Apparently you have to be extra careful about not keeping frame objects or other people's scopes around. Less so with the circular garbage collector in place, but frames are still special.
# Ian Bicking

This already exists in Itpl, and has since at least 2000 [1]:

>>> import Itpl
>>> pp = Itpl.itpl
>>> x = "This is a test"
>>> print pp("Today made me say $x.upper()")
Today made me say THIS IS A TEST
>>> print pp("Today made me say ${' '.join(('what', 'is', 'going', 'on?'))}")
Today made me say what is going on?

[1] http://lfw.org/python/Itpl.py

# Bill Mill

This reminds me of all of the new text editors that show up on freshmeat as being "smaller than vi" or "less bloated than emacs"... and then the authors drop those once they start growing in size and realize that they actually want some of those things in vi and emacs.

So, I think your comment is probably correct... if you keep heading down this path, you'll probably end up with Cheetah again :)

# Kevin Dangoor

Ian, what do you think of using % operator as basic templates? I do it now and delay decision on some templating language.

>>> name = 'Bob'
>>> size = 5
>>> '%(name)s\'s size is %(size)d.' % locals()
"Bob's size is 5."
>>>
# anonymous

I've played with all the templates and till I decide on the web framework to use I use preppy. Which is very light weight and works well similar to what you are trying to do.
# anonymous

In the early days of Aquarium, before I started using Cheetah, I had a decent interpolation library that fit in a single function, but was still quite handy:

def evalstr(s, dollar="$"):

    """Evaluate a string with embedded python expressions.

    This is a Python version of the function evalstr, which is used for
    variable interpolation.

    For instance,

        a = 5
        evalStr("I have $a$ dogs.")

    will return

        "I have 5 dogs."

    The code between the $'s can be any valid Python expression.  I'll
    just basically do

        str(eval(expression), yourGlobals, yourLocals)

    Since eval is being used, the expression must return some value.
    To escape $'s in s, just use $$ (even within expressions).  To use
    a different character than $, pass the desired character in the
    second (optional) argument.  If there are an odd number of $
    symbols, a ValueError exception will be raised.  Here are some
    additional examples (assuming a = 5):

    "My dog is $a$ years old." -> "My dog is 5 years old.",
    "I have $$5.00" -> "I have $5.00",
    "My $a$ year old dog has $$5.00" -> "My 5 year old dog has $5.00",
    "My dog has $'$$%s.' % a$" -> "My dog has $5.",
    "My dog has $'$$' + str(a) + '.'" -> ValueError exception,
    "My dog is $a$.$" -> ValueError exception,
    "$$ is a char." -> "$ is a char.",
    "$a$ is a number." -> "5 is a number.",
    "$a$" -> "5",
    "$a" -> ValueError exception,
    "I have a $$" -> "I have a $",
    "" -> "",
    "Spam" -> "Spam",
    "$" -> ValueError exception,
    "$ evalstr('I am #a#.', '#') $" -> "I am 5."

    """
    
    # By the way, I'm sorry if this stuff is hard to read.  If you
    # can come up with something more elegant, I'd love to see it, 
    # but this is the best that I could come up with.  

    import string
    import sys
    inEv = 0     # Are we in eval mode (i.e. inside a single $)?
    inDollar = 0 # We just got a dollar.  We might be in an escape.
    ret = ""     # This is the string we're returning.
    tok = ""     # The token we're currently working with.
    stack = []   # All good languages are stack based ;)
    i=j=0        # These are indexes into s.

    # Grab the calling function's namespace.  I can't find a nice 
    # way to do this other than manually generating an exception 
    # and surfing the traceback.  As for performance, you can do 
    # this about 10000 times in a bit less than a second on my 
    # PIII 600 mhz system.

    try:
        1/0
    except:
        traceback = sys.exc_info()[2]
        currFrame = traceback.tb_frame
        prevFrame = currFrame.f_back
        prevGlobals = prevFrame.f_globals
        prevLocals = prevFrame.f_locals
    
    # If they forget to close the $, raise a ValueError exception.

    if string.count(s, dollar) % 2: 
        raise ValueError, "unmatched dollar symbol"

    sLen = len(s)
    while 1:

        # Get next token.

        j = i
        if j==sLen:
            tok = ""
        elif s[j] == dollar:
            tok = dollar
            i = j+1
        else:
            while j<sLen and s[j]!=dollar: j=j+1
            if j==sLen:
                tok = s[i:]
                i = sLen
            else:
                tok = s[i:j]
                i = j

        # Deal with receiving $'s (may call continue).

        if tok == dollar:
            if inDollar: inDollar = 0
            else: 
                inDollar = 1
                continue

        # Deal with appending stuff to the stack or to ret.

        if inDollar:
            inEv = not inEv
            inDollar = 0
        if inEv: stack = stack + [tok]
        else:
            if len(stack) > 0:
                ret = ret + \
                    str(eval(string.join(stack, ""),
                         prevGlobals, 
                         prevLocals))
            ret = ret + tok
            stack = []

        # Deal with end of string.

        if tok == "": break
    return ret
# Shannon -jj Behrens