| | """ |
| | A small templating language |
| | |
| | This implements a small templating language. This language implements |
| | if/elif/else, for/continue/break, expressions, and blocks of Python |
| | code. The syntax is:: |
| | |
| | {{any expression (function calls etc)}} |
| | {{any expression | filter}} |
| | {{for x in y}}...{{endfor}} |
| | {{if x}}x{{elif y}}y{{else}}z{{endif}} |
| | {{py:x=1}} |
| | {{py: |
| | def foo(bar): |
| | return 'baz' |
| | }} |
| | {{default var = default_value}} |
| | {{# comment}} |
| | |
| | You use this with the ``Template`` class or the ``sub`` shortcut. |
| | The ``Template`` class takes the template string and the name of |
| | the template (for errors) and a default namespace. Then (like |
| | ``string.Template``) you can call the ``tmpl.substitute(**kw)`` |
| | method to make a substitution (or ``tmpl.substitute(a_dict)``). |
| | |
| | ``sub(content, **kw)`` substitutes the template immediately. You |
| | can use ``__name='tmpl.html'`` to set the name of the template. |
| | |
| | If there are syntax errors ``TemplateError`` will be raised. |
| | """ |
| |
|
| | from __future__ import absolute_import |
| |
|
| | import re |
| | import sys |
| | import cgi |
| | try: |
| | from urllib import quote as url_quote |
| | except ImportError: |
| | from urllib.parse import quote as url_quote |
| | import os |
| | import tokenize |
| | from io import StringIO |
| |
|
| | from ._looper import looper |
| | from .compat3 import bytes, unicode_, basestring_, next, is_unicode, coerce_text |
| |
|
| | __all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', |
| | 'sub_html', 'html', 'bunch'] |
| |
|
| | in_re = re.compile(r'\s+in\s+') |
| | var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) |
| |
|
| |
|
| | class TemplateError(Exception): |
| | """Exception raised while parsing a template |
| | """ |
| |
|
| | def __init__(self, message, position, name=None): |
| | Exception.__init__(self, message) |
| | self.position = position |
| | self.name = name |
| |
|
| | def __str__(self): |
| | msg = ' '.join(self.args) |
| | if self.position: |
| | msg = '%s at line %s column %s' % ( |
| | msg, self.position[0], self.position[1]) |
| | if self.name: |
| | msg += ' in %s' % self.name |
| | return msg |
| |
|
| |
|
| | class _TemplateContinue(Exception): |
| | pass |
| |
|
| |
|
| | class _TemplateBreak(Exception): |
| | pass |
| |
|
| |
|
| | def get_file_template(name, from_template): |
| | path = os.path.join(os.path.dirname(from_template.name), name) |
| | return from_template.__class__.from_filename( |
| | path, namespace=from_template.namespace, |
| | get_template=from_template.get_template) |
| |
|
| |
|
| | class Template(object): |
| |
|
| | default_namespace = { |
| | 'start_braces': '{{', |
| | 'end_braces': '}}', |
| | 'looper': looper, |
| | } |
| |
|
| | default_encoding = 'utf8' |
| | default_inherit = None |
| |
|
| | def __init__(self, content, name=None, namespace=None, stacklevel=None, |
| | get_template=None, default_inherit=None, line_offset=0, |
| | delimeters=None): |
| | self.content = content |
| |
|
| | |
| | if delimeters is None: |
| | delimeters = (self.default_namespace['start_braces'], |
| | self.default_namespace['end_braces']) |
| | else: |
| | |
| | |
| | self.default_namespace = self.__class__.default_namespace.copy() |
| | self.default_namespace['start_braces'] = delimeters[0] |
| | self.default_namespace['end_braces'] = delimeters[1] |
| | self.delimeters = delimeters |
| |
|
| | self._unicode = is_unicode(content) |
| | if name is None and stacklevel is not None: |
| | try: |
| | caller = sys._getframe(stacklevel) |
| | except ValueError: |
| | pass |
| | else: |
| | globals = caller.f_globals |
| | lineno = caller.f_lineno |
| | if '__file__' in globals: |
| | name = globals['__file__'] |
| | if name.endswith('.pyc') or name.endswith('.pyo'): |
| | name = name[:-1] |
| | elif '__name__' in globals: |
| | name = globals['__name__'] |
| | else: |
| | name = '<string>' |
| | if lineno: |
| | name += ':%s' % lineno |
| | self.name = name |
| | self._parsed = parse(content, name=name, line_offset=line_offset, delimeters=self.delimeters) |
| | if namespace is None: |
| | namespace = {} |
| | self.namespace = namespace |
| | self.get_template = get_template |
| | if default_inherit is not None: |
| | self.default_inherit = default_inherit |
| |
|
| | def from_filename(cls, filename, namespace=None, encoding=None, |
| | default_inherit=None, get_template=get_file_template): |
| | f = open(filename, 'rb') |
| | c = f.read() |
| | f.close() |
| | if encoding: |
| | c = c.decode(encoding) |
| | return cls(content=c, name=filename, namespace=namespace, |
| | default_inherit=default_inherit, get_template=get_template) |
| |
|
| | from_filename = classmethod(from_filename) |
| |
|
| | def __repr__(self): |
| | return '<%s %s name=%r>' % ( |
| | self.__class__.__name__, |
| | hex(id(self))[2:], self.name) |
| |
|
| | def substitute(self, *args, **kw): |
| | if args: |
| | if kw: |
| | raise TypeError( |
| | "You can only give positional *or* keyword arguments") |
| | if len(args) > 1: |
| | raise TypeError( |
| | "You can only give one positional argument") |
| | if not hasattr(args[0], 'items'): |
| | raise TypeError( |
| | "If you pass in a single argument, you must pass in a dictionary-like object (with a .items() method); you gave %r" |
| | % (args[0],)) |
| | kw = args[0] |
| | ns = kw |
| | ns['__template_name__'] = self.name |
| | if self.namespace: |
| | ns.update(self.namespace) |
| | result, defs, inherit = self._interpret(ns) |
| | if not inherit: |
| | inherit = self.default_inherit |
| | if inherit: |
| | result = self._interpret_inherit(result, defs, inherit, ns) |
| | return result |
| |
|
| | def _interpret(self, ns): |
| | __traceback_hide__ = True |
| | parts = [] |
| | defs = {} |
| | self._interpret_codes(self._parsed, ns, out=parts, defs=defs) |
| | if '__inherit__' in defs: |
| | inherit = defs.pop('__inherit__') |
| | else: |
| | inherit = None |
| | return ''.join(parts), defs, inherit |
| |
|
| | def _interpret_inherit(self, body, defs, inherit_template, ns): |
| | __traceback_hide__ = True |
| | if not self.get_template: |
| | raise TemplateError( |
| | 'You cannot use inheritance without passing in get_template', |
| | position=None, name=self.name) |
| | templ = self.get_template(inherit_template, self) |
| | self_ = TemplateObject(self.name) |
| | for name, value in defs.items(): |
| | setattr(self_, name, value) |
| | self_.body = body |
| | ns = ns.copy() |
| | ns['self'] = self_ |
| | return templ.substitute(ns) |
| |
|
| | def _interpret_codes(self, codes, ns, out, defs): |
| | __traceback_hide__ = True |
| | for item in codes: |
| | if isinstance(item, basestring_): |
| | out.append(item) |
| | else: |
| | self._interpret_code(item, ns, out, defs) |
| |
|
| | def _interpret_code(self, code, ns, out, defs): |
| | __traceback_hide__ = True |
| | name, pos = code[0], code[1] |
| | if name == 'py': |
| | self._exec(code[2], ns, pos) |
| | elif name == 'continue': |
| | raise _TemplateContinue() |
| | elif name == 'break': |
| | raise _TemplateBreak() |
| | elif name == 'for': |
| | vars, expr, content = code[2], code[3], code[4] |
| | expr = self._eval(expr, ns, pos) |
| | self._interpret_for(vars, expr, content, ns, out, defs) |
| | elif name == 'cond': |
| | parts = code[2:] |
| | self._interpret_if(parts, ns, out, defs) |
| | elif name == 'expr': |
| | parts = code[2].split('|') |
| | base = self._eval(parts[0], ns, pos) |
| | for part in parts[1:]: |
| | func = self._eval(part, ns, pos) |
| | base = func(base) |
| | out.append(self._repr(base, pos)) |
| | elif name == 'default': |
| | var, expr = code[2], code[3] |
| | if var not in ns: |
| | result = self._eval(expr, ns, pos) |
| | ns[var] = result |
| | elif name == 'inherit': |
| | expr = code[2] |
| | value = self._eval(expr, ns, pos) |
| | defs['__inherit__'] = value |
| | elif name == 'def': |
| | name = code[2] |
| | signature = code[3] |
| | parts = code[4] |
| | ns[name] = defs[name] = TemplateDef(self, name, signature, body=parts, ns=ns, |
| | pos=pos) |
| | elif name == 'comment': |
| | return |
| | else: |
| | assert 0, "Unknown code: %r" % name |
| |
|
| | def _interpret_for(self, vars, expr, content, ns, out, defs): |
| | __traceback_hide__ = True |
| | for item in expr: |
| | if len(vars) == 1: |
| | ns[vars[0]] = item |
| | else: |
| | if len(vars) != len(item): |
| | raise ValueError( |
| | 'Need %i items to unpack (got %i items)' |
| | % (len(vars), len(item))) |
| | for name, value in zip(vars, item): |
| | ns[name] = value |
| | try: |
| | self._interpret_codes(content, ns, out, defs) |
| | except _TemplateContinue: |
| | continue |
| | except _TemplateBreak: |
| | break |
| |
|
| | def _interpret_if(self, parts, ns, out, defs): |
| | __traceback_hide__ = True |
| | |
| | for part in parts: |
| | assert not isinstance(part, basestring_) |
| | name, pos = part[0], part[1] |
| | if name == 'else': |
| | result = True |
| | else: |
| | result = self._eval(part[2], ns, pos) |
| | if result: |
| | self._interpret_codes(part[3], ns, out, defs) |
| | break |
| |
|
| | def _eval(self, code, ns, pos): |
| | __traceback_hide__ = True |
| | try: |
| | try: |
| | value = eval(code, self.default_namespace, ns) |
| | except SyntaxError as e: |
| | raise SyntaxError( |
| | 'invalid syntax in expression: %s' % code) |
| | return value |
| | except Exception as e: |
| | if getattr(e, 'args', None): |
| | arg0 = e.args[0] |
| | else: |
| | arg0 = coerce_text(e) |
| | e.args = (self._add_line_info(arg0, pos),) |
| | raise |
| |
|
| | def _exec(self, code, ns, pos): |
| | __traceback_hide__ = True |
| | try: |
| | exec(code, self.default_namespace, ns) |
| | except Exception as e: |
| | if e.args: |
| | e.args = (self._add_line_info(e.args[0], pos),) |
| | else: |
| | e.args = (self._add_line_info(None, pos),) |
| | raise |
| |
|
| | def _repr(self, value, pos): |
| | __traceback_hide__ = True |
| | try: |
| | if value is None: |
| | return '' |
| | if self._unicode: |
| | try: |
| | value = unicode_(value) |
| | except UnicodeDecodeError: |
| | value = bytes(value) |
| | else: |
| | if not isinstance(value, basestring_): |
| | value = coerce_text(value) |
| | if (is_unicode(value) |
| | and self.default_encoding): |
| | value = value.encode(self.default_encoding) |
| | except Exception as e: |
| | e.args = (self._add_line_info(e.args[0], pos),) |
| | raise |
| | else: |
| | if self._unicode and isinstance(value, bytes): |
| | if not self.default_encoding: |
| | raise UnicodeDecodeError( |
| | 'Cannot decode bytes value %r into unicode ' |
| | '(no default_encoding provided)' % value) |
| | try: |
| | value = value.decode(self.default_encoding) |
| | except UnicodeDecodeError as e: |
| | raise UnicodeDecodeError( |
| | e.encoding, |
| | e.object, |
| | e.start, |
| | e.end, |
| | e.reason + ' in string %r' % value) |
| | elif not self._unicode and is_unicode(value): |
| | if not self.default_encoding: |
| | raise UnicodeEncodeError( |
| | 'Cannot encode unicode value %r into bytes ' |
| | '(no default_encoding provided)' % value) |
| | value = value.encode(self.default_encoding) |
| | return value |
| |
|
| | def _add_line_info(self, msg, pos): |
| | msg = "%s at line %s column %s" % ( |
| | msg, pos[0], pos[1]) |
| | if self.name: |
| | msg += " in file %s" % self.name |
| | return msg |
| |
|
| |
|
| | def sub(content, delimeters=None, **kw): |
| | name = kw.get('__name') |
| | tmpl = Template(content, name=name, delimeters=delimeters) |
| | return tmpl.substitute(kw) |
| |
|
| |
|
| | def paste_script_template_renderer(content, vars, filename=None): |
| | tmpl = Template(content, name=filename) |
| | return tmpl.substitute(vars) |
| |
|
| |
|
| | class bunch(dict): |
| |
|
| | def __init__(self, **kw): |
| | for name, value in kw.items(): |
| | setattr(self, name, value) |
| |
|
| | def __setattr__(self, name, value): |
| | self[name] = value |
| |
|
| | def __getattr__(self, name): |
| | try: |
| | return self[name] |
| | except KeyError: |
| | raise AttributeError(name) |
| |
|
| | def __getitem__(self, key): |
| | if 'default' in self: |
| | try: |
| | return dict.__getitem__(self, key) |
| | except KeyError: |
| | return dict.__getitem__(self, 'default') |
| | else: |
| | return dict.__getitem__(self, key) |
| |
|
| | def __repr__(self): |
| | return '<%s %s>' % ( |
| | self.__class__.__name__, |
| | ' '.join(['%s=%r' % (k, v) for k, v in sorted(self.items())])) |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | class html(object): |
| |
|
| | def __init__(self, value): |
| | self.value = value |
| |
|
| | def __str__(self): |
| | return self.value |
| |
|
| | def __html__(self): |
| | return self.value |
| |
|
| | def __repr__(self): |
| | return '<%s %r>' % ( |
| | self.__class__.__name__, self.value) |
| |
|
| |
|
| | def html_quote(value, force=True): |
| | if not force and hasattr(value, '__html__'): |
| | return value.__html__() |
| | if value is None: |
| | return '' |
| | if not isinstance(value, basestring_): |
| | value = coerce_text(value) |
| | if sys.version >= "3" and isinstance(value, bytes): |
| | value = cgi.escape(value.decode('latin1'), 1) |
| | value = value.encode('latin1') |
| | else: |
| | value = cgi.escape(value, 1) |
| | if sys.version < "3": |
| | if is_unicode(value): |
| | value = value.encode('ascii', 'xmlcharrefreplace') |
| | return value |
| |
|
| |
|
| | def url(v): |
| | v = coerce_text(v) |
| | if is_unicode(v): |
| | v = v.encode('utf8') |
| | return url_quote(v) |
| |
|
| |
|
| | def attr(**kw): |
| | parts = [] |
| | for name, value in sorted(kw.items()): |
| | if value is None: |
| | continue |
| | if name.endswith('_'): |
| | name = name[:-1] |
| | parts.append('%s="%s"' % (html_quote(name), html_quote(value))) |
| | return html(' '.join(parts)) |
| |
|
| |
|
| | class HTMLTemplate(Template): |
| |
|
| | default_namespace = Template.default_namespace.copy() |
| | default_namespace.update(dict( |
| | html=html, |
| | attr=attr, |
| | url=url, |
| | html_quote=html_quote, |
| | )) |
| |
|
| | def _repr(self, value, pos): |
| | if hasattr(value, '__html__'): |
| | value = value.__html__() |
| | quote = False |
| | else: |
| | quote = True |
| | plain = Template._repr(self, value, pos) |
| | if quote: |
| | return html_quote(plain) |
| | else: |
| | return plain |
| |
|
| |
|
| | def sub_html(content, **kw): |
| | name = kw.get('__name') |
| | tmpl = HTMLTemplate(content, name=name) |
| | return tmpl.substitute(kw) |
| |
|
| |
|
| | class TemplateDef(object): |
| | def __init__(self, template, func_name, func_signature, |
| | body, ns, pos, bound_self=None): |
| | self._template = template |
| | self._func_name = func_name |
| | self._func_signature = func_signature |
| | self._body = body |
| | self._ns = ns |
| | self._pos = pos |
| | self._bound_self = bound_self |
| |
|
| | def __repr__(self): |
| | return '<tempita function %s(%s) at %s:%s>' % ( |
| | self._func_name, self._func_signature, |
| | self._template.name, self._pos) |
| |
|
| | def __str__(self): |
| | return self() |
| |
|
| | def __call__(self, *args, **kw): |
| | values = self._parse_signature(args, kw) |
| | ns = self._ns.copy() |
| | ns.update(values) |
| | if self._bound_self is not None: |
| | ns['self'] = self._bound_self |
| | out = [] |
| | subdefs = {} |
| | self._template._interpret_codes(self._body, ns, out, subdefs) |
| | return ''.join(out) |
| |
|
| | def __get__(self, obj, type=None): |
| | if obj is None: |
| | return self |
| | return self.__class__( |
| | self._template, self._func_name, self._func_signature, |
| | self._body, self._ns, self._pos, bound_self=obj) |
| |
|
| | def _parse_signature(self, args, kw): |
| | values = {} |
| | sig_args, var_args, var_kw, defaults = self._func_signature |
| | extra_kw = {} |
| | for name, value in kw.items(): |
| | if not var_kw and name not in sig_args: |
| | raise TypeError( |
| | 'Unexpected argument %s' % name) |
| | if name in sig_args: |
| | values[sig_args] = value |
| | else: |
| | extra_kw[name] = value |
| | args = list(args) |
| | sig_args = list(sig_args) |
| | while args: |
| | while sig_args and sig_args[0] in values: |
| | sig_args.pop(0) |
| | if sig_args: |
| | name = sig_args.pop(0) |
| | values[name] = args.pop(0) |
| | elif var_args: |
| | values[var_args] = tuple(args) |
| | break |
| | else: |
| | raise TypeError( |
| | 'Extra position arguments: %s' |
| | % ', '.join([repr(v) for v in args])) |
| | for name, value_expr in defaults.items(): |
| | if name not in values: |
| | values[name] = self._template._eval( |
| | value_expr, self._ns, self._pos) |
| | for name in sig_args: |
| | if name not in values: |
| | raise TypeError( |
| | 'Missing argument: %s' % name) |
| | if var_kw: |
| | values[var_kw] = extra_kw |
| | return values |
| |
|
| |
|
| | class TemplateObject(object): |
| |
|
| | def __init__(self, name): |
| | self.__name = name |
| | self.get = TemplateObjectGetter(self) |
| |
|
| | def __repr__(self): |
| | return '<%s %s>' % (self.__class__.__name__, self.__name) |
| |
|
| |
|
| | class TemplateObjectGetter(object): |
| |
|
| | def __init__(self, template_obj): |
| | self.__template_obj = template_obj |
| |
|
| | def __getattr__(self, attr): |
| | return getattr(self.__template_obj, attr, Empty) |
| |
|
| | def __repr__(self): |
| | return '<%s around %r>' % (self.__class__.__name__, self.__template_obj) |
| |
|
| |
|
| | class _Empty(object): |
| | def __call__(self, *args, **kw): |
| | return self |
| |
|
| | def __str__(self): |
| | return '' |
| |
|
| | def __repr__(self): |
| | return 'Empty' |
| |
|
| | def __unicode__(self): |
| | return u'' |
| |
|
| | def __iter__(self): |
| | return iter(()) |
| |
|
| | def __bool__(self): |
| | return False |
| |
|
| | if sys.version < "3": |
| | __nonzero__ = __bool__ |
| |
|
| | Empty = _Empty() |
| | del _Empty |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | def lex(s, name=None, trim_whitespace=True, line_offset=0, delimeters=None): |
| | """ |
| | Lex a string into chunks: |
| | |
| | >>> lex('hey') |
| | ['hey'] |
| | >>> lex('hey {{you}}') |
| | ['hey ', ('you', (1, 7))] |
| | >>> lex('hey {{') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: No }} to finish last expression at line 1 column 7 |
| | >>> lex('hey }}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: }} outside expression at line 1 column 7 |
| | >>> lex('hey {{ {{') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: {{ inside expression at line 1 column 10 |
| | |
| | """ |
| | if delimeters is None: |
| | delimeters = ( Template.default_namespace['start_braces'], |
| | Template.default_namespace['end_braces'] ) |
| | in_expr = False |
| | chunks = [] |
| | last = 0 |
| | last_pos = (line_offset + 1, 1) |
| |
|
| | token_re = re.compile(r'%s|%s' % (re.escape(delimeters[0]), |
| | re.escape(delimeters[1]))) |
| | for match in token_re.finditer(s): |
| | expr = match.group(0) |
| | pos = find_position(s, match.end(), last, last_pos) |
| | if expr == delimeters[0] and in_expr: |
| | raise TemplateError('%s inside expression' % delimeters[0], |
| | position=pos, |
| | name=name) |
| | elif expr == delimeters[1] and not in_expr: |
| | raise TemplateError('%s outside expression' % delimeters[1], |
| | position=pos, |
| | name=name) |
| | if expr == delimeters[0]: |
| | part = s[last:match.start()] |
| | if part: |
| | chunks.append(part) |
| | in_expr = True |
| | else: |
| | chunks.append((s[last:match.start()], last_pos)) |
| | in_expr = False |
| | last = match.end() |
| | last_pos = pos |
| | if in_expr: |
| | raise TemplateError('No %s to finish last expression' % delimeters[1], |
| | name=name, position=last_pos) |
| | part = s[last:] |
| | if part: |
| | chunks.append(part) |
| | if trim_whitespace: |
| | chunks = trim_lex(chunks) |
| | return chunks |
| |
|
| | statement_re = re.compile(r'^(?:if |elif |for |def |inherit |default |py:)') |
| | single_statements = ['else', 'endif', 'endfor', 'enddef', 'continue', 'break'] |
| | trail_whitespace_re = re.compile(r'\n\r?[\t ]*$') |
| | lead_whitespace_re = re.compile(r'^[\t ]*\n') |
| |
|
| |
|
| | def trim_lex(tokens): |
| | r""" |
| | Takes a lexed set of tokens, and removes whitespace when there is |
| | a directive on a line by itself: |
| | |
| | >>> tokens = lex('{{if x}}\nx\n{{endif}}\ny', trim_whitespace=False) |
| | >>> tokens |
| | [('if x', (1, 3)), '\nx\n', ('endif', (3, 3)), '\ny'] |
| | >>> trim_lex(tokens) |
| | [('if x', (1, 3)), 'x\n', ('endif', (3, 3)), 'y'] |
| | """ |
| | last_trim = None |
| | for i, current in enumerate(tokens): |
| | if isinstance(current, basestring_): |
| | |
| | continue |
| | item = current[0] |
| | if not statement_re.search(item) and item not in single_statements: |
| | continue |
| | if not i: |
| | prev = '' |
| | else: |
| | prev = tokens[i - 1] |
| | if i + 1 >= len(tokens): |
| | next_chunk = '' |
| | else: |
| | next_chunk = tokens[i + 1] |
| | if (not isinstance(next_chunk, basestring_) |
| | or not isinstance(prev, basestring_)): |
| | continue |
| | prev_ok = not prev or trail_whitespace_re.search(prev) |
| | if i == 1 and not prev.strip(): |
| | prev_ok = True |
| | if last_trim is not None and last_trim + 2 == i and not prev.strip(): |
| | prev_ok = 'last' |
| | if (prev_ok |
| | and (not next_chunk or lead_whitespace_re.search(next_chunk) |
| | or (i == len(tokens) - 2 and not next_chunk.strip()))): |
| | if prev: |
| | if ((i == 1 and not prev.strip()) |
| | or prev_ok == 'last'): |
| | tokens[i - 1] = '' |
| | else: |
| | m = trail_whitespace_re.search(prev) |
| | |
| | prev = prev[:m.start() + 1] |
| | tokens[i - 1] = prev |
| | if next_chunk: |
| | last_trim = i |
| | if i == len(tokens) - 2 and not next_chunk.strip(): |
| | tokens[i + 1] = '' |
| | else: |
| | m = lead_whitespace_re.search(next_chunk) |
| | next_chunk = next_chunk[m.end():] |
| | tokens[i + 1] = next_chunk |
| | return tokens |
| |
|
| |
|
| | def find_position(string, index, last_index, last_pos): |
| | """Given a string and index, return (line, column)""" |
| | lines = string.count('\n', last_index, index) |
| | if lines > 0: |
| | column = index - string.rfind('\n', last_index, index) |
| | else: |
| | column = last_pos[1] + (index - last_index) |
| | return (last_pos[0] + lines, column) |
| |
|
| |
|
| | def parse(s, name=None, line_offset=0, delimeters=None): |
| | r""" |
| | Parses a string into a kind of AST |
| | |
| | >>> parse('{{x}}') |
| | [('expr', (1, 3), 'x')] |
| | >>> parse('foo') |
| | ['foo'] |
| | >>> parse('{{if x}}test{{endif}}') |
| | [('cond', (1, 3), ('if', (1, 3), 'x', ['test']))] |
| | >>> parse('series->{{for x in y}}x={{x}}{{endfor}}') |
| | ['series->', ('for', (1, 11), ('x',), 'y', ['x=', ('expr', (1, 27), 'x')])] |
| | >>> parse('{{for x, y in z:}}{{continue}}{{endfor}}') |
| | [('for', (1, 3), ('x', 'y'), 'z', [('continue', (1, 21))])] |
| | >>> parse('{{py:x=1}}') |
| | [('py', (1, 3), 'x=1')] |
| | >>> parse('{{if x}}a{{elif y}}b{{else}}c{{endif}}') |
| | [('cond', (1, 3), ('if', (1, 3), 'x', ['a']), ('elif', (1, 12), 'y', ['b']), ('else', (1, 23), None, ['c']))] |
| | |
| | Some exceptions:: |
| | |
| | >>> parse('{{continue}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: continue outside of for loop at line 1 column 3 |
| | >>> parse('{{if x}}foo') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: No {{endif}} at line 1 column 3 |
| | >>> parse('{{else}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: else outside of an if block at line 1 column 3 |
| | >>> parse('{{if x}}{{for x in y}}{{endif}}{{endfor}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: Unexpected endif at line 1 column 25 |
| | >>> parse('{{if}}{{endif}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: if with no expression at line 1 column 3 |
| | >>> parse('{{for x y}}{{endfor}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: Bad for (no "in") in 'x y' at line 1 column 3 |
| | >>> parse('{{py:x=1\ny=2}}') |
| | Traceback (most recent call last): |
| | ... |
| | TemplateError: Multi-line py blocks must start with a newline at line 1 column 3 |
| | """ |
| | if delimeters is None: |
| | delimeters = ( Template.default_namespace['start_braces'], |
| | Template.default_namespace['end_braces'] ) |
| | tokens = lex(s, name=name, line_offset=line_offset, delimeters=delimeters) |
| | result = [] |
| | while tokens: |
| | next_chunk, tokens = parse_expr(tokens, name) |
| | result.append(next_chunk) |
| | return result |
| |
|
| |
|
| | def parse_expr(tokens, name, context=()): |
| | if isinstance(tokens[0], basestring_): |
| | return tokens[0], tokens[1:] |
| | expr, pos = tokens[0] |
| | expr = expr.strip() |
| | if expr.startswith('py:'): |
| | expr = expr[3:].lstrip(' \t') |
| | if expr.startswith('\n') or expr.startswith('\r'): |
| | expr = expr.lstrip('\r\n') |
| | if '\r' in expr: |
| | expr = expr.replace('\r\n', '\n') |
| | expr = expr.replace('\r', '') |
| | expr += '\n' |
| | else: |
| | if '\n' in expr: |
| | raise TemplateError( |
| | 'Multi-line py blocks must start with a newline', |
| | position=pos, name=name) |
| | return ('py', pos, expr), tokens[1:] |
| | elif expr in ('continue', 'break'): |
| | if 'for' not in context: |
| | raise TemplateError( |
| | 'continue outside of for loop', |
| | position=pos, name=name) |
| | return (expr, pos), tokens[1:] |
| | elif expr.startswith('if '): |
| | return parse_cond(tokens, name, context) |
| | elif (expr.startswith('elif ') |
| | or expr == 'else'): |
| | raise TemplateError( |
| | '%s outside of an if block' % expr.split()[0], |
| | position=pos, name=name) |
| | elif expr in ('if', 'elif', 'for'): |
| | raise TemplateError( |
| | '%s with no expression' % expr, |
| | position=pos, name=name) |
| | elif expr in ('endif', 'endfor', 'enddef'): |
| | raise TemplateError( |
| | 'Unexpected %s' % expr, |
| | position=pos, name=name) |
| | elif expr.startswith('for '): |
| | return parse_for(tokens, name, context) |
| | elif expr.startswith('default '): |
| | return parse_default(tokens, name, context) |
| | elif expr.startswith('inherit '): |
| | return parse_inherit(tokens, name, context) |
| | elif expr.startswith('def '): |
| | return parse_def(tokens, name, context) |
| | elif expr.startswith('#'): |
| | return ('comment', pos, tokens[0][0]), tokens[1:] |
| | return ('expr', pos, tokens[0][0]), tokens[1:] |
| |
|
| |
|
| | def parse_cond(tokens, name, context): |
| | start = tokens[0][1] |
| | pieces = [] |
| | context = context + ('if',) |
| | while 1: |
| | if not tokens: |
| | raise TemplateError( |
| | 'Missing {{endif}}', |
| | position=start, name=name) |
| | if (isinstance(tokens[0], tuple) |
| | and tokens[0][0] == 'endif'): |
| | return ('cond', start) + tuple(pieces), tokens[1:] |
| | next_chunk, tokens = parse_one_cond(tokens, name, context) |
| | pieces.append(next_chunk) |
| |
|
| |
|
| | def parse_one_cond(tokens, name, context): |
| | (first, pos), tokens = tokens[0], tokens[1:] |
| | content = [] |
| | if first.endswith(':'): |
| | first = first[:-1] |
| | if first.startswith('if '): |
| | part = ('if', pos, first[3:].lstrip(), content) |
| | elif first.startswith('elif '): |
| | part = ('elif', pos, first[5:].lstrip(), content) |
| | elif first == 'else': |
| | part = ('else', pos, None, content) |
| | else: |
| | assert 0, "Unexpected token %r at %s" % (first, pos) |
| | while 1: |
| | if not tokens: |
| | raise TemplateError( |
| | 'No {{endif}}', |
| | position=pos, name=name) |
| | if (isinstance(tokens[0], tuple) |
| | and (tokens[0][0] == 'endif' |
| | or tokens[0][0].startswith('elif ') |
| | or tokens[0][0] == 'else')): |
| | return part, tokens |
| | next_chunk, tokens = parse_expr(tokens, name, context) |
| | content.append(next_chunk) |
| |
|
| |
|
| | def parse_for(tokens, name, context): |
| | first, pos = tokens[0] |
| | tokens = tokens[1:] |
| | context = ('for',) + context |
| | content = [] |
| | assert first.startswith('for ') |
| | if first.endswith(':'): |
| | first = first[:-1] |
| | first = first[3:].strip() |
| | match = in_re.search(first) |
| | if not match: |
| | raise TemplateError( |
| | 'Bad for (no "in") in %r' % first, |
| | position=pos, name=name) |
| | vars = first[:match.start()] |
| | if '(' in vars: |
| | raise TemplateError( |
| | 'You cannot have () in the variable section of a for loop (%r)' |
| | % vars, position=pos, name=name) |
| | vars = tuple([ |
| | v.strip() for v in first[:match.start()].split(',') |
| | if v.strip()]) |
| | expr = first[match.end():] |
| | while 1: |
| | if not tokens: |
| | raise TemplateError( |
| | 'No {{endfor}}', |
| | position=pos, name=name) |
| | if (isinstance(tokens[0], tuple) |
| | and tokens[0][0] == 'endfor'): |
| | return ('for', pos, vars, expr, content), tokens[1:] |
| | next_chunk, tokens = parse_expr(tokens, name, context) |
| | content.append(next_chunk) |
| |
|
| |
|
| | def parse_default(tokens, name, context): |
| | first, pos = tokens[0] |
| | assert first.startswith('default ') |
| | first = first.split(None, 1)[1] |
| | parts = first.split('=', 1) |
| | if len(parts) == 1: |
| | raise TemplateError( |
| | "Expression must be {{default var=value}}; no = found in %r" % first, |
| | position=pos, name=name) |
| | var = parts[0].strip() |
| | if ',' in var: |
| | raise TemplateError( |
| | "{{default x, y = ...}} is not supported", |
| | position=pos, name=name) |
| | if not var_re.search(var): |
| | raise TemplateError( |
| | "Not a valid variable name for {{default}}: %r" |
| | % var, position=pos, name=name) |
| | expr = parts[1].strip() |
| | return ('default', pos, var, expr), tokens[1:] |
| |
|
| |
|
| | def parse_inherit(tokens, name, context): |
| | first, pos = tokens[0] |
| | assert first.startswith('inherit ') |
| | expr = first.split(None, 1)[1] |
| | return ('inherit', pos, expr), tokens[1:] |
| |
|
| |
|
| | def parse_def(tokens, name, context): |
| | first, start = tokens[0] |
| | tokens = tokens[1:] |
| | assert first.startswith('def ') |
| | first = first.split(None, 1)[1] |
| | if first.endswith(':'): |
| | first = first[:-1] |
| | if '(' not in first: |
| | func_name = first |
| | sig = ((), None, None, {}) |
| | elif not first.endswith(')'): |
| | raise TemplateError("Function definition doesn't end with ): %s" % first, |
| | position=start, name=name) |
| | else: |
| | first = first[:-1] |
| | func_name, sig_text = first.split('(', 1) |
| | sig = parse_signature(sig_text, name, start) |
| | context = context + ('def',) |
| | content = [] |
| | while 1: |
| | if not tokens: |
| | raise TemplateError( |
| | 'Missing {{enddef}}', |
| | position=start, name=name) |
| | if (isinstance(tokens[0], tuple) |
| | and tokens[0][0] == 'enddef'): |
| | return ('def', start, func_name, sig, content), tokens[1:] |
| | next_chunk, tokens = parse_expr(tokens, name, context) |
| | content.append(next_chunk) |
| |
|
| |
|
| | def parse_signature(sig_text, name, pos): |
| | tokens = tokenize.generate_tokens(StringIO(sig_text).readline) |
| | sig_args = [] |
| | var_arg = None |
| | var_kw = None |
| | defaults = {} |
| |
|
| | def get_token(pos=False): |
| | try: |
| | tok_type, tok_string, (srow, scol), (erow, ecol), line = next(tokens) |
| | except StopIteration: |
| | return tokenize.ENDMARKER, '' |
| | if pos: |
| | return tok_type, tok_string, (srow, scol), (erow, ecol) |
| | else: |
| | return tok_type, tok_string |
| | while 1: |
| | var_arg_type = None |
| | tok_type, tok_string = get_token() |
| | if tok_type == tokenize.ENDMARKER: |
| | break |
| | if tok_type == tokenize.OP and (tok_string == '*' or tok_string == '**'): |
| | var_arg_type = tok_string |
| | tok_type, tok_string = get_token() |
| | if tok_type != tokenize.NAME: |
| | raise TemplateError('Invalid signature: (%s)' % sig_text, |
| | position=pos, name=name) |
| | var_name = tok_string |
| | tok_type, tok_string = get_token() |
| | if tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','): |
| | if var_arg_type == '*': |
| | var_arg = var_name |
| | elif var_arg_type == '**': |
| | var_kw = var_name |
| | else: |
| | sig_args.append(var_name) |
| | if tok_type == tokenize.ENDMARKER: |
| | break |
| | continue |
| | if var_arg_type is not None: |
| | raise TemplateError('Invalid signature: (%s)' % sig_text, |
| | position=pos, name=name) |
| | if tok_type == tokenize.OP and tok_string == '=': |
| | nest_type = None |
| | unnest_type = None |
| | nest_count = 0 |
| | start_pos = end_pos = None |
| | parts = [] |
| | while 1: |
| | tok_type, tok_string, s, e = get_token(True) |
| | if start_pos is None: |
| | start_pos = s |
| | end_pos = e |
| | if tok_type == tokenize.ENDMARKER and nest_count: |
| | raise TemplateError('Invalid signature: (%s)' % sig_text, |
| | position=pos, name=name) |
| | if (not nest_count and |
| | (tok_type == tokenize.ENDMARKER or (tok_type == tokenize.OP and tok_string == ','))): |
| | default_expr = isolate_expression(sig_text, start_pos, end_pos) |
| | defaults[var_name] = default_expr |
| | sig_args.append(var_name) |
| | break |
| | parts.append((tok_type, tok_string)) |
| | if nest_count and tok_type == tokenize.OP and tok_string == nest_type: |
| | nest_count += 1 |
| | elif nest_count and tok_type == tokenize.OP and tok_string == unnest_type: |
| | nest_count -= 1 |
| | if not nest_count: |
| | nest_type = unnest_type = None |
| | elif not nest_count and tok_type == tokenize.OP and tok_string in ('(', '[', '{'): |
| | nest_type = tok_string |
| | nest_count = 1 |
| | unnest_type = {'(': ')', '[': ']', '{': '}'}[nest_type] |
| | return sig_args, var_arg, var_kw, defaults |
| |
|
| |
|
| | def isolate_expression(string, start_pos, end_pos): |
| | srow, scol = start_pos |
| | srow -= 1 |
| | erow, ecol = end_pos |
| | erow -= 1 |
| | lines = string.splitlines(True) |
| | if srow == erow: |
| | return lines[srow][scol:ecol] |
| | parts = [lines[srow][scol:]] |
| | parts.extend(lines[srow+1:erow]) |
| | if erow < len(lines): |
| | |
| | parts.append(lines[erow][:ecol]) |
| | return ''.join(parts) |
| |
|
| | _fill_command_usage = """\ |
| | %prog [OPTIONS] TEMPLATE arg=value |
| | |
| | Use py:arg=value to set a Python value; otherwise all values are |
| | strings. |
| | """ |
| |
|
| |
|
| | def fill_command(args=None): |
| | import sys |
| | import optparse |
| | import pkg_resources |
| | import os |
| | if args is None: |
| | args = sys.argv[1:] |
| | dist = pkg_resources.get_distribution('Paste') |
| | parser = optparse.OptionParser( |
| | version=coerce_text(dist), |
| | usage=_fill_command_usage) |
| | parser.add_option( |
| | '-o', '--output', |
| | dest='output', |
| | metavar="FILENAME", |
| | help="File to write output to (default stdout)") |
| | parser.add_option( |
| | '--html', |
| | dest='use_html', |
| | action='store_true', |
| | help="Use HTML style filling (including automatic HTML quoting)") |
| | parser.add_option( |
| | '--env', |
| | dest='use_env', |
| | action='store_true', |
| | help="Put the environment in as top-level variables") |
| | options, args = parser.parse_args(args) |
| | if len(args) < 1: |
| | print('You must give a template filename') |
| | sys.exit(2) |
| | template_name = args[0] |
| | args = args[1:] |
| | vars = {} |
| | if options.use_env: |
| | vars.update(os.environ) |
| | for value in args: |
| | if '=' not in value: |
| | print('Bad argument: %r' % value) |
| | sys.exit(2) |
| | name, value = value.split('=', 1) |
| | if name.startswith('py:'): |
| | name = name[:3] |
| | value = eval(value) |
| | vars[name] = value |
| | if template_name == '-': |
| | template_content = sys.stdin.read() |
| | template_name = '<stdin>' |
| | else: |
| | f = open(template_name, 'rb') |
| | template_content = f.read() |
| | f.close() |
| | if options.use_html: |
| | TemplateClass = HTMLTemplate |
| | else: |
| | TemplateClass = Template |
| | template = TemplateClass(template_content, name=template_name) |
| | result = template.substitute(vars) |
| | if options.output: |
| | f = open(options.output, 'wb') |
| | f.write(result) |
| | f.close() |
| | else: |
| | sys.stdout.write(result) |
| |
|
| | if __name__ == '__main__': |
| | fill_command() |
| |
|