Skip to content

Commit a9f6b3c

Browse files
committed
Merge branch 'stable'
2 parents 7d67f88 + 042c4c5 commit a9f6b3c

File tree

21 files changed

+392
-165
lines changed

21 files changed

+392
-165
lines changed

.github/workflows/pre-commit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ jobs:
2020
with:
2121
path: ~/.cache/pre-commit
2222
key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }}
23-
- run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files
23+
- run: uv run --locked --no-default-groups --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files
2424
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
2525
if: ${{ !cancelled() }}

.github/workflows/publish.yaml

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ on:
55
jobs:
66
build:
77
runs-on: ubuntu-latest
8+
outputs:
9+
artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
810
steps:
911
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
12+
with:
13+
persist-credentials: false
1014
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
1115
with:
1216
enable-cache: true
@@ -17,17 +21,23 @@ jobs:
1721
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
1822
- run: uv build
1923
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
24+
id: upload-artifact
2025
with:
21-
path: ./dist
26+
name: dist
27+
path: dist/
28+
if-no-files-found: error
2229
create-release:
2330
needs: [build]
2431
runs-on: ubuntu-latest
2532
permissions:
2633
contents: write
2734
steps:
2835
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
36+
with:
37+
artifact-ids: ${{ needs.build.outputs.artifact-id }}
38+
path: dist/
2939
- name: create release
30-
run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} artifact/*
40+
run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} dist/*
3141
env:
3242
GH_TOKEN: ${{ github.token }}
3343
publish-pypi:
@@ -40,6 +50,9 @@ jobs:
4050
id-token: write
4151
steps:
4252
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
53+
with:
54+
artifact-ids: ${{ needs.build.outputs.artifact-id }}
55+
path: dist/
4356
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
4457
with:
45-
packages-dir: artifact/
58+
packages-dir: "dist/"

.github/workflows/tests.yaml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
include:
16+
- {python: '3.14'}
17+
- {python: '3.14t'}
18+
- {name: Windows, python: '3.14', os: windows-latest}
19+
# server tests are very slow on macos-latest/-15/-26
20+
- {name: Mac, python: '3.14', os: macos-14}
1621
- {python: '3.13'}
17-
- {name: Windows, python: '3.13', os: windows-latest}
18-
- {name: Mac, python: '3.13', os: macos-latest}
1922
- {python: '3.12'}
2023
- {python: '3.11'}
2124
- {python: '3.10'}
@@ -30,7 +33,7 @@ jobs:
3033
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
3134
with:
3235
python-version: ${{ matrix.python }}
33-
- run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
36+
- run: uv run --locked --no-default-groups --group dev tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
3437
typing:
3538
runs-on: ubuntu-latest
3639
steps:
@@ -47,4 +50,4 @@ jobs:
4750
with:
4851
path: ./.mypy_cache
4952
key: mypy|${{ hashFiles('pyproject.toml') }}
50-
- run: uv run --locked tox run -e typing
53+
- run: uv run --locked --no-default-groups --group dev tox run -e typing

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
repos:
22
- repo: https://github.com/astral-sh/ruff-pre-commit
3-
rev: fa1ed65ba95bfa7771a71003a9d74402541f5a5c # frozen: v0.14.6
3+
rev: 36243b70e5ce219623c3503f5afba0f8c96fda55 # frozen: v0.14.7
44
hooks:
55
- id: ruff-check
66
- id: ruff-format
77
- repo: https://github.com/astral-sh/uv-pre-commit
8-
rev: 016d6d8dbc18eb9cc42594297fa8af0c79f6df72 # frozen: 0.9.12
8+
rev: ed07c6e8889cf83a7245866d1dd007d1f93bf55e # frozen: 0.9.13
99
hooks:
1010
- id: uv-lock
1111
- repo: https://github.com/pre-commit/pre-commit-hooks

CHANGES.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,25 @@ Version 3.2.0
66
Unreleased
77

88

9+
Version 3.1.4
10+
-------------
11+
12+
Released 2025-11-28
13+
14+
- ``safe_join`` on Windows does not allow special device names. This prevents
15+
reading from these when using `send_from_directory`. ``secure_filename``
16+
already prevented writing to these. :ghsa:`hgf8-39gv-g3f2`
17+
- The debugger pin fails after 10 attempts instead of 11. :pr:`3020`
18+
- The multipart form parser handles a ``\r\n`` sequence at a chunk boundary.
19+
:issue:`3065`
20+
- Improve CPU usage during Watchdog reloader. :issue:`3054`
21+
- `Request.json` annotation is more accurate. :issue:`3067`
22+
- Traceback rendering handles when the line number is beyond the available
23+
source lines. :issue:`3044`
24+
- `HTTPException.get_response` annotation and doc better conveys the
25+
distinction between WSGI and sans-IO responses. :issue:`3056`
26+
27+
928
Version 3.1.3
1029
-------------
1130

docs/tutorial.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ The way we will do it in this tutorial is by calling the method ``on_``
265265
return e
266266

267267
We bind the URL map to the current environment and get back a
268-
:class:`~werkzeug.routing.URLAdapter`. The adapter can be used to match
268+
:class:`~werkzeug.routing.MapAdapter`. The adapter can be used to match
269269
the request but also to reverse URLs. The match method will return the
270270
endpoint and a dictionary of values in the URL. For instance the rule for
271271
``follow_short_link`` has a variable part called ``short_id``. When we go

pyproject.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ order-by-type = false
158158

159159
[tool.tox]
160160
env_list = [
161-
"py3.13", "py3.12", "py3.11", "py3.10", "py3.9",
161+
"py3.14", "py3.14t",
162+
"py3.13",
163+
"py3.12", "py3.11", "py3.10", "py3.9",
162164
"pypy3.11",
163165
"style",
164166
"typing",
@@ -179,6 +181,13 @@ commands = [[
179181
{replace = "posargs", default = [], extend = true},
180182
]]
181183

184+
[tool.tox.env.py3.14t]
185+
commands_pre = [
186+
# watchdog doesn't support free threading yet, no marker to exclude it yet
187+
# watchdog_fsevents.c extension on macOS, and stability on Windows.
188+
["uv", "pip", "uninstall", "watchdog"],
189+
]
190+
182191
[tool.tox.env.style]
183192
description = "run all pre-commit hooks on all files"
184193
dependency_groups = ["pre-commit"]

src/werkzeug/_reloader.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626

2727
_stat_ignore_scan = tuple(prefix)
2828
del prefix
29+
# Ignore __pycache__ since a change there will always have a change to
30+
# the source file (or initial pyc file) as well. Ignore common version control
31+
# internals. Ignore common tool caches.
2932
_ignore_common_dirs = {
3033
"__pycache__",
3134
".git",
@@ -86,9 +89,6 @@ def _find_stat_paths(
8689
parent_has_py = {os.path.dirname(path): True}
8790

8891
for root, dirs, files in os.walk(path):
89-
# Optimizations: ignore system prefixes, __pycache__ will
90-
# have a py or pyc module at the import path, ignore some
91-
# common known dirs such as version control and tool caches.
9292
if (
9393
root.startswith(_stat_ignore_scan)
9494
or os.path.basename(root) in _ignore_common_dirs
@@ -325,7 +325,7 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
325325
trigger_reload = self.trigger_reload
326326

327327
class EventHandler(PatternMatchingEventHandler):
328-
def on_any_event(self, event: FileModifiedEvent): # type: ignore
328+
def on_any_event(self, event: FileModifiedEvent) -> None: # type: ignore[override]
329329
if event.event_type not in {
330330
EVENT_TYPE_CLOSED,
331331
EVENT_TYPE_CREATED,
@@ -345,26 +345,21 @@ def on_any_event(self, event: FileModifiedEvent): # type: ignore
345345

346346
self.name = f"watchdog ({reloader_name})"
347347
self.observer = Observer()
348-
# Extra patterns can be non-Python files, match them in addition
349-
# to all Python files in default and extra directories. Ignore
350-
# __pycache__ since a change there will always have a change to
351-
# the source file (or initial pyc file) as well. Ignore Git and
352-
# Mercurial internal changes.
353-
extra_patterns = [p for p in self.extra_files if not os.path.isdir(p)]
348+
extra_patterns = (p for p in self.extra_files if not os.path.isdir(p))
354349
self.event_handler = EventHandler(
355350
patterns=["*.py", "*.pyc", "*.zip", *extra_patterns],
356351
ignore_patterns=[
357352
*[f"*/{d}/*" for d in _ignore_common_dirs],
358353
*self.exclude_patterns,
359354
],
360355
)
361-
self.should_reload = False
356+
self.should_reload = threading.Event()
362357

363358
def trigger_reload(self, filename: str | bytes) -> None:
364359
# This is called inside an event handler, which means throwing
365360
# SystemExit has no effect.
366361
# https://github.com/gorakhargosh/watchdog/issues/294
367-
self.should_reload = True
362+
self.should_reload.set()
368363
self.log_reload(filename)
369364

370365
def __enter__(self) -> ReloaderLoop:
@@ -377,9 +372,8 @@ def __exit__(self, exc_type, exc_val, exc_tb): # type: ignore
377372
self.observer.join()
378373

379374
def run(self) -> None:
380-
while not self.should_reload:
375+
while not self.should_reload.wait(timeout=self.interval):
381376
self.run_step()
382-
time.sleep(self.interval)
383377

384378
sys.exit(3)
385379

@@ -393,7 +387,7 @@ def run_step(self) -> None:
393387
self.event_handler, path, recursive=True
394388
)
395389
except OSError:
396-
# Clear this path from list of watches We don't want
390+
# Clear this path from list of watches. We don't want
397391
# the same error message showing again in the next
398392
# iteration.
399393
self.watches[path] = None

src/werkzeug/debug/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,11 @@ def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None:
441441
"""
442442
if self.pin is None:
443443
return True
444+
445+
# If we failed too many times, then we're locked out.
446+
if self._failed_pin_auth.value >= 10:
447+
return False
448+
444449
val = parse_cookie(environ).get(self.pin_cookie_name)
445450
if not val or "|" not in val:
446451
return False
@@ -490,7 +495,7 @@ def pin_auth(self, request: Request) -> Response:
490495
auth = True
491496

492497
# If we failed too many times, then we're locked out.
493-
elif self._failed_pin_auth.value > 10:
498+
elif self._failed_pin_auth.value >= 10:
494499
exhausted = True
495500

496501
# Otherwise go through pin based authentication

src/werkzeug/debug/tbtools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def render_line(line: str, cls: str) -> None:
420420
f"{arrow if arrow else ''}</pre>"
421421
)
422422

423-
if lines:
423+
if line_idx < len(lines):
424424
for line in lines[start_idx:line_idx]:
425425
render_line(line, "before")
426426

0 commit comments

Comments
 (0)