1from __future__ import annotations
2
3import collections.abc as cabc
4import typing as t
5
6from ..http import dump_header
7from ..http import parse_list_header
8
9
10class HeaderSet(cabc.MutableSet[str]):
11 """A parsed set of case-insensitive values from a header. Retains the
12 original case and order that values were parsed or added.
13
14 :attr:`.Request.access_control_allow_headers` returns an instance.
15
16 Set :attr:`.Response.vary`, :attr:`.Response.content_language`,
17 :attr:`.Response.allow`, :attr:`.Response.access_control_allow_headers`,
18 :attr:`.Response.access_control_allow_methods`, and
19 :attr:`.Response.access_control_expose_headers` to an instance to set the
20 header. Modifying the instance will update the header.
21
22 .. versionchanged:: 3.2
23 The ``on_update`` parameter was removed.
24 """
25
26 def __init__(self, headers: cabc.Iterable[str] | None = None) -> None:
27 self._order = list(headers or ())
28 self._lower_set = {x.lower() for x in self._order}
29 self._on_update: cabc.Callable[[HeaderSet], None] | None = None
30
31 def add(self, value: str) -> None:
32 """Add a value to the set.
33
34 :param value: The value to add.
35 """
36 self.update((value,))
37
38 def remove(self, value: str) -> None:
39 """Remove a value from the set.
40
41 :param value: The value to remove.
42 :raises KeyError: If the value is not in the set.
43
44 .. versionchanged:: 0.5
45 Raises ``KeyError`` instead of ``IndexError``.
46 """
47 value_lower = value.lower()
48
49 if value_lower not in self._lower_set:
50 raise KeyError(value)
51
52 self._lower_set.remove(value_lower)
53
54 for idx, order_value in enumerate(self._order):
55 if order_value.lower() == value_lower:
56 del self._order[idx]
57 break
58
59 if self._on_update is not None:
60 self._on_update(self)
61
62 def update(self, iterable: cabc.Iterable[str]) -> None:
63 """Add all values to the set.
64
65 :param iterable: The values to add.
66 """
67 inserted_any = False
68
69 for value in iterable:
70 value_lower = value.lower()
71
72 if value_lower not in self._lower_set:
73 self._order.append(value)
74 self._lower_set.add(value_lower)
75 inserted_any = True
76
77 if inserted_any and self._on_update is not None:
78 self._on_update(self)
79
80 def discard(self, value: str) -> None:
81 """Remove a value from the set if it is present.
82
83 :param value: The value to remove.
84 """
85 try:
86 self.remove(value)
87 except KeyError:
88 pass
89
90 def find(self, value: str) -> int:
91 """Return the index of the value in the set, or -1 if not found.
92
93 :param value: The value to find.
94 """
95 value_lower = value.lower()
96
97 for idx, order_value in enumerate(self._order):
98 if order_value.lower() == value_lower:
99 return idx
100
101 return -1
102
103 def index(self, value: str) -> int:
104 """Return the index of the value in the set.
105
106 :param value: The value to find.
107 :raises IndexError: If the value is not in the set.
108 """
109 if (rv := self.find(value)) == -1:
110 raise IndexError(value)
111
112 return rv
113
114 def clear(self) -> None:
115 """Remove all values from the set."""
116 self._lower_set.clear()
117 self._order.clear()
118
119 if self._on_update is not None:
120 self._on_update(self)
121
122 def as_set(self, preserve_casing: bool = False) -> set[str]:
123 """Convert to a plain :class:`set`. Unlike ``set(hs)``, values will be
124 lowercase instead of their original case.
125
126 :param preserve_casing: Use the original values instead of the lowercase
127 values. Equivalent to ``set(hs)``.
128 """
129 if preserve_casing:
130 return set(self._order)
131 return set(self._lower_set)
132
133 @classmethod
134 def from_header(cls, value: str | None) -> t.Self:
135 """Parse a header value and create an instance of this class.
136
137 .. versionadded:: 3.2
138 """
139 if not value:
140 return cls()
141
142 return cls(parse_list_header(value))
143
144 def to_header(self) -> str:
145 """Convert to a header value."""
146 return dump_header(self._order)
147
148 def __getitem__(self, idx: t.SupportsIndex) -> str:
149 return self._order[idx]
150
151 def __delitem__(self, idx: t.SupportsIndex) -> None:
152 value = self._order.pop(idx)
153 self._lower_set.remove(value.lower())
154
155 if self._on_update is not None:
156 self._on_update(self)
157
158 def __setitem__(self, idx: t.SupportsIndex, value: str) -> None:
159 self._lower_set.remove(self._order[idx].lower())
160 self._order[idx] = value
161 self._lower_set.add(value.lower())
162
163 if self._on_update is not None:
164 self._on_update(self)
165
166 def __contains__(self, value: str) -> bool: # type: ignore[override]
167 return value.lower() in self._lower_set
168
169 def __len__(self) -> int:
170 return len(self._lower_set)
171
172 def __iter__(self) -> cabc.Iterator[str]:
173 return iter(self._order)
174
175 def __bool__(self) -> bool:
176 return bool(self._lower_set)
177
178 def __str__(self) -> str:
179 return self.to_header()
180
181 def __repr__(self) -> str:
182 return f"{type(self).__name__}({self._order!r})"