1from __future__ import annotations
2
3import collections.abc as cabc
4import typing as t
5from inspect import cleandoc
6
7from .._internal import _plain_int
8from ..http import dump_header
9from ..http import parse_dict_header
10
11
12def _deprecated_cache_control_property(
13 key: str,
14 empty: t.Any,
15 type: type[t.Any] | None,
16 *,
17 doc: str | None = None,
18) -> t.Any:
19 """Create a property for a ``Cache-Control`` directive.
20
21 :param key: The directive name.
22 :param empty: The value when the directive is present without a value.
23 :param type: The type to convert the value to. A ``ValueError`` returns ``None``.
24 :param doc: The docstring for the property. If not given, it is generated
25 based on the other params.
26
27 .. deprecated:: 3.2
28 Will be removed in Werkzeug 4.0. Use indexing ``cc[key]`` for unknown
29 directives.
30
31 .. versionchanged:: 3.1
32 Added the ``doc`` parameter.
33
34 .. versionchanged:: 2.0
35 Renamed from ``cache_property``.
36 """
37 import warnings
38
39 warnings.warn(
40 "The 'cache_property' method is deprecated and will be removed in Werkzeug 4.0."
41 " Use indexing 'cc[key]' for unknown directives.",
42 DeprecationWarning,
43 stacklevel=2,
44 )
45 return _cache_control_property(key, type, empty=empty, mutable=True, doc=doc)
46
47
48def _cache_control_property(
49 key: str,
50 convert: type[t.Any] | None = None,
51 *,
52 empty: t.Any = None,
53 mutable: bool = False,
54 doc: str | None = None,
55) -> t.Any:
56 """Create a property for a ``Cache-Control`` directive.
57
58 :meta private:
59
60 :param key: The directive name.
61 :param empty: The value when the directive is present without a value.
62 :param convert: The type to convert the value to. A ``ValueError`` returns ``None``.
63 :param doc: The docstring for the property. If not given, it is generated
64 based on the other params.
65 :param mutable: Whether the property has a setter and a deleter.
66 """
67 if doc is None:
68 parts = [f"The ``{key}`` directive."]
69
70 if convert is bool:
71 parts.append("A ``bool``, ``True`` if present, ``False`` if not.")
72 else:
73 if convert is None:
74 parts.append("A ``str``,")
75 else:
76 parts.append(f"A ``{convert.__name__}``,")
77
78 if empty is not None:
79 parts.append(f"or ``{empty!r}`` if present with no value,")
80
81 parts.append("or ``None`` if not present.")
82
83 doc = " ".join(parts)
84
85 doc = cleandoc(doc)
86 get_convert = _plain_int if convert is int else convert
87
88 if not mutable:
89 return property(lambda x: x._get_directive(key, empty, get_convert), doc=doc)
90
91 return property(
92 lambda x: x._get_directive(key, empty, get_convert),
93 lambda x, v: x._set_directive(key, v, convert),
94 lambda x: x._del_directive(key),
95 doc=doc,
96 )
97
98
99class _CacheControl(cabc.Mapping[str, str | None]):
100 _data: cabc.Mapping[str, str | None]
101
102 def __getitem__(self, key: str, /) -> str | None:
103 return self._data[key]
104
105 def __len__(self) -> int:
106 return len(self._data)
107
108 def __iter__(self) -> cabc.Iterator[str]:
109 return iter(self._data)
110
111 def _get_directive(
112 self, key: str, empty: t.Any, convert: t.Callable[[str], t.Any] | None
113 ) -> t.Any:
114 if convert is bool:
115 return key in self
116
117 if key not in self:
118 return None
119
120 if (value := self[key]) is None:
121 return empty
122
123 if convert is None:
124 return value
125
126 try:
127 return convert(value)
128 except ValueError:
129 return None
130
131 @classmethod
132 def from_header(cls, value: str | None) -> t.Self:
133 """Parse a ``Cache-Control`` header value and create an instance of this
134 class.
135
136 .. versionadded:: 3.2
137 """
138 if not value:
139 return cls()
140
141 return cls(parse_dict_header(value)) # type: ignore[call-arg]
142
143 def to_header(self) -> str:
144 """Convert to a ``Cache-Control`` header value."""
145 return dump_header(self._data)
146
147 def __str__(self) -> str:
148 return self.to_header()
149
150 def __repr__(self) -> str:
151 kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self._data.items()))
152 return f"<{type(self).__name__} {kv_str}>"
153
154 cache_property = staticmethod(_deprecated_cache_control_property)
155
156
157class RequestCacheControl(_CacheControl):
158 """A parsed ``Cache-Control`` request header.
159 :attr:`.Request.cache_control` returns an instance.
160
161 Typically, you'll access the various directive properties. It also allows
162 indexing `cc[directive]` to access unknown directives that do not have
163 corresponding properties.
164
165 :param values: Values parsed from the request header.
166
167 .. versionchanged:: 3.2
168 Inherits ``Mapping`` instead of ``ImmutableDict``.
169
170 The ``on_update`` parameter was removed.
171
172 The ``cache_property`` method is deprecated and will be removed in
173 Werkzeug 4.0. Use indexing ``cc[key]`` for unknown directives.
174
175 .. versionchanged:: 3.1
176 Dict values are always ``str | None``. Setting properties will
177 convert the value to a string. Setting a non-bool property to
178 ``False`` is equivalent to setting it to ``None``. Getting typed
179 properties will return ``None`` if conversion raises
180 ``ValueError``, rather than the string.
181
182 ``max_age`` is ``None`` if present without a value, rather
183 than ``-1``.
184
185 ``no_cache`` is a boolean, it is ``True`` instead of ``"*"``
186 when present.
187
188 ``max_stale`` is ``True`` if present without a value, rather
189 than ``"*"``.
190
191 ``no_transform`` is a boolean. Previously it was mistakenly
192 always ``None``.
193
194 ``min_fresh`` is ``None`` if present without a value, rather
195 than ``"*"``.
196
197 .. versionchanged:: 2.1
198 Setting int properties such as ``max_age`` will convert the
199 value to an int.
200
201 .. versionadded:: 0.5
202 Response-only properties are not present on this request class.
203 """
204
205 def __init__(
206 self,
207 values: cabc.Mapping[str, str | None]
208 | cabc.Iterable[tuple[str, str | None]]
209 | None = None,
210 ) -> None:
211 if values is None:
212 values = {}
213 elif not isinstance(values, cabc.Mapping):
214 import warnings
215
216 warnings.warn(
217 "Passing an iterable instead of a mapping is deprecated and"
218 " will be removed in Werkzeug 4.0.",
219 DeprecationWarning,
220 stacklevel=2,
221 )
222 values = dict(values)
223
224 self._data = values
225
226 max_age = _cache_control_property("max-age", int)
227 max_stale: int | t.Literal[True] | None = _cache_control_property(
228 "max-stale", int, empty=True
229 )
230 min_fresh: int | None = _cache_control_property("min-fresh", int)
231 no_cache: bool = _cache_control_property("no-cache", bool)
232 no_store: bool = _cache_control_property("no-store", bool)
233 no_transform: bool = _cache_control_property("no-transform", bool)
234 only_if_cached: bool = _cache_control_property("only-if-cached", bool)
235 stale_if_error: int | None = _cache_control_property("stale-if-error", int)
236
237
238class ResponseCacheControl(cabc.MutableMapping[str, str | None], _CacheControl):
239 """A parsed ``Cache-Control`` response header.
240
241 Set :attr:`.Response.cache_control` to an instance to set the header.
242 Modifying the instance will update the header.
243
244 Typically, you'll use the various directive properties. It also allows
245 indexing `cc[directive]` to get, set, or delete unknown directives that do
246 not have corresponding properties.
247
248 :param values: Initial values to set.
249
250 .. versionchanged:: 3.2
251 Inherits ``MutableMapping`` instead of ``dict``.
252
253 The ``on_update`` parameter was removed.
254
255 The ``cache_property`` method is deprecated and will be removed in
256 Werkzeug 4.0. Use indexing ``cc[key]`` for unknown directives.
257
258 .. versionchanged:: 3.1
259 Dict values are always ``str | None``. Setting properties will
260 convert the value to a string. Setting a non-bool property to
261 ``False`` is equivalent to setting it to ``None``. Getting typed
262 properties will return ``None`` if conversion raises
263 ``ValueError``, rather than the string.
264
265 ``no_cache`` is ``True`` if present without a value, rather than
266 ``"*"``.
267
268 ``private`` is ``True`` if present without a value, rather than
269 ``"*"``.
270
271 ``no_transform`` is a boolean. Previously it was mistakenly
272 always ``None``.
273
274 Added the ``must_understand``, ``stale_while_revalidate``, and
275 ``stale_if_error`` properties.
276
277 .. versionchanged:: 2.1.1
278 ``s_maxage`` converts the value to an int.
279
280 .. versionchanged:: 2.1
281 Setting int properties such as ``max_age`` will convert the
282 value to an int.
283
284 .. versionadded:: 0.5
285 Request-only properties are not present on this response class.
286
287 .. versionchanged:: 0.4
288 Setting ``no_cache`` or ``private`` to ``True`` will set the
289 implicit value ``"*"``.
290 """
291
292 _data: cabc.MutableMapping[str, str | None]
293
294 def __init__(
295 self,
296 values: cabc.Mapping[str, str | None]
297 | cabc.Iterable[tuple[str, str | None]]
298 | None = None,
299 ) -> None:
300 if values is None:
301 values = {}
302 elif isinstance(values, cabc.Mapping):
303 if not isinstance(values, cabc.MutableMapping):
304 values = dict(values)
305 else:
306 import warnings
307
308 warnings.warn(
309 "Passing an iterable instead of a mapping is deprecated and"
310 " will be removed in Werkzeug 4.0.",
311 DeprecationWarning,
312 stacklevel=2,
313 )
314 values = dict(values)
315
316 self._data = values
317 self._on_update: t.Callable[[ResponseCacheControl], None] | None = None
318
319 def _trigger_on_update(self) -> None:
320 if self._on_update is not None:
321 self._on_update(self)
322
323 def __setitem__(self, key: str, value: str | None, /) -> None:
324 self._data[key] = value
325 self._trigger_on_update()
326
327 def __delitem__(self, key: str, /) -> None:
328 del self._data[key]
329 self._trigger_on_update()
330
331 def _set_directive(
332 self, key: str, value: t.Any, convert: type[t.Any] | None
333 ) -> None:
334 if convert is bool:
335 if value:
336 self._data[key] = None
337 else:
338 self._data.pop(key, None)
339 elif value is None or value is False:
340 self._data.pop(key, None)
341 elif value is True:
342 self._data[key] = None
343 else:
344 if convert is not None:
345 value = convert(value)
346
347 self._data[key] = str(value)
348
349 self._trigger_on_update()
350
351 def _del_directive(self, key: str) -> None:
352 self._data.pop(key, None)
353 self._trigger_on_update()
354
355 max_age: int | None = _cache_control_property("max-age", int, mutable=True)
356 s_maxage: int | None = _cache_control_property("s-maxage", int, mutable=True)
357 # https://httpwg.org/specs/rfc9111.html#cache-response-directive.no-cache
358 # This can be with or without a value, not mentioned on MDN.
359 no_cache: str | t.Literal[True] | None = _cache_control_property(
360 "no-cache", str, empty=True, mutable=True
361 )
362 no_store: bool = _cache_control_property("no-store", bool, mutable=True)
363 no_transform: bool = _cache_control_property("no-transform", bool, mutable=True)
364 must_revalidate: bool = _cache_control_property(
365 "must-revalidate", bool, mutable=True
366 )
367 proxy_revalidate: bool = _cache_control_property(
368 "proxy-revalidate", bool, mutable=True
369 )
370 must_understand: bool = _cache_control_property(
371 "must-understand", bool, mutable=True
372 )
373 # https://httpwg.org/specs/rfc9111.html#cache-response-directive.private
374 # This can be with or without a value, not mentioned on MDN.
375 private: str | t.Literal[True] | None = _cache_control_property(
376 "private", str, empty=True, mutable=True
377 )
378 public: bool = _cache_control_property("public", bool, mutable=True)
379 immutable: bool = _cache_control_property("immutable", bool, mutable=True)
380 stale_while_revalidate: int | None = _cache_control_property(
381 "stale-while-revalidate", int, mutable=True
382 )
383 stale_if_error: int | None = _cache_control_property(
384 "stale-if-error", int, mutable=True
385 )
386
387
388if not t.TYPE_CHECKING:
389
390 def __getattr__(name: str) -> t.Any:
391 if name == "cache_control_property":
392 import warnings
393
394 warnings.warn(
395 "The 'cache_control_property' function is deprecated and will"
396 " be removed in Werkzeug 4.0. Use indexing 'cc[key]' for"
397 " unknown directives.",
398 DeprecationWarning,
399 stacklevel=2,
400 )
401 return _deprecated_cache_control_property
402
403 raise AttributeError(name)