Edgewall Software

Changeset 868


Ignore:
Timestamp:
Jun 9, 2008, 8:39:46 AM (15 years ago)
Author:
athomas
Message:

Lots of Transformer cleanup:

  • Content-insertion transformations (before, after, etc.) now accept a callable.
  • .prepend() now only operates on elements. Previously it also operated on OUTSIDE marked events.
  • Where it makes sense, transformations are now consistently applied to individually selected objects in the document, rather than on any contiguous selection. This means that adjacent selected elements will be treated individually rather than as a whole.
  • Transformations should now consistently work on the context node.
  • .substitute() now defaults to a count of 0 (ie. all) rather than 1. This is to be consistent with Python's regex substitution.
  • ATTR events now have a kind of ATTR in addition to having this as their mark.
  • Added the BREAK mark. This allows cuts of otherwise seamlessly joined objects to be operated on.
  • Added a full test suite.
Location:
trunk/genshi/filters
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/genshi/filters/tests/transform.py

    r784 r868  
    1313
    1414import doctest
     15from pprint import pprint
    1516import unittest
    1617
     18from genshi import HTML
     19from genshi.builder import Element
     20from genshi.core import START, END, TEXT, QName, Attrs
     21from genshi.filters.transform import Transformer, StreamBuffer, ENTER, EXIT, \
     22                                     OUTSIDE, INSIDE, ATTR, BREAK
    1723import genshi.filters.transform
     24
     25
     26FOO = '<root>ROOT<foo name="foo">FOO</foo></root>'
     27FOOBAR = '<root>ROOT<foo name="foo" size="100">FOO</foo><bar name="bar">BAR</bar></root>'
     28
     29
     30def _simplify(stream, with_attrs=False):
     31    """Simplify a marked stream."""
     32    def _generate():
     33        for mark, (kind, data, pos) in stream:
     34            if kind is START:
     35                if with_attrs:
     36                    data = (unicode(data[0]), dict((unicode(k), v)
     37                                                   for k, v in data[1]))
     38                else:
     39                    data = unicode(data[0])
     40            elif kind is END:
     41                data = unicode(data)
     42            elif kind is ATTR:
     43                kind = ATTR
     44                data = dict((unicode(k), v) for k, v in data[1])
     45            yield mark, kind, data
     46    return list(_generate())
     47
     48
     49def _transform(html, transformer, with_attrs=False):
     50    """Apply transformation returning simplified marked stream."""
     51    if isinstance(html, basestring):
     52        html = HTML(html)
     53    stream = transformer(html, keep_marks=True)
     54    return _simplify(stream, with_attrs)
     55
     56
     57class SelectTest(unittest.TestCase):
     58    """Test .select()"""
     59    def _select(self, select):
     60        html = HTML(FOOBAR)
     61        if isinstance(select, basestring):
     62            select = [select]
     63        transformer = Transformer(select[0])
     64        for sel in select[1:]:
     65            transformer = transformer.select(sel)
     66        return _transform(html, transformer)
     67
     68    def test_select_single_element(self):
     69        self.assertEqual(
     70            self._select('foo'),
     71            [(None, START, u'root'),
     72             (None, TEXT, u'ROOT'),
     73             (ENTER, START, u'foo'),
     74             (INSIDE, TEXT, u'FOO'),
     75             (EXIT, END, u'foo'),
     76             (None, START, u'bar'),
     77             (None, TEXT, u'BAR'),
     78             (None, END, u'bar'),
     79             (None, END, u'root')],
     80            )
     81
     82    def test_select_context(self):
     83        self.assertEqual(
     84            self._select('.'),
     85            [(ENTER, START, u'root'),
     86             (INSIDE, TEXT, u'ROOT'),
     87             (INSIDE, START, u'foo'),
     88             (INSIDE, TEXT, u'FOO'),
     89             (INSIDE, END, u'foo'),
     90             (INSIDE, START, u'bar'),
     91             (INSIDE, TEXT, u'BAR'),
     92             (INSIDE, END, u'bar'),
     93             (EXIT, END, u'root')]
     94            )
     95
     96    def test_select_inside_select(self):
     97        self.assertEqual(
     98            self._select(['.', 'foo']),
     99            [(None, START, u'root'),
     100             (None, TEXT, u'ROOT'),
     101             (ENTER, START, u'foo'),
     102             (INSIDE, TEXT, u'FOO'),
     103             (EXIT, END, u'foo'),
     104             (None, START, u'bar'),
     105             (None, TEXT, u'BAR'),
     106             (None, END, u'bar'),
     107             (None, END, u'root')],
     108            )
     109
     110    def test_select_text(self):
     111        self.assertEqual(
     112            self._select('*/text()'),
     113            [(None, START, u'root'),
     114             (None, TEXT, u'ROOT'),
     115             (None, START, u'foo'),
     116             (OUTSIDE, TEXT, u'FOO'),
     117             (None, END, u'foo'),
     118             (None, START, u'bar'),
     119             (OUTSIDE, TEXT, u'BAR'),
     120             (None, END, u'bar'),
     121             (None, END, u'root')],
     122            )
     123
     124    def test_select_attr(self):
     125        self.assertEqual(
     126            self._select('foo/@name'),
     127            [(None, START, u'root'),
     128             (None, TEXT, u'ROOT'),
     129             (ATTR, ATTR, {'name': u'foo'}),
     130             (None, START, u'foo'),
     131             (None, TEXT, u'FOO'),
     132             (None, END, u'foo'),
     133             (None, START, u'bar'),
     134             (None, TEXT, u'BAR'),
     135             (None, END, u'bar'),
     136             (None, END, u'root')]
     137            )
     138
     139    def test_select_text_context(self):
     140        self.assertEqual(
     141            list(Transformer('.')(HTML('foo'), keep_marks=True)),
     142            [('OUTSIDE', ('TEXT', u'foo', (None, 1, 0)))],
     143            )
     144
     145
     146class InvertTest(unittest.TestCase):
     147    def _invert(self, select):
     148        return _transform(FOO, Transformer(select).invert())
     149
     150    def test_invert_element(self):
     151        self.assertEqual(
     152            self._invert('foo'),
     153            [(OUTSIDE, START, u'root'),
     154             (OUTSIDE, TEXT, u'ROOT'),
     155             (None, START, u'foo'),
     156             (None, TEXT, u'FOO'),
     157             (None, END, u'foo'),
     158             (OUTSIDE, END, u'root')]
     159            )
     160
     161    def test_invert_inverted_element(self):
     162        self.assertEqual(
     163            _transform(FOO, Transformer('foo').invert().invert()),
     164            [(None, START, u'root'),
     165             (None, TEXT, u'ROOT'),
     166             (OUTSIDE, START, u'foo'),
     167             (OUTSIDE, TEXT, u'FOO'),
     168             (OUTSIDE, END, u'foo'),
     169             (None, END, u'root')]
     170            )
     171
     172    def test_invert_text(self):
     173        self.assertEqual(
     174            self._invert('foo/text()'),
     175            [(OUTSIDE, START, u'root'),
     176             (OUTSIDE, TEXT, u'ROOT'),
     177             (OUTSIDE, START, u'foo'),
     178             (None, TEXT, u'FOO'),
     179             (OUTSIDE, END, u'foo'),
     180             (OUTSIDE, END, u'root')]
     181            )
     182
     183    def test_invert_attribute(self):
     184        self.assertEqual(
     185            self._invert('foo/@name'),
     186            [(OUTSIDE, START, u'root'),
     187             (OUTSIDE, TEXT, u'ROOT'),
     188             (None, ATTR, {'name': u'foo'}),
     189             (OUTSIDE, START, u'foo'),
     190             (OUTSIDE, TEXT, u'FOO'),
     191             (OUTSIDE, END, u'foo'),
     192             (OUTSIDE, END, u'root')]
     193            )
     194
     195    def test_invert_context(self):
     196        self.assertEqual(
     197            self._invert('.'),
     198            [(None, START, u'root'),
     199             (None, TEXT, u'ROOT'),
     200             (None, START, u'foo'),
     201             (None, TEXT, u'FOO'),
     202             (None, END, u'foo'),
     203             (None, END, u'root')]
     204            )
     205
     206    def test_invert_text_context(self):
     207        self.assertEqual(
     208            _simplify(Transformer('.').invert()(HTML('foo'), keep_marks=True)),
     209            [(None, 'TEXT', u'foo')],
     210            )
     211
     212
     213
     214class EndTest(unittest.TestCase):
     215    def test_end(self):
     216        stream = _transform(FOO, Transformer('foo').end())
     217        self.assertEqual(
     218            stream,
     219            [(OUTSIDE, START, u'root'),
     220             (OUTSIDE, TEXT, u'ROOT'),
     221             (OUTSIDE, START, u'foo'),
     222             (OUTSIDE, TEXT, u'FOO'),
     223             (OUTSIDE, END, u'foo'),
     224             (OUTSIDE, END, u'root')]
     225            )
     226
     227
     228class EmptyTest(unittest.TestCase):
     229    def _empty(self, select):
     230        return _transform(FOO, Transformer(select).empty())
     231
     232    def test_empty_element(self):
     233        self.assertEqual(
     234            self._empty('foo'),
     235            [(None, START, u'root'),
     236             (None, TEXT, u'ROOT'),
     237             (ENTER, START, u'foo'),
     238             (EXIT, END, u'foo'),
     239             (None, END, u'root')],
     240            )
     241
     242    def test_empty_text(self):
     243        self.assertEqual(
     244            self._empty('foo/text()'),
     245            [(None, START, u'root'),
     246             (None, TEXT, u'ROOT'),
     247             (None, START, u'foo'),
     248             (OUTSIDE, TEXT, u'FOO'),
     249             (None, END, u'foo'),
     250             (None, END, u'root')]
     251            )
     252
     253    def test_empty_attr(self):
     254        self.assertEqual(
     255            self._empty('foo/@name'),
     256            [(None, START, u'root'),
     257             (None, TEXT, u'ROOT'),
     258             (ATTR, ATTR, {'name': u'foo'}),
     259             (None, START, u'foo'),
     260             (None, TEXT, u'FOO'),
     261             (None, END, u'foo'),
     262             (None, END, u'root')]
     263            )
     264
     265    def test_empty_context(self):
     266        self.assertEqual(
     267            self._empty('.'),
     268            [(ENTER, START, u'root'),
     269             (EXIT, END, u'root')]
     270            )
     271
     272    def test_empty_text_context(self):
     273        self.assertEqual(
     274            _simplify(Transformer('.')(HTML('foo'), keep_marks=True)),
     275            [(OUTSIDE, TEXT, u'foo')],
     276            )
     277
     278
     279class RemoveTest(unittest.TestCase):
     280    def _remove(self, select):
     281        return _transform(FOO, Transformer(select).remove())
     282
     283    def test_remove_element(self):
     284        self.assertEqual(
     285            self._remove('foo|bar'),
     286            [(None, START, u'root'),
     287             (None, TEXT, u'ROOT'),
     288             (None, END, u'root')]
     289            )
     290
     291    def test_remove_text(self):
     292        self.assertEqual(
     293            self._remove('//text()'),
     294            [(None, START, u'root'),
     295             (None, START, u'foo'),
     296             (None, END, u'foo'),
     297             (None, END, u'root')]
     298            )
     299
     300    def test_remove_attr(self):
     301        self.assertEqual(
     302            self._remove('foo/@name'),
     303            [(None, START, u'root'),
     304             (None, TEXT, u'ROOT'),
     305             (None, START, u'foo'),
     306             (None, TEXT, u'FOO'),
     307             (None, END, u'foo'),
     308             (None, END, u'root')]
     309            )
     310
     311    def test_remove_context(self):
     312        self.assertEqual(
     313            self._remove('.'),
     314            [],
     315            )
     316
     317    def test_remove_text_context(self):
     318        self.assertEqual(
     319            _transform('foo', Transformer('.').remove()),
     320            [],
     321            )
     322
     323
     324class UnwrapText(unittest.TestCase):
     325    def _unwrap(self, select):
     326        return _transform(FOO, Transformer(select).unwrap())
     327
     328    def test_unwrap_element(self):
     329        self.assertEqual(
     330            self._unwrap('foo'),
     331            [(None, START, u'root'),
     332             (None, TEXT, u'ROOT'),
     333             (INSIDE, TEXT, u'FOO'),
     334             (None, END, u'root')]
     335            )
     336
     337    def test_unwrap_text(self):
     338        self.assertEqual(
     339            self._unwrap('foo/text()'),
     340            [(None, START, u'root'),
     341             (None, TEXT, u'ROOT'),
     342             (None, START, u'foo'),
     343             (OUTSIDE, TEXT, u'FOO'),
     344             (None, END, u'foo'),
     345             (None, END, u'root')]
     346            )
     347
     348    def test_unwrap_attr(self):
     349        self.assertEqual(
     350            self._unwrap('foo/@name'),
     351            [(None, START, u'root'),
     352             (None, TEXT, u'ROOT'),
     353             (ATTR, ATTR, {'name': u'foo'}),
     354             (None, START, u'foo'),
     355             (None, TEXT, u'FOO'),
     356             (None, END, u'foo'),
     357             (None, END, u'root')]
     358            )
     359
     360    def test_unwrap_adjacent(self):
     361        self.assertEqual(
     362            _transform(FOOBAR, Transformer('foo|bar').unwrap()),
     363            [(None, START, u'root'),
     364             (None, TEXT, u'ROOT'),
     365             (INSIDE, TEXT, u'FOO'),
     366             (INSIDE, TEXT, u'BAR'),
     367             (None, END, u'root')]
     368            )
     369
     370    def test_unwrap_root(self):
     371        self.assertEqual(
     372            self._unwrap('.'),
     373            [(INSIDE, TEXT, u'ROOT'),
     374             (INSIDE, START, u'foo'),
     375             (INSIDE, TEXT, u'FOO'),
     376             (INSIDE, END, u'foo')]
     377            )
     378
     379    def test_unwrap_text_root(self):
     380        self.assertEqual(
     381            _transform('foo', Transformer('.').unwrap()),
     382            [(OUTSIDE, TEXT, 'foo')],
     383            )
     384
     385
     386class WrapTest(unittest.TestCase):
     387    def _wrap(self, select, wrap='wrap'):
     388        return _transform(FOO, Transformer(select).wrap(wrap))
     389
     390    def test_wrap_element(self):
     391        self.assertEqual(
     392            self._wrap('foo'),
     393            [(None, START, u'root'),
     394             (None, TEXT, u'ROOT'),
     395             (None, START, u'wrap'),
     396             (ENTER, START, u'foo'),
     397             (INSIDE, TEXT, u'FOO'),
     398             (EXIT, END, u'foo'),
     399             (None, END, u'wrap'),
     400             (None, END, u'root')]
     401            )
     402
     403    def test_wrap_adjacent_elements(self):
     404        self.assertEqual(
     405            _transform(FOOBAR, Transformer('foo|bar').wrap('wrap')),
     406            [(None, START, u'root'),
     407             (None, TEXT, u'ROOT'),
     408             (None, START, u'wrap'),
     409             (ENTER, START, u'foo'),
     410             (INSIDE, TEXT, u'FOO'),
     411             (EXIT, END, u'foo'),
     412             (None, END, u'wrap'),
     413             (None, START, u'wrap'),
     414             (ENTER, START, u'bar'),
     415             (INSIDE, TEXT, u'BAR'),
     416             (EXIT, END, u'bar'),
     417             (None, END, u'wrap'),
     418             (None, END, u'root')]
     419            )
     420
     421    def test_wrap_text(self):
     422        self.assertEqual(
     423            self._wrap('foo/text()'),
     424            [(None, START, u'root'),
     425             (None, TEXT, u'ROOT'),
     426             (None, START, u'foo'),
     427             (None, START, u'wrap'),
     428             (OUTSIDE, TEXT, u'FOO'),
     429             (None, END, u'wrap'),
     430             (None, END, u'foo'),
     431             (None, END, u'root')]
     432            )
     433
     434    def test_wrap_root(self):
     435        self.assertEqual(
     436            self._wrap('.'),
     437            [(None, START, u'wrap'),
     438             (ENTER, START, u'root'),
     439             (INSIDE, TEXT, u'ROOT'),
     440             (INSIDE, START, u'foo'),
     441             (INSIDE, TEXT, u'FOO'),
     442             (INSIDE, END, u'foo'),
     443             (EXIT, END, u'root'),
     444             (None, END, u'wrap')]
     445            )
     446
     447    def test_wrap_text_root(self):
     448        self.assertEqual(
     449            _transform('foo', Transformer('.').wrap('wrap')),
     450            [(None, START, u'wrap'),
     451             (OUTSIDE, TEXT, u'foo'),
     452             (None, END, u'wrap')],
     453            )
     454
     455    def test_wrap_with_element(self):
     456        element = Element('a', href='http://localhost')
     457        self.assertEqual(
     458            _transform('foo', Transformer('.').wrap(element), with_attrs=True),
     459            [(None, START, (u'a', {u'href': u'http://localhost'})),
     460             (OUTSIDE, TEXT, u'foo'),
     461             (None, END, u'a')]
     462            )
     463
     464
     465class FilterTest(unittest.TestCase):
     466    def _filter(self, select, html=FOOBAR):
     467        """Returns a list of lists of filtered elements."""
     468        output = []
     469        def filtered(stream):
     470            interval = []
     471            output.append(interval)
     472            for event in stream:
     473                interval.append(event)
     474                yield event
     475        _transform(html, Transformer(select).filter(filtered))
     476        simplified = []
     477        for sub in output:
     478            simplified.append(_simplify([(None, event) for event in sub]))
     479        return simplified
     480
     481    def test_filter_element(self):
     482        self.assertEqual(
     483            self._filter('foo'),
     484            [[(None, START, u'foo'),
     485              (None, TEXT, u'FOO'),
     486              (None, END, u'foo')]]
     487            )
     488
     489    def test_filter_adjacent_elements(self):
     490        self.assertEqual(
     491            self._filter('foo|bar'),
     492            [[(None, START, u'foo'),
     493              (None, TEXT, u'FOO'),
     494              (None, END, u'foo')],
     495             [(None, START, u'bar'),
     496              (None, TEXT, u'BAR'),
     497              (None, END, u'bar')]]
     498            )
     499
     500    def test_filter_text(self):
     501        self.assertEqual(
     502            self._filter('*/text()'),
     503            [[(None, TEXT, u'FOO')],
     504             [(None, TEXT, u'BAR')]]
     505            )
     506    def test_filter_root(self):
     507        self.assertEqual(
     508            self._filter('.'),
     509            [[(None, START, u'root'),
     510              (None, TEXT, u'ROOT'),
     511              (None, START, u'foo'),
     512              (None, TEXT, u'FOO'),
     513              (None, END, u'foo'),
     514              (None, START, u'bar'),
     515              (None, TEXT, u'BAR'),
     516              (None, END, u'bar'),
     517              (None, END, u'root')]]
     518            )
     519
     520    def test_filter_text_root(self):
     521        self.assertEqual(
     522            self._filter('.', 'foo'),
     523            [[(None, TEXT, u'foo')]])
     524
     525
     526class MapTest(unittest.TestCase):
     527    def _map(self, select, kind=None):
     528        data = []
     529        def record(d):
     530            data.append(d)
     531            return d
     532        _transform(FOOBAR, Transformer(select).map(record, kind))
     533        return data
     534
     535    def test_map_element(self):
     536        self.assertEqual(
     537            self._map('foo'),
     538            [(QName(u'foo'), Attrs([(QName(u'name'), u'foo'),
     539                                    (QName(u'size'), u'100')])),
     540             u'FOO',
     541             QName(u'foo')]
     542            )
     543
     544    def test_map_with_text_kind(self):
     545        self.assertEqual(
     546            self._map('.', TEXT),
     547            [u'ROOT', u'FOO', u'BAR']
     548            )
     549
     550    def test_map_with_root_and_end_kind(self):
     551        self.assertEqual(
     552            self._map('.', END),
     553            [QName(u'foo'), QName(u'bar'), QName(u'root')]
     554            )
     555
     556    def test_map_with_attribute(self):
     557        self.assertEqual(
     558            self._map('foo/@name'),
     559            [(QName(u'foo@*'), Attrs([('name', u'foo')]))]
     560            )
     561
     562
     563class SubstituteTest(unittest.TestCase):
     564    def _substitute(self, select, pattern, replace):
     565        return _transform(FOOBAR, Transformer(select).substitute(pattern, replace))
     566
     567    def test_substitute_foo(self):
     568        self.assertEqual(
     569            self._substitute('foo', 'FOO|BAR', 'FOOOOO'),
     570            [(None, START, u'root'),
     571             (None, TEXT, u'ROOT'),
     572             (ENTER, START, u'foo'),
     573             (INSIDE, TEXT, u'FOOOOO'),
     574             (EXIT, END, u'foo'),
     575             (None, START, u'bar'),
     576             (None, TEXT, u'BAR'),
     577             (None, END, u'bar'),
     578             (None, END, u'root')]
     579            )
     580
     581    def test_substitute_foobar_with_group(self):
     582        self.assertEqual(
     583            self._substitute('foo|bar', '(FOO|BAR)', r'(\1)'),
     584            [(None, START, u'root'),
     585             (None, TEXT, u'ROOT'),
     586             (ENTER, START, u'foo'),
     587             (INSIDE, TEXT, u'(FOO)'),
     588             (EXIT, END, u'foo'),
     589             (ENTER, START, u'bar'),
     590             (INSIDE, TEXT, u'(BAR)'),
     591             (EXIT, END, u'bar'),
     592             (None, END, u'root')]
     593            )
     594
     595
     596class RenameTest(unittest.TestCase):
     597    def _rename(self, select):
     598        return _transform(FOOBAR, Transformer(select).rename('foobar'))
     599
     600    def test_rename_root(self):
     601        self.assertEqual(
     602            self._rename('.'),
     603            [(ENTER, START, u'foobar'),
     604             (INSIDE, TEXT, u'ROOT'),
     605             (INSIDE, START, u'foo'),
     606             (INSIDE, TEXT, u'FOO'),
     607             (INSIDE, END, u'foo'),
     608             (INSIDE, START, u'bar'),
     609             (INSIDE, TEXT, u'BAR'),
     610             (INSIDE, END, u'bar'),
     611             (EXIT, END, u'foobar')]
     612            )
     613
     614    def test_rename_element(self):
     615        self.assertEqual(
     616            self._rename('foo|bar'),
     617            [(None, START, u'root'),
     618             (None, TEXT, u'ROOT'),
     619             (ENTER, START, u'foobar'),
     620             (INSIDE, TEXT, u'FOO'),
     621             (EXIT, END, u'foobar'),
     622             (ENTER, START, u'foobar'),
     623             (INSIDE, TEXT, u'BAR'),
     624             (EXIT, END, u'foobar'),
     625             (None, END, u'root')]
     626            )
     627
     628    def test_rename_text(self):
     629        self.assertEqual(
     630            self._rename('foo/text()'),
     631            [(None, START, u'root'),
     632             (None, TEXT, u'ROOT'),
     633             (None, START, u'foo'),
     634             (OUTSIDE, TEXT, u'FOO'),
     635             (None, END, u'foo'),
     636             (None, START, u'bar'),
     637             (None, TEXT, u'BAR'),
     638             (None, END, u'bar'),
     639             (None, END, u'root')]
     640            )
     641
     642
     643class ContentTestMixin(object):
     644    def _apply(self, select, content=None, html=FOOBAR):
     645        class Injector(object):
     646            count = 0
     647
     648            def __iter__(self):
     649                self.count += 1
     650                return iter(HTML('CONTENT %i' % self.count))
     651
     652        if isinstance(html, basestring):
     653            html = HTML(html)
     654        if content is None:
     655            content = Injector()
     656        elif isinstance(content, basestring):
     657            content = HTML(content)
     658        return _transform(html, getattr(Transformer(select), self.operation)
     659                                (content))
     660
     661
     662class ReplaceTest(unittest.TestCase, ContentTestMixin):
     663    operation = 'replace'
     664
     665    def test_replace_element(self):
     666        self.assertEqual(
     667            self._apply('foo'),
     668            [(None, START, u'root'),
     669             (None, TEXT, u'ROOT'),
     670             (None, TEXT, u'CONTENT 1'),
     671             (None, START, u'bar'),
     672             (None, TEXT, u'BAR'),
     673             (None, END, u'bar'),
     674             (None, END, u'root')]
     675            )
     676
     677    def test_replace_text(self):
     678        self.assertEqual(
     679            self._apply('text()'),
     680            [(None, START, u'root'),
     681             (None, TEXT, u'CONTENT 1'),
     682             (None, START, u'foo'),
     683             (None, TEXT, u'FOO'),
     684             (None, END, u'foo'),
     685             (None, START, u'bar'),
     686             (None, TEXT, u'BAR'),
     687             (None, END, u'bar'),
     688             (None, END, u'root')]
     689            )
     690
     691    def test_replace_context(self):
     692        self.assertEqual(
     693            self._apply('.'),
     694            [(None, TEXT, u'CONTENT 1')],
     695            )
     696
     697    def test_replace_text_context(self):
     698        self.assertEqual(
     699            self._apply('.', html='foo'),
     700            [(None, TEXT, u'CONTENT 1')],
     701            )
     702
     703    def test_replace_adjacent_elements(self):
     704        self.assertEqual(
     705            self._apply('*'),
     706            [(None, START, u'root'),
     707             (None, TEXT, u'ROOT'),
     708             (None, TEXT, u'CONTENT 1'),
     709             (None, TEXT, u'CONTENT 2'),
     710             (None, END, u'root')],
     711            )
     712
     713    def test_replace_all(self):
     714        self.assertEqual(
     715            self._apply('*|text()'),
     716            [(None, START, u'root'),
     717             (None, TEXT, u'CONTENT 1'),
     718             (None, TEXT, u'CONTENT 2'),
     719             (None, TEXT, u'CONTENT 3'),
     720             (None, END, u'root')],
     721            )
     722
     723    def test_replace_with_callback(self):
     724        count = [0]
     725        def content():
     726            count[0] += 1
     727            yield '%2i.' % count[0]
     728        self.assertEqual(
     729            self._apply('*', content),
     730            [(None, START, u'root'),
     731             (None, TEXT, u'ROOT'),
     732             (None, TEXT, u' 1.'),
     733             (None, TEXT, u' 2.'),
     734             (None, END, u'root')]
     735            )
     736
     737
     738class BeforeTest(unittest.TestCase, ContentTestMixin):
     739    operation = 'before'
     740
     741    def test_before_element(self):
     742        self.assertEqual(
     743            self._apply('foo'),
     744            [(None, START, u'root'),
     745             (None, TEXT, u'ROOT'),
     746             (None, TEXT, u'CONTENT 1'),
     747             (ENTER, START, u'foo'),
     748             (INSIDE, TEXT, u'FOO'),
     749             (EXIT, END, u'foo'),
     750             (None, START, u'bar'),
     751             (None, TEXT, u'BAR'),
     752             (None, END, u'bar'),
     753             (None, END, u'root')]
     754            )
     755
     756    def test_before_text(self):
     757        self.assertEqual(
     758            self._apply('text()'),
     759            [(None, START, u'root'),
     760             (None, TEXT, u'CONTENT 1'),
     761             (OUTSIDE, TEXT, u'ROOT'),
     762             (None, START, u'foo'),
     763             (None, TEXT, u'FOO'),
     764             (None, END, u'foo'),
     765             (None, START, u'bar'),
     766             (None, TEXT, u'BAR'),
     767             (None, END, u'bar'),
     768             (None, END, u'root')]
     769            )
     770
     771    def test_before_context(self):
     772        self.assertEqual(
     773            self._apply('.'),
     774            [(None, TEXT, u'CONTENT 1'),
     775             (ENTER, START, u'root'),
     776             (INSIDE, TEXT, u'ROOT'),
     777             (INSIDE, START, u'foo'),
     778             (INSIDE, TEXT, u'FOO'),
     779             (INSIDE, END, u'foo'),
     780             (INSIDE, START, u'bar'),
     781             (INSIDE, TEXT, u'BAR'),
     782             (INSIDE, END, u'bar'),
     783             (EXIT, END, u'root')]
     784            )
     785
     786    def test_before_text_context(self):
     787        self.assertEqual(
     788            self._apply('.', html='foo'),
     789            [(None, TEXT, u'CONTENT 1'),
     790             (OUTSIDE, TEXT, u'foo')]
     791            )
     792
     793    def test_before_adjacent_elements(self):
     794        self.assertEqual(
     795            self._apply('*'),
     796            [(None, START, u'root'),
     797             (None, TEXT, u'ROOT'),
     798             (None, TEXT, u'CONTENT 1'),
     799             (ENTER, START, u'foo'),
     800             (INSIDE, TEXT, u'FOO'),
     801             (EXIT, END, u'foo'),
     802             (None, TEXT, u'CONTENT 2'),
     803             (ENTER, START, u'bar'),
     804             (INSIDE, TEXT, u'BAR'),
     805             (EXIT, END, u'bar'),
     806             (None, END, u'root')]
     807
     808            )
     809
     810    def test_before_all(self):
     811        self.assertEqual(
     812            self._apply('*|text()'),
     813            [(None, START, u'root'),
     814             (None, TEXT, u'CONTENT 1'),
     815             (OUTSIDE, TEXT, u'ROOT'),
     816             (None, TEXT, u'CONTENT 2'),
     817             (ENTER, START, u'foo'),
     818             (INSIDE, TEXT, u'FOO'),
     819             (EXIT, END, u'foo'),
     820             (None, TEXT, u'CONTENT 3'),
     821             (ENTER, START, u'bar'),
     822             (INSIDE, TEXT, u'BAR'),
     823             (EXIT, END, u'bar'),
     824             (None, END, u'root')]
     825            )
     826
     827    def test_before_with_callback(self):
     828        count = [0]
     829        def content():
     830            count[0] += 1
     831            yield '%2i.' % count[0]
     832        self.assertEqual(
     833            self._apply('foo/text()', content),
     834            [(None, 'START', u'root'),
     835             (None, 'TEXT', u'ROOT'),
     836             (None, 'START', u'foo'),
     837             (None, 'TEXT', u' 1.'),
     838             ('OUTSIDE', 'TEXT', u'FOO'),
     839             (None, 'END', u'foo'),
     840             (None, 'START', u'bar'),
     841             (None, 'TEXT', u'BAR'),
     842             (None, 'END', u'bar'),
     843             (None, 'END', u'root')]
     844            )
     845
     846
     847class AfterTest(unittest.TestCase, ContentTestMixin):
     848    operation = 'after'
     849
     850    def test_after_element(self):
     851        self.assertEqual(
     852            self._apply('foo'),
     853            [(None, START, u'root'),
     854             (None, TEXT, u'ROOT'),
     855             (ENTER, START, u'foo'),
     856             (INSIDE, TEXT, u'FOO'),
     857             (EXIT, END, u'foo'),
     858             (None, TEXT, u'CONTENT 1'),
     859             (None, START, u'bar'),
     860             (None, TEXT, u'BAR'),
     861             (None, END, u'bar'),
     862             (None, END, u'root')]
     863            )
     864
     865    def test_after_text(self):
     866        self.assertEqual(
     867            self._apply('text()'),
     868            [(None, START, u'root'),
     869             (OUTSIDE, TEXT, u'ROOT'),
     870             (None, TEXT, u'CONTENT 1'),
     871             (None, START, u'foo'),
     872             (None, TEXT, u'FOO'),
     873             (None, END, u'foo'),
     874             (None, START, u'bar'),
     875             (None, TEXT, u'BAR'),
     876             (None, END, u'bar'),
     877             (None, END, u'root')]
     878            )
     879
     880    def test_after_context(self):
     881        self.assertEqual(
     882            self._apply('.'),
     883            [(ENTER, START, u'root'),
     884             (INSIDE, TEXT, u'ROOT'),
     885             (INSIDE, START, u'foo'),
     886             (INSIDE, TEXT, u'FOO'),
     887             (INSIDE, END, u'foo'),
     888             (INSIDE, START, u'bar'),
     889             (INSIDE, TEXT, u'BAR'),
     890             (INSIDE, END, u'bar'),
     891             (EXIT, END, u'root'),
     892             (None, TEXT, u'CONTENT 1')]
     893            )
     894
     895    def test_after_text_context(self):
     896        self.assertEqual(
     897            self._apply('.', html='foo'),
     898            [(OUTSIDE, TEXT, u'foo'),
     899             (None, TEXT, u'CONTENT 1')]
     900            )
     901
     902    def test_after_adjacent_elements(self):
     903        self.assertEqual(
     904            self._apply('*'),
     905            [(None, START, u'root'),
     906             (None, TEXT, u'ROOT'),
     907             (ENTER, START, u'foo'),
     908             (INSIDE, TEXT, u'FOO'),
     909             (EXIT, END, u'foo'),
     910             (None, TEXT, u'CONTENT 1'),
     911             (ENTER, START, u'bar'),
     912             (INSIDE, TEXT, u'BAR'),
     913             (EXIT, END, u'bar'),
     914             (None, TEXT, u'CONTENT 2'),
     915             (None, END, u'root')]
     916
     917            )
     918
     919    def test_after_all(self):
     920        self.assertEqual(
     921            self._apply('*|text()'),
     922            [(None, START, u'root'),
     923             (OUTSIDE, TEXT, u'ROOT'),
     924             (None, TEXT, u'CONTENT 1'),
     925             (ENTER, START, u'foo'),
     926             (INSIDE, TEXT, u'FOO'),
     927             (EXIT, END, u'foo'),
     928             (None, TEXT, u'CONTENT 2'),
     929             (ENTER, START, u'bar'),
     930             (INSIDE, TEXT, u'BAR'),
     931             (EXIT, END, u'bar'),
     932             (None, TEXT, u'CONTENT 3'),
     933             (None, END, u'root')]
     934            )
     935
     936    def test_after_with_callback(self):
     937        count = [0]
     938        def content():
     939            count[0] += 1
     940            yield '%2i.' % count[0]
     941        self.assertEqual(
     942            self._apply('foo/text()', content),
     943            [(None, 'START', u'root'),
     944             (None, 'TEXT', u'ROOT'),
     945             (None, 'START', u'foo'),
     946             ('OUTSIDE', 'TEXT', u'FOO'),
     947             (None, 'TEXT', u' 1.'),
     948             (None, 'END', u'foo'),
     949             (None, 'START', u'bar'),
     950             (None, 'TEXT', u'BAR'),
     951             (None, 'END', u'bar'),
     952             (None, 'END', u'root')]
     953            )
     954
     955
     956class PrependTest(unittest.TestCase, ContentTestMixin):
     957    operation = 'prepend'
     958
     959    def test_prepend_element(self):
     960        self.assertEqual(
     961            self._apply('foo'),
     962            [(None, START, u'root'),
     963             (None, TEXT, u'ROOT'),
     964             (ENTER, START, u'foo'),
     965             (None, TEXT, u'CONTENT 1'),
     966             (INSIDE, TEXT, u'FOO'),
     967             (EXIT, END, u'foo'),
     968             (None, START, u'bar'),
     969             (None, TEXT, u'BAR'),
     970             (None, END, u'bar'),
     971             (None, END, u'root')]
     972            )
     973
     974    def test_prepend_text(self):
     975        self.assertEqual(
     976            self._apply('text()'),
     977            [(None, START, u'root'),
     978             (OUTSIDE, TEXT, u'ROOT'),
     979             (None, START, u'foo'),
     980             (None, TEXT, u'FOO'),
     981             (None, END, u'foo'),
     982             (None, START, u'bar'),
     983             (None, TEXT, u'BAR'),
     984             (None, END, u'bar'),
     985             (None, END, u'root')]
     986            )
     987
     988    def test_prepend_context(self):
     989        self.assertEqual(
     990            self._apply('.'),
     991            [(ENTER, START, u'root'),
     992             (None, TEXT, u'CONTENT 1'),
     993             (INSIDE, TEXT, u'ROOT'),
     994             (INSIDE, START, u'foo'),
     995             (INSIDE, TEXT, u'FOO'),
     996             (INSIDE, END, u'foo'),
     997             (INSIDE, START, u'bar'),
     998             (INSIDE, TEXT, u'BAR'),
     999             (INSIDE, END, u'bar'),
     1000             (EXIT, END, u'root')],
     1001            )
     1002
     1003    def test_prepend_text_context(self):
     1004        self.assertEqual(
     1005            self._apply('.', html='foo'),
     1006            [(OUTSIDE, TEXT, u'foo')]
     1007            )
     1008
     1009    def test_prepend_adjacent_elements(self):
     1010        self.assertEqual(
     1011            self._apply('*'),
     1012            [(None, START, u'root'),
     1013             (None, TEXT, u'ROOT'),
     1014             (ENTER, START, u'foo'),
     1015             (None, TEXT, u'CONTENT 1'),
     1016             (INSIDE, TEXT, u'FOO'),
     1017             (EXIT, END, u'foo'),
     1018             (ENTER, START, u'bar'),
     1019             (None, TEXT, u'CONTENT 2'),
     1020             (INSIDE, TEXT, u'BAR'),
     1021             (EXIT, END, u'bar'),
     1022             (None, END, u'root')]
     1023
     1024            )
     1025
     1026    def test_prepend_all(self):
     1027        self.assertEqual(
     1028            self._apply('*|text()'),
     1029            [(None, START, u'root'),
     1030             (OUTSIDE, TEXT, u'ROOT'),
     1031             (ENTER, START, u'foo'),
     1032             (None, TEXT, u'CONTENT 1'),
     1033             (INSIDE, TEXT, u'FOO'),
     1034             (EXIT, END, u'foo'),
     1035             (ENTER, START, u'bar'),
     1036             (None, TEXT, u'CONTENT 2'),
     1037             (INSIDE, TEXT, u'BAR'),
     1038             (EXIT, END, u'bar'),
     1039             (None, END, u'root')]
     1040            )
     1041
     1042    def test_prepend_with_callback(self):
     1043        count = [0]
     1044        def content():
     1045            count[0] += 1
     1046            yield '%2i.' % count[0]
     1047        self.assertEqual(
     1048            self._apply('foo', content),
     1049            [(None, 'START', u'root'),
     1050             (None, 'TEXT', u'ROOT'),
     1051             (ENTER, 'START', u'foo'),
     1052             (None, 'TEXT', u' 1.'),
     1053             (INSIDE, 'TEXT', u'FOO'),
     1054             (EXIT, 'END', u'foo'),
     1055             (None, 'START', u'bar'),
     1056             (None, 'TEXT', u'BAR'),
     1057             (None, 'END', u'bar'),
     1058             (None, 'END', u'root')]
     1059            )
     1060
     1061
     1062class AppendTest(unittest.TestCase, ContentTestMixin):
     1063    operation = 'append'
     1064
     1065    def test_append_element(self):
     1066        self.assertEqual(
     1067            self._apply('foo'),
     1068            [(None, START, u'root'),
     1069             (None, TEXT, u'ROOT'),
     1070             (ENTER, START, u'foo'),
     1071             (INSIDE, TEXT, u'FOO'),
     1072             (None, TEXT, u'CONTENT 1'),
     1073             (EXIT, END, u'foo'),
     1074             (None, START, u'bar'),
     1075             (None, TEXT, u'BAR'),
     1076             (None, END, u'bar'),
     1077             (None, END, u'root')]
     1078            )
     1079
     1080    def test_append_text(self):
     1081        self.assertEqual(
     1082            self._apply('text()'),
     1083            [(None, START, u'root'),
     1084             (OUTSIDE, TEXT, u'ROOT'),
     1085             (None, START, u'foo'),
     1086             (None, TEXT, u'FOO'),
     1087             (None, END, u'foo'),
     1088             (None, START, u'bar'),
     1089             (None, TEXT, u'BAR'),
     1090             (None, END, u'bar'),
     1091             (None, END, u'root')]
     1092            )
     1093
     1094    def test_append_context(self):
     1095        self.assertEqual(
     1096            self._apply('.'),
     1097            [(ENTER, START, u'root'),
     1098             (INSIDE, TEXT, u'ROOT'),
     1099             (INSIDE, START, u'foo'),
     1100             (INSIDE, TEXT, u'FOO'),
     1101             (INSIDE, END, u'foo'),
     1102             (INSIDE, START, u'bar'),
     1103             (INSIDE, TEXT, u'BAR'),
     1104             (INSIDE, END, u'bar'),
     1105             (None, TEXT, u'CONTENT 1'),
     1106             (EXIT, END, u'root')],
     1107            )
     1108
     1109    def test_append_text_context(self):
     1110        self.assertEqual(
     1111            self._apply('.', html='foo'),
     1112            [(OUTSIDE, TEXT, u'foo')]
     1113            )
     1114
     1115    def test_append_adjacent_elements(self):
     1116        self.assertEqual(
     1117            self._apply('*'),
     1118            [(None, START, u'root'),
     1119             (None, TEXT, u'ROOT'),
     1120             (ENTER, START, u'foo'),
     1121             (INSIDE, TEXT, u'FOO'),
     1122             (None, TEXT, u'CONTENT 1'),
     1123             (EXIT, END, u'foo'),
     1124             (ENTER, START, u'bar'),
     1125             (INSIDE, TEXT, u'BAR'),
     1126             (None, TEXT, u'CONTENT 2'),
     1127             (EXIT, END, u'bar'),
     1128             (None, END, u'root')]
     1129
     1130            )
     1131
     1132    def test_append_all(self):
     1133        self.assertEqual(
     1134            self._apply('*|text()'),
     1135            [(None, START, u'root'),
     1136             (OUTSIDE, TEXT, u'ROOT'),
     1137             (ENTER, START, u'foo'),
     1138             (INSIDE, TEXT, u'FOO'),
     1139             (None, TEXT, u'CONTENT 1'),
     1140             (EXIT, END, u'foo'),
     1141             (ENTER, START, u'bar'),
     1142             (INSIDE, TEXT, u'BAR'),
     1143             (None, TEXT, u'CONTENT 2'),
     1144             (EXIT, END, u'bar'),
     1145             (None, END, u'root')]
     1146            )
     1147
     1148    def test_append_with_callback(self):
     1149        count = [0]
     1150        def content():
     1151            count[0] += 1
     1152            yield '%2i.' % count[0]
     1153        self.assertEqual(
     1154            self._apply('foo', content),
     1155            [(None, 'START', u'root'),
     1156             (None, 'TEXT', u'ROOT'),
     1157             (ENTER, 'START', u'foo'),
     1158             (INSIDE, 'TEXT', u'FOO'),
     1159             (None, 'TEXT', u' 1.'),
     1160             (EXIT, 'END', u'foo'),
     1161             (None, 'START', u'bar'),
     1162             (None, 'TEXT', u'BAR'),
     1163             (None, 'END', u'bar'),
     1164             (None, 'END', u'root')]
     1165            )
     1166
     1167
     1168
     1169class AttrTest(unittest.TestCase):
     1170    def _attr(self, select, name, value):
     1171        return _transform(FOOBAR, Transformer(select).attr(name, value),
     1172                          with_attrs=True)
     1173
     1174    def test_set_existing_attr(self):
     1175        self.assertEqual(
     1176            self._attr('foo', 'name', 'FOO'),
     1177            [(None, START, (u'root', {})),
     1178             (None, TEXT, u'ROOT'),
     1179             (ENTER, START, (u'foo', {u'name': 'FOO', u'size': '100'})),
     1180             (INSIDE, TEXT, u'FOO'),
     1181             (EXIT, END, u'foo'),
     1182             (None, START, (u'bar', {u'name': u'bar'})),
     1183             (None, TEXT, u'BAR'),
     1184             (None, END, u'bar'),
     1185             (None, END, u'root')]
     1186            )
     1187
     1188    def test_set_new_attr(self):
     1189        self.assertEqual(
     1190            self._attr('foo', 'title', 'FOO'),
     1191            [(None, START, (u'root', {})),
     1192             (None, TEXT, u'ROOT'),
     1193             (ENTER, START, (u'foo', {u'name': u'foo', u'title': 'FOO', u'size': '100'})),
     1194             (INSIDE, TEXT, u'FOO'),
     1195             (EXIT, END, u'foo'),
     1196             (None, START, (u'bar', {u'name': u'bar'})),
     1197             (None, TEXT, u'BAR'),
     1198             (None, END, u'bar'),
     1199             (None, END, u'root')]
     1200            )
     1201
     1202    def test_attr_from_function(self):
     1203        def set(name, event):
     1204            self.assertEqual(name, 'name')
     1205            return event[1][1].get('name').upper()
     1206
     1207        self.assertEqual(
     1208            self._attr('foo|bar', 'name', set),
     1209            [(None, START, (u'root', {})),
     1210             (None, TEXT, u'ROOT'),
     1211             (ENTER, START, (u'foo', {u'name': 'FOO', u'size': '100'})),
     1212             (INSIDE, TEXT, u'FOO'),
     1213             (EXIT, END, u'foo'),
     1214             (ENTER, START, (u'bar', {u'name': 'BAR'})),
     1215             (INSIDE, TEXT, u'BAR'),
     1216             (EXIT, END, u'bar'),
     1217             (None, END, u'root')]
     1218            )
     1219
     1220    def test_remove_attr(self):
     1221        self.assertEqual(
     1222            self._attr('foo', 'name', None),
     1223            [(None, START, (u'root', {})),
     1224             (None, TEXT, u'ROOT'),
     1225             (ENTER, START, (u'foo', {u'size': '100'})),
     1226             (INSIDE, TEXT, u'FOO'),
     1227             (EXIT, END, u'foo'),
     1228             (None, START, (u'bar', {u'name': u'bar'})),
     1229             (None, TEXT, u'BAR'),
     1230             (None, END, u'bar'),
     1231             (None, END, u'root')]
     1232            )
     1233
     1234    def test_remove_attr_with_function(self):
     1235        def set(name, event):
     1236            return None
     1237
     1238        self.assertEqual(
     1239            self._attr('foo', 'name', set),
     1240            [(None, START, (u'root', {})),
     1241             (None, TEXT, u'ROOT'),
     1242             (ENTER, START, (u'foo', {u'size': '100'})),
     1243             (INSIDE, TEXT, u'FOO'),
     1244             (EXIT, END, u'foo'),
     1245             (None, START, (u'bar', {u'name': u'bar'})),
     1246             (None, TEXT, u'BAR'),
     1247             (None, END, u'bar'),
     1248             (None, END, u'root')]
     1249            )
     1250
     1251
     1252class BufferTestMixin(object):
     1253    def _apply(self, select, with_attrs=False):
     1254        buffer = StreamBuffer()
     1255        events = buffer.events
     1256
     1257        class Trace(object):
     1258            last = None
     1259            trace = []
     1260
     1261            def __call__(self, stream):
     1262                for event in stream:
     1263                    if events and hash(tuple(events)) != self.last:
     1264                        self.last = hash(tuple(events))
     1265                        self.trace.append(list(events))
     1266                    yield event
     1267
     1268        trace = Trace()
     1269        output = _transform(FOOBAR, getattr(Transformer(select), self.operation)
     1270                                    (buffer).apply(trace), with_attrs=with_attrs)
     1271        simplified = []
     1272        for interval in trace.trace:
     1273            simplified.append(_simplify([(None, e) for e in interval],
     1274                                         with_attrs=with_attrs))
     1275        return output, simplified
     1276
     1277
     1278class CopyTest(unittest.TestCase, BufferTestMixin):
     1279    operation = 'copy'
     1280
     1281    def test_copy_element(self):
     1282        self.assertEqual(
     1283            self._apply('foo')[1],
     1284            [[(None, START, u'foo'),
     1285              (None, TEXT, u'FOO'),
     1286              (None, END, u'foo')]]
     1287            )
     1288
     1289    def test_copy_adjacent_elements(self):
     1290        self.assertEqual(
     1291            self._apply('foo|bar')[1],
     1292            [[(None, START, u'foo'),
     1293              (None, TEXT, u'FOO'),
     1294              (None, END, u'foo')],
     1295             [(None, START, u'bar'),
     1296              (None, TEXT, u'BAR'),
     1297              (None, END, u'bar')]]
     1298            )
     1299
     1300    def test_copy_all(self):
     1301        self.assertEqual(
     1302            self._apply('*|text()')[1],
     1303            [[(None, TEXT, u'ROOT')],
     1304             [(None, START, u'foo'),
     1305              (None, TEXT, u'FOO'),
     1306              (None, END, u'foo')],
     1307             [(None, START, u'bar'),
     1308              (None, TEXT, u'BAR'),
     1309              (None, END, u'bar')]]
     1310            )
     1311
     1312    def test_copy_text(self):
     1313        self.assertEqual(
     1314            self._apply('*/text()')[1],
     1315            [[(None, TEXT, u'FOO')],
     1316             [(None, TEXT, u'BAR')]]
     1317            )
     1318
     1319    def test_copy_context(self):
     1320        self.assertEqual(
     1321            self._apply('.')[1],
     1322            [[(None, START, u'root'),
     1323              (None, TEXT, u'ROOT'),
     1324              (None, START, u'foo'),
     1325              (None, TEXT, u'FOO'),
     1326              (None, END, u'foo'),
     1327              (None, START, u'bar'),
     1328              (None, TEXT, u'BAR'),
     1329              (None, END, u'bar'),
     1330              (None, END, u'root')]]
     1331            )
     1332
     1333    def test_copy_attribute(self):
     1334        self.assertEqual(
     1335            self._apply('foo/@name', with_attrs=True)[1],
     1336            [[(None, ATTR, {'name': u'foo'})]]
     1337            )
     1338
     1339    def test_copy_attributes(self):
     1340        self.assertEqual(
     1341            self._apply('foo/@*', with_attrs=True)[1],
     1342            [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]]
     1343            )
     1344
     1345
     1346class CutTest(unittest.TestCase, BufferTestMixin):
     1347    operation = 'cut'
     1348
     1349    def test_cut_element(self):
     1350        self.assertEqual(
     1351            self._apply('foo'),
     1352            ([(None, START, u'root'),
     1353              (None, TEXT, u'ROOT'),
     1354              (None, START, u'bar'),
     1355              (None, TEXT, u'BAR'),
     1356              (None, END, u'bar'),
     1357              (None, END, u'root')],
     1358             [[(None, START, u'foo'),
     1359               (None, TEXT, u'FOO'),
     1360               (None, END, u'foo')]])
     1361            )
     1362
     1363    def test_cut_adjacent_elements(self):
     1364        self.assertEqual(
     1365            self._apply('foo|bar'),
     1366            ([(None, START, u'root'),
     1367              (None, TEXT, u'ROOT'),
     1368              (BREAK, BREAK, None),
     1369              (None, END, u'root')],
     1370             [[(None, START, u'foo'),
     1371               (None, TEXT, u'FOO'),
     1372               (None, END, u'foo')],
     1373              [(None, START, u'bar'),
     1374               (None, TEXT, u'BAR'),
     1375               (None, END, u'bar')]])
     1376            )
     1377
     1378    def test_cut_all(self):
     1379        self.assertEqual(
     1380            self._apply('*|text()'),
     1381            ([(None, 'START', u'root'),
     1382              ('BREAK', 'BREAK', None),
     1383              ('BREAK', 'BREAK', None),
     1384              (None, 'END', u'root')],
     1385             [[(None, 'TEXT', u'ROOT')],
     1386              [(None, 'START', u'foo'),
     1387               (None, 'TEXT', u'FOO'),
     1388               (None, 'END', u'foo')],
     1389              [(None, 'START', u'bar'),
     1390               (None, 'TEXT', u'BAR'),
     1391               (None, 'END', u'bar')]])
     1392            )
     1393
     1394    def test_cut_text(self):
     1395        self.assertEqual(
     1396            self._apply('*/text()'),
     1397            ([(None, 'START', u'root'),
     1398              (None, 'TEXT', u'ROOT'),
     1399              (None, 'START', u'foo'),
     1400              (None, 'END', u'foo'),
     1401              (None, 'START', u'bar'),
     1402              (None, 'END', u'bar'),
     1403              (None, 'END', u'root')],
     1404             [[(None, 'TEXT', u'FOO')],
     1405              [(None, 'TEXT', u'BAR')]])
     1406            )
     1407
     1408    def test_cut_context(self):
     1409        self.assertEqual(
     1410            self._apply('.')[1],
     1411            [[(None, 'START', u'root'),
     1412              (None, 'TEXT', u'ROOT'),
     1413              (None, 'START', u'foo'),
     1414              (None, 'TEXT', u'FOO'),
     1415              (None, 'END', u'foo'),
     1416              (None, 'START', u'bar'),
     1417              (None, 'TEXT', u'BAR'),
     1418              (None, 'END', u'bar'),
     1419              (None, 'END', u'root')]]
     1420            )
     1421
     1422    def test_cut_attribute(self):
     1423        self.assertEqual(
     1424            self._apply('foo/@name', with_attrs=True),
     1425            ([(None, START, (u'root', {})),
     1426              (None, TEXT, u'ROOT'),
     1427              (None, START, (u'foo', {u'size': u'100'})),
     1428              (None, TEXT, u'FOO'),
     1429              (None, END, u'foo'),
     1430              (None, START, (u'bar', {u'name': u'bar'})),
     1431              (None, TEXT, u'BAR'),
     1432              (None, END, u'bar'),
     1433              (None, END, u'root')],
     1434             [[(None, ATTR, {u'name': u'foo'})]])
     1435            )
     1436
     1437    def test_cut_attributes(self):
     1438        self.assertEqual(
     1439            self._apply('foo/@*', with_attrs=True),
     1440            ([(None, START, (u'root', {})),
     1441              (None, TEXT, u'ROOT'),
     1442              (None, START, (u'foo', {})),
     1443              (None, TEXT, u'FOO'),
     1444              (None, END, u'foo'),
     1445              (None, START, (u'bar', {u'name': u'bar'})),
     1446              (None, TEXT, u'BAR'),
     1447              (None, END, u'bar'),
     1448              (None, END, u'root')],
     1449             [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]])
     1450            )
     1451
     1452# XXX Test this when the XPath implementation is fixed (#233).
     1453#    def test_cut_attribute_or_attribute(self):
     1454#        self.assertEqual(
     1455#            self._apply('foo/@name | foo/@size', with_attrs=True),
     1456#            ([(None, START, (u'root', {})),
     1457#              (None, TEXT, u'ROOT'),
     1458#              (None, START, (u'foo', {})),
     1459#              (None, TEXT, u'FOO'),
     1460#              (None, END, u'foo'),
     1461#              (None, START, (u'bar', {u'name': u'bar'})),
     1462#              (None, TEXT, u'BAR'),
     1463#              (None, END, u'bar'),
     1464#              (None, END, u'root')],
     1465#             [[(None, ATTR, {u'name': u'foo', u'size': u'100'})]])
     1466#            )
     1467
     1468
    181469
    191470
     
    221473    from genshi.core import Markup
    231474    from genshi.builder import tag
    24     suite = doctest.DocTestSuite(genshi.filters.transform,
    25                                  optionflags=doctest.NORMALIZE_WHITESPACE,
    26                                  extraglobs={'HTML': HTML, 'tag': tag,
    27                                      'Markup': Markup})
     1475    suite = unittest.TestSuite()
     1476    for test in (SelectTest, InvertTest, EndTest,
     1477                 EmptyTest, RemoveTest, UnwrapText, WrapTest, FilterTest,
     1478                 MapTest, SubstituteTest, RenameTest, ReplaceTest, BeforeTest,
     1479                 AfterTest, PrependTest, AppendTest, AttrTest, CopyTest, CutTest):
     1480        suite.addTest(unittest.makeSuite(test, 'test'))
     1481    suite.addTest(doctest.DocTestSuite(
     1482        genshi.filters.transform, optionflags=doctest.NORMALIZE_WHITESPACE,
     1483        extraglobs={'HTML': HTML, 'tag': tag, 'Markup': Markup}))
    281484    return suite
     1485
    291486
    301487if __name__ == '__main__':
  • trunk/genshi/filters/transform.py

    r858 r868  
    5656
    5757__all__ = ['Transformer', 'StreamBuffer', 'InjectorTransformation', 'ENTER',
    58            'EXIT', 'INSIDE', 'OUTSIDE']
     58           'EXIT', 'INSIDE', 'OUTSIDE', 'BREAK']
    5959
    6060
     
    8686"""Stream augmentation mark indicating that a selected element is being
    8787exited."""
     88
     89BREAK = TransformMark('BREAK')
     90"""Stream augmentation mark indicating a break between two otherwise contiguous
     91blocks of marked events.
     92
     93This is used primarily by the cut() transform to provide later transforms with
     94an opportunity to operate on the cut buffer.
     95"""
     96
     97
     98class PushBackStream(object):
     99    """Allows a single event to be pushed back onto the stream and re-consumed.
     100    """
     101    def __init__(self, stream):
     102        self.stream = iter(stream)
     103        self.peek = None
     104
     105    def push(self, event):
     106        assert self.peek is None
     107        self.peek = event
     108
     109    def __iter__(self):
     110        while True:
     111            if self.peek is not None:
     112                peek = self.peek
     113                self.peek = None
     114                yield peek
     115            else:
     116                try:
     117                    event = self.stream.next()
     118                    yield event
     119                except StopIteration:
     120                    if self.peek is None:
     121                        raise
    88122
    89123
     
    151185        self.transforms = [SelectTransformation(path)]
    152186
    153     def __call__(self, stream):
     187    def __call__(self, stream, keep_marks=False):
    154188        """Apply the transform filter to the marked stream.
    155189
    156190        :param stream: the marked event stream to filter
     191        :param keep_marks: Do not strip transformer selection marks from the
     192                           stream. Useful for testing.
    157193        :return: the transformed stream
    158194        :rtype: `Stream`
     
    161197        for link in self.transforms:
    162198            transforms = link(transforms)
    163         return Stream(self._unmark(transforms),
     199        if not keep_marks:
     200            transforms = self._unmark(transforms)
     201        return Stream(transforms,
    164202                      serializer=getattr(stream, 'serializer', None))
    165203
     
    330368        text.</body></html>
    331369
    332         :param content: Either an iterable of events or a string to insert.
     370        :param content: Either a callable, an iterable of events, or a string
     371                        to insert.
    333372        :rtype: `Transformer`
    334373        """
     
    347386        <em>body</em> text.</body></html>
    348387
    349         :param content: Either an iterable of events or a string to insert.
     388        :param content: Either a callable, an iterable of events, or a string
     389                        to insert.
    350390        :rtype: `Transformer`
    351391        """
     
    363403        rock text.</body></html>
    364404
    365         :param content: Either an iterable of events or a string to insert.
     405        :param content: Either a callable, an iterable of events, or a string
     406                        to insert.
    366407        :rtype: `Transformer`
    367408        """
     
    379420        Some <em>body</em> text.</body></html>
    380421
    381         :param content: Either an iterable of events or a string to insert.
     422        :param content: Either a callable, an iterable of events, or a string
     423                        to insert.
    382424        :rtype: `Transformer`
    383425        """
     
    393435        text. Some new body text.</body></html>
    394436
    395         :param content: Either an iterable of events or a string to insert.
     437        :param content: Either a callable, an iterable of events, or a string
     438                        to insert.
    396439        :rtype: `Transformer`
    397440        """
     
    444487        """Copy selection into buffer.
    445488
    446         The buffer is replaced by each contiguous selection before being passed
     489        The buffer is replaced by each *contiguous* selection before being passed
    447490        to the next transformation. If accumulate=True, further selections will
    448491        be appended to the buffer rather than replacing it.
     
    487530                       stored
    488531        :rtype: `Transformer`
    489         :note: this transformation will buffer the entire input stream
     532        note: Copy (and cut) copy each individual selected object into the
     533               buffer before passing to the next transform. For example, the
     534               XPath ``*|text()`` will select all elements and text, each
     535               instance of which will be copied to the buffer individually
     536               before passing to the next transform. This has implications for
     537               how ``StreamBuffer`` objects can be used, so some
     538               experimentation may be required.
     539
    490540        """
    491541        return self.apply(CopyTransformation(buffer, accumulate))
     
    635685    def _unmark(self, stream):
    636686        for mark, event in stream:
    637             if event[0] is not None:
     687            kind = event[0]
     688            if not (kind is None or kind is ATTR or kind is BREAK):
    638689                yield event
    639690
     
    687738                # XXX  Selected *attributes* are given a "kind" of None to
    688739                # indicate they are not really part of the stream.
    689                 yield ATTR, (None, (QName(event[1][0] + '@*'), result), event[2])
     740                yield ATTR, (ATTR, (QName(event[1][0] + '@*'), result), event[2])
    690741                yield None, event
     742            elif isinstance(result, tuple):
     743                yield OUTSIDE, result
    691744            elif result:
     745                # XXX Assume everything else is "text"?
    692746                yield None, (TEXT, unicode(result), (None, -1, -1))
    693747            else:
     
    735789        """
    736790        for mark, event in stream:
    737             if mark not in (INSIDE, OUTSIDE):
    738                 yield mark, event
     791            yield mark, event
     792            if mark is ENTER:
     793                for mark, event in stream:
     794                    if mark is EXIT:
     795                        yield mark, event
     796                        break
    739797
    740798
     
    781839                    yield None, prefix
    782840                yield mark, event
    783                 while True:
    784                     try:
    785                         mark, event = stream.next()
    786                     except StopIteration:
    787                         yield None, element[-1]
     841                start = mark
     842                stopped = False
     843                for mark, event in stream:
     844                    if start is ENTER and mark is EXIT:
     845                        yield mark, event
     846                        stopped = True
     847                        break
    788848                    if not mark:
    789849                        break
    790850                    yield mark, event
     851                else:
     852                    stopped = True
    791853                yield None, element[-1]
    792                 yield mark, event
     854                if not stopped:
     855                    yield mark, event
    793856            else:
    794857                yield mark, event
     
    819882class FilterTransformation(object):
    820883    """Apply a normal stream filter to the selection. The filter is called once
    821     for each contiguous block of marked events."""
     884    for each selection."""
    822885
    823886    def __init__(self, filter):
     
    841904        queue = []
    842905        for mark, event in stream:
    843             if mark:
     906            if mark is ENTER:
    844907                queue.append(event)
    845             else:
     908                for mark, event in stream:
     909                    queue.append(event)
     910                    if mark is EXIT:
     911                        break
    846912                for queue_event in flush(queue):
    847913                    yield queue_event
    848                 yield None, event
    849         for event in flush(queue):
    850             yield event
     914            elif mark is OUTSIDE:
     915                stopped = True
     916                queue.append(event)
     917                for mark, event in stream:
     918                    if mark is not OUTSIDE:
     919                        break
     920                    queue.append(event)
     921                else:
     922                    stopped = True
     923                for queue_event in flush(queue):
     924                    yield queue_event
     925                if not stopped:
     926                    yield None, event
     927            else:
     928                yield mark, event
     929        for queue_event in flush(queue):
     930            yield queue_event
    851931
    852932
     
    883963    Refer to the documentation for ``re.sub()`` for details.
    884964    """
    885     def __init__(self, pattern, replace, count=1):
     965    def __init__(self, pattern, replace, count=0):
    886966        """Create the transform.
    887967
     
    9571037
    9581038    def _inject(self):
    959         for event in _ensure(self.content):
     1039        content = self.content
     1040        if callable(content):
     1041            content = content()
     1042        for event in _ensure(content):
    9601043            yield None, event
    9611044
     
    9691052        :param stream: The marked event stream to filter
    9701053        """
     1054        stream = PushBackStream(stream)
    9711055        for mark, event in stream:
    9721056            if mark is not None:
     1057                start = mark
    9731058                for subevent in self._inject():
    9741059                    yield subevent
    975                 while True:
    976                     mark, event = stream.next()
    977                     if mark is None:
    978                         yield mark, event
     1060                for mark, event in stream:
     1061                    if start is ENTER:
     1062                        if mark is EXIT:
     1063                            break
     1064                    elif mark != start:
     1065                        stream.push((mark, event))
    9791066                        break
    9801067            else:
     
    9901077        :param stream: The marked event stream to filter
    9911078        """
     1079        stream = PushBackStream(stream)
    9921080        for mark, event in stream:
    9931081            if mark is not None:
     1082                start = mark
    9941083                for subevent in self._inject():
    9951084                    yield subevent
    9961085                yield mark, event
    997                 while True:
    998                     mark, event = stream.next()
    999                     if not mark:
     1086                for mark, event in stream:
     1087                    if mark != start and start is not ENTER:
     1088                        stream.push((mark, event))
    10001089                        break
    10011090                    yield mark, event
    1002             yield mark, event
     1091                    if start is ENTER and mark is EXIT:
     1092                        break
     1093            else:
     1094                yield mark, event
    10031095
    10041096
     
    10111103        :param stream: The marked event stream to filter
    10121104        """
     1105        stream = PushBackStream(stream)
    10131106        for mark, event in stream:
    10141107            yield mark, event
    10151108            if mark:
    1016                 while True:
    1017                     try:
    1018                         mark, event = stream.next()
    1019                     except StopIteration:
    1020                         break
    1021                     if not mark:
     1109                start = mark
     1110                for mark, event in stream:
     1111                    if start is not ENTER and mark != start:
     1112                        stream.push((mark, event))
    10221113                        break
    10231114                    yield mark, event
     1115                    if start is ENTER and mark is EXIT:
     1116                        break
    10241117                for subevent in self._inject():
    10251118                    yield subevent
    1026                 yield mark, event
    10271119
    10281120
     
    10371129        for mark, event in stream:
    10381130            yield mark, event
    1039             if mark in (ENTER, OUTSIDE):
     1131            if mark is ENTER:
    10401132                for subevent in self._inject():
    10411133                    yield subevent
     
    10531145            yield mark, event
    10541146            if mark is ENTER:
    1055                 while True:
    1056                     mark, event = stream.next()
     1147                for mark, event in stream:
    10571148                    if mark is EXIT:
    10581149                        break
     
    11111202
    11121203    def reset(self):
    1113         """Reset the buffer so that it's empty."""
     1204        """Empty the buffer of events."""
    11141205        del self.events[:]
    11151206
     
    11341225        :param stream: the marked event stream to filter
    11351226        """
    1136         stream = iter(stream)
     1227        stream = PushBackStream(stream)
     1228
    11371229        for mark, event in stream:
    11381230            if mark:
    11391231                if not self.accumulate:
    11401232                    self.buffer.reset()
    1141                 events = []
    1142                 while mark:
     1233                events = [(mark, event)]
     1234                self.buffer.append(event)
     1235                start = mark
     1236                for mark, event in stream:
     1237                    if start is not ENTER and mark != start:
     1238                        stream.push((mark, event))
     1239                        break
    11431240                    events.append((mark, event))
    11441241                    self.buffer.append(event)
    1145                     mark, event = stream.next()
     1242                    if start is ENTER and mark is EXIT:
     1243                        break
    11461244                for i in events:
    11471245                    yield i
    1148             yield mark, event
     1246            else:
     1247                yield mark, event
    11491248
    11501249
     
    11601259                       stored
    11611260        """
    1162         if not accumulate:
    1163             buffer.reset()
    11641261        self.buffer = buffer
    11651262        self.accumulate = accumulate
     
    11711268        :param stream: the marked event stream to filter
    11721269        """
    1173         attributes = None
    1174         stream = iter(stream)
     1270        attributes = []
     1271        stream = PushBackStream(stream)
     1272        broken = False
     1273        if not self.accumulate:
     1274            self.buffer.reset()
    11751275        for mark, event in stream:
    11761276            if mark:
     1277                # Send a BREAK event if there was no other event sent between
    11771278                if not self.accumulate:
     1279                    if not broken and self.buffer:
     1280                        yield BREAK, (BREAK, None, None)
    11781281                    self.buffer.reset()
    1179                 while mark:
    1180                     if mark is ATTR:
    1181                         attributes = [name for name, _ in data[1]]
     1282                self.buffer.append(event)
     1283                start = mark
     1284                if mark is ATTR:
     1285                    attributes.extend([name for name, _ in event[1][1]])
     1286                for mark, event in stream:
     1287                    if start is mark is ATTR:
     1288                        attributes.extend([name for name, _ in event[1][1]])
     1289                    # Handle non-element contiguous selection
     1290                    if start is not ENTER and mark != start:
     1291                        # Operating on the attributes of a START event
     1292                        if start is ATTR:
     1293                            kind, data, pos = event
     1294                            assert kind is START
     1295                            data = (data[0], data[1] - attributes)
     1296                            attributes = None
     1297                            stream.push((mark, (kind, data, pos)))
     1298                        else:
     1299                            stream.push((mark, event))
     1300                        break
    11821301                    self.buffer.append(event)
    1183                     mark, event = stream.next()
    1184                 # If we've cut attributes, the associated element should START
    1185                 # immediately after.
    1186                 if attributes:
    1187                     assert kind is START
    1188                     data = (data[0], data[1] - attributes)
    1189                     attributes = None
    1190 
    1191             yield mark, event
     1302                    if start is ENTER and mark is EXIT:
     1303                        break
     1304                broken = False
     1305            else:
     1306                broken = True
     1307                yield mark, event
     1308        if not broken and self.buffer:
     1309            yield BREAK, (BREAK, None, None)
Note: See TracChangeset for help on using the changeset viewer.