Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions h11/_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
68 changes: 68 additions & 0 deletions h11/tests/test_headers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Iterator, Tuple
from unittest.mock import ANY

import pytest

from .._events import Request
Expand Down Expand Up @@ -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(
[
Expand Down
1 change: 1 addition & 0 deletions newsfragments/214.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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``.