changeset 958:6fc92535c888 experimental-performance-improvement-exploration

Be more careful about what is passed into streams as events and remove many uses of _ensure as a result. An ATTRS event is added for handling Attributes returned by gensh.path.select().
author hodgestar
date Tue, 13 Mar 2012 03:03:02 +0000
parents 51ab60299647
children 2a45c61aedbc
files genshi/core.py genshi/filters/i18n.py genshi/filters/tests/transform.py genshi/filters/transform.py genshi/output.py genshi/path.py genshi/template/base.py genshi/template/directives.py genshi/tests/core.py
diffstat 9 files changed, 97 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -107,7 +107,7 @@
         
         Filters can be any function that accepts and produces a stream (where
         a stream is anything that iterates over events):
-        
+
         >>> def uppercase(stream):
         ...     for kind, data, pos in stream:
         ...         if kind is TEXT:
@@ -130,7 +130,13 @@
         :return: the filtered stream
         :rtype: `Stream`
         """
-        return Stream(_ensure(function(self)), serializer=self.serializer)
+        # TODO: this is horribly slow because is has to guess whether
+        #       the function passed in is something that produces stream
+        #       events or something that produces a sequence of strings.
+        #       Sequences of strings are converted back to a sequence of
+        #       stream events (and then back to text when rendered).
+        events = _possible_text_iterator_to_stream(function(self))
+        return Stream(events, serializer=self.serializer)
 
     def filter(self, *filters):
         """Apply filters to the stream.
@@ -242,7 +248,7 @@
         from genshi.output import get_serializer
         if method is None:
             method = self.serializer or 'xml'
-        return get_serializer(method, **kwargs)(_ensure(self))
+        return get_serializer(method, **kwargs)(self)
 
     def __str__(self):
         return self.render()
@@ -267,26 +273,30 @@
 COMMENT = Stream.COMMENT
 
 
-def _ensure(stream):
-    """Ensure that every item on the stream is actually a markup event."""
-    stream = iter(stream)
-    event = stream.next()
+def _text_event(text):
+    return (TEXT, unicode(text), (None, -1, -1))
+
+
+def _text_to_stream(text):
+    yield _text_event(text)
+
+
+def _possible_text_iterator_to_stream(textiter_or_stream):
+    it = iter(textiter_or_stream)
+    event = it.next()
 
     # Check whether the iterable is a real markup event stream by examining the
     # first item it yields; if it's not we'll need to do some conversion
-    if type(event) is not tuple or len(event) != 3:
-        for event in chain([event], stream):
-            if hasattr(event, 'totuple'):
-                event = event.totuple()
-            else:
-                event = TEXT, unicode(event), (None, -1, -1)
-            yield event
+    if type(event) is not tuple:
+        yield TEXT, unicode(event), (None, -1, -1)
+        for event in it:
+            yield TEXT, unicode(event), (None, -1, -1)
         return
 
     # This looks like a markup event stream, so we'll just pass it through
     # unchanged
     yield event
-    for event in stream:
+    for event in it:
         yield event
 
 
@@ -344,6 +354,8 @@
     """
     __slots__ = []
 
+    ATTRS = StreamEventKind('ATTRS')
+
     def __contains__(self, name):
         """Return whether the list includes an attribute with the specified
         name.
@@ -427,19 +439,33 @@
                 return value
         return default
 
-    def totuple(self):
+    def toevent(self):
         """Return the attributes as a markup event.
         
-        The returned event is a `TEXT` event, the data is the value of all
-        attributes joined together.
+        The returned event is an `ATTRS` event, the data is the Attr object.
+
+        >>> a = Attrs([('href', '#'), ('title', 'Foo')])
+        >>> a.toevent()
+        ('ATTRS', Attrs([('href', '#'), ('title', 'Foo')]), (None, -1, -1))
         
-        >>> Attrs([('href', '#'), ('title', 'Foo')]).totuple()
-        ('TEXT', '#Foo', (None, -1, -1))
-        
-        :return: a `TEXT` event
+        :return: an `ATTR` event
         :rtype: `tuple`
         """
