[Zope-Checkins] CVS: Zope/lib/python/Products/PageTemplates - Expressions.py:1.34 PageTemplate.py:1.23 PageTemplateFile.py:1.13 TALES.py:1.29 ZopePageTemplate.py:1.32

Shane Hathaway shane@cvs.zope.org
Wed, 3 Apr 2002 15:44:25 -0500


Update of /cvs-repository/Zope/lib/python/Products/PageTemplates
In directory cvs.zope.org:/tmp/cvs-serv3170/lib/python/Products/PageTemplates

Modified Files:
	Expressions.py PageTemplate.py PageTemplateFile.py TALES.py 
	ZopePageTemplate.py 
Log Message:
Merged shane-better-tracebacks-branch.  The changes are explained in http://dev.zope.org/Wikis/DevSite/Proposals/BetterTracebacks

=== Zope/lib/python/Products/PageTemplates/Expressions.py 1.33 => 1.34 ===
 import re, sys
 from TALES import Engine, CompilerError, _valid_name, NAME_RE, \
-     TALESError, Undefined, Default, _parse_expr
+     Undefined, Default, _parse_expr
 from string import strip, split, join, replace, lstrip
 from Acquisition import aq_base, aq_inner, aq_parent
 
@@ -33,7 +33,6 @@
         from PathIterator import Iterator
         _engine = Engine(Iterator)
         installHandlers(_engine)
-        _engine._nocatch = (TALESError, 'Redirect')
     return _engine
 
 def installHandlers(engine):
@@ -171,7 +170,7 @@
     def _eval(self, econtext,
               isinstance=isinstance, StringType=type(''), render=render):
         for expr in self._subexprs[:-1]:
-            # Try all but the last subexpression, skipping undefined ones
+            # Try all but the last subexpression, skipping undefined ones.
             try:
                 ob = expr(econtext)
             except Undefs:
@@ -179,12 +178,8 @@
             else:
                 break
         else:
-            # On the last subexpression allow exceptions through, but
-            # wrap ones that indicate that the subexpression was undefined
-            try:
-                ob = self._subexprs[-1](econtext)
-            except Undefs[1:]:
-                raise Undefined(self._s, sys.exc_info())
+            # On the last subexpression allow exceptions through.
+            ob = self._subexprs[-1](econtext)
 
         if self._name == 'nocall' or isinstance(ob, StringType):
             return ob
@@ -234,8 +229,9 @@
         vvals = []
         for var in self._vars:
             v = var(econtext)
-            if isinstance(v, Exception):
-                raise v
+            # I hope this isn't in use anymore.
+            ## if isinstance(v, Exception):
+            ##     raise v
             vvals.append(v)
         return self._expr % tuple(vvals)
 
@@ -328,9 +324,10 @@
             if not validate(object, container, name, o):
                 raise Unauthorized, name
         else:
-            o=get(object, name, M)
+            # Try an attribute.
+            o = get(object, name, M)
             if o is not M:
-                # Check security.
+                # Check access to the attribute.
                 if has(object, 'aq_acquire'):
                     object.aq_acquire(
                         name, validate2, validate)
@@ -338,12 +335,31 @@
                     if not validate(object, object, name, o):
                         raise Unauthorized, name
             else:
+                # Try an item.
                 try:
-                    o=object[name]
-                except (AttributeError, TypeError):
-                    raise AttributeError, name
-                if not validate(object, object, name, o):
-                    raise Unauthorized, name
+                    # XXX maybe in Python 2.2 we can just check whether
+                    # the object has the attribute "__getitem__"
+                    # instead of blindly catching exceptions.
+                    o = object[name]
+                except AttributeError, exc:
+                    if str(exc).find('__getitem__') >= 0:
+                        # The object does not support the item interface.
+                        # Try to re-raise the original attribute error.
+                        # XXX I think this only happens with
+                        # ExtensionClass instances.
+                        get(object, name)
+                    raise
+                except TypeError, exc:
+                    if str(exc).find('unsubscriptable') >= 0:
+                        # The object does not support the item interface.
+                        # Try to re-raise the original attribute error.
+                        # XXX This is sooooo ugly.
+                        get(object, name)
+                    raise
+                else:
+                    # Check access to the item.
+                    if not validate(object, object, name, o):
+                        raise Unauthorized, name
         object = o
 
     return object


=== Zope/lib/python/Products/PageTemplates/PageTemplate.py 1.22 => 1.23 ===
 __version__='$Revision$'[11:-2]
 
