Welcome, guest | Sign In | My Account | Store | Cart
import re

# Match any % formatting stanza
# Any character is allowed in names, unicode names work in python 2.2+
# The name can be an empty string
safmt_pat = re.compile(r'''
    %                     # Start with percent,
    (?:\( ([^()]*) \))?   # optional name in parens (do not capture parens),
    [-+ #0]*              # zero or more flags
    (?:\*|[0-9]*)         # optional minimum field width
    (?:\.(?:\*|[0-9]*))?  # optional dot and length modifier
    [EGXcdefgiorsux%]     # type code (or [formatted] percent character)
    ''', re.VERBOSE)

# Wrapper to allow e.g. safmt("%blah", 10, 20, spam='eggs')
# First argument must be the template
def safmt(*args, **kw):
    return safmtb(args[0], args[1:], kw)

# Safe and augmented "%" string interpolation:
# - preserve % stanzas in case of missing argument or key
# - allow mixed positional and named arguments
# Note: TypeError exceptions can still happen, e.g. safmt("%d", "abc")
# Function arguments:
#  template: a string containing "%" format specifiers
#  args    : sequence arguments for format string
#  kw      : mapping arguments for format string
#  savepc  : optionally preserve "escaped percent" stanzas
#            (parameterised positional stanzas always eat args)
#  verb    : verbose execution, prints debug output to stdout
def safmtb(template, args=(), kw=None, savepc=0, verb=0):
    if verb:
        print "safmt(%r)" % (template,)
    
    if kw is None:
        kw = {}
    
    ret = []
    last = i = 0
    d = {}
    di = 0
    pat = safmt_pat
    while 1:
        mo = pat.search(template, i)
        
        if not mo:
            # End of string
            ret.append(template[last:])
            break
        
        i = mo.end(0)
        if verb: print mo.start(), mo.group(0, 1),
        
        stanza, name = mo.group(0, 1)
        if name is not None:
            # str[-1]=='x' is faster than str.endswith('x'),
            # and stanza is always non-empty here so slice will never fail
            if stanza[-1] == "%":
                if savepc:
                    if verb: print 'saving stanza'
                    continue
                # Workaround weird behaviour in python2.1-2.5: a named
                # argument that is just a percent escape still raises
                # KeyError, even though a positional escaped percent eats
                # no args and is happy with an empty sequence.
                # Workaround: provide a dummy key which never gets used.
                dat = stanza % {name: None}
            else:
                try:
                    dat = stanza % kw
                except KeyError:
                    if verb: print 'ignore missing key'
                    continue
            if verb: print "fmt %r" % dat
        else:
            # %<blah>% does not use up arguments, but "%*.*%" does
            numargs = stanza[-1] != "%"
            if verb: print "args=%s" % numargs,
            # Allow for "*" parameterisation (uses up to 2)
            numargs += mo.group(0).count("*")
            if verb: print "args=%s" % numargs,
            
            p = args[di: di + numargs]
            di += numargs
            if verb: print "p=%s" % (p,),
            if len(p) != numargs:
                if verb: print "not enough pos args"
                continue
            if savepc and stanza[-1] == "%":
                if verb: print 'saving stanza'
                continue
            dat = stanza % p
            if verb: print "fmt %r" % dat
        
        ret.append(template[last:mo.start()])
        ret.append(dat)
        last = i
    
    return ''.join(ret)


# ****** Related Recipe ******

from ConfigParser import *
from ConfigParser import DEFAULTSECT
class SafeConfigParser(ConfigParser):
    # Override get() method to use safe string interpolation
    def get(self, section, option, raw=0, vars=None):
        # In python2.3, the name changed from __sections to _sections
        if hasattr(self, '_sections'):
            sections = self._sections
            defaults = self._defaults
        else:
            sections = self._ConfigParser__sections
            defaults = self._ConfigParser__defaults
        
        try:
            sectdict = sections[section].copy()
        except KeyError:
            if section == DEFAULTSECT:
                sectdict = {}
            else:
                raise NoSectionError(section)
        d = defaults.copy()
        d.update(sectdict)
        # Update with the entry specific variables
        if vars:
            d.update(vars)
        option = self.optionxform(option)
        try:
            rawval = d[option]
        except KeyError:
            raise NoOptionError(option, section)

        if raw:
            return rawval

        # do the string interpolation
        value = rawval                  # Make it a pretty variable name
        for depth in range(10):
            oldvalue = value
            value = safmtb(value, kw=d, savepc=1)
            if value == oldvalue: break
        else:
            raise InterpolationDepthError(option, section, rawval)
        
        return value

History

  • revision 6 (16 years ago)
  • previous revisions are not available