# HG changeset patch
# User cmlenz
# Date 1163172456 0
# Node ID 8e75b83d3e71e19d6761f070f38b553ed14f41b6
# Parent 4ff2338e89cd10208abe055dbe7a112abe40908b
Make `Attrs` instances immutable.
diff --git a/ChangeLog b/ChangeLog
--- 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/
diff --git a/UPGRADE.txt b/UPGRADE.txt
--- 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
---------------------
diff --git a/examples/basic/test.html b/examples/basic/test.html
--- a/examples/basic/test.html
+++ b/examples/basic/test.html
@@ -10,7 +10,6 @@
- Item $prefix${item.split()[-1]}
- XYZ ${hey}
${macro1()} ${macro1()} ${macro1()}
${macro2('john')}
diff --git a/genshi/builder.py b/genshi/builder.py
--- 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
diff --git a/genshi/core.py b/genshi/core.py
--- 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.
diff --git a/genshi/filters.py b/genshi/filters.py
--- 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 = []
diff --git a/genshi/output.py b/genshi/output.py
--- 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)
diff --git a/genshi/template/core.py b/genshi/template/core.py
--- 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)
diff --git a/genshi/template/directives.py b/genshi/template/directives.py
--- 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
diff --git a/genshi/tests/builder.py b/genshi/tests/builder.py
--- 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)