Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add headers.get and headers.getlist #180

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ have a newline inside a header value, and ``Content-Length: hello`` is
an error because `Content-Length` should always be an integer. We may
add additional checks in the future.

It is possible to get the first or all headers for a given name.

.. ipython:: python

res = h11.Response(status_code=204, headers=[
("Date", b"Thu, 09 Jan 2025 18:37:23 GMT"),
("Set-Cookie", b"sid=1234"),
("Set-Cookie", b"lang=en_US"),
])
res.headers.get(b"date")
res.headers.getlist(b"set-cookie")

While we make sure to expose header names as lowercased bytes, we also
preserve the original header casing that is used. Compliant HTTP
agents should always treat headers in a case insensitive manner, but
Expand Down
43 changes: 38 additions & 5 deletions h11/_headers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union
from typing import List, overload, Sequence, Tuple, TYPE_CHECKING, TypeVar, Union
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My IDE was telling me AnyStr and cast were not used, I used the opportunity of changing the line to remove them


from ._abnf import field_name, field_value
from ._util import bytesify, LocalProtocolError, validate
Expand All @@ -13,6 +13,8 @@
from typing_extensions import Literal # type: ignore


T = TypeVar("T")

# Facts
# -----
#
Expand Down Expand Up @@ -84,19 +86,29 @@ class Headers(Sequence[Tuple[bytes, bytes]]):
r = Request(
method="GET",
target="/",
headers=[("Host", "example.org"), ("Connection", "keep-alive")],
headers=[
("Host", "example.org"),
("Connection", "keep-alive"),
("Cookie", "session=1234"),
("Cookie", "lang=en_US"),
],
http_version="1.1",
)
assert r.headers == [
(b"host", b"example.org"),
(b"connection", b"keep-alive")
(b"connection", b"keep-alive"),
(b"cookie", b"session=1234"),
(b"cookie", b"lang=en_US"),
]
assert r.headers.raw_items() == [
(b"Host", b"example.org"),
(b"Connection", b"keep-alive")
(b"Connection", b"keep-alive"),
(b"Cookie", b"session=1234"),
(b"Cookie", b"lang=en_US"),
]
assert r.headers.get(b"host") == b"example.org"
assert r.headers.getlist(b"cookie") == [b"session=1234", b"lang=en_US"]
"""

__slots__ = "_full_items"

def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None:
Expand All @@ -118,6 +130,27 @@ def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override
_, name, value = self._full_items[idx]
return (name, value)

def get(self, key: bytes, default: T = None) -> Union[bytes, T]:
"""Find the first header with lowercased-name :param:`key`, it returns
its value when found, and :param:`default` otherwise.

Args:
key (bytes): The lowercased header name to find.

default: The value to return when the header is not found.
"""
return next((value for name, value in self if name == key), default)

def getlist(self, key: bytes) -> List[bytes]:
"""Find the all the headers with lowercased-name :param:`key`,
it returns their values in a list. It returns an empty list when
no header matched.

Args:
key (bytes): The lowercased header name to find.
"""
return [value for name, value in self if name == key]

def raw_items(self) -> List[Tuple[bytes, bytes]]:
return [(raw_name, value) for raw_name, _, value in self._full_items]

Expand Down
16 changes: 16 additions & 0 deletions h11/tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,19 @@ def test_header_casing() -> None:
(b"Host", b"example.org"),
(b"Connection", b"keep-alive"),
]

def test_header_get() -> None:
r = Response(status_code=204, headers=[
("Date", b"Thu, 09 Jan 2025 18:37:23 GMT"),
("Set-Cookie", b"sid=1234"),
("Set-Cookie", b"lang=en_US"),
])

assert r.headers.get(b"date") == b"Thu, 09 Jan 2025 18:37:23 GMT"
assert r.headers.get(b"set-cookie") == b"sid=1234"
assert r.headers.get(b"content-length") is None
assert r.headers.get(b"content-length", b"0") == b"0"

assert r.headers.getlist(b"date") == [b"Thu, 09 Jan 2025 18:37:23 GMT"]
assert r.headers.getlist(b"set-cookie") == [b"sid=1234", b"lang=en_US"]
assert r.headers.getlist(b"content-length") == []