Code

Fixed a backlog of bug reports, and worked on python 2.3 compatibility:
[roundup.git] / roundup / cgi / PageTemplates / TALES.py
1 ##############################################################################
2 #
3 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
4
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE
11
12 ##############################################################################
13 """TALES
15 An implementation of a generic TALES engine
17 Modified for Roundup 0.5 release:
19 - changed imports to import from roundup.cgi
20 """
22 __version__='$Revision: 1.5 $'[11:-2]
24 import re, sys
25 from roundup.cgi import ZTUtils
26 from MultiMapping import MultiMapping
28 StringType = type('')
30 NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
31 _parse_expr = re.compile(r"(%s):" % NAME_RE).match
32 _valid_name = re.compile('%s$' % NAME_RE).match
34 class TALESError(Exception):
35     """Error during TALES expression evaluation"""
37 class Undefined(TALESError):
38     '''Exception raised on traversal of an undefined path'''
40 class RegistrationError(Exception):
41     '''TALES Type Registration Error'''
43 class CompilerError(Exception):
44     '''TALES Compiler Error'''
46 class Default:
47     '''Retain Default'''
48 Default = Default()
50 _marker = []
52 class SafeMapping(MultiMapping):
53     '''Mapping with security declarations and limited method exposure.
55     Since it subclasses MultiMapping, this class can be used to wrap
56     one or more mapping objects.  Restricted Python code will not be
57     able to mutate the SafeMapping or the wrapped mappings, but will be
58     able to read any value.
59     '''
60     __allow_access_to_unprotected_subobjects__ = 1
61     push = pop = None
63     _push = MultiMapping.push
64     _pop = MultiMapping.pop
66     def has_get(self, key, _marker=[]):
67         v = self.get(key, _marker)
68         return v is not _marker, v
70 class Iterator(ZTUtils.Iterator):
71     def __init__(self, name, seq, context):
72         ZTUtils.Iterator.__init__(self, seq)
73         self.name = name
74         self._context = context
76     def next(self):
77         if ZTUtils.Iterator.next(self):
78             self._context.setLocal(self.name, self.item)
79             return 1
80         return 0
83 class ErrorInfo:
84     """Information about an exception passed to an on-error handler."""
85     __allow_access_to_unprotected_subobjects__ = 1
87     def __init__(self, err, position=(None, None)):
88         if isinstance(err, Exception):
89             self.type = err.__class__
90             self.value = err
91         else:
92             self.type = err
93             self.value = None
94         self.lineno = position[0]
95         self.offset = position[1]
98 class Engine:
99     '''Expression Engine
101     An instance of this class keeps a mutable collection of expression
102     type handlers.  It can compile expression strings by delegating to
103     these handlers.  It can provide an expression Context, which is
104     capable of holding state and evaluating compiled expressions.
105     '''
106     Iterator = Iterator
108     def __init__(self, Iterator=None):
109         self.types = {}
110         if Iterator is not None:
111             self.Iterator = Iterator
113     def registerType(self, name, handler):
114         if not _valid_name(name):
115             raise RegistrationError, 'Invalid Expression type "%s".' % name
116         types = self.types
117         if types.has_key(name):
118             raise RegistrationError, (
119                 'Multiple registrations for Expression type "%s".' %
120                 name)
121         types[name] = handler
123     def getTypes(self):
124         return self.types
126     def compile(self, expression):
127         m = _parse_expr(expression)
128         if m:
129             type = m.group(1)
130             expr = expression[m.end():]
131         else:
132             type = "standard"
133             expr = expression
134         try:
135             handler = self.types[type]
136         except KeyError:
137             raise CompilerError, (
138                 'Unrecognized expression type "%s".' % type)
139         return handler(type, expr, self)
140     
141     def getContext(self, contexts=None, **kwcontexts):
142         if contexts is not None:
143             if kwcontexts:
144                 kwcontexts.update(contexts)
145             else:
146                 kwcontexts = contexts
147         return Context(self, kwcontexts)
149     def getCompilerError(self):
150         return CompilerError
152 class Context:
153     '''Expression Context
155     An instance of this class holds context information that it can
156     use to evaluate compiled expressions.
157     '''
159     _context_class = SafeMapping
160     position = (None, None)
161     source_file = None
163     def __init__(self, compiler, contexts):
164         self._compiler = compiler
165         self.contexts = contexts
166         contexts['nothing'] = None
167         contexts['default'] = Default
169         self.repeat_vars = rv = {}
170         # Wrap this, as it is visible to restricted code
171         contexts['repeat'] = rep =  self._context_class(rv)
172         contexts['loop'] = rep # alias
174         self.global_vars = gv = contexts.copy()
175         self.local_vars = lv = {}
176         self.vars = self._context_class(gv, lv)
178         # Keep track of what needs to be popped as each scope ends.
179         self._scope_stack = []
181     def getCompiler(self):
182         return self._compiler
184     def beginScope(self):
185         self._scope_stack.append([self.local_vars.copy()])
187     def endScope(self):
188         scope = self._scope_stack.pop()
189         self.local_vars = lv = scope[0]
190         v = self.vars
191         v._pop()
192         v._push(lv)
193         # Pop repeat variables, if any
194         i = len(scope) - 1
195         while i:
196             name, value = scope[i]
197             if value is None:
198                 del self.repeat_vars[name]
199             else:
200                 self.repeat_vars[name] = value
201             i = i - 1
203     def setLocal(self, name, value):
204         self.local_vars[name] = value
206     def setGlobal(self, name, value):
207         self.global_vars[name] = value
209     def setRepeat(self, name, expr):
210         expr = self.evaluate(expr)
211         if not expr:
212             return self._compiler.Iterator(name, (), self)
213         it = self._compiler.Iterator(name, expr, self)
214         old_value = self.repeat_vars.get(name)
215         self._scope_stack[-1].append((name, old_value))
216         self.repeat_vars[name] = it
217         return it
219     def evaluate(self, expression,
220                  isinstance=isinstance, StringType=StringType):
221         if isinstance(expression, StringType):
222             expression = self._compiler.compile(expression)
223         __traceback_supplement__ = (
224             TALESTracebackSupplement, self, expression)
225         v = expression(self)
226         return v
228     evaluateValue = evaluate
229     evaluateBoolean = evaluate
231     def evaluateText(self, expr):
232         text = self.evaluate(expr)
233         if text is Default or text is None:
234             return text
235         return str(text)
237     def evaluateStructure(self, expr):
238         return self.evaluate(expr)
239     evaluateStructure = evaluate
241     def evaluateMacro(self, expr):
242         # XXX Should return None or a macro definition
243         return self.evaluate(expr)
244     evaluateMacro = evaluate
246     def createErrorInfo(self, err, position):
247         return ErrorInfo(err, position)
249     def getDefault(self):
250         return Default
252     def setSourceFile(self, source_file):
253         self.source_file = source_file
255     def setPosition(self, position):
256         self.position = position
260 class TALESTracebackSupplement:
261     """Implementation of ITracebackSupplement"""
262     def __init__(self, context, expression):
263         self.context = context
264         self.source_url = context.source_file
265         self.line = context.position[0]
266         self.column = context.position[1]
267         self.expression = repr(expression)
269     def getInfo(self, as_html=0):
270         import pprint
271         data = self.context.contexts.copy()
272         s = pprint.pformat(data)
273         if not as_html:
274             return '   - Names:\n      %s' % string.replace(s, '\n', '\n      ')
275         else:
276             from cgi import escape
277             return '<b>Names:</b><pre>%s</pre>' % (escape(s))
278         return None
282 class SimpleExpr:
283     '''Simple example of an expression type handler'''
284     def __init__(self, name, expr, engine):
285         self._name = name
286         self._expr = expr
287     def __call__(self, econtext):
288         return self._name, self._expr
289     def __repr__(self):
290         return '<SimpleExpr %s %s>' % (self._name, `self._expr`)