-import os, sys, traceback, pprint
+import sys
+
 from TAL.TALParser import TALParser
 from TAL.HTMLTALParser import HTMLTALParser
 from TAL.TALGenerator import TALGenerator
@@ -26,12 +27,8 @@
 from string import join, strip, rstrip, split, replace, lower, find
 from cStringIO import StringIO
 from ExtensionClass import Base
+from ComputedAttribute import ComputedAttribute
 
-Z_DEBUG_MODE = os.environ.get('Z_DEBUG_MODE') == '1'
-
-class MacroCollection(Base):
-    def __of__(self, parent):
-        return parent.pt_macros()
 
 class PageTemplate(Base):
     "Page Templates using TAL, TALES, and METAL"
@@ -40,11 +37,16 @@
     expand = 0
     _v_errors = ()
     _v_warnings = ()
+    _v_program = None
+    _v_macros = None
+    _v_cooked = 0
     id = '(unknown)'
     _text = ''
     _error_start = '<!-- Page Template Diagnostics'
 
-    macros = MacroCollection()
+    def macros(self):
+        return self.pt_macros()
+    macros = ComputedAttribute(macros, 1)
 
     def pt_edit(self, text, content_type):
         if content_type:
@@ -72,13 +74,16 @@
     
     def pt_render(self, source=0, extra_context={}):
         """Render this Page Template"""
+        if not self._v_cooked:
+            self._cook()
+
+        __traceback_supplement__ = (PageTemplateTracebackSupplement, self)
+
         if self._v_errors:
             raise PTRuntimeError, 'Page Template %s has errors.' % self.id
         output = StringIO()
         c = self.pt_getContext()
         c.update(extra_context)
