[Zope3-checkins] SVN: Zope3/branches/Zope-3.1/ Update TAL/METAL implementation:

Fred L. Drake, Jr. fdrake at gmail.com
Fri Aug 19 19:01:37 EDT 2005


Log message for revision 38014:
  Update TAL/METAL implementation:
  
  - move to METAL 1.1, including the macro extension feature
  
  - integrate various maintenance patches from the Zope 3 trunk intended
    to reduce the difference between the Zope 2 and Zope 3
    implementations of page templates
  
  This update merges the following revisions from the trunk:
  
  Revision 37825:
    add the FasterStringIO to replace the basic StringIO, so this matches the
    Zope 2 code and gets a tiny bit faster as well
  
  Revision 37830:
    correct a broken comment, and expand with useful information
  
  Revision 37939:
    - add a comment about a mysterious instance variable
    - remove an unused assignment statement
    - simplify an expression assignment
  
  Revision 37944:
    changed the macro stack entries to have meaningful attribute names so
    references to individual fields are easier to decipher
  
  Revision 38008:
    repair outdated comment
  
  Revision 38009:
    "surface" portion of the move to METAL 1.1: macro extension requires the
    extend-macro attribute instead of use-macro, and cannot be combined with
    use-macro
  
  Revision 38010:
    update comment based on the move to extend-macro
  
  Revision 38012:
    fix the semantics of macro extension to match the METAL 1.1 specification
  
  Revision 38013:
    note the move to METAL 1.1
  

Changed:
  U   Zope3/branches/Zope-3.1/doc/CHANGES.txt
  U   Zope3/branches/Zope-3.1/src/zope/app/ftesting.zcml
  A   Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/__init__.py
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/configure.zcml
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/inner.pt
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/intermediate.pt
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/outer.pt
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/test_nested.py
  _U  Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests/test_nested.txt
  U   Zope3/branches/Zope-3.1/src/zope/tal/taldefs.py
  U   Zope3/branches/Zope-3.1/src/zope/tal/talgenerator.py
  U   Zope3/branches/Zope-3.1/src/zope/tal/talinterpreter.py
  U   Zope3/branches/Zope-3.1/src/zope/tal/tests/input/acme_template.pt
  U   Zope3/branches/Zope-3.1/src/zope/tal/tests/input/test_metal9.html
  U   Zope3/branches/Zope-3.1/src/zope/tal/tests/output/test_metal9.html
  U   Zope3/branches/Zope-3.1/src/zope/tal/tests/test_htmltalparser.py
  U   Zope3/branches/Zope-3.1/src/zope/tal/tests/test_talinterpreter.py

-=-
Modified: Zope3/branches/Zope-3.1/doc/CHANGES.txt
===================================================================
--- Zope3/branches/Zope-3.1/doc/CHANGES.txt	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/doc/CHANGES.txt	2005-08-19 23:01:37 UTC (rev 38014)
@@ -10,6 +10,12 @@
 
     New features
 
+      - Implemented the METAL 1.1 macro language in page templates.
+        This adds the "macro extension" feature to the METAL 1.0
+        implementation included in previous versions of Zope.  The new
+        specification is available in the ZPT wiki:
+        http://www.zope.org/Wikis/DevSite/Projects/ZPT/MetalSpecification11
+
       - Change ``zope.app.intid.addIntIdSubscriber`` and
         ``.removeIntIdSubscriber`` to adapt it's ob argument to
         ``IKeyReference``. They register/unregister the object only if

Modified: Zope3/branches/Zope-3.1/src/zope/app/ftesting.zcml
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/app/ftesting.zcml	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/app/ftesting.zcml	2005-08-19 23:01:37 UTC (rev 38014)
@@ -5,5 +5,6 @@
     >
 
 <include package=".container.browser.ftests" />
+<include package=".pagetemplate.ftests" />
 
 </configure>

