← requests / src/requests/structures.py
| 1 | """ |
| 2 | requests.structures |
| 3 | ~~~~~~~~~~~~~~~~~~~ |
| 4 | |
| 5 | Data structures that power Requests. |
| 6 | """ |
| 7 | |
| 8 | from __future__ import annotations |
| 9 | |
| 10 | from collections import OrderedDict |
| 11 | from collections.abc import Iterable, Iterator, Mapping |
| 12 | from typing import Any, Generic, TypeVar, overload |
| 13 | |
| 14 | from .compat import MutableMapping |
| 15 | |
| 16 | _VT = TypeVar("_VT") |
| 17 | _D = TypeVar("_D") |
| 18 | |
| 19 | |
| 20 | class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]): |
| 21 | """A case-insensitive ``dict``-like object. |
| 22 | |
| 23 | Implements all methods and operations of |
| 24 | ``MutableMapping`` as well as dict's ``copy``. Also |
| 25 | provides ``lower_items``. |
| 26 | |
| 27 | All keys are expected to be strings. The structure remembers the |
| 28 | case of the last key to be set, and ``iter(instance)``, |
| 29 | ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` |
| 30 | will contain case-sensitive keys. However, querying and contains |
| 31 | testing is case insensitive:: |
| 32 | |
| 33 | cid = CaseInsensitiveDict() |
| 34 | cid['Accept'] = 'application/json' |
| 35 | cid['aCCEPT'] == 'application/json' # True |
| 36 | list(cid) == ['Accept'] # True |
| 37 | |
| 38 | For example, ``headers['content-encoding']`` will return the |
| 39 | value of a ``'Content-Encoding'`` response header, regardless |
| 40 | of how the header name was originally stored. |
| 41 | |
| 42 | If the constructor, ``.update``, or equality comparison |
| 43 | operations are given keys that have equal ``.lower()``s, the |
| 44 | behavior is undefined. |
| 45 | """ |
| 46 | |
| 47 | _store: OrderedDict[str, tuple[str, _VT]] |
| 48 | |
| 49 | def __init__( |
| 50 | self, |
| 51 | data: Mapping[str, _VT] | Iterable[tuple[str, _VT]] | None = None, |
| 52 | **kwargs: _VT, |
| 53 | ) -> None: |
| 54 | self._store = OrderedDict() |
| 55 | if data is None: |
| 56 | data = {} |
| 57 | self.update(data, **kwargs) |
| 58 | |
| 59 | def __setitem__(self, key: str, value: _VT) -> None: |
| 60 | # Use the lowercased key for lookups, but store the actual |
| 61 | # key alongside the value. |
| 62 | self._store[key.lower()] = (key, value) |
| 63 | |
| 64 | def __getitem__(self, key: str) -> _VT: |
| 65 | return self._store[key.lower()][1] |
| 66 | |
| 67 | def __delitem__(self, key: str) -> None: |
| 68 | del self._store[key.lower()] |
| 69 | |
| 70 | def __iter__(self) -> Iterator[str]: |
| 71 | return (casedkey for casedkey, _ in self._store.values()) |
| 72 | |
| 73 | def __len__(self) -> int: |
| 74 | return len(self._store) |
| 75 | |
| 76 | def lower_items(self) -> Iterator[tuple[str, _VT]]: |
| 77 | """Like iteritems(), but with all lowercase keys.""" |
| 78 | return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items()) |
| 79 | |
| 80 | def __eq__(self, other: object) -> bool: |
| 81 | if isinstance(other, Mapping): |
| 82 | other_dict: CaseInsensitiveDict[Any] = CaseInsensitiveDict(other) # type: ignore[reportUnknownArgumentType] |
| 83 | else: |
| 84 | return NotImplemented |
| 85 | # Compare insensitively |
| 86 | return dict(self.lower_items()) == dict(other_dict.lower_items()) |
| 87 | |
| 88 | # Copy is required |
| 89 | def copy(self) -> CaseInsensitiveDict[_VT]: |
| 90 | return CaseInsensitiveDict(self._store.values()) |
| 91 | |
| 92 | def __repr__(self) -> str: |
| 93 | return str(dict(self.items())) |
| 94 | |
| 95 | |
| 96 | class LookupDict(dict[str, _VT]): |
| 97 | """Dictionary lookup object.""" |
| 98 | |
| 99 | name: Any |
| 100 | |
| 101 | def __init__(self, name: Any = None) -> None: |
| 102 | self.name = name |
| 103 | super().__init__() |
| 104 | |
| 105 | def __repr__(self) -> str: |
| 106 | return f"<lookup '{self.name}'>" |
| 107 | |
| 108 | def __getattr__(self, key: str) -> _VT | None: |
| 109 | # We need this for type checkers to infer typing |
| 110 | # on attribute access with status_codes.py |
| 111 | if key in self.__dict__: |
| 112 | return self.__dict__[key] |
| 113 | else: |
| 114 | raise AttributeError( |
| 115 | f"'{type(self).__name__}' object has no attribute '{key}'" |
| 116 | ) |
| 117 | |
| 118 | def __getitem__(self, key: str) -> _VT | None: # type: ignore[override] |
| 119 | # We allow fall-through here, so values default to None |
| 120 | |
| 121 | return self.__dict__.get(key, None) |
| 122 | |
| 123 | @overload |
| 124 | def get(self, key: str, default: None = None) -> _VT | None: ... |
| 125 | |
| 126 | @overload |
| 127 | def get(self, key: str, default: _D | _VT) -> _D | _VT: ... |
| 128 | |
| 129 | def get(self, key: str, default: _D | None = None) -> _VT | _D | None: |
| 130 | return self.__dict__.get(key, default) |
| 131 |