# HG changeset patch # User hodgestar # Date 1331607782 0 # Node ID 6fc92535c88849a5f8b66fb0df028ccc71ca7c98 # Parent 51ab60299647527a218455aa42e2f36aad024614 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(). diff --git a/genshi/core.py b/genshi/core.py --- 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): diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py --- 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 diff --git a/genshi/filters/tests/transform.py b/genshi/filters/tests/transform.py --- 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'), diff --git a/genshi/filters/transform.py b/genshi/filters/transform.py --- 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 diff --git a/genshi/output.py b/genshi/output.py --- 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('' % 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. diff --git a/genshi/path.py b/genshi/path.py --- 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)) diff --git a/genshi/template/base.py b/genshi/template/base.py --- 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 diff --git a/genshi/template/directives.py b/genshi/template/directives.py --- 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 |= [ diff --git a/genshi/tests/core.py b/genshi/tests/core.py --- 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):