Skip to content

Commit

Permalink
fix cache control issues
Browse files Browse the repository at this point in the history
Cache-Control no-transform directive is a boolean

no-transform has no arguments as a request or response directive (RFC
9111). Prior to this fix, cc.no_transform would return None whether the
directive is present or not.

Cache-Control min-fresh directive requires argument

The type for this property is `int | None`, so getting `"*"` for a
malformed directive is surprising. I think dropping the empty value here
is better than fixing the type.

Fix CacheControl getter type stubs

- cache_control_property with type=bool never return None
- some non-bool types were marked as returning bool instead of str
- max_stale can return "*" in addition to int or None

Reflect immutability of RequestCacheControl in type stubs

Fix CacheControl setter type stubs

mypy doesn't use the type of setters as of 1.9.0 (see python/mypy#3004),
but I think it's still good to have these be accurate (maybe the other
type checkers work better here).

mypy's recommendation is to use `# type: ignore` comments if setter
types don't match getters, which you see when setting no_cache to True.

Support must-understand response directive
  • Loading branch information
RazerM authored and davidism committed Jul 12, 2024
1 parent 8556654 commit b28001e
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Version 3.1.0
Unreleased

- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`
- ``CacheControl.no_transform`` is a boolean when present. ``min_fresh`` is
``None`` when not present. Added the ``must_understand`` attribute. Fixed
some typing issues on cache control. :issue:`2881`


Version 3.0.3
-------------
Expand Down
21 changes: 19 additions & 2 deletions src/werkzeug/datastructures/cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ class _CacheControl(UpdateDictMixin, dict):
to subclass it and add your own items have a look at the sourcecode for
that class.
.. versionchanged:: 3.1
``no_transform`` is a boolean when present.
.. versionchanged:: 2.1.0
Setting int properties such as ``max_age`` will convert the
value to an int.
Expand All @@ -58,7 +62,7 @@ class _CacheControl(UpdateDictMixin, dict):
no_cache = cache_control_property("no-cache", "*", None)
no_store = cache_control_property("no-store", None, bool)
max_age = cache_control_property("max-age", -1, int)
no_transform = cache_control_property("no-transform", None, None)
no_transform = cache_control_property("no-transform", None, bool)

def __init__(self, values=(), on_update=None):
dict.__init__(self, values or ())
Expand Down Expand Up @@ -127,6 +131,12 @@ class RequestCacheControl(ImmutableDictMixin, _CacheControl):
you plan to subclass it and add your own items have a look at the sourcecode
for that class.
.. versionchanged:: 3.1
``no_transform`` is a boolean when present.
.. versionchanged:: 3.1
``min_fresh`` is ``None`` if a value is not provided for the attribute.
.. versionchanged:: 2.1.0
Setting int properties such as ``max_age`` will convert the
value to an int.
Expand All @@ -137,7 +147,7 @@ class RequestCacheControl(ImmutableDictMixin, _CacheControl):
"""

max_stale = cache_control_property("max-stale", "*", int)
min_fresh = cache_control_property("min-fresh", "*", int)
min_fresh = cache_control_property("min-fresh", None, int)
only_if_cached = cache_control_property("only-if-cached", None, bool)


Expand All @@ -151,6 +161,12 @@ class ResponseCacheControl(_CacheControl):
you plan to subclass it and add your own items have a look at the sourcecode
for that class.
.. versionchanged:: 3.1
``no_transform`` is a boolean when present.
.. versionchanged:: 3.1
Added the ``must_understand`` attribute.
.. versionchanged:: 2.1.1
``s_maxage`` converts the value to an int.
Expand All @@ -169,6 +185,7 @@ class ResponseCacheControl(_CacheControl):
proxy_revalidate = cache_control_property("proxy-revalidate", None, bool)
s_maxage = cache_control_property("s-maxage", None, int)
immutable = cache_control_property("immutable", None, bool)
must_understand = cache_control_property("must-understand", None, bool)


# circular dependencies
Expand Down
49 changes: 26 additions & 23 deletions src/werkzeug/datastructures/cache_control.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Callable
from collections.abc import Iterable
from collections.abc import Mapping
from typing import Literal
from typing import TypeVar

from .mixins import ImmutableDictMixin
Expand All @@ -24,13 +25,13 @@ class _CacheControl(
on_update: Callable[[_CacheControl], None] | None = None,
) -> None: ...
@property
def no_cache(self) -> bool | None: ...
def no_cache(self) -> str | None: ...
@no_cache.setter
def no_cache(self, value: bool | None) -> None: ...
def no_cache(self, value: Literal[True] | str | None) -> None: ...
@no_cache.deleter
def no_cache(self) -> None: ...
@property
def no_store(self) -> bool | None: ...
def no_store(self) -> bool: ...
@no_store.setter
def no_store(self, value: bool | None) -> None: ...
@no_store.deleter
Expand All @@ -42,7 +43,7 @@ class _CacheControl(
@max_age.deleter
def max_age(self) -> None: ...
@property
def no_transform(self) -> bool | None: ...
def no_transform(self) -> bool: ...
@no_transform.setter
def no_transform(self, value: bool | None) -> None: ...
@no_transform.deleter
Expand All @@ -57,46 +58,42 @@ class _CacheControl(
class RequestCacheControl( # type: ignore[misc]
ImmutableDictMixin[str, str | int | bool | None], _CacheControl
):
@property # type: ignore
def no_cache(self) -> str | None: ...
@property # type: ignore
def no_store(self) -> bool: ...
@property # type: ignore
def max_age(self) -> int | None: ...
@property # type: ignore
def no_transform(self) -> bool: ...
@property
def max_stale(self) -> int | None: ...
@max_stale.setter
def max_stale(self, value: int | None) -> None: ...
@max_stale.deleter
def max_stale(self) -> None: ...
def max_stale(self) -> int | Literal["*"] | None: ...
@property
def min_fresh(self) -> int | None: ...
@min_fresh.setter
def min_fresh(self, value: int | None) -> None: ...
@min_fresh.deleter
def min_fresh(self) -> None: ...
@property
def only_if_cached(self) -> bool | None: ...
@only_if_cached.setter
def only_if_cached(self, value: bool | None) -> None: ...
@only_if_cached.deleter
def only_if_cached(self) -> None: ...

class ResponseCacheControl(_CacheControl):
@property
def public(self) -> bool | None: ...
def public(self) -> bool: ...
@public.setter
def public(self, value: bool | None) -> None: ...
@public.deleter
def public(self) -> None: ...
@property
def private(self) -> bool | None: ...
def private(self) -> str | None: ...
@private.setter
def private(self, value: bool | None) -> None: ...
def private(self, value: Literal[True] | str | None) -> None: ...
@private.deleter
def private(self) -> None: ...
@property
def must_revalidate(self) -> bool | None: ...
def must_revalidate(self) -> bool: ...
@must_revalidate.setter
def must_revalidate(self, value: bool | None) -> None: ...
@must_revalidate.deleter
def must_revalidate(self) -> None: ...
@property
def proxy_revalidate(self) -> bool | None: ...
def proxy_revalidate(self) -> bool: ...
@proxy_revalidate.setter
def proxy_revalidate(self, value: bool | None) -> None: ...
@proxy_revalidate.deleter
Expand All @@ -108,8 +105,14 @@ class ResponseCacheControl(_CacheControl):
@s_maxage.deleter
def s_maxage(self) -> None: ...
@property
def immutable(self) -> bool | None: ...
def immutable(self) -> bool: ...
@immutable.setter
def immutable(self, value: bool | None) -> None: ...
@immutable.deleter
def immutable(self) -> None: ...
@property
def must_understand(self) -> bool: ...
@must_understand.setter
def must_understand(self, value: bool | None) -> None: ...
@must_understand.deleter
def must_understand(self) -> None: ...
2 changes: 1 addition & 1 deletion src/werkzeug/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ def send_file(
elif mtime is not None:
rv.last_modified = mtime # type: ignore

rv.cache_control.no_cache = True
rv.cache_control.no_cache = True # type: ignore[assignment]

# Flask will pass app.get_send_file_max_age, allowing its send_file
# wrapper to not have to deal with paths.
Expand Down
20 changes: 20 additions & 0 deletions tests/test_datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,26 @@ def test_set_none(self):
cc.no_cache = False
assert cc.no_cache is False

def test_no_transform(self):
cc = ds.RequestCacheControl([("no-transform", None)])
assert cc.no_transform is True
cc = ds.RequestCacheControl()
assert cc.no_transform is False

def test_min_fresh(self):
cc = ds.RequestCacheControl([("min-fresh", "0")])
assert cc.min_fresh == 0
cc = ds.RequestCacheControl([("min-fresh", None)])
assert cc.min_fresh is None
cc = ds.RequestCacheControl()
assert cc.min_fresh is None

def test_must_understand(self):
cc = ds.ResponseCacheControl([("must-understand", None)])
assert cc.must_understand is True
cc = ds.ResponseCacheControl()
assert cc.must_understand is False


class TestContentSecurityPolicy:
def test_construct(self):
Expand Down

0 comments on commit b28001e

Please sign in to comment.