-        return TEXT, ''.join([x[1] for x in self]), (None, -1, -1)
+        return self.ATTRS, self, (None, -1, -1)
+
+    def concatenate_values(self):
+        """Return the values of the attributes concatenated into a string.
+
+        >>> a = Attrs([('href', '#'), ('title', 'Foo')])
+        >>> a.concatenate_values()
+        '#Foo'
+
+        :return: the concatenated attribute values
+        :rtype: `str`
+        """
+        return ''.join([x[1] for x in self])
+
+ATTRS = Attrs.ATTRS
 
 
 class Markup(unicode):
--- a/genshi/filters/i18n.py
+++ b/genshi/filters/i18n.py
@@ -28,7 +28,8 @@
 from types import FunctionType
 
 from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
-                        XML_NAMESPACE, _ensure, StreamEventKind
+                        XML_NAMESPACE, _text_to_stream, \
+                        StreamEventKind
 from genshi.template.eval import _ast
 from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
 from genshi.template.directives import Directive, StripDirective
@@ -717,7 +718,7 @@
                             newval = gettext(value)
                     else:
                         newval = list(
-                            self(_ensure(value), ctxt, translate_text=False)
+                            self(value, ctxt, translate_text=False)
                         )
                     if newval != value:
                         value = newval
@@ -916,13 +917,19 @@
 
     def _extract_attrs(self, event, gettext_functions, search_text):
         for name, value in event[1][1]:
-            if search_text and isinstance(value, basestring):
-                if name in self.include_attrs:
-                    text = value.strip()
-                    if text:
-                        yield event[2][1], None, text, []
+            if isinstance(value, basestring):
+                if search_text:
+                    if name in self.include_attrs:
+                        text = value.strip()
+                        if text:
+                            yield event[2][1], None, text, []
+                else:
+                    for message in self.extract(_text_to_stream(value),
+                                                gettext_functions,
+                                                search_text=False):
+                        yield message
             else:
