Repository: html5lib/html5lib-python

Issue:
alphabeticalattributes filter TypeError when comparing namespaced attributes with non-namespaced attributes
In SVG 1.1, you could have something like this:

```
<svg><pattern id="patt1" xlink:href="http://example.com/#whatever" /></svg>
```

In Python 3, that creates the following attributes:

```
dict_items([((None, 'id'), 'patt1'), (('http://www.w3.org/1999/xlink', 'href'), '#patt2')])
```

The problem is that this means we're comparing `None` with a str here:

https://github.com/html5lib/html5lib-python/blob/17499b9763a090f7715af49555d21fe4b558958b/html5lib/filters/alphabeticalattributes.py#L17

That kicks up a TypeError.

Script:

```
import html5lib
from html5lib.serializer import HTMLSerializer


text = '<svg><pattern id="patt1" xlink:href="http://example.com/#foo" /></svg>'

parser = html5lib.HTMLParser()
dom = parser.parseFragment(text)
walker = html5lib.getTreeWalker('etree')
ser = HTMLSerializer(alphabetical_attributes=True)

ser.render(walker(dom))
```

Traceback:

```
Traceback (most recent call last):
  File "foo.py", line 12, in <module>
    ser.render(walker(dom))
  File "/home/willkg/mozilla/bleach/.tox/py34-html5lib99999999/lib/python3.4/site-packages/html5lib/serializer.py", line 323, in render
    return "".join(list(self.serialize(treewalker)))
  File "/home/willkg/mozilla/bleach/.tox/py34-html5lib99999999/lib/python3.4/site-packages/html5lib/serializer.py", line 209, in serialize
    for token in treewalker:
  File "/home/willkg/mozilla/bleach/.tox/py34-html5lib99999999/lib/python3.4/site-packages/html5lib/filters/optionaltags.py", line 18, in __iter__
    for previous, token, next in self.slider():
  File "/home/willkg/mozilla/bleach/.tox/py34-html5lib99999999/lib/python3.4/site-packages/html5lib/filters/optionaltags.py", line 9, in slider
    for token in self.source:
  File "/home/willkg/mozilla/bleach/.tox/py34-html5lib99999999/lib/python3.4/site-packages/html5lib/filters/alphabeticalattributes.py", line 17, in __iter__
    key=lambda x: x[0]):
TypeError: unorderable types: str() < NoneType()
```

This is true of html5lib 0.99999999 (8 9s) and 0.999999999 (9 9s).

Discussion:
willkg: This has been sitting around for 6 months. I was told if I put a PR together that someone would help me land it: https://github.com/html5lib/html5lib-python/issues/322#issuecomment-281811976

What's going on? What can I do to help land this and move forward with the other 17 pull requests in the queue?

Patch:
diff --git a/html5lib/filters/alphabeticalattributes.py b/html5lib/filters/alphabeticalattributes.py
index 4795bae..f938ba1 100644
--- a/html5lib/filters/alphabeticalattributes.py
+++ b/html5lib/filters/alphabeticalattributes.py
@@ -8,13 +8,24 @@ except ImportError:
     from ordereddict import OrderedDict
 
 
+def _attr_key(attr):
+    """Return an appropriate key for an attribute for sorting
+
+    Attributes have a namespace that can be either ``None`` or a string. We
+    can't compare the two because they're different types, so we convert
+    ``None`` to an empty string first.
+
+    """
+    return (attr[0][0] or ''), attr[0][1]
+
+
 class Filter(base.Filter):
     def __iter__(self):
         for token in base.Filter.__iter__(self):
             if token["type"] in ("StartTag", "EmptyTag"):
                 attrs = OrderedDict()
                 for name, value in sorted(token["data"].items(),
-                                          key=lambda x: x[0]):
+                                          key=_attr_key):
                     attrs[name] = value
                 token["data"] = attrs
             yield token


Test patch:
diff --git a/html5lib/tests/test_alphabeticalattributes.py b/html5lib/tests/test_alphabeticalattributes.py
new file mode 100644
index 0000000..9e560a1
--- /dev/null
+++ b/html5lib/tests/test_alphabeticalattributes.py
@@ -0,0 +1,81 @@
+from __future__ import absolute_import, division, unicode_literals
+
+try:
+    from collections import OrderedDict
+except ImportError:
+    from ordereddict import OrderedDict
+
+import pytest
+
+import html5lib
+from html5lib.filters.alphabeticalattributes import Filter
+from html5lib.serializer import HTMLSerializer
+
+
+@pytest.mark.parametrize('msg, attrs, expected_attrs', [
+    (
+        'no attrs',
+        {},
+        {}
+    ),
+    (
+        'one attr',
+        {(None, 'alt'): 'image'},
+        OrderedDict([((None, 'alt'), 'image')])
+    ),
+    (
+        'multiple attrs',
+        {
+            (None, 'src'): 'foo',
+            (None, 'alt'): 'image',
+            (None, 'style'): 'border: 1px solid black;'
+        },
+        OrderedDict([
+            ((None, 'alt'), 'image'),
+            ((None, 'src'), 'foo'),
+            ((None, 'style'), 'border: 1px solid black;')
+        ])
+    ),
+])
+def test_alphabetizing(msg, attrs, expected_attrs):
+    tokens = [{'type': 'StartTag', 'name': 'img', 'data': attrs}]
+    output_tokens = list(Filter(tokens))
+
+    attrs = output_tokens[0]['data']
+    assert attrs == expected_attrs
+
+
+def test_with_different_namespaces():
+    tokens = [{
+        'type': 'StartTag',
+        'name': 'pattern',
+        'data': {
+            (None, 'id'): 'patt1',
+            ('http://www.w3.org/1999/xlink', 'href'): '#patt2'
+        }
+    }]
+    output_tokens = list(Filter(tokens))
+
+    attrs = output_tokens[0]['data']
+    assert attrs == OrderedDict([
+        ((None, 'id'), 'patt1'),
+        (('http://www.w3.org/1999/xlink', 'href'), '#patt2')
+    ])
+
+
+def test_with_serializer():
+    """Verify filter works in the context of everything else"""
+    parser = html5lib.HTMLParser()
+    dom = parser.parseFragment('<svg><pattern xlink:href="#patt2" id="patt1"></svg>')
+    walker = html5lib.getTreeWalker('etree')
+    ser = HTMLSerializer(
+        alphabetical_attributes=True,
+        quote_attr_values='always'
+    )
+
+    # FIXME(willkg): The "xlink" namespace gets dropped by the serializer. When
+    # that gets fixed, we can fix this expected result.
+    assert (
+        ser.render(walker(dom)) ==
+        '<svg><pattern id="patt1" href="#patt2"></pattern></svg>'
+    )