From 0b24ea45905cf2f1b5e86af07a39f48a15d7a932 Mon Sep 17 00:00:00 2001 From: wynneyin <2830851361@qq.com> Date: Sun, 8 Nov 2026 18:21:44 +0800 Subject: [PATCH 1/2] Preserve equality fallback for unsupported header operands Return NotImplemented for operands without an iteration protocol so sentinel comparisons and reflected equality work for event headers. Keep existing iterable comparisons and iteration exceptions intact. Add regression coverage for unsupported operands, reflected equality, legacy sequences and errors during iterator setup and consumption. Co-Authored-By: Claude Code --- h11/_headers.py | 3 ++ h11/tests/test_headers.py | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/h11/_headers.py b/h11/_headers.py index 31da3e2..0d4cd23 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -108,6 +108,9 @@ def __bool__(self) -> bool: return bool(self._full_items) def __eq__(self, other: object) -> bool: + other_type = type(other) + if not (hasattr(other_type, "__iter__") or hasattr(other_type, "__getitem__")): + return NotImplemented return list(self) == list(other) # type: ignore def __len__(self) -> int: diff --git a/h11/tests/test_headers.py b/h11/tests/test_headers.py index b57274c..d3d1956 100644 --- a/h11/tests/test_headers.py +++ b/h11/tests/test_headers.py @@ -1,3 +1,6 @@ +from typing import Iterator, Tuple +from unittest.mock import ANY + import pytest from .._events import Request @@ -94,6 +97,71 @@ def test_normalize_and_validate() -> None: assert excinfo.value.error_status_hint == 501 # Not Implemented +@pytest.mark.parametrize("other", [None, 42, object()]) +def test_headers_compare_unequal_to_non_iterables(other: object) -> None: + headers = Request( + method="GET", target="/", headers=[("Host", "example.com")] + ).headers + + assert not (headers == other) + assert not (other == headers) + assert headers != other + assert other != headers + + +def test_headers_allow_reflected_equality() -> None: + headers = Request( + method="GET", target="/", headers=[("Host", "example.com")] + ).headers + + assert headers == ANY + assert ANY == headers + assert not (headers != ANY) + assert not (ANY != headers) + + +def test_headers_compare_with_iterables() -> None: + headers = normalize_and_validate([("Host", "example.com")]) + + assert headers == [(b"host", b"example.com")] + assert headers == ((b"host", b"example.com"),) + assert headers == iter([(b"host", b"example.com")]) + assert headers == normalize_and_validate([("HOST", "example.com")]) + assert headers != [(b"host", b"different.com")] + + +def test_headers_compare_with_sequence_protocol() -> None: + class HeaderSequence: + def __getitem__(self, index: int) -> Tuple[bytes, bytes]: + return [(b"host", b"example.com")][index] + + headers = normalize_and_validate([("Host", "example.com")]) + + assert headers == HeaderSequence() + + +def test_headers_equality_propagates_iterator_setup_errors() -> None: + class BrokenIterable: + def __iter__(self) -> Iterator[Tuple[bytes, bytes]]: + raise TypeError("iterator setup failed") + + headers = normalize_and_validate([("Host", "example.com")]) + + with pytest.raises(TypeError, match="iterator setup failed"): + headers == BrokenIterable() + + +def test_headers_equality_propagates_iteration_errors() -> None: + def broken_iterator() -> Iterator[Tuple[bytes, bytes]]: + yield (b"host", b"example.com") + raise TypeError("broken iterator") + + headers = normalize_and_validate([("Host", "example.com")]) + + with pytest.raises(TypeError, match="broken iterator"): + headers == broken_iterator() + + def test_get_set_comma_header() -> None: headers = normalize_and_validate( [ From 3bbcf5018479c6d91670f08682d47e421d4aaa93 Mon Sep 17 00:00:00 2001 From: wynneyin <2830851361@qq.com> Date: Sun, 8 Nov 2026 19:28:31 +0800 Subject: [PATCH 2/2] Add release note for header equality fallback Document the user-visible sentinel and reflected-comparison repair tracked in issue #214. Co-Authored-By: Claude Code --- newsfragments/214.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/214.bugfix.rst diff --git a/newsfragments/214.bugfix.rst b/newsfragments/214.bugfix.rst new file mode 100644 index 0000000..280545d --- /dev/null +++ b/newsfragments/214.bugfix.rst @@ -0,0 +1 @@ +Comparing event headers with non-iterable objects now follows Python's equality fallback instead of raising ``TypeError``, allowing sentinel comparisons and reflected equality such as ``unittest.mock.ANY``.