Skip to content

Commit e3a50c9

Browse files
committed
more cache-control cleanup
1 parent 1eb7ada commit e3a50c9

File tree

5 files changed

+167
-71
lines changed

5 files changed

+167
-71
lines changed

CHANGES.rst

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,33 @@ Unreleased
1212
error. :issue:`2964`
1313
- ``OrderedMultiDict`` and ``ImmutableOrderedMultiDict`` are deprecated.
1414
Use ``MultiDict`` and ``ImmutableMultiDict`` instead. :issue:`2968`
15+
- Behavior of properties on ``request.cache_control`` and
16+
``response.cache_control`` has been significantly adjusted.
17+
18+
- Dict values are always ``str | None``. Setting properties will convert
19+
the value to a string. Setting a property to ``False`` is equivalent to
20+
setting it to ``None``. Getting typed properties will return ``None`` if
21+
conversion raises ``ValueError``, rather than the string. :issue:`2980`
22+
- ``max_age`` is ``None`` if not present, rather than ``-1``.
23+
:issue:`2980`
24+
- ``no_cache`` is a boolean for requests, it is ``False`` instead of
25+
``"*"`` when not present. It remains a string for responses.
26+
issue:`2980`
27+
- ``max_stale`` is an int, it is ``None`` instead of ``"*"`` if it is
28+
present with no value. ``max_stale_any`` is a boolean indicating if
29+
the property is present regardless of if it has a value. :issue:`2980`
30+
- ``no_transform`` is a boolean. Previously it was mistakenly always
31+
``None``. :issue:`2881`
32+
- ``min_fresh`` is ``None`` if not present instead of ``"*"``.
33+
:issue:`2881`
34+
- ``private`` is a boolean, it is ``False`` instead of ``"*"`` when not
35+
present. :issue:`2980`
36+
- Added the ``must_understand`` property. :issue:`2881`
37+
- Added the ``stale_while_revalidate``, and ``stale_if_error``
38+
properties. :issue:`2948`
39+
- Type annotations more accurately reflect the values. :issue:`2881`
40+
1541
- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`
16-
- ``CacheControl.no_transform`` is a boolean when present. ``min_fresh`` is
17-
``None`` when not present. Added the ``must_understand`` attribute. Fixed
18-
some typing issues on cache control. :issue:`2881`
19-
- Add ``stale_while_revalidate`` and ``stale_if_error`` properties to
20-
``ResponseCacheControl``. :issue:`2948`
2142
- Add 421 ``MisdirectedRequest`` HTTP exception. :issue:`2850`
2243
- Increase default work factor for PBKDF2 to 1,000,000 iterations.
2344
:issue:`2969`

docs/datastructures.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,13 @@ HTTP Related
9393

9494
.. autoclass:: RequestCacheControl
9595
:members:
96-
:inherited-members:
96+
:inherited-members: ImmutableDictMixin, CallbackDict
97+
:member-order: groupwise
9798

9899
.. autoclass:: ResponseCacheControl
99100
:members:
100-
:inherited-members:
101+
:inherited-members: CallbackDict
102+
:member-order: groupwise
101103

102104
.. autoclass:: ETags
103105
:members:

src/werkzeug/datastructures/cache_control.py

Lines changed: 131 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,58 @@
22

33
import collections.abc as cabc
44
import typing as t
5+
from inspect import cleandoc
56

67
from .mixins import ImmutableDictMixin
78
from .structures import CallbackDict
89

910

10-
def cache_control_property(key: str, empty: t.Any, type: type[t.Any] | None) -> t.Any:
11+
def cache_control_property(
12+
key: str, empty: t.Any, type: type[t.Any] | None, *, doc: str | None = None
13+
) -> t.Any:
1114
"""Return a new property object for a cache header. Useful if you
1215
want to add support for a cache extension in a subclass.
1316
17+
:param key: The attribute name present in the parsed cache-control header dict.
18+
:param empty: The value to use if the key is present without a value.
19+
:param type: The type to convert the string value to instead of a string. If
20+
conversion raises a ``ValueError``, the returned value is ``None``.
21+
:param doc: The docstring for the property. If not given, it is generated
22+
based on the other params.
23+
24+
.. versionchanged:: 3.1
25+
Added the ``doc`` param.
26+
1427
.. versionchanged:: 2.0
1528
Renamed from ``cache_property``.
1629
"""
30+
if doc is None:
31+
parts = [f"The ``{key}`` attribute."]
32+
33+
if type is bool:
34+
parts.append("A ``bool``, either present or not.")
35+
else:
36+
if type is None:
37+
parts.append("A ``str``,")
38+
else:
39+
parts.append(f"A ``{type.__name__}``,")
40+
41+
if empty is not None:
42+
parts.append(f"``{empty!r}`` if present with no value,")
43+
44+
parts.append("or ``None`` if not present.")
45+
46+
doc = " ".join(parts)
47+
1748
return property(
1849
lambda x: x._get_cache_value(key, empty, type),
1950
lambda x, v: x._set_cache_value(key, v, type),
2051
lambda x: x._del_cache_value(key),
21-
f"accessor for {key!r}",
52+
doc=cleandoc(doc),
2253
)
2354

2455

25-
class _CacheControl(CallbackDict[str, t.Any]):
56+
class _CacheControl(CallbackDict[str, t.Optional[str]]):
2657
"""Subclass of a dict that stores values for a Cache-Control header. It
2758
has accessors for all the cache-control directives specified in RFC 2616.
2859
The class does not differentiate between request and response directives.
@@ -36,36 +67,25 @@ class _CacheControl(CallbackDict[str, t.Any]):
3667
that class.
3768
3869
.. versionchanged:: 3.1
70+
Dict values are always ``str | None``. Setting properties will
71+
convert the value to a string. Setting a non-bool property to
72+
``False`` is equivalent to setting it to ``None``. Getting typed
73+
properties will return ``None`` if conversion raises
74+
``ValueError``, rather than the string.
3975
40-
``no_transform`` is a boolean when present.
41-
42-
.. versionchanged:: 2.1.0
76+
.. versionchanged:: 2.1
4377
Setting int properties such as ``max_age`` will convert the
4478
value to an int.
4579
4680
.. versionchanged:: 0.4
47-
48-
Setting `no_cache` or `private` to boolean `True` will set the implicit
49-
none-value which is ``*``:
50-
51-
>>> cc = ResponseCacheControl()
52-
>>> cc.no_cache = True
53-
>>> cc
54-
<ResponseCacheControl 'no-cache'>
55-
>>> cc.no_cache
56-
'*'
57-
>>> cc.no_cache = None
58-
>>> cc
59-
<ResponseCacheControl ''>
60-
61-
In versions before 0.5 the behavior documented here affected the now
62-
no longer existing `CacheControl` class.
81+
Setting ``no_cache`` or ``private`` to ``True`` will set the
82+
implicit value ``"*"``.
6383
"""
6484

65-
no_cache: str | bool | None = cache_control_property("no-cache", "*", None)
6685
no_store: bool = cache_control_property("no-store", None, bool)
67-
max_age: int | None = cache_control_property("max-age", -1, int)
86+
max_age: int | None = cache_control_property("max-age", None, int)
6887
no_transform: bool = cache_control_property("no-transform", None, bool)
88+
stale_if_error: int | None = cache_control_property("stale-if-error", None, int)
6989

7090
def __init__(
7191
self,
@@ -81,17 +101,20 @@ def _get_cache_value(
81101
"""Used internally by the accessor properties."""
82102
if type is bool:
83103
return key in self
84-
if key in self:
85-
value = self[key]
86-
if value is None:
87-
return empty
88-
elif type is not None:
89-
try:
90-
value = type(value)
91-
except ValueError:
92-
pass
93-
return value
94-
return None
104+
105+
if key not in self:
106+
return None
107+
108+
if (value := self[key]) is None:
109+
return empty
110+
111+
if type is not None:
112+
try:
113+
value = type(value)
114+
except ValueError:
115+
return None
116+
117+
return value
95118

96119
def _set_cache_value(
97120
self, key: str, value: t.Any, type: type[t.Any] | None
@@ -102,16 +125,15 @@ def _set_cache_value(
102125
self[key] = None
103126
else:
104127
self.pop(key, None)
128+
elif value is None or value is False:
129+
self.pop(key, None)
130+
elif value is True:
131+
self[key] = None
105132
else:
106-
if value is None:
107-
self.pop(key, None)
108-
elif value is True:
109-
self[key] = None
110-
else:
111-
if type is not None:
112-
self[key] = type(value)
113-
else:
114-
self[key] = value
133+
if type is not None:
134+
value = type(value)
135+
136+
self[key] = str(value)
115137

116138
def _del_cache_value(self, key: str) -> None:
117139
"""Used internally by the accessor properties."""
@@ -132,7 +154,7 @@ def __repr__(self) -> str:
132154
cache_property = staticmethod(cache_control_property)
133155

134156

135-
class RequestCacheControl(ImmutableDictMixin[str, t.Any], _CacheControl): # type: ignore[misc]
157+
class RequestCacheControl(ImmutableDictMixin[str, t.Optional[str]], _CacheControl): # type: ignore[misc]
136158
"""A cache control for requests. This is immutable and gives access
137159
to all the request-relevant cache control headers.
138160
@@ -142,21 +164,61 @@ class RequestCacheControl(ImmutableDictMixin[str, t.Any], _CacheControl): # typ
142164
for that class.
143165
144166
.. versionchanged:: 3.1
145-
``no_transform`` is a boolean when present.
167+
Dict values are always ``str | None``. Setting properties will
168+
convert the value to a string. Setting a non-bool property to
169+
``False`` is equivalent to setting it to ``None``. Getting typed
170+
properties will return ``None`` if conversion raises
171+
``ValueError``, rather than the string.
172+
173+
.. versionchanged:: 3.1
174+
``max_age`` is ``None`` if not present, rather than ``-1``.
175+
176+
.. versionchanged:: 3.1
177+
``no_cache`` is a boolean, it is ``False`` instead of ``"*"``
178+
when not present.
179+
180+
.. versionchanged:: 3.1
181+
``max_stale`` is an int, it is ``None`` instead of ``"*"`` if it is
182+
present with no value. ``max_stale_any`` is a boolean indicating if
183+
the property is present regardless of if it has a value.
146184
147185
.. versionchanged:: 3.1
148-
``min_fresh`` is ``None`` if a value is not provided for the attribute.
186+
``no_transform`` is a boolean. Previously it was mistakenly
187+
always ``None``.
149188
150-
.. versionchanged:: 2.1.0
189+
.. versionchanged:: 3.1
190+
``min_fresh`` is ``None`` if not present instead of ``"*"``.
191+
192+
.. versionchanged:: 2.1
151193
Setting int properties such as ``max_age`` will convert the
152194
value to an int.
153195
154196
.. versionadded:: 0.5
155-
In previous versions a `CacheControl` class existed that was used
156-
both for request and response.
197+
Response-only properties are not present on this request class.
157198
"""
158199

159-
max_stale: str | int | None = cache_control_property("max-stale", "*", int)
200+
no_cache: bool = cache_control_property("no-cache", None, bool)
201+
max_stale: int | None = cache_control_property(
202+
"max-stale",
203+
None,
204+
int,
205+
doc="""The ``max-stale`` attribute if it has a value. A ``int``, or
206+
``None`` if not present or no value.
207+
208+
This attribute can also be present without a value. To check that, use
209+
:attr:`max_stale_any`.
210+
""",
211+
)
212+
max_stale_any: bool = cache_control_property(
213+
"max-stale",
214+
None,
215+
bool,
216+
doc="""The ``max-stale`` attribute presence regardless of value. A
217+
``bool``, either present or not.
218+
219+
To check the value of the attribute if present, use :attr:`max_stale`.
220+
""",
221+
)
160222
min_fresh: int | None = cache_control_property("min-fresh", None, int)
161223
only_if_cached: bool = cache_control_property("only-if-cached", None, bool)
162224

@@ -172,26 +234,38 @@ class ResponseCacheControl(_CacheControl):
172234
for that class.
173235
174236
.. versionchanged:: 3.1
175-
``no_transform`` is a boolean when present.
237+
Dict values are always ``str | None``. Setting properties will
238+
convert the value to a string. Setting a non-bool property to
239+
``False`` is equivalent to setting it to ``None``. Getting typed
240+
properties will return ``None`` if conversion raises
241+
``ValueError``, rather than the string.
242+
243+
.. versionchanged:: 3.1
244+
``private`` is a boolean, it is ``False`` instead of ``"*"``
245+
when not present.
246+
247+
.. versionchanged:: 3.1
248+
``no_transform`` is a boolean. Previously it was mistakenly always
249+
``None``.
176250
177251
.. versionchanged:: 3.1
178252
Added the ``must_understand``, ``stale_while_revalidate``, and
179-
``stale_if_error`` attributes.
253+
``stale_if_error`` properties.
180254
181255
.. versionchanged:: 2.1.1
182256
``s_maxage`` converts the value to an int.
183257
184-
.. versionchanged:: 2.1.0
258+
.. versionchanged:: 2.1
185259
Setting int properties such as ``max_age`` will convert the
186260
value to an int.
187261
188262
.. versionadded:: 0.5
189-
In previous versions a `CacheControl` class existed that was used
190-
both for request and response.
263+
Request-only properties are not present on this response class.
191264
"""
192265

266+
no_cache: str | bool | None = cache_control_property("no-cache", "*", None)
193267
public: bool = cache_control_property("public", None, bool)
194-
private: str | None = cache_control_property("private", "*", None)
268+
private: bool = cache_control_property("private", None, bool)
195269
must_revalidate: bool = cache_control_property("must-revalidate", None, bool)
196270
proxy_revalidate: bool = cache_control_property("proxy-revalidate", None, bool)
197271
s_maxage: int | None = cache_control_property("s-maxage", None, int)
@@ -200,7 +274,6 @@ class ResponseCacheControl(_CacheControl):
200274
stale_while_revalidate: int | None = cache_control_property(
201275
"stale-while-revalidate", None, int
202276
)
203-
stale_if_error: int | None = cache_control_property("stale-if-error", None, int)
204277

205278

206279
# circular dependencies

tests/test_datastructures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@ def test_set_none(self):
10001000
cc.no_cache = None
10011001
assert cc.no_cache is None
10021002
cc.no_cache = False
1003-
assert cc.no_cache is False
1003+
assert cc.no_cache is None
10041004

10051005
def test_no_transform(self):
10061006
cc = ds.RequestCacheControl([("no-transform", None)])

tests/test_http.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,22 @@ def test_dict_header(self, value, expect):
121121
def test_cache_control_header(self):
122122
cc = http.parse_cache_control_header("max-age=0, no-cache")
123123
assert cc.max_age == 0
124-
assert cc.no_cache
124+
assert cc.no_cache is True
125125
cc = http.parse_cache_control_header(
126126
'private, community="UCI"', None, datastructures.ResponseCacheControl
127127
)
128-
assert cc.private
128+
assert cc.private is True
129129
assert cc["community"] == "UCI"
130130

131131
c = datastructures.ResponseCacheControl()
132132
assert c.no_cache is None
133-
assert c.private is None
133+
assert c.private is False
134134
c.no_cache = True
135135
assert c.no_cache == "*"
136136
c.private = True
137-
assert c.private == "*"
137+
assert c.private is True
138138
del c.private
139-
assert c.private is None
139+
assert c.private is False
140140
# max_age is an int, other types are converted
141141
c.max_age = 3.1
142142
assert c.max_age == 3

0 commit comments

Comments
 (0)