|
from __future__ import annotations |
|
|
|
from collections.abc import Iterable, Iterator, Mapping, MutableMapping |
|
from typing import Any, Protocol, Union |
|
|
|
|
|
__all__ = [ |
|
"Headers", |
|
"HeadersLike", |
|
"MultipleValuesError", |
|
] |
|
|
|
|
|
class MultipleValuesError(LookupError): |
|
""" |
|
Exception raised when :class:`Headers` has multiple values for a key. |
|
|
|
""" |
|
|
|
def __str__(self) -> str: |
|
|
|
if len(self.args) == 1: |
|
return repr(self.args[0]) |
|
return super().__str__() |
|
|
|
|
|
class Headers(MutableMapping[str, str]): |
|
""" |
|
Efficient data structure for manipulating HTTP headers. |
|
|
|
A :class:`list` of ``(name, values)`` is inefficient for lookups. |
|
|
|
A :class:`dict` doesn't suffice because header names are case-insensitive |
|
and multiple occurrences of headers with the same name are possible. |
|
|
|
:class:`Headers` stores HTTP headers in a hybrid data structure to provide |
|
efficient insertions and lookups while preserving the original data. |
|
|
|
In order to account for multiple values with minimal hassle, |
|
:class:`Headers` follows this logic: |
|
|
|
- When getting a header with ``headers[name]``: |
|
- if there's no value, :exc:`KeyError` is raised; |
|
- if there's exactly one value, it's returned; |
|
- if there's more than one value, :exc:`MultipleValuesError` is raised. |
|
|
|
- When setting a header with ``headers[name] = value``, the value is |
|
appended to the list of values for that header. |
|
|
|
- When deleting a header with ``del headers[name]``, all values for that |
|
header are removed (this is slow). |
|
|
|
Other methods for manipulating headers are consistent with this logic. |
|
|
|
As long as no header occurs multiple times, :class:`Headers` behaves like |
|
:class:`dict`, except keys are lower-cased to provide case-insensitivity. |
|
|
|
Two methods support manipulating multiple values explicitly: |
|
|
|
- :meth:`get_all` returns a list of all values for a header; |
|
- :meth:`raw_items` returns an iterator of ``(name, values)`` pairs. |
|
|
|
""" |
|
|
|
__slots__ = ["_dict", "_list"] |
|
|
|
|
|
def __init__(self, *args: HeadersLike, **kwargs: str) -> None: |
|
self._dict: dict[str, list[str]] = {} |
|
self._list: list[tuple[str, str]] = [] |
|
self.update(*args, **kwargs) |
|
|
|
def __str__(self) -> str: |
|
return "".join(f"{key}: {value}\r\n" for key, value in self._list) + "\r\n" |
|
|
|
def __repr__(self) -> str: |
|
return f"{self.__class__.__name__}({self._list!r})" |
|
|
|
def copy(self) -> Headers: |
|
copy = self.__class__() |
|
copy._dict = self._dict.copy() |
|
copy._list = self._list.copy() |
|
return copy |
|
|
|
def serialize(self) -> bytes: |
|
|
|
return str(self).encode() |
|
|
|
|
|
|
|
def __contains__(self, key: object) -> bool: |
|
return isinstance(key, str) and key.lower() in self._dict |
|
|
|
def __iter__(self) -> Iterator[str]: |
|
return iter(self._dict) |
|
|
|
def __len__(self) -> int: |
|
return len(self._dict) |
|
|
|
|
|
|
|
def __getitem__(self, key: str) -> str: |
|
value = self._dict[key.lower()] |
|
if len(value) == 1: |
|
return value[0] |
|
else: |
|
raise MultipleValuesError(key) |
|
|
|
def __setitem__(self, key: str, value: str) -> None: |
|
self._dict.setdefault(key.lower(), []).append(value) |
|
self._list.append((key, value)) |
|
|
|
def __delitem__(self, key: str) -> None: |
|
key_lower = key.lower() |
|
self._dict.__delitem__(key_lower) |
|
|
|
self._list = [(k, v) for k, v in self._list if k.lower() != key_lower] |
|
|
|
def __eq__(self, other: Any) -> bool: |
|
if not isinstance(other, Headers): |
|
return NotImplemented |
|
return self._dict == other._dict |
|
|
|
def clear(self) -> None: |
|
""" |
|
Remove all headers. |
|
|
|
""" |
|
self._dict = {} |
|
self._list = [] |
|
|
|
def update(self, *args: HeadersLike, **kwargs: str) -> None: |
|
""" |
|
Update from a :class:`Headers` instance and/or keyword arguments. |
|
|
|
""" |
|
args = tuple( |
|
arg.raw_items() if isinstance(arg, Headers) else arg for arg in args |
|
) |
|
super().update(*args, **kwargs) |
|
|
|
|
|
|
|
def get_all(self, key: str) -> list[str]: |
|
""" |
|
Return the (possibly empty) list of all values for a header. |
|
|
|
Args: |
|
key: Header name. |
|
|
|
""" |
|
return self._dict.get(key.lower(), []) |
|
|
|
def raw_items(self) -> Iterator[tuple[str, str]]: |
|
""" |
|
Return an iterator of all values as ``(name, value)`` pairs. |
|
|
|
""" |
|
return iter(self._list) |
|
|
|
|
|
|
|
class SupportsKeysAndGetItem(Protocol): |
|
""" |
|
Dict-like types with ``keys() -> str`` and ``__getitem__(key: str) -> str`` methods. |
|
|
|
""" |
|
|
|
def keys(self) -> Iterable[str]: ... |
|
|
|
def __getitem__(self, key: str) -> str: ... |
|
|
|
|
|
|
|
HeadersLike = Union[ |
|
Headers, |
|
Mapping[str, str], |
|
Iterable[tuple[str, str]], |
|
SupportsKeysAndGetItem, |
|
] |
|
""" |
|
Types accepted where :class:`Headers` is expected. |
|
|
|
In addition to :class:`Headers` itself, this includes dict-like types where both |
|
keys and values are :class:`str`. |
|
|
|
""" |
|
|