Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
prune-cache: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: false
prune-cache: false
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@ jobs:
- {python: '3.13'}
- {python: '3.12'}
- {python: '3.11'}
- {python: '3.10'}
- {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
prune-cache: false
Expand All @@ -49,7 +48,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
enable-cache: true
prune-cache: false
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zizmor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
advanced-security: false
annotations: true
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ repos:
args:
- --ignore-words-list=fo,te,worl
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: cb8c523fd4835aba42af70f4cad5568db4df0b6c # frozen: v0.16.0
rev: a56c0b927e6465d37cae3e97d35d4d18ab2b96cd # frozen: v0.16.9
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 5900cba2cfe6d20f562458d4d308ac55569f92eb # frozen: 0.12.0
rev: 2aeb804518b4373dbf5adb8dddc5466d9a6e6b21 # frozen: 0.12.19
hooks:
- id: uv-lock
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
66 changes: 49 additions & 17 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
Version 3.2.0
-------------

- Drop support for Python 3.9. :pr:`3098`
- Drop support for Python 3.9 and 3.10. :pr:`3098`
- Remove previous deprecated code: :pr:`3099`

- ``OrderedMultiDict`` and ``ImmutableOrderedMultiDict are removed.
- ``OrderedMultiDict`` and ``ImmutableOrderedMultiDict`` are removed.
The base ``MultiDict`` already retains order.

- Minimum required version of MarkupSafe is 3.0.3.
Expand Down Expand Up @@ -117,6 +117,23 @@ Version 3.2.0
:issue:`3302`
- ``FileWrapper`` is deprecated. ``wrap_file`` will return the file as-is if
``wsgi.file_wrapper`` isn't provided by the server. :issue:`3301`
- ``Response.make_conditional`` is deprecated. It is split into
``apply_conditions`` and ``apply_ranges``. Preconditions apply to all
methods, are checked in the correct order and use the correct comparisons,
handle star ETag based on if the resource exists, and raise
``PreconditionFailed``. Range is applied after conditions, only supports
``bytes``, clamps too large ``stop`` and suffix values, and uses the first
range when multiple are given. :issue:`3291`
- ``Range.make_content_range`` clamps too large ``stop`` and suffix values.
:issue:`3291`
- ``Range.make_content_range`` uses the first range if multiple are given.
:issue:`3291`
- Added the ``IfMatch.check`` method. :issue:`3291`
- ``is_resource_modified`` is deprecated. ``Response.apply_conditions`` and
``apply_range`` perform their own checks. :issue:`3291`
- ``is_byte_range_valid`` is deprecated. ``Range.from_header``,
``Range.make_content_range``, and ``ContentRange.from_header`` validate
their values. :issue:`3291`
- ``redirect`` returns a ``303`` status code by default instead of ``302``.
This tells the client to always switch to ``GET``, rather than only
switching ``POST`` to ``GET``. This preserves the current behavior of
Expand Down Expand Up @@ -151,7 +168,7 @@ Version 3.2.0
an equal rule. :issue:`3037`
- Add ``Request.sec_fetch_site``, ``sec_fetch_mode``, ``sec_fetch_user``, and
``sec_fetch_dest`` header properties. :pr:`3082`
- ``Response.make_conditional`` sets the ``Accept-Ranges`` header even if it
- ``Response.apply_conditions`` sets the ``Accept-Ranges`` header even if it
is not a satisfiable range request. :issue:`3108`
- ``Request.host``, ``get_host``, and ``host_is_trusted`` validate the
characters of the value. An empty value is no longer allowed. A Unix socket
Expand All @@ -164,7 +181,7 @@ Version 3.2.0
- The development server does not send an extra ``100 Continue`` response, as
Python's base server already sends it. :issue:`3138`
- The ``int`` and ``float`` URL converters do not accept non-ASCII digits.
:issue:`3242`
:issue:`3242`
- The ``int`` and ``float`` URL converters validate the value when building
URLs. :issue:`3242`
- The ``int`` URL converter does not count the negative sign for
Expand All @@ -181,8 +198,7 @@ Version 3.2.0
- ``generate_password_hash`` uses ``secrets.token_urlsafe`` to generate salt.
The private ``gen_salt`` method is removed. :pr:`3167`
- The test ``Client`` has a ``query`` method for the ``QUERY`` request method.
- ``unquote_etag`` and ``ETags.from_header`` discard invalid unquoted values.
- ``ETags.__call__`` checks the ``*`` value.
- ``ETagSet.from_header`` discards invalid unquoted values.
- ``cached_property`` can be deleted even if it hasn't been accessed and
cached yet.
- ``Request`` and ``FileStorage`` header properties are cached to skip parsing
Expand All @@ -200,31 +216,47 @@ Version 3.2.0
:issue:`3295`
- Removed the ``DebuggedApplication`` ``request_key`` parameter and attribute.
:issue:`3297`
- ``send_file`` will detect the path, size, and modification time from
file-like objects if possible. :issue:`3291`
- ``send_file`` calls ``seek`` on file-like objects if possible, to reset the
position to 0. This fixes a common bug, and ensures range responses show the
correct complete and content lengths. :issue:`3291`
- ``send_file`` does not include a hash of the path in the ETag. :issue:`3291`
- ``send_file`` defaults to the ``application/octet-stream`` mimetype if
``download_name`` is not passed, instead of raising. :issue:`3291`
- ``SharedDataMiddleware`` uses ``send_file``. ``cache_timeout`` is not set by
default, enabling conditional caching. `` The ``cache`` and
``fallback_mimetype`` parameters are deprecated. :issue:`3291`


Version 3.1.9
-------------

Unreleased
Released 2026-09-27

- ``ProfilerMiddleware`` uses ``profiling.tracing`` on Python 3.15. :issue:`3207`
- ``safe_join`` on Windows does not allow special devices names with empty ADS
markers on NTFS. :ghsa:`g6x2-hccm-hh4m`
- ``ProfilerMiddleware`` uses ``profiling.tracing`` on Python 3.15.
:issue:`3207`
- ``uri_to_iri`` and ``iri_to_uri`` preserve empty username, password, and
port 0. :issue:`3189`
- Improve performance of ``parse_options_header``.
- Improve performance of ``parse_etags``.
- Improve performance of ``parse_cookie``.
- ``get_host`` also checks that the port is in the valid range.
- Improve performance of ``parse_options_header``. :pr:`3231`
- Improve performance of ``parse_etags``. :pr:`3231`
- Improve performance of ``parse_cookie``. :pr:`3231`
- ``get_host`` also checks that the port is in the valid range. :pr:`3236`
- The ``int`` URL converter returns a 404 instead of 500 error when the value
is longer than ``sys.get_int_max_str_digits()``. :issue:`3237`
- Improve debugger PIN generation from cgroup data inside Podman. :issue:`3245`
- Improve debugger PIN generation from cgroup data inside Podman.
:issue:`3245`
- ``Authorization`` parsing ``basic`` auth disallows non-base64 characters.
:pr:`3248`
- ``application/x-www-form-urlencoded`` form data is no longer limited to
``max_form_memory_size``, only ``max_content_length``.
``max_form_memory_size``, only ``max_content_length``. :pr:`3251`
- ``LimitedStream.readinto`` does not resize the buffer when it reads less
than the remaining size.
than the remaining size. :pr:`3253`
- Rules with 10 or more converters in a single part assign matched values
correctly.
- The invalid ``Range`` suffix length ``-0`` is no longer accepted.
correctly. :pr:`3254`
- The invalid ``Range`` suffix length ``-0`` is no longer accepted. :pr:`3255`


Version 3.1.8
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ applications.
# save this as app.py
from werkzeug.wrappers import Request, Response


@Request.application
def application(request: Request) -> Response:
return Response("Hello, World!")


if __name__ == "__main__":
from werkzeug.serving import run_simple

run_simple("127.0.0.1", 5000, application)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Python Version
--------------

We recommend using the latest version of Python. Werkzeug supports
Python 3.10 and newer.
Python 3.11 and newer.


Optional dependencies
Expand Down
19 changes: 10 additions & 9 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,16 @@ method to get all values for a header:
>>> response.headers.getlist('Set-Cookie')
['name=value; Path=/', 'name2=value2; Path=/']

Finally if you have set all the conditional values, you can make the
response conditional against a request. Which means that if the request
can assure that it has the information already, no data besides the headers
is sent over the network which saves traffic. For that you should set at
least an ETag (which is used for comparison) and the date header and then
call :class:`~Request.make_conditional` with the request object.

The response is modified accordingly (status code changed, response body
removed, entity headers removed etc.)
If the request has precondition headers, you can make the response conditional
to avoid sending data if it's already cached by the client. Ensure that the
response has set :attr:`~.Response.etag`, :attr:`~.Response.last_modified`,
then call :meth:`~.Response.apply_conditions`.

If the request has a ``Range`` header, you can apply the range to the response
to send only the partial data requested by the client. Ensure that the response
has set :attr:`~.Response.data`, :attr:`~.Response.content_length`,
:attr:`~.Response.etag`, and :attr:`~.Response.last_modified`, then call
:meth:`~.Response.apply_range`.


Trusting Data
Expand Down
11 changes: 3 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Typing :: Typed",
]
requires-python = ">=3.10"
requires-python = ">=3.11"
dependencies = [
"markupsafe>=3.0.3",
]
Expand Down Expand Up @@ -112,7 +112,7 @@ exclude_also = [
]