-        if Z_DEBUG_MODE:
-            __traceback_info__ = pprint.pformat(c)
 
         TALInterpreter(self._v_program, self._v_macros,
                        getEngine().getContext(c),
@@ -92,6 +97,8 @@
         return self.pt_render(extra_context={'options': kwargs})
 
     def pt_errors(self):
+        if not self._v_cooked:
+            self._cook()
         err = self._v_errors
         if err:
             return err
@@ -102,13 +109,21 @@
             return ('Macro expansion failed', '%s: %s' % sys.exc_info()[:2])
         
     def pt_warnings(self):
+        if not self._v_cooked:
+            self._cook()
         return self._v_warnings
 
     def pt_macros(self):
+        if not self._v_cooked:
+            self._cook()
         if self._v_errors:
+            __traceback_supplement__ = (PageTemplateTracebackSupplement, self)
             raise PTRuntimeError, 'Page Template %s has errors.' % self.id
         return self._v_macros
 
+    def pt_source_file(self):
+        return None  # Unknown.
+
     def write(self, text):
         assert type(text) is type('')
         if text[:len(self._error_start)] == self._error_start:
@@ -120,6 +135,8 @@
         self._cook()
 
     def read(self):
+        if not self._v_cooked:
+            self._cook()
         if not self._v_errors:
             if not self.expand:
                 return self._text
@@ -137,14 +154,14 @@
     def _cook(self):
         """Compile the TAL and METAL statments.
 
-        A Page Template must always be cooked, and cooking must not
-        fail due to user input.
+        Cooking must not fail due to compilation errors in templates.
         """
+        source_file = self.pt_source_file()
         if self.html():
-            gen = TALGenerator(getEngine(), xml=0)
+            gen = TALGenerator(getEngine(), xml=0, source_file=source_file)
             parser = HTMLTALParser(gen)
         else:
-            gen = TALGenerator(getEngine())
+            gen = TALGenerator(getEngine(), source_file=source_file)
             parser = TALParser(gen)
 
         self._v_errors = ()
@@ -155,6 +172,7 @@
             self._v_errors = ["Compilation failed",
                               "%s: %s" % sys.exc_info()[:2]]
         self._v_warnings = parser.getWarnings()
+        self._v_cooked = 1
 
     def html(self):
         if not hasattr(getattr(self, 'aq_base', self), 'is_html'):
@@ -174,3 +192,16 @@
 class PTRuntimeError(RuntimeError):
     '''The Page Template has template errors that prevent it from rendering.'''
     pass
+
+
+class PageTemplateTracebackSupplement:
+    #__implements__ = ITracebackSupplement
+
+    def __init__(self, pt):
+        self.object = pt
+        w = pt.pt_warnings()
+        e = pt.pt_errors()
+        if e:
+            w = list(w) + list(e)
+        self.warnings = w
+


=== Zope/lib/python/Products/PageTemplates/PageTemplateFile.py 1.12 => 1.13 ===
         return PageTemplate.pt_macros(self)
 
+    def pt_source_file(self):
+        """Returns a file name to be compiled into the TAL code."""
+        return self.__name__  # Don't reveal filesystem paths
+
     def _cook_check(self):
         if self._v_last_read and not DevelopmentMode:
             return
@@ -132,6 +136,6 @@
 
     __roles__ = ComputedAttribute(_get__roles__, 1)
 
-    def __setstate__(self, state):
+    def __getstate__(self):
         raise StorageError, ("Instance of AntiPersistent class %s "
                              "cannot be stored." % self.__class__.__name__)


=== Zope/lib/python/Products/PageTemplates/TALES.py 1.28 => 1.29 ===
 
 class TALESError(Exception):
-    __allow_access_to_unprotected_subobjects__ = 1
-    def __init__(self, expression, info=(None, None, None),
-                 position=(None, None)):
-        self.type, self.value, self.traceback = info
-        self.expression = expression
-        self.setPosition(position)
-    def setPosition(self, position):
-        self.lineno = position[0]
-        self.offset = position[1]
-    def takeTraceback(self):
-        t = self.traceback
-        self.traceback = None
-        return t
-    def __str__(self):
-        if self.type is None:
-            s = self.expression
-        else:
-            s = '%s on %s in %s' % (self.type, self.value,
-                                    `self.expression`)
-        if self.lineno is not None:
-            s = "%s, at line %d" % (s, self.lineno)
-        if self.offset is not None:
-            s = "%s, column %d" % (s, self.offset + 1)
-        return s
-    def __nonzero__(self):
-        return 1
+    """Error during TALES expression evaluation"""
 
 class Undefined(TALESError):
     '''Exception raised on traversal of an undefined path'''
-    def __str__(self):
-        if self.type is None:
-            s = self.expression
-        else:
-            s = '%s not found in %s' % (self.value,
-                                        `self.expression`)
-        if self.lineno is not None:
-            s = "%s, at line %d" % (s, self.lineno)
-        if self.offset is not None:
-            s = "%s, column %d" % (s, self.offset + 1)
-        return s
 
 class RegistrationError(Exception):
     '''TALES Type Registration Error'''
@@ -107,18 +71,27 @@
         self._context = context
 
     def next(self):
-        try:
-            if ZTUtils.Iterator.next(self):
-                self._context.setLocal(self.name, self.item)
-                return 1
-        except TALESError:
-            raise
-        except:
-            raise TALESError, ('repeat/%s' % self.name,
-                               sys.exc_info()), sys.exc_info()[2]
+        if ZTUtils.Iterator.next(self):
+            self._context.setLocal(self.name, self.item)
+            return 1
         return 0
 
 
+class ErrorInfo:
+    """Information about an exception passed to an on-error handler."""
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    def __init__(self, err, position=(None, None)):
+        if isinstance(err, Exception):
+            self.type = err.__class__
+            self.value = err
+        else:
+            self.type = err
+            self.value = None
+        self.lineno = position[0]
+        self.offset = position[1]
+
+
 class Engine:
     '''Expression Engine
 
@@ -181,13 +154,11 @@
     '''
 
     _context_class = SafeMapping
-    _nocatch = TALESError
     position = (None, None)
+    source_file = None
 
     def __init__(self, engine, contexts):
         self._engine = engine
-        if hasattr(engine, '_nocatch'):
-            self._nocatch = engine._nocatch
         self.contexts = contexts
         contexts['nothing'] = None
         contexts['default'] = Default
@@ -243,18 +214,10 @@
                  isinstance=isinstance, StringType=StringType):
         if isinstance(expression, StringType):
             expression = self._engine.compile(expression)
-        try:
-            v = expression(self)
-        except TALESError, err:
-            err.setPosition(self.position)
-            raise err, None, sys.exc_info()[2]
-        except self._nocatch:
-            raise
-        except:
-            raise TALESError, (`expression`, sys.exc_info(),
-                               self.position), sys.exc_info()[2]
-        else:
-            return v
+        __traceback_supplement__ = (
+            TALESTracebackSupplement, self, expression)
+        v = expression(self)
+        return v
 
     evaluateValue = evaluate
 