-                for message in self.extract(_ensure(value), gettext_functions,
+                for message in self.extract(value, gettext_functions,
                                             search_text=False):
                     yield message
 
--- a/genshi/filters/tests/transform.py
+++ b/genshi/filters/tests/transform.py
@@ -17,7 +17,7 @@
 
 from genshi import HTML
 from genshi.builder import Element
-from genshi.core import START, END, TEXT, QName, Attrs
+from genshi.core import START, END, TEXT, QName, Attrs, _text_event
 from genshi.filters.transform import Transformer, StreamBuffer, ENTER, EXIT, \
                                      OUTSIDE, INSIDE, ATTR, BREAK
 import genshi.filters.transform
@@ -737,7 +737,7 @@
         count = [0]
         def content():
             count[0] += 1
-            yield '%2i.' % count[0]
+            yield _text_event('%2i.' % count[0])
         self.assertEqual(
             self._apply('*', content),
             [(None, START, u'root'),
@@ -841,7 +841,7 @@
         count = [0]
         def content():
             count[0] += 1
-            yield '%2i.' % count[0]
+            yield _text_event('%2i.' % count[0])
         self.assertEqual(
             self._apply('foo/text()', content),
             [(None, 'START', u'root'),
@@ -950,7 +950,7 @@
         count = [0]
         def content():
             count[0] += 1
-            yield '%2i.' % count[0]
+            yield _text_event('%2i.' % count[0])
         self.assertEqual(
             self._apply('foo/text()', content),
             [(None, 'START', u'root'),
@@ -1056,7 +1056,7 @@
         count = [0]
         def content():
             count[0] += 1
-            yield '%2i.' % count[0]
+            yield _text_event('%2i.' % count[0])
         self.assertEqual(
             self._apply('foo', content),
             [(None, 'START', u'root'),
@@ -1162,7 +1162,7 @@
         count = [0]
         def content():
             count[0] += 1
-            yield '%2i.' % count[0]
+            yield _text_event('%2i.' % count[0])
         self.assertEqual(
             self._apply('foo', content),
             [(None, 'START', u'root'),
--- a/genshi/filters/transform.py
+++ b/genshi/filters/transform.py
@@ -52,7 +52,8 @@
 import sys
 
 from genshi.builder import Element
-from genshi.core import Stream, Attrs, QName, TEXT, START, END, _ensure, Markup
+from genshi.core import Stream, Attrs, QName, TEXT, START, END, \
+                        _text_to_stream, Markup
 from genshi.path import Path
 
 __all__ = ['Transformer', 'StreamBuffer', 'InjectorTransformation', 'ENTER',
@@ -1060,7 +1061,9 @@
         content = self.content
         if hasattr(content, '__call__'):
             content = content()
-        for event in _ensure(content):
+        if isinstance(content, basestring):
+            content = _text_to_stream(content)
+        for event in content:
             yield None, event
 
 
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -20,7 +20,7 @@
 
 from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
 from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
-                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
+                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE, ATTRS
 
 __all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer',
            'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
@@ -301,6 +301,12 @@
             elif kind is PI:
                 yield _emit(kind, data, Markup('<?%s %s?>' % data))
 
+            elif kind is ATTRS:
+                # this is specifically to support the rendering of
+                # streams generated by genshi.path.select() and provides
+                # backwards compatibility with genshi < 0.7
+                yield data.concatenate_values()
+
 
 class XHTMLSerializer(XMLSerializer):
     """Produces XHTML text from an event stream.
--- a/genshi/path.py
+++ b/genshi/path.py
@@ -593,6 +593,10 @@
                             yield subevent
                             test(subevent, ns, vs, updateonly=True)
                 elif result:
+                    # check for Attrs and wrap them in an ATTRS event
+                    # if found
+                    if hasattr(result, 'toevent'):
+                        result = result.toevent()
                     yield result
         return Stream(_generate(),
                       serializer=getattr(stream, 'serializer', None))
--- a/genshi/template/base.py
+++ b/genshi/template/base.py
@@ -18,7 +18,8 @@
 import sys
 
 from genshi.compat import StringIO, BytesIO
-from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
+from genshi.core import (Attrs, Stream, StreamEventKind, START, TEXT,
+                         _possible_text_iterator_to_stream)
 from genshi.input import ParseError
 
 __all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
@@ -586,7 +587,7 @@
                             yield TEXT, number_conv(result), pos
                         elif hasattr(result, '__iter__'):
                             push(stream)
-                            stream = _ensure(result)
+                            stream = _possible_text_iterator_to_stream(result)
                             break
                         else:
                             yield TEXT, unicode(result), pos
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -13,7 +13,7 @@
 
 """Implementation of the various template directives."""
 
-from genshi.core import QName, Stream
+from genshi.core import QName, Stream, ATTRS
 from genshi.path import Path
 from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
                                  EXPR, _apply_directives, _eval_expr
@@ -183,9 +183,11 @@
             if attrs:
                 if isinstance(attrs, Stream):
                     try:
-                        attrs = iter(attrs).next()
+                        attr_kind, attr_data, _pos = iter(attrs).next()
                     except StopIteration:
-                        attrs = []
+                        attr_kind, attr_data = ATTRS, {}
+                    assert attr_kind == ATTRS
+                    attrs = attr_data
                 elif not isinstance(attrs, list): # assume it's a dict
                     attrs = attrs.items()
                 attrib |= [
--- a/genshi/tests/core.py
+++ b/genshi/tests/core.py
@@ -179,8 +179,9 @@
                           repr(unpickled))
 
     def test_non_ascii(self):
-        attrs_tuple = Attrs([("attr1", u"föö"), ("attr2", u"bär")]).totuple()
-        self.assertEqual(u'fööbär', attrs_tuple[1])
+        attrs = Attrs([("attr1", u"föö"), ("attr2", u"bär")])
+        value = attrs.concatenate_values()
+        self.assertEqual(u'fööbär', value)
 
 
 class NamespaceTestCase(unittest.TestCase):
Copyright (C) 2012-2017 Edgewall Software