Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix CacheControl issues #2882

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/werkzeug/datastructures/cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,17 @@ class _CacheControl(UpdateDictMixin, dict):

In versions before 0.5 the behavior documented here affected the now
no longer existing `CacheControl` class.

.. versionchanged:: 3.1

`no_transform` is now a boolean instead of returning `None` whether
the directive is present or not.
"""

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 @@ -134,10 +139,18 @@ class RequestCacheControl(ImmutableDictMixin, _CacheControl):
.. versionadded:: 0.5
In previous versions a `CacheControl` class existed that was used
both for request and response.

.. versionchanged:: 3.1

`no_transform` is now a boolean instead of returning `None` whether
the directive is present or not.

`min_fresh` no longer returns ``*`` if the directive is missing its
required argument.
"""

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 @@ -161,6 +174,13 @@ class ResponseCacheControl(_CacheControl):
.. versionadded:: 0.5
In previous versions a `CacheControl` class existed that was used
both for request and response.

.. versionchanged:: 3.1

`no_transform` is now a boolean instead of returning `None` whether
the directive is present or not.

`must_understand` added.
"""

public = cache_control_property("public", None, bool)
Expand All @@ -169,6 +189,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

# 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