# 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 @@ ${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)