From ca8c5c11d97ede79fc869d3a8ca7bdb4cd76301c Mon Sep 17 00:00:00 2001 From: ianb Date: Sat, 22 Nov 2008 17:36:12 +0000 Subject: Added template inheritance --- docs/index.txt | 51 +++++++- setup.py | 4 +- tempita/__init__.py | 332 +++++++++++++++++++++++++++++++++++++++++++++--- tests/test_template.txt | 49 ++++++- 4 files changed, 411 insertions(+), 25 deletions(-) diff --git a/docs/index.txt b/docs/index.txt index ef2da2f..65cd520 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -186,7 +186,54 @@ Loops should be unsurprising:: {{a}} = {{b | repr}} {{endfor}} -See? Unsurprising. +See? Unsurprising. Note that nested tuples (like ``for a, (b, c) +in...``) are not supported. + +inherit & def +------------- + +You can do template inheritance. To inherit from another template +do:: + + {{inherit "some_other_file"}} + +From Python you must also pass in, to `Template`, a `get_template` +function; the implementation for ``Template.from_filename(...)`` is:: + + def get_file_template(name, from_template): + path = os.path.join(from_template.name, name) + return from_template.__class__.from_filename( + path, namespace=from_template.namespace, + get_template=from_template.get_template) + +You can also pass in a constructor argument `default_inherit`, which +will be the inherited template name when no ``{{inherit}}` is in the +template. + +The inherited template is executed with a variable ``self``, which +includes ``self.body`` which is the text of the child template. You +can also put in definitions in the child, like:: + + {{def sidebar}} + sidebar links... + {{enddef}} + +Then in the parent/inherited template:: + + {{self.sidebar}} + +If you want to make the sidebar method optional, in the inherited +template use:: + + {{self.get.sidebar}} + +If ``sidebar`` is not defined then this will just result in an object +that shows up as the empty string (but is also callable). + +This can be called like ``self.sidebar`` or ``self.sidebar()`` -- defs +can have arguments (like ``{{def sidebar(name)}}``), but when there +are no arguments you can leave off ``()`` (in the call and +definition). Python blocks ------------- @@ -424,6 +471,8 @@ News svn trunk --------- +* Added ``{{inherit}}`` and ``{{def}}`` for doing template inheritance. + * Make error message annotation slightly more robust 0.2 diff --git a/setup.py b/setup.py index c58b49e..935f849 100644 --- a/setup.py +++ b/setup.py @@ -19,8 +19,6 @@ You can read about the `language `_, and there's nothing more to learn about it. -0.1 is the initial release. - You can install from the `svn repository `__ with ``easy_install Tempita==dev``. @@ -36,7 +34,7 @@ You can install from the `svn repository author_email='ianb@colorstudy.com', url='http://pythonpaste.org/tempita/', license='MIT', - packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + packages=['tempita'], include_package_data=True, zip_safe=True, ) diff --git a/tempita/__init__.py b/tempita/__init__.py index 0ddb670..6d4af7a 100644 --- a/tempita/__init__.py +++ b/tempita/__init__.py @@ -33,6 +33,9 @@ import re import sys import cgi import urllib +import os +import tokenize +from cStringIO import StringIO from tempita._looper import looper __all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', @@ -52,8 +55,10 @@ class TemplateError(Exception): self.name = name def __str__(self): - msg = '%s at line %s column %s' % ( - ' '.join(self.args), self.position[0], self.position[1]) + 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 @@ -64,6 +69,12 @@ class _TemplateContinue(Exception): class _TemplateBreak(Exception): pass +def get_file_template(name, from_template): + path = os.path.join(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 = { @@ -73,8 +84,10 @@ class Template(object): } default_encoding = 'utf8' + default_inherit = None - def __init__(self, content, name=None, namespace=None, stacklevel=None): + def __init__(self, content, name=None, namespace=None, stacklevel=None, + get_template=None, default_inherit=None): self.content = content self._unicode = isinstance(content, unicode) if name is None and stacklevel is not None: @@ -100,14 +113,19 @@ class Template(object): 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): + 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) + return cls(content=c, name=filename, namespace=namespace, + default_inherit=default_inherit, get_template=get_template) from_filename = classmethod(from_filename) @@ -133,24 +151,48 @@ class Template(object): ns['__name__'] = self.name ns.update(self.namespace) ns.update(kw) - result = self._interpret(ns) + 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 = [] - self._interpret_codes(self._parsed, ns, out=parts) - return ''.join(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_codes(self, codes, ns, out): + 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) + self._interpret_code(item, ns, out, defs) - def _interpret_code(self, code, ns, out): + def _interpret_code(self, code, ns, out, defs): __traceback_hide__ = True name, pos = code[0], code[1] if name == 'py': @@ -162,10 +204,10 @@ class Template(object): 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) + self._interpret_for(vars, expr, content, ns, out, defs) elif name == 'cond': parts = code[2:] - self._interpret_if(parts, ns, out) + self._interpret_if(parts, ns, out, defs) elif name == 'expr': parts = code[2].split('|') base = self._eval(parts[0], ns, pos) @@ -178,12 +220,22 @@ class Template(object): 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): + def _interpret_for(self, vars, expr, content, ns, out, defs): __traceback_hide__ = True for item in expr: if len(vars) == 1: @@ -196,13 +248,13 @@ class Template(object): for name, value in zip(vars, item): ns[name] = value try: - self._interpret_codes(content, ns, out) + self._interpret_codes(content, ns, out, defs) except _TemplateContinue: continue except _TemplateBreak: break - def _interpret_if(self, parts, ns, out): + def _interpret_if(self, parts, ns, out, defs): __traceback_hide__ = True # @@: if/else/else gets through for part in parts: @@ -213,13 +265,17 @@ class Template(object): else: result = self._eval(part[2], ns, pos) if result: - self._interpret_codes(part[3], ns, out) + self._interpret_codes(part[3], ns, out, defs) break def _eval(self, code, ns, pos): __traceback_hide__ = True try: - value = eval(code, ns) + try: + value = eval(code, ns) + except SyntaxError, e: + raise SyntaxError( + 'invalid syntax in expression: %s' % code) return value except: exc_info = sys.exc_info() @@ -421,6 +477,114 @@ def sub_html(content, **kw): return result +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 '' % ( + 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 __nonzero__(self): + return False + +Empty = _Empty() +del _Empty + ############################################################ ## Lexing and Parsing ############################################################ @@ -623,7 +787,7 @@ def parse_expr(tokens, name, context=()): raise TemplateError( '%s with no expression' % expr, position=pos, name=name) - elif expr in ('endif', 'endfor'): + elif expr in ('endif', 'endfor', 'enddef'): raise TemplateError( 'Unexpected %s' % expr, position=pos, name=name) @@ -631,6 +795,10 @@ def parse_expr(tokens, name, context=()): 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:] @@ -731,6 +899,132 @@ def parse_default(tokens, name, context): 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, tokens = parse_expr(tokens, name, context) + content.append(next) + +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 = tokens.next() + 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): + # It'll sometimes give (end_row_past_finish, 0) + parts.append(lines[erow][:ecol]) + return ''.join(parts) + _fill_command_usage = """\ %prog [OPTIONS] TEMPLATE arg=value diff --git a/tests/test_template.txt b/tests/test_template.txt index 7b74e23..7481aaa 100644 --- a/tests/test_template.txt +++ b/tests/test_template.txt @@ -1,4 +1,7 @@ -The templating language is fairly simple, just {{stuff}}. For +Normal templating +================= + +The templating language is fairly simple, just ``{{stuff}}``. For example:: >>> from tempita import Template, sub @@ -117,7 +120,7 @@ contains a directive/statement (if/for, etc):: >>> sub('{{if 1}}\nx={{x}}\n{{endif}}\n', x=1) 'x=1\n' -Lastly, there is a special directive that will create a default value +There is a special directive that will create a default value for a variable, if no value is given:: >>> sub('{{default x=1}}{{x}}', x=2) @@ -134,3 +137,45 @@ And comments work:: >>> sub('Test=x{{#whatever}}') 'Test=x' + +Inheritance +=========== + +You can have inherited templates; you have to pass in some kind of +get_template function, for example:: + + >>> def get_template(name, from_template): + ... return globals()[name] + +Then we'll define a template that inherits:: + + >>> tmpl = Template('''\ + ... {{inherit "super_"+master}} + ... Hi there! + ... {{def block}}some text{{enddef}} + ... ''', get_template=get_template) + >>> super_test = Template('''\ + ... This is the parent {{master}}. The block: {{self.block}} + ... Then the body: {{self.body}} + ... ''') + >>> print tmpl.substitute(master='test').strip() + This is the parent test. The block: some text + Then the body: + Hi there! + >>> tmpl2 = Template('''\ + ... {{def block(arg='hi_'+master):}}hey {{master}}: {{arg}}{{enddef}} + ... ''', get_template=get_template, default_inherit='super_test') + >>> print tmpl2.substitute(master='test2').strip() + This is the parent test2. The block: hey test2: hi_test2 + Then the body: + >>> super_test = Template('''\ + ... The block: {{self.block('blah')}} + ... ''') + >>> print tmpl2.substitute(master='test2').strip() + The block: hey test2: blah + >>> super_test = Template('''\ + ... Something: {{self.get.something('hi')}} + ... ''') + >>> print tmpl2.substitute(master='test2').strip() + Something: + -- cgit v1.2.1