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( [ 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``.