[tool.mypy]
python_version = "3.10"
python_version = "3.11"
files = ["src"]
show_error_codes = true
pretty = true
Expand All @@ -125,11 +125,6 @@ module = [
]
ignore_missing_imports = true

[tool.pyright]
pythonVersion = "3.10"
include = ["src"]
typeCheckingMode = "standard"

[tool.ruff]
extend-exclude = ["examples/"]
src = ["src"]
Expand All @@ -155,7 +150,7 @@ order-by-type = false
env_list = [
"py3.15",
"py3.14", "py3.14t",
"py3.13", "py3.12", "py3.11", "py3.10",
"py3.13", "py3.12", "py3.11",
"pypy3.11",
"style",
"typing",
Expand Down
27 changes: 11 additions & 16 deletions src/werkzeug/_internal.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from __future__ import annotations

import datetime as dt
import logging
import re
import sys
import typing as t
from datetime import datetime
from datetime import timezone

if t.TYPE_CHECKING:
from _typeshed.wsgi import WSGIEnvironment
Expand Down Expand Up @@ -98,23 +97,19 @@ def _log(type: str, message: str, *args: t.Any, **kwargs: t.Any) -> None:


@t.overload
def _dt_as_utc(dt: None) -> None: ...


def _dt_as_utc(value: None) -> None: ...
@t.overload
def _dt_as_utc(dt: datetime) -> datetime: ...


def _dt_as_utc(dt: datetime | None) -> datetime | None:
if dt is None:
return dt
def _dt_as_utc(value: dt.datetime) -> dt.datetime: ...
def _dt_as_utc(value: dt.datetime | None) -> dt.datetime | None:
if value is None:
return None

if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
elif dt.tzinfo != timezone.utc:
return dt.astimezone(timezone.utc)
if value.tzinfo is None:
return value.replace(tzinfo=dt.UTC)
elif value.tzinfo != dt.UTC:
return value.astimezone(dt.UTC)

return dt
return value


_plain_int_re = {
Expand Down
6 changes: 1 addition & 5 deletions src/werkzeug/datastructures/etag.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ def __init__(
weak_etags: cabc.Iterable[str] | None = None,
star_tag: bool = False,
):
if not star_tag and strong_etags:
self._strong = frozenset(strong_etags)
else:
self._strong = frozenset()

self._strong = frozenset(strong_etags or ())
self._weak = frozenset(weak_etags or ())
self.star_tag = star_tag

Expand Down
9 changes: 1 addition & 8 deletions src/werkzeug/datastructures/file_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,7 @@ def __bool__(self) -> bool:
return bool(self.filename)

def __getattr__(self, name: str) -> t.Any:
try:
return getattr(self.stream, name)
except AttributeError:
# SpooledTemporaryFile on Python < 3.11 doesn't implement IOBase,
# get the attribute from its backing file instead.
if hasattr(self.stream, "_file"):
return getattr(self.stream._file, name)
raise
return getattr(self.stream, name)

def __iter__(self) -> cabc.Iterator[bytes]:
return iter(self.stream)
Expand Down
Loading
Loading