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?
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

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).
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>'
+    )

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest pytest-expect mock
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
chardet==5.2.0
datrie==0.8.2
exceptiongroup==1.2.2
Genshi==0.7.9
-e git+https://github.com/html5lib/html5lib-python.git@984f934d30ba2fde0e9ce5d4581c73fb81654474#egg=html5lib
iniconfig==2.1.0
lxml==5.3.1
mock==5.2.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
pytest-expect==1.1.0
six==1.17.0
tomli==2.2.1
u-msgpack-python==2.8.0
webencodings==0.5.1

name: html5lib-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - chardet==5.2.0
      - datrie==0.8.2
      - exceptiongroup==1.2.2
      - genshi==0.7.9
      - iniconfig==2.1.0
      - lxml==5.3.1
      - mock==5.2.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-expect==1.1.0
      - six==1.17.0
      - tomli==2.2.1
      - u-msgpack-python==2.8.0
      - webencodings==0.5.1
prefix: /opt/conda/envs/html5lib-python

html5lib/tests/test_alphabeticalattributes.py::test_with_different_namespaces html5lib/tests/test_alphabeticalattributes.py::test_with_serializer
html5lib/tests/test_alphabeticalattributes.py::test_alphabetizing[no html5lib/tests/test_alphabeticalattributes.py::test_alphabetizing[one html5lib/tests/test_alphabeticalattributes.py::test_alphabetizing[multiple
html5lib/filters/alphabeticalattributes.py
html5lib/filters/alphabeticalattributes.py