@@ -276,14 +239,41 @@
         return self.evaluate(expr)
     evaluateMacro = evaluate
 
-    def getTALESError(self):
-        return TALESError
+    def createErrorInfo(self, err, position):
+        return ErrorInfo(err, position)
 
     def getDefault(self):
         return Default
 
+    def setSourceFile(self, source_file):
+        self.source_file = source_file
+
     def setPosition(self, position):
         self.position = position
+
+
+
+class TALESTracebackSupplement:
+    """Implementation of ITracebackSupplement"""
+    def __init__(self, context, expression):
+        self.context = context
+        self.source_url = context.source_file
+        self.line = context.position[0]
+        self.column = context.position[1]
+        self.expression = repr(expression)
+
+    def getInfo(self, as_html=0):
+        import pprint
+        data = self.context.contexts.copy()
+        s = pprint.pformat(data)
+        if not as_html:
+            return '   - Names:\n      %s' % s.replace('\n', '\n      ')
+        else:
+            from cgi import escape
+            return '<b>Names:</b><pre>%s</pre>' % (escape(s))
+        return None
+
+
 
 class SimpleExpr:
     '''Simple example of an expression type handler'''


=== Zope/lib/python/Products/PageTemplates/ZopePageTemplate.py 1.31 => 1.32 ===
 from OFS.PropertyManager import PropertyManager
 from PageTemplate import PageTemplate
-from TALES import TALESError
 from Expressions import SecureModuleImporter
 from PageTemplateFile import PageTemplateFile
 
@@ -187,7 +186,8 @@
         try:
             self.REQUEST.RESPONSE.setHeader('content-type',
                                             self.content_type)
-        except AttributeError: pass
+        except AttributeError:
+            pass
 
         security=getSecurityManager()
         bound_names['user'] = security.getUser()
@@ -206,15 +206,7 @@
         # Execute the template in a new security context.
         security.addContext(self)
         try:
-            try:
-                result = self.pt_render(extra_context=bound_names)
-            except TALESError, err:
-                if (err.type == Unauthorized or
-                    (isinstance(Unauthorized, Exception) and
-                     isinstance(err.type, Unauthorized))):
-                    raise err.type, err.value, err.takeTraceback()
-                err.takeTraceback()
-                raise
+            result = self.pt_render(extra_context=bound_names)
             if keyset is not None:
                 # Store the result in the cache.
                 self.ZCacheable_set(result, keywords=keyset)
@@ -264,6 +256,8 @@
         """Return a list of icon URLs to be displayed by an ObjectManager"""
         icons = ({'path': 'misc_/PageTemplates/zpt.gif',
                   'alt': self.meta_type, 'title': self.meta_type},)
+        if not self._v_cooked:
+            self._cook()
         if self._v_errors:
             icons = icons + ({'path': 'misc_/PageTemplates/exclamation.gif',
                               'alt': 'Error',
@@ -271,13 +265,23 @@
         return icons
 
     def __setstate__(self, state):
+        # This is here for backward compatibility. :-(
         ZopePageTemplate.inheritedAttribute('__setstate__')(self, state)
-        self._cook()
+
+    def pt_source_file(self):
+        """Returns a file name to be compiled into the TAL code."""
+        try:
+            return '/'.join(self.getPhysicalPath())
+        except:
+            # This page template is being compiled without an
+            # acquisition context, so we don't know where it is. :-(
+            return None
 
     if not SUPPORTS_WEBDAV_LOCKS:
         def wl_isLocked(self):
             return 0
 
+
 class Src(Acquisition.Explicit):
     " "
 
@@ -295,6 +299,7 @@
 d = ZopePageTemplate.__dict__
 d['source.xml'] = d['source.html'] = Src()
 
+
 # Product registration and Add support
 manage_addPageTemplateForm = PageTemplateFile(
     'www/ptAdd', globals(), __name__='manage_addPageTemplateForm')
@@ -322,9 +327,13 @@
             
         self._setObject(id, zpt)
 
-        try: u = self.DestinationURL()
-        except: u = REQUEST['URL1']
-        if submit==" Add and Edit ": u="%s/%s" % (u,quote(id))
+        try:
+            u = self.DestinationURL()
+        except AttributeError:
+            u = REQUEST['URL1']
+
+        if submit == " Add and Edit ":
+            u = "%s/%s" % (u, quote(id))
         REQUEST.RESPONSE.redirect(u+'/manage_main')
     return ''