diff options
Diffstat (limited to 'websdk/genshi/template')
-rw-r--r-- | websdk/genshi/template/__init__.py | 23 | ||||
-rw-r--r-- | websdk/genshi/template/_ast24.py | 446 | ||||
-rw-r--r-- | websdk/genshi/template/ast24.py | 505 | ||||
-rw-r--r-- | websdk/genshi/template/astutil.py | 784 | ||||
-rw-r--r-- | websdk/genshi/template/base.py | 634 | ||||
-rw-r--r-- | websdk/genshi/template/directives.py | 725 | ||||
-rw-r--r-- | websdk/genshi/template/eval.py | 629 | ||||
-rw-r--r-- | websdk/genshi/template/interpolation.py | 153 | ||||
-rw-r--r-- | websdk/genshi/template/loader.py | 344 | ||||
-rw-r--r-- | websdk/genshi/template/markup.py | 397 | ||||
-rw-r--r-- | websdk/genshi/template/plugin.py | 176 | ||||
-rw-r--r-- | websdk/genshi/template/text.py | 333 |
12 files changed, 5149 insertions, 0 deletions
diff --git a/websdk/genshi/template/__init__.py b/websdk/genshi/template/__init__.py new file mode 100644 index 0000000..47a9310 --- /dev/null +++ b/websdk/genshi/template/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2007 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Implementation of the template engine.""" + +from genshi.template.base import Context, Template, TemplateError, \ + TemplateRuntimeError, TemplateSyntaxError, \ + BadDirectiveError +from genshi.template.loader import TemplateLoader, TemplateNotFound +from genshi.template.markup import MarkupTemplate +from genshi.template.text import TextTemplate, OldTextTemplate, NewTextTemplate + +__docformat__ = 'restructuredtext en' diff --git a/websdk/genshi/template/_ast24.py b/websdk/genshi/template/_ast24.py new file mode 100644 index 0000000..05d241b --- /dev/null +++ b/websdk/genshi/template/_ast24.py @@ -0,0 +1,446 @@ +# Generated automatically, please do not edit +# Generator can be found in Genshi SVN, scripts/ast-generator.py + +__version__ = 43614 + +class AST(object): + _fields = None + __doc__ = None + +class operator(AST): + _fields = None + __doc__ = None + _attributes = [] +class Add(operator): + _fields = None + __doc__ = None + +class boolop(AST): + _fields = None + __doc__ = None + _attributes = [] +class And(boolop): + _fields = None + __doc__ = None + +class stmt(AST): + _fields = None + __doc__ = None + _attributes = ['lineno', 'col_offset'] +class Assert(stmt): + _fields = ('test', 'msg') + __doc__ = None + +class Assign(stmt): + _fields = ('targets', 'value') + __doc__ = None + +class expr(AST): + _fields = None + __doc__ = None + _attributes = ['lineno', 'col_offset'] +class Attribute(expr): + _fields = ('value', 'attr', 'ctx') + __doc__ = None + +class AugAssign(stmt): + _fields = ('target', 'op', 'value') + __doc__ = None + +class expr_context(AST): + _fields = None + __doc__ = None + _attributes = [] +class AugLoad(expr_context): + _fields = None + __doc__ = None + +class AugStore(expr_context): + _fields = None + __doc__ = None + +class BinOp(expr): + _fields = ('left', 'op', 'right') + __doc__ = None + +class BitAnd(operator): + _fields = None + __doc__ = None + +class BitOr(operator): + _fields = None + __doc__ = None + +class BitXor(operator): + _fields = None + __doc__ = None + +class BoolOp(expr): + _fields = ('op', 'values') + __doc__ = None + +class Break(stmt): + _fields = None + __doc__ = None + +class Call(expr): + _fields = ('func', 'args', 'keywords', 'starargs', 'kwargs') + __doc__ = None + +class ClassDef(stmt): + _fields = ('name', 'bases', 'body') + __doc__ = None + +class Compare(expr): + _fields = ('left', 'ops', 'comparators') + __doc__ = None + +class Continue(stmt): + _fields = None + __doc__ = None + +class Del(expr_context): + _fields = None + __doc__ = None + +class Delete(stmt): + _fields = ('targets',) + __doc__ = None + +class Dict(expr): + _fields = ('keys', 'values') + __doc__ = None + +class Div(operator): + _fields = None + __doc__ = None + +class slice(AST): + _fields = None + __doc__ = None + _attributes = [] +class Ellipsis(slice): + _fields = None + __doc__ = None + +class cmpop(AST): + _fields = None + __doc__ = None + _attributes = [] +class Eq(cmpop): + _fields = None + __doc__ = None + +class Exec(stmt): + _fields = ('body', 'globals', 'locals') + __doc__ = None + +class Expr(stmt): + _fields = ('value',) + __doc__ = None + +class mod(AST): + _fields = None + __doc__ = None + _attributes = [] +class Expression(mod): + _fields = ('body',) + __doc__ = None + +class ExtSlice(slice): + _fields = ('dims',) + __doc__ = None + +class FloorDiv(operator): + _fields = None + __doc__ = None + +class For(stmt): + _fields = ('target', 'iter', 'body', 'orelse') + __doc__ = None + +class FunctionDef(stmt): + _fields = ('name', 'args', 'body', 'decorators') + __doc__ = None + +class GeneratorExp(expr): + _fields = ('elt', 'generators') + __doc__ = None + +class Global(stmt): + _fields = ('names',) + __doc__ = None + +class Gt(cmpop): + _fields = None + __doc__ = None + +class GtE(cmpop): + _fields = None + __doc__ = None + +class If(stmt): + _fields = ('test', 'body', 'orelse') + __doc__ = None + +class IfExp(expr): + _fields = ('test', 'body', 'orelse') + __doc__ = None + +class Import(stmt): + _fields = ('names',) + __doc__ = None + +class ImportFrom(stmt): + _fields = ('module', 'names', 'level') + __doc__ = None + +class In(cmpop): + _fields = None + __doc__ = None + +class Index(slice): + _fields = ('value',) + __doc__ = None + +class Interactive(mod): + _fields = ('body',) + __doc__ = None + +class unaryop(AST): + _fields = None + __doc__ = None + _attributes = [] +class Invert(unaryop): + _fields = None + __doc__ = None + +class Is(cmpop): + _fields = None + __doc__ = None + +class IsNot(cmpop): + _fields = None + __doc__ = None + +class LShift(operator): + _fields = None + __doc__ = None + +class Lambda(expr): + _fields = ('args', 'body') + __doc__ = None + +class List(expr): + _fields = ('elts', 'ctx') + __doc__ = None + +class ListComp(expr): + _fields = ('elt', 'generators') + __doc__ = None + +class Load(expr_context): + _fields = None + __doc__ = None + +class Lt(cmpop): + _fields = None + __doc__ = None + +class LtE(cmpop): + _fields = None + __doc__ = None + +class Mod(operator): + _fields = None + __doc__ = None + +class Module(mod): + _fields = ('body',) + __doc__ = None + +class Mult(operator): + _fields = None + __doc__ = None + +class Name(expr): + _fields = ('id', 'ctx') + __doc__ = None + +class Not(unaryop): + _fields = None + __doc__ = None + +class NotEq(cmpop): + _fields = None + __doc__ = None + +class NotIn(cmpop): + _fields = None + __doc__ = None + +class Num(expr): + _fields = ('n',) + __doc__ = None + +class Or(boolop): + _fields = None + __doc__ = None + +class Param(expr_context): + _fields = None + __doc__ = None + +class Pass(stmt): + _fields = None + __doc__ = None + +class Pow(operator): + _fields = None + __doc__ = None + +class Print(stmt): + _fields = ('dest', 'values', 'nl') + __doc__ = None + +class RShift(operator): + _fields = None + __doc__ = None + +class Raise(stmt): + _fields = ('type', 'inst', 'tback') + __doc__ = None + +class Repr(expr): + _fields = ('value',) + __doc__ = None + +class Return(stmt): + _fields = ('value',) + __doc__ = None + +class Slice(slice): + _fields = ('lower', 'upper', 'step') + __doc__ = None + +class Store(expr_context): + _fields = None + __doc__ = None + +class Str(expr): + _fields = ('s',) + __doc__ = None + +class Sub(operator): + _fields = None + __doc__ = None + +class Subscript(expr): + _fields = ('value', 'slice', 'ctx') + __doc__ = None + +class Suite(mod): + _fields = ('body',) + __doc__ = None + +class TryExcept(stmt): + _fields = ('body', 'handlers', 'orelse') + __doc__ = None + +class TryFinally(stmt): + _fields = ('body', 'finalbody') + __doc__ = None + +class Tuple(expr): + _fields = ('elts', 'ctx') + __doc__ = None + +class UAdd(unaryop): + _fields = None + __doc__ = None + +class USub(unaryop): + _fields = None + __doc__ = None + +class UnaryOp(expr): + _fields = ('op', 'operand') + __doc__ = None + +class While(stmt): + _fields = ('test', 'body', 'orelse') + __doc__ = None + +class With(stmt): + _fields = ('context_expr', 'optional_vars', 'body') + __doc__ = None + +class Yield(expr): + _fields = ('value',) + __doc__ = None + +class alias(AST): + _fields = ('name', 'asname') + __doc__ = None + +class arguments(AST): + _fields = ('args', 'vararg', 'kwarg', 'defaults') + __doc__ = None + +class boolop(AST): + _fields = None + __doc__ = None + _attributes = [] + +class cmpop(AST): + _fields = None + __doc__ = None + _attributes = [] + +class comprehension(AST): + _fields = ('target', 'iter', 'ifs') + __doc__ = None + +class excepthandler(AST): + _fields = ('type', 'name', 'body', 'lineno', 'col_offset') + __doc__ = None + +class expr(AST): + _fields = None + __doc__ = None + _attributes = ['lineno', 'col_offset'] + +class expr_context(AST): + _fields = None + __doc__ = None + _attributes = [] + +class keyword(AST): + _fields = ('arg', 'value') + __doc__ = None + +class mod(AST): + _fields = None + __doc__ = None + _attributes = [] + +class operator(AST): + _fields = None + __doc__ = None + _attributes = [] + +class slice(AST): + _fields = None + __doc__ = None + _attributes = [] + +class stmt(AST): + _fields = None + __doc__ = None + _attributes = ['lineno', 'col_offset'] + +class unaryop(AST): + _fields = None + __doc__ = None + _attributes = [] + diff --git a/websdk/genshi/template/ast24.py b/websdk/genshi/template/ast24.py new file mode 100644 index 0000000..af6dce9 --- /dev/null +++ b/websdk/genshi/template/ast24.py @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Emulation of the proper abstract syntax tree API for Python 2.4.""" + +import compiler +import compiler.ast + +from genshi.template import _ast24 as _ast + +__all__ = ['_ast', 'parse'] +__docformat__ = 'restructuredtext en' + + +def _new(cls, *args, **kwargs): + ret = cls() + if ret._fields: + for attr, value in zip(ret._fields, args): + if attr in kwargs: + raise ValueError('Field set both in args and kwargs') + setattr(ret, attr, value) + for attr in kwargs: + if (getattr(ret, '_fields', None) and attr in ret._fields) \ + or (getattr(ret, '_attributes', None) and + attr in ret._attributes): + setattr(ret, attr, kwargs[attr]) + return ret + + +class ASTUpgrader(object): + """Transformer changing structure of Python 2.4 ASTs to + Python 2.5 ones. + + Transforms ``compiler.ast`` Abstract Syntax Tree to builtin ``_ast``. + It can use fake`` _ast`` classes and this way allow ``_ast`` emulation + in Python 2.4. + """ + + def __init__(self): + self.out_flags = None + self.lines = [-1] + + def _new(self, *args, **kwargs): + return _new(lineno = self.lines[-1], *args, **kwargs) + + def visit(self, node): + if node is None: + return None + if type(node) is tuple: + return tuple([self.visit(n) for n in node]) + lno = getattr(node, 'lineno', None) + if lno is not None: + self.lines.append(lno) + visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None) + if visitor is None: + raise Exception('Unhandled node type %r' % type(node)) + + retval = visitor(node) + if lno is not None: + self.lines.pop() + return retval + + def visit_Module(self, node): + body = self.visit(node.node) + if node.doc: + body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body + return self._new(_ast.Module, body) + + def visit_Expression(self, node): + return self._new(_ast.Expression, self.visit(node.node)) + + def _extract_args(self, node): + tab = node.argnames[:] + if node.flags & compiler.ast.CO_VARKEYWORDS: + kwarg = tab[-1] + tab = tab[:-1] + else: + kwarg = None + + if node.flags & compiler.ast.CO_VARARGS: + vararg = tab[-1] + tab = tab[:-1] + else: + vararg = None + + def _tup(t): + if isinstance(t, str): + return self._new(_ast.Name, t, _ast.Store()) + elif isinstance(t, tuple): + elts = [_tup(x) for x in t] + return self._new(_ast.Tuple, elts, _ast.Store()) + else: + raise NotImplemented + + args = [] + for arg in tab: + if isinstance(arg, str): + args.append(self._new(_ast.Name, arg, _ast.Param())) + elif isinstance(arg, tuple): + args.append(_tup(arg)) + else: + assert False, node.__class__ + + defaults = [self.visit(d) for d in node.defaults] + return self._new(_ast.arguments, args, vararg, kwarg, defaults) + + + def visit_Function(self, node): + if getattr(node, 'decorators', ()): + decorators = [self.visit(d) for d in node.decorators.nodes] + else: + decorators = [] + + args = self._extract_args(node) + body = self.visit(node.code) + if node.doc: + body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body + return self._new(_ast.FunctionDef, node.name, args, body, decorators) + + def visit_Class(self, node): + #self.name_types.append(_ast.Load) + bases = [self.visit(b) for b in node.bases] + #self.name_types.pop() + body = self.visit(node.code) + if node.doc: + body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body + return self._new(_ast.ClassDef, node.name, bases, body) + + def visit_Return(self, node): + return self._new(_ast.Return, self.visit(node.value)) + + def visit_Assign(self, node): + #self.name_types.append(_ast.Store) + targets = [self.visit(t) for t in node.nodes] + #self.name_types.pop() + return self._new(_ast.Assign, targets, self.visit(node.expr)) + + aug_operators = { + '+=': _ast.Add, + '/=': _ast.Div, + '//=': _ast.FloorDiv, + '<<=': _ast.LShift, + '%=': _ast.Mod, + '*=': _ast.Mult, + '**=': _ast.Pow, + '>>=': _ast.RShift, + '-=': _ast.Sub, + } + + def visit_AugAssign(self, node): + target = self.visit(node.node) + + # Because it's AugAssign target can't be list nor tuple + # so we only have to change context of one node + target.ctx = _ast.Store() + op = self.aug_operators[node.op]() + return self._new(_ast.AugAssign, target, op, self.visit(node.expr)) + + def _visit_Print(nl): + def _visit(self, node): + values = [self.visit(v) for v in node.nodes] + return self._new(_ast.Print, self.visit(node.dest), values, nl) + return _visit + + visit_Print = _visit_Print(False) + visit_Printnl = _visit_Print(True) + del _visit_Print + + def visit_For(self, node): + return self._new(_ast.For, self.visit(node.assign), self.visit(node.list), + self.visit(node.body), self.visit(node.else_)) + + def visit_While(self, node): + return self._new(_ast.While, self.visit(node.test), self.visit(node.body), + self.visit(node.else_)) + + def visit_If(self, node): + def _level(tests, else_): + test = self.visit(tests[0][0]) + body = self.visit(tests[0][1]) + if len(tests) == 1: + orelse = self.visit(else_) + else: + orelse = [_level(tests[1:], else_)] + return self._new(_ast.If, test, body, orelse) + return _level(node.tests, node.else_) + + def visit_With(self, node): + return self._new(_ast.With, self.visit(node.expr), + self.visit(node.vars), self.visit(node.body)) + + def visit_Raise(self, node): + return self._new(_ast.Raise, self.visit(node.expr1), + self.visit(node.expr2), self.visit(node.expr3)) + + def visit_TryExcept(self, node): + handlers = [] + for type, name, body in node.handlers: + handlers.append(self._new(_ast.excepthandler, self.visit(type), + self.visit(name), self.visit(body))) + return self._new(_ast.TryExcept, self.visit(node.body), + handlers, self.visit(node.else_)) + + def visit_TryFinally(self, node): + return self._new(_ast.TryFinally, self.visit(node.body), + self.visit(node.final)) + + def visit_Assert(self, node): + return self._new(_ast.Assert, self.visit(node.test), self.visit(node.fail)) + + def visit_Import(self, node): + names = [self._new(_ast.alias, n[0], n[1]) for n in node.names] + return self._new(_ast.Import, names) + + def visit_From(self, node): + names = [self._new(_ast.alias, n[0], n[1]) for n in node.names] + return self._new(_ast.ImportFrom, node.modname, names, 0) + + def visit_Exec(self, node): + return self._new(_ast.Exec, self.visit(node.expr), + self.visit(node.locals), self.visit(node.globals)) + + def visit_Global(self, node): + return self._new(_ast.Global, node.names[:]) + + def visit_Discard(self, node): + return self._new(_ast.Expr, self.visit(node.expr)) + + def _map_class(to): + def _visit(self, node): + return self._new(to) + return _visit + + visit_Pass = _map_class(_ast.Pass) + visit_Break = _map_class(_ast.Break) + visit_Continue = _map_class(_ast.Continue) + + def _visit_BinOperator(opcls): + def _visit(self, node): + return self._new(_ast.BinOp, self.visit(node.left), + opcls(), self.visit(node.right)) + return _visit + visit_Add = _visit_BinOperator(_ast.Add) + visit_Div = _visit_BinOperator(_ast.Div) + visit_FloorDiv = _visit_BinOperator(_ast.FloorDiv) + visit_LeftShift = _visit_BinOperator(_ast.LShift) + visit_Mod = _visit_BinOperator(_ast.Mod) + visit_Mul = _visit_BinOperator(_ast.Mult) + visit_Power = _visit_BinOperator(_ast.Pow) + visit_RightShift = _visit_BinOperator(_ast.RShift) + visit_Sub = _visit_BinOperator(_ast.Sub) + del _visit_BinOperator + + def _visit_BitOperator(opcls): + def _visit(self, node): + def _make(nodes): + if len(nodes) == 1: + return self.visit(nodes[0]) + left = _make(nodes[:-1]) + right = self.visit(nodes[-1]) + return self._new(_ast.BinOp, left, opcls(), right) + return _make(node.nodes) + return _visit + visit_Bitand = _visit_BitOperator(_ast.BitAnd) + visit_Bitor = _visit_BitOperator(_ast.BitOr) + visit_Bitxor = _visit_BitOperator(_ast.BitXor) + del _visit_BitOperator + + def _visit_UnaryOperator(opcls): + def _visit(self, node): + return self._new(_ast.UnaryOp, opcls(), self.visit(node.expr)) + return _visit + + visit_Invert = _visit_UnaryOperator(_ast.Invert) + visit_Not = _visit_UnaryOperator(_ast.Not) + visit_UnaryAdd = _visit_UnaryOperator(_ast.UAdd) + visit_UnarySub = _visit_UnaryOperator(_ast.USub) + del _visit_UnaryOperator + + def _visit_BoolOperator(opcls): + def _visit(self, node): + values = [self.visit(n) for n in node.nodes] + return self._new(_ast.BoolOp, opcls(), values) + return _visit + visit_And = _visit_BoolOperator(_ast.And) + visit_Or = _visit_BoolOperator(_ast.Or) + del _visit_BoolOperator + + cmp_operators = { + '==': _ast.Eq, + '!=': _ast.NotEq, + '<': _ast.Lt, + '<=': _ast.LtE, + '>': _ast.Gt, + '>=': _ast.GtE, + 'is': _ast.Is, + 'is not': _ast.IsNot, + 'in': _ast.In, + 'not in': _ast.NotIn, + } + + def visit_Compare(self, node): + left = self.visit(node.expr) + ops = [] + comparators = [] + for optype, expr in node.ops: + ops.append(self.cmp_operators[optype]()) + comparators.append(self.visit(expr)) + return self._new(_ast.Compare, left, ops, comparators) + + def visit_Lambda(self, node): + args = self._extract_args(node) + body = self.visit(node.code) + return self._new(_ast.Lambda, args, body) + + def visit_IfExp(self, node): + return self._new(_ast.IfExp, self.visit(node.test), self.visit(node.then), + self.visit(node.else_)) + + def visit_Dict(self, node): + keys = [self.visit(x[0]) for x in node.items] + values = [self.visit(x[1]) for x in node.items] + return self._new(_ast.Dict, keys, values) + + def visit_ListComp(self, node): + generators = [self.visit(q) for q in node.quals] + return self._new(_ast.ListComp, self.visit(node.expr), generators) + + def visit_GenExprInner(self, node): + generators = [self.visit(q) for q in node.quals] + return self._new(_ast.GeneratorExp, self.visit(node.expr), generators) + + def visit_GenExpr(self, node): + return self.visit(node.code) + + def visit_GenExprFor(self, node): + ifs = [self.visit(i) for i in node.ifs] + return self._new(_ast.comprehension, self.visit(node.assign), + self.visit(node.iter), ifs) + + def visit_ListCompFor(self, node): + ifs = [self.visit(i) for i in node.ifs] + return self._new(_ast.comprehension, self.visit(node.assign), + self.visit(node.list), ifs) + + def visit_GenExprIf(self, node): + return self.visit(node.test) + visit_ListCompIf = visit_GenExprIf + + def visit_Yield(self, node): + return self._new(_ast.Yield, self.visit(node.value)) + + def visit_CallFunc(self, node): + args = [] + keywords = [] + for arg in node.args: + if isinstance(arg, compiler.ast.Keyword): + keywords.append(self._new(_ast.keyword, arg.name, + self.visit(arg.expr))) + else: + args.append(self.visit(arg)) + return self._new(_ast.Call, self.visit(node.node), args, keywords, + self.visit(node.star_args), self.visit(node.dstar_args)) + + def visit_Backquote(self, node): + return self._new(_ast.Repr, self.visit(node.expr)) + + def visit_Const(self, node): + if node.value is None: # appears in slices + return None + elif isinstance(node.value, basestring): + return self._new(_ast.Str, node.value) + else: + return self._new(_ast.Num, node.value) + + def visit_Name(self, node): + return self._new(_ast.Name, node.name, _ast.Load()) + + def visit_Getattr(self, node): + return self._new(_ast.Attribute, self.visit(node.expr), node.attrname, + _ast.Load()) + + def visit_Tuple(self, node): + nodes = [self.visit(n) for n in node.nodes] + return self._new(_ast.Tuple, nodes, _ast.Load()) + + def visit_List(self, node): + nodes = [self.visit(n) for n in node.nodes] + return self._new(_ast.List, nodes, _ast.Load()) + + def get_ctx(self, flags): + if flags == 'OP_DELETE': + return _ast.Del() + elif flags == 'OP_APPLY': + return _ast.Load() + elif flags == 'OP_ASSIGN': + return _ast.Store() + else: + # FIXME Exception here + assert False, repr(flags) + + def visit_AssName(self, node): + self.out_flags = node.flags + ctx = self.get_ctx(node.flags) + return self._new(_ast.Name, node.name, ctx) + + def visit_AssAttr(self, node): + self.out_flags = node.flags + ctx = self.get_ctx(node.flags) + return self._new(_ast.Attribute, self.visit(node.expr), + node.attrname, ctx) + + def _visit_AssCollection(cls): + def _visit(self, node): + flags = None + elts = [] + for n in node.nodes: + elts.append(self.visit(n)) + if flags is None: + flags = self.out_flags + else: + assert flags == self.out_flags + self.out_flags = flags + ctx = self.get_ctx(flags) + return self._new(cls, elts, ctx) + return _visit + + visit_AssList = _visit_AssCollection(_ast.List) + visit_AssTuple = _visit_AssCollection(_ast.Tuple) + del _visit_AssCollection + + def visit_Slice(self, node): + lower = self.visit(node.lower) + upper = self.visit(node.upper) + ctx = self.get_ctx(node.flags) + self.out_flags = node.flags + return self._new(_ast.Subscript, self.visit(node.expr), + self._new(_ast.Slice, lower, upper, None), ctx) + + def visit_Subscript(self, node): + ctx = self.get_ctx(node.flags) + subs = [self.visit(s) for s in node.subs] + + advanced = (_ast.Slice, _ast.Ellipsis) + slices = [] + nonindex = False + for sub in subs: + if isinstance(sub, advanced): + nonindex = True + slices.append(sub) + else: + slices.append(self._new(_ast.Index, sub)) + if len(slices) == 1: + slice = slices[0] + elif nonindex: + slice = self._new(_ast.ExtSlice, slices) + else: + slice = self._new(_ast.Tuple, slices, _ast.Load()) + + self.out_flags = node.flags + return self._new(_ast.Subscript, self.visit(node.expr), slice, ctx) + + def visit_Sliceobj(self, node): + a = [self.visit(n) for n in node.nodes + [None]*(3 - len(node.nodes))] + return self._new(_ast.Slice, a[0], a[1], a[2]) + + def visit_Ellipsis(self, node): + return self._new(_ast.Ellipsis) + + def visit_Stmt(self, node): + def _check_del(n): + # del x is just AssName('x', 'OP_DELETE') + # we want to transform it to Delete([Name('x', Del())]) + dcls = (_ast.Name, _ast.List, _ast.Subscript, _ast.Attribute) + if isinstance(n, dcls) and isinstance(n.ctx, _ast.Del): + return self._new(_ast.Delete, [n]) + elif isinstance(n, _ast.Tuple) and isinstance(n.ctx, _ast.Del): + # unpack last tuple to avoid making del (x, y, z,); + # out of del x, y, z; (there's no difference between + # this two in compiler.ast) + return self._new(_ast.Delete, n.elts) + else: + return n + def _keep(n): + if isinstance(n, _ast.Expr) and n.value is None: + return False + else: + return True + return [s for s in [_check_del(self.visit(n)) for n in node.nodes] + if _keep(s)] + + +def parse(source, mode): + node = compiler.parse(source, mode) + return ASTUpgrader().visit(node) diff --git a/websdk/genshi/template/astutil.py b/websdk/genshi/template/astutil.py new file mode 100644 index 0000000..c3ad107 --- /dev/null +++ b/websdk/genshi/template/astutil.py @@ -0,0 +1,784 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Support classes for generating code from abstract syntax trees.""" + +try: + import _ast +except ImportError: + from genshi.template.ast24 import _ast, parse +else: + def parse(source, mode): + return compile(source, '', mode, _ast.PyCF_ONLY_AST) + + +__docformat__ = 'restructuredtext en' + + +class ASTCodeGenerator(object): + """General purpose base class for AST transformations. + + Every visitor method can be overridden to return an AST node that has been + altered or replaced in some way. + """ + def __init__(self, tree): + self.lines_info = [] + self.line_info = None + self.code = '' + self.line = None + self.last = None + self.indent = 0 + self.blame_stack = [] + self.visit(tree) + if self.line.strip(): + self.code += self.line + '\n' + self.lines_info.append(self.line_info) + self.line = None + self.line_info = None + + def _change_indent(self, delta): + self.indent += delta + + def _new_line(self): + if self.line is not None: + self.code += self.line + '\n' + self.lines_info.append(self.line_info) + self.line = ' '*4*self.indent + if len(self.blame_stack) == 0: + self.line_info = [] + self.last = None + else: + self.line_info = [(0, self.blame_stack[-1],)] + self.last = self.blame_stack[-1] + + def _write(self, s): + if len(s) == 0: + return + if len(self.blame_stack) == 0: + if self.last is not None: + self.last = None + self.line_info.append((len(self.line), self.last)) + else: + if self.last != self.blame_stack[-1]: + self.last = self.blame_stack[-1] + self.line_info.append((len(self.line), self.last)) + self.line += s + + def visit(self, node): + if node is None: + return None + if type(node) is tuple: + return tuple([self.visit(n) for n in node]) + try: + self.blame_stack.append((node.lineno, node.col_offset,)) + info = True + except AttributeError: + info = False + visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None) + if visitor is None: + raise Exception('Unhandled node type %r' % type(node)) + ret = visitor(node) + if info: + self.blame_stack.pop() + return ret + + def visit_Module(self, node): + for n in node.body: + self.visit(n) + visit_Interactive = visit_Module + visit_Suite = visit_Module + + def visit_Expression(self, node): + self._new_line() + return self.visit(node.body) + + # arguments = (expr* args, identifier? vararg, + # identifier? kwarg, expr* defaults) + def visit_arguments(self, node): + first = True + no_default_count = len(node.args) - len(node.defaults) + for i, arg in enumerate(node.args): + if not first: + self._write(', ') + else: + first = False + self.visit(arg) + if i >= no_default_count: + self._write('=') + self.visit(node.defaults[i - no_default_count]) + if getattr(node, 'vararg', None): + if not first: + self._write(', ') + else: + first = False + self._write('*' + node.vararg) + if getattr(node, 'kwarg', None): + if not first: + self._write(', ') + else: + first = False + self._write('**' + node.kwarg) + + # FunctionDef(identifier name, arguments args, + # stmt* body, expr* decorator_list) + def visit_FunctionDef(self, node): + decarators = () + if hasattr(node, 'decorator_list'): + decorators = getattr(node, 'decorator_list') + else: # different name in earlier Python versions + decorators = getattr(node, 'decorators', ()) + for decorator in decorators: + self._new_line() + self._write('@') + self.visit(decorator) + self._new_line() + self._write('def ' + node.name + '(') + self.visit(node.args) + self._write('):') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + # ClassDef(identifier name, expr* bases, stmt* body) + def visit_ClassDef(self, node): + self._new_line() + self._write('class ' + node.name) + if node.bases: + self._write('(') + self.visit(node.bases[0]) + for base in node.bases[1:]: + self._write(', ') + self.visit(base) + self._write(')') + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + # Return(expr? value) + def visit_Return(self, node): + self._new_line() + self._write('return') + if getattr(node, 'value', None): + self._write(' ') + self.visit(node.value) + + # Delete(expr* targets) + def visit_Delete(self, node): + self._new_line() + self._write('del ') + self.visit(node.targets[0]) + for target in node.targets[1:]: + self._write(', ') + self.visit(target) + + # Assign(expr* targets, expr value) + def visit_Assign(self, node): + self._new_line() + for target in node.targets: + self.visit(target) + self._write(' = ') + self.visit(node.value) + + # AugAssign(expr target, operator op, expr value) + def visit_AugAssign(self, node): + self._new_line() + self.visit(node.target) + self._write(' ' + self.binary_operators[node.op.__class__] + '= ') + self.visit(node.value) + + # Print(expr? dest, expr* values, bool nl) + def visit_Print(self, node): + self._new_line() + self._write('print') + if getattr(node, 'dest', None): + self._write(' >> ') + self.visit(node.dest) + if getattr(node, 'values', None): + self._write(', ') + else: + self._write(' ') + if getattr(node, 'values', None): + self.visit(node.values[0]) + for value in node.values[1:]: + self._write(', ') + self.visit(value) + if not node.nl: + self._write(',') + + # For(expr target, expr iter, stmt* body, stmt* orelse) + def visit_For(self, node): + self._new_line() + self._write('for ') + self.visit(node.target) + self._write(' in ') + self.visit(node.iter) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # While(expr test, stmt* body, stmt* orelse) + def visit_While(self, node): + self._new_line() + self._write('while ') + self.visit(node.test) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # If(expr test, stmt* body, stmt* orelse) + def visit_If(self, node): + self._new_line() + self._write('if ') + self.visit(node.test) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # With(expr context_expr, expr? optional_vars, stmt* body) + def visit_With(self, node): + self._new_line() + self._write('with ') + self.visit(node.context_expr) + if getattr(node, 'optional_vars', None): + self._write(' as ') + self.visit(node.optional_vars) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + + # Raise(expr? type, expr? inst, expr? tback) + def visit_Raise(self, node): + self._new_line() + self._write('raise') + if not node.type: + return + self._write(' ') + self.visit(node.type) + if not node.inst: + return + self._write(', ') + self.visit(node.inst) + if not node.tback: + return + self._write(', ') + self.visit(node.tback) + + # TryExcept(stmt* body, excepthandler* handlers, stmt* orelse) + def visit_TryExcept(self, node): + self._new_line() + self._write('try:') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'handlers', None): + for handler in node.handlers: + self.visit(handler) + self._new_line() + if getattr(node, 'orelse', None): + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # excepthandler = (expr? type, expr? name, stmt* body) + def visit_ExceptHandler(self, node): + self._new_line() + self._write('except') + if getattr(node, 'type', None): + self._write(' ') + self.visit(node.type) + if getattr(node, 'name', None): + self._write(', ') + self.visit(node.name) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + visit_excepthandler = visit_ExceptHandler + + # TryFinally(stmt* body, stmt* finalbody) + def visit_TryFinally(self, node): + self._new_line() + self._write('try:') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + if getattr(node, 'finalbody', None): + self._new_line() + self._write('finally:') + self._change_indent(1) + for statement in node.finalbody: + self.visit(statement) + self._change_indent(-1) + + # Assert(expr test, expr? msg) + def visit_Assert(self, node): + self._new_line() + self._write('assert ') + self.visit(node.test) + if getattr(node, 'msg', None): + self._write(', ') + self.visit(node.msg) + + def visit_alias(self, node): + self._write(node.name) + if getattr(node, 'asname', None): + self._write(' as ') + self._write(node.asname) + + # Import(alias* names) + def visit_Import(self, node): + self._new_line() + self._write('import ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # ImportFrom(identifier module, alias* names, int? level) + def visit_ImportFrom(self, node): + self._new_line() + self._write('from ') + if node.level: + self._write('.' * node.level) + self._write(node.module) + self._write(' import ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # Exec(expr body, expr? globals, expr? locals) + def visit_Exec(self, node): + self._new_line() + self._write('exec ') + self.visit(node.body) + if not node.globals: + return + self._write(', ') + self.visit(node.globals) + if not node.locals: + return + self._write(', ') + self.visit(node.locals) + + # Global(identifier* names) + def visit_Global(self, node): + self._new_line() + self._write('global ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # Expr(expr value) + def visit_Expr(self, node): + self._new_line() + self.visit(node.value) + + # Pass + def visit_Pass(self, node): + self._new_line() + self._write('pass') + + # Break + def visit_Break(self, node): + self._new_line() + self._write('break') + + # Continue + def visit_Continue(self, node): + self._new_line() + self._write('continue') + + ### EXPRESSIONS + def with_parens(f): + def _f(self, node): + self._write('(') + f(self, node) + self._write(')') + return _f + + bool_operators = {_ast.And: 'and', _ast.Or: 'or'} + + # BoolOp(boolop op, expr* values) + @with_parens + def visit_BoolOp(self, node): + joiner = ' ' + self.bool_operators[node.op.__class__] + ' ' + self.visit(node.values[0]) + for value in node.values[1:]: + self._write(joiner) + self.visit(value) + + binary_operators = { + _ast.Add: '+', + _ast.Sub: '-', + _ast.Mult: '*', + _ast.Div: '/', + _ast.Mod: '%', + _ast.Pow: '**', + _ast.LShift: '<<', + _ast.RShift: '>>', + _ast.BitOr: '|', + _ast.BitXor: '^', + _ast.BitAnd: '&', + _ast.FloorDiv: '//' + } + + # BinOp(expr left, operator op, expr right) + @with_parens + def visit_BinOp(self, node): + self.visit(node.left) + self._write(' ' + self.binary_operators[node.op.__class__] + ' ') + self.visit(node.right) + + unary_operators = { + _ast.Invert: '~', + _ast.Not: 'not', + _ast.UAdd: '+', + _ast.USub: '-', + } + + # UnaryOp(unaryop op, expr operand) + def visit_UnaryOp(self, node): + self._write(self.unary_operators[node.op.__class__] + ' ') + self.visit(node.operand) + + # Lambda(arguments args, expr body) + @with_parens + def visit_Lambda(self, node): + self._write('lambda ') + self.visit(node.args) + self._write(': ') + self.visit(node.body) + + # IfExp(expr test, expr body, expr orelse) + @with_parens + def visit_IfExp(self, node): + self.visit(node.body) + self._write(' if ') + self.visit(node.test) + self._write(' else ') + self.visit(node.orelse) + + # Dict(expr* keys, expr* values) + def visit_Dict(self, node): + self._write('{') + for key, value in zip(node.keys, node.values): + self.visit(key) + self._write(': ') + self.visit(value) + self._write(', ') + self._write('}') + + # ListComp(expr elt, comprehension* generators) + def visit_ListComp(self, node): + self._write('[') + self.visit(node.elt) + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self._write(' for ') + self.visit(generator.target) + self._write(' in ') + self.visit(generator.iter) + for ifexpr in generator.ifs: + self._write(' if ') + self.visit(ifexpr) + self._write(']') + + # GeneratorExp(expr elt, comprehension* generators) + def visit_GeneratorExp(self, node): + self._write('(') + self.visit(node.elt) + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self._write(' for ') + self.visit(generator.target) + self._write(' in ') + self.visit(generator.iter) + for ifexpr in generator.ifs: + self._write(' if ') + self.visit(ifexpr) + self._write(')') + + # Yield(expr? value) + def visit_Yield(self, node): + self._write('yield') + if getattr(node, 'value', None): + self._write(' ') + self.visit(node.value) + + comparision_operators = { + _ast.Eq: '==', + _ast.NotEq: '!=', + _ast.Lt: '<', + _ast.LtE: '<=', + _ast.Gt: '>', + _ast.GtE: '>=', + _ast.Is: 'is', + _ast.IsNot: 'is not', + _ast.In: 'in', + _ast.NotIn: 'not in', + } + + # Compare(expr left, cmpop* ops, expr* comparators) + @with_parens + def visit_Compare(self, node): + self.visit(node.left) + for op, comparator in zip(node.ops, node.comparators): + self._write(' ' + self.comparision_operators[op.__class__] + ' ') + self.visit(comparator) + + # Call(expr func, expr* args, keyword* keywords, + # expr? starargs, expr? kwargs) + def visit_Call(self, node): + self.visit(node.func) + self._write('(') + first = True + for arg in node.args: + if not first: + self._write(', ') + first = False + self.visit(arg) + + for keyword in node.keywords: + if not first: + self._write(', ') + first = False + # keyword = (identifier arg, expr value) + self._write(keyword.arg) + self._write('=') + self.visit(keyword.value) + if getattr(node, 'starargs', None): + if not first: + self._write(', ') + first = False + self._write('*') + self.visit(node.starargs) + + if getattr(node, 'kwargs', None): + if not first: + self._write(', ') + first = False + self._write('**') + self.visit(node.kwargs) + self._write(')') + + # Repr(expr value) + def visit_Repr(self, node): + self._write('`') + self.visit(node.value) + self._write('`') + + # Num(object n) + def visit_Num(self, node): + self._write(repr(node.n)) + + # Str(string s) + def visit_Str(self, node): + self._write(repr(node.s)) + + # Attribute(expr value, identifier attr, expr_context ctx) + def visit_Attribute(self, node): + self.visit(node.value) + self._write('.') + self._write(node.attr) + + # Subscript(expr value, slice slice, expr_context ctx) + def visit_Subscript(self, node): + self.visit(node.value) + self._write('[') + def _process_slice(node): + if isinstance(node, _ast.Ellipsis): + self._write('...') + elif isinstance(node, _ast.Slice): + if getattr(node, 'lower', 'None'): + self.visit(node.lower) + self._write(':') + if getattr(node, 'upper', None): + self.visit(node.upper) + if getattr(node, 'step', None): + self._write(':') + self.visit(node.step) + elif isinstance(node, _ast.Index): + self.visit(node.value) + elif isinstance(node, _ast.ExtSlice): + self.visit(node.dims[0]) + for dim in node.dims[1:]: + self._write(', ') + self.visit(dim) + else: + raise NotImplemented('Slice type not implemented') + _process_slice(node.slice) + self._write(']') + + # Name(identifier id, expr_context ctx) + def visit_Name(self, node): + self._write(node.id) + + # List(expr* elts, expr_context ctx) + def visit_List(self, node): + self._write('[') + for elt in node.elts: + self.visit(elt) + self._write(', ') + self._write(']') + + # Tuple(expr *elts, expr_context ctx) + def visit_Tuple(self, node): + self._write('(') + for elt in node.elts: + self.visit(elt) + self._write(', ') + self._write(')') + + +class ASTTransformer(object): + """General purpose base class for AST transformations. + + Every visitor method can be overridden to return an AST node that has been + altered or replaced in some way. + """ + + def visit(self, node): + if node is None: + return None + if type(node) is tuple: + return tuple([self.visit(n) for n in node]) + visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None) + if visitor is None: + return node + return visitor(node) + + def _clone(self, node): + clone = node.__class__() + for name in getattr(clone, '_attributes', ()): + try: + setattr(clone, 'name', getattr(node, name)) + except AttributeError: + pass + for name in clone._fields: + try: + value = getattr(node, name) + except AttributeError: + pass + else: + if value is None: + pass + elif isinstance(value, list): + value = [self.visit(x) for x in value] + elif isinstance(value, tuple): + value = tuple(self.visit(x) for x in value) + else: + value = self.visit(value) + setattr(clone, name, value) + return clone + + visit_Module = _clone + visit_Interactive = _clone + visit_Expression = _clone + visit_Suite = _clone + + visit_FunctionDef = _clone + visit_ClassDef = _clone + visit_Return = _clone + visit_Delete = _clone + visit_Assign = _clone + visit_AugAssign = _clone + visit_Print = _clone + visit_For = _clone + visit_While = _clone + visit_If = _clone + visit_With = _clone + visit_Raise = _clone + visit_TryExcept = _clone + visit_TryFinally = _clone + visit_Assert = _clone + visit_ExceptHandler = _clone + + visit_Import = _clone + visit_ImportFrom = _clone + visit_Exec = _clone + visit_Global = _clone + visit_Expr = _clone + # Pass, Break, Continue don't need to be copied + + visit_BoolOp = _clone + visit_BinOp = _clone + visit_UnaryOp = _clone + visit_Lambda = _clone + visit_IfExp = _clone + visit_Dict = _clone + visit_ListComp = _clone + visit_GeneratorExp = _clone + visit_Yield = _clone + visit_Compare = _clone + visit_Call = _clone + visit_Repr = _clone + # Num, Str don't need to be copied + + visit_Attribute = _clone + visit_Subscript = _clone + visit_Name = _clone + visit_List = _clone + visit_Tuple = _clone + + visit_comprehension = _clone + visit_excepthandler = _clone + visit_arguments = _clone + visit_keyword = _clone + visit_alias = _clone + + visit_Slice = _clone + visit_ExtSlice = _clone + visit_Index = _clone + + del _clone diff --git a/websdk/genshi/template/base.py b/websdk/genshi/template/base.py new file mode 100644 index 0000000..202faae --- /dev/null +++ b/websdk/genshi/template/base.py @@ -0,0 +1,634 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Basic templating functionality.""" + +from collections import deque +import os +from StringIO import StringIO +import sys + +from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure +from genshi.input import ParseError + +__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError', + 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError'] +__docformat__ = 'restructuredtext en' + + +class TemplateError(Exception): + """Base exception class for errors related to template processing.""" + + def __init__(self, message, filename=None, lineno=-1, offset=-1): + """Create the exception. + + :param message: the error message + :param filename: the filename of the template + :param lineno: the number of line in the template at which the error + occurred + :param offset: the column number at which the error occurred + """ + if filename is None: + filename = '<string>' + self.msg = message #: the error message string + if filename != '<string>' or lineno >= 0: + message = '%s (%s, line %d)' % (self.msg, filename, lineno) + Exception.__init__(self, message) + self.filename = filename #: the name of the template file + self.lineno = lineno #: the number of the line containing the error + self.offset = offset #: the offset on the line + + +class TemplateSyntaxError(TemplateError): + """Exception raised when an expression in a template causes a Python syntax + error, or the template is not well-formed. + """ + + def __init__(self, message, filename=None, lineno=-1, offset=-1): + """Create the exception + + :param message: the error message + :param filename: the filename of the template + :param lineno: the number of line in the template at which the error + occurred + :param offset: the column number at which the error occurred + """ + if isinstance(message, SyntaxError) and message.lineno is not None: + message = str(message).replace(' (line %d)' % message.lineno, '') + TemplateError.__init__(self, message, filename, lineno) + + +class BadDirectiveError(TemplateSyntaxError): + """Exception raised when an unknown directive is encountered when parsing + a template. + + An unknown directive is any attribute using the namespace for directives, + with a local name that doesn't match any registered directive. + """ + + def __init__(self, name, filename=None, lineno=-1): + """Create the exception + + :param name: the name of the directive + :param filename: the filename of the template + :param lineno: the number of line in the template at which the error + occurred + """ + TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name, + filename, lineno) + + +class TemplateRuntimeError(TemplateError): + """Exception raised when an the evaluation of a Python expression in a + template causes an error. + """ + + +class Context(object): + """Container for template input data. + + A context provides a stack of scopes (represented by dictionaries). + + Template directives such as loops can push a new scope on the stack with + data that should only be available inside the loop. When the loop + terminates, that scope can get popped off the stack again. + + >>> ctxt = Context(one='foo', other=1) + >>> ctxt.get('one') + 'foo' + >>> ctxt.get('other') + 1 + >>> ctxt.push(dict(one='frost')) + >>> ctxt.get('one') + 'frost' + >>> ctxt.get('other') + 1 + >>> ctxt.pop() + {'one': 'frost'} + >>> ctxt.get('one') + 'foo' + """ + + def __init__(self, **data): + """Initialize the template context with the given keyword arguments as + data. + """ + self.frames = deque([data]) + self.pop = self.frames.popleft + self.push = self.frames.appendleft + self._match_templates = [] + self._choice_stack = [] + + # Helper functions for use in expressions + def defined(name): + """Return whether a variable with the specified name exists in the + expression scope.""" + return name in self + def value_of(name, default=None): + """If a variable of the specified name is defined, return its value. + Otherwise, return the provided default value, or ``None``.""" + return self.get(name, default) + data.setdefault('defined', defined) + data.setdefault('value_of', value_of) + + def __repr__(self): + return repr(list(self.frames)) + + def __contains__(self, key): + """Return whether a variable exists in any of the scopes. + + :param key: the name of the variable + """ + return self._find(key)[1] is not None + has_key = __contains__ + + def __delitem__(self, key): + """Remove a variable from all scopes. + + :param key: the name of the variable + """ + for frame in self.frames: + if key in frame: + del frame[key] + + def __getitem__(self, key): + """Get a variables's value, starting at the current scope and going + upward. + + :param key: the name of the variable + :return: the variable value + :raises KeyError: if the requested variable wasn't found in any scope + """ + value, frame = self._find(key) + if frame is None: + raise KeyError(key) + return value + + def __len__(self): + """Return the number of distinctly named variables in the context. + + :return: the number of variables in the context + """ + return len(self.items()) + + def __setitem__(self, key, value): + """Set a variable in the current scope. + + :param key: the name of the variable + :param value: the variable value + """ + self.frames[0][key] = value + + def _find(self, key, default=None): + """Retrieve a given variable's value and the frame it was found in. + + Intended primarily for internal use by directives. + + :param key: the name of the variable + :param default: the default value to return when the variable is not + found + """ + for frame in self.frames: + if key in frame: + return frame[key], frame + return default, None + + def get(self, key, default=None): + """Get a variable's value, starting at the current scope and going + upward. + + :param key: the name of the variable + :param default: the default value to return when the variable is not + found + """ + for frame in self.frames: + if key in frame: + return frame[key] + return default + + def keys(self): + """Return the name of all variables in the context. + + :return: a list of variable names + """ + keys = [] + for frame in self.frames: + keys += [key for key in frame if key not in keys] + return keys + + def items(self): + """Return a list of ``(name, value)`` tuples for all variables in the + context. + + :return: a list of variables + """ + return [(key, self.get(key)) for key in self.keys()] + + def update(self, mapping): + """Update the context from the mapping provided.""" + self.frames[0].update(mapping) + + def push(self, data): + """Push a new scope on the stack. + + :param data: the data dictionary to push on the context stack. + """ + + def pop(self): + """Pop the top-most scope from the stack.""" + + +def _apply_directives(stream, directives, ctxt, vars): + """Apply the given directives to the stream. + + :param stream: the stream the directives should be applied to + :param directives: the list of directives to apply + :param ctxt: the `Context` + :param vars: additional variables that should be available when Python + code is executed + :return: the stream with the given directives applied + """ + if directives: + stream = directives[0](iter(stream), directives[1:], ctxt, **vars) + return stream + + +def _eval_expr(expr, ctxt, vars=None): + """Evaluate the given `Expression` object. + + :param expr: the expression to evaluate + :param ctxt: the `Context` + :param vars: additional variables that should be available to the + expression + :return: the result of the evaluation + """ + if vars: + ctxt.push(vars) + retval = expr.evaluate(ctxt) + if vars: + ctxt.pop() + return retval + + +def _exec_suite(suite, ctxt, vars=None): + """Execute the given `Suite` object. + + :param suite: the code suite to execute + :param ctxt: the `Context` + :param vars: additional variables that should be available to the + code + """ + if vars: + ctxt.push(vars) + ctxt.push({}) + suite.execute(ctxt) + if vars: + top = ctxt.pop() + ctxt.pop() + ctxt.frames[0].update(top) + + +class DirectiveFactoryMeta(type): + """Meta class for directive factories.""" + + def __new__(cls, name, bases, d): + if 'directives' in d: + d['_dir_by_name'] = dict(d['directives']) + d['_dir_order'] = [directive[1] for directive in d['directives']] + + return type.__new__(cls, name, bases, d) + + +class DirectiveFactory(object): + """Base for classes that provide a set of template directives. + + :since: version 0.6 + """ + __metaclass__ = DirectiveFactoryMeta + + directives = [] + """A list of ``(name, cls)`` tuples that define the set of directives + provided by this factory. + """ + + def get_directive(self, name): + """Return the directive class for the given name. + + :param name: the directive name as used in the template + :return: the directive class + :see: `Directive` + """ + return self._dir_by_name.get(name) + + def get_directive_index(self, dir_cls): + """Return a key for the given directive class that should be used to + sort it among other directives on the same `SUB` event. + + The default implementation simply returns the index of the directive in + the `directives` list. + + :param dir_cls: the directive class + :return: the sort key + """ + if dir_cls in self._dir_order: + return self._dir_order.index(dir_cls) + return len(self._dir_order) + + +class Template(DirectiveFactory): + """Abstract template base class. + + This class implements most of the template processing model, but does not + specify the syntax of templates. + """ + + EXEC = StreamEventKind('EXEC') + """Stream event kind representing a Python code suite to execute.""" + + EXPR = StreamEventKind('EXPR') + """Stream event kind representing a Python expression.""" + + INCLUDE = StreamEventKind('INCLUDE') + """Stream event kind representing the inclusion of another template.""" + + SUB = StreamEventKind('SUB') + """Stream event kind representing a nested stream to which one or more + directives should be applied. + """ + + serializer = None + _number_conv = unicode # function used to convert numbers to event data + + def __init__(self, source, filepath=None, filename=None, loader=None, + encoding=None, lookup='strict', allow_exec=True): + """Initialize a template from either a string, a file-like object, or + an already parsed markup stream. + + :param source: a string, file-like object, or markup stream to read the + template from + :param filepath: the absolute path to the template file + :param filename: the path to the template file relative to the search + path + :param loader: the `TemplateLoader` to use for loading included + templates + :param encoding: the encoding of the `source` + :param lookup: the variable lookup mechanism; either "strict" (the + default), "lenient", or a custom lookup class + :param allow_exec: whether Python code blocks in templates should be + allowed + + :note: Changed in 0.5: Added the `allow_exec` argument + """ + self.filepath = filepath or filename + self.filename = filename + self.loader = loader + self.lookup = lookup + self.allow_exec = allow_exec + self._init_filters() + self._init_loader() + self._prepared = False + + if isinstance(source, basestring): + source = StringIO(source) + else: + source = source + try: + self._stream = self._parse(source, encoding) + except ParseError, e: + raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset) + + def __getstate__(self): + state = self.__dict__.copy() + state['filters'] = [] + return state + + def __setstate__(self, state): + self.__dict__ = state + self._init_filters() + + def __repr__(self): + return '<%s "%s">' % (type(self).__name__, self.filename) + + def _init_filters(self): + self.filters = [self._flatten, self._include] + + def _init_loader(self): + if self.loader is None: + from genshi.template.loader import TemplateLoader + if self.filename: + if self.filepath != self.filename: + basedir = os.path.normpath(self.filepath)[:-len( + os.path.normpath(self.filename)) + ] + else: + basedir = os.path.dirname(self.filename) + else: + basedir = '.' + self.loader = TemplateLoader([os.path.abspath(basedir)]) + + @property + def stream(self): + if not self._prepared: + self._stream = list(self._prepare(self._stream)) + self._prepared = True + return self._stream + + def _parse(self, source, encoding): + """Parse the template. + + The parsing stage parses the template and constructs a list of + directives that will be executed in the render stage. The input is + split up into literal output (text that does not depend on the context + data) and directives or expressions. + + :param source: a file-like object containing the XML source of the + template, or an XML event stream + :param encoding: the encoding of the `source` + """ + raise NotImplementedError + + def _prepare(self, stream): + """Call the `attach` method of every directive found in the template. + + :param stream: the event stream of the template + """ + from genshi.template.loader import TemplateNotFound + + for kind, data, pos in stream: + if kind is SUB: + directives = [] + substream = data[1] + for _, cls, value, namespaces, pos in sorted(data[0]): + directive, substream = cls.attach(self, substream, value, + namespaces, pos) + if directive: + directives.append(directive) + substream = self._prepare(substream) + if directives: + yield kind, (directives, list(substream)), pos + else: + for event in substream: + yield event + else: + if kind is INCLUDE: + href, cls, fallback = data + if isinstance(href, basestring) and \ + not getattr(self.loader, 'auto_reload', True): + # If the path to the included template is static, and + # auto-reloading is disabled on the template loader, + # the template is inlined into the stream + try: + tmpl = self.loader.load(href, relative_to=pos[0], + cls=cls or self.__class__) + for event in tmpl.stream: + yield event + except TemplateNotFound: + if fallback is None: + raise + for event in self._prepare(fallback): + yield event + continue + elif fallback: + # Otherwise the include is performed at run time + data = href, cls, list(self._prepare(fallback)) + + yield kind, data, pos + + def generate(self, *args, **kwargs): + """Apply the template to the given context data. + + Any keyword arguments are made available to the template as context + data. + + Only one positional argument is accepted: if it is provided, it must be + an instance of the `Context` class, and keyword arguments are ignored. + This calling style is used for internal processing. + + :return: a markup event stream representing the result of applying + the template to the context data. + """ + vars = {} + if args: + assert len(args) == 1 + ctxt = args[0] + if ctxt is None: + ctxt = Context(**kwargs) + else: + vars = kwargs + assert isinstance(ctxt, Context) + else: + ctxt = Context(**kwargs) + + stream = self.stream + for filter_ in self.filters: + stream = filter_(iter(stream), ctxt, **vars) + return Stream(stream, self.serializer) + + def _flatten(self, stream, ctxt, **vars): + number_conv = self._number_conv + stack = [] + push = stack.append + pop = stack.pop + stream = iter(stream) + + while 1: + for kind, data, pos in stream: + + if kind is START and data[1]: + # Attributes may still contain expressions in start tags at + # this point, so do some evaluation + tag, attrs = data + new_attrs = [] + for name, value in attrs: + if type(value) is list: # this is an interpolated string + values = [event[1] + for event in self._flatten(value, ctxt, **vars) + if event[0] is TEXT and event[1] is not None + ] + if not values: + continue + value = ''.join(values) + new_attrs.append((name, value)) + yield kind, (tag, Attrs(new_attrs)), pos + + elif kind is EXPR: + result = _eval_expr(data, ctxt, vars) + if result is not None: + # First check for a string, otherwise the iterable test + # below succeeds, and the string will be chopped up into + # individual characters + if isinstance(result, basestring): + yield TEXT, result, pos + elif isinstance(result, (int, float, long)): + yield TEXT, number_conv(result), pos + elif hasattr(result, '__iter__'): + push(stream) + stream = _ensure(result) + break + else: + yield TEXT, unicode(result), pos + + elif kind is SUB: + # This event is a list of directives and a list of nested + # events to which those directives should be applied + push(stream) + stream = _apply_directives(data[1], data[0], ctxt, vars) + break + + elif kind is EXEC: + _exec_suite(data, ctxt, vars) + + else: + yield kind, data, pos + + else: + if not stack: + break + stream = pop() + + def _include(self, stream, ctxt, **vars): + """Internal stream filter that performs inclusion of external + template files. + """ + from genshi.template.loader import TemplateNotFound + + for event in stream: + if event[0] is INCLUDE: + href, cls, fallback = event[1] + if not isinstance(href, basestring): + parts = [] + for subkind, subdata, subpos in self._flatten(href, ctxt, + **vars): + if subkind is TEXT: + parts.append(subdata) + href = ''.join([x for x in parts if x is not None]) + try: + tmpl = self.loader.load(href, relative_to=event[2][0], + cls=cls or self.__class__) + for event in tmpl.generate(ctxt, **vars): + yield event + except TemplateNotFound: + if fallback is None: + raise + for filter_ in self.filters: + fallback = filter_(iter(fallback), ctxt, **vars) + for event in fallback: + yield event + else: + yield event + + +EXEC = Template.EXEC +EXPR = Template.EXPR +INCLUDE = Template.INCLUDE +SUB = Template.SUB diff --git a/websdk/genshi/template/directives.py b/websdk/genshi/template/directives.py new file mode 100644 index 0000000..e2c9424 --- /dev/null +++ b/websdk/genshi/template/directives.py @@ -0,0 +1,725 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Implementation of the various template directives.""" + +from genshi.core import QName, Stream +from genshi.path import Path +from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \ + EXPR, _apply_directives, _eval_expr +from genshi.template.eval import Expression, ExpressionASTTransformer, \ + _ast, _parse + +__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective', + 'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective', + 'OtherwiseDirective', 'ReplaceDirective', 'StripDirective', + 'WhenDirective', 'WithDirective'] +__docformat__ = 'restructuredtext en' + + +class DirectiveMeta(type): + """Meta class for template directives.""" + + def __new__(cls, name, bases, d): + d['tagname'] = name.lower().replace('directive', '') + return type.__new__(cls, name, bases, d) + + +class Directive(object): + """Abstract base class for template directives. + + A directive is basically a callable that takes three positional arguments: + ``ctxt`` is the template data context, ``stream`` is an iterable over the + events that the directive applies to, and ``directives`` is is a list of + other directives on the same stream that need to be applied. + + Directives can be "anonymous" or "registered". Registered directives can be + applied by the template author using an XML attribute with the + corresponding name in the template. Such directives should be subclasses of + this base class that can be instantiated with the value of the directive + attribute as parameter. + + Anonymous directives are simply functions conforming to the protocol + described above, and can only be applied programmatically (for example by + template filters). + """ + __metaclass__ = DirectiveMeta + __slots__ = ['expr'] + + def __init__(self, value, template=None, namespaces=None, lineno=-1, + offset=-1): + self.expr = self._parse_expr(value, template, lineno, offset) + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + """Called after the template stream has been completely parsed. + + :param template: the `Template` object + :param stream: the event stream associated with the directive + :param value: the argument value for the directive; if the directive was + specified as an element, this will be an `Attrs` instance + with all specified attributes, otherwise it will be a + `unicode` object with just the attribute value + :param namespaces: a mapping of namespace URIs to prefixes + :param pos: a ``(filename, lineno, offset)`` tuple describing the + location where the directive was found in the source + + This class method should return a ``(directive, stream)`` tuple. If + ``directive`` is not ``None``, it should be an instance of the `Directive` + class, and gets added to the list of directives applied to the substream + at runtime. `stream` is an event stream that replaces the original + stream associated with the directive. + """ + return cls(value, template, namespaces, *pos[1:]), stream + + def __call__(self, stream, directives, ctxt, **vars): + """Apply the directive to the given stream. + + :param stream: the event stream + :param directives: a list of the remaining directives that should + process the stream + :param ctxt: the context data + :param vars: additional variables that should be made available when + Python code is executed + """ + raise NotImplementedError + + def __repr__(self): + expr = '' + if getattr(self, 'expr', None) is not None: + expr = ' "%s"' % self.expr.source + return '<%s%s>' % (type(self).__name__, expr) + + @classmethod + def _parse_expr(cls, expr, template, lineno=-1, offset=-1): + """Parses the given expression, raising a useful error message when a + syntax error is encountered. + """ + try: + return expr and Expression(expr, template.filepath, lineno, + lookup=template.lookup) or None + except SyntaxError, err: + err.msg += ' in expression "%s" of "%s" directive' % (expr, + cls.tagname) + raise TemplateSyntaxError(err, template.filepath, lineno, + offset + (err.offset or 0)) + + +def _assignment(ast): + """Takes the AST representation of an assignment, and returns a + function that applies the assignment of a given value to a dictionary. + """ + def _names(node): + if isinstance(node, _ast.Tuple): + return tuple([_names(child) for child in node.elts]) + elif isinstance(node, _ast.Name): + return node.id + def _assign(data, value, names=_names(ast)): + if type(names) is tuple: + for idx in range(len(names)): + _assign(data, value[idx], names[idx]) + else: + data[names] = value + return _assign + + +class AttrsDirective(Directive): + """Implementation of the ``py:attrs`` template directive. + + The value of the ``py:attrs`` attribute should be a dictionary or a sequence + of ``(name, value)`` tuples. The items in that dictionary or sequence are + added as attributes to the element: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:attrs="foo">Bar</li> + ... </ul>''') + >>> print(tmpl.generate(foo={'class': 'collapse'})) + <ul> + <li class="collapse">Bar</li> + </ul> + >>> print(tmpl.generate(foo=[('class', 'collapse')])) + <ul> + <li class="collapse">Bar</li> + </ul> + + If the value evaluates to ``None`` (or any other non-truth value), no + attributes are added: + + >>> print(tmpl.generate(foo=None)) + <ul> + <li>Bar</li> + </ul> + """ + __slots__ = [] + + def __call__(self, stream, directives, ctxt, **vars): + def _generate(): + kind, (tag, attrib), pos = stream.next() + attrs = _eval_expr(self.expr, ctxt, vars) + if attrs: + if isinstance(attrs, Stream): + try: + attrs = iter(attrs).next() + except StopIteration: + attrs = [] + elif not isinstance(attrs, list): # assume it's a dict + attrs = attrs.items() + attrib -= [name for name, val in attrs if val is None] + attrib |= [(QName(name), unicode(val).strip()) for name, val + in attrs if val is not None] + yield kind, (tag, attrib), pos + for event in stream: + yield event + + return _apply_directives(_generate(), directives, ctxt, vars) + + +class ContentDirective(Directive): + """Implementation of the ``py:content`` template directive. + + This directive replaces the content of the element with the result of + evaluating the value of the ``py:content`` attribute: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:content="bar">Hello</li> + ... </ul>''') + >>> print(tmpl.generate(bar='Bye')) + <ul> + <li>Bye</li> + </ul> + """ + __slots__ = [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + raise TemplateSyntaxError('The content directive can not be used ' + 'as an element', template.filepath, + *pos[1:]) + expr = cls._parse_expr(value, template, *pos[1:]) + return None, [stream[0], (EXPR, expr, pos), stream[-1]] + + +class DefDirective(Directive): + """Implementation of the ``py:def`` template directive. + + This directive can be used to create "Named Template Functions", which + are template snippets that are not actually output during normal + processing, but rather can be expanded from expressions in other places + in the template. + + A named template function can be used just like a normal Python function + from template expressions: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="echo(greeting, name='world')" class="message"> + ... ${greeting}, ${name}! + ... </p> + ... ${echo('Hi', name='you')} + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + <p class="message"> + Hi, you! + </p> + </div> + + If a function does not require parameters, the parenthesis can be omitted + in the definition: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <p py:def="helloworld" class="message"> + ... Hello, world! + ... </p> + ... ${helloworld()} + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + <p class="message"> + Hello, world! + </p> + </div> + """ + __slots__ = ['name', 'args', 'star_args', 'dstar_args', 'defaults'] + + def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + ast = _parse(args).body + self.args = [] + self.star_args = None + self.dstar_args = None + self.defaults = {} + if isinstance(ast, _ast.Call): + self.name = ast.func.id + for arg in ast.args: + # only names + self.args.append(arg.id) + for kwd in ast.keywords: + self.args.append(kwd.arg) + exp = Expression(kwd.value, template.filepath, + lineno, lookup=template.lookup) + self.defaults[kwd.arg] = exp + if getattr(ast, 'starargs', None): + self.star_args = ast.starargs.id + if getattr(ast, 'kwargs', None): + self.dstar_args = ast.kwargs.id + else: + self.name = ast.id + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('function') + return super(DefDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + stream = list(stream) + + def function(*args, **kwargs): + scope = {} + args = list(args) # make mutable + for name in self.args: + if args: + scope[name] = args.pop(0) + else: + if name in kwargs: + val = kwargs.pop(name) + else: + val = _eval_expr(self.defaults.get(name), ctxt, vars) + scope[name] = val + if not self.star_args is None: + scope[self.star_args] = args + if not self.dstar_args is None: + scope[self.dstar_args] = kwargs + ctxt.push(scope) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt.pop() + function.__name__ = self.name + + # Store the function reference in the bottom context frame so that it + # doesn't get popped off before processing the template has finished + # FIXME: this makes context data mutable as a side-effect + ctxt.frames[-1][self.name] = function + + return [] + + def __repr__(self): + return '<%s "%s">' % (type(self).__name__, self.name) + + +class ForDirective(Directive): + """Implementation of the ``py:for`` template directive for repeating an + element based on an iterable in the context data. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:for="item in items">${item}</li> + ... </ul>''') + >>> print(tmpl.generate(items=[1, 2, 3])) + <ul> + <li>1</li><li>2</li><li>3</li> + </ul> + """ + __slots__ = ['assign', 'filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + if ' in ' not in value: + raise TemplateSyntaxError('"in" keyword missing in "for" directive', + template.filepath, lineno, offset) + assign, value = value.split(' in ', 1) + ast = _parse(assign, 'exec') + value = 'iter(%s)' % value.strip() + self.assign = _assignment(ast.body[0].value) + self.filename = template.filepath + Directive.__init__(self, value, template, namespaces, lineno, offset) + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('each') + return super(ForDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + iterable = _eval_expr(self.expr, ctxt, vars) + if iterable is None: + return + + assign = self.assign + scope = {} + stream = list(stream) + for item in iterable: + assign(scope, item) + ctxt.push(scope) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt.pop() + + def __repr__(self): + return '<%s>' % type(self).__name__ + + +class IfDirective(Directive): + """Implementation of the ``py:if`` template directive for conditionally + excluding elements from being output. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <b py:if="foo">${bar}</b> + ... </div>''') + >>> print(tmpl.generate(foo=True, bar='Hello')) + <div> + <b>Hello</b> + </div> + """ + __slots__ = [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('test') + return super(IfDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + value = _eval_expr(self.expr, ctxt, vars) + if value: + return _apply_directives(stream, directives, ctxt, vars) + return [] + + +class MatchDirective(Directive): + """Implementation of the ``py:match`` template directive. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:match="greeting"> + ... Hello ${select('@name')} + ... </span> + ... <greeting name="Dude" /> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span> + Hello Dude + </span> + </div> + """ + __slots__ = ['path', 'namespaces', 'hints'] + + def __init__(self, value, template, hints=None, namespaces=None, + lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.path = Path(value, template.filepath, lineno) + self.namespaces = namespaces or {} + self.hints = hints or () + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + hints = [] + if type(value) is dict: + if value.get('buffer', '').lower() == 'false': + hints.append('not_buffered') + if value.get('once', '').lower() == 'true': + hints.append('match_once') + if value.get('recursive', '').lower() == 'false': + hints.append('not_recursive') + value = value.get('path') + return cls(value, template, frozenset(hints), namespaces, *pos[1:]), \ + stream + + def __call__(self, stream, directives, ctxt, **vars): + ctxt._match_templates.append((self.path.test(ignore_context=True), + self.path, list(stream), self.hints, + self.namespaces, directives)) + return [] + + def __repr__(self): + return '<%s "%s">' % (type(self).__name__, self.path.source) + + +class ReplaceDirective(Directive): + """Implementation of the ``py:replace`` template directive. + + This directive replaces the element with the result of evaluating the + value of the ``py:replace`` attribute: + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:replace="bar">Hello</span> + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + Bye + </div> + + This directive is equivalent to ``py:content`` combined with ``py:strip``, + providing a less verbose way to achieve the same effect: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:content="bar" py:strip="">Hello</span> + ... </div>''') + >>> print(tmpl.generate(bar='Bye')) + <div> + Bye + </div> + """ + __slots__ = [] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('value') + if not value: + raise TemplateSyntaxError('missing value for "replace" directive', + template.filepath, *pos[1:]) + expr = cls._parse_expr(value, template, *pos[1:]) + return None, [(EXPR, expr, pos)] + + +class StripDirective(Directive): + """Implementation of the ``py:strip`` template directive. + + When the value of the ``py:strip`` attribute evaluates to ``True``, the + element is stripped from the output + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:strip="True"><b>foo</b></div> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <b>foo</b> + </div> + + Leaving the attribute value empty is equivalent to a truth value. + + This directive is particulary interesting for named template functions or + match templates that do not generate a top-level element: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <div py:def="echo(what)" py:strip=""> + ... <b>${what}</b> + ... </div> + ... ${echo('foo')} + ... </div>''') + >>> print(tmpl.generate()) + <div> + <b>foo</b> + </div> + """ + __slots__ = [] + + def __call__(self, stream, directives, ctxt, **vars): + def _generate(): + if not self.expr or _eval_expr(self.expr, ctxt, vars): + stream.next() # skip start tag + previous = stream.next() + for event in stream: + yield previous + previous = event + else: + for event in stream: + yield event + return _apply_directives(_generate(), directives, ctxt, vars) + + +class ChooseDirective(Directive): + """Implementation of the ``py:choose`` directive for conditionally selecting + one of several body elements to display. + + If the ``py:choose`` expression is empty the expressions of nested + ``py:when`` directives are tested for truth. The first true ``py:when`` + body is output. If no ``py:when`` directive is matched then the fallback + directive ``py:otherwise`` will be used. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose=""> + ... <span py:when="0 == 1">0</span> + ... <span py:when="1 == 1">1</span> + ... <span py:otherwise="">2</span> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span>1</span> + </div> + + If the ``py:choose`` directive contains an expression, the nested + ``py:when`` directives are tested for equality to the ``py:choose`` + expression: + + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/" + ... py:choose="2"> + ... <span py:when="1">1</span> + ... <span py:when="2">2</span> + ... </div>''') + >>> print(tmpl.generate()) + <div> + <span>2</span> + </div> + + Behavior is undefined if a ``py:choose`` block contains content outside a + ``py:when`` or ``py:otherwise`` block. Behavior is also undefined if a + ``py:otherwise`` occurs before ``py:when`` blocks. + """ + __slots__ = ['matched', 'value'] + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('test') + return super(ChooseDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + info = [False, bool(self.expr), None] + if self.expr: + info[2] = _eval_expr(self.expr, ctxt, vars) + ctxt._choice_stack.append(info) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt._choice_stack.pop() + + +class WhenDirective(Directive): + """Implementation of the ``py:when`` directive for nesting in a parent with + the ``py:choose`` directive. + + See the documentation of the `ChooseDirective` for usage. + """ + __slots__ = ['filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, value, template, namespaces, lineno, offset) + self.filename = template.filepath + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('test') + return super(WhenDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + info = ctxt._choice_stack and ctxt._choice_stack[-1] + if not info: + raise TemplateRuntimeError('"when" directives can only be used ' + 'inside a "choose" directive', + self.filename, *stream.next()[2][1:]) + if info[0]: + return [] + if not self.expr and not info[1]: + raise TemplateRuntimeError('either "choose" or "when" directive ' + 'must have a test expression', + self.filename, *stream.next()[2][1:]) + if info[1]: + value = info[2] + if self.expr: + matched = value == _eval_expr(self.expr, ctxt, vars) + else: + matched = bool(value) + else: + matched = bool(_eval_expr(self.expr, ctxt, vars)) + info[0] = matched + if not matched: + return [] + + return _apply_directives(stream, directives, ctxt, vars) + + +class OtherwiseDirective(Directive): + """Implementation of the ``py:otherwise`` directive for nesting in a parent + with the ``py:choose`` directive. + + See the documentation of `ChooseDirective` for usage. + """ + __slots__ = ['filename'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.filename = template.filepath + + def __call__(self, stream, directives, ctxt, **vars): + info = ctxt._choice_stack and ctxt._choice_stack[-1] + if not info: + raise TemplateRuntimeError('an "otherwise" directive can only be ' + 'used inside a "choose" directive', + self.filename, *stream.next()[2][1:]) + if info[0]: + return [] + info[0] = True + + return _apply_directives(stream, directives, ctxt, vars) + + +class WithDirective(Directive): + """Implementation of the ``py:with`` template directive, which allows + shorthand access to variables and expressions. + + >>> from genshi.template import MarkupTemplate + >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"> + ... <span py:with="y=7; z=x+10">$x $y $z</span> + ... </div>''') + >>> print(tmpl.generate(x=42)) + <div> + <span>42 7 52</span> + </div> + """ + __slots__ = ['vars'] + + def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1): + Directive.__init__(self, None, template, namespaces, lineno, offset) + self.vars = [] + value = value.strip() + try: + ast = _parse(value, 'exec') + for node in ast.body: + if not isinstance(node, _ast.Assign): + raise TemplateSyntaxError('only assignment allowed in ' + 'value of the "with" directive', + template.filepath, lineno, offset) + self.vars.append(([_assignment(n) for n in node.targets], + Expression(node.value, template.filepath, + lineno, lookup=template.lookup))) + except SyntaxError, err: + err.msg += ' in expression "%s" of "%s" directive' % (value, + self.tagname) + raise TemplateSyntaxError(err, template.filepath, lineno, + offset + (err.offset or 0)) + + @classmethod + def attach(cls, template, stream, value, namespaces, pos): + if type(value) is dict: + value = value.get('vars') + return super(WithDirective, cls).attach(template, stream, value, + namespaces, pos) + + def __call__(self, stream, directives, ctxt, **vars): + frame = {} + ctxt.push(frame) + for targets, expr in self.vars: + value = _eval_expr(expr, ctxt, vars) + for assign in targets: + assign(frame, value) + for event in _apply_directives(stream, directives, ctxt, vars): + yield event + ctxt.pop() + + def __repr__(self): + return '<%s>' % (type(self).__name__) diff --git a/websdk/genshi/template/eval.py b/websdk/genshi/template/eval.py new file mode 100644 index 0000000..8593aaa --- /dev/null +++ b/websdk/genshi/template/eval.py @@ -0,0 +1,629 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Support for "safe" evaluation of Python expressions.""" + +import __builtin__ + +from textwrap import dedent +from types import CodeType + +from genshi.core import Markup +from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, \ + _ast, parse +from genshi.template.base import TemplateRuntimeError +from genshi.util import flatten + +__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup', + 'Undefined', 'UndefinedError'] +__docformat__ = 'restructuredtext en' + + +# Check for a Python 2.4 bug in the eval loop +has_star_import_bug = False +try: + class _FakeMapping(object): + __getitem__ = __setitem__ = lambda *a: None + exec 'from sys import *' in {}, _FakeMapping() +except SystemError: + has_star_import_bug = True +del _FakeMapping + + +def _star_import_patch(mapping, modname): + """This function is used as helper if a Python version with a broken + star-import opcode is in use. + """ + module = __import__(modname, None, None, ['__all__']) + if hasattr(module, '__all__'): + members = module.__all__ + else: + members = [x for x in module.__dict__ if not x.startswith('_')] + mapping.update([(name, getattr(module, name)) for name in members]) + + +class Code(object): + """Abstract base class for the `Expression` and `Suite` classes.""" + __slots__ = ['source', 'code', 'ast', '_globals'] + + def __init__(self, source, filename=None, lineno=-1, lookup='strict', + xform=None): + """Create the code object, either from a string, or from an AST node. + + :param source: either a string containing the source code, or an AST + node + :param filename: the (preferably absolute) name of the file containing + the code + :param lineno: the number of the line on which the code was found + :param lookup: the lookup class that defines how variables are looked + up in the context; can be either "strict" (the default), + "lenient", or a custom lookup class + :param xform: the AST transformer that should be applied to the code; + if `None`, the appropriate transformation is chosen + depending on the mode + """ + if isinstance(source, basestring): + self.source = source + node = _parse(source, mode=self.mode) + else: + assert isinstance(source, _ast.AST), \ + 'Expected string or AST node, but got %r' % source + self.source = '?' + if self.mode == 'eval': + node = _ast.Expression() + node.body = source + else: + node = _ast.Module() + node.body = [source] + + self.ast = node + self.code = _compile(node, self.source, mode=self.mode, + filename=filename, lineno=lineno, xform=xform) + if lookup is None: + lookup = LenientLookup + elif isinstance(lookup, basestring): + lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup] + self._globals = lookup.globals + + def __getstate__(self): + state = {'source': self.source, 'ast': self.ast, + 'lookup': self._globals.im_self} + c = self.code + state['code'] = (c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code, + c.co_consts, c.co_names, c.co_varnames, c.co_filename, + c.co_name, c.co_firstlineno, c.co_lnotab, (), ()) + return state + + def __setstate__(self, state): + self.source = state['source'] + self.ast = state['ast'] + self.code = CodeType(0, *state['code']) + self._globals = state['lookup'].globals + + def __eq__(self, other): + return (type(other) == type(self)) and (self.code == other.code) + + def __hash__(self): + return hash(self.code) + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return '%s(%r)' % (type(self).__name__, self.source) + + +class Expression(Code): + """Evaluates Python expressions used in templates. + + >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) + >>> Expression('test').evaluate(data) + 'Foo' + + >>> Expression('items[0]').evaluate(data) + 1 + >>> Expression('items[-1]').evaluate(data) + 3 + >>> Expression('dict["some"]').evaluate(data) + 'thing' + + Similar to e.g. Javascript, expressions in templates can use the dot + notation for attribute access to access items in mappings: + + >>> Expression('dict.some').evaluate(data) + 'thing' + + This also works the other way around: item access can be used to access + any object attribute: + + >>> class MyClass(object): + ... myattr = 'Bar' + >>> data = dict(mine=MyClass(), key='myattr') + >>> Expression('mine.myattr').evaluate(data) + 'Bar' + >>> Expression('mine["myattr"]').evaluate(data) + 'Bar' + >>> Expression('mine[key]').evaluate(data) + 'Bar' + + All of the standard Python operators are available to template expressions. + Built-in functions such as ``len()`` are also available in template + expressions: + + >>> data = dict(items=[1, 2, 3]) + >>> Expression('len(items)').evaluate(data) + 3 + """ + __slots__ = [] + mode = 'eval' + + def evaluate(self, data): + """Evaluate the expression against the given data dictionary. + + :param data: a mapping containing the data to evaluate against + :return: the result of the evaluation + """ + __traceback_hide__ = 'before_and_this' + _globals = self._globals(data) + return eval(self.code, _globals, {'__data__': data}) + + +class Suite(Code): + """Executes Python statements used in templates. + + >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) + >>> Suite("foo = dict['some']").execute(data) + >>> data['foo'] + 'thing' + """ + __slots__ = [] + mode = 'exec' + + def execute(self, data): + """Execute the suite in the given data dictionary. + + :param data: a mapping containing the data to execute in + """ + __traceback_hide__ = 'before_and_this' + _globals = self._globals(data) + exec self.code in _globals, data + + +UNDEFINED = object() + + +class UndefinedError(TemplateRuntimeError): + """Exception thrown when a template expression attempts to access a variable + not defined in the context. + + :see: `LenientLookup`, `StrictLookup` + """ + def __init__(self, name, owner=UNDEFINED): + if owner is not UNDEFINED: + message = '%s has no member named "%s"' % (repr(owner), name) + else: + message = '"%s" not defined' % name + TemplateRuntimeError.__init__(self, message) + + +class Undefined(object): + """Represents a reference to an undefined variable. + + Unlike the Python runtime, template expressions can refer to an undefined + variable without causing a `NameError` to be raised. The result will be an + instance of the `Undefined` class, which is treated the same as ``False`` in + conditions, but raise an exception on any other operation: + + >>> foo = Undefined('foo') + >>> bool(foo) + False + >>> list(foo) + [] + >>> print(foo) + undefined + + However, calling an undefined variable, or trying to access an attribute + of that variable, will raise an exception that includes the name used to + reference that undefined variable. + + >>> foo('bar') + Traceback (most recent call last): + ... + UndefinedError: "foo" not defined + + >>> foo.bar + Traceback (most recent call last): + ... + UndefinedError: "foo" not defined + + :see: `LenientLookup` + """ + __slots__ = ['_name', '_owner'] + + def __init__(self, name, owner=UNDEFINED): + """Initialize the object. + + :param name: the name of the reference + :param owner: the owning object, if the variable is accessed as a member + """ + self._name = name + self._owner = owner + + def __iter__(self): + return iter([]) + + def __nonzero__(self): + return False + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self._name) + + def __str__(self): + return 'undefined' + + def _die(self, *args, **kwargs): + """Raise an `UndefinedError`.""" + __traceback_hide__ = True + raise UndefinedError(self._name, self._owner) + __call__ = __getattr__ = __getitem__ = _die + + # Hack around some behavior introduced in Python 2.6.2 + # http://genshi.edgewall.org/ticket/324 + __length_hint__ = None + + +class LookupBase(object): + """Abstract base class for variable lookup implementations.""" + + @classmethod + def globals(cls, data): + """Construct the globals dictionary to use as the execution context for + the expression or suite. + """ + return { + '__data__': data, + '_lookup_name': cls.lookup_name, + '_lookup_attr': cls.lookup_attr, + '_lookup_item': cls.lookup_item, + '_star_import_patch': _star_import_patch, + 'UndefinedError': UndefinedError, + } + + @classmethod + def lookup_name(cls, data, name): + __traceback_hide__ = True + val = data.get(name, UNDEFINED) + if val is UNDEFINED: + val = BUILTINS.get(name, val) + if val is UNDEFINED: + val = cls.undefined(name) + return val + + @classmethod + def lookup_attr(cls, obj, key): + __traceback_hide__ = True + try: + val = getattr(obj, key) + except AttributeError: + if hasattr(obj.__class__, key): + raise + else: + try: + val = obj[key] + except (KeyError, TypeError): + val = cls.undefined(key, owner=obj) + return val + + @classmethod + def lookup_item(cls, obj, key): + __traceback_hide__ = True + if len(key) == 1: + key = key[0] + try: + return obj[key] + except (AttributeError, KeyError, IndexError, TypeError), e: + if isinstance(key, basestring): + val = getattr(obj, key, UNDEFINED) + if val is UNDEFINED: + val = cls.undefined(key, owner=obj) + return val + raise + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Can be overridden by subclasses to specify behavior when undefined + variables are accessed. + + :param key: the name of the variable + :param owner: the owning object, if the variable is accessed as a member + """ + raise NotImplementedError + + +class LenientLookup(LookupBase): + """Default variable lookup mechanism for expressions. + + When an undefined variable is referenced using this lookup style, the + reference evaluates to an instance of the `Undefined` class: + + >>> expr = Expression('nothing', lookup='lenient') + >>> undef = expr.evaluate({}) + >>> undef + <Undefined 'nothing'> + + The same will happen when a non-existing attribute or item is accessed on + an existing object: + + >>> expr = Expression('something.nil', lookup='lenient') + >>> expr.evaluate({'something': dict()}) + <Undefined 'nil'> + + See the documentation of the `Undefined` class for details on the behavior + of such objects. + + :see: `StrictLookup` + """ + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Return an ``Undefined`` object.""" + __traceback_hide__ = True + return Undefined(key, owner=owner) + + +class StrictLookup(LookupBase): + """Strict variable lookup mechanism for expressions. + + Referencing an undefined variable using this lookup style will immediately + raise an ``UndefinedError``: + + >>> expr = Expression('nothing', lookup='strict') + >>> expr.evaluate({}) + Traceback (most recent call last): + ... + UndefinedError: "nothing" not defined + + The same happens when a non-existing attribute or item is accessed on an + existing object: + + >>> expr = Expression('something.nil', lookup='strict') + >>> expr.evaluate({'something': dict()}) + Traceback (most recent call last): + ... + UndefinedError: {} has no member named "nil" + """ + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Raise an ``UndefinedError`` immediately.""" + __traceback_hide__ = True + raise UndefinedError(key, owner=owner) + + +def _parse(source, mode='eval'): + source = source.strip() + if mode == 'exec': + lines = [line.expandtabs() for line in source.splitlines()] + if lines: + first = lines[0] + rest = dedent('\n'.join(lines[1:])).rstrip() + if first.rstrip().endswith(':') and not rest[0].isspace(): + rest = '\n'.join([' %s' % line for line in rest.splitlines()]) + source = '\n'.join([first, rest]) + if isinstance(source, unicode): + source = '\xef\xbb\xbf' + source.encode('utf-8') + return parse(source, mode) + + +def _compile(node, source=None, mode='eval', filename=None, lineno=-1, + xform=None): + if isinstance(filename, unicode): + # unicode file names not allowed for code objects + filename = filename.encode('utf-8', 'replace') + elif not filename: + filename = '<string>' + if lineno <= 0: + lineno = 1 + + if xform is None: + xform = { + 'eval': ExpressionASTTransformer + }.get(mode, TemplateASTTransformer) + tree = xform().visit(node) + + if mode == 'eval': + name = '<Expression %r>' % (source or '?') + else: + lines = source.splitlines() + if not lines: + extract = '' + else: + extract = lines[0] + if len(lines) > 1: + extract += ' ...' + name = '<Suite %r>' % (extract) + new_source = ASTCodeGenerator(tree).code + code = compile(new_source, filename, mode) + + try: + # We'd like to just set co_firstlineno, but it's readonly. So we need + # to clone the code object while adjusting the line number + return CodeType(0, code.co_nlocals, code.co_stacksize, + code.co_flags | 0x0040, code.co_code, code.co_consts, + code.co_names, code.co_varnames, filename, name, + lineno, code.co_lnotab, (), ()) + except RuntimeError: + return code + + +def _new(class_, *args, **kwargs): + ret = class_() + for attr, value in zip(ret._fields, args): + if attr in kwargs: + raise ValueError('Field set both in args and kwargs') + setattr(ret, attr, value) + for attr, value in kwargs: + setattr(ret, attr, value) + return ret + + +BUILTINS = __builtin__.__dict__.copy() +BUILTINS.update({'Markup': Markup, 'Undefined': Undefined}) +CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis']) + + +class TemplateASTTransformer(ASTTransformer): + """Concrete AST transformer that implements the AST transformations needed + for code embedded in templates. + """ + + def __init__(self): + self.locals = [CONSTANTS] + + def _extract_names(self, node): + names = set() + def _process(node): + if isinstance(node, _ast.Name): + names.add(node.id) + elif isinstance(node, _ast.alias): + names.add(node.asname or node.name) + elif isinstance(node, _ast.Tuple): + for elt in node.elts: + _process(elt) + if hasattr(node, 'args'): + for arg in node.args: + _process(arg) + if hasattr(node, 'vararg'): + names.add(node.vararg) + if hasattr(node, 'kwarg'): + names.add(node.kwarg) + elif hasattr(node, 'names'): + for elt in node.names: + _process(elt) + return names + + def visit_Str(self, node): + if isinstance(node.s, str): + try: # If the string is ASCII, return a `str` object + node.s.decode('ascii') + except ValueError: # Otherwise return a `unicode` object + return _new(_ast.Str, node.s.decode('utf-8')) + return node + + def visit_ClassDef(self, node): + if len(self.locals) > 1: + self.locals[-1].add(node.name) + self.locals.append(set()) + try: + return ASTTransformer.visit_ClassDef(self, node) + finally: + self.locals.pop() + + def visit_Import(self, node): + if len(self.locals) > 1: + self.locals[-1].update(self._extract_names(node)) + return ASTTransformer.visit_Import(self, node) + + def visit_ImportFrom(self, node): + if [a.name for a in node.names] == ['*']: + if has_star_import_bug: + # This is a Python 2.4 bug. Only if we have a broken Python + # version do we need to apply this hack + node = _new(_ast.Expr, _new(_ast.Call, + _new(_ast.Name, '_star_import_patch'), [ + _new(_ast.Name, '__data__'), + _new(_ast.Str, node.module) + ], (), ())) + return node + if len(self.locals) > 1: + self.locals[-1].update(self._extract_names(node)) + return ASTTransformer.visit_ImportFrom(self, node) + + def visit_FunctionDef(self, node): + if len(self.locals) > 1: + self.locals[-1].add(node.name) + + self.locals.append(self._extract_names(node.args)) + try: + return ASTTransformer.visit_FunctionDef(self, node) + finally: + self.locals.pop() + + # GeneratorExp(expr elt, comprehension* generators) + def visit_GeneratorExp(self, node): + gens = [] + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self.locals.append(set()) + gen = _new(_ast.comprehension, self.visit(generator.target), + self.visit(generator.iter), + [self.visit(if_) for if_ in generator.ifs]) + gens.append(gen) + + # use node.__class__ to make it reusable as ListComp + ret = _new(node.__class__, self.visit(node.elt), gens) + #delete inserted locals + del self.locals[-len(node.generators):] + return ret + + # ListComp(expr elt, comprehension* generators) + visit_ListComp = visit_GeneratorExp + + def visit_Lambda(self, node): + self.locals.append(self._extract_names(node.args)) + try: + return ASTTransformer.visit_Lambda(self, node) + finally: + self.locals.pop() + + def visit_Name(self, node): + # If the name refers to a local inside a lambda, list comprehension, or + # generator expression, leave it alone + if isinstance(node.ctx, _ast.Load) and \ + node.id not in flatten(self.locals): + # Otherwise, translate the name ref into a context lookup + name = _new(_ast.Name, '_lookup_name', _ast.Load()) + namearg = _new(_ast.Name, '__data__', _ast.Load()) + strarg = _new(_ast.Str, node.id) + node = _new(_ast.Call, name, [namearg, strarg], []) + elif isinstance(node.ctx, _ast.Store): + if len(self.locals) > 1: + self.locals[-1].add(node.id) + + return node + + +class ExpressionASTTransformer(TemplateASTTransformer): + """Concrete AST transformer that implements the AST transformations needed + for code embedded in templates. + """ + + def visit_Attribute(self, node): + if not isinstance(node.ctx, _ast.Load): + return ASTTransformer.visit_Attribute(self, node) + + func = _new(_ast.Name, '_lookup_attr', _ast.Load()) + args = [self.visit(node.value), _new(_ast.Str, node.attr)] + return _new(_ast.Call, func, args, []) + + def visit_Subscript(self, node): + if not isinstance(node.ctx, _ast.Load) or \ + not isinstance(node.slice, _ast.Index): + return ASTTransformer.visit_Subscript(self, node) + + func = _new(_ast.Name, '_lookup_item', _ast.Load()) + args = [ + self.visit(node.value), + _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load()) + ] + return _new(_ast.Call, func, args, []) diff --git a/websdk/genshi/template/interpolation.py b/websdk/genshi/template/interpolation.py new file mode 100644 index 0000000..1e1a385 --- /dev/null +++ b/websdk/genshi/template/interpolation.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""String interpolation routines, i.e. the splitting up a given text into some +parts that are literal strings, and others that are Python expressions. +""" + +from itertools import chain +import os +import re +from tokenize import PseudoToken + +from genshi.core import TEXT +from genshi.template.base import TemplateSyntaxError, EXPR +from genshi.template.eval import Expression + +__all__ = ['interpolate'] +__docformat__ = 'restructuredtext en' + +NAMESTART = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' +NAMECHARS = NAMESTART + '.0123456789' +PREFIX = '$' + +token_re = re.compile('%s|%s(?s)' % ( + r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1', + PseudoToken +)) + + +def interpolate(text, filepath=None, lineno=-1, offset=0, lookup='strict'): + """Parse the given string and extract expressions. + + This function is a generator that yields `TEXT` events for literal strings, + and `EXPR` events for expressions, depending on the results of parsing the + string. + + >>> for kind, data, pos in interpolate("hey ${foo}bar"): + ... print('%s %r' % (kind, data)) + TEXT 'hey ' + EXPR Expression('foo') + TEXT 'bar' + + :param text: the text to parse + :param filepath: absolute path to the file in which the text was found + (optional) + :param lineno: the line number at which the text was found (optional) + :param offset: the column number at which the text starts in the source + (optional) + :param lookup: the variable lookup mechanism; either "lenient" (the + default), "strict", or a custom lookup class + :return: a list of `TEXT` and `EXPR` events + :raise TemplateSyntaxError: when a syntax error in an expression is + encountered + """ + pos = [filepath, lineno, offset] + + textbuf = [] + textpos = None + for is_expr, chunk in chain(lex(text, pos, filepath), [(True, '')]): + if is_expr: + if textbuf: + yield TEXT, ''.join(textbuf), textpos + del textbuf[:] + textpos = None + if chunk: + try: + expr = Expression(chunk.strip(), pos[0], pos[1], + lookup=lookup) + yield EXPR, expr, tuple(pos) + except SyntaxError, err: + raise TemplateSyntaxError(err, filepath, pos[1], + pos[2] + (err.offset or 0)) + else: + textbuf.append(chunk) + if textpos is None: + textpos = tuple(pos) + + if '\n' in chunk: + lines = chunk.splitlines() + pos[1] += len(lines) - 1 + pos[2] += len(lines[-1]) + else: + pos[2] += len(chunk) + + +def lex(text, textpos, filepath): + offset = pos = 0 + end = len(text) + escaped = False + + while 1: + if escaped: + offset = text.find(PREFIX, offset + 2) + escaped = False + else: + offset = text.find(PREFIX, pos) + if offset < 0 or offset == end - 1: + break + next = text[offset + 1] + + if next == '{': + if offset > pos: + yield False, text[pos:offset] + pos = offset + 2 + level = 1 + while level: + match = token_re.match(text, pos) + if match is None: + raise TemplateSyntaxError('invalid syntax', filepath, + *textpos[1:]) + pos = match.end() + tstart, tend = match.regs[3] + token = text[tstart:tend] + if token == '{': + level += 1 + elif token == '}': + level -= 1 + yield True, text[offset + 2:pos - 1] + + elif next in NAMESTART: + if offset > pos: + yield False, text[pos:offset] + pos = offset + pos += 1 + while pos < end: + char = text[pos] + if char not in NAMECHARS: + break + pos += 1 + yield True, text[offset + 1:pos].strip() + + elif not escaped and next == PREFIX: + if offset > pos: + yield False, text[pos:offset] + escaped = True + pos = offset + 1 + + else: + yield False, text[pos:offset + 1] + pos = offset + 1 + + if pos < end: + yield False, text[pos:] diff --git a/websdk/genshi/template/loader.py b/websdk/genshi/template/loader.py new file mode 100644 index 0000000..0e7cda7 --- /dev/null +++ b/websdk/genshi/template/loader.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Template loading and caching.""" + +import os +try: + import threading +except ImportError: + import dummy_threading as threading + +from genshi.template.base import TemplateError +from genshi.util import LRUCache + +__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package', + 'prefixed'] +__docformat__ = 'restructuredtext en' + + +class TemplateNotFound(TemplateError): + """Exception raised when a specific template file could not be found.""" + + def __init__(self, name, search_path): + """Create the exception. + + :param name: the filename of the template + :param search_path: the search path used to lookup the template + """ + TemplateError.__init__(self, 'Template "%s" not found' % name) + self.search_path = search_path + + +class TemplateLoader(object): + """Responsible for loading templates from files on the specified search + path. + + >>> import tempfile + >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') + >>> os.write(fd, '<p>$var</p>') + 11 + >>> os.close(fd) + + The template loader accepts a list of directory paths that are then used + when searching for template files, in the given order: + + >>> loader = TemplateLoader([os.path.dirname(path)]) + + The `load()` method first checks the template cache whether the requested + template has already been loaded. If not, it attempts to locate the + template file, and returns the corresponding `Template` object: + + >>> from genshi.template import MarkupTemplate + >>> template = loader.load(os.path.basename(path)) + >>> isinstance(template, MarkupTemplate) + True + + Template instances are cached: requesting a template with the same name + results in the same instance being returned: + + >>> loader.load(os.path.basename(path)) is template + True + + The `auto_reload` option can be used to control whether a template should + be automatically reloaded when the file it was loaded from has been + changed. Disable this automatic reloading to improve performance. + + >>> os.remove(path) + """ + def __init__(self, search_path=None, auto_reload=False, + default_encoding=None, max_cache_size=25, default_class=None, + variable_lookup='strict', allow_exec=True, callback=None): + """Create the template laoder. + + :param search_path: a list of absolute path names that should be + searched for template files, or a string containing + a single absolute path; alternatively, any item on + the list may be a ''load function'' that is passed + a filename and returns a file-like object and some + metadata + :param auto_reload: whether to check the last modification time of + template files, and reload them if they have changed + :param default_encoding: the default encoding to assume when loading + templates; defaults to UTF-8 + :param max_cache_size: the maximum number of templates to keep in the + cache + :param default_class: the default `Template` subclass to use when + instantiating templates + :param variable_lookup: the variable lookup mechanism; either "strict" + (the default), "lenient", or a custom lookup + class + :param allow_exec: whether to allow Python code blocks in templates + :param callback: (optional) a callback function that is invoked after a + template was initialized by this loader; the function + is passed the template object as only argument. This + callback can be used for example to add any desired + filters to the template + :see: `LenientLookup`, `StrictLookup` + + :note: Changed in 0.5: Added the `allow_exec` argument + """ + from genshi.template.markup import MarkupTemplate + + self.search_path = search_path + if self.search_path is None: + self.search_path = [] + elif not isinstance(self.search_path, (list, tuple)): + self.search_path = [self.search_path] + + self.auto_reload = auto_reload + """Whether templates should be reloaded when the underlying file is + changed""" + + self.default_encoding = default_encoding + self.default_class = default_class or MarkupTemplate + self.variable_lookup = variable_lookup + self.allow_exec = allow_exec + if callback is not None and not hasattr(callback, '__call__'): + raise TypeError('The "callback" parameter needs to be callable') + self.callback = callback + self._cache = LRUCache(max_cache_size) + self._uptodate = {} + self._lock = threading.RLock() + + def __getstate__(self): + state = self.__dict__.copy() + state['_lock'] = None + return state + + def __setstate__(self, state): + self.__dict__ = state + self._lock = threading.RLock() + + def load(self, filename, relative_to=None, cls=None, encoding=None): + """Load the template with the given name. + + If the `filename` parameter is relative, this method searches the + search path trying to locate a template matching the given name. If the + file name is an absolute path, the search path is ignored. + + If the requested template is not found, a `TemplateNotFound` exception + is raised. Otherwise, a `Template` object is returned that represents + the parsed template. + + Template instances are cached to avoid having to parse the same + template file more than once. Thus, subsequent calls of this method + with the same template file name will return the same `Template` + object (unless the ``auto_reload`` option is enabled and the file was + changed since the last parse.) + + If the `relative_to` parameter is provided, the `filename` is + interpreted as being relative to that path. + + :param filename: the relative path of the template file to load + :param relative_to: the filename of the template from which the new + template is being loaded, or ``None`` if the + template is being loaded directly + :param cls: the class of the template object to instantiate + :param encoding: the encoding of the template to load; defaults to the + ``default_encoding`` of the loader instance + :return: the loaded `Template` instance + :raises TemplateNotFound: if a template with the given name could not + be found + """ + if cls is None: + cls = self.default_class + search_path = self.search_path + + # Make the filename relative to the template file its being loaded + # from, but only if that file is specified as a relative path, or no + # search path has been set up + if relative_to and (not search_path or not os.path.isabs(relative_to)): + filename = os.path.join(os.path.dirname(relative_to), filename) + + filename = os.path.normpath(filename) + cachekey = filename + + self._lock.acquire() + try: + # First check the cache to avoid reparsing the same file + try: + tmpl = self._cache[cachekey] + if not self.auto_reload: + return tmpl + uptodate = self._uptodate[cachekey] + if uptodate is not None and uptodate(): + return tmpl + except (KeyError, OSError): + pass + + isabs = False + + if os.path.isabs(filename): + # Bypass the search path if the requested filename is absolute + search_path = [os.path.dirname(filename)] + isabs = True + + elif relative_to and os.path.isabs(relative_to): + # Make sure that the directory containing the including + # template is on the search path + dirname = os.path.dirname(relative_to) + if dirname not in search_path: + search_path = list(search_path) + [dirname] + isabs = True + + elif not search_path: + # Uh oh, don't know where to look for the template + raise TemplateError('Search path for templates not configured') + + for loadfunc in search_path: + if isinstance(loadfunc, basestring): + loadfunc = directory(loadfunc) + try: + filepath, filename, fileobj, uptodate = loadfunc(filename) + except IOError: + continue + else: + try: + if isabs: + # If the filename of either the included or the + # including template is absolute, make sure the + # included template gets an absolute path, too, + # so that nested includes work properly without a + # search path + filename = filepath + tmpl = self._instantiate(cls, fileobj, filepath, + filename, encoding=encoding) + if self.callback: + self.callback(tmpl) + self._cache[cachekey] = tmpl + self._uptodate[cachekey] = uptodate + finally: + if hasattr(fileobj, 'close'): + fileobj.close() + return tmpl + + raise TemplateNotFound(filename, search_path) + + finally: + self._lock.release() + + def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): + """Instantiate and return the `Template` object based on the given + class and parameters. + + This function is intended for subclasses to override if they need to + implement special template instantiation logic. Code that just uses + the `TemplateLoader` should use the `load` method instead. + + :param cls: the class of the template object to instantiate + :param fileobj: a readable file-like object containing the template + source + :param filepath: the absolute path to the template file + :param filename: the path to the template file relative to the search + path + :param encoding: the encoding of the template to load; defaults to the + ``default_encoding`` of the loader instance + :return: the loaded `Template` instance + :rtype: `Template` + """ + if encoding is None: + encoding = self.default_encoding + return cls(fileobj, filepath=filepath, filename=filename, loader=self, + encoding=encoding, lookup=self.variable_lookup, + allow_exec=self.allow_exec) + + @staticmethod + def directory(path): + """Loader factory for loading templates from a local directory. + + :param path: the path to the local directory containing the templates + :return: the loader function to load templates from the given directory + :rtype: ``function`` + """ + def _load_from_directory(filename): + filepath = os.path.join(path, filename) + fileobj = open(filepath, 'U') + mtime = os.path.getmtime(filepath) + def _uptodate(): + return mtime == os.path.getmtime(filepath) + return filepath, filename, fileobj, _uptodate + return _load_from_directory + + @staticmethod + def package(name, path): + """Loader factory for loading templates from egg package data. + + :param name: the name of the package containing the resources + :param path: the path inside the package data + :return: the loader function to load templates from the given package + :rtype: ``function`` + """ + from pkg_resources import resource_stream + def _load_from_package(filename): + filepath = os.path.join(path, filename) + return filepath, filename, resource_stream(name, filepath), None + return _load_from_package + + @staticmethod + def prefixed(**delegates): + """Factory for a load function that delegates to other loaders + depending on the prefix of the requested template path. + + The prefix is stripped from the filename when passing on the load + request to the delegate. + + >>> load = prefixed( + ... app1 = lambda filename: ('app1', filename, None, None), + ... app2 = lambda filename: ('app2', filename, None, None) + ... ) + >>> print(load('app1/foo.html')) + ('app1', 'app1/foo.html', None, None) + >>> print(load('app2/bar.html')) + ('app2', 'app2/bar.html', None, None) + + :param delegates: mapping of path prefixes to loader functions + :return: the loader function + :rtype: ``function`` + """ + def _dispatch_by_prefix(filename): + for prefix, delegate in delegates.items(): + if filename.startswith(prefix): + if isinstance(delegate, basestring): + delegate = directory(delegate) + filepath, _, fileobj, uptodate = delegate( + filename[len(prefix):].lstrip('/\\') + ) + return filepath, filename, fileobj, uptodate + raise TemplateNotFound(filename, list(delegates.keys())) + return _dispatch_by_prefix + + +directory = TemplateLoader.directory +package = TemplateLoader.package +prefixed = TemplateLoader.prefixed diff --git a/websdk/genshi/template/markup.py b/websdk/genshi/template/markup.py new file mode 100644 index 0000000..0e31632 --- /dev/null +++ b/websdk/genshi/template/markup.py @@ -0,0 +1,397 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Markup templating engine.""" + +from itertools import chain + +from genshi.core import Attrs, Markup, Namespace, Stream, StreamEventKind +from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT +from genshi.input import XMLParser +from genshi.template.base import BadDirectiveError, Template, \ + TemplateSyntaxError, _apply_directives, \ + EXEC, INCLUDE, SUB +from genshi.template.eval import Suite +from genshi.template.interpolation import interpolate +from genshi.template.directives import * +from genshi.template.text import NewTextTemplate + +__all__ = ['MarkupTemplate'] +__docformat__ = 'restructuredtext en' + + +class MarkupTemplate(Template): + """Implementation of the template language for XML-based templates. + + >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/"> + ... <li py:for="item in items">${item}</li> + ... </ul>''') + >>> print(tmpl.generate(items=[1, 2, 3])) + <ul> + <li>1</li><li>2</li><li>3</li> + </ul> + """ + + DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/' + XINCLUDE_NAMESPACE = 'http://www.w3.org/2001/XInclude' + + directives = [('def', DefDirective), + ('match', MatchDirective), + ('when', WhenDirective), + ('otherwise', OtherwiseDirective), + ('for', ForDirective), + ('if', IfDirective), + ('choose', ChooseDirective), + ('with', WithDirective), + ('replace', ReplaceDirective), + ('content', ContentDirective), + ('attrs', AttrsDirective), + ('strip', StripDirective)] + serializer = 'xml' + _number_conv = Markup + + def __init__(self, source, filepath=None, filename=None, loader=None, + encoding=None, lookup='strict', allow_exec=True): + Template.__init__(self, source, filepath=filepath, filename=filename, + loader=loader, encoding=encoding, lookup=lookup, + allow_exec=allow_exec) + self.add_directives(self.DIRECTIVE_NAMESPACE, self) + + def _init_filters(self): + Template._init_filters(self) + # Make sure the include filter comes after the match filter + self.filters.remove(self._include) + self.filters += [self._match, self._include] + + def _parse(self, source, encoding): + if not isinstance(source, Stream): + source = XMLParser(source, filename=self.filename, + encoding=encoding) + stream = [] + + for kind, data, pos in source: + + if kind is TEXT: + for kind, data, pos in interpolate(data, self.filepath, pos[1], + pos[2], lookup=self.lookup): + stream.append((kind, data, pos)) + + elif kind is PI and data[0] == 'python': + if not self.allow_exec: + raise TemplateSyntaxError('Python code blocks not allowed', + self.filepath, *pos[1:]) + try: + suite = Suite(data[1], self.filepath, pos[1], + lookup=self.lookup) + except SyntaxError, err: + raise TemplateSyntaxError(err, self.filepath, + pos[1] + (err.lineno or 1) - 1, + pos[2] + (err.offset or 0)) + stream.append((EXEC, suite, pos)) + + elif kind is COMMENT: + if not data.lstrip().startswith('!'): + stream.append((kind, data, pos)) + + else: + stream.append((kind, data, pos)) + + return stream + + def _extract_directives(self, stream, namespace, factory): + depth = 0 + dirmap = {} # temporary mapping of directives to elements + new_stream = [] + ns_prefix = {} # namespace prefixes in use + + for kind, data, pos in stream: + + if kind is START: + tag, attrs = data + directives = [] + strip = False + + if tag.namespace == namespace: + cls = factory.get_directive(tag.localname) + if cls is None: + raise BadDirectiveError(tag.localname, + self.filepath, pos[1]) + args = dict([(name.localname, value) for name, value + in attrs if not name.namespace]) + directives.append((factory.get_directive_index(cls), cls, + args, ns_prefix.copy(), pos)) + strip = True + + new_attrs = [] + for name, value in attrs: + if name.namespace == namespace: + cls = factory.get_directive(name.localname) + if cls is None: + raise BadDirectiveError(name.localname, + self.filepath, pos[1]) + if type(value) is list and len(value) == 1: + value = value[0][1] + directives.append((factory.get_directive_index(cls), + cls, value, ns_prefix.copy(), pos)) + else: + new_attrs.append((name, value)) + new_attrs = Attrs(new_attrs) + + if directives: + directives.sort() + dirmap[(depth, tag)] = (directives, len(new_stream), + strip) + + new_stream.append((kind, (tag, new_attrs), pos)) + depth += 1 + + elif kind is END: + depth -= 1 + new_stream.append((kind, data, pos)) + + # If there have have directive attributes with the + # corresponding start tag, move the events inbetween into + # a "subprogram" + if (depth, data) in dirmap: + directives, offset, strip = dirmap.pop((depth, data)) + substream = new_stream[offset:] + if strip: + substream = substream[1:-1] + new_stream[offset:] = [ + (SUB, (directives, substream), pos) + ] + + elif kind is SUB: + directives, substream = data + substream = self._extract_directives(substream, namespace, + factory) + + if len(substream) == 1 and substream[0][0] is SUB: + added_directives, substream = substream[0][1] + directives += added_directives + + new_stream.append((kind, (directives, substream), pos)) + + elif kind is START_NS: + # Strip out the namespace declaration for template + # directives + prefix, uri = data + ns_prefix[prefix] = uri + if uri != namespace: + new_stream.append((kind, data, pos)) + + elif kind is END_NS: + uri = ns_prefix.pop(data, None) + if uri and uri != namespace: + new_stream.append((kind, data, pos)) + + else: + new_stream.append((kind, data, pos)) + + return new_stream + + def _extract_includes(self, stream): + streams = [[]] # stacked lists of events of the "compiled" template + prefixes = {} + fallbacks = [] + includes = [] + xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE) + + for kind, data, pos in stream: + stream = streams[-1] + + if kind is START: + # Record any directive attributes in start tags + tag, attrs = data + if tag in xinclude_ns: + if tag.localname == 'include': + include_href = attrs.get('href') + if not include_href: + raise TemplateSyntaxError('Include misses required ' + 'attribute "href"', + self.filepath, *pos[1:]) + includes.append((include_href, attrs.get('parse'))) + streams.append([]) + elif tag.localname == 'fallback': + streams.append([]) + fallbacks.append(streams[-1]) + else: + stream.append((kind, (tag, attrs), pos)) + + elif kind is END: + if fallbacks and data == xinclude_ns['fallback']: + assert streams.pop() is fallbacks[-1] + elif data == xinclude_ns['include']: + fallback = None + if len(fallbacks) == len(includes): + fallback = fallbacks.pop() + streams.pop() # discard anything between the include tags + # and the fallback element + stream = streams[-1] + href, parse = includes.pop() + try: + cls = { + 'xml': MarkupTemplate, + 'text': NewTextTemplate + }.get(parse) or self.__class__ + except KeyError: + raise TemplateSyntaxError('Invalid value for "parse" ' + 'attribute of include', + self.filepath, *pos[1:]) + stream.append((INCLUDE, (href, cls, fallback), pos)) + else: + stream.append((kind, data, pos)) + + elif kind is START_NS and data[1] == xinclude_ns: + # Strip out the XInclude namespace + prefixes[data[0]] = data[1] + + elif kind is END_NS and data in prefixes: + prefixes.pop(data) + + else: + stream.append((kind, data, pos)) + + assert len(streams) == 1 + return streams[0] + + def _interpolate_attrs(self, stream): + for kind, data, pos in stream: + + if kind is START: + # Record any directive attributes in start tags + tag, attrs = data + new_attrs = [] + for name, value in attrs: + if value: + value = list(interpolate(value, self.filepath, pos[1], + pos[2], lookup=self.lookup)) + if len(value) == 1 and value[0][0] is TEXT: + value = value[0][1] + new_attrs.append((name, value)) + data = tag, Attrs(new_attrs) + + yield kind, data, pos + + def _prepare(self, stream): + return Template._prepare(self, + self._extract_includes(self._interpolate_attrs(stream)) + ) + + def add_directives(self, namespace, factory): + """Register a custom `DirectiveFactory` for a given namespace. + + :param namespace: the namespace URI + :type namespace: `basestring` + :param factory: the directive factory to register + :type factory: `DirectiveFactory` + :since: version 0.6 + """ + assert not self._prepared, 'Too late for adding directives, ' \ + 'template already prepared' + self._stream = self._extract_directives(self._stream, namespace, + factory) + + def _match(self, stream, ctxt, start=0, end=None, **vars): + """Internal stream filter that applies any defined match templates + to the stream. + """ + match_templates = ctxt._match_templates + + tail = [] + def _strip(stream, append=tail.append): + depth = 1 + next = stream.next + while 1: + event = next() + if event[0] is START: + depth += 1 + elif event[0] is END: + depth -= 1 + if depth > 0: + yield event + else: + append(event) + break + + for event in stream: + + # We (currently) only care about start and end events for matching + # We might care about namespace events in the future, though + if not match_templates or (event[0] is not START and + event[0] is not END): + yield event + continue + + for idx, (test, path, template, hints, namespaces, directives) \ + in enumerate(match_templates): + if idx < start or end is not None and idx >= end: + continue + + if test(event, namespaces, ctxt) is True: + if 'match_once' in hints: + del match_templates[idx] + idx -= 1 + + # Let the remaining match templates know about the event so + # they get a chance to update their internal state + for test in [mt[0] for mt in match_templates[idx + 1:]]: + test(event, namespaces, ctxt, updateonly=True) + + # Consume and store all events until an end event + # corresponding to this start event is encountered + pre_end = idx + 1 + if 'match_once' not in hints and 'not_recursive' in hints: + pre_end -= 1 + inner = _strip(stream) + if pre_end > 0: + inner = self._match(inner, ctxt, start=start, + end=pre_end, **vars) + content = self._include(chain([event], inner, tail), ctxt) + if 'not_buffered' not in hints: + content = list(content) + content = Stream(content) + + # Make the select() function available in the body of the + # match template + selected = [False] + def select(path): + selected[0] = True + return content.select(path, namespaces, ctxt) + vars = dict(select=select) + + # Recursively process the output + template = _apply_directives(template, directives, ctxt, + vars) + for event in self._match(self._flatten(template, ctxt, + **vars), + ctxt, start=idx + 1, **vars): + yield event + + # If the match template did not actually call select to + # consume the matched stream, the original events need to + # be consumed here or they'll get appended to the output + if not selected[0]: + for event in content: + pass + + # Let the remaining match templates know about the last + # event in the matched content, so they can update their + # internal state accordingly + for test in [mt[0] for mt in match_templates[idx + 1:]]: + test(tail[0], namespaces, ctxt, updateonly=True) + + break + + else: # no matches + yield event diff --git a/websdk/genshi/template/plugin.py b/websdk/genshi/template/plugin.py new file mode 100644 index 0000000..70d56af --- /dev/null +++ b/websdk/genshi/template/plugin.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# Copyright (C) 2006 Matthew Good +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Basic support for the template engine plugin API used by TurboGears and +CherryPy/Buffet. +""" + +from genshi.input import ET, HTML, XML +from genshi.output import DocType +from genshi.template.base import Template +from genshi.template.loader import TemplateLoader +from genshi.template.markup import MarkupTemplate +from genshi.template.text import TextTemplate, NewTextTemplate + +__all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin', + 'MarkupTemplateEnginePlugin', 'TextTemplateEnginePlugin'] +__docformat__ = 'restructuredtext en' + + +class ConfigurationError(ValueError): + """Exception raised when invalid plugin options are encountered.""" + + +class AbstractTemplateEnginePlugin(object): + """Implementation of the plugin API.""" + + template_class = None + extension = None + + def __init__(self, extra_vars_func=None, options=None): + self.get_extra_vars = extra_vars_func + if options is None: + options = {} + self.options = options + + self.default_encoding = options.get('genshi.default_encoding', 'utf-8') + auto_reload = options.get('genshi.auto_reload', '1') + if isinstance(auto_reload, basestring): + auto_reload = auto_reload.lower() in ('1', 'on', 'yes', 'true') + search_path = [p for p in + options.get('genshi.search_path', '').split(':') if p] + self.use_package_naming = not search_path + try: + max_cache_size = int(options.get('genshi.max_cache_size', 25)) + except ValueError: + raise ConfigurationError('Invalid value for max_cache_size: "%s"' % + options.get('genshi.max_cache_size')) + + loader_callback = options.get('genshi.loader_callback', None) + if loader_callback and not hasattr(loader_callback, '__call__'): + raise ConfigurationError('loader callback must be a function') + + lookup_errors = options.get('genshi.lookup_errors', 'strict') + if lookup_errors not in ('lenient', 'strict'): + raise ConfigurationError('Unknown lookup errors mode "%s"' % + lookup_errors) + + try: + allow_exec = bool(options.get('genshi.allow_exec', True)) + except ValueError: + raise ConfigurationError('Invalid value for allow_exec "%s"' % + options.get('genshi.allow_exec')) + + self.loader = TemplateLoader([p for p in search_path if p], + auto_reload=auto_reload, + max_cache_size=max_cache_size, + default_class=self.template_class, + variable_lookup=lookup_errors, + allow_exec=allow_exec, + callback=loader_callback) + + def load_template(self, templatename, template_string=None): + """Find a template specified in python 'dot' notation, or load one from + a string. + """ + if template_string is not None: + return self.template_class(template_string) + + if self.use_package_naming: + divider = templatename.rfind('.') + if divider >= 0: + from pkg_resources import resource_filename + package = templatename[:divider] + basename = templatename[divider + 1:] + self.extension + templatename = resource_filename(package, basename) + + return self.loader.load(templatename) + + def _get_render_options(self, format=None, fragment=False): + if format is None: + format = self.default_format + kwargs = {'method': format} + if self.default_encoding: + kwargs['encoding'] = self.default_encoding + return kwargs + + def render(self, info, format=None, fragment=False, template=None): + """Render the template to a string using the provided info.""" + kwargs = self._get_render_options(format=format, fragment=fragment) + return self.transform(info, template).render(**kwargs) + + def transform(self, info, template): + """Render the output to an event stream.""" + if not isinstance(template, Template): + template = self.load_template(template) + return template.generate(**info) + + +class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin): + """Implementation of the plugin API for markup templates.""" + + template_class = MarkupTemplate + extension = '.html' + + def __init__(self, extra_vars_func=None, options=None): + AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options) + + default_doctype = self.options.get('genshi.default_doctype') + if default_doctype: + doctype = DocType.get(default_doctype) + if doctype is None: + raise ConfigurationError('Unknown doctype %r' % default_doctype) + self.default_doctype = doctype + else: + self.default_doctype = None + + format = self.options.get('genshi.default_format', 'html').lower() + if format not in ('html', 'xhtml', 'xml', 'text'): + raise ConfigurationError('Unknown output format %r' % format) + self.default_format = format + + def _get_render_options(self, format=None, fragment=False): + kwargs = super(MarkupTemplateEnginePlugin, + self)._get_render_options(format, fragment) + if self.default_doctype and not fragment: + kwargs['doctype'] = self.default_doctype + return kwargs + + def transform(self, info, template): + """Render the output to an event stream.""" + data = {'ET': ET, 'HTML': HTML, 'XML': XML} + if self.get_extra_vars: + data.update(self.get_extra_vars()) + data.update(info) + return super(MarkupTemplateEnginePlugin, self).transform(data, template) + + +class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin): + """Implementation of the plugin API for text templates.""" + + template_class = TextTemplate + extension = '.txt' + default_format = 'text' + + def __init__(self, extra_vars_func=None, options=None): + if options is None: + options = {} + + new_syntax = options.get('genshi.new_text_syntax') + if isinstance(new_syntax, basestring): + new_syntax = new_syntax.lower() in ('1', 'on', 'yes', 'true') + if new_syntax: + self.template_class = NewTextTemplate + + AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options) diff --git a/websdk/genshi/template/text.py b/websdk/genshi/template/text.py new file mode 100644 index 0000000..746226c --- /dev/null +++ b/websdk/genshi/template/text.py @@ -0,0 +1,333 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Plain text templating engine. + +This module implements two template language syntaxes, at least for a certain +transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines +a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other +hand is inspired by the syntax of the Django template language, which has more +explicit delimiting of directives, and is more flexible with regards to +white space and line breaks. + +In a future release, `OldTextTemplate` will be phased out in favor of +`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly +recommended for new projects, and existing projects may want to migrate to the +new syntax to remain compatible with future Genshi releases. +""" + +import re + +from genshi.core import TEXT +from genshi.template.base import BadDirectiveError, Template, \ + TemplateSyntaxError, EXEC, INCLUDE, SUB +from genshi.template.eval import Suite +from genshi.template.directives import * +from genshi.template.directives import Directive +from genshi.template.interpolation import interpolate + +__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate'] +__docformat__ = 'restructuredtext en' + + +class NewTextTemplate(Template): + r"""Implementation of a simple text-based template engine. This class will + replace `OldTextTemplate` in a future release. + + It uses a more explicit delimiting style for directives: instead of the old + style which required putting directives on separate lines that were prefixed + with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs + (by default ``{% ... %}`` and ``{# ... #}``, respectively). + + Variable substitution uses the same interpolation syntax as for markup + languages: simple references are prefixed with a dollar sign, more complex + expression enclosed in curly braces. + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... {# This is a comment #} + ... We have the following items for you: + ... {% for item in items %} + ... * ${'Item %d' % item} + ... {% end %} + ... ''') + >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) + Dear Joe, + <BLANKLINE> + <BLANKLINE> + We have the following items for you: + <BLANKLINE> + * Item 1 + <BLANKLINE> + * Item 2 + <BLANKLINE> + * Item 3 + <BLANKLINE> + <BLANKLINE> + + By default, no spaces or line breaks are removed. If a line break should + not be included in the output, prefix it with a backslash: + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... {# This is a comment #}\ + ... We have the following items for you: + ... {% for item in items %}\ + ... * $item + ... {% end %}\ + ... ''') + >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) + Dear Joe, + <BLANKLINE> + We have the following items for you: + * 1 + * 2 + * 3 + <BLANKLINE> + + Backslashes are also used to escape the start delimiter of directives and + comments: + + >>> tmpl = NewTextTemplate('''Dear $name, + ... + ... \{# This is a comment #} + ... We have the following items for you: + ... {% for item in items %}\ + ... * $item + ... {% end %}\ + ... ''') + >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) + Dear Joe, + <BLANKLINE> + {# This is a comment #} + We have the following items for you: + * 1 + * 2 + * 3 + <BLANKLINE> + + :since: version 0.5 + """ + directives = [('def', DefDirective), + ('when', WhenDirective), + ('otherwise', OtherwiseDirective), + ('for', ForDirective), + ('if', IfDirective), + ('choose', ChooseDirective), + ('with', WithDirective)] + serializer = 'text' + + _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)' + _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)' + + def __init__(self, source, filepath=None, filename=None, loader=None, + encoding=None, lookup='strict', allow_exec=False, + delims=('{%', '%}', '{#', '#}')): + self.delimiters = delims + Template.__init__(self, source, filepath=filepath, filename=filename, + loader=loader, encoding=encoding, lookup=lookup) + + def _get_delims(self): + return self._delims + def _set_delims(self, delims): + if len(delims) != 4: + raise ValueError('delimiers tuple must have exactly four elements') + self._delims = delims + self._directive_re = re.compile(self._DIRECTIVE_RE % tuple( + [re.escape(d) for d in delims] + ), re.DOTALL) + self._escape_re = re.compile(self._ESCAPE_RE % tuple( + [re.escape(d) for d in delims[::2]] + )) + delimiters = property(_get_delims, _set_delims, """\ + The delimiters for directives and comments. This should be a four item tuple + of the form ``(directive_start, directive_end, comment_start, + comment_end)``, where each item is a string. + """) + + def _parse(self, source, encoding): + """Parse the template from text input.""" + stream = [] # list of events of the "compiled" template + dirmap = {} # temporary mapping of directives to elements + depth = 0 + + source = source.read() + if isinstance(source, str): + source = source.decode(encoding or 'utf-8', 'replace') + offset = 0 + lineno = 1 + + _escape_sub = self._escape_re.sub + def _escape_repl(mo): + groups = [g for g in mo.groups() if g] + if not groups: + return '' + return groups[0] + + for idx, mo in enumerate(self._directive_re.finditer(source)): + start, end = mo.span(1) + if start > offset: + text = _escape_sub(_escape_repl, source[offset:start]) + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + lineno += len(text.splitlines()) + + lineno += len(source[start:end].splitlines()) + command, value = mo.group(2, 3) + + if command == 'include': + pos = (self.filename, lineno, 0) + value = list(interpolate(value, self.filepath, lineno, 0, + lookup=self.lookup)) + if len(value) == 1 and value[0][0] is TEXT: + value = value[0][1] + stream.append((INCLUDE, (value, None, []), pos)) + + elif command == 'python': + if not self.allow_exec: + raise TemplateSyntaxError('Python code blocks not allowed', + self.filepath, lineno) + try: + suite = Suite(value, self.filepath, lineno, + lookup=self.lookup) + except SyntaxError, err: + raise TemplateSyntaxError(err, self.filepath, + lineno + (err.lineno or 1) - 1) + pos = (self.filename, lineno, 0) + stream.append((EXEC, suite, pos)) + + elif command == 'end': + depth -= 1 + if depth in dirmap: + directive, start_offset = dirmap.pop(depth) + substream = stream[start_offset:] + stream[start_offset:] = [(SUB, ([directive], substream), + (self.filepath, lineno, 0))] + + elif command: + cls = self.get_directive(command) + if cls is None: + raise BadDirectiveError(command) + directive = 0, cls, value, None, (self.filepath, lineno, 0) + dirmap[depth] = (directive, len(stream)) + depth += 1 + + offset = end + + if offset < len(source): + text = _escape_sub(_escape_repl, source[offset:]) + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + + return stream + + +class OldTextTemplate(Template): + """Legacy implementation of the old syntax text-based templates. This class + is provided in a transition phase for backwards compatibility. New code + should use the `NewTextTemplate` class and the improved syntax it provides. + + >>> tmpl = OldTextTemplate('''Dear $name, + ... + ... We have the following items for you: + ... #for item in items + ... * $item + ... #end + ... + ... All the best, + ... Foobar''') + >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) + Dear Joe, + <BLANKLINE> + We have the following items for you: + * 1 + * 2 + * 3 + <BLANKLINE> + All the best, + Foobar + """ + directives = [('def', DefDirective), + ('when', WhenDirective), + ('otherwise', OtherwiseDirective), + ('for', ForDirective), + ('if', IfDirective), + ('choose', ChooseDirective), + ('with', WithDirective)] + serializer = 'text' + + _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|' + r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)', + re.MULTILINE) + + def _parse(self, source, encoding): + """Parse the template from text input.""" + stream = [] # list of events of the "compiled" template + dirmap = {} # temporary mapping of directives to elements + depth = 0 + + source = source.read() + if isinstance(source, str): + source = source.decode(encoding or 'utf-8', 'replace') + offset = 0 + lineno = 1 + + for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): + start, end = mo.span() + if start > offset: + text = source[offset:start] + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + lineno += len(text.splitlines()) + + text = source[start:end].lstrip()[1:] + lineno += len(text.splitlines()) + directive = text.split(None, 1) + if len(directive) > 1: + command, value = directive + else: + command, value = directive[0], None + + if command == 'end': + depth -= 1 + if depth in dirmap: + directive, start_offset = dirmap.pop(depth) + substream = stream[start_offset:] + stream[start_offset:] = [(SUB, ([directive], substream), + (self.filepath, lineno, 0))] + elif command == 'include': + pos = (self.filename, lineno, 0) + stream.append((INCLUDE, (value.strip(), None, []), pos)) + elif command != '#': + cls = self.get_directive(command) + if cls is None: + raise BadDirectiveError(command) + directive = 0, cls, value, None, (self.filepath, lineno, 0) + dirmap[depth] = (directive, len(stream)) + depth += 1 + + offset = end + + if offset < len(source): + text = source[offset:].replace('\\#', '#') + for kind, data, pos in interpolate(text, self.filepath, lineno, + lookup=self.lookup): + stream.append((kind, data, pos)) + + return stream + + +TextTemplate = OldTextTemplate |