changeset 345:2aa7ca37ae6a trunk

Make `Attrs` instances immutable.
author cmlenz
date Fri, 10 Nov 2006 15:27:36 +0000
parents 35189e960252
children 96882a191686
files ChangeLog UPGRADE.txt examples/basic/test.html genshi/builder.py genshi/core.py genshi/filters.py genshi/output.py genshi/template/core.py genshi/template/directives.py genshi/tests/builder.py
diffstat 10 files changed, 119 insertions(+), 120 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -28,6 +28,8 @@
    `genshi.template` package.
  * Results of expression evaluation are no longer implicitly called if they
    are callable.
+ * Instances of the `genshi.core.Attrs` class are now immutable (they are
+   subclasses of `tuple` instead of `list`).
 
 Version 0.3.4
 http://svn.edgewall.org/repos/genshi/tags/0.3.4/
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -14,6 +14,10 @@
 called if they are callable. If you have been using that feature, you
 will need to add the parenthesis to actually call the function.
 
+Instances of `genshi.core.Attrs` are now immutable. Filters
+manipulating the attributes in a stream may need to be updated. See
+the docstring of the `Attrs` for more information.
+
 
 Upgrading from Markup
 ---------------------
--- a/examples/basic/test.html
+++ b/examples/basic/test.html
@@ -10,7 +10,6 @@
  <body class="$bozz">
   <ul py:attrs="{'id': 'second', 'class': None}" py:if="len(items) > 0">
    <li py:for="item in items">Item $prefix${item.split()[-1]}</li>
-   XYZ ${hey}
   </ul>
   ${macro1()} ${macro1()} ${macro1()}
   ${macro2('john')}
--- a/genshi/builder.py
+++ b/genshi/builder.py
@@ -73,6 +73,16 @@
         return Stream(self._generate())
 
 
+def _value_to_unicode(value):
+    if isinstance(value, unicode):
+        return value
+    return unicode(value)
+
+def _kwargs_to_attrs(kwargs):
+    return [(k.rstrip('_').replace('_', '-'), _value_to_unicode(v))
+            for k, v in kwargs.items() if v is not None]
+
+
 class Element(Fragment):
     """Simple XML output generator based on the builder pattern.
 
@@ -157,20 +167,10 @@
     def __init__(self, tag_, **attrib):
         Fragment.__init__(self)
         self.tag = QName(tag_)
-        self.attrib = Attrs()
-        for attr, value in attrib.items():
-            if value is not None:
-                if not isinstance(value, basestring):
-                    value = unicode(value)
-                self.attrib.append((QName(attr.rstrip('_').replace('_', '-')),
-                                    value))
+        self.attrib = Attrs(_kwargs_to_attrs(attrib))
 
     def __call__(self, *args, **kwargs):
-        for attr, value in kwargs.items():
-            if value is not None:
-                if not isinstance(value, basestring):
-                    value = unicode(value)
-                self.attrib.set(attr.rstrip('_').replace('_', '-'), value)
+        self.attrib |= Attrs(_kwargs_to_attrs(kwargs))
         Fragment.__call__(self, *args)
         return self
 
--- a/genshi/core.py
+++ b/genshi/core.py
@@ -213,11 +213,11 @@
         yield event
 
 
-class Attrs(list):
-    """Sequence type that stores the attributes of an element.
+class Attrs(tuple):
+    """Immutable sequence type that stores the attributes of an element.
     
-    The order of the attributes is preserved, while accessing and manipulating
-    attributes by name is also supported.
+    Ordering of the attributes is preserved, while accessing by name is also
+    supported.
     
     >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
     >>> attrs
@@ -227,58 +227,53 @@
     True
     >>> 'tabindex' in attrs
     False
-    
     >>> attrs.get(u'title')
     'Foo'
-    >>> attrs.set(u'title', 'Bar')
+    
+    Instances may not be manipulated directly. Instead, the operators `|` and
+    `-` can be used to produce new instances that have specific attributes
+    added, replaced or removed.
+    
+    To remove an attribute, use the `-` operator. The right hand side can be
+    either a string or a set/sequence of strings, identifying the name(s) of
+    the attribute(s) to remove:
+    
+    >>> attrs - 'title'
+    Attrs([(QName(u'href'), '#')])
+    >>> attrs - ('title', 'href')
+    Attrs()
+    
+    The original instance is not modified, but the operator can of course be
+    used with an assignment:
+
     >>> attrs
-    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Bar')])
-    >>> attrs.remove(u'title')
+    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Foo')])
+    >>> attrs -= 'title'
     >>> attrs
     Attrs([(QName(u'href'), '#')])
     
-    New attributes added using the `set()` method are appended to the end of
-    the list:
-    
-    >>> attrs.set(u'accesskey', 'k')
-    >>> attrs
-    Attrs([(QName(u'href'), '#'), (QName(u'accesskey'), 'k')])
-    
-    An `Attrs` instance can also be initialized with keyword arguments.
+    To add a new attribute, use the `|` operator, where the right hand value
+    is a sequence of `(name, value)` tuples (which includes `Attrs` instances):
     
-    >>> attrs = Attrs(class_='bar', href='#', title='Foo')
-    >>> attrs.get('class')
-    'bar'
-    >>> attrs.get('href')
-    '#'
-    >>> attrs.get('title')
-    'Foo'
+    >>> attrs | [(u'title', 'Bar')]
+    Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Bar')])
     
-    Reserved words can be used by appending a trailing underscore to the name,
-    and any other underscore is replaced by a dash:
+    If the attributes already contain an attribute with a given name, the value
+    of that attribute is replaced:
     
-    >>> attrs = Attrs(class_='bar', accept_charset='utf-8')
-    >>> attrs.get('class')
-    'bar'
-    >>> attrs.get('accept-charset')
-    'utf-8'
+    >>> attrs | [(u'href', 'http://example.org/')]
+    Attrs([(QName(u'href'), 'http://example.org/')])
     
-    Thus this shorthand can not be used if attribute names should contain
-    actual underscore characters.
     """
     __slots__ = []
 