Copied: Zope3/branches/Zope-3.1/src/zope/app/pagetemplate/ftests (from rev 38013, Zope3/trunk/src/zope/app/pagetemplate/ftests)

Modified: Zope3/branches/Zope-3.1/src/zope/tal/taldefs.py
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/taldefs.py	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/taldefs.py	2005-08-19 23:01:37 UTC (rev 38014)
@@ -36,6 +36,7 @@
 # TODO: In Python 2.4 we can use frozenset() instead of dict.fromkeys()
 KNOWN_METAL_ATTRIBUTES = dict.fromkeys([
     "define-macro",
+    "extend-macro",
     "use-macro",
     "define-slot",
     "fill-slot",

Modified: Zope3/branches/Zope-3.1/src/zope/tal/talgenerator.py
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/talgenerator.py	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/talgenerator.py	2005-08-19 23:01:37 UTC (rev 38014)
@@ -442,12 +442,12 @@
         return None
 
     def replaceAttrs(self, attrlist, repldict):
-        # Each entry in attrlist starts like (name, value).
-        # Result is (name, value, action, expr, xlat) if there is a
+        # Each entry in attrlist starts like (name, value).  Result is
+        # (name, value, action, expr, xlat, msgid) if there is a
         # tal:attributes entry for that attribute.  Additional attrs
         # defined only by tal:attributes are added here.
         #
-        # (name, value, action, expr, xlat)
+        # (name, value, action, expr, xlat, msgid)
         if not repldict:
             return attrlist
         newlist = []
@@ -508,6 +508,7 @@
 
         todo = {}
         defineMacro = metaldict.get("define-macro")
+        extendMacro = metaldict.get("extend-macro")
         useMacro = metaldict.get("use-macro")
         defineSlot = metaldict.get("define-slot")
         fillSlot = metaldict.get("fill-slot")
@@ -537,12 +538,25 @@
             raise I18NError("i18n:data must be accompanied by i18n:translate",
                             position)
 
-        if defineMacro or useMacro:
+        if extendMacro:
+            if useMacro:
+                raise METALError(
+                    "extend-macro cannot be used with use-macro", position)
+            if not defineMacro:
+                raise METALError(
+                    "extend-macro must be used with define-macro", position)
+
+        if defineMacro or extendMacro or useMacro:
             if fillSlot or defineSlot:
                 raise METALError(
                     "define-slot and fill-slot cannot be used with "
-                    "define-macro or use-macro", position)
+                    "define-macro, extend-macro, or use-macro", position)
+            if defineMacro and useMacro:
+                raise METALError(
+                    "define-macro may not be used with use-macro", position)
 
+            useMacro = useMacro or extendMacro
+
         if content and msgid:
             raise I18NError(
                 "explicit message id and tal:content can't be used together",

Modified: Zope3/branches/Zope-3.1/src/zope/tal/talinterpreter.py
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/talinterpreter.py	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/talinterpreter.py	2005-08-19 23:01:37 UTC (rev 38014)
@@ -16,6 +16,7 @@
 $Id$
 """
 import cgi
+import operator
 import sys
 
 # Do not use cStringIO here!  It's not unicode aware. :(
@@ -87,6 +88,28 @@
         return TALGenerator.replaceAttrs(self, attrlist, repldict)
 
 
+
+class MacroStackItem(list):
+    # This is a `list` subclass for backward compability.
+    """Stack entry for the TALInterpreter.macroStack.
+
+    This offers convenience attributes for more readable access.
+
+    """
+    __slots__ = ()
+
+    # These would be nicer using @syntax, but that would require
+    # Python 2.4.x; this will do for now.
+
+    macroName = property(lambda self: self[0])
+    slots = property(lambda self: self[1])
+    definingName = property(lambda self: self[2])
+    extending = property(lambda self: self[3])
+    entering = property(lambda self: self[4],
+                        lambda self, value: operator.setitem(self, 4, value))
+    i18nContext = property(lambda self: self[5])
+
+
 class TALInterpreter(object):
     """TAL interpreter.
 
@@ -180,9 +203,11 @@
         self.html = 0
         self.endsep = "/>"
         self.endlen = len(self.endsep)
-        # macroStack contains:
-        # [(macroName, slots, definingName, extending, entering, i18ncontext)]
+        # macroStack entries are MacroStackItem instances;
+        # the entries are mutated while on the stack
         self.macroStack = []
+        # `inUseDirective` is set iff we're handling either a
+        # metal:use-macro or a metal:extend-macro
         self.inUseDirective = False
         self.position = None, None  # (lineno, offset)
         self.col = 0
@@ -194,6 +219,11 @@
         self.i18nContext = TranslationContext()
         self.sourceAnnotations = sourceAnnotations
 
+    def StringIO(self):
+        # Third-party products wishing to provide a full Unicode-aware
+        # StringIO can do so by monkey-patching this method.
+        return FasterStringIO()
+
     def saveState(self):
         return (self.position, self.col, self.stream, self._stream_stack,
                 self.scopeLevel, self.level, self.i18nContext)
@@ -222,12 +252,13 @@
         assert self.level == level
         assert self.scopeLevel == scopeLevel
 
-    def pushMacro(self, macroName, slots, definingName, extending, entering=1):
+    def pushMacro(self, macroName, slots, definingName, extending):
         if len(self.macroStack) >= self.stackLimit:
             raise METALError("macro nesting limit (%d) exceeded "
                              "by %s" % (self.stackLimit, `macroName`))
-        self.macroStack.append([macroName, slots, definingName, extending,
-                                entering, self.i18nContext])
+        self.macroStack.append(
+            MacroStackItem((macroName, slots, definingName, extending,
+                            True, self.i18nContext)))
 
     def popMacro(self):
         return self.macroStack.pop()
@@ -405,20 +436,18 @@
             # use-macro and its extensions
             if len(macs) > 1:
                 for macro in macs[1:]:
-                    extending = macro[3]
-                    if not extending:
+                    if not macro.extending:
                         return ()
-            if not macs[-1][4]:
+            if not macs[-1].entering:
                 return ()
-            # Clear 'entering' flag
-            macs[-1][4] = 0
+            macs[-1].entering = False
             # Convert or drop depth-one METAL attributes.
             i = name.rfind(":") + 1
             prefix, suffix = name[:i], name[i:]
             if suffix == "define-macro":
                 # Convert define-macro as we enter depth one.
-                useName = macs[0][0]
-                defName = macs[0][2]
+                useName = macs[0].macroName
+                defName = macs[0].definingName
                 res = []
                 if defName:
                     res.append('%sdefine-macro=%s' % (prefix, quote(defName)))
@@ -475,7 +504,7 @@
 
     def no_tag(self, start, program):
         state = self.saveState()
-        self.stream = stream = StringIO()
+        self.stream = stream = self.StringIO()
         self._stream_write = stream.write
         self.interpret(start)
         self.restoreOutputState(state)
@@ -631,7 +660,7 @@
             # evaluate the mini-program to get the value of the variable.
             state = self.saveState()
             try:
-                tmpstream = StringIO()
+                tmpstream = self.StringIO()
                 self.pushStream(tmpstream)
                 try:
                     self.interpret(program)
@@ -691,7 +720,7 @@
         # Use a temporary stream to capture the interpretation of the
         # subnodes, which should /not/ go to the output stream.
         currentTag = self._currentTag
-        tmpstream = StringIO()
+        tmpstream = self.StringIO()
         self.pushStream(tmpstream)
         try:
             self.interpret(stuff[1])
@@ -788,7 +817,7 @@
         lang, program = stuff
         # Use a temporary stream to capture the interpretation of the
         # subnodes, which should /not/ go to the output stream.
-        tmpstream = StringIO()
+        tmpstream = self.StringIO()
         self.pushStream(tmpstream)
         try:
             self.interpret(program)
@@ -833,7 +862,6 @@
     bytecode_handlers["condition"] = do_condition
 
     def do_defineMacro(self, (macroName, macro)):
-        macs = self.macroStack
         wasInUse = self.inUseDirective
         self.inUseDirective = False
         self.interpret(macro)
@@ -882,10 +910,7 @@
         # extendMacro results from a combination of define-macro and
         # use-macro.  definingName has the value of the
         # metal:define-macro attribute.
-        extending = False
-        if self.metal and self.inUseDirective:
-            # extend the calling directive.
-            extending = True
+        extending = self.metal and self.inUseDirective
         self.do_useMacro((macroName, macroExpr, compiledSlots, block),
                          definingName, extending)
     bytecode_handlers["extendMacro"] = do_extendMacro
@@ -906,7 +931,7 @@
             # Measure the extension depth of this use-macro
             depth = 1
             while depth < len_macs:
-                if macs[-depth][3]:
+                if macs[-depth].extending:
                     depth += 1
                 else:
                     break
@@ -914,13 +939,12 @@
             # most general macro.  The most general is at the top of
             # the stack.
             slot = None
-            i = len_macs - depth
-            while i < len_macs:
-                slots = macs[i][1]
-                slot = slots.get(slotName)
+            i = len_macs - 1
+            while i >= (len_macs - depth):
+                slot = macs[i].slots.get(slotName)
                 if slot is not None:
                     break
-                i += 1
+                i -= 1
             if slot is not None:
                 # Found a slot filler.  Temporarily chop the macro
                 # stack starting at the macro that filled the slot and
@@ -934,7 +958,7 @@
                     self.sourceFile = prev_source
                 # Restore the stack entries.
                 for mac in chopped:
-                    mac[4] = 0  # Not entering
+                    mac.entering = False  # Not entering
                 macs.extend(chopped)
                 return
             # Falling out of the 'if' allows the macro to be interpreted.
@@ -946,7 +970,7 @@
 
     def do_onError_tal(self, (block, handler)):
         state = self.saveState()
-        self.stream = stream = StringIO()
+        self.stream = stream = self.StringIO()
         self._stream_write = stream.write
         try:
             self.interpret(block)
@@ -980,3 +1004,26 @@
     bytecode_handlers_tal["onError"] = do_onError_tal
     bytecode_handlers_tal["<attrAction>"] = attrAction_tal
     bytecode_handlers_tal["optTag"] = do_optTag_tal
+
+
+class FasterStringIO(StringIO):
+    """Append-only version of StringIO.
+
+    This let's us have a much faster write() method.
+    """
+    def close(self):
+        if not self.closed:
+            self.write = _write_ValueError
+            StringIO.close(self)
+
+    def seek(self, pos, mode=0):
+        raise RuntimeError("FasterStringIO.seek() not allowed")
+
+    def write(self, s):
+        #assert self.pos == self.len
+        self.buflist.append(s)
+        self.len = self.pos = self.pos + len(s)
+
+
+def _write_ValueError(s):
+    raise ValueError, "I/O operation on closed file"

Modified: Zope3/branches/Zope-3.1/src/zope/tal/tests/input/acme_template.pt
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/tests/input/acme_template.pt	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/tests/input/acme_template.pt	2005-08-19 23:01:37 UTC (rev 38014)
@@ -1,6 +1,6 @@
 <!-- This is ACME's generic look and feel, which is based on
 PNOME's look and feel. -->
-<html metal:use-macro="pnome_macros_page" metal:define-macro="page">
+<html metal:extend-macro="pnome_macros_page" metal:define-macro="page">
 <head>
 <title metal:fill-slot="title">ACME Look and Feel</title>
 </head>

Modified: Zope3/branches/Zope-3.1/src/zope/tal/tests/input/test_metal9.html
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/tests/input/test_metal9.html	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/tests/input/test_metal9.html	2005-08-19 23:01:37 UTC (rev 38014)
@@ -4,7 +4,7 @@
 </span>
 </div>
 
-<div metal:define-macro="macro2" metal:use-macro="macro1" i18n:domain="zope">
+<div metal:define-macro="macro2" metal:extend-macro="macro1" i18n:domain="zope">
 <span metal:fill-slot="slot1">
 Macro 2's slot 1 decoration
 <span metal:define-slot="slot1">

Modified: Zope3/branches/Zope-3.1/src/zope/tal/tests/output/test_metal9.html
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/tests/output/test_metal9.html	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/tests/output/test_metal9.html	2005-08-19 23:01:37 UTC (rev 38014)
@@ -24,6 +24,9 @@
 
 <div metal:use-macro="macro2" i18n:domain="zope">
 <span metal:fill-slot="slot1">
+Macro 2's slot 1 decoration
+<span metal:fill-slot="slot1">
 Custom slot1
 </span>
+</span>
 </div>

Modified: Zope3/branches/Zope-3.1/src/zope/tal/tests/test_htmltalparser.py
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/tests/test_htmltalparser.py	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/tests/test_htmltalparser.py	2005-08-19 23:01:37 UTC (rev 38014)
@@ -557,6 +557,18 @@
         self._should_error("<p metal:foobar='x' />", exc)
         self._should_error("<p metal:define-macro='x'>", exc)
 
+    def test_extend_macro_errors(self):
+        exc = taldefs.METALError
+        # extend-macro requires define-macro:
+        self._should_error("<p metal:extend-macro='x'>xxx</p>", exc)
+        # extend-macro prevents use-macro:
+        self._should_error("<p metal:extend-macro='x'"
+                           "   metal:use-macro='x'"
+                           "   metal:define-macro='y'>xxx</p>", exc)
+        # use-macro doesn't co-exist with define-macro:
+        self._should_error("<p metal:use-macro='x'"
+                           "   metal:define-macro='y'>xxx</p>", exc)
+
     #
     #  I18N test cases
     #

Modified: Zope3/branches/Zope-3.1/src/zope/tal/tests/test_talinterpreter.py
===================================================================
--- Zope3/branches/Zope-3.1/src/zope/tal/tests/test_talinterpreter.py	2005-08-19 22:29:21 UTC (rev 38013)
+++ Zope3/branches/Zope-3.1/src/zope/tal/tests/test_talinterpreter.py	2005-08-19 23:01:37 UTC (rev 38014)
@@ -99,30 +99,6 @@
         f.close()
         return data
 
-    def test_acme_extends_pnome(self):
-        # ACME inc. has a document_list template that uses ACME's
-        # common look and feel.  ACME's look and feel is based on the
-        # work of PNOME, Inc., a company that creates Pretty Nice
-        # Object Management Environments for Zope.  This test verifies
-        # that document_list works as expected.
-        result = StringIO()
-        interpreter = TALInterpreter(
-            self.doclist_program, {}, self.engine, stream=result)
-        interpreter()
-        actual = result.getvalue().strip()
-        expected = self._read(('output', 'document_list.html')).strip()
-        self.assertEqual(actual, expected)
-
-    def test_acme_extends_pnome_source(self):
-        # Render METAL attributes in document_list
-        result = StringIO()
-        interpreter = TALInterpreter(
-            self.doclist_program, {}, self.engine, stream=result, tal=False)
-        interpreter()
-        actual = result.getvalue().strip()
-        expected = self._read(('output', 'document_list_source.html')).strip()
-        self.assertEqual(actual, expected)
-
     def test_preview_acme_template(self):
         # An ACME designer is previewing the ACME design.  For the
         # purposes of this use case, extending a macro should act the



More information about the Zope3-Checkins mailing list