# Copyright (c) 2006-2009 Nathan Binkert # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer; # redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution; # neither the name of the copyright holders nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import __builtin__ import inspect import os import re import string class lookup(object): def __init__(self, formatter, frame, *args, **kwargs): self.frame = frame self.formatter = formatter self.dict = self.formatter._dict self.args = args self.kwargs = kwargs self.locals = {} def __setitem__(self, item, val): self.locals[item] = val def __getitem__(self, item): if item in self.locals: return self.locals[item] if item in self.kwargs: return self.kwargs[item] if item == '__file__': return self.frame.f_code.co_filename if item == '__line__': return self.frame.f_lineno if self.formatter.locals and item in self.frame.f_locals: return self.frame.f_locals[item] if item in self.dict: return self.dict[item] if self.formatter.globals and item in self.frame.f_globals: return self.frame.f_globals[item] if item in __builtin__.__dict__: return __builtin__.__dict__[item] try: item = int(item) return self.args[item] except ValueError: pass raise IndexError, "Could not find '%s'" % item class code_formatter_meta(type): pattern = r""" (?: %(delim)s(?P%(delim)s) | # escaped delimiter ^(?P[ ]*)%(delim)s(?P%(ident)s)$ | # lone identifier %(delim)s(?P%(ident)s) | # identifier %(delim)s%(lb)s(?P%(ident)s)%(rb)s | # braced identifier %(delim)s(?P%(pos)s) | # positional parameter %(delim)s%(lb)s(?P%(pos)s)%(rb)s | # braced positional %(delim)s%(ldb)s(?P.*?)%(rdb)s | # double braced expression %(delim)s(?P) # ill-formed delimiter exprs ) """ def __init__(cls, name, bases, dct): super(code_formatter_meta, cls).__init__(name, bases, dct) if 'pattern' in dct: pat = cls.pattern else: # tuple expansion to ensure strings are proper length lb,rb = cls.braced lb1,lb2,rb2,rb1 = cls.double_braced pat = code_formatter_meta.pattern % { 'delim' : re.escape(cls.delim), 'ident' : cls.ident, 'pos' : cls.pos, 'lb' : re.escape(lb), 'rb' : re.escape(rb), 'ldb' : re.escape(lb1+lb2), 'rdb' : re.escape(rb2+rb1), } cls.pattern = re.compile(pat, re.VERBOSE | re.DOTALL | re.MULTILINE) class code_formatter(object): __metaclass__ = code_formatter_meta delim = r'$' ident = r'[_A-z]\w*' pos = r'[0-9]+' braced = r'{}' double_braced = r'{{}}' globals = True locals = True fix_newlines = True def __init__(self, *args, **kwargs): self._data = [] self._dict = {} self._indent_level = 0 self._indent_spaces = 4 self.globals = kwargs.pop('globals', type(self).globals) self.locals = kwargs.pop('locals', type(self).locals) self._fix_newlines = \ kwargs.pop('fix_newlines', type(self).fix_newlines) if args: self.__call__(args) def indent(self, count=1): self._indent_level += self._indent_spaces * count def dedent(self, count=1): assert self._indent_level >= (self._indent_spaces * count) self._indent_level -= self._indent_spaces * count def fix(self, status): previous = self._fix_newlines self._fix_newlines = status return previous def nofix(self): previous = self._fix_newlines self._fix_newlines = False return previous def clear(): self._data = [] def write(self, *args): f = file(os.path.join(*args), "w") for data in self._data: f.write(data) f.close() def __str__(self): data = string.join(self._data, '') self._data = [ data ] return data def __getitem__(self, item): return self._dict[item] def __setitem__(self, item, value): self._dict[item] = value def __delitem__(self, item): del self._dict[item] def __contains__(self, item): return item in self._dict def __iadd__(self, data): self.append(data) def append(self, data): if isinstance(data, code_formatter): self._data.extend(data._data) else: self._append(str(data)) def _append(self, data): if not self._fix_newlines: self._data.append(data) return initial_newline = not self._data or self._data[-1] == '\n' for line in data.splitlines(): if line: if self._indent_level: self._data.append(' ' * self._indent_level) self._data.append(line) if line or not initial_newline: self._data.append('\n') initial_newline = False def __call__(self, *args, **kwargs): if not args: self._data.append('\n') return format = args[0] args = args[1:] frame = inspect.currentframe().f_back l = lookup(self, frame, *args, **kwargs) def convert(match): ident = match.group('lone') # check for a lone identifier if ident: indent = match.group('indent') # must be spaces lone = '%s' % (l[ident], ) def indent_lines(gen): for line in gen: yield indent yield line return ''.join(indent_lines(lone.splitlines(True))) # check for an identifier, braced or not ident = match.group('ident') or match.group('b_ident') if ident is not None: return '%s' % (l[ident], ) # check for a positional parameter, braced or not pos = match.group('pos') or match.group('b_pos') if pos is not None: pos = int(pos) if pos > len(args): raise ValueError \ ('Positional parameter #%d not found in pattern' % pos, code_formatter.pattern) return '%s' % (args[int(pos)], ) # check for a double braced expression eval_expr = match.group('eval') if eval_expr is not None: result = eval(eval_expr, {}, l) return '%s' % (result, ) # check for an escaped delimiter if match.group('escaped') is not None: return '$' # At this point, we have to match invalid if match.group('invalid') is None: # didn't match invalid! raise ValueError('Unrecognized named group in pattern', code_formatter.pattern) i = match.start('invalid') if i == 0: colno = 1 lineno = 1 else: lines = format[:i].splitlines(True) colno = i - reduce(lambda x,y: x+y, (len(z) for z in lines)) lineno = len(lines) raise ValueError('Invalid format string: line %d, col %d' % (lineno, colno)) d = code_formatter.pattern.sub(convert, format) self._append(d) __all__ = [ "code_formatter" ] if __name__ == '__main__': from code_formatter import code_formatter f = code_formatter() class Foo(dict): def __init__(self, **kwargs): self.update(kwargs) def __getattr__(self, attr): return self[attr] x = "this is a test" l = [ [Foo(x=[Foo(y=9)])] ] y = code_formatter() y(''' { this_is_a_test(); } ''') f(' $y') f('''$__file__:$__line__ {''') f("${{', '.join(str(x) for x in xrange(4))}}") f('${x}') f('$x') f.indent() for i in xrange(5): f('$x') f('$i') f('$0', "zero") f('$1 $0', "zero", "one") f('${0}', "he went") f('${0}asdf', "he went") f.dedent() f(''' ${{l[0][0]["x"][0].y}} } ''', 1, 9) print f,