-    def __init__(self, attrib=None, **kwargs):
+    def __new__(cls, items=()):
         """Create the `Attrs` instance.
         
-        If the `attrib` parameter is provided, it is expected to be a sequence
+        If the `items` parameter is provided, it is expected to be a sequence
         of `(name, value)` tuples.
         """
-        if attrib is None:
-            attrib = []
-        list.__init__(self, [(QName(name), value) for name, value in attrib])
-        for name, value in kwargs.items():
-            self.set(name.rstrip('_').replace('_', '-'), value)
+        return tuple.__new__(cls, [(QName(name), val) for name, val in items])
 
     def __contains__(self, name):
         """Return whether the list includes an attribute with the specified
@@ -288,10 +283,29 @@
             if attr == name:
                 return True
 
+    def __getslice__(self, i, j):
+        return Attrs(tuple.__getslice__(self, i, j))
+
+    def __or__(self, attrs):
+        """Return a new instance that contains the attributes in `attrs` in
+        addition to any already existing attributes.
+        """
+        repl = dict([(an, av) for an, av in attrs if an in self])
+        return Attrs([(sn, repl.get(sn, sv)) for sn, sv in self] +
+                     [(an, av) for an, av in attrs if an not in self])
+
     def __repr__(self):
         if not self:
             return 'Attrs()'
-        return 'Attrs(%s)' % list.__repr__(self)
+        return 'Attrs([%s])' % ', '.join([repr(item) for item in self])
+
+    def __sub__(self, names):
+        """Return a new instance with all attributes with a name in `names` are
+        removed.
+        """
+        if isinstance(names, basestring):
+            names = (names,)
+        return Attrs([(name, val) for name, val in self if name not in names])
 
     def get(self, name, default=None):
         """Return the value of the attribute with the specified name, or the
@@ -302,30 +316,6 @@
                 return value
         return default
 
