pymacro.py - text macros in python

Using embedded python as a macro language. In a sense, this is a bit like php or asp - the text goes straight through, except for @ signalled stuff, which can be processed in a variety of ways. It's not the same in the sense that asp/php have an implied structure across boundaries; an example would be instructive.

for more information,
contact:
Danbala Software

      13    __Id__ = "$Id: pymacro.py.html,v 1.1 2005/01/26 01:13:52 u37519820 Exp $"

Table of Contents


Introduction

macro syntax:
    
            
    direct evaluation
        @(python expression)
    
    direct execution
        @{python code}
    
    indirect evaluation
        @function(parameters...)
    
    implied string parameter
        @function{a string without quotes}
    
    alternative string delimeters:
        @function/almost any delimeter/

    to end of block/of text (depending on context)
        @function:to end of text

    comment to end of line:
        @#comment

    single symbol - evaluated or executed (no () needed for 0 parm functions)
        @symbol - looks for and doesn't eat white space after
        use @symbol@ if white space isn't desired
        
    assignment syntaxes:
        as a shortcut for
            @{symbol='string'} or @{symbol=integer}
        to white space, which is eaten, assumed to be string unless all digits
            @symbol=value
        can also use quotes (single or triple of either):
            @symbol='string'
        also:
            @symbol=(expression)
            
    needed(?): a way for a macro to eat input outside itself
    setup an implied read like the implied write?
    some other way: primarily for @iflike macros?
    
    needed(?):
    another syntax? for what? dictionaries and list access/assignment
        @symbol[X]
        @symbol[X]=value
    multiple string parameters?
        @xlink{danbala|http://www.danbala.com}

Was using re to find the brackets and parentheses, which isn't constructive due to nesting issues. _find_matching implemented instead.
                       


Imports

All standard python: sys, string, re, traceback, cStringIO
      85    
      86    import sys
      87    import string
      88    import re
      89    import traceback
      90    from cStringIO import StringIO

Globals

      94    _prefix = '@'                    # macro _prefix
      95    
      96    # kind of a loose definition of symbol, so what
      97    symbol_match = r"[A-Za-z_][A-Za-z0-9_\.]*"
      98    integer_match = r"[0-9]+"
      99    
     100    _re_white = re.compile("\s")
     101    _re_symbol = re.compile(symbol_match)
     102    _re_integer = re.compile(integer_match)
     103    
     104    # be careful, macros execute in this name space
     105    
     106    _g = globals()
     107    _l = _g
     108    
     109    write = sys.stdout.write

pymax_process - public

     114    def pymax_process(line):
     115        temp = sys.stdout
     116        out = StringIO()    # in case somebody prints
     117        sys.stdout = out
     118        try:
     119            _process(line, out)
     120            line = out.getvalue()
     121        except:
     122            line = pymax_excepted('pymax_process', line)
     123        sys.stdout = temp
     124        return line

_process: primary internal function

     129    def _process(line, out):
so macro code can 'write' without worrying about to where repeatedly look for prefix
     139            pos = string.find(line, _prefix)
not found, done get the next char watch for double 'escaped' prefix ignore free standing prefix chop off the front, write it out
     162            write(line[:pos])
     163            line = line[p:]
look at the next character - special cases
     166            if c == '{':    # exec anything
     167                ep = _find_matching(line, 0, '{', '}')   # not necessary?
     168                v = pymax_exec(line[1:ep-1])
     169            elif c == '(':  # eval anything
     170                ep = _find_matching(line, 0, '(', ')')   # necessary!
     171                v = pymax_eval(line[:ep])
     172            elif c == '#':  # comment to end of line
     173                ep = _must_find(line, '\n', 0)
     174                v = ''
symbol case
     177            else:           # symbol + something?
     178                m = _re_symbol.match(line)
not a symbol?
     180                if not m:
     181                    #if line[0] == '\n':
     182                    line=line[1:]   # ignore one (\n?) character after the @??
     183                    continue    # or complain?
handle symbol followed by...
     191                if c == '{':        # single string
     192                    ep = _find_matching(line, p, '{', '}')
     193                    v = _do(s, line[p+1:ep-1])
     194                elif c == '(':      # parameter list
     195                    ep = _find_matching(line, p, '(', ')')   # necessary!
     196                    v = pymax_eval(line[:ep])
     197                elif c == ':':      # to EOF
     198                    ep = 0
     199                    v = _do(s, line[p+1:])
     200                    line = ''
     201                elif c == '=':      # assignment
     202                    ep, v = _assign(s, line, p+1)
     203                elif c == '@':      # standalone, abutted
     204                    ep = p+1
     205                    v = pymax_eval(s)
     206                    if callable(v):
     207                        v = v()
     208                elif c in string.whitespace:    # standalone
     209                    ep = p
     210                    v = pymax_eval(s)
     211                    if callable(v):
     212                        v = v()
     213                else:               # single string, arbitrary delimeter
     214                    ep = _must_find(line, c, p+1)
     215                    v = _do(s, line[p+1:ep-1])
write the result, and strip out the eaten stuff, continue
     219            if v is None:
     220                v = ''
     221            write(str(v))
     222            line = line[ep:]  # ep points after the last et char

_assign: handle assignment

     226    def _assign(s, line, p):
     227        if line[p] == '"':
     228            if line[p+1] == '"':
     229                ep = _must_find(line, '"""', p+3) + 2
     230            else:
     231                ep = _must_find(line, '"', p+1)
     232            v = pymax_exec(line[:ep])        #assuming s @ line[0]
     233                
     234        elif line[p] == "'":
     235            if line[p+1] == "'":
     236                ep = _must_find(line, "'''", p+3) + 2
     237            else:
     238                ep = _must_find(line, "'", p+1)
     239            v = pymax_exec(line[:ep])        #assuming s @ line[0]
     240        
     241        elif line[p] == '(':
     242            ep = _find_matching(line, p, '(', ')')   # not necessary?
     243            v = pymax_exec(line[:ep])        #assuming s @ line[0]
     244        
     245        else:
     246            m = _re_white.search(line, p)
     247            if not m:
     248                ep = len(line)
     249            else:
     250                ep = m.end()
     251            if _re_integer.match(line[p:ep]):
     252                v = pymax_exec(line[:ep])        #assuming s @ line[0]
     253            else:   # assume it's a string
     254                v = pymax_exec( s + '=' + '"""' + line[p:ep] + '"""' )
     255        
     256        return ep, v

Utility functions

     262    def pymax_excepted(f, l):
     263        """ error wrapping """
     264        t = sys.exc_info()
     265        s = string.join(traceback.format_exception(t[0], t[1], t[2])) + "\nFor: " + l
     266        # report to stderr as well
     267        sys.stderr.write(s)
     268        return s
     269    
     270    def pymax_eval(line):
     271        """ eval with error wrapping """
     272        try:
     273            return eval(line,_g,_l)
     274        except: 
     275            return pymax_excepted('pymax_eval', line)
     276    
     277    def pymax_exec(line):
     278        """ exec with error wrapping """
     279        try:
     280            exec line in _g, _l
     281            return ''
     282        except:
     283            return pymax_excepted('pymax_exec', line)
     284    
     285    def _must_find(s, e, p=0):
     286        """ puke if not found """
     287        ep = string.find(s, e, p)
     288        if ep < 0:
     289            raise "NoMatching", e
     290        return ep+1
     291        
     292    def _find_matching(s, p, b, e):
     293        """ handle nesting () and {} """
     294        nb = 1
     295        ne = 0
     296        p = p + 1
     297        while ne < nb:
     298            ep = string.find(s, e, p)
     299            if ep < 0:
     300                raise "NoMatching", e
     301            nb = nb + string.count(s, b, p, ep)
     302            ne = ne + 1
     303            p  = ep + 1
     304        return ep+1 # next char
     305    
     306    def _do(v, p):
     307        """ one symbol with one string parameter """
     308        try:
     309            v = pymax_eval(v)
     310            if callable(v):
     311                return v(p)
     312            else:   # auto format % string
     313                return v % p
     314        except:
     315            return pymax_excepted('_do', p)

macros

     321    # maybe 'using' or something for import X?
     322    
     323    def load(s):
     324        s = 'from %s import *' % s
     325        pymax_exec(s)
     326        return ''

main - for testing

     331    if __name__ == '__main__':  
     332        import fileinput
     333        import string
     334        lines = []
     335        for line in fileinput.input():
     336            lines.append(line)     
     337        print pymax_process(string.join(lines, ''))