-    def remove(self, name):
-        """Remove the attribute with the specified name.
-        
-        If no such attribute is found, this method does nothing.
-        """
-        for idx, (attr, _) in enumerate(self):
-            if attr == name:
-                del self[idx]
-                break
-
-    def set(self, name, value):
-        """Set the specified attribute to the given value.
-        
-        If an attribute with the specified name is already in the list, the
-        value of the existing entry is updated. Otherwise, a new attribute is
-        appended to the end of the list.
-        """
-        for idx, (attr, _) in enumerate(self):
-            if attr == name:
-                self[idx] = (QName(attr), value)
-                break
-        else:
-            self.append((QName(name), value))
-
     def totuple(self):
         """Return the attributes as a markup event.
         
--- a/genshi/filters.py
+++ b/genshi/filters.py
@@ -73,23 +73,23 @@
         for kind, data, pos in stream:
 
             if kind is START:
-                tag, attrib = data
+                tag, attrs = data
                 tagname = tag.localname
 
                 if tagname == 'form' and (
-                        self.name and attrib.get('name') == self.name or
-                        self.id and attrib.get('id') == self.id or
+                        self.name and attrs.get('name') == self.name or
+                        self.id and attrs.get('id') == self.id or
                         not (self.id or self.name)):
                     in_form = True
 
                 elif in_form:
                     if tagname == 'input':
-                        type = attrib.get('type')
+                        type = attrs.get('type')
                         if type in ('checkbox', 'radio'):
-                            name = attrib.get('name')
+                            name = attrs.get('name')
                             if name:
                                 value = self.data.get(name)
-                                declval = attrib.get('value')
+                                declval = attrs.get('value')
                                 checked = False
                                 if isinstance(value, (list, tuple)):
                                     if declval:
@@ -102,32 +102,34 @@
                                     elif type == 'checkbox':
                                         checked = bool(value)
                                 if checked:
-                                    attrib.set('checked', 'checked')
-                                else:
-                                    attrib.remove('checked')
+                                    attrs |= [('checked', 'checked')]
+                                elif 'checked' in attrs:
+                                    attrs -= 'checked'
                         elif type in (None, 'hidden', 'text'):
-                            name = attrib.get('name')
+                            name = attrs.get('name')
                             if name:
                                 value = self.data.get(name)
                                 if isinstance(value, (list, tuple)):
                                     value = value[0]
                                 if value is not None:
-                                    attrib.set('value', unicode(value))
+                                    attrs |= [('value', unicode(value))]
                     elif tagname == 'select':
-                        name = attrib.get('name')
+                        name = attrs.get('name')
                         select_value = self.data.get(name)
                         in_select = True
                     elif tagname == 'textarea':
-                        name = attrib.get('name')
+                        name = attrs.get('name')
                         textarea_value = self.data.get(name)
                         if isinstance(textarea_value, (list, tuple)):
                             textarea_value = textarea_value[0]
                         in_textarea = True
                     elif in_select and tagname == 'option':
                         option_start = kind, data, pos
-                        option_value = attrib.get('value')
+                        option_value = attrs.get('value')
                         in_option = True
                         continue
+                yield kind, (tag, attrs), pos
+
 
             elif in_form and kind is TEXT:
                 if in_select and in_option:
@@ -137,6 +139,7 @@
                     continue
                 elif in_textarea:
                     continue
+                yield kind, data, pos
 
             elif in_form and kind is END:
                 tagname = data.localname
@@ -150,12 +153,12 @@
                         selected = option_value in select_value
                     else:
                         selected = option_value == select_value
-                    attrib = option_start[1][1]
+                    okind, (tag, attrs), opos = option_start
                     if selected:
-                        attrib.set('selected', 'selected')
-                    else:
-                        attrib.remove('selected')
-                    yield option_start
+                        attrs |= [('selected', 'selected')]
+                    elif 'selected' in attrs:
+                        attrs -= 'selected'
+                    yield okind, (tag, attrs), opos
                     if option_text:
                         yield option_text
                     in_option = False
@@ -164,8 +167,10 @@
                     if textarea_value:
                         yield TEXT, unicode(textarea_value), pos
                     in_textarea = False
+                yield kind, data, pos
 
-            yield kind, data, pos
+            else:
+                yield kind, data, pos
 
 
 class HTMLSanitizer(object):
@@ -234,13 +239,13 @@
             if kind is START:
                 if waiting_for:
                     continue
-                tag, attrib = data
+                tag, attrs = data
                 if tag not in self.safe_tags:
                     waiting_for = tag
                     continue
 
-                new_attrib = Attrs()
-                for attr, value in attrib:
+                new_attrs = []
+                for attr, value in attrs:
                     value = stripentities(value)
                     if attr not in self.safe_attrs:
                         continue
@@ -264,9 +269,9 @@
                         if not decls:
                             continue
                         value = '; '.join(decls)
-                    new_attrib.append((attr, value))
+                    new_attrs.append((attr, value))
 
-                yield kind, (tag, new_attrib), pos
+                yield kind, (tag, Attrs(new_attrs)), pos
 
             elif kind is END:
                 tag = data
@@ -313,9 +318,9 @@
         for kind, data, pos in stream:
 
             if kind is START and not in_fallback and data[0] in namespace:
-                tag, attrib = data
+                tag, attrs = data
                 if tag.localname == 'include':
-                    include_href = attrib.get('href')
+                    include_href = attrs.get('href')
                 elif tag.localname == 'fallback':
                     in_fallback = True
                     fallback_stream = []
--- a/genshi/output.py
+++ b/genshi/output.py
@@ -98,7 +98,7 @@
                         ns_attrib.append((QName('xmlns'), namespace))
                 buf = ['<', tagname]
 
-                for attr, value in attrib + ns_attrib:
+                for attr, value in attrib + tuple(ns_attrib):
                     attrname = attr.localname
                     if attr.namespace:
                         prefix = ns_mapping.get(attr.namespace)
@@ -213,7 +213,7 @@
                         ns_attrib.append((QName('xmlns'), tagns))
                 buf = ['<', tagname]
 
-                for attr, value in attrib + ns_attrib:
+                for attr, value in chain(attrib, ns_attrib):
                     attrname = attr.localname
                     if attr.namespace:
                         prefix = ns_mapping.get(attr.namespace)
--- a/genshi/template/core.py
+++ b/genshi/template/core.py
@@ -326,9 +326,9 @@
             if kind is START and data[1]:
                 # Attributes may still contain expressions in start tags at
                 # this point, so do some evaluation
-                tag, attrib = data
-                new_attrib = []
-                for name, substream in attrib:
+                tag, attrs = data
+                new_attrs = []
+                for name, substream in attrs:
                     if isinstance(substream, basestring):
                         value = substream
                     else:
@@ -340,8 +340,8 @@
                         value = [x for x in values if x is not None]
                         if not value:
                             continue
-                    new_attrib.append((name, u''.join(value)))
-                yield kind, (tag, Attrs(new_attrib)), pos
+                    new_attrs.append((name, u''.join(value)))
+                yield kind, (tag, Attrs(new_attrs)), pos
 
             elif kind is EXPR:
                 result = data.evaluate(ctxt)
--- a/genshi/template/directives.py
+++ b/genshi/template/directives.py
@@ -80,7 +80,6 @@
             kind, (tag, attrib), pos  = stream.next()
             attrs = self.expr.evaluate(ctxt)
             if attrs:
-                attrib = Attrs(attrib[:])
                 if isinstance(attrs, Stream):
                     try:
                         attrs = iter(attrs).next()
@@ -88,11 +87,9 @@
                         attrs = []
                 elif not isinstance(attrs, list): # assume it's a dict
                     attrs = attrs.items()
-                for name, value in attrs:
-                    if value is None:
-                        attrib.remove(name)
-                    else:
-                        attrib.set(name, unicode(value).strip())
+                attrib -= [name for name, val in attrs if val is None]
+                attrib |= [(name, unicode(val).strip()) for name, val in attrs
+                           if val is not None]
             yield kind, (tag, attrib), pos
             for event in stream:
                 yield event
--- a/genshi/tests/builder.py
+++ b/genshi/tests/builder.py
@@ -16,7 +16,7 @@
 import unittest
 
 from genshi.builder import Element, tag
-from genshi.core import Stream
+from genshi.core import Attrs, Stream
 
 
 class ElementFactoryTestCase(unittest.TestCase):
@@ -24,7 +24,8 @@
     def test_link(self):
         link = tag.a(href='#', title='Foo', accesskey=None)('Bar')
         bits = iter(link.generate())
-        self.assertEqual((Stream.START, ('a', [('href', "#"), ('title', "Foo")]),
+        self.assertEqual((Stream.START,
+                          ('a', Attrs([('href', "#"), ('title', "Foo")])),
                           (None, -1, -1)), bits.next())
         self.assertEqual((Stream.TEXT, u'Bar', (None, -1, -1)), bits.next())
         self.assertEqual((Stream.END, 'a', (None, -1, -1)), bits.next())
@@ -36,7 +37,8 @@
         generated.
         """
         event = iter(tag.foo(id=3)).next()
-        self.assertEqual((Stream.START, ('foo', [('id', '3')]), (None, -1, -1)),
+        self.assertEqual((Stream.START, ('foo', Attrs([('id', '3')])),
+                          (None, -1, -1)),
                          event)
 
 
Copyright (C) 2012-2017 Edgewall Software