Compare commits

...
Sign in to create a new pull request.

148 commits

Author SHA1 Message Date
David Lord
36e4a824f3
Any for CertParamType type 2026-05-31 07:42:46 -07:00
David Lord
954f5684e4
update dev dependencies 2026-05-18 16:35:44 -07:00
David Lord
9fcd34c9f3
Merge branch 'stable' 2026-05-13 07:37:53 -07:00
David Lord
1d49747264
update flask-mongoengine link 2026-05-13 07:37:21 -07:00
David Lord
7374c85dde
remove leftover setuptools 2026-05-02 05:59:12 -07:00
David Lord
dcbede0cb0
autoescape selection uses case-insensitive comparison (#6013) 2026-05-01 20:58:56 -07:00
David Lord
9368fb3f3c
case-insensitive comparison 2026-05-01 20:56:39 -07:00
David Lord
06ea505ce2
separate copy per call 2026-05-01 20:31:50 -07:00
David Lord
2ac89889f4
Merge branch 'stable' 2026-04-08 21:04:03 -07:00
David Lord
689362089e
fix typo 2026-04-08 21:01:29 -07:00
David Lord
258d68b6ff
Merge branch 'stable' 2026-04-05 12:32:14 -07:00
David Lord
a31e6b7346
remove werkzeug host tests 2026-04-05 12:31:01 -07:00
David Lord
e4e4bf6543
Merge branch 'stable' 2026-04-05 11:24:48 -07:00
David Lord
b21425d6df
reduce venv size 2026-04-05 11:13:13 -07:00
David Lord
83dbcb222a
update dev dependencies 2026-04-03 15:59:16 -07:00
David Lord
7ef2946fb5
remove unicode host test (#5962) 2026-03-24 06:55:57 -07:00
David Lord
91c6b3fecf
remove unicode host test 2026-03-24 06:51:39 -07:00
David Lord
4cae5d8e41
Merge branch 'stable' 2026-03-08 16:21:50 -07:00
David Lord
a197702e2c
update dev dependencies 2026-03-08 16:20:07 -07:00
David Lord
4774385abd
add zizmor to scan workflows (#5945) 2026-03-08 16:15:00 -07:00
David Lord
560c119e3d
add zizmor to scan workflows 2026-03-08 16:05:00 -07:00
David Lord
3a9d54f3da
Merge branch 'stable' 2026-03-04 07:36:21 -08:00
David Lord
a29f88ce6f
document that headers must be set before streaming 2026-03-04 07:36:09 -08:00
David Lord
c34d6e81fd
all teardown callbacks are called despite errors (#5928) 2026-02-19 20:00:34 -08:00
David Lord
fbb6f0bc4c
all teardown callbacks are called despite errors 2026-02-19 19:41:50 -08:00
David Lord
7b0088693e
fix typing 2026-02-19 08:42:33 -08:00
David Lord
a411a2434b
add back opening session on context push 2026-02-19 08:35:48 -08:00
David Lord
daca74d93a
Merge branch 'stable' 2026-02-18 21:56:24 -08:00
David Lord
f00ad424ee
release version 3.1.3 (#5924) 2026-02-18 21:01:53 -08:00
David Lord
22d924701a
release version 3.1.3 2026-02-18 19:41:55 -08:00
David Lord
089cb86dd2
Merge commit from fork
request context tracks session access
2026-02-18 19:35:58 -08:00
David Lord
c17f379390
request context tracks session access 2026-02-18 19:02:54 -08:00
David Lord
27be933840
start version 3.1.3 2026-02-18 14:52:52 -08:00
David Lord
d98eb69a35
revert cli test change 2026-02-12 13:11:01 -08:00
David Lord
12e95c93b4
fix provide_automatic_options override (#5917) 2026-02-12 13:07:50 -08:00
David Lord
e82db2ca3a
fix provide_automatic_options override 2026-02-12 13:03:03 -08:00
David Lord
d3b78fd18a
Merge remote-tracking branch 'origin/stable' 2026-02-06 13:22:54 -08:00
David Lord
663198d7b4
update dev dependencies 2026-02-03 10:22:19 -08:00
David Lord
976459f7cb
fix editable werkzeug 2026-02-03 10:20:49 -08:00
David Lord
5e621a2801
update domain matching tests for Werkzeug 3.2 2026-02-03 10:19:45 -08:00
David Lord
4e652d3f68
Abort if the instance folder cannot be created (#5903) 2026-01-28 07:43:00 -08:00
Markus Heidelberg
3d03098a97 Abort if the instance folder cannot be created
According to the comment, the instance folder should exist in any case.
But a PermissionError was ignored silently.

Since Python 3.9 is the minimum required version, it is safe to use
"exist_ok" added in Python 3.2 and avoid exception handling.
2026-01-27 09:18:37 +01:00
David Lord
798e006f43
Merge branch 'stable' 2026-01-25 10:38:42 -08:00
David Lord
407eb76b27
document using gevent for async (#5900) 2026-01-25 10:35:12 -08:00
David Lord
ac5664d228
document using gevent for async 2026-01-25 10:33:13 -08:00
David Lord
23df07d799
Merge branch 'stable' 2026-01-24 19:55:36 -08:00
David Lord
4b8bde97d4
deprecate should_ignore_error (#5899) 2026-01-24 19:53:11 -08:00
David Lord
0292047b22
remove unused ruff check rule 2026-01-24 19:52:11 -08:00
David Lord
c77a520343
deprecate should_ignore_error 2026-01-24 19:50:30 -08:00
David Lord
9b74a90dd3
fix codespell findings 2026-01-24 19:11:02 -08:00
David Lord
5880befcd2
Merge branch 'stable' 2026-01-24 19:05:26 -08:00
David Lord
4f79d5b59a
Increase required flit_core version to 3.11 (#5865) 2026-01-24 19:04:07 -08:00
Markus Heidelberg
fe3b215d3a
Increase required flit_core version to 3.11
Needed since Flask 3.1.1 after having set the "license" keyword to an
SPDX license expression. Avoids this possible build error:

  flit_core.config.ConfigError: license field should be <class 'dict'>, not <class 'str'>

Fixes: 0109e496f ("use uv").
2026-01-24 19:02:37 -08:00
David Lord
5559ef42b5
pre-commit: Add codespell (#5844) 2026-01-24 19:00:23 -08:00
David Lord
3709c4a9a8
update ruff hook and noqa
co-authored-by: Christian Clauss <cclauss@me.com>
2026-01-24 18:58:45 -08:00
Christian Clauss
709f83f6a3
pre-commit: Add codespell 2026-01-24 18:53:30 -08:00
ADITYA SAH
30da640ffe
clarify 415 vs 400 errors for request.json (#5827) 2026-01-24 18:46:56 -08:00
David Lord
25642fd1fd
fix annotation for select_jinja_autoescape (#5808) 2026-01-24 18:25:31 -08:00
David Lord
809d5a8869
redirect defaults to 303 (#5898) 2026-01-24 17:18:35 -08:00
David Lord
eca5fd1dfd
redirect defaults to 303 2026-01-24 17:16:38 -08:00
David Lord
eb58d862cc
Merge branch 'stable' 2026-01-24 17:15:54 -08:00
David Lord
64dd0809c2
update dev dependencies 2026-01-24 17:14:20 -08:00
David Lord
97bddc1f61
update dev dependencies 2026-01-05 08:50:52 -08:00
David Lord
ad68a12645
drop experimental 3.13t test env 2025-11-28 11:05:52 -08:00
David Lord
2579ce9f18
Merge branch 'stable' 2025-11-17 10:05:51 -08:00
David Lord
607d1948b8
split free threading envs 2025-11-17 10:05:39 -08:00
David Lord
218880c7fd
Merge branch 'stable' 2025-11-17 10:03:42 -08:00
David Lord
917000097f
test py3.14 2025-11-17 10:02:53 -08:00
David Lord
96a01e420b
Merge branch 'stable' 2025-11-17 09:45:56 -08:00
David Lord
da6d075dfd
update dev dependencies 2025-11-17 09:43:40 -08:00
David Lord
70d04b5a26
pass context through dispatch methods (#5818) 2025-11-17 08:49:53 -08:00
Hynek Schlawack
88a65bb374
Docs typo/markup fixes (#5829) 2025-10-14 13:26:26 -07:00
David Lord
6a64969009
pass context through dispatch methods 2025-09-19 17:33:30 -07:00
David Lord
adf363679d
merge app and request context (#5812) 2025-09-19 16:45:27 -07:00
David Lord
c2705ffd9c
merge app and request context 2025-09-19 16:43:53 -07:00
subhajitsaha01
dbd4c28825 Changed the static annotated type of select_jinja_autoescape method in src/flask/sansio/app.py 2025-09-06 22:01:22 +05:30
David Lord
330123258e
Merge branch 'stable' 2025-08-19 14:09:56 -07:00
David Lord
85793d6c22
release version 3.1.2 (#5800) 2025-08-19 14:03:43 -07:00
David Lord
2c1b30d050
release version 3.1.2 2025-08-19 13:57:47 -07:00
David Lord
1292419ddf
Update GitHub Actions workflow for artifact handling (#5795) 2025-08-19 13:56:49 -07:00
Grant Birkinbine
4dd52ca9c7
Update GitHub Actions workflow for artifact handling 2025-08-19 13:50:03 -07:00
David Lord
55c6255657
update dev dependencies 2025-08-19 13:41:24 -07:00
David Lord
ed1c9e953e
support call template_filter without parens (#5736) 2025-08-19 12:36:00 -07:00
David Lord
edebd37044
rewrite docs, clean up typing for template decorators 2025-08-19 12:33:21 -07:00
kadai0308
daf1510a4b
use template_filter without parens 2025-08-19 12:33:21 -07:00
David Lord
d8259eb119
use Jinja name consistently 2025-08-19 10:43:16 -07:00
David Lord
38b4c1e19b
refactor stream_with_context for async views (#5799) 2025-08-19 08:23:51 -07:00
David Lord
9822a03515
refactor stream_with_context for async views 2025-08-19 08:18:55 -07:00
David Lord
49b7e7bc8f
security docs for TRUSTED_HOSTS (#5798) 2025-08-18 11:44:57 -07:00
David Lord
b228ca3d87
security docs for TRUSTED_HOSTS 2025-08-18 11:42:48 -07:00
David Lord
ff64079a51
update flask-talisman link 2025-08-18 10:51:12 -07:00
David Lord
1dfd7cd555
use IO[bytes] instead of BinaryIO for wider compatibility (#5777) 2025-08-18 10:26:12 -07:00
Tero Vuotila
d44f1c6523
relax type hint for bytes io 2025-08-18 10:22:59 -07:00
David Lord
c56c5ec7c4
Docs: Fix escaping in HTML escaping example (#5742) 2025-08-18 10:20:06 -07:00
Badhreesh
0f83958247
demonstrate escaping with query string
slash in value would be interpreted as a path separator in the URL
2025-08-18 10:19:18 -07:00
David Lord
7fea7cf156
Update macOS UI reference to “System Settings” (#5723) 2025-08-18 10:08:07 -07:00
David Lord
24824ff666
push preserved contexts in correct order (#5797) 2025-08-18 09:56:39 -07:00
David Lord
53b8f08218
push preserved contexts in correct order 2025-08-18 09:45:56 -07:00
David Lord
5addaf833b
start version 3.1.2 2025-08-18 09:42:21 -07:00
David Lord
85c5d93cbd
Merge branch 'stable' 2025-06-12 13:48:07 -07:00
David Lord
85cc710464
svg logo 2025-06-12 13:46:49 -07:00
David Lord
284273e3c5
Merge branch 'stable' 2025-06-10 13:18:26 -07:00
David Lord
f17d986948
cleanup svg 2025-06-10 13:18:15 -07:00
David Lord
d6009c0aeb
Merge branch 'stable' 2025-06-09 21:20:46 -07:00
David Lord
2b42a803a2
cleanup svg 2025-06-09 21:20:36 -07:00
David Lord
211cce038a
Merge branch 'stable' 2025-06-09 14:33:44 -07:00
David Lord
a7b67c99f9
svg logo (#5757) 2025-06-09 14:33:26 -07:00
David Lord
a758915893
svg logo 2025-06-09 14:31:31 -07:00
David Lord
e974128863
Merge branch 'stable' 2025-06-08 09:54:32 -07:00
David Lord
f04c5e6964
update dev dependencies 2025-06-08 09:52:11 -07:00
David Lord
c07b201ce3
Merge pull request #5754
remove slsa provenance
2025-06-08 09:47:45 -07:00
David Lord
adeea00707
remove slsa provenance
PyPI trusted publishing has its own attestation support now.
2025-06-08 09:43:05 -07:00
abhiram kamini
7bf3be8dfa
Update server.rst
made changes to rename system preferences to system settings according to new mac os name change
2025-06-04 17:24:45 -07:00
David Lord
a42c4d54a3
Fix global CONTRIBUTING link (#5737) 2025-05-20 12:30:04 -04:00
AJ Jordan
184ec3c545
Fix global CONTRIBUTING link 2025-05-20 12:26:17 -04:00
David Lord
a5f9742398
drop end of life python versions (#5731) 2025-05-13 08:35:41 -07:00
David Lord
52df9eed45
drop end of life python versions 2025-05-13 08:31:54 -07:00
David Lord
e7e5380776
Merge branch 'stable' 2025-05-13 08:10:30 -07:00
David Lord
bbaf13333f
fix syntax 2025-05-13 08:09:39 -07:00
David Lord
57e7286948
release version 3.1.1 (#5730) 2025-05-13 08:01:42 -07:00
David Lord
7fff56f517
release version 3.1.1 2025-05-13 07:51:12 -07:00
David Lord
73d6504063
Merge commit from fork
Sessions: fix signing key selection when key rotation is enabled
2025-05-13 07:46:54 -07:00
David Lord
cbb6c36692
update docs about fallback order 2025-05-12 18:30:27 -07:00
James Addison
fb54159861
secret key rotation: fix key list ordering
The `itsdangerous` serializer interface[1] expects keys to be
provided with the oldest key at index zero and the active signing key
at the end of the list.

We document[2] that `SECRET_KEY_FALLBACKS` should be configured with
the most recent first (at index zero), so to achieve the expected
behaviour, those should be inserted in reverse-order at the head of
the list.

[1] - https://itsdangerous.palletsprojects.com/en/stable/serializer/#itsdangerous.serializer.Serializer

[2] - https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY_FALLBACKS
2025-05-12 18:30:27 -07:00
David Lord
bc143499cf
Merge branch 'stable' 2025-05-11 18:08:43 -07:00
David Lord
941efd4a36
use uv (#5727) 2025-05-11 18:04:35 -07:00
David Lord
0109e496f6
use uv 2025-05-11 17:58:53 -07:00
David Lord
11c45eeba3
update dev dependencies 2025-05-11 05:58:48 -07:00
David Lord
b78b5a210b
Merge branch 'stable' 2025-03-30 13:17:17 -07:00
David Lord
e785166507
Async Iterable Response (#5659) 2025-03-30 13:15:54 -07:00
CoolCat467
410e5ab7ed
Accept AsyncIterable for responses 2025-03-30 13:14:25 -07:00
David Lord
bfffe87d4c
add ghsa links 2025-03-29 15:57:16 -07:00
David Lord
73ce26c3e8
remove tests about deprecated pkgutil.get_loader (#5702) 2025-03-29 15:45:11 -07:00
David Lord
41ec5760a2
remove tests about deprecated pkgutil.get_loader 2025-03-29 15:42:58 -07:00
David Lord
2732c4db66
add endpoint name in favicon example (#5701) 2025-03-29 15:32:28 -07:00
David Lord
c94d2a77db
add endpoint name in favicon example 2025-03-29 15:30:56 -07:00
David Lord
315ebc1176
better type checking during deprecation (#5700) 2025-03-29 15:28:27 -07:00
David Lord
7d5d187458
better type checking during deprecation 2025-03-29 15:23:34 -07:00
David Lord
c7c8dc38ea
Remove HTTP Public Key Pinning from docs (#5695) 2025-03-29 15:17:48 -07:00
black
2ae36c8dd5
Remove HTTP Public Key Pinning from docs
The header is considered obsolete and no longer supported by any major
browser. MDN link is dead.
2025-03-29 15:16:24 -07:00
David Lord
5ea0ab8ea2
Handle help arg by itself the same as no args (#5674) 2025-03-29 15:15:04 -07:00
George Waters
da60039486
Handle help arg by itself the same as no args
When the 'flask' command is used with only the '--help' parameter, this
change will make sure to try and load the app before the help callback
is run. This was previously only being done when the 'flask' command was
used by itself. This meant when passing in '--help', any custom commands
were not getting shown in the help message. With this change, custom
commands will be included in the help message when running 'flask' on
the command line by itself or with the '--help' parameter.
2025-03-29 15:13:23 -07:00
David Lord
08c480b3b3
Update app factory docs (#5671) 2025-03-29 15:10:55 -07:00
kotvkvante
f51a23839a fix bash cli syntax error and app name 2025-03-29 15:09:26 -07:00
David Lord
04b070fa26
Fix typo in the docs (#5650) 2025-03-29 14:49:06 -07:00
zhuangzhuang
75a8327cfd
Update mongoengine.rst 2025-03-29 14:47:41 -07:00
David Lord
165af0a090
update dev dependencies 2025-03-29 14:44:59 -07:00
David Lord
235c52fa10
fix rtd build 2025-03-29 14:37:13 -07:00
101 changed files with 4403 additions and 2441 deletions

View file

@ -7,16 +7,19 @@ name: Lock inactive closed issues
on: on:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
permissions: permissions: {}
issues: write
pull-requests: write
concurrency: concurrency:
group: lock group: lock
cancel-in-progress: true
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
discussions: write
steps: steps:
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with: with:
issue-inactive-days: 14 issue-inactive-days: 14
pr-inactive-days: 14 pr-inactive-days: 14

View file

@ -3,14 +3,27 @@ on:
pull_request: pull_request:
push: push:
branches: [main, stable] branches: [main, stable]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: 3.x persist-credentials: false
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 with:
if: ${{ !cancelled() }} enable-cache: true
prune-cache: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
id: setup-python
with:
python-version-file: pyproject.toml
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }}
- run: uv run --locked --no-default-groups --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files

View file

@ -1,61 +1,51 @@
name: Publish name: Publish
on: on:
push: push:
tags: tags: ['*']
- '*' permissions: {}
concurrency:
group: publish-${{ github.event.push.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
hash: ${{ steps.hash.outputs.hash }} artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: '3.x' persist-credentials: false
cache: pip - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
cache-dependency-path: requirements*/*.txt with:
- run: pip install -r requirements/build.txt enable-cache: false
# Use the commit date instead of the current date during the build. prune-cache: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: pyproject.toml
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- run: python -m build - run: uv build
# Generate hashes used for provenance. - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- name: generate hash id: upload-artifact
id: hash
run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
path: ./dist name: dist
provenance: path: dist/
needs: [build] if-no-files-found: error
permissions:
actions: read
id-token: write
contents: write
# Can't pin with hash due to how this workflow works.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: ${{ needs.build.outputs.hash }}
create-release: create-release:
# Upload the sdist, wheels, and provenance to a GitHub release. They remain needs: [build]
# available as build artifacts for a while as well.
needs: [provenance]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
artifact-ids: ${{ needs.build.outputs.artifact-id }}
path: dist/
- name: create release - name: create release
run: > run: gh release create --draft --repo ${GITHUB_REPOSITORY} ${GITHUB_REF_NAME} dist/*
gh release create --draft --repo ${{ github.repository }}
${{ github.ref_name }}
*.intoto.jsonl/* artifact/*
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
publish-pypi: publish-pypi:
needs: [provenance] needs: [build]
# Wait for approval before attempting to upload to PyPI. This allows reviewing the
# files in the draft release.
environment: environment:
name: publish name: publish
url: https://pypi.org/project/Flask/${{ github.ref_name }} url: https://pypi.org/project/Flask/${{ github.ref_name }}
@ -63,7 +53,10 @@ jobs:
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
with: with:
packages-dir: artifact/ artifact-ids: ${{ needs.build.outputs.artifact-id }}
path: dist/
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: "dist/"

View file

@ -1,10 +1,14 @@
name: Tests name: Tests
on: on:
pull_request:
paths-ignore: ['docs/**', 'README.md']
push: push:
branches: [main, stable] branches: [main, stable]
paths-ignore: ['docs/**', '*.md', '*.rst'] paths-ignore: ['docs/**', 'README.md']
pull_request: permissions: {}
paths-ignore: [ 'docs/**', '*.md', '*.rst' ] concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
tests: tests:
name: ${{ matrix.name || matrix.python }} name: ${{ matrix.name || matrix.python }}
@ -13,39 +17,47 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- {python: '3.14'}
- {python: '3.14t'}
- {name: Windows, python: '3.14', os: windows-latest}
- {name: Mac, python: '3.14', os: macos-latest}
- {python: '3.13'} - {python: '3.13'}
- {python: '3.12'} - {python: '3.12'}
- {name: Windows, python: '3.12', os: windows-latest}
- {name: Mac, python: '3.12', os: macos-latest}
- {python: '3.11'} - {python: '3.11'}
- {python: '3.10'} - {python: '3.10'}
- {python: '3.9'} - {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
- {name: PyPy, python: 'pypy-3.10', tox: pypy310} - {name: Minimum Versions, python: '3.14', tox: tests-min}
- {name: Minimum Versions, python: '3.12', tox: py-min} - {name: Development Versions, python: '3.10', tox: tests-dev}
- {name: Development Versions, python: '3.9', tox: py-dev}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 with:
persist-credentials: false
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
enable-cache: true
prune-cache: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
allow-prereleases: true - run: uv run --locked --no-default-groups --group dev tox run
cache: pip env:
cache-dependency-path: requirements*/*.txt TOX_ENV: ${{ matrix.tox || format('py{0}', matrix.python) }}
- run: pip install tox
- run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
typing: typing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: '3.x' persist-credentials: false
cache: pip - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
cache-dependency-path: requirements*/*.txt with:
enable-cache: true
prune-cache: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: pyproject.toml
- name: cache mypy - name: cache mypy
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with: with:
path: ./.mypy_cache path: ./.mypy_cache
key: mypy|${{ hashFiles('pyproject.toml') }} key: mypy|${{ hashFiles('pyproject.toml') }}
- run: pip install tox - run: uv run --locked --no-default-groups --group dev tox run -e typing
- run: tox run -e typing

22
.github/workflows/zizmor.yaml vendored Normal file
View file

@ -0,0 +1,22 @@
name: GitHub Actions security analysis with zizmor
on:
pull_request:
paths: ["**/*.yaml?"]
push:
branches: [main, stable]
paths: ["**/*.yaml?"]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
zizmor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
with:
advanced-security: false
annotations: true

2
.gitignore vendored
View file

@ -1,7 +1,5 @@
.idea/ .idea/
.vscode/ .vscode/
.venv*/
venv*/
__pycache__/ __pycache__/
dist/ dist/
.coverage* .coverage*

View file

@ -1,11 +1,20 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.3 rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13
hooks: hooks:
- id: ruff - id: ruff-check
- id: ruff-format - id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: fa60a193803535a9e2accdb3ca4b1b584b1150cb # frozen: 0.11.15
hooks:
- id: uv-lock
- repo: https://github.com/codespell-project/codespell
rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2
hooks:
- id: codespell
args: ['--write-changes']
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0 rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
hooks: hooks:
- id: check-merge-conflict - id: check-merge-conflict
- id: debug-statements - id: debug-statements

View file

@ -1,13 +1,10 @@
version: 2 version: 2
build: build:
os: ubuntu-22.04 os: ubuntu-24.04
tools: tools:
python: '3.12' python: '3.13'
python: commands:
install: - asdf plugin add uv
- requirements: requirements/docs.txt - asdf install uv latest
- method: pip - asdf global uv latest
path: . - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html
sphinx:
builder: dirhtml
fail_on_warning: true

View file

@ -3,15 +3,66 @@ Version 3.2.0
Unreleased Unreleased
- Drop support for Python 3.9. :pr:`5730`
- Remove previously deprecated code: ``__version__``. :pr:`5648` - Remove previously deprecated code: ``__version__``. :pr:`5648`
- ``RequestContext`` has merged with ``AppContext``. ``RequestContext`` is now
a deprecated alias. If an app context is already pushed, it is not reused
when dispatching a request. This greatly simplifies the internal code for tracking
the active context. :issue:`5639`
- Many ``Flask`` methods involved in request dispatch now take the current
``AppContext`` as the first parameter, instead of using the proxy objects.
If subclasses were overriding these methods, the old signature is detected,
shows a deprecation warning, and will continue to work during the
deprecation period. :issue:`5815`
- All teardown callbacks are called, even if any raise an error. :pr:`5928`
- The ``should_ignore_error`` is deprecated. Handle errors as needed in
teardown handlers instead. :issue:`5816`
- ``template_filter``, ``template_test``, and ``template_global`` decorators
can be used without parentheses. :issue:`5729`
- ``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
``GET`` and ``POST`` redirects, and is also correct for frontend libraries
such as HTMX. :issue:`5895`
- ``provide_automatic_options=True`` can be used to enable it for a view when
it's disabled in config. Previously, only disabling worked. :issue:`5916`
- ``Flask.select_jinja_autoescape`` uses case-insensitive comparison instead
of only lower case file extensions. :pr:`6012`
Version 3.1.3
-------------
Released 2026-02-18
- The session is marked as accessed for operations that only access the keys
but not the values, such as ``in`` and ``len``. :ghsa:`68rp-wp8r-4726`
Version 3.1.2
-------------
Released 2025-08-19
- ``stream_with_context`` does not fail inside async views. :issue:`5774`
- When using ``follow_redirects`` in the test client, the final state
of ``session`` is correct. :issue:`5786`
- Relax type hint for passing bytes IO to ``send_file``. :issue:`5776`
Version 3.1.1 Version 3.1.1
------------- -------------
Unreleased Released 2025-05-13
- Fix type hint for `cli_runner.invoke`. :issue:`5645` - Fix signing key selection order when key rotation is enabled via
``SECRET_KEY_FALLBACKS``. :ghsa:`4grg-w6v8-c28g`
- Fix type hint for ``cli_runner.invoke``. :issue:`5645`
- ``flask --help`` loads the app and plugins first to make sure all commands
are shown. :issue:`5673`
- Mark sans-io base class as being able to handle views that return
``AsyncIterable``. This is not accurate for Flask, but makes typing easier
for Quart. :pr:`5659`
Version 3.1.0 Version 3.1.0
@ -113,6 +164,7 @@ Released 2023-05-01
- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. - Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed.
- Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes. - Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes.
:ghsa:`m2qf-hxjv-5gpq`
Version 2.3.1 Version 2.3.1
@ -1365,7 +1417,7 @@ Released 2011-09-29, codename Rakija
of Flask itself and no longer of the test client. This cleaned up of Flask itself and no longer of the test client. This cleaned up
some internal logic and lowers the odds of runaway request contexts some internal logic and lowers the odds of runaway request contexts
in unittests. in unittests.
- Fixed the Jinja2 environment's ``list_templates`` method not - Fixed the Jinja environment's ``list_templates`` method not
returning the correct names when blueprints or modules were returning the correct names when blueprints or modules were
involved. involved.
@ -1451,7 +1503,7 @@ Released 2010-12-31
- Fixed an issue where the default ``OPTIONS`` response was not - Fixed an issue where the default ``OPTIONS`` response was not
exposing all valid methods in the ``Allow`` header. exposing all valid methods in the ``Allow`` header.
- Jinja2 template loading syntax now allows "./" in front of a - Jinja template loading syntax now allows "./" in front of a
template load path. Previously this caused issues with module template load path. Previously this caused issues with module
setups. setups.
- Fixed an issue where the subdomain setting for modules was ignored - Fixed an issue where the subdomain setting for modules was ignored

View file

@ -1,3 +1,5 @@
<div align="center"><img src="https://raw.githubusercontent.com/pallets/flask/refs/heads/stable/docs/_static/flask-name.svg" alt="" height="150"></div>
# Flask # Flask
Flask is a lightweight [WSGI] web application framework. It is designed Flask is a lightweight [WSGI] web application framework. It is designed

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

15
docs/_static/flask-icon.svg vendored Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Icon" x="0" y="0" width="500" height="500" style="fill:none;"/>
<clipPath id="_clip1">
<rect x="0" y="0" width="500" height="500"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g>
<path d="M224.446,59.975c-0.056,-4.151 -0.483,-5.543 -2.7,-6.823c-2.104,-1.393 -5.288,-1.421 -8.329,-0.085l-204.674,87.64c-3.042,1.336 -5.913,4.008 -7.448,6.908c-1.535,2.899 -1.705,5.97 -0.511,8.158l17.084,31.384l0.228,0.369c1.847,2.928 6.026,3.696 10.29,1.82l1.251,-0.54c5.344,22.4 14.1,50.429 25.783,70.413l178.294,-79.794c-2.559,-23.14 -9.552,-89.602 -9.268,-119.479l0,0.029Z" style="fill:#3babc3;fill-rule:nonzero;"/>
<path d="M238.603,205.776l-171.698,76.838c10.091,19.132 22.542,39.428 37.722,58.986c50.429,-25.698 100.887,-51.396 151.316,-77.094c-3.269,-8.471 -6.452,-17.653 -17.34,-58.73Z" style="fill:#3babc3;fill-rule:nonzero;"/>
<path d="M497.601,388.846l-12.139,-18.535c-1.819,-2.018 -4.633,-2.786 -7.106,-1.791l-15.578,5.999c-1.848,-2.047 -4.52,-2.815 -7.135,-1.791c-5.089,1.99 -10.206,4.008 -15.294,5.998c-1.649,0.625 -2.104,1.847 -1.791,3.439l0.995,4.861c-28.711,3.099 -77.236,1.564 -120.701,-32.577c-19.216,-15.066 -37.239,-36.386 -52.277,-66.206l-144.75,73.768c26.466,29.08 59.697,54.864 100.973,70.385c57.422,21.633 130.593,23.679 222.838,-13.475l0.512,2.616c0.455,2.928 3.98,6.026 8.755,4.15l15.323,-5.97c5.258,-1.99 5.287,-6.026 4.519,-8.641l19.729,-7.704c2.217,-0.853 9.096,-6.169 3.183,-14.526l-0.056,-0Z" style="fill:#3babc3;fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

17
docs/_static/flask-logo.svg vendored Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect id="Logo" x="0" y="0" width="500" height="500" style="fill:none;"/>
<g>
<path id="Box" d="M500,50l0,400c0,27.596 -22.404,50 -50,50l-400,0c-27.596,0 -50,-22.404 -50,-50l0,-400c0,-27.596 22.404,-50 50,-50l400,0c27.596,0 50,22.404 50,50Z" style="fill:url(#_Linear1);"/>
<path id="Shadow" d="M500,398.646l0,51.354c0,27.596 -22.404,50 -50,50l-94.111,0l-170.452,-170.451c13.541,12.279 29.511,22.845 48.242,29.888c34.453,12.98 78.356,14.208 133.703,-8.084l0.307,1.569c0.273,1.757 2.388,3.616 5.253,2.49l9.193,-3.582c3.156,-1.194 3.173,-3.616 2.712,-5.185l11.837,-4.622c1.331,-0.512 5.458,-3.701 1.91,-8.716l-0.034,0l-7.283,-11.12c-1.091,-1.211 -2.78,-1.672 -4.264,-1.075l-9.346,3.599c-1.109,-1.228 -2.712,-1.688 -4.281,-1.074c-3.054,1.194 -6.124,2.404 -9.177,3.598c-0.989,0.376 -1.262,1.109 -1.074,2.064l0.597,2.917c-17.227,1.859 -46.342,0.938 -72.421,-19.547c-11.53,-9.039 -22.343,-21.831 -31.366,-39.723l-83.923,42.769l-13.246,-10.755c30.258,-15.419 60.532,-30.837 90.79,-46.256c-1.961,-5.083 -3.872,-10.592 -10.404,-35.238l-98.082,43.893l-11.828,-11.828l106.976,-47.876c-1.534,-13.88 -5.728,-53.736 -5.56,-71.67l-0,-0.017c-0.027,-1.894 -0.184,-2.827 -0.85,-3.504l266.182,266.182Zm-388.562,-185.436c1.272,1.164 3.414,1.356 5.594,0.397l0.75,-0.324c0.645,2.703 1.373,5.543 2.181,8.452l-8.525,-8.525Z" style="fill:#3b808b;"/>
<g id="Icon">
<path d="M234.668,135.985c-0.034,-2.49 -0.29,-3.326 -1.62,-4.094c-1.263,-0.835 -3.173,-0.852 -4.998,-0.051l-122.804,52.584c-1.825,0.802 -3.548,2.405 -4.469,4.145c-0.921,1.74 -1.023,3.582 -0.307,4.895l10.251,18.83l0.136,0.222c1.109,1.757 3.616,2.217 6.175,1.091l0.75,-0.324c3.207,13.441 8.46,30.258 15.47,42.248l106.976,-47.876c-1.535,-13.884 -5.731,-53.761 -5.56,-71.687l-0,0.017Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M243.162,223.466l-103.019,46.103c6.055,11.478 13.525,23.656 22.633,35.391c30.258,-15.419 60.532,-30.837 90.79,-46.256c-1.961,-5.083 -3.872,-10.592 -10.404,-35.238Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M398.56,333.307l-7.283,-11.12c-1.091,-1.211 -2.78,-1.672 -4.264,-1.075l-9.346,3.599c-1.109,-1.228 -2.712,-1.688 -4.281,-1.074c-3.054,1.194 -6.124,2.404 -9.177,3.598c-0.989,0.376 -1.262,1.109 -1.074,2.064l0.597,2.917c-17.227,1.859 -46.342,0.938 -72.421,-19.547c-11.53,-9.039 -22.343,-21.831 -31.366,-39.723l-86.85,44.26c15.879,17.449 35.818,32.919 60.584,42.231c34.453,12.98 78.356,14.208 133.703,-8.084l0.307,1.569c0.273,1.757 2.388,3.616 5.253,2.49l9.193,-3.582c3.156,-1.194 3.173,-3.616 2.712,-5.185l11.837,-4.622c1.331,-0.512 5.458,-3.701 1.91,-8.716l-0.034,0Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.06162e-14,500,-500,3.06162e-14,267.59,0)"><stop offset="0" style="stop-color:#bdddeb;stop-opacity:1"/><stop offset="1" style="stop-color:#53a9d1;stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

23
docs/_static/flask-name.svg vendored Normal file
View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 706 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g>
<path id="Name" d="M420.35,117.6l-37.65,-0c-4.4,-0 -7.475,0.85 -9.225,2.55c-1.75,1.7 -2.625,4.65 -2.625,8.85l-0,14.85l39.15,-0l-1.5,18.6l-37.65,-0l-0,43.35l-20.85,-0l-0,-85.05c-0,-7.9 1.725,-13.5 5.175,-16.8c3.45,-3.3 9.375,-4.95 17.775,-4.95l47.4,-0l0,18.6Z" style="fill-rule:nonzero;"/>
<path d="M455.75,205.8l-19.5,0.15l-0,-112.95l19.5,-0l-0,112.8Z" style="fill-rule:nonzero;"/>
<path d="M535.7,205.8l-13.05,-0l-6,-10.35l-20.85,11.25c-11.6,-0 -19.4,-4.2 -23.4,-12.6c-2,-4.1 -3.375,-8.525 -4.125,-13.275c-0.75,-4.75 -1.125,-9.7 -1.125,-14.85c-0,-5.15 0.05,-8.95 0.15,-11.4c0.1,-2.45 0.35,-5.3 0.75,-8.55c0.4,-3.25 0.975,-5.975 1.725,-8.175c0.75,-2.2 1.825,-4.475 3.225,-6.825c1.4,-2.35 3.1,-4.225 5.1,-5.625c4.5,-3.1 10.35,-4.65 17.55,-4.65l20.55,-0l19.5,-1.2l-0,86.25Zm-19.5,-27.3l-0,-40.2l-14.85,-0c-5.5,-0 -9.325,2.1 -11.475,6.3c-2.15,4.2 -3.225,10.475 -3.225,18.825c-0,8.35 1.025,14.225 3.075,17.625c2.05,3.4 5.925,5.1 11.625,5.1l14.85,-7.65Z" style="fill-rule:nonzero;"/>
<path d="M615.65,182.1l0,2.25c-0.6,7.5 -3.775,13.15 -9.525,16.95c-5.75,3.8 -12.925,5.7 -21.525,5.7c-12.7,-0 -21.6,-2.3 -26.7,-6.9c-4.7,-4.2 -7.05,-10.4 -7.05,-18.6l0,-1.8l17.7,0c0,4.6 1.2,7.75 3.6,9.45c2.4,1.7 6.55,2.55 12.45,2.55c8,0 12,-2.9 12,-8.7c0,-4.8 -1.4,-8 -4.2,-9.6c-1.3,-0.8 -2.95,-1.4 -4.95,-1.8l-15.15,-2.55c-13.2,-2.1 -19.8,-10.35 -19.8,-24.75c0,-8 2.925,-14.225 8.775,-18.675c5.85,-4.45 13.275,-6.675 22.275,-6.675c20.5,0 30.75,8.85 30.75,26.55l0,1.95l-16.95,0c-0.2,-4.7 -1.45,-7.9 -3.75,-9.6c-2.3,-1.7 -5.525,-2.55 -9.675,-2.55c-4.15,0 -7.275,0.825 -9.375,2.475c-2.1,1.65 -3.15,3.475 -3.15,5.475c0,5.7 2.3,8.95 6.9,9.75l18.15,3.3c12.8,2.4 19.2,11 19.2,25.8Z" style="fill-rule:nonzero;"/>
<path d="M705.65,205.8l-23.4,-0l-22.5,-30.3l-8.55,12.15l0,18.15l-19.5,-0l0,-112.8l19.5,-0l0,71.25l27.3,-40.65l22.05,-0l-28.05,38.4l33.15,43.8Z" style="fill-rule:nonzero;"/>
<g id="Logo">
<path id="Box" d="M300,30l0,240c0,16.557 -13.443,30 -30,30l-240,-0c-16.557,-0 -30,-13.443 -30,-30l0,-240c0,-16.557 13.443,-30 30,-30l240,0c16.557,0 30,13.443 30,30Z" style="fill:url(#_Linear1);"/>
<path id="Shadow" d="M300,239.188l0,30.812c0,16.557 -13.443,30 -30,30l-56.467,-0l-102.271,-102.271c8.125,7.368 17.707,13.707 28.945,17.933c20.672,7.788 47.014,8.525 80.222,-4.85l0.184,0.941c0.164,1.054 1.433,2.17 3.152,1.494l5.516,-2.149c1.893,-0.716 1.904,-2.169 1.627,-3.111l7.103,-2.773c0.798,-0.307 3.274,-2.221 1.146,-5.23l-0.021,0l-4.37,-6.672c-0.655,-0.727 -1.668,-1.003 -2.558,-0.645l-5.608,2.16c-0.665,-0.737 -1.627,-1.013 -2.569,-0.645c-1.832,0.716 -3.674,1.443 -5.505,2.159c-0.594,0.225 -0.758,0.665 -0.645,1.239l0.358,1.749c-10.336,1.116 -27.805,0.563 -43.452,-11.727c-6.918,-5.424 -13.406,-13.099 -18.82,-23.835l-50.354,25.662l-7.947,-6.453c18.154,-9.251 36.319,-18.502 54.474,-27.754c-1.177,-3.049 -2.323,-6.355 -6.243,-21.143l-58.849,26.336l-7.097,-7.096l64.186,-28.726c-0.921,-8.328 -3.437,-32.242 -3.336,-43.002l-0,-0.01c-0.016,-1.137 -0.111,-1.697 -0.51,-2.103l159.709,159.71Zm-233.137,-111.262c0.763,0.699 2.048,0.814 3.356,0.238l0.45,-0.194c0.387,1.622 0.824,3.325 1.309,5.071l-5.115,-5.115Z" style="fill:#3b808b;"/>
<g id="Icon">
<path d="M140.801,81.591c-0.021,-1.494 -0.174,-1.996 -0.972,-2.456c-0.758,-0.502 -1.904,-0.512 -2.999,-0.031l-73.683,31.551c-1.095,0.481 -2.128,1.443 -2.681,2.486c-0.552,1.044 -0.614,2.149 -0.184,2.937l6.151,11.298l0.081,0.133c0.666,1.055 2.17,1.331 3.705,0.655l0.45,-0.194c1.924,8.064 5.076,18.155 9.282,25.349l64.186,-28.726c-0.921,-8.33 -3.439,-32.257 -3.336,-43.012l-0,0.01Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M145.897,134.079l-61.811,27.662c3.633,6.887 8.115,14.194 13.58,21.235c18.154,-9.251 36.319,-18.502 54.474,-27.754c-1.177,-3.049 -2.323,-6.355 -6.243,-21.143Z" style="fill:#fff;fill-rule:nonzero;"/>
<path d="M239.136,199.984l-4.37,-6.672c-0.655,-0.727 -1.668,-1.003 -2.558,-0.645l-5.608,2.16c-0.665,-0.737 -1.627,-1.013 -2.569,-0.645c-1.832,0.716 -3.674,1.443 -5.505,2.159c-0.594,0.225 -0.758,0.665 -0.645,1.239l0.358,1.749c-10.336,1.116 -27.805,0.563 -43.452,-11.727c-6.918,-5.424 -13.406,-13.099 -18.82,-23.835l-52.11,26.557c9.528,10.469 21.491,19.751 36.35,25.338c20.672,7.788 47.014,8.525 80.222,-4.85l0.184,0.941c0.164,1.054 1.433,2.17 3.152,1.494l5.516,-2.149c1.893,-0.716 1.904,-2.169 1.627,-3.111l7.103,-2.773c0.798,-0.307 3.274,-2.221 1.146,-5.23l-0.021,0Z" style="fill:#fff;fill-rule:nonzero;"/>
</g>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.83697e-14,300,-300,1.83697e-14,160.554,0)"><stop offset="0" style="stop-color:#bdddeb;stop-opacity:1"/><stop offset="1" style="stop-color:#53a9d1;stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -31,17 +31,15 @@ Incoming Request Data
:inherited-members: :inherited-members:
:exclude-members: json_module :exclude-members: json_module
.. attribute:: request .. data:: request
To access incoming request data, you can use the global `request` A proxy to the request data for the current request, an instance of
object. Flask parses incoming request data for you and gives you :class:`.Request`.
access to it through that global object. Internally Flask makes
sure that you always get the correct data for the active thread if you
are in a multithreaded environment.
This is a proxy. See :ref:`notes-on-proxies` for more information. This is only available when a :doc:`request context </appcontext>` is
active.
The request object is an instance of a :class:`~flask.Request`. This is a proxy. See :ref:`context-visibility` for more information.
Response Objects Response Objects
@ -62,40 +60,33 @@ does this is by using a signed cookie. The user can look at the session
contents, but can't modify it unless they know the secret key, so make sure to contents, but can't modify it unless they know the secret key, so make sure to
set that to something complex and unguessable. set that to something complex and unguessable.
To access the current session you can use the :class:`session` object: To access the current session you can use the :data:`.session` proxy.
.. class:: session .. data:: session
The session object works pretty much like an ordinary dict, with the A proxy to the session data for the current request, an instance of
difference that it keeps track of modifications. :class:`.SessionMixin`.
This is a proxy. See :ref:`notes-on-proxies` for more information. This is only available when a :doc:`request context </appcontext>` is
active.
The following attributes are interesting: This is a proxy. See :ref:`context-visibility` for more information.
.. attribute:: new The session object works like a dict but tracks assignment and access to its
keys. It cannot track modifications to mutable values, you need to set
:attr:`~.SessionMixin.modified` manually when modifying a list, dict, etc.
``True`` if the session is new, ``False`` otherwise. .. code-block:: python
.. attribute:: modified # appending to a list is not detected
session["numbers"].append(42)
``True`` if the session object detected a modification. Be advised
that modifications on mutable structures are not picked up
automatically, in that situation you have to explicitly set the
attribute to ``True`` yourself. Here an example::
# this change is not picked up because a mutable object (here
# a list) is changed.
session['objects'].append(42)
# so mark it as modified yourself # so mark it as modified yourself
session.modified = True session.modified = True
.. attribute:: permanent The session is persisted across requests using a cookie. By default the
users's browser will clear the cookie when it is closed. Set
If set to ``True`` the session lives for :attr:`~.SessionMixin.permanent` to ``True`` to persist the cookie for
:attr:`~flask.Flask.permanent_session_lifetime` seconds. The :data:`PERMANENT_SESSION_LIFETIME`.
default is 31 days. If set to ``False`` (which is the default) the
session will be deleted when the user closes the browser.
Session Interface Session Interface
@ -158,20 +149,21 @@ another, a global variable is not good enough because it would break in
threaded environments. Flask provides you with a special object that threaded environments. Flask provides you with a special object that
ensures it is only valid for the active request and that will return ensures it is only valid for the active request and that will return
different values for each request. In a nutshell: it does the right different values for each request. In a nutshell: it does the right
thing, like it does for :class:`request` and :class:`session`. thing, like it does for :data:`.request` and :data:`.session`.
.. data:: g .. data:: g
A namespace object that can store data during an A proxy to a namespace object used to store data during a single request or
:doc:`application context </appcontext>`. This is an instance of app context. An instance of :attr:`.Flask.app_ctx_globals_class`, which
:attr:`Flask.app_ctx_globals_class`, which defaults to defaults to :class:`._AppCtxGlobals`.
:class:`ctx._AppCtxGlobals`.
This is a good place to store resources during a request. For This is a good place to store resources during a request. For example, a
example, a ``before_request`` function could load a user object from :meth:`~.Flask.before_request` function could load a user object from a
a session id, then set ``g.user`` to be used in the view function. session id, then set ``g.user`` to be used in the view function.
This is a proxy. See :ref:`notes-on-proxies` for more information. This is only available when an :doc:`app context </appcontext>` is active.
This is a proxy. See :ref:`context-visibility` for more information.
.. versionchanged:: 0.10 .. versionchanged:: 0.10
Bound to the application context instead of the request context. Bound to the application context instead of the request context.
@ -185,17 +177,16 @@ Useful Functions and Classes
.. data:: current_app .. data:: current_app
A proxy to the application handling the current request. This is A proxy to the :class:`.Flask` application handling the current request or
useful to access the application without needing to import it, or if other activity.
it can't be imported, such as when using the application factory
pattern or in blueprints and extensions.
This is only available when an This is useful to access the application without needing to import it, or if
:doc:`application context </appcontext>` is pushed. This happens it can't be imported, such as when using the application factory pattern or
automatically during requests and CLI commands. It can be controlled in blueprints and extensions.
manually with :meth:`~flask.Flask.app_context`.
This is a proxy. See :ref:`notes-on-proxies` for more information. This is only available when an :doc:`app context </appcontext>` is active.
This is a proxy. See :ref:`context-visibility` for more information.
.. autofunction:: has_request_context .. autofunction:: has_request_context
@ -299,31 +290,31 @@ Stream Helpers
Useful Internals Useful Internals
---------------- ----------------
.. autoclass:: flask.ctx.RequestContext
:members:
.. data:: flask.globals.request_ctx
The current :class:`~flask.ctx.RequestContext`. If a request context
is not active, accessing attributes on this proxy will raise a
``RuntimeError``.
This is an internal object that is essential to how Flask handles
requests. Accessing this should not be needed in most cases. Most
likely you want :data:`request` and :data:`session` instead.
.. autoclass:: flask.ctx.AppContext .. autoclass:: flask.ctx.AppContext
:members: :members:
.. data:: flask.globals.app_ctx .. data:: flask.globals.app_ctx
The current :class:`~flask.ctx.AppContext`. If an app context is not A proxy to the active :class:`.AppContext`.
active, accessing attributes on this proxy will raise a
``RuntimeError``.
This is an internal object that is essential to how Flask handles This is an internal object that is essential to how Flask handles requests.
requests. Accessing this should not be needed in most cases. Most Accessing this should not be needed in most cases. Most likely you want
likely you want :data:`current_app` and :data:`g` instead. :data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session` instead.
This is only available when a :doc:`request context </appcontext>` is
active.
This is a proxy. See :ref:`context-visibility` for more information.
.. class:: flask.ctx.RequestContext
.. deprecated:: 3.2
Merged with :class:`AppContext`. This alias will be removed in Flask 4.0.
.. data:: flask.globals.request_ctx
.. deprecated:: 3.2
Merged with :data:`.app_ctx`. This alias will be removed in Flask 4.0.
.. autoclass:: flask.blueprints.BlueprintSetupState .. autoclass:: flask.blueprints.BlueprintSetupState
:members: :members:
@ -605,7 +596,7 @@ This specifies that ``/users/`` will be the URL for page one and
``/users/page/N`` will be the URL for page ``N``. ``/users/page/N`` will be the URL for page ``N``.
If a URL contains a default value, it will be redirected to its simpler If a URL contains a default value, it will be redirected to its simpler
form with a 301 redirect. In the above example, ``/users/page/1`` will form with a 308 redirect. In the above example, ``/users/page/1`` will
be redirected to ``/users/``. If your route handles ``GET`` and ``POST`` be redirected to ``/users/``. If your route handles ``GET`` and ``POST``
requests, make sure the default route only handles ``GET``, as redirects requests, make sure the default route only handles ``GET``, as redirects
can't preserve form data. :: can't preserve form data. ::

View file

@ -1,74 +1,63 @@
.. currentmodule:: flask The App and Request Context
===========================
The Application Context The context keeps track of data and objects during a request, CLI command, or
======================= other activity. Rather than passing this data around to every function, the
:data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session` proxies
are accessed instead.
The application context keeps track of the application-level data during When handling a request, the context is referred to as the "request context"
a request, CLI command, or other activity. Rather than passing the because it contains request data in addition to application data. Otherwise,
application around to each function, the :data:`current_app` and such as during a CLI command, it is referred to as the "app context". During an
:data:`g` proxies are accessed instead. app context, :data:`.current_app` and :data:`.g` are available, while during a
request context :data:`.request` and :data:`.session` are also available.
This is similar to :doc:`/reqcontext`, which keeps track of
request-level data during a request. A corresponding application context
is pushed when a request context is pushed.
Purpose of the Context Purpose of the Context
---------------------- ----------------------
The :class:`Flask` application object has attributes, such as The context and proxies help solve two development issues: circular imports, and
:attr:`~Flask.config`, that are useful to access within views and passing around global data during a request.
:doc:`CLI commands </cli>`. However, importing the ``app`` instance
within the modules in your project is prone to circular import issues.
When using the :doc:`app factory pattern </patterns/appfactories>` or
writing reusable :doc:`blueprints </blueprints>` or
:doc:`extensions </extensions>` there won't be an ``app`` instance to
import at all.
Flask solves this issue with the *application context*. Rather than The :class:`.Flask` application object has attributes, such as
referring to an ``app`` directly, you use the :data:`current_app` :attr:`~.Flask.config`, that are useful to access within views and other
proxy, which points to the application handling the current activity. functions. However, importing the ``app`` instance within the modules in your
project is prone to circular import issues. When using the
:doc:`app factory pattern </patterns/appfactories>` or writing reusable
:doc:`blueprints </blueprints>` or :doc:`extensions </extensions>` there won't
be an ``app`` instance to import at all.
Flask automatically *pushes* an application context when handling a When the application handles a request, it creates a :class:`.Request` object.
request. View functions, error handlers, and other functions that run Because a *worker* handles only one request at a time, the request data can be
during a request will have access to :data:`current_app`. considered global to that worker during that request. Passing it as an argument
through every function during the request becomes verbose and redundant.
Flask will also automatically push an app context when running CLI Flask solves these issues with the *active context* pattern. Rather than
commands registered with :attr:`Flask.cli` using ``@app.cli.command()``. importing an ``app`` directly, or having to pass it and the request through to
every single function, you import and access the proxies, which point to the
currently active application and request data. This is sometimes referred to
as "context local" data.
Lifetime of the Context Context During Setup
----------------------- --------------------
The application context is created and destroyed as necessary. When a If you try to access :data:`.current_app`, :data:`.g`, or anything that uses it,
Flask application begins handling a request, it pushes an application outside an app context, you'll get this error message:
context and a :doc:`request context </reqcontext>`. When the request
ends it pops the request context then the application context.
Typically, an application context will have the same lifetime as a
request.
See :doc:`/reqcontext` for more information about how the contexts work
and the full life cycle of a request.
Manually Push a Context
-----------------------
If you try to access :data:`current_app`, or anything that uses it,
outside an application context, you'll get this error message:
.. code-block:: pytb .. code-block:: pytb
RuntimeError: Working outside of application context. RuntimeError: Working outside of application context.
This typically means that you attempted to use functionality that Attempted to use functionality that expected a current application to be
needed to interface with the current application object in some way. set. To solve this, set up an app context using 'with app.app_context()'.
To solve this, set up an application context with app.app_context(). See the documentation on app context for more information.
If you see that error while configuring your application, such as when If you see that error while configuring your application, such as when
initializing an extension, you can push a context manually since you initializing an extension, you can push a context manually since you have direct
have direct access to the ``app``. Use :meth:`~Flask.app_context` in a access to the ``app``. Use :meth:`.Flask.app_context` in a ``with`` block.
``with`` block, and everything that runs in the block will have access
to :data:`current_app`. :: .. code-block:: python
def create_app(): def create_app():
app = Flask(__name__) app = Flask(__name__)
@ -78,70 +67,121 @@ to :data:`current_app`. ::
return app return app
If you see that error somewhere else in your code not related to If you see that error somewhere else in your code not related to setting up the
configuring the application, it most likely indicates that you should application, it most likely indicates that you should move that code into a view
move that code into a view function or CLI command. function or CLI command.
Storing Data Context During Testing
------------ ----------------------
The application context is a good place to store common data during a See :doc:`/testing` for detailed information about managing the context during
request or CLI command. Flask provides the :data:`g object <g>` for this tests.
purpose. It is a simple namespace object that has the same lifetime as
an application context.
.. note:: If you try to access :data:`.request`, :data:`.session`, or anything that uses
The ``g`` name stands for "global", but that is referring to the it, outside a request context, you'll get this error message:
data being global *within a context*. The data on ``g`` is lost
after the context ends, and it is not an appropriate place to store
data between requests. Use the :data:`session` or a database to
store data across requests.
A common use for :data:`g` is to manage resources during a request. .. code-block:: pytb
1. ``get_X()`` creates resource ``X`` if it does not exist, caching it RuntimeError: Working outside of request context.
as ``g.X``.
2. ``teardown_X()`` closes or otherwise deallocates the resource if it
exists. It is registered as a :meth:`~Flask.teardown_appcontext`
handler.
For example, you can manage a database connection using this pattern:: Attempted to use functionality that expected an active HTTP request. See the
documentation on request context for more information.
from flask import g This will probably only happen during tests. If you see that error somewhere
else in your code not related to testing, it most likely indicates that you
should move that code into a view function.
def get_db(): The primary way to solve this is to use :meth:`.Flask.test_client` to simulate
if 'db' not in g: a full request.
g.db = connect_to_database()
return g.db If you only want to unit test one function, rather than a full request, use
:meth:`.Flask.test_request_context` in a ``with`` block.
@app.teardown_appcontext .. code-block:: python
def teardown_db(exception):
db = g.pop('db', None)
if db is not None: def generate_report(year):
db.close() format = request.args.get("format")
...
During a request, every call to ``get_db()`` will return the same with app.test_request_context(
connection, and it will be closed automatically at the end of the "/make_report/2017", query_string={"format": "short"}
request. ):
generate_report()
You can use :class:`~werkzeug.local.LocalProxy` to make a new context
local from ``get_db()``::
from werkzeug.local import LocalProxy
db = LocalProxy(get_db)
Accessing ``db`` will call ``get_db`` internally, in the same way that
:data:`current_app` works.
Events and Signals .. _context-visibility:
------------------
The application will call functions registered with :meth:`~Flask.teardown_appcontext` Visibility of the Context
when the application context is popped. -------------------------
The following signals are sent: :data:`appcontext_pushed`, The context will have the same lifetime as an activity, such as a request, CLI
:data:`appcontext_tearing_down`, and :data:`appcontext_popped`. command, or ``with`` block. Various callbacks and signals registered with the
app will be run during the context.
When a Flask application handles a request, it pushes a request context
to set the active application and request data. When it handles a CLI command,
it pushes an app context to set the active application. When the activity ends,
it pops that context. Proxy objects like :data:`.request`, :data:`.session`,
:data:`.g`, and :data:`.current_app`, are accessible while the context is pushed
and active, and are not accessible after the context is popped.
The context is unique to each thread (or other worker type). The proxies cannot
be passed to another worker, which has a different context space and will not
know about the active context in the parent's space.
Besides being scoped to each worker, the proxy object has a separate type and
identity than the proxied real object. In some cases you'll need access to the
real object, rather than the proxy. Use the
:meth:`~.LocalProxy._get_current_object` method in those cases.
.. code-block:: python
app = current_app._get_current_object()
my_signal.send(app)
Lifecycle of the Context
------------------------
Flask dispatches a request in multiple stages which can affect the request,
response, and how errors are handled. See :doc:`/lifecycle` for a list of all
the steps, callbacks, and signals during each request. The following are the
steps directly related to the context.
- The app context is pushed, the proxies are available.
- The :data:`.appcontext_pushed` signal is sent.
- The request is dispatched.
- Any :meth:`.Flask.teardown_request` decorated functions are called.
- The :data:`.request_tearing_down` signal is sent.
- Any :meth:`.Flask.teardown_appcontext` decorated functions are called.
- The :data:`.appcontext_tearing_down` signal is sent.
- The app context is popped, the proxies are no longer available.
- The :data:`.appcontext_popped` signal is sent.
The teardown callbacks are called by the context when it is popped. They are
called even if there is an unhandled exception during dispatch. They may be
called multiple times in some test scenarios. This means there is no guarantee
that any other parts of the request dispatch have run. Be sure to write these
functions in a way that does not depend on other callbacks. All callbacks are
called even if any raise an error.
How the Context Works
---------------------
Context locals are implemented using Python's :mod:`contextvars` and Werkzeug's
:class:`~werkzeug.local.LocalProxy`. Python's contextvars are a low level
structure to manage data local to a thread or coroutine. ``LocalProxy`` wraps
the contextvar so that access to any attributes and methods is forwarded to the
object stored in the contextvar.
The context is tracked like a stack, with the active context at the top of the
stack. Flask manages pushing and popping contexts during requests, CLI commands,
testing, ``with`` blocks, etc. The proxies access attributes on the active
context.
Because it is a stack, other contexts may be pushed to change the proxies during
an already active context. This is not a common pattern, but can be used in
advanced use cases. For example, a Flask application can be used as WSGI
middleware, calling another wrapped Flask app from a view.

View file

@ -23,12 +23,6 @@ method in views that inherit from the :class:`flask.views.View` class, as
well as all the HTTP method handlers in views that inherit from the well as all the HTTP method handlers in views that inherit from the
:class:`flask.views.MethodView` class. :class:`flask.views.MethodView` class.
.. admonition:: Using ``async`` with greenlet
When using gevent or eventlet to serve an application or patch the
runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
required.
Performance Performance
----------- -----------
@ -78,15 +72,15 @@ Flask based on the `ASGI`_ standard instead of WSGI. This allows it to
handle many concurrent requests, long running requests, and websockets handle many concurrent requests, long running requests, and websockets
without requiring multiple worker processes or threads. without requiring multiple worker processes or threads.
It has also already been possible to run Flask with Gevent or Eventlet It has also already been possible to :doc:`run Flask with Gevent </gevent>` to
to get many of the benefits of async request handling. These libraries get many of the benefits of async request handling. Gevent patches low-level
patch low-level Python functions to accomplish this, whereas ``async``/ Python functions to accomplish this, whereas ``async``/``await`` and ASGI use
``await`` and ASGI use standard, modern Python capabilities. Deciding standard, modern Python capabilities. Deciding whether you should use gevent
whether you should use Flask, Quart, or something else is ultimately up with Flask, or Quart, or something else is ultimately up to understanding the
to understanding the specific needs of your project. specific needs of your project.
.. _Quart: https://github.com/pallets/quart .. _Quart: https://quart.palletsprojects.com
.. _ASGI: https://asgi.readthedocs.io/en/latest/ .. _ASGI: https://asgi.readthedocs.io
Extensions Extensions
@ -120,6 +114,6 @@ implemented async support, or make a feature request or PR to them.
Other event loops Other event loops
----------------- -----------------
At the moment Flask only supports :mod:`asyncio`. It's possible to At the moment Flask only supports :mod:`asyncio`. It's possible to override
override :meth:`flask.Flask.ensure_sync` to change how async functions :meth:`flask.Flask.ensure_sync` to change how async functions are wrapped to use
are wrapped to use a different library. a different library. See :ref:`gevent-asyncio` for an example.

View file

@ -26,6 +26,7 @@ autodoc_preserve_defaults = True
extlinks = { extlinks = {
"issue": ("https://github.com/pallets/flask/issues/%s", "#%s"), "issue": ("https://github.com/pallets/flask/issues/%s", "#%s"),
"pr": ("https://github.com/pallets/flask/pull/%s", "#%s"), "pr": ("https://github.com/pallets/flask/pull/%s", "#%s"),
"ghsa": ("https://github.com/pallets/flask/security/advisories/GHSA-%s", "GHSA-%s"),
} }
intersphinx_mapping = { intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None), "python": ("https://docs.python.org/3/", None),
@ -57,8 +58,8 @@ html_sidebars = {
} }
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
html_static_path = ["_static"] html_static_path = ["_static"]
html_favicon = "_static/shortcut-icon.png" html_favicon = "_static/flask-icon.svg"
html_logo = "_static/flask-vertical.png" html_logo = "_static/flask-logo.svg"
html_title = f"Flask Documentation ({version})" html_title = f"Flask Documentation ({version})"
html_show_sourcelink = False html_show_sourcelink = False

View file

@ -79,7 +79,7 @@ The following configuration values are used internally by Flask:
.. py:data:: TESTING .. py:data:: TESTING
Enable testing mode. Exceptions are propagated rather than handled by the Enable testing mode. Exceptions are propagated rather than handled by
the app's error handlers. Extensions may also change their behavior to the app's error handlers. Extensions may also change their behavior to
facilitate easier testing. You should enable this in your own tests. facilitate easier testing. You should enable this in your own tests.
@ -127,13 +127,16 @@ The following configuration values are used internally by Flask:
.. py:data:: SECRET_KEY_FALLBACKS .. py:data:: SECRET_KEY_FALLBACKS
A list of old secret keys that can still be used for unsigning, most recent A list of old secret keys that can still be used for unsigning. This allows
first. This allows a project to implement key rotation without invalidating a project to implement key rotation without invalidating active sessions or
active sessions or other recently-signed secrets. other recently-signed secrets.
Keys should be removed after an appropriate period of time, as checking each Keys should be removed after an appropriate period of time, as checking each
additional key adds some overhead. additional key adds some overhead.
Order should not matter, but the default implementation will test the last
key in the list first, so it might make sense to order oldest to newest.
Flask's built-in secure cookie session supports this. Extensions that use Flask's built-in secure cookie session supports this. Extensions that use
:data:`SECRET_KEY` may not support this yet. :data:`SECRET_KEY` may not support this yet.
@ -442,7 +445,7 @@ The following configuration values are used internally by Flask:
.. versionchanged:: 2.3 .. versionchanged:: 2.3
``ENV`` was removed. ``ENV`` was removed.
.. versionadded:: 3.10 .. versionadded:: 3.1
Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default
addition of autogenerated OPTIONS responses. addition of autogenerated OPTIONS responses.

View file

@ -1,7 +1,7 @@
Contributing Contributing
============ ============
See the Pallets `detailed contributing documentation <_contrib>`_ for many ways See the Pallets `detailed contributing documentation <contrib_>`_ for many ways
to contribute, including reporting issues, requesting features, asking or to contribute, including reporting issues, requesting features, asking or
answering questions, and making PRs. answering questions, and making PRs.

View file

@ -1,80 +1,8 @@
:orphan:
eventlet eventlet
======== ========
Prefer using :doc:`gunicorn` with eventlet workers rather than using `Eventlet is no longer maintained.`__ Use :doc:`/deploying/gevent` instead.
`eventlet`_ directly. Gunicorn provides a much more configurable and
production-tested server.
`eventlet`_ allows writing asynchronous, coroutine-based code that looks __ https://eventlet.readthedocs.io
like standard synchronous Python. It uses `greenlet`_ to enable task
switching without writing ``async/await`` or using ``asyncio``.
:doc:`gevent` is another library that does the same thing. Certain
dependencies you have, or other considerations, may affect which of the
two you choose to use.
eventlet provides a WSGI server that can handle many connections at once
instead of one per worker process. You must actually use eventlet in
your own code to see any benefit to using the server.
.. _eventlet: https://eventlet.net/
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
Installing
----------
When using eventlet, greenlet>=1.0 is required, otherwise context locals
such as ``request`` will not work as expected. When using PyPy,
PyPy>=7.3.7 is required.
Create a virtualenv, install your application, then install
``eventlet``.
.. code-block:: text
$ cd hello-app
$ python -m venv .venv
$ . .venv/bin/activate
$ pip install . # install your application
$ pip install eventlet
Running
-------
To use eventlet to serve your application, write a script that imports
its ``wsgi.server``, as well as your app or app factory.
.. code-block:: python
:caption: ``wsgi.py``
import eventlet
from eventlet import wsgi
from hello import create_app
app = create_app()
wsgi.server(eventlet.listen(("127.0.0.1", 8000)), app)
.. code-block:: text
$ python wsgi.py
(x) wsgi starting up on http://127.0.0.1:8000
Binding Externally
------------------
eventlet should not be run as root because it would cause your
application code to run as root, which is not secure. However, this
means it will not be possible to bind to port 80 or 443. Instead, a
reverse proxy such as :doc:`nginx` or :doc:`apache-httpd` should be used
in front of eventlet.
You can bind to all external IPs on a non-privileged port by using
``0.0.0.0`` in the server arguments shown in the previous section.
Don't do this when using a reverse proxy setup, otherwise it will be
possible to bypass the proxy.
``0.0.0.0`` is not a valid address to navigate to, you'd use a specific
IP address in your browser.

View file

@ -7,15 +7,12 @@ configurable and production-tested servers.
`gevent`_ allows writing asynchronous, coroutine-based code that looks `gevent`_ allows writing asynchronous, coroutine-based code that looks
like standard synchronous Python. It uses `greenlet`_ to enable task like standard synchronous Python. It uses `greenlet`_ to enable task
switching without writing ``async/await`` or using ``asyncio``. switching without writing ``async/await`` or using ``asyncio``. This is
not the same as Python's ``async/await``, or the ASGI server spec.
:doc:`eventlet` is another library that does the same thing. Certain
dependencies you have, or other considerations, may affect which of the
two you choose to use.
gevent provides a WSGI server that can handle many connections at once gevent provides a WSGI server that can handle many connections at once
instead of one per worker process. You must actually use gevent in your instead of one per worker process. See :doc:`/gevent` for more
own code to see any benefit to using the server. information about enabling it in your application.
.. _gevent: https://www.gevent.org/ .. _gevent: https://www.gevent.org/
.. _greenlet: https://greenlet.readthedocs.io/en/latest/ .. _greenlet: https://greenlet.readthedocs.io/en/latest/
@ -24,8 +21,7 @@ own code to see any benefit to using the server.
Installing Installing
---------- ----------
When using gevent, greenlet>=1.0 is required, otherwise context locals When using gevent, greenlet>=1.0 is required. When using PyPy,
such as ``request`` will not work as expected. When using PyPy,
PyPy>=7.3.7 is required. PyPy>=7.3.7 is required.
Create a virtualenv, install your application, then install ``gevent``. Create a virtualenv, install your application, then install ``gevent``.

View file

@ -8,7 +8,7 @@ multiple worker implementations for performance tuning.
* It does not support Windows (but does run on WSL). * It does not support Windows (but does run on WSL).
* It is easy to install as it does not require additional dependencies * It is easy to install as it does not require additional dependencies
or compilation. or compilation.
* It has built-in async worker support using gevent or eventlet. * It has built-in async worker support using gevent.
This page outlines the basics of running Gunicorn. Be sure to read its This page outlines the basics of running Gunicorn. Be sure to read its
`documentation`_ and use ``gunicorn --help`` to understand what features `documentation`_ and use ``gunicorn --help`` to understand what features
@ -93,20 +93,19 @@ otherwise it will be possible to bypass the proxy.
IP address in your browser. IP address in your browser.
Async with gevent or eventlet Async with gevent
----------------------------- -----------------
The default sync worker is appropriate for many use cases. If you need The default sync worker is appropriate for most use cases. If you need numerous,
asynchronous support, Gunicorn provides workers using either `gevent`_ long running, concurrent connections, Gunicorn provides an asynchronous worker
or `eventlet`_. This is not the same as Python's ``async/await``, or the using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI
ASGI server spec. You must actually use gevent/eventlet in your own code server spec. See :doc:`/gevent` for more information about enabling it in your
to see any benefit to using the workers. application.
When using either gevent or eventlet, greenlet>=1.0 is required, .. _gevent: https://www.gevent.org/
otherwise context locals such as ``request`` will not work as expected.
When using PyPy, PyPy>=7.3.7 is required.
To use gevent: When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
required.
.. code-block:: text .. code-block:: text
@ -115,16 +114,3 @@ To use gevent:
Listening at: http://127.0.0.1:8000 (x) Listening at: http://127.0.0.1:8000 (x)
Using worker: gevent Using worker: gevent
Booting worker with pid: x Booting worker with pid: x
To use eventlet:
.. code-block:: text
$ gunicorn -k eventlet 'hello:create_app()'
Starting gunicorn 20.1.0
Listening at: http://127.0.0.1:8000 (x)
Using worker: eventlet
Booting worker with pid: x
.. _gevent: https://www.gevent.org/
.. _eventlet: https://eventlet.net/

View file

@ -36,7 +36,6 @@ discusses platforms that can manage this for you.
mod_wsgi mod_wsgi
uwsgi uwsgi
gevent gevent
eventlet
asgi asgi
WSGI servers have HTTP servers built-in. However, a dedicated HTTP WSGI servers have HTTP servers built-in. However, a dedicated HTTP

View file

@ -119,15 +119,16 @@ IP address in your browser.
Async with gevent Async with gevent
----------------- -----------------
The default sync worker is appropriate for many use cases. If you need The default sync worker is appropriate for most use cases. If you need numerous,
asynchronous support, uWSGI provides a `gevent`_ worker. This is not the long running, concurrent connections, uWSGI provides an asynchronous worker
same as Python's ``async/await``, or the ASGI server spec. You must using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI
actually use gevent in your own code to see any benefit to using the server spec. See :doc:`/gevent` for more information about enabling it in your
worker. application.
When using gevent, greenlet>=1.0 is required, otherwise context locals .. _gevent: https://www.gevent.org/
such as ``request`` will not work as expected. When using PyPy,
PyPy>=7.3.7 is required. When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
required.
.. code-block:: text .. code-block:: text
@ -140,6 +141,3 @@ PyPy>=7.3.7 is required.
spawned uWSGI worker 1 (pid: x, cores: 100) spawned uWSGI worker 1 (pid: x, cores: 100)
spawned uWSGI http 1 (pid: x) spawned uWSGI http 1 (pid: x)
*** running gevent loop engine [addr:x] *** *** running gevent loop engine [addr:x] ***
.. _gevent: https://www.gevent.org/

View file

@ -96,10 +96,10 @@ is ambiguous.
One Template Engine One Template Engine
------------------- -------------------
Flask decides on one template engine: Jinja2. Why doesn't Flask have a Flask decides on one template engine: Jinja. Why doesn't Flask have a
pluggable template engine interface? You can obviously use a different pluggable template engine interface? You can obviously use a different
template engine, but Flask will still configure Jinja2 for you. While template engine, but Flask will still configure Jinja for you. While
that limitation that Jinja2 is *always* configured will probably go away, that limitation that Jinja is *always* configured will probably go away,
the decision to bundle one template engine and use that will not. the decision to bundle one template engine and use that will not.
Template engines are like programming languages and each of those engines Template engines are like programming languages and each of those engines
@ -107,7 +107,7 @@ has a certain understanding about how things work. On the surface they
all work the same: you tell the engine to evaluate a template with a set all work the same: you tell the engine to evaluate a template with a set
of variables and take the return value as string. of variables and take the return value as string.
But that's about where similarities end. Jinja2 for example has an But that's about where similarities end. Jinja for example has an
extensive filter system, a certain way to do template inheritance, extensive filter system, a certain way to do template inheritance,
support for reusable blocks (macros) that can be used from inside support for reusable blocks (macros) that can be used from inside
templates and also from Python code, supports iterative template templates and also from Python code, supports iterative template
@ -118,8 +118,8 @@ other hand treats templates similar to Python modules.
When it comes to connecting a template engine with an application or When it comes to connecting a template engine with an application or
framework there is more than just rendering templates. For instance, framework there is more than just rendering templates. For instance,
Flask uses Jinja2's extensive autoescaping support. Also it provides Flask uses Jinja's extensive autoescaping support. Also it provides
ways to access macros from Jinja2 templates. ways to access macros from Jinja templates.
A template abstraction layer that would not take the unique features of A template abstraction layer that would not take the unique features of
the template engines away is a science on its own and a too large the template engines away is a science on its own and a too large
@ -150,7 +150,7 @@ authentication technologies, and more. Flask may be "micro", but it's ready for
production use on a variety of needs. production use on a variety of needs.
Why does Flask call itself a microframework and yet it depends on two Why does Flask call itself a microframework and yet it depends on two
libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look libraries (namely Werkzeug and Jinja). Why shouldn't it? If we look
over to the Ruby side of web development there we have a protocol very over to the Ruby side of web development there we have a protocol very
similar to WSGI. Just that it's called Rack there, but besides that it similar to WSGI. Just that it's called Rack there, but besides that it
looks very much like a WSGI rendition for Ruby. But nearly all looks very much like a WSGI rendition for Ruby. But nearly all
@ -169,19 +169,20 @@ infrastructure, packages with dependencies are no longer an issue and
there are very few reasons against having libraries that depend on others. there are very few reasons against having libraries that depend on others.
Thread Locals Context Locals
------------- --------------
Flask uses thread local objects (context local objects in fact, they Flask uses special context locals and proxies to provide access to the
support greenlet contexts as well) for request, session and an extra current app and request data to any code running during a request, CLI command,
object you can put your own things on (:data:`~flask.g`). Why is that and etc. Context locals are specific to the worker handling the activity, such as a
isn't that a bad idea? thread, process, coroutine, or greenlet.
Yes it is usually not such a bright idea to use thread locals. They cause The context and proxies help solve two development issues: circular imports, and
troubles for servers that are not based on the concept of threads and make passing around global data. :data:`.current_app` can be used to access the
large applications harder to maintain. However Flask is just not designed application object without needing to import the app object directly, avoiding
for large applications or asynchronous servers. Flask wants to make it circular import issues. :data:`.request`, :data:`.session`, and :data:`.g` can
quick and easy to write a traditional web application. be imported to access the current data for the request, rather than needing to
pass them as arguments through every single function in your project.
Async/await and ASGI support Async/await and ASGI support
@ -208,7 +209,7 @@ What Flask is, What Flask is Not
Flask will never have a database layer. It will not have a form library Flask will never have a database layer. It will not have a form library
or anything else in that direction. Flask itself just bridges to Werkzeug or anything else in that direction. Flask itself just bridges to Werkzeug
to implement a proper WSGI application and to Jinja2 to handle templating. to implement a proper WSGI application and to Jinja to handle templating.
It also binds to a few common standard library packages such as logging. It also binds to a few common standard library packages such as logging.
Everything else is up for extensions. Everything else is up for extensions.

View file

@ -67,7 +67,7 @@ application instance.
It is important that the app is not stored on the extension, don't do It is important that the app is not stored on the extension, don't do
``self.app = app``. The only time the extension should have direct ``self.app = app``. The only time the extension should have direct
access to an app is during ``init_app``, otherwise it should use access to an app is during ``init_app``, otherwise it should use
:data:`current_app`. :data:`.current_app`.
This allows the extension to support the application factory pattern, This allows the extension to support the application factory pattern,
avoids circular import issues when importing the extension instance avoids circular import issues when importing the extension instance
@ -105,7 +105,7 @@ during an extension's ``init_app`` method.
A common pattern is to use :meth:`~Flask.before_request` to initialize A common pattern is to use :meth:`~Flask.before_request` to initialize
some data or a connection at the beginning of each request, then some data or a connection at the beginning of each request, then
:meth:`~Flask.teardown_request` to clean it up at the end. This can be :meth:`~Flask.teardown_request` to clean it up at the end. This can be
stored on :data:`g`, discussed more below. stored on :data:`.g`, discussed more below.
A more lazy approach is to provide a method that initializes and caches A more lazy approach is to provide a method that initializes and caches
the data or connection. For example, a ``ext.get_db`` method could the data or connection. For example, a ``ext.get_db`` method could
@ -179,13 +179,12 @@ name as a prefix, or as a namespace.
g._hello = SimpleNamespace() g._hello = SimpleNamespace()
g._hello.user_id = 2 g._hello.user_id = 2
The data in ``g`` lasts for an application context. An application The data in ``g`` lasts for an application context. An application context is
context is active when a request context is, or when a CLI command is active during a request, CLI command, or ``with app.app_context()`` block. If
run. If you're storing something that should be closed, use you're storing something that should be closed, use
:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed :meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed when the
when the application context ends. If it should only be valid during a app context ends. If it should only be valid during a request, or would not be
request, or would not be used in the CLI outside a request, use used in the CLI outside a request, use :meth:`~flask.Flask.teardown_request`.
:meth:`~flask.Flask.teardown_request`.
Views and Models Views and Models
@ -294,11 +293,13 @@ ecosystem remain consistent and compatible.
indicate minimum compatibility support. For example, indicate minimum compatibility support. For example,
``sqlalchemy>=1.4``. ``sqlalchemy>=1.4``.
9. Indicate the versions of Python supported using ``python_requires=">=version"``. 9. Indicate the versions of Python supported using ``python_requires=">=version"``.
Flask itself supports Python >=3.9 as of October 2024, and this will update Flask and Pallets policy is to support all Python versions that are not
over time. within six months of end of life (EOL). See Python's `EOL calendar`_ for
timing.
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
.. _Discord Chat: https://discord.gg/pallets .. _Discord Chat: https://discord.gg/pallets
.. _GitHub Discussions: https://github.com/pallets/flask/discussions .. _GitHub Discussions: https://github.com/pallets/flask/discussions
.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/ .. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/
.. _Pallets-Eco: https://github.com/pallets-eco .. _Pallets-Eco: https://github.com/pallets-eco
.. _EOL calendar: https://devguide.python.org/versions/

125
docs/gevent.rst Normal file
View file

@ -0,0 +1,125 @@
Async with Gevent
=================
`Gevent`_ patches Python's standard library to run within special async workers
called `greenlets`_. Gevent has existed since long before Python's native
asyncio was available, and Flask has always worked with it.
.. _gevent: https://www.gevent.org
.. _greenlets: https://greenlet.readthedocs.io
Gevent is a reliable way to handle numerous, long lived, concurrent connections,
and to achieve similar capabilities to ASGI and asyncio. This works without
needing to write ``async def`` or ``await`` anywhere, but relies on gevent and
greenlet's low level manipulation of the Python interpreter.
Deciding whether you should use gevent with Flask, or `Quart`_, or something
else, is ultimately up to understanding the specific needs of your project.
.. _quart: https://quart.palletsprojects.com
Enabling gevent
---------------
You need to apply gevent's patching as early as possible in your code. This
enables gevent's underlying event loop and converts many Python internals to run
inside it. Add the following at the top of your project's module or top
``__init__.py``:
.. code-block:: python
import gevent.monkey
gevent.monkey.patch_all()
When deploying in production, use :doc:`/deploying/gunicorn` or
:doc:`/deploying/uwsgi` with a gevent worker, as described on those pages.
To run concurrent tasks within your own code, such as views, use
|gevent.spawn|_:
.. |gevent.spawn| replace:: ``gevent.spawn()``
.. _gevent.spawn: https://www.gevent.org/api/gevent.html#gevent.spawn
.. code-block:: python
@app.post("/send")
def send_email():
gevent.spawn(email.send, to="example@example.example", text="example")
return "Email is being sent."
If you need to access :data:`request` or other Flask context globals within the
spawned function, decorate the function with :func:`.stream_with_context` or
:func:`.copy_current_request_context`. Prefer passing the exact data you need
when spawning the function, rather than using the decorators.
.. note::
When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7
is required.
.. _gevent-asyncio:
Combining with ``async``/``await``
----------------------------------
Gevent's patching does not interact well with Flask's built-in asyncio support.
If you want to use Gevent and asyncio in the same app, you'll need to override
:meth:`flask.Flask.async_to_sync` to run async functions inside gevent.
.. code-block:: python
import gevent.monkey
gevent.monkey.patch_all()
import asyncio
from flask import Flask, request
loop = asyncio.EventLoop()
gevent.spawn(loop.run_forever)
class GeventFlask(Flask):
def async_to_sync(self, func):
def run(*args, **kwargs):
coro = func(*args, **kwargs)
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result()
return run
app = GeventFlask(__name__)
@app.get("/")
async def greet():
await asyncio.sleep(1)
return f"Hello, {request.args.get("name", "World")}!"
This starts an asyncio event loop in a gevent worker. Async functions are
scheduled on that event loop. This may still have limitations, and may need to
be modified further when using other asyncio implementations.
libuv
~~~~~
`libuv`_ is another event loop implementation that `gevent supports`_. There's
also a project called `uvloop`_ that enables libuv in asyncio. If you want to
use libuv, use gevent's support, not uvloop. It may be possible to further
modify the ``async_to_sync`` code from the previous section to work with uvloop,
but that's not currently known.
.. _libuv: https://libuv.org/
.. _gevent supports: https://www.gevent.org/loop_impls.html
.. _uvloop: https://uvloop.readthedocs.io/
To enable gevent's libuv support, add the following at the *very* top of your
code, before ``gevent.monkey.patch_all()``:
.. code-block:: python
import gevent
gevent.config.loop = "libuv"
import gevent.monkey
gevent.monkey.patch_all()

View file

@ -3,8 +3,9 @@
Welcome to Flask Welcome to Flask
================ ================
.. image:: _static/flask-horizontal.png .. image:: _static/flask-name.svg
:align: center :align: center
:height: 200px
Welcome to Flask's documentation. Flask is a lightweight WSGI web application framework. Welcome to Flask's documentation. Flask is a lightweight WSGI web application framework.
It is designed to make getting started quick and easy, with the ability to scale up to It is designed to make getting started quick and easy, with the ability to scale up to
@ -51,7 +52,6 @@ community-maintained extensions to add even more functionality.
views views
lifecycle lifecycle
appcontext appcontext
reqcontext
blueprints blueprints
extensions extensions
cli cli
@ -60,6 +60,7 @@ community-maintained extensions to add even more functionality.
patterns/index patterns/index
web-security web-security
deploying/index deploying/index
gevent
async-await async-await

View file

@ -5,7 +5,7 @@ Installation
Python Version Python Version
-------------- --------------
We recommend using the latest version of Python. Flask supports Python 3.9 and newer. We recommend using the latest version of Python. Flask supports Python 3.10 and newer.
Dependencies Dependencies
@ -51,9 +51,8 @@ use them if you install them.
greenlet greenlet
~~~~~~~~ ~~~~~~~~
You may choose to use gevent or eventlet with your application. In this You may choose to use :doc:`/gevent` with your application. In this case,
case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is required.
required.
These are not minimum supported versions, they only indicate the first These are not minimum supported versions, they only indicate the first
versions that added necessary features. You should use the latest versions that added necessary features. You should use the latest

View file

@ -117,15 +117,12 @@ the view function, and pass the return value back to the server. But there are m
parts that you can use to customize its behavior. parts that you can use to customize its behavior.
#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. #. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`.
#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` #. An :class:`.AppContext` object is created. This converts the WSGI ``environ``
dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. dict into a :class:`.Request` object.
#. The :doc:`app context <appcontext>` is pushed, which makes :data:`.current_app` and #. The :doc:`app context <appcontext>` is pushed, which makes
:data:`.g` available. :data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session`
available.
#. The :data:`.appcontext_pushed` signal is sent. #. The :data:`.appcontext_pushed` signal is sent.
#. The :doc:`request context <reqcontext>` is pushed, which makes :attr:`.request` and
:class:`.session` available.
#. The session is opened, loading any existing session data using the app's
:attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`.
#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` #. The URL is matched against the URL rules registered with the :meth:`~.Flask.route`
decorator during application setup. If there is no match, the error - usually a 404, decorator during application setup. If there is no match, the error - usually a 404,
405, or redirect - is stored to be handled later. 405, or redirect - is stored to be handled later.
@ -141,7 +138,8 @@ parts that you can use to customize its behavior.
called to handle the error and return a response. called to handle the error and return a response.
#. Whatever returned a response value - a before request function, the view, or an #. Whatever returned a response value - a before request function, the view, or an
error handler, that value is converted to a :class:`.Response` object. error handler, that value is converted to a :class:`.Response` object.
#. Any :func:`~.after_this_request` decorated functions are called, then cleared. #. Any :func:`~.after_this_request` decorated functions are called, which can modify
the response object. They are then cleared.
#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify #. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify
the response object. the response object.
#. The session is saved, persisting any modified session data using the app's #. The session is saved, persisting any modified session data using the app's
@ -154,14 +152,19 @@ parts that you can use to customize its behavior.
#. The response object's status, headers, and body are returned to the WSGI server. #. The response object's status, headers, and body are returned to the WSGI server.
#. Any :meth:`~.Flask.teardown_request` decorated functions are called. #. Any :meth:`~.Flask.teardown_request` decorated functions are called.
#. The :data:`.request_tearing_down` signal is sent. #. The :data:`.request_tearing_down` signal is sent.
#. The request context is popped, :attr:`.request` and :class:`.session` are no longer
available.
#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. #. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called.
#. The :data:`.appcontext_tearing_down` signal is sent. #. The :data:`.appcontext_tearing_down` signal is sent.
#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer #. The app context is popped, :data:`.current_app`, :data:`.g`, :data:`.request`,
available. and :data:`.session` are no longer available.
#. The :data:`.appcontext_popped` signal is sent. #. The :data:`.appcontext_popped` signal is sent.
When executing a CLI command or plain app context without request data, the same
order of steps is followed, omitting the steps that refer to the request.
A :class:`Blueprint` can add handlers for these events that are specific to the
blueprint. The handlers for a blueprint will run if the blueprint
owns the route that matches the request.
There are even more decorators and customization points than this, but that aren't part There are even more decorators and customization points than this, but that aren't part
of every request lifecycle. They're more specific to certain things you might use during of every request lifecycle. They're more specific to certain things you might use during
a request, such as templates, building URLs, or handling JSON data. See the rest of this a request, such as templates, building URLs, or handling JSON data. See the rest of this

View file

@ -99,9 +99,9 @@ to the factory like this:
.. code-block:: text .. code-block:: text
$ flask --app hello:create_app(local_auth=True) run $ flask --app 'hello:create_app(local_auth=True)' run
Then the ``create_app`` factory in ``myapp`` is called with the keyword Then the ``create_app`` factory in ``hello`` is called with the keyword
argument ``local_auth=True``. See :doc:`/cli` for more detail. argument ``local_auth=True``. See :doc:`/cli` for more detail.
Factory Improvements Factory Improvements

View file

@ -24,8 +24,11 @@ the root path of the domain you either need to configure the web server to
serve the icon at the root or if you can't do that you're out of luck. If serve the icon at the root or if you can't do that you're out of luck. If
however your application is the root you can simply route a redirect:: however your application is the root you can simply route a redirect::
app.add_url_rule('/favicon.ico', app.add_url_rule(
redirect_to=url_for('static', filename='favicon.ico')) "/favicon.ico",
endpoint="favicon",
redirect_to=url_for("static", filename="favicon.ico"),
)
If you want to save the extra redirect request you can also write a view If you want to save the extra redirect request you can also write a view
using :func:`~flask.send_from_directory`:: using :func:`~flask.send_from_directory`::

View file

@ -136,7 +136,8 @@ In general, prefer sending request data as form data, as would be used
when submitting an HTML form. JSON can represent more complex data, but when submitting an HTML form. JSON can represent more complex data, but
unless you need that it's better to stick with the simpler format. When unless you need that it's better to stick with the simpler format. When
sending JSON data, the ``Content-Type: application/json`` header must be sending JSON data, the ``Content-Type: application/json`` header must be
sent as well, otherwise Flask will return a 400 error. sent as well, otherwise Flask will return a 415 Unsupported Media Type
error.
.. code-block:: javascript .. code-block:: javascript
@ -244,8 +245,9 @@ Receiving JSON in Views
Use the :attr:`~flask.Request.json` property of the Use the :attr:`~flask.Request.json` property of the
:data:`~flask.request` object to decode the request's body as JSON. If :data:`~flask.request` object to decode the request's body as JSON. If
the body is not valid JSON, or the ``Content-Type`` header is not set to the body is not valid JSON, a 400 Bad Request error will be raised. If
``application/json``, a 400 Bad Request error will be raised. the ``Content-Type`` header is not set to ``application/json``, a 415
Unsupported Media Type error will be raised.
.. code-block:: python .. code-block:: python

View file

@ -10,8 +10,7 @@ A running MongoDB server and `Flask-MongoEngine`_ are required. ::
pip install flask-mongoengine pip install flask-mongoengine
.. _MongoEngine: http://mongoengine.org .. _MongoEngine: http://mongoengine.org
.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io .. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/
Configuration Configuration
------------- -------------
@ -80,7 +79,7 @@ Queries
Use the class ``objects`` attribute to make queries. A keyword argument Use the class ``objects`` attribute to make queries. A keyword argument
looks for an equal value on the field. :: looks for an equal value on the field. ::
bttf = Movies.objects(title="Back To The Future").get_or_404() bttf = Movie.objects(title="Back To The Future").get_or_404()
Query operators may be used by concatenating them with the field name Query operators may be used by concatenating them with the field name
using a double-underscore. ``objects``, and queries returned by using a double-underscore. ``objects``, and queries returned by

View file

@ -131,9 +131,8 @@ Here is an example :file:`database.py` module for your application::
def init_db(): def init_db():
metadata.create_all(bind=engine) metadata.create_all(bind=engine)
As in the declarative approach, you need to close the session after As in the declarative approach, you need to close the session after each app
each request or application context shutdown. Put this into your context. Put this into your application module::
application module::
from yourapplication.database import db_session from yourapplication.database import db_session

View file

@ -1,9 +1,9 @@
Using SQLite 3 with Flask Using SQLite 3 with Flask
========================= =========================
In Flask you can easily implement the opening of database connections on You can implement a few functions to work with a SQLite connection during a
demand and closing them when the context dies (usually at the end of the request context. The connection is created the first time it's accessed,
request). reused on subsequent access, until it is closed when the request context ends.
Here is a simple example of how you can use SQLite 3 with Flask:: Here is a simple example of how you can use SQLite 3 with Flask::

View file

@ -8,6 +8,21 @@ roundtrip to the filesystem?
The answer is by using generators and direct responses. The answer is by using generators and direct responses.
HTTP Response Behavior
----------------------
**Headers cannot be changed after the streaming response starts.**
When using streaming, it's important to be aware of the order than an HTTP
response is sent. All headers must be sent first, then the body. More headers
cannot be sent after the body has begun. Therefore, you must make sure all
headers are set before starting the response, outside the generator.
In particular, if the generator will access ``session``, be sure to do so in the
view as well so that the ``Vary: cookie`` header will be set. Do not modify the
session in the generator, as the ``Set-Cookie`` header will already be sent.
Basic Usage Basic Usage
----------- -----------
@ -29,7 +44,7 @@ debug environments with profilers and other things you might have enabled.
Streaming from Templates Streaming from Templates
------------------------ ------------------------
The Jinja2 template engine supports rendering a template piece by The Jinja template engine supports rendering a template piece by
piece, returning an iterator of strings. Flask provides the piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string` :func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use. functions to make this easier to use.
@ -49,13 +64,13 @@ the template.
Streaming with Context Streaming with Context
---------------------- ----------------------
The :data:`~flask.request` will not be active while the generator is The :data:`.request` proxy will not be active while the generator is
running, because the view has already returned at that point. If you try running, because the app has already returned control to the WSGI server at that
to access ``request``, you'll get a ``RuntimeError``. point. If you try to access ``request``, you'll get a ``RuntimeError``.
If your generator function relies on data in ``request``, use the If your generator function relies on data in ``request``, use the
:func:`~flask.stream_with_context` wrapper. This will keep the request :func:`.stream_with_context` wrapper. This will keep the request context active
context active during the generator. during the generator.
.. code-block:: python .. code-block:: python

View file

@ -99,7 +99,7 @@ WTForm's field function, which renders the field for us. The keyword
arguments will be inserted as HTML attributes. So, for example, you can arguments will be inserted as HTML attributes. So, for example, you can
call ``render_field(form.username, class='username')`` to add a class to call ``render_field(form.username, class='username')`` to add a class to
the input element. Note that WTForms returns standard Python strings, the input element. Note that WTForms returns standard Python strings,
so we have to tell Jinja2 that this data is already HTML-escaped with so we have to tell Jinja that this data is already HTML-escaped with
the ``|safe`` filter. the ``|safe`` filter.
Here is the :file:`register.html` template for the function we used above, which Here is the :file:`register.html` template for the function we used above, which

View file

@ -139,18 +139,16 @@ how you're using untrusted data.
.. code-block:: python .. code-block:: python
from flask import request
from markupsafe import escape from markupsafe import escape
@app.route("/<name>") @app.route("/hello")
def hello(name): def hello():
name = request.args.get("name", "Flask")
return f"Hello, {escape(name)}!" return f"Hello, {escape(name)}!"
If a user managed to submit the name ``<script>alert("bad")</script>``, If a user submits ``/hello?name=<script>alert("bad")</script>``, escaping causes
escaping causes it to be rendered as text, rather than running the it to be rendered as text, rather than running the script in the user's browser.
script in the user's browser.
``<name>`` in the route captures a value from the URL and passes it to
the view function. These variable rules are explained below.
Routing Routing
@ -260,7 +258,7 @@ Why would you want to build URLs using the URL reversing function
For example, here we use the :meth:`~flask.Flask.test_request_context` method For example, here we use the :meth:`~flask.Flask.test_request_context` method
to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context`
tells Flask to behave as though it's handling a request even while we use a tells Flask to behave as though it's handling a request even while we use a
Python shell. See :ref:`context-locals`. Python shell. See :doc:`/appcontext`.
.. code-block:: python .. code-block:: python
@ -354,7 +352,7 @@ Rendering Templates
Generating HTML from within Python is not fun, and actually pretty Generating HTML from within Python is not fun, and actually pretty
cumbersome because you have to do the HTML escaping on your own to keep cumbersome because you have to do the HTML escaping on your own to keep
the application secure. Because of that Flask configures the `Jinja2 the application secure. Because of that Flask configures the `Jinja
<https://palletsprojects.com/p/jinja/>`_ template engine for you automatically. <https://palletsprojects.com/p/jinja/>`_ template engine for you automatically.
Templates can be used to generate any type of text file. For web applications, you'll Templates can be used to generate any type of text file. For web applications, you'll
@ -394,8 +392,8 @@ package it's actually inside your package:
/templates /templates
/hello.html /hello.html
For templates you can use the full power of Jinja2 templates. Head over For templates you can use the full power of Jinja templates. Head over
to the official `Jinja2 Template Documentation to the official `Jinja Template Documentation
<https://jinja.palletsprojects.com/templates/>`_ for more information. <https://jinja.palletsprojects.com/templates/>`_ for more information.
Here is an example template: Here is an example template:
@ -451,105 +449,58 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:
Accessing Request Data Accessing Request Data
---------------------- ----------------------
For web applications it's crucial to react to the data a client sends to For web applications it's crucial to react to the data a client sends to the
the server. In Flask this information is provided by the global server. In Flask this information is provided by the global :data:`.request`
:class:`~flask.request` object. If you have some experience with Python object, which is an instance of :class:`.Request`. This object has many
you might be wondering how that object can be global and how Flask attributes and methods to work with the incoming request data, but here is a
manages to still be threadsafe. The answer is context locals: broad overview. First it needs to be imported.
.. code-block:: python
.. _context-locals:
Context Locals
``````````````
.. admonition:: Insider Information
If you want to understand how that works and how you can implement
tests with context locals, read this section, otherwise just skip it.
Certain objects in Flask are global objects, but not of the usual kind.
These objects are actually proxies to objects that are local to a specific
context. What a mouthful. But that is actually quite easy to understand.
Imagine the context being the handling thread. A request comes in and the
web server decides to spawn a new thread (or something else, the
underlying object is capable of dealing with concurrency systems other
than threads). When Flask starts its internal request handling it
figures out that the current thread is the active context and binds the
current application and the WSGI environments to that context (thread).
It does that in an intelligent way so that one application can invoke another
application without breaking.
So what does this mean to you? Basically you can completely ignore that
this is the case unless you are doing something like unit testing. You
will notice that code which depends on a request object will suddenly break
because there is no request object. The solution is creating a request
object yourself and binding it to the context. The easiest solution for
unit testing is to use the :meth:`~flask.Flask.test_request_context`
context manager. In combination with the ``with`` statement it will bind a
test request so that you can interact with it. Here is an example::
from flask import request from flask import request
with app.test_request_context('/hello', method='POST'): If you have some experience with Python you might be wondering how that object
# now you can do something with the request until the can be global when Flask handles multiple requests at a time. The answer is
# end of the with block, such as basic assertions: that :data:`.request` is actually a proxy, pointing at whatever request is
assert request.path == '/hello' currently being handled by a given worker, which is managed internally by Flask
assert request.method == 'POST' and Python. See :doc:`/appcontext` for much more information.
The other possibility is passing a whole WSGI environment to the The current request method is available in the :attr:`~.Request.method`
:meth:`~flask.Flask.request_context` method:: attribute. To access form data (data transmitted in a ``POST`` or ``PUT``
request), use the :attr:`~flask.Request.form` attribute, which behaves like a
dict.
with app.request_context(environ): .. code-block:: python
assert request.method == 'POST'
The Request Object @app.route("/login", methods=["GET", "POST"])
``````````````````
The request object is documented in the API section and we will not cover
it here in detail (see :class:`~flask.Request`). Here is a broad overview of
some of the most common operations. First of all you have to import it from
the ``flask`` module::
from flask import request
The current request method is available by using the
:attr:`~flask.Request.method` attribute. To access form data (data
transmitted in a ``POST`` or ``PUT`` request) you can use the
:attr:`~flask.Request.form` attribute. Here is a full example of the two
attributes mentioned above::
@app.route('/login', methods=['POST', 'GET'])
def login(): def login():
error = None error = None
if request.method == 'POST':
if valid_login(request.form['username'], if request.method == "POST":
request.form['password']): if valid_login(request.form["username"], request.form["password"]):
return log_the_user_in(request.form['username']) return store_login(request.form["username"])
else: else:
error = 'Invalid username/password' error = "Invalid username or password"
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)
What happens if the key does not exist in the ``form`` attribute? In that # Executed if the request method was GET or the credentials were invalid.
case a special :exc:`KeyError` is raised. You can catch it like a return render_template("login.html", error=error)
standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request
error page is shown instead. So for many situations you don't have to
deal with that problem.
To access parameters submitted in the URL (``?key=value``) you can use the If the key does not exist in ``form``, a special :exc:`KeyError` is raised. You
:attr:`~flask.Request.args` attribute:: can catch it like a normal ``KeyError``, otherwise it will return a HTTP 400
Bad Request error page. You can also use the
:meth:`~werkzeug.datastructures.MultiDict.get` method to get a default
instead of an error.
To access parameters submitted in the URL (``?key=value``), use the
:attr:`~.Request.args` attribute. Key errors behave the same as ``form``,
returning a 400 response if not caught.
.. code-block:: python
searchword = request.args.get('key', '') searchword = request.args.get('key', '')
We recommend accessing URL parameters with `get` or by catching the For a full list of methods and attributes of the request object, see the
:exc:`KeyError` because users might change the URL and presenting them a 400 :class:`~.Request` documentation.
bad request page in that case is not user friendly.
For a full list of methods and attributes of the request object, head over
to the :class:`~flask.Request` documentation.
File Uploads File Uploads

View file

@ -1,243 +1,6 @@
.. currentmodule:: flask :orphan:
The Request Context The Request Context
=================== ===================
The request context keeps track of the request-level data during a Obsolete, see :doc:`/appcontext` instead.
request. Rather than passing the request object to each function that
runs during a request, the :data:`request` and :data:`session` proxies
are accessed instead.
This is similar to :doc:`/appcontext`, which keeps track of the
application-level data independent of a request. A corresponding
application context is pushed when a request context is pushed.
Purpose of the Context
----------------------
When the :class:`Flask` application handles a request, it creates a
:class:`Request` object based on the environment it received from the
WSGI server. Because a *worker* (thread, process, or coroutine depending
on the server) handles only one request at a time, the request data can
be considered global to that worker during that request. Flask uses the
term *context local* for this.
Flask automatically *pushes* a request context when handling a request.
View functions, error handlers, and other functions that run during a
request will have access to the :data:`request` proxy, which points to
the request object for the current request.
Lifetime of the Context
-----------------------
When a Flask application begins handling a request, it pushes a request
context, which also pushes an :doc:`app context </appcontext>`. When the
request ends it pops the request context then the application context.
The context is unique to each thread (or other worker type).
:data:`request` cannot be passed to another thread, the other thread has
a different context space and will not know about the request the parent
thread was pointing to.
Context locals are implemented using Python's :mod:`contextvars` and
Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the
lifetime of context vars automatically, and local proxy wraps that
low-level interface to make the data easier to work with.
Manually Push a Context
-----------------------
If you try to access :data:`request`, or anything that uses it, outside
a request context, you'll get this error message:
.. code-block:: pytb
RuntimeError: Working outside of request context.
This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.
This should typically only happen when testing code that expects an
active request. One option is to use the
:meth:`test client <Flask.test_client>` to simulate a full request. Or
you can use :meth:`~Flask.test_request_context` in a ``with`` block, and
everything that runs in the block will have access to :data:`request`,
populated with your test data. ::
def generate_report(year):
format = request.args.get("format")
...
with app.test_request_context(
"/make_report/2017", query_string={"format": "short"}
):
generate_report()
If you see that error somewhere else in your code not related to
testing, it most likely indicates that you should move that code into a
view function.
For information on how to use the request context from the interactive
Python shell, see :doc:`/shell`.
How the Context Works
---------------------
The :meth:`Flask.wsgi_app` method is called to handle each request. It
manages the contexts during the request. Internally, the request and
application contexts work like stacks. When contexts are pushed, the
proxies that depend on them are available and point at information from
the top item.
When the request starts, a :class:`~ctx.RequestContext` is created and
pushed, which creates and pushes an :class:`~ctx.AppContext` first if
a context for that application is not already the top context. While
these contexts are pushed, the :data:`current_app`, :data:`g`,
:data:`request`, and :data:`session` proxies are available to the
original thread handling the request.
Other contexts may be pushed to change the proxies during a request.
While this is not a common pattern, it can be used in advanced
applications to, for example, do internal redirects or chain different
applications together.
After the request is dispatched and a response is generated and sent,
the request context is popped, which then pops the application context.
Immediately before they are popped, the :meth:`~Flask.teardown_request`
and :meth:`~Flask.teardown_appcontext` functions are executed. These
execute even if an unhandled exception occurred during dispatch.
.. _callbacks-and-errors:
Callbacks and Errors
--------------------
Flask dispatches a request in multiple stages which can affect the
request, response, and how errors are handled. The contexts are active
during all of these stages.
A :class:`Blueprint` can add handlers for these events that are specific
to the blueprint. The handlers for a blueprint will run if the blueprint
owns the route that matches the request.
#. Before each request, :meth:`~Flask.before_request` functions are
called. If one of these functions return a value, the other
functions are skipped. The return value is treated as the response
and the view function is not called.
#. If the :meth:`~Flask.before_request` functions did not return a
response, the view function for the matched route is called and
returns a response.
#. The return value of the view is converted into an actual response
object and passed to the :meth:`~Flask.after_request`
functions. Each function returns a modified or new response object.
#. After the response is returned, the contexts are popped, which calls
the :meth:`~Flask.teardown_request` and
:meth:`~Flask.teardown_appcontext` functions. These functions are
called even if an unhandled exception was raised at any point above.
If an exception is raised before the teardown functions, Flask tries to
match it with an :meth:`~Flask.errorhandler` function to handle the
exception and return a response. If no error handler is found, or the
handler itself raises an exception, Flask returns a generic
``500 Internal Server Error`` response. The teardown functions are still
called, and are passed the exception object.
If debug mode is enabled, unhandled exceptions are not converted to a
``500`` response and instead are propagated to the WSGI server. This
allows the development server to present the interactive debugger with
the traceback.
Teardown Callbacks
~~~~~~~~~~~~~~~~~~
The teardown callbacks are independent of the request dispatch, and are
instead called by the contexts when they are popped. The functions are
called even if there is an unhandled exception during dispatch, and for
manually pushed contexts. This means there is no guarantee that any
other parts of the request dispatch have run first. Be sure to write
these functions in a way that does not depend on other callbacks and
will not fail.
During testing, it can be useful to defer popping the contexts after the
request ends, so that their data can be accessed in the test function.
Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the
contexts until the ``with`` block exits.
.. code-block:: python
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def hello():
print('during view')
return 'Hello, World!'
@app.teardown_request
def show_teardown(exception):
print('after with block')
with app.test_request_context():
print('during with block')
# teardown functions are called after the context with block exits
with app.test_client() as client:
client.get('/')
# the contexts are not popped even though the request ended
print(request.path)
# the contexts are popped and teardown functions are called after
# the client with block exits
Signals
~~~~~~~
The following signals are sent:
#. :data:`request_started` is sent before the :meth:`~Flask.before_request` functions
are called.
#. :data:`request_finished` is sent after the :meth:`~Flask.after_request` functions
are called.
#. :data:`got_request_exception` is sent when an exception begins to be handled, but
before an :meth:`~Flask.errorhandler` is looked up or called.
#. :data:`request_tearing_down` is sent after the :meth:`~Flask.teardown_request`
functions are called.
.. _notes-on-proxies:
Notes On Proxies
----------------
Some of the objects provided by Flask are proxies to other objects. The
proxies are accessed in the same way for each worker thread, but
point to the unique object bound to each worker behind the scenes as
described on this page.
Most of the time you don't have to care about that, but there are some
exceptions where it is good to know that this object is actually a proxy:
- The proxy objects cannot fake their type as the actual object types.
If you want to perform instance checks, you have to do that on the
object being proxied.
- The reference to the proxied object is needed in some situations,
such as sending :doc:`signals` or passing data to a background
thread.
If you need to access the underlying object that is proxied, use the
:meth:`~werkzeug.local.LocalProxy._get_current_object` method::
app = current_app._get_current_object()
my_signal.send(app)

View file

@ -77,7 +77,7 @@ following example shows that process id 6847 is using port 5000.
macOS Monterey and later automatically starts a service that uses port macOS Monterey and later automatically starts a service that uses port
5000. You can choose to disable this service instead of using a different port by 5000. You can choose to disable this service instead of using a different port by
searching for "AirPlay Receiver" in System Preferences and toggling it off. searching for "AirPlay Receiver" in System Settings and toggling it off.
Deferred Errors on Reload Deferred Errors on Reload

View file

@ -1,56 +1,37 @@
Working with the Shell Working with the Shell
====================== ======================
.. versionadded:: 0.3 One of the reasons everybody loves Python is the interactive shell. It allows
you to play around with code in real time and immediately get results back.
Flask provides the ``flask shell`` CLI command to start an interactive Python
shell with some setup done to make working with the Flask app easier.
One of the reasons everybody loves Python is the interactive shell. It .. code-block:: text
basically allows you to execute Python commands in real time and
immediately get results back. Flask itself does not come with an
interactive shell, because it does not require any specific setup upfront,
just import your application and start playing around.
There are however some handy helpers to make playing around in the shell a $ flask shell
more pleasant experience. The main issue with interactive console
sessions is that you're not triggering a request like a browser does which
means that :data:`~flask.g`, :data:`~flask.request` and others are not
available. But the code you want to test might depend on them, so what
can you do?
This is where some helper functions come in handy. Keep in mind however
that these functions are not only there for interactive shell usage, but
also for unit testing and other situations that require a faked request
context.
Generally it's recommended that you read :doc:`reqcontext` first.
Command Line Interface
----------------------
Starting with Flask 0.11 the recommended way to work with the shell is the
``flask shell`` command which does a lot of this automatically for you.
For instance the shell is automatically initialized with a loaded
application context.
For more information see :doc:`/cli`.
Creating a Request Context Creating a Request Context
-------------------------- --------------------------
``flask shell`` pushes an app context automatically, so :data:`.current_app` and
:data:`.g` are already available. However, there is no HTTP request being
handled in the shell, so :data:`.request` and :data:`.session` are not yet
available.
The easiest way to create a proper request context from the shell is by The easiest way to create a proper request context from the shell is by
using the :attr:`~flask.Flask.test_request_context` method which creates using the :attr:`~flask.Flask.test_request_context` method which creates
us a :class:`~flask.ctx.RequestContext`: us a :class:`~flask.ctx.RequestContext`:
>>> ctx = app.test_request_context() >>> ctx = app.test_request_context()
Normally you would use the ``with`` statement to make this request object Normally you would use the ``with`` statement to make this context active, but
active, but in the shell it's easier to use the in the shell it's easier to call :meth:`~.RequestContext.push` and
:meth:`~flask.ctx.RequestContext.push` and :meth:`~.RequestContext.pop` manually:
:meth:`~flask.ctx.RequestContext.pop` methods by hand:
>>> ctx.push() >>> ctx.push()
From that point onwards you can work with the request object until you From that point onwards you can work with the request object until you call
call `pop`: ``pop``:
>>> ctx.pop() >>> ctx.pop()

View file

@ -144,11 +144,10 @@ function, you can pass ``current_app._get_current_object()`` as sender.
Signals and Flask's Request Context Signals and Flask's Request Context
----------------------------------- -----------------------------------
Signals fully support :doc:`reqcontext` when receiving signals. Context-local proxies are available between :data:`~flask.request_started` and
Context-local variables are consistently available between :data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others
:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can as needed. Note the limitations described in :ref:`signals-sending` and the
rely on :class:`flask.g` and others as needed. Note the limitations described :data:`~flask.request_tearing_down` signal.
in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal.
Decorator Based Signal Subscriptions Decorator Based Signal Subscriptions

View file

@ -1,21 +1,21 @@
Templates Templates
========= =========
Flask leverages Jinja2 as its template engine. You are obviously free to use Flask leverages Jinja as its template engine. You are obviously free to use
a different template engine, but you still have to install Jinja2 to run a different template engine, but you still have to install Jinja to run
Flask itself. This requirement is necessary to enable rich extensions. Flask itself. This requirement is necessary to enable rich extensions.
An extension can depend on Jinja2 being present. An extension can depend on Jinja being present.
This section only gives a very quick introduction into how Jinja2 This section only gives a very quick introduction into how Jinja
is integrated into Flask. If you want information on the template is integrated into Flask. If you want information on the template
engine's syntax itself, head over to the official `Jinja2 Template engine's syntax itself, head over to the official `Jinja Template
Documentation <https://jinja.palletsprojects.com/templates/>`_ for Documentation <https://jinja.palletsprojects.com/templates/>`_ for
more information. more information.
Jinja Setup Jinja Setup
----------- -----------
Unless customized, Jinja2 is configured by Flask as follows: Unless customized, Jinja is configured by Flask as follows:
- autoescaping is enabled for all templates ending in ``.html``, - autoescaping is enabled for all templates ending in ``.html``,
``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using
@ -25,13 +25,13 @@ Unless customized, Jinja2 is configured by Flask as follows:
- a template has the ability to opt in/out autoescaping with the - a template has the ability to opt in/out autoescaping with the
``{% autoescape %}`` tag. ``{% autoescape %}`` tag.
- Flask inserts a couple of global functions and helpers into the - Flask inserts a couple of global functions and helpers into the
Jinja2 context, additionally to the values that are present by Jinja context, additionally to the values that are present by
default. default.
Standard Context Standard Context
---------------- ----------------
The following global variables are available within Jinja2 templates The following global variables are available within Jinja templates
by default: by default:
.. data:: config .. data:: config
@ -137,32 +137,58 @@ using in this block.
.. _registering-filters: .. _registering-filters:
Registering Filters Registering Filters, Tests, and Globals
------------------- ---------------------------------------
If you want to register your own filters in Jinja2 you have two ways to do The Flask app and blueprints provide decorators and methods to register your own
that. You can either put them by hand into the filters, tests, and global functions for use in Jinja templates. They all follow
:attr:`~flask.Flask.jinja_env` of the application or use the the same pattern, so the following examples only discuss filters.
:meth:`~flask.Flask.template_filter` decorator.
The two following examples work the same and both reverse an object:: Decorate a function with :meth:`~.Flask.template_filter` to register it as a
template filter.
@app.template_filter('reverse') .. code-block:: python
def reverse_filter(s):
return s[::-1]
def reverse_filter(s): @app.template_filter
return s[::-1] def reverse(s):
app.jinja_env.filters['reverse'] = reverse_filter return reversed(s)
In case of the decorator the argument is optional if you want to use the .. code-block:: jinja
function name as name of the filter. Once registered, you can use the filter
in your templates in the same way as Jinja2's builtin filters, for example if
you have a Python list in context called `mylist`::
{% for x in mylist | reverse %} {% for item in data | reverse %}
{% endfor %} {% endfor %}
By default it will use the name of the function as the name of the filter, but
that can be changed by passing a name to the decorator.
.. code-block:: python
@app.template_filter("reverse")
def reverse_filter(s):
return reversed(s)
A filter can be registered separately using :meth:`~.Flask.add_template_filter`.
The name is optional and will use the function name if not given.
.. code-block:: python
def reverse_filter(s):
return reversed(s)
app.add_template_filter(reverse_filter, "reverse")
For template tests, use the :meth:`~.Flask.template_test` decorator or
:meth:`~.Flask.add_template_test` method. For template global functions, use the
:meth:`~.Flask.template_global` decorator or :meth:`~.Flask.add_template_global`
method.
The same methods also exist on :class:`.Blueprint`, prefixed with ``app_`` to
indicate that the registered functions will be available to all templates, not
only when rendering from within the blueprint.
The Jinja environment is also available as :attr:`~.Flask.jinja_env`. It may be
modified directly, as you would when using Jinja outside Flask.
Context Processors Context Processors
------------------ ------------------
@ -211,7 +237,7 @@ strings. This can be used for streaming HTML in chunks to speed up
initial page load, or to save memory when rendering a very large initial page load, or to save memory when rendering a very large
template. template.
The Jinja2 template engine supports rendering a template piece The Jinja template engine supports rendering a template piece
by piece, returning an iterator of strings. Flask provides the by piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string` :func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use. functions to make this easier to use.
@ -225,5 +251,11 @@ functions to make this easier to use.
return stream_template("timeline.html") return stream_template("timeline.html")
These functions automatically apply the These functions automatically apply the
:func:`~flask.stream_with_context` wrapper if a request is active, so :func:`~flask.stream_with_context` wrapper if a request is active, so that
that it remains available in the template. :data:`.request`, :data:`.session`, and :data:`.g` remain available in the
template.
More headers cannot be sent after the body has begun. Therefore, you must
make sure all headers are set before starting the response. In particular,
if the template will access ``session``, be sure to do so in the view as
well so that the ``Vary: cookie`` header will be set.

View file

@ -275,11 +275,10 @@ command from the command line.
Tests that depend on an Active Context Tests that depend on an Active Context
-------------------------------------- --------------------------------------
You may have functions that are called from views or commands, that You may have functions that are called from views or commands, that expect an
expect an active :doc:`application context </appcontext>` or active :doc:`app context </appcontext>` because they access :data:`.request`,
:doc:`request context </reqcontext>` because they access ``request``, :data:`.session`, :data:`.g`, or :data:`.current_app`. Rather than testing them by
``session``, or ``current_app``. Rather than testing them by making a making a request or invoking the command, you can create and activate a context
request or invoking the command, you can create and activate a context
directly. directly.
Use ``with app.app_context()`` to push an application context. For Use ``with app.app_context()`` to push an application context. For

View file

@ -305,7 +305,7 @@ The pattern ``{{ request.form['title'] or post['title'] }}`` is used to
choose what data appears in the form. When the form hasn't been choose what data appears in the form. When the form hasn't been
submitted, the original ``post`` data appears, but if invalid form data submitted, the original ``post`` data appears, but if invalid form data
was posted you want to display that so the user can fix the error, so was posted you want to display that so the user can fix the error, so
``request.form`` is used instead. :data:`request` is another variable ``request.form`` is used instead. :data:`.request` is another variable
that's automatically available in templates. that's automatically available in templates.

View file

@ -60,17 +60,17 @@ response is sent.
if db is not None: if db is not None:
db.close() db.close()
:data:`g` is a special object that is unique for each request. It is :data:`.g` is a special object that is unique for each request. It is
used to store data that might be accessed by multiple functions during used to store data that might be accessed by multiple functions during
the request. The connection is stored and reused instead of creating a the request. The connection is stored and reused instead of creating a
new connection if ``get_db`` is called a second time in the same new connection if ``get_db`` is called a second time in the same
request. request.
:data:`current_app` is another special object that points to the Flask :data:`.current_app` is another special object that points to the Flask
application handling the request. Since you used an application factory, application handling the request. Since you used an application factory,
there is no application object when writing the rest of your code. there is no application object when writing the rest of your code.
``get_db`` will be called when the application has been created and is ``get_db`` will be called when the application has been created and is
handling a request, so :data:`current_app` can be used. handling a request, so :data:`.current_app` can be used.
:func:`sqlite3.connect` establishes a connection to the file pointed at :func:`sqlite3.connect` establishes a connection to the file pointed at
by the ``DATABASE`` configuration key. This file doesn't have to exist by the ``DATABASE`` configuration key. This file doesn't have to exist

View file

@ -56,10 +56,7 @@ directory should be treated as a package.
app.config.from_mapping(test_config) app.config.from_mapping(test_config)
# ensure the instance folder exists # ensure the instance folder exists
try: os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(app.instance_path)
except OSError:
pass
# a simple page that says hello # a simple page that says hello
@app.route('/hello') @app.route('/hello')

View file

@ -81,8 +81,7 @@ By the end, your project layout will look like this:
│ ├── test_auth.py │ ├── test_auth.py
│ └── test_blog.py │ └── test_blog.py
├── .venv/ ├── .venv/
├── pyproject.toml └── pyproject.toml
└── MANIFEST.in
If you're using version control, the following files that are generated If you're using version control, the following files that are generated
while running your project should be ignored. There may be other files while running your project should be ignored. There may be other files
@ -103,8 +102,4 @@ write. For example, with git:
.coverage .coverage
htmlcov/ htmlcov/
dist/
build/
*.egg-info/
Continue to :doc:`factory`. Continue to :doc:`factory`.

View file

@ -71,7 +71,7 @@ specific sections.
{% block content %}{% endblock %} {% block content %}{% endblock %}
</section> </section>
:data:`g` is automatically available in templates. Based on if :data:`.g` is automatically available in templates. Based on if
``g.user`` is set (from ``load_logged_in_user``), either the username ``g.user`` is set (from ``load_logged_in_user``), either the username
and a log out link are displayed, or links to register and log in and a log out link are displayed, or links to register and log in
are displayed. :func:`url_for` is also automatically available, and is are displayed. :func:`url_for` is also automatically available, and is

View file

@ -311,7 +311,7 @@ input and error messages without writing the same code three times.
The tests for the ``login`` view are very similar to those for The tests for the ``login`` view are very similar to those for
``register``. Rather than testing the data in the database, ``register``. Rather than testing the data in the database,
:data:`session` should have ``user_id`` set after logging in. :data:`.session` should have ``user_id`` set after logging in.
.. code-block:: python .. code-block:: python
:caption: ``tests/test_auth.py`` :caption: ``tests/test_auth.py``
@ -336,10 +336,10 @@ The tests for the ``login`` view are very similar to those for
assert message in response.data assert message in response.data
Using ``client`` in a ``with`` block allows accessing context variables Using ``client`` in a ``with`` block allows accessing context variables
such as :data:`session` after the response is returned. Normally, such as :data:`.session` after the response is returned. Normally,
accessing ``session`` outside of a request would raise an error. accessing ``session`` outside of a request would raise an error.
Testing ``logout`` is the opposite of ``login``. :data:`session` should Testing ``logout`` is the opposite of ``login``. :data:`.session` should
not contain ``user_id`` after logging out. not contain ``user_id`` after logging out.
.. code-block:: python .. code-block:: python

View file

@ -208,13 +208,13 @@ There are a few differences from the ``register`` view:
password in the same way as the stored hash and securely compares password in the same way as the stored hash and securely compares
them. If they match, the password is valid. them. If they match, the password is valid.
#. :data:`session` is a :class:`dict` that stores data across requests. #. :data:`.session` is a :class:`dict` that stores data across requests.
When validation succeeds, the user's ``id`` is stored in a new When validation succeeds, the user's ``id`` is stored in a new
session. The data is stored in a *cookie* that is sent to the session. The data is stored in a *cookie* that is sent to the
browser, and the browser then sends it back with subsequent requests. browser, and the browser then sends it back with subsequent requests.
Flask securely *signs* the data so that it can't be tampered with. Flask securely *signs* the data so that it can't be tampered with.
Now that the user's ``id`` is stored in the :data:`session`, it will be Now that the user's ``id`` is stored in the :data:`.session`, it will be
available on subsequent requests. At the beginning of each request, if available on subsequent requests. At the beginning of each request, if
a user is logged in their information should be loaded and made a user is logged in their information should be loaded and made
available to other views. available to other views.
@ -236,7 +236,7 @@ available to other views.
:meth:`bp.before_app_request() <Blueprint.before_app_request>` registers :meth:`bp.before_app_request() <Blueprint.before_app_request>` registers
a function that runs before the view function, no matter what URL is a function that runs before the view function, no matter what URL is
requested. ``load_logged_in_user`` checks if a user id is stored in the requested. ``load_logged_in_user`` checks if a user id is stored in the
:data:`session` and gets that user's data from the database, storing it :data:`.session` and gets that user's data from the database, storing it
on :data:`g.user <g>`, which lasts for the length of the request. If on :data:`g.user <g>`, which lasts for the length of the request. If
there is no user id, or if the id doesn't exist, ``g.user`` will be there is no user id, or if the id doesn't exist, ``g.user`` will be
``None``. ``None``.
@ -245,7 +245,7 @@ there is no user id, or if the id doesn't exist, ``g.user`` will be
Logout Logout
------ ------
To log out, you need to remove the user id from the :data:`session`. To log out, you need to remove the user id from the :data:`.session`.
Then ``load_logged_in_user`` won't load a user on subsequent requests. Then ``load_logged_in_user`` won't load a user on subsequent requests.
.. code-block:: python .. code-block:: python

View file

@ -51,12 +51,12 @@ tags. For more information on that have a look at the Wikipedia article
on `Cross-Site Scripting on `Cross-Site Scripting
<https://en.wikipedia.org/wiki/Cross-site_scripting>`_. <https://en.wikipedia.org/wiki/Cross-site_scripting>`_.
Flask configures Jinja2 to automatically escape all values unless Flask configures Jinja to automatically escape all values unless
explicitly told otherwise. This should rule out all XSS problems caused explicitly told otherwise. This should rule out all XSS problems caused
in templates, but there are still other places where you have to be in templates, but there are still other places where you have to be
careful: careful:
- generating HTML without the help of Jinja2 - generating HTML without the help of Jinja
- calling :class:`~markupsafe.Markup` on data submitted by users - calling :class:`~markupsafe.Markup` on data submitted by users
- sending out HTML from uploaded files, never do that, use the - sending out HTML from uploaded files, never do that, use the
``Content-Disposition: attachment`` header to prevent that problem. ``Content-Disposition: attachment`` header to prevent that problem.
@ -65,7 +65,7 @@ careful:
trick a browser to execute HTML. trick a browser to execute HTML.
Another thing that is very important are unquoted attributes. While Another thing that is very important are unquoted attributes. While
Jinja2 can protect you from XSS issues by escaping HTML, there is one Jinja can protect you from XSS issues by escaping HTML, there is one
thing it cannot protect you from: XSS by attribute injection. To counter thing it cannot protect you from: XSS by attribute injection. To counter
this possible attack vector, be sure to always quote your attributes with this possible attack vector, be sure to always quote your attributes with
either double or single quotes when using Jinja expressions in them: either double or single quotes when using Jinja expressions in them:
@ -158,7 +158,7 @@ recommend reviewing each of the headers below for use in your application.
The `Flask-Talisman`_ extension can be used to manage HTTPS and the security The `Flask-Talisman`_ extension can be used to manage HTTPS and the security
headers for you. headers for you.
.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman .. _Flask-Talisman: https://github.com/wntrblm/flask-talisman
HTTP Strict Transport Security (HSTS) HTTP Strict Transport Security (HSTS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -269,17 +269,25 @@ values (or any values that need secure signatures).
.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute .. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute
HTTP Public Key Pinning (HPKP) Host Header Validation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ----------------------
This tells the browser to authenticate with the server using only the specific The ``Host`` header is used by the client to indicate what host name the request
certificate key to prevent MITM attacks. was made to. This is used, for example, by ``url_for(..., _external=True)`` to
generate full URLs, for use in email or other messages outside the browser
window.
.. warning:: By default the app doesn't know what host(s) it is allowed to be accessed
Be careful when enabling this, as it is very difficult to undo if you set up through, and assumes any host is valid. Although browsers do not allow setting
or upgrade your key incorrectly. the ``Host`` header, requests made by attackers in other scenarios could set
the ``Host`` header to a value they want.
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning When deploying your application, set :data:`TRUSTED_HOSTS` to restrict what
values the ``Host`` header may be.
The ``Host`` header may be modified by proxies in between the client and your
application. See :doc:`deploying/proxy_fix` to tell your app which proxy values
to trust.
Copy/Paste to Terminal Copy/Paste to Terminal

View file

@ -21,10 +21,7 @@ def create_app(test_config=None):
app.config.update(test_config) app.config.update(test_config)
# ensure the instance folder exists # ensure the instance folder exists
try: os.makedirs(app.instance_path, exist_ok=True)
os.makedirs(app.instance_path)
except OSError:
pass
@app.route("/hello") @app.route("/hello")
def hello(): def hello():

View file

@ -3,14 +3,14 @@ name = "Flask"
version = "3.2.0.dev" version = "3.2.0.dev"
description = "A simple framework for building complex web applications." description = "A simple framework for building complex web applications."
readme = "README.md" readme = "README.md"
license = {file = "LICENSE.txt"} license = "BSD-3-Clause"
license-files = ["LICENSE.txt"]
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Environment :: Web Environment", "Environment :: Web Environment",
"Framework :: Flask", "Framework :: Flask",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
@ -19,32 +19,70 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Application Frameworks",
"Typing :: Typed", "Typing :: Typed",
] ]
requires-python = ">=3.9" requires-python = ">=3.10"
dependencies = [ dependencies = [
"Werkzeug>=3.1", "blinker>=1.9.0",
"Jinja2>=3.1.2",
"itsdangerous>=2.2",
"click>=8.1.3", "click>=8.1.3",
"blinker>=1.9", "itsdangerous>=2.2.0",
"importlib-metadata>=3.6; python_version < '3.10'", "jinja2>=3.1.2",
"markupsafe>=2.1.1",
"werkzeug>=3.1.0",
] ]
[project.urls]
Donate = "https://palletsprojects.com/donate"
Documentation = "https://flask.palletsprojects.com/"
Changes = "https://flask.palletsprojects.com/changes/"
Source = "https://github.com/pallets/flask/"
Chat = "https://discord.gg/pallets"
[project.optional-dependencies] [project.optional-dependencies]
async = ["asgiref>=3.2"] async = ["asgiref>=3.2"]
dotenv = ["python-dotenv"] dotenv = ["python-dotenv"]
[dependency-groups]
dev = [
"ruff",
"tox",
"tox-uv",
]
docs = [
"pallets-sphinx-themes",
"sphinx<9",
"sphinx-tabs",
"sphinxcontrib-log-cabinet",
]
docs-auto = [
"sphinx-autobuild",
]
gha-update = [
"gha-update ; python_full_version >= '3.12'",
]
pre-commit = [
"pre-commit",
"pre-commit-uv",
]
tests = [
"asgiref",
"pytest",
"python-dotenv",
]
typing = [
"asgiref",
"cryptography",
"mypy",
"pyright",
"pytest",
"python-dotenv",
"types-contextvars",
"types-dataclasses",
]
[project.urls]
Donate = "https://palletsprojects.com/donate"
Documentation = "https://flask.palletsprojects.com/"
Changes = "https://flask.palletsprojects.com/page/changes/"
Source = "https://github.com/pallets/flask/"
Chat = "https://discord.gg/pallets"
[project.scripts] [project.scripts]
flask = "flask.cli:main" flask = "flask.cli:main"
[build-system] [build-system]
requires = ["flit_core<4"] requires = ["flit_core>=3.11,<4"]
build-backend = "flit_core.buildapi" build-backend = "flit_core.buildapi"
[tool.flit.module] [tool.flit.module]
@ -54,16 +92,17 @@ name = "flask"
include = [ include = [
"docs/", "docs/",
"examples/", "examples/",
"requirements/",
"tests/", "tests/",
"CHANGES.rst", "CHANGES.rst",
"CONTRIBUTING.rst", "uv.lock"
"tox.ini",
] ]
exclude = [ exclude = [
"docs/_build/", "docs/_build/",
] ]
[tool.uv]
default-groups = ["dev", "pre-commit", "tests", "typing"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
filterwarnings = [ filterwarnings = [
@ -77,9 +116,16 @@ source = ["flask", "tests"]
[tool.coverage.paths] [tool.coverage.paths]
source = ["src", "*/site-packages"] source = ["src", "*/site-packages"]
[tool.coverage.report]
exclude_also = [
"if t.TYPE_CHECKING",
"raise NotImplementedError",
": \\.{3}",
]
[tool.mypy] [tool.mypy]
python_version = "3.9" python_version = "3.10"
files = ["src/flask", "tests/type_check"] files = ["src", "tests/type_check"]
show_error_codes = true show_error_codes = true
pretty = true pretty = true
strict = true strict = true
@ -94,8 +140,8 @@ module = [
ignore_missing_imports = true ignore_missing_imports = true
[tool.pyright] [tool.pyright]
pythonVersion = "3.9" pythonVersion = "3.10"
include = ["src/flask", "tests/type_check"] include = ["src", "tests/type_check"]
typeCheckingMode = "basic" typeCheckingMode = "basic"
[tool.ruff] [tool.ruff]
@ -118,7 +164,114 @@ select = [
force-single-line = true force-single-line = true
order-by-type = false order-by-type = false
[tool.gha-update] [tool.codespell]
tag-only = [ ignore-words-list = "te"
"slsa-framework/slsa-github-generator",
[tool.tox]
env_list = [
"py3.14", "py3.14t",
"py3.13", "py3.12", "py3.11", "py3.10",
"pypy3.11",
"tests-min", "tests-dev",
"style",
"typing",
"docs",
] ]
[tool.tox.env_run_base]
description = "pytest on latest dependency versions"
runner = "uv-venv-lock-runner"
package = "wheel"
wheel_build_env = ".pkg"
constrain_package_deps = true
use_frozen_constraints = true
dependency_groups = ["tests"]
env_tmp_dir = "{toxworkdir}/tmp/{envname}"
commands = [[
"pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}",
{replace = "posargs", default = [], extend = true},
]]
[tool.tox.env.tests-min]
description = "pytest on minimum dependency versions"
base_python = ["3.14"]
commands = [
[
"uv", "pip", "install",
"blinker==1.9.0",
"click==8.1.3",
"itsdangerous==2.2.0",
"jinja2==3.1.2",
"markupsafe==2.1.1",
"werkzeug==3.1.0",
],
[
"pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}",
{replace = "posargs", default = [], extend = true},
],
]
[tool.tox.env.tests-dev]
description = "pytest on development dependency versions (git main branch)"
base_python = ["3.10"]
commands = [
[
"uv", "pip", "install",
"https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz",
"https://github.com/pallets/click/archive/refs/heads/main.tar.gz",
"https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz",
"https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz",
"https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz",
"https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz",
],
[
"pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}",
{replace = "posargs", default = [], extend = true},
],
]
[tool.tox.env.style]
description = "run all pre-commit hooks on all files"
dependency_groups = ["pre-commit"]
skip_install = true
commands = [["pre-commit", "run", "--all-files"]]
[tool.tox.env.typing]
description = "run static type checkers"
dependency_groups = ["typing"]
commands = [
["mypy"],
["pyright"],
]
[tool.tox.env.docs]
description = "build docs"
dependency_groups = ["docs"]
commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]]
[tool.tox.env.docs-auto]
description = "continuously rebuild docs and start a local server"
dependency_groups = ["docs", "docs-auto"]
commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]]
[tool.tox.env.update-actions]
description = "update GitHub Actions pins"
labels = ["update"]
dependency_groups = ["gha-update"]
skip_install = true
commands = [["gha-update"]]
[tool.tox.env.update-pre_commit]
description = "update pre-commit pins"
labels = ["update"]
dependency_groups = ["pre-commit"]
skip_install = true
commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]]
[tool.tox.env.update-requirements]
description = "update uv lock"
labels = ["update"]
dependency_groups = []
no_default_groups = true
skip_install = true
commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]]

View file

@ -1 +0,0 @@
build

View file

@ -1,12 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile build.in
#
build==1.2.2.post1
# via -r build.in
packaging==24.2
# via build
pyproject-hooks==1.2.0
# via build

View file

@ -1,5 +0,0 @@
-r docs.txt
-r tests.txt
-r typing.txt
pre-commit
tox

View file

@ -1,198 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile dev.in
#
alabaster==1.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
asgiref==3.8.1
# via
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
babel==2.16.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
cachetools==5.5.0
# via tox
certifi==2024.8.30
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
cffi==1.17.1
# via
# -r /Users/david/Projects/flask/requirements/typing.txt
# cryptography
cfgv==3.4.0
# via pre-commit
chardet==5.2.0
# via tox
charset-normalizer==3.4.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
colorama==0.4.6
# via tox
cryptography==43.0.3
# via -r /Users/david/Projects/flask/requirements/typing.txt
distlib==0.3.9
# via virtualenv
docutils==0.21.2
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
# sphinx-tabs
filelock==3.16.1
# via
# tox
# virtualenv
identify==2.6.2
# via pre-commit
idna==3.10
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
imagesize==1.4.1
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
iniconfig==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pytest
jinja2==3.1.4
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
markupsafe==3.0.2
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# jinja2
mypy==1.13.0
# via -r /Users/david/Projects/flask/requirements/typing.txt
mypy-extensions==1.0.0
# via
# -r /Users/david/Projects/flask/requirements/typing.txt
# mypy
nodeenv==1.9.1
# via
# -r /Users/david/Projects/flask/requirements/typing.txt
# pre-commit
# pyright
packaging==24.2
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pallets-sphinx-themes
# pyproject-api
# pytest
# sphinx
# tox
pallets-sphinx-themes==2.3.0
# via -r /Users/david/Projects/flask/requirements/docs.txt
platformdirs==4.3.6
# via
# tox
# virtualenv
pluggy==1.5.0
# via
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
# pytest
# tox
pre-commit==4.0.1
# via -r dev.in
pycparser==2.22
# via
# -r /Users/david/Projects/flask/requirements/typing.txt
# cffi
pygments==2.18.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
# sphinx-tabs
pyproject-api==1.8.0
# via tox
pyright==1.1.389
# via -r /Users/david/Projects/flask/requirements/typing.txt
pytest==8.3.3
# via
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
python-dotenv==1.0.1
# via
# -r /Users/david/Projects/flask/requirements/tests.txt
# -r /Users/david/Projects/flask/requirements/typing.txt
pyyaml==6.0.2
# via pre-commit
requests==2.32.3
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
snowballstemmer==2.2.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinx==8.1.3
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# pallets-sphinx-themes
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-notfound-page==1.0.4
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r /Users/david/Projects/flask/requirements/docs.txt
sphinxcontrib-applehelp==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-devhelp==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-htmlhelp==2.1.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-jsmath==1.0.1
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r /Users/david/Projects/flask/requirements/docs.txt
sphinxcontrib-qthelp==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
sphinxcontrib-serializinghtml==2.0.0
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# sphinx
tox==4.23.2
# via -r dev.in
types-contextvars==2.4.7.3
# via -r /Users/david/Projects/flask/requirements/typing.txt
types-dataclasses==0.6.6
# via -r /Users/david/Projects/flask/requirements/typing.txt
typing-extensions==4.12.2
# via
# -r /Users/david/Projects/flask/requirements/typing.txt
# mypy
# pyright
urllib3==2.2.3
# via
# -r /Users/david/Projects/flask/requirements/docs.txt
# requests
virtualenv==20.27.1
# via
# pre-commit
# tox

View file

@ -1,4 +0,0 @@
pallets-sphinx-themes
sphinx
sphinxcontrib-log-cabinet
sphinx-tabs

View file

@ -1,67 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile docs.in
#
alabaster==1.0.0
# via sphinx
babel==2.16.0
# via sphinx
certifi==2024.8.30
# via requests
charset-normalizer==3.4.0
# via requests
docutils==0.21.2
# via
# sphinx
# sphinx-tabs
idna==3.10
# via requests
imagesize==1.4.1
# via sphinx
jinja2==3.1.4
# via sphinx
markupsafe==3.0.2
# via jinja2
packaging==24.2
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.3.0
# via -r docs.in
pygments==2.18.0
# via
# sphinx
# sphinx-tabs
requests==2.32.3
# via sphinx
snowballstemmer==2.2.0
# via sphinx
sphinx==8.1.3
# via
# -r docs.in
# pallets-sphinx-themes
# sphinx-notfound-page
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-notfound-page==1.0.4
# via pallets-sphinx-themes
sphinx-tabs==3.4.7
# via -r docs.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-log-cabinet==1.0.1
# via -r docs.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
urllib3==2.2.3
# via requests

View file

@ -1,6 +0,0 @@
https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz
https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz
https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz
https://github.com/pallets/click/archive/refs/heads/main.tar.gz
https://github.com/pallets-eco/blinker/archive/refs/heads/main.tar.gz

View file

@ -1,6 +0,0 @@
werkzeug==3.1.0
jinja2==3.1.2
markupsafe==2.1.1
itsdangerous==2.2.0
click==8.1.3
blinker==1.9.0

View file

@ -1,21 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile tests-min.in
#
blinker==1.9.0
# via -r tests-min.in
click==8.1.3
# via -r tests-min.in
itsdangerous==2.2.0
# via -r tests-min.in
jinja2==3.1.2
# via -r tests-min.in
markupsafe==2.1.1
# via
# -r tests-min.in
# jinja2
# werkzeug
werkzeug==3.1.0
# via -r tests-min.in

View file

@ -1,4 +0,0 @@
pytest
asgiref
greenlet ; python_version < "3.11"
python-dotenv

View file

@ -1,18 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile tests.in
#
asgiref==3.8.1
# via -r tests.in
iniconfig==2.0.0
# via pytest
packaging==24.2
# via pytest
pluggy==1.5.0
# via pytest
pytest==8.3.3
# via -r tests.in
python-dotenv==1.0.1
# via -r tests.in

View file

@ -1,8 +0,0 @@
mypy
pyright
pytest
types-contextvars
types-dataclasses
asgiref
cryptography
python-dotenv

View file

@ -1,40 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
# pip-compile typing.in
#
asgiref==3.8.1
# via -r typing.in
cffi==1.17.1
# via cryptography
cryptography==43.0.3
# via -r typing.in
iniconfig==2.0.0
# via pytest
mypy==1.13.0
# via -r typing.in
mypy-extensions==1.0.0
# via mypy
nodeenv==1.9.1
# via pyright
packaging==24.2
# via pytest
pluggy==1.5.0
# via pytest
pycparser==2.22
# via cffi
pyright==1.1.389
# via -r typing.in
pytest==8.3.3
# via -r typing.in
python-dotenv==1.0.1
# via -r typing.in
types-contextvars==2.4.7.3
# via -r typing.in
types-dataclasses==0.6.6
# via -r typing.in
typing-extensions==4.12.2
# via
# mypy
# pyright

View file

@ -1,11 +1,13 @@
from __future__ import annotations from __future__ import annotations
import collections.abc as cabc import collections.abc as cabc
import inspect
import os import os
import sys import sys
import typing as t import typing as t
import weakref import weakref
from datetime import timedelta from datetime import timedelta
from functools import update_wrapper
from inspect import iscoroutinefunction from inspect import iscoroutinefunction
from itertools import chain from itertools import chain
from types import TracebackType from types import TracebackType
@ -29,20 +31,17 @@ from werkzeug.wsgi import get_host
from . import cli from . import cli
from . import typing as ft from . import typing as ft
from .ctx import AppContext from .ctx import AppContext
from .ctx import RequestContext
from .globals import _cv_app from .globals import _cv_app
from .globals import _cv_request from .globals import app_ctx
from .globals import current_app
from .globals import g from .globals import g
from .globals import request from .globals import request
from .globals import request_ctx
from .globals import session from .globals import session
from .helpers import _CollectErrors
from .helpers import get_debug_flag from .helpers import get_debug_flag
from .helpers import get_flashed_messages from .helpers import get_flashed_messages
from .helpers import get_load_dotenv from .helpers import get_load_dotenv
from .helpers import send_from_directory from .helpers import send_from_directory
from .sansio.app import App from .sansio.app import App
from .sansio.scaffold import _sentinel
from .sessions import SecureCookieSessionInterface from .sessions import SecureCookieSessionInterface
from .sessions import SessionInterface from .sessions import SessionInterface
from .signals import appcontext_tearing_down from .signals import appcontext_tearing_down
@ -78,6 +77,35 @@ def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
return timedelta(seconds=value) return timedelta(seconds=value)
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
# Other methods may call the overridden method with the new ctx arg. Remove it
# and call the method with the remaining args.
def remove_ctx(f: F) -> F:
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
if args and isinstance(args[0], AppContext):
args = args[1:]
return f(self, *args, **kwargs)
return update_wrapper(wrapper, f) # type: ignore[return-value]
# The overridden method may call super().base_method without the new ctx arg.
# Add it to the args for the call.
def add_ctx(f: F) -> F:
def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any:
if not args:
args = (app_ctx._get_current_object(),)
elif not isinstance(args[0], AppContext):
args = (app_ctx._get_current_object(), *args)
return f(self, *args, **kwargs)
return update_wrapper(wrapper, f) # type: ignore[return-value]
class Flask(App): class Flask(App):
"""The flask object implements a WSGI application and acts as the central """The flask object implements a WSGI application and acts as the central
object. It is passed the name of the module or package of the object. It is passed the name of the module or package of the
@ -223,6 +251,62 @@ class Flask(App):
#: .. versionadded:: 0.8 #: .. versionadded:: 0.8
session_interface: SessionInterface = SecureCookieSessionInterface() session_interface: SessionInterface = SecureCookieSessionInterface()
def __init_subclass__(cls, **kwargs: t.Any) -> None:
import warnings
# These method signatures were updated to take a ctx param. Detect
# overridden methods in subclasses that still have the old signature.
# Show a deprecation warning and wrap to call with correct args.
for method in (
cls.handle_http_exception,
cls.handle_user_exception,
cls.handle_exception,
cls.log_exception,
cls.dispatch_request,
cls.full_dispatch_request,
cls.finalize_request,
cls.make_default_options_response,
cls.preprocess_request,
cls.process_response,
cls.do_teardown_request,
cls.do_teardown_appcontext,
):
base_method = getattr(Flask, method.__name__)
if method is base_method:
# not overridden
continue
# get the second parameter (first is self)
iter_params = iter(inspect.signature(method).parameters.values())
next(iter_params)
param = next(iter_params, None)
# must have second parameter named ctx or annotated AppContext
if param is None or not (
# no annotation, match name
(param.annotation is inspect.Parameter.empty and param.name == "ctx")
or (
# string annotation, access path ends with AppContext
isinstance(param.annotation, str)
and param.annotation.rpartition(".")[2] == "AppContext"
)
or (
# class annotation
inspect.isclass(param.annotation)
and issubclass(param.annotation, AppContext)
)
):
warnings.warn(
f"The '{method.__name__}' method now takes 'ctx: AppContext'"
" as the first parameter. The old signature is deprecated"
" and will not be supported in Flask 4.0.",
DeprecationWarning,
stacklevel=2,
)
setattr(cls, method.__name__, remove_ctx(method))
setattr(Flask, method.__name__, add_ctx(base_method))
def __init__( def __init__(
self, self,
import_name: str, import_name: str,
@ -265,9 +349,9 @@ class Flask(App):
# For one, it might be created while the server is running (e.g. during # For one, it might be created while the server is running (e.g. during
# development). Also, Google App Engine stores static files somewhere # development). Also, Google App Engine stores static files somewhere
if self.has_static_folder: if self.has_static_folder:
assert ( assert bool(static_host) == host_matching, (
bool(static_host) == host_matching "Invalid static_host/host_matching combination"
), "Invalid static_host/host_matching combination" )
# Use a weakref to avoid creating a reference cycle between the app # Use a weakref to avoid creating a reference cycle between the app
# and the view function (see #3761). # and the view function (see #3761).
self_ref = weakref.ref(self) self_ref = weakref.ref(self)
@ -275,7 +359,7 @@ class Flask(App):
f"{self.static_url_path}/<path:filename>", f"{self.static_url_path}/<path:filename>",
endpoint="static", endpoint="static",
host=static_host, host=static_host,
view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore
) )
def get_send_file_max_age(self, filename: str | None) -> int | None: def get_send_file_max_age(self, filename: str | None) -> int | None:
@ -295,7 +379,7 @@ class Flask(App):
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] value = self.config["SEND_FILE_MAX_AGE_DEFAULT"]
if value is None: if value is None:
return None return None
@ -503,7 +587,9 @@ class Flask(App):
raise FormDataRoutingRedirect(request) raise FormDataRoutingRedirect(request)
def update_template_context(self, context: dict[str, t.Any]) -> None: def update_template_context(
self, ctx: AppContext, context: dict[str, t.Any]
) -> None:
"""Update the template context with some commonly used variables. """Update the template context with some commonly used variables.
This injects request, session, config and g into the template This injects request, session, config and g into the template
context as well as everything template context processors want context as well as everything template context processors want
@ -517,8 +603,8 @@ class Flask(App):
names: t.Iterable[str | None] = (None,) names: t.Iterable[str | None] = (None,)
# A template may be rendered outside a request context. # A template may be rendered outside a request context.
if request: if ctx.has_request:
names = chain(names, reversed(request.blueprints)) names = chain(names, reversed(ctx.request.blueprints))
# The values passed to render_template take precedence. Keep a # The values passed to render_template take precedence. Keep a
# copy to re-apply after all context functions. # copy to re-apply after all context functions.
@ -742,7 +828,7 @@ class Flask(App):
return cls(self, **kwargs) # type: ignore return cls(self, **kwargs) # type: ignore
def handle_http_exception( def handle_http_exception(
self, e: HTTPException self, ctx: AppContext, e: HTTPException
) -> HTTPException | ft.ResponseReturnValue: ) -> HTTPException | ft.ResponseReturnValue:
"""Handles an HTTP exception. By default this will invoke the """Handles an HTTP exception. By default this will invoke the
registered error handlers and fall back to returning the registered error handlers and fall back to returning the
@ -771,13 +857,13 @@ class Flask(App):
if isinstance(e, RoutingException): if isinstance(e, RoutingException):
return e return e
handler = self._find_error_handler(e, request.blueprints) handler = self._find_error_handler(e, ctx.request.blueprints)
if handler is None: if handler is None:
return e return e
return self.ensure_sync(handler)(e) # type: ignore[no-any-return] return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
def handle_user_exception( def handle_user_exception(
self, e: Exception self, ctx: AppContext, e: Exception
) -> HTTPException | ft.ResponseReturnValue: ) -> HTTPException | ft.ResponseReturnValue:
"""This method is called whenever an exception occurs that """This method is called whenever an exception occurs that
should be handled. A special case is :class:`~werkzeug should be handled. A special case is :class:`~werkzeug
@ -799,16 +885,16 @@ class Flask(App):
e.show_exception = True e.show_exception = True
if isinstance(e, HTTPException) and not self.trap_http_exception(e): if isinstance(e, HTTPException) and not self.trap_http_exception(e):
return self.handle_http_exception(e) return self.handle_http_exception(ctx, e)
handler = self._find_error_handler(e, request.blueprints) handler = self._find_error_handler(e, ctx.request.blueprints)
if handler is None: if handler is None:
raise raise
return self.ensure_sync(handler)(e) # type: ignore[no-any-return] return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
def handle_exception(self, e: Exception) -> Response: def handle_exception(self, ctx: AppContext, e: Exception) -> Response:
"""Handle an exception that did not have an error handler """Handle an exception that did not have an error handler
associated with it, or that was raised from an error handler. associated with it, or that was raised from an error handler.
This always causes a 500 ``InternalServerError``. This always causes a 500 ``InternalServerError``.
@ -851,19 +937,20 @@ class Flask(App):
raise e raise e
self.log_exception(exc_info) self.log_exception(ctx, exc_info)
server_error: InternalServerError | ft.ResponseReturnValue server_error: InternalServerError | ft.ResponseReturnValue
server_error = InternalServerError(original_exception=e) server_error = InternalServerError(original_exception=e)
handler = self._find_error_handler(server_error, request.blueprints) handler = self._find_error_handler(server_error, ctx.request.blueprints)
if handler is not None: if handler is not None:
server_error = self.ensure_sync(handler)(server_error) server_error = self.ensure_sync(handler)(server_error)
return self.finalize_request(server_error, from_error_handler=True) return self.finalize_request(ctx, server_error, from_error_handler=True)
def log_exception( def log_exception(
self, self,
exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), ctx: AppContext,
exc_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None],
) -> None: ) -> None:
"""Logs an exception. This is called by :meth:`handle_exception` """Logs an exception. This is called by :meth:`handle_exception`
if debugging is disabled and right before the handler is called. if debugging is disabled and right before the handler is called.
@ -873,10 +960,10 @@ class Flask(App):
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
self.logger.error( self.logger.error(
f"Exception on {request.path} [{request.method}]", exc_info=exc_info f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info
) )
def dispatch_request(self) -> ft.ResponseReturnValue: def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
"""Does the request dispatching. Matches the URL and returns the """Does the request dispatching. Matches the URL and returns the
return value of the view or error handler. This does not have to return value of the view or error handler. This does not have to
be a response object. In order to convert the return value to a be a response object. In order to convert the return value to a
@ -886,7 +973,8 @@ class Flask(App):
This no longer does the exception handling, this code was This no longer does the exception handling, this code was
moved to the new :meth:`full_dispatch_request`. moved to the new :meth:`full_dispatch_request`.
""" """
req = request_ctx.request req = ctx.request
if req.routing_exception is not None: if req.routing_exception is not None:
self.raise_routing_exception(req) self.raise_routing_exception(req)
rule: Rule = req.url_rule # type: ignore[assignment] rule: Rule = req.url_rule # type: ignore[assignment]
@ -896,31 +984,43 @@ class Flask(App):
getattr(rule, "provide_automatic_options", False) getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS" and req.method == "OPTIONS"
): ):
return self.make_default_options_response() return self.make_default_options_response(ctx)
# otherwise dispatch to the handler for that endpoint # otherwise dispatch to the handler for that endpoint
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
def full_dispatch_request(self) -> Response: def full_dispatch_request(self, ctx: AppContext) -> Response:
"""Dispatches the request and on top of that performs request """Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and pre and postprocessing as well as HTTP exception catching and
error handling. error handling.
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
if not self._got_first_request and self.should_ignore_error is not None:
import warnings
warnings.warn(
"The 'should_ignore_error' method is deprecated and will"
" be removed in Flask 3.3. Handle errors as needed in"
" teardown handlers instead.",
DeprecationWarning,
stacklevel=1,
)
self._got_first_request = True self._got_first_request = True
try: try:
request_started.send(self, _async_wrapper=self.ensure_sync) request_started.send(self, _async_wrapper=self.ensure_sync)
rv = self.preprocess_request() rv = self.preprocess_request(ctx)
if rv is None: if rv is None:
rv = self.dispatch_request() rv = self.dispatch_request(ctx)
except Exception as e: except Exception as e:
rv = self.handle_user_exception(e) rv = self.handle_user_exception(ctx, e)
return self.finalize_request(rv) return self.finalize_request(ctx, rv)
def finalize_request( def finalize_request(
self, self,
ctx: AppContext,
rv: ft.ResponseReturnValue | HTTPException, rv: ft.ResponseReturnValue | HTTPException,
from_error_handler: bool = False, from_error_handler: bool = False,
) -> Response: ) -> Response:
@ -938,7 +1038,7 @@ class Flask(App):
""" """
response = self.make_response(rv) response = self.make_response(rv)
try: try:
response = self.process_response(response) response = self.process_response(ctx, response)
request_finished.send( request_finished.send(
self, _async_wrapper=self.ensure_sync, response=response self, _async_wrapper=self.ensure_sync, response=response
) )
@ -950,15 +1050,14 @@ class Flask(App):
) )
return response return response
def make_default_options_response(self) -> Response: def make_default_options_response(self, ctx: AppContext) -> Response:
"""This method is called to create the default ``OPTIONS`` response. """This method is called to create the default ``OPTIONS`` response.
This can be changed through subclassing to change the default This can be changed through subclassing to change the default
behavior of ``OPTIONS`` responses. behavior of ``OPTIONS`` responses.
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
adapter = request_ctx.url_adapter methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr]
methods = adapter.allowed_methods() # type: ignore[union-attr]
rv = self.response_class() rv = self.response_class()
rv.allow.update(methods) rv.allow.update(methods)
return rv return rv
@ -1057,11 +1156,9 @@ class Flask(App):
.. versionadded:: 2.2 .. versionadded:: 2.2
Moved from ``flask.url_for``, which calls this method. Moved from ``flask.url_for``, which calls this method.
""" """
req_ctx = _cv_request.get(None) if (ctx := _cv_app.get(None)) is not None and ctx.has_request:
url_adapter = ctx.url_adapter
if req_ctx is not None: blueprint_name = ctx.request.blueprint
url_adapter = req_ctx.url_adapter
blueprint_name = req_ctx.request.blueprint
# If the endpoint starts with "." and the request matches a # If the endpoint starts with "." and the request matches a
# blueprint, the endpoint is relative to the blueprint. # blueprint, the endpoint is relative to the blueprint.
@ -1076,13 +1173,11 @@ class Flask(App):
if _external is None: if _external is None:
_external = _scheme is not None _external = _scheme is not None
else: else:
app_ctx = _cv_app.get(None)
# If called by helpers.url_for, an app context is active, # If called by helpers.url_for, an app context is active,
# use its url_adapter. Otherwise, app.url_for was called # use its url_adapter. Otherwise, app.url_for was called
# directly, build an adapter. # directly, build an adapter.
if app_ctx is not None: if ctx is not None:
url_adapter = app_ctx.url_adapter url_adapter = ctx.url_adapter
else: else:
url_adapter = self.create_url_adapter(None) url_adapter = self.create_url_adapter(None)
@ -1222,7 +1317,7 @@ class Flask(App):
# waiting to do it manually, so that the class can handle any # waiting to do it manually, so that the class can handle any
# special logic # special logic
rv = self.response_class( rv = self.response_class(
rv, rv, # pyright: ignore
status=status, status=status,
headers=headers, # type: ignore[arg-type] headers=headers, # type: ignore[arg-type]
) )
@ -1268,7 +1363,7 @@ class Flask(App):
return rv return rv
def preprocess_request(self) -> ft.ResponseReturnValue | None: def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None:
"""Called before the request is dispatched. Calls """Called before the request is dispatched. Calls
:attr:`url_value_preprocessors` registered with the app and the :attr:`url_value_preprocessors` registered with the app and the
current blueprint (if any). Then calls :attr:`before_request_funcs` current blueprint (if any). Then calls :attr:`before_request_funcs`
@ -1278,12 +1373,13 @@ class Flask(App):
value is handled as if it was the return value from the view, and value is handled as if it was the return value from the view, and
further request handling is stopped. further request handling is stopped.
""" """
names = (None, *reversed(request.blueprints)) req = ctx.request
names = (None, *reversed(req.blueprints))
for name in names: for name in names:
if name in self.url_value_preprocessors: if name in self.url_value_preprocessors:
for url_func in self.url_value_preprocessors[name]: for url_func in self.url_value_preprocessors[name]:
url_func(request.endpoint, request.view_args) url_func(req.endpoint, req.view_args)
for name in names: for name in names:
if name in self.before_request_funcs: if name in self.before_request_funcs:
@ -1295,7 +1391,7 @@ class Flask(App):
return None return None
def process_response(self, response: Response) -> Response: def process_response(self, ctx: AppContext, response: Response) -> Response:
"""Can be overridden in order to modify the response object """Can be overridden in order to modify the response object
before it's sent to the WSGI server. By default this will before it's sent to the WSGI server. By default this will
call all the :meth:`after_request` decorated functions. call all the :meth:`after_request` decorated functions.
@ -1308,92 +1404,90 @@ class Flask(App):
:return: a new response object or the same, has to be an :return: a new response object or the same, has to be an
instance of :attr:`response_class`. instance of :attr:`response_class`.
""" """
ctx = request_ctx._get_current_object() # type: ignore[attr-defined]
for func in ctx._after_request_functions: for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response) response = self.ensure_sync(func)(response)
for name in chain(request.blueprints, (None,)): for name in chain(ctx.request.blueprints, (None,)):
if name in self.after_request_funcs: if name in self.after_request_funcs:
for func in reversed(self.after_request_funcs[name]): for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(func)(response) response = self.ensure_sync(func)(response)
if not self.session_interface.is_null_session(ctx.session): if not self.session_interface.is_null_session(ctx._get_session()):
self.session_interface.save_session(self, ctx.session, response) self.session_interface.save_session(self, ctx._get_session(), response)
return response return response
def do_teardown_request( def do_teardown_request(
self, self, ctx: AppContext, exc: BaseException | None = None
exc: BaseException | None = _sentinel, # type: ignore[assignment]
) -> None: ) -> None:
"""Called after the request is dispatched and the response is """Called after the request is dispatched and the response is finalized,
returned, right before the request context is popped. right before the request context is popped. Called by
:meth:`.AppContext.pop`.
This calls all functions decorated with This calls all functions decorated with :meth:`teardown_request`, and
:meth:`teardown_request`, and :meth:`Blueprint.teardown_request` :meth:`Blueprint.teardown_request` if a blueprint handled the request.
if a blueprint handled the request. Finally, the Finally, the :data:`request_tearing_down` signal is sent.
:data:`request_tearing_down` signal is sent.
This is called by :param exc: An unhandled exception raised while dispatching the request.
:meth:`RequestContext.pop() <flask.ctx.RequestContext.pop>`, Passed to each teardown function.
which may be delayed during testing to maintain access to
resources.
:param exc: An unhandled exception raised while dispatching the .. versionchanged:: 3.2
request. Detected from the current exception information if All callbacks are called rather than stopping on the first error.
not passed. Passed to each teardown function.
.. versionchanged:: 0.9 .. versionchanged:: 0.9
Added the ``exc`` argument. Added the ``exc`` argument.
""" """
if exc is _sentinel: collect_errors = _CollectErrors()
exc = sys.exc_info()[1]
for name in chain(request.blueprints, (None,)): for name in chain(ctx.request.blueprints, (None,)):
if name in self.teardown_request_funcs: if name in self.teardown_request_funcs:
for func in reversed(self.teardown_request_funcs[name]): for func in reversed(self.teardown_request_funcs[name]):
with collect_errors:
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
with collect_errors:
request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
collect_errors.raise_any("Errors during request teardown")
def do_teardown_appcontext( def do_teardown_appcontext(
self, self, ctx: AppContext, exc: BaseException | None = None
exc: BaseException | None = _sentinel, # type: ignore[assignment]
) -> None: ) -> None:
"""Called right before the application context is popped. """Called right before the application context is popped. Called by
:meth:`.AppContext.pop`.
When handling a request, the application context is popped This calls all functions decorated with :meth:`teardown_appcontext`.
after the request context. See :meth:`do_teardown_request`. Then the :data:`appcontext_tearing_down` signal is sent.
This calls all functions decorated with :param exc: An unhandled exception raised while the context was active.
:meth:`teardown_appcontext`. Then the Passed to each teardown function.
:data:`appcontext_tearing_down` signal is sent.
This is called by .. versionchanged:: 3.2
:meth:`AppContext.pop() <flask.ctx.AppContext.pop>`. All callbacks are called rather than stopping on the first error.
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
if exc is _sentinel: collect_errors = _CollectErrors()
exc = sys.exc_info()[1]
for func in reversed(self.teardown_appcontext_funcs): for func in reversed(self.teardown_appcontext_funcs):
with collect_errors:
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
with collect_errors:
appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
collect_errors.raise_any("Errors during app teardown")
def app_context(self) -> AppContext: def app_context(self) -> AppContext:
"""Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` """Create an :class:`.AppContext`. When the context is pushed,
block to push the context, which will make :data:`current_app` :data:`.current_app` and :data:`.g` become available.
point at this application.
An application context is automatically pushed by A context is automatically pushed when handling each request, and when
:meth:`RequestContext.push() <flask.ctx.RequestContext.push>` running any ``flask`` CLI command. Use this as a ``with`` block to
when handling a request, and when running a CLI command. Use manually push a context outside of those situations, such as during
this to manually create a context outside of these situations. setup or testing.
:: .. code-block:: python
with app.app_context(): with app.app_context():
init_db() init_db()
@ -1404,44 +1498,37 @@ class Flask(App):
""" """
return AppContext(self) return AppContext(self)
def request_context(self, environ: WSGIEnvironment) -> RequestContext: def request_context(self, environ: WSGIEnvironment) -> AppContext:
"""Create a :class:`~flask.ctx.RequestContext` representing a """Create an :class:`.AppContext` with request information representing
WSGI environment. Use a ``with`` block to push the context, the given WSGI environment. A context is automatically pushed when
which will make :data:`request` point at this request. handling each request. When the context is pushed, :data:`.request`,
:data:`.session`, :data:`g:, and :data:`.current_app` become available.
See :doc:`/reqcontext`. This method should not be used in your own code. Creating a valid WSGI
environ is not trivial. Use :meth:`test_request_context` to correctly
create a WSGI environ and request context instead.
Typically you should not call this from your own code. A request See :doc:`/appcontext`.
context is automatically pushed by the :meth:`wsgi_app` when
handling a request. Use :meth:`test_request_context` to create
an environment and context instead of this method.
:param environ: a WSGI environment :param environ: A WSGI environment.
""" """
return RequestContext(self, environ) return AppContext.from_environ(self, environ)
def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> AppContext:
"""Create a :class:`~flask.ctx.RequestContext` for a WSGI """Create an :class:`.AppContext` with request information created from
environment created from the given values. This is mostly useful the given arguments. When the context is pushed, :data:`.request`,
during testing, where you may want to run a function that uses :data:`.session`, :data:`g:, and :data:`.current_app` become available.
request data without dispatching a full request.
See :doc:`/reqcontext`. This is useful during testing to run a function that uses request data
without dispatching a full request. Use this as a ``with`` block to push
a context.
Use a ``with`` block to push the context, which will make .. code-block:: python
:data:`request` point at the request for the created
environment. ::
with app.test_request_context(...): with app.test_request_context(...):
generate_report() generate_report()
When using the shell, it may be easier to push and pop the See :doc:`/appcontext`.
context manually to avoid indentation. ::
ctx = app.test_request_context(...)
ctx.push()
...
ctx.pop()
Takes the same arguments as Werkzeug's Takes the same arguments as Werkzeug's
:class:`~werkzeug.test.EnvironBuilder`, with some defaults from :class:`~werkzeug.test.EnvironBuilder`, with some defaults from
@ -1451,20 +1538,18 @@ class Flask(App):
:param path: URL path being requested. :param path: URL path being requested.
:param base_url: Base URL where the app is being served, which :param base_url: Base URL where the app is being served, which
``path`` is relative to. If not given, built from ``path`` is relative to. If not given, built from
:data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`,
:data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. and :data:`APPLICATION_ROOT`.
:param subdomain: Subdomain name to append to :param subdomain: Subdomain name to prepend to :data:`SERVER_NAME`.
:data:`SERVER_NAME`.
:param url_scheme: Scheme to use instead of :param url_scheme: Scheme to use instead of
:data:`PREFERRED_URL_SCHEME`. :data:`PREFERRED_URL_SCHEME`.
:param data: The request body, either as a string or a dict of :param data: The request body text or bytes,or a dict of form data.
form keys and values.
:param json: If given, this is serialized as JSON and passed as :param json: If given, this is serialized as JSON and passed as
``data``. Also defaults ``content_type`` to ``data``. Also defaults ``content_type`` to
``application/json``. ``application/json``.
:param args: other positional arguments passed to :param args: Other positional arguments passed to
:class:`~werkzeug.test.EnvironBuilder`. :class:`~werkzeug.test.EnvironBuilder`.
:param kwargs: other keyword arguments passed to :param kwargs: Other keyword arguments passed to
:class:`~werkzeug.test.EnvironBuilder`. :class:`~werkzeug.test.EnvironBuilder`.
""" """
from .testing import EnvironBuilder from .testing import EnvironBuilder
@ -1472,10 +1557,12 @@ class Flask(App):
builder = EnvironBuilder(self, *args, **kwargs) builder = EnvironBuilder(self, *args, **kwargs)
try: try:
return self.request_context(builder.get_environ()) environ = builder.get_environ()
finally: finally:
builder.close() builder.close()
return self.request_context(environ)
def wsgi_app( def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse self, environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]: ) -> cabc.Iterable[bytes]:
@ -1496,7 +1583,6 @@ class Flask(App):
Teardown events for the request and app contexts are called Teardown events for the request and app contexts are called
even if an unhandled error occurs. Other events may not be even if an unhandled error occurs. Other events may not be
called depending on when an error occurs during dispatch. called depending on when an error occurs during dispatch.
See :ref:`callbacks-and-errors`.
:param environ: A WSGI environment. :param environ: A WSGI environment.
:param start_response: A callable accepting a status code, :param start_response: A callable accepting a status code,
@ -1508,20 +1594,23 @@ class Flask(App):
try: try:
try: try:
ctx.push() ctx.push()
response = self.full_dispatch_request() response = self.full_dispatch_request(ctx)
except Exception as e: except Exception as e:
error = e error = e
response = self.handle_exception(e) response = self.handle_exception(ctx, e)
except: # noqa: B001 except:
error = sys.exc_info()[1] error = sys.exc_info()[1]
raise raise
return response(environ, start_response) return response(environ, start_response)
finally: finally:
if "werkzeug.debug.preserve_context" in environ: if "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get()) environ["werkzeug.debug.preserve_context"](ctx)
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error): if (
error is not None
and self.should_ignore_error is not None
and self.should_ignore_error(error)
):
error = None error = None
ctx.pop(error) ctx.pop(error)

View file

@ -468,7 +468,7 @@ _app_option = click.Option(
def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None:
# If the flag isn't provided, it will default to False. Don't use # If the flag isn't provided, it will default to False. Don't use
# that, let debug be set by env in that case. # that, let debug be set by env in that case.
source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] source = ctx.get_parameter_source(param.name)
if source is not None and source in ( if source is not None and source in (
ParameterSource.DEFAULT, ParameterSource.DEFAULT,
@ -601,15 +601,7 @@ class FlaskGroup(AppGroup):
if self._loaded_plugin_commands: if self._loaded_plugin_commands:
return return
if sys.version_info >= (3, 10): for ep in importlib.metadata.entry_points(group="flask.commands"):
from importlib import metadata
else:
# Use a backport on Python < 3.10. We technically have
# importlib.metadata on 3.8+, but the API changed in 3.10,
# so use the backport for consistency.
import importlib_metadata as metadata # pyright: ignore
for ep in metadata.entry_points(group="flask.commands"):
self.add_command(ep.load(), ep.name) self.add_command(ep.load(), ep.name)
self._loaded_plugin_commands = True self._loaded_plugin_commands = True
@ -636,7 +628,7 @@ class FlaskGroup(AppGroup):
# Push an app context for the loaded app unless it is already # Push an app context for the loaded app unless it is already
# active somehow. This makes the context available to parameter # active somehow. This makes the context available to parameter
# and command callbacks without needing @with_appcontext. # and command callbacks without needing @with_appcontext.
if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] if not current_app or current_app._get_current_object() is not app:
ctx.with_resource(app.app_context()) ctx.with_resource(app.app_context())
return app.cli.get_command(ctx, name) return app.cli.get_command(ctx, name)
@ -684,7 +676,9 @@ class FlaskGroup(AppGroup):
return super().make_context(info_name, args, parent=parent, **extra) return super().make_context(info_name, args, parent=parent, **extra)
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
if not args and self.no_args_is_help: if (not args and self.no_args_is_help) or (
len(args) == 1 and args[0] in self.get_help_option_names(ctx)
):
# Attempt to load --env-file and --app early in case they # Attempt to load --env-file and --app early in case they
# were given as env vars. Otherwise no_args_is_help will not # were given as env vars. Otherwise no_args_is_help will not
# see commands from app.cli. # see commands from app.cli.
@ -783,7 +777,7 @@ def show_server_banner(debug: bool, app_import_path: str | None) -> None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}") click.echo(f" * Debug mode: {'on' if debug else 'off'}")
class CertParamType(click.ParamType): class CertParamType(click.ParamType[t.Any]):
"""Click option type for the ``--cert`` option. Allows either an """Click option type for the ``--cert`` option. Allows either an
existing file, the string ``'adhoc'``, or an import for a existing file, the string ``'adhoc'``, or an import for a
:class:`~ssl.SSLContext` object. :class:`~ssl.SSLContext` object.
@ -809,7 +803,7 @@ class CertParamType(click.ParamType):
try: try:
return self.path_type(value, param, ctx) return self.path_type(value, param, ctx)
except click.BadParameter: except click.BadParameter:
value = click.STRING(value, param, ctx).lower() value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr]
if value == "adhoc": if value == "adhoc":
try: try:

View file

@ -1,20 +1,21 @@
from __future__ import annotations from __future__ import annotations
import contextvars import contextvars
import sys
import typing as t import typing as t
from functools import update_wrapper from functools import update_wrapper
from types import TracebackType from types import TracebackType
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.routing import MapAdapter
from . import typing as ft from . import typing as ft
from .globals import _cv_app from .globals import _cv_app
from .globals import _cv_request from .helpers import _CollectErrors
from .signals import appcontext_popped from .signals import appcontext_popped
from .signals import appcontext_pushed from .signals import appcontext_pushed
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING:
import typing_extensions as te
from _typeshed.wsgi import WSGIEnvironment from _typeshed.wsgi import WSGIEnvironment
from .app import Flask from .app import Flask
@ -31,7 +32,7 @@ class _AppCtxGlobals:
application context. application context.
Creating an app context automatically creates this object, which is Creating an app context automatically creates this object, which is
made available as the :data:`g` proxy. made available as the :data:`.g` proxy.
.. describe:: 'key' in g .. describe:: 'key' in g
@ -117,29 +118,27 @@ class _AppCtxGlobals:
def after_this_request( def after_this_request(
f: ft.AfterRequestCallable[t.Any], f: ft.AfterRequestCallable[t.Any],
) -> ft.AfterRequestCallable[t.Any]: ) -> ft.AfterRequestCallable[t.Any]:
"""Executes a function after this request. This is useful to modify """Decorate a function to run after the current request. The behavior is the
response objects. The function is passed the response object and has same as :meth:`.Flask.after_request`, except it only applies to the current
to return the same or a new one. request, rather than every request. Therefore, it must be used within a
request context, rather than during setup.
Example:: .. code-block:: python
@app.route('/') @app.route("/")
def index(): def index():
@after_this_request @after_this_request
def add_header(response): def add_header(response):
response.headers['X-Foo'] = 'Parachute' response.headers["X-Foo"] = "Parachute"
return response return response
return 'Hello World!'
This is more useful if a function other than the view function wants to return "Hello, World!"
modify a response. For instance think of a decorator that wants to add
some headers without converting the return value into a response object.
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
ctx = _cv_request.get(None) ctx = _cv_app.get(None)
if ctx is None: if ctx is None or not ctx.has_request:
raise RuntimeError( raise RuntimeError(
"'after_this_request' can only be used when a request" "'after_this_request' can only be used when a request"
" context is active, such as in a view function." " context is active, such as in a view function."
@ -153,13 +152,27 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def copy_current_request_context(f: F) -> F: def copy_current_request_context(f: F) -> F:
"""A helper function that decorates a function to retain the current """Decorate a function to run inside the current request context. This can
request context. This is useful when working with greenlets. The moment be used when starting a background task, otherwise it will not see the app
the function is decorated a copy of the request context is created and and request objects that were active in the parent.
then pushed when the function is called. The current session is also
included in the copied request context.
Example:: .. warning::
Due to the following caveats, it is often safer (and simpler) to pass
the data you need when starting the task, rather than using this and
relying on the context objects.
In order to avoid execution switching partially though reading data, either
read the request body (access ``form``, ``json``, ``data``, etc) before
starting the task, or use a lock. This can be an issue when using threading,
but shouldn't be an issue when using greenlet/gevent or asyncio.
If the task will access ``session``, be sure to do so in the parent as well
so that the ``Vary: cookie`` header will be set. Modifying ``session`` in
the task should be avoided, as it may execute after the response cookie has
already been written.
.. code-block:: python
import gevent import gevent
from flask import copy_current_request_context from flask import copy_current_request_context
@ -176,59 +189,68 @@ def copy_current_request_context(f: F) -> F:
.. versionadded:: 0.10 .. versionadded:: 0.10
""" """
ctx = _cv_request.get(None) # Store the context that was active when the decorator was applied.
original = _cv_app.get(None)
if ctx is None: if original is None:
raise RuntimeError( raise RuntimeError(
"'copy_current_request_context' can only be used when a" "'copy_current_request_context' can only be used when a"
" request context is active, such as in a view function." " request context is active, such as in a view function."
) )
ctx = ctx.copy()
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
with ctx: # type: ignore[union-attr] # Copy the context before pushing, so each worker acts independently.
return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] with original.copy() as ctx:
return ctx.app.ensure_sync(f)(*args, **kwargs)
return update_wrapper(wrapper, f) # type: ignore[return-value] return update_wrapper(wrapper, f) # type: ignore[return-value]
def has_request_context() -> bool: def has_request_context() -> bool:
"""If you have code that wants to test if a request context is there or """Test if an app context is active and if it has request information.
not this function can be used. For instance, you may want to take advantage
of request information if the request object is available, but fail
silently if it is unavailable.
:: .. code-block:: python
class User(db.Model): from flask import has_request_context, request
def __init__(self, username, remote_addr=None): if has_request_context():
self.username = username
if remote_addr is None and has_request_context():
remote_addr = request.remote_addr remote_addr = request.remote_addr
self.remote_addr = remote_addr
Alternatively you can also just test any of the context bound objects If a request context is active, the :data:`.request` and :data:`.session`
(such as :class:`request` or :class:`g`) for truthness:: context proxies will available and ``True``, otherwise ``False``. You can
use that to test the data you use, rather than using this function.
class User(db.Model): .. code-block:: python
def __init__(self, username, remote_addr=None): from flask import request
self.username = username
if remote_addr is None and request: if request:
remote_addr = request.remote_addr remote_addr = request.remote_addr
self.remote_addr = remote_addr
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
return _cv_request.get(None) is not None return (ctx := _cv_app.get(None)) is not None and ctx.has_request
def has_app_context() -> bool: def has_app_context() -> bool:
"""Works like :func:`has_request_context` but for the application """Test if an app context is active. Unlike :func:`has_request_context`
context. You can also just do a boolean check on the this can be true outside a request, such as in a CLI command.
:data:`current_app` object instead.
.. code-block:: python
from flask import has_app_context, g
if has_app_context():
g.cached_data = ...
If an app context is active, the :data:`.g` and :data:`.current_app` context
proxies will available and ``True``, otherwise ``False``. You can use that
to test the data you use, rather than using this function.
from flask import g
if g:
g.cached_data = ...
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
@ -236,214 +258,283 @@ def has_app_context() -> bool:
class AppContext: class AppContext:
"""The app context contains application-specific information. An app """An app context contains information about an app, and about the request
context is created and pushed at the beginning of each request if when handling a request. A context is pushed at the beginning of each
one is not already active. An app context is also pushed when request and CLI command, and popped at the end. The context is referred to
running CLI commands. as a "request context" if it has request information, and an "app context"
""" if not.
def __init__(self, app: Flask) -> None: Do not use this class directly. Use :meth:`.Flask.app_context` to create an
self.app = app app context if needed during setup, and :meth:`.Flask.test_request_context`
self.url_adapter = app.create_url_adapter(None) to create a request context if needed during tests.
self.g: _AppCtxGlobals = app.app_ctx_globals_class()
self._cv_tokens: list[contextvars.Token[AppContext]] = []
def push(self) -> None: When the context is popped, it will evaluate all the teardown functions
"""Binds the app context to the current context.""" registered with :meth:`~flask.Flask.teardown_request` (if handling a
self._cv_tokens.append(_cv_app.set(self)) request) then :meth:`.Flask.teardown_appcontext`.
appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore When using the interactive debugger, the context will be restored so
"""Pops the app context.""" ``request`` is still accessible. Similarly, the test client can preserve the
try: context after the request ends. However, teardown functions may already have
if len(self._cv_tokens) == 1: closed some resources such as database connections, and will run again when
if exc is _sentinel: the restored context is popped.
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
ctx = _cv_app.get()
_cv_app.reset(self._cv_tokens.pop())
if ctx is not self: :param app: The application this context represents.
raise AssertionError( :param request: The request data this context represents.
f"Popped wrong app context. ({ctx!r} instead of {self!r})" :param session: The session data this context represents. If not given,
) loaded from the request on first access.
appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) .. versionchanged:: 3.2
Merged with ``RequestContext``. The ``RequestContext`` alias will be
removed in Flask 4.0.
def __enter__(self) -> AppContext: .. versionchanged:: 3.2
self.push() A combined app and request context is pushed for every request and CLI
return self command, rather than trying to detect if an app context is already
pushed.
def __exit__( .. versionchanged:: 3.2
self, The session is loaded the first time it is accessed, rather than when
exc_type: type | None, the context is pushed.
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.pop(exc_value)
class RequestContext:
"""The request context contains per-request information. The Flask
app creates and pushes it at the beginning of the request, then pops
it at the end of the request. It will create the URL adapter and
request object for the WSGI environment provided.
Do not attempt to use this class directly, instead use
:meth:`~flask.Flask.test_request_context` and
:meth:`~flask.Flask.request_context` to create this object.
When the request context is popped, it will evaluate all the
functions registered on the application for teardown execution
(:meth:`~flask.Flask.teardown_request`).
The request context is automatically popped at the end of the
request. When using the interactive debugger, the context will be
restored so ``request`` is still accessible. Similarly, the test
client can preserve the context after the request ends. However,
teardown functions may already have closed some resources such as
database connections.
""" """
def __init__( def __init__(
self, self,
app: Flask, app: Flask,
environ: WSGIEnvironment, *,
request: Request | None = None, request: Request | None = None,
session: SessionMixin | None = None, session: SessionMixin | None = None,
) -> None: ) -> None:
self.app = app self.app = app
if request is None: """The application represented by this context. Accessed through
request = app.request_class(environ) :data:`.current_app`.
request.json_module = app.json """
self.request: Request = request
self.url_adapter = None self.g: _AppCtxGlobals = app.app_ctx_globals_class()
try: """The global data for this context. Accessed through :data:`.g`."""
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e: self.url_adapter: MapAdapter | None = None
self.request.routing_exception = e """The URL adapter bound to the request, or the app if not in a request.
self.flashes: list[tuple[str, str]] | None = None May be ``None`` if binding failed.
self.session: SessionMixin | None = session """
# Functions that should be executed after the request on the response
# object. These will be called before the regular "after_request" self._request: Request | None = request
# functions. self._session: SessionMixin | None = session
self._flashes: list[tuple[str, str]] | None = None
self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
self._cv_tokens: list[ try:
tuple[contextvars.Token[RequestContext], AppContext | None] self.url_adapter = app.create_url_adapter(self._request)
] = [] except HTTPException as e:
if self._request is not None:
self._request.routing_exception = e
def copy(self) -> RequestContext: self._cv_token: contextvars.Token[AppContext] | None = None
"""Creates a copy of this request context with the same request object. """The previous state to restore when popping."""
This can be used to move a request context to a different greenlet.
Because the actual request object is the same this cannot be used to
move a request context to a different thread unless access to the
request object is locked.
.. versionadded:: 0.10 self._push_count: int = 0
"""Track nested pushes of this context. Cleanup will only run once the
original push has been popped.
"""
@classmethod
def from_environ(cls, app: Flask, environ: WSGIEnvironment, /) -> te.Self:
"""Create an app context with request data from the given WSGI environ.
:param app: The application this context represents.
:param environ: The request data this context represents.
"""
request = app.request_class(environ)
request.json_module = app.json
return cls(app, request=request)
@property
def has_request(self) -> bool:
"""True if this context was created with request data."""
return self._request is not None
def copy(self) -> te.Self:
"""Create a new context with the same data objects as this context. See
:func:`.copy_current_request_context`.
.. versionchanged:: 1.1 .. versionchanged:: 1.1
The current session object is used instead of reloading the original The current session data is used instead of reloading the original data.
data. This prevents `flask.session` pointing to an out-of-date object.
.. versionadded:: 0.10
""" """
return self.__class__( return self.__class__(
self.app, self.app,
environ=self.request.environ, request=self._request,
request=self.request, session=self._session,
session=self.session,
) )
@property
def request(self) -> Request:
"""The request object associated with this context. Accessed through
:data:`.request`. Only available in request contexts, otherwise raises
:exc:`RuntimeError`.
"""
if self._request is None:
raise RuntimeError("There is no request in this context.")
return self._request
def _get_session(self) -> SessionMixin:
"""Open the session if it is not already open for this request context."""
if self._request is None:
raise RuntimeError("There is no request in this context.")
if self._session is None:
si = self.app.session_interface
self._session = si.open_session(self.app, self.request)
if self._session is None:
self._session = si.make_null_session(self.app)
return self._session
@property
def session(self) -> SessionMixin:
"""The session object associated with this context. Accessed through
:data:`.session`. Only available in request contexts, otherwise raises
:exc:`RuntimeError`. Accessing this sets :attr:`.SessionMixin.accessed`.
"""
session = self._get_session()
session.accessed = True
return session
def match_request(self) -> None: def match_request(self) -> None:
"""Can be overridden by a subclass to hook into the matching """Apply routing to the current request, storing either the matched
of the request. endpoint and args, or a routing exception.
""" """
try: try:
result = self.url_adapter.match(return_rule=True) # type: ignore result = self.url_adapter.match(return_rule=True) # type: ignore[union-attr]
self.request.url_rule, self.request.view_args = result # type: ignore
except HTTPException as e: except HTTPException as e:
self.request.routing_exception = e self._request.routing_exception = e # type: ignore[union-attr]
else:
self._request.url_rule, self._request.view_args = result # type: ignore[union-attr]
def push(self) -> None: def push(self) -> None:
# Before we push the request context we have to ensure that there """Push this context so that it is the active context. If this is a
# is an application context. request context, calls :meth:`match_request` to perform routing with
app_ctx = _cv_app.get(None) the context active.
if app_ctx is None or app_ctx.app is not self.app: Typically, this is not used directly. Instead, use a ``with`` block
app_ctx = self.app.app_context() to manage the context.
app_ctx.push()
else:
app_ctx = None
self._cv_tokens.append((_cv_request.set(self), app_ctx)) In some situations, such as streaming or testing, the context may be
pushed multiple times. It will only trigger matching and signals if it
is not currently pushed.
"""
self._push_count += 1
if self._cv_token is not None:
return
self._cv_token = _cv_app.set(self)
appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
if self._request is not None:
# Open the session at the moment that the request context is available. # Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context. # This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was self._get_session()
# pushed, otherwise stream_with_context loses the session.
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(self.app, self.request)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
# Match the request URL after loading the session, so that the # Match the request URL after loading the session, so that the
# session is available in custom URL converters. # session is available in custom URL converters.
if self.url_adapter is not None: if self.url_adapter is not None:
self.match_request() self.match_request()
def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore def pop(self, exc: BaseException | None = None) -> None:
"""Pops the request context and unbinds it by doing that. This will """Pop this context so that it is no longer the active context. Then
also trigger the execution of functions registered by the call teardown functions and signals.
:meth:`~flask.Flask.teardown_request` decorator.
Typically, this is not used directly. Instead, use a ``with`` block
to manage the context.
This context must currently be the active context, otherwise a
:exc:`RuntimeError` is raised. In some situations, such as streaming or
testing, the context may have been pushed multiple times. It will only
trigger cleanup once it has been popped as many times as it was pushed.
Until then, it will remain the active context.
:param exc: An unhandled exception that was raised while the context was
active. Passed to teardown functions.
.. versionchanged:: 0.9 .. versionchanged:: 0.9
Added the `exc` argument. Added the ``exc`` argument.
""" """
clear_request = len(self._cv_tokens) == 1 if self._cv_token is None:
raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.")
try: ctx = _cv_app.get(None)
if clear_request:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
request_close = getattr(self.request, "close", None) if ctx is None or self._cv_token is None:
if request_close is not None: raise RuntimeError(
request_close() f"Cannot pop this context ({self!r}), there is no active context."
finally:
ctx = _cv_request.get()
token, app_ctx = self._cv_tokens.pop()
_cv_request.reset(token)
# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
ctx.request.environ["werkzeug.request"] = None
if app_ctx is not None:
app_ctx.pop(exc)
if ctx is not self:
raise AssertionError(
f"Popped wrong request context. ({ctx!r} instead of {self!r})"
) )
def __enter__(self) -> RequestContext: if ctx is not self:
raise RuntimeError(
f"Cannot pop this context ({self!r}), it is not the active"
f" context ({ctx!r})."
)
self._push_count -= 1
if self._push_count > 0:
return
collect_errors = _CollectErrors()
if self._request is not None:
with collect_errors:
self.app.do_teardown_request(self, exc)
with collect_errors:
self._request.close()
with collect_errors:
self.app.do_teardown_appcontext(self, exc)
_cv_app.reset(self._cv_token)
self._cv_token = None
with collect_errors:
appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync)
collect_errors.raise_any("Errors during context teardown")
def __enter__(self) -> te.Self:
self.push() self.push()
return self return self
def __exit__( def __exit__(
self, self,
exc_type: type | None, exc_type: type[BaseException] | None,
exc_value: BaseException | None, exc_value: BaseException | None,
tb: TracebackType | None, tb: TracebackType | None,
) -> None: ) -> None:
self.pop(exc_value) self.pop(exc_value)
def __repr__(self) -> str: def __repr__(self) -> str:
if self._request is not None:
return ( return (
f"<{type(self).__name__} {self.request.url!r}" f"<{type(self).__name__} {id(self)} of {self.app.name},"
f" [{self.request.method}] of {self.app.name}>" f" {self.request.method} {self.request.url!r}>"
) )
return f"<{type(self).__name__} {id(self)} of {self.app.name}>"
def __getattr__(name: str) -> t.Any:
import warnings
if name == "RequestContext":
warnings.warn(
"'RequestContext' has merged with 'AppContext', and will be removed"
" in Flask 4.0. Use 'AppContext' instead.",
DeprecationWarning,
stacklevel=2,
)
return AppContext
raise AttributeError(name)

View file

@ -6,7 +6,7 @@ from jinja2.loaders import BaseLoader
from werkzeug.routing import RequestRedirect from werkzeug.routing import RequestRedirect
from .blueprints import Blueprint from .blueprints import Blueprint
from .globals import request_ctx from .globals import _cv_app
from .sansio.app import App from .sansio.app import App
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@ -136,8 +136,9 @@ def explain_template_loading_attempts(
info = [f"Locating template {template!r}:"] info = [f"Locating template {template!r}:"]
total_found = 0 total_found = 0
blueprint = None blueprint = None
if request_ctx and request_ctx.request.blueprint is not None:
blueprint = request_ctx.request.blueprint if (ctx := _cv_app.get(None)) is not None and ctx.has_request:
blueprint = ctx.request.blueprint
for idx, (loader, srcobj, triple) in enumerate(attempts): for idx, (loader, srcobj, triple) in enumerate(attempts):
if isinstance(srcobj, App): if isinstance(srcobj, App):

View file

@ -9,43 +9,69 @@ if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask from .app import Flask
from .ctx import _AppCtxGlobals from .ctx import _AppCtxGlobals
from .ctx import AppContext from .ctx import AppContext
from .ctx import RequestContext
from .sessions import SessionMixin from .sessions import SessionMixin
from .wrappers import Request from .wrappers import Request
T = t.TypeVar("T", covariant=True)
class ProxyMixin(t.Protocol[T]):
def _get_current_object(self) -> T: ...
# These subclasses inform type checkers that the proxy objects look like the
# proxied type along with the _get_current_object method.
class FlaskProxy(ProxyMixin[Flask], Flask): ...
class AppContextProxy(ProxyMixin[AppContext], AppContext): ...
class _AppCtxGlobalsProxy(ProxyMixin[_AppCtxGlobals], _AppCtxGlobals): ...
class RequestProxy(ProxyMixin[Request], Request): ...
class SessionMixinProxy(ProxyMixin[SessionMixin], SessionMixin): ...
_no_app_msg = """\ _no_app_msg = """\
Working outside of application context. Working outside of application context.
This typically means that you attempted to use functionality that needed Attempted to use functionality that expected a current application to be set. To
the current application. To solve this, set up an application context solve this, set up an app context using 'with app.app_context()'. See the
with app.app_context(). See the documentation for more information.\ documentation on app context for more information.\
""" """
_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") _cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
app_ctx: AppContext = LocalProxy( # type: ignore[assignment] app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment]
_cv_app, unbound_message=_no_app_msg _cv_app, unbound_message=_no_app_msg
) )
current_app: Flask = LocalProxy( # type: ignore[assignment] current_app: FlaskProxy = LocalProxy( # type: ignore[assignment]
_cv_app, "app", unbound_message=_no_app_msg _cv_app, "app", unbound_message=_no_app_msg
) )
g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment]
_cv_app, "g", unbound_message=_no_app_msg _cv_app, "g", unbound_message=_no_app_msg
) )
_no_req_msg = """\ _no_req_msg = """\
Working outside of request context. Working outside of request context.
This typically means that you attempted to use functionality that needed Attempted to use functionality that expected an active HTTP request. See the
an active HTTP request. Consult the documentation on testing for documentation on request context for more information.\
information about how to avoid this problem.\
""" """
_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") request: RequestProxy = LocalProxy( # type: ignore[assignment]
request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] _cv_app, "request", unbound_message=_no_req_msg
_cv_request, unbound_message=_no_req_msg
) )
request: Request = LocalProxy( # type: ignore[assignment] session: SessionMixinProxy = LocalProxy( # type: ignore[assignment]
_cv_request, "request", unbound_message=_no_req_msg _cv_app, "session", unbound_message=_no_req_msg
) )
session: SessionMixin = LocalProxy( # type: ignore[assignment]
_cv_request, "session", unbound_message=_no_req_msg
def __getattr__(name: str) -> t.Any:
import warnings
if name == "request_ctx":
warnings.warn(
"'request_ctx' has merged with 'app_ctx', and will be removed"
" in Flask 4.0. Use 'app_ctx' instead.",
DeprecationWarning,
stacklevel=2,
) )
return app_ctx
raise AttributeError(name)

View file

@ -7,16 +7,17 @@ import typing as t
from datetime import datetime from datetime import datetime
from functools import cache from functools import cache
from functools import update_wrapper from functools import update_wrapper
from types import TracebackType
import werkzeug.utils import werkzeug.utils
from werkzeug.exceptions import abort as _wz_abort from werkzeug.exceptions import abort as _wz_abort
from werkzeug.utils import redirect as _wz_redirect from werkzeug.utils import redirect as _wz_redirect
from werkzeug.wrappers import Response as BaseResponse from werkzeug.wrappers import Response as BaseResponse
from .globals import _cv_request from .globals import _cv_app
from .globals import app_ctx
from .globals import current_app from .globals import current_app
from .globals import request from .globals import request
from .globals import request_ctx
from .globals import session from .globals import session
from .signals import message_flashed from .signals import message_flashed
@ -62,35 +63,52 @@ def stream_with_context(
def stream_with_context( def stream_with_context(
generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]],
) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]:
"""Request contexts disappear when the response is started on the server. """Wrap a response generator function so that it runs inside the current
This is done for efficiency reasons and to make it less likely to encounter request context. This keeps :data:`.request`, :data:`.session`, and :data:`.g`
memory leaks with badly written WSGI middlewares. The downside is that if available, even though at the point the generator runs the request context
you are using streamed responses, the generator cannot access request bound will typically have ended.
information any more.
This function however can help you keep the context around for longer:: .. warning::
Due to the following caveat, it is often safer to pass the data you
need as arguments to the generator, rather than relying on the context
objects.
More headers cannot be sent after the body has begun. Therefore, you must
make sure all headers are set before starting the response. In particular,
if the generator will access ``session``, be sure to do so in the view as
well so that the ``Vary: cookie`` header will be set. Do not modify the
session in the generator, as the ``Set-Cookie`` header will already be sent.
Use it as a decorator on a generator function:
.. code-block:: python
from flask import stream_with_context, request, Response from flask import stream_with_context, request, Response
@app.route('/stream') @app.get("/stream")
def streamed_response(): def streamed_response():
@stream_with_context @stream_with_context
def generate(): def generate():
yield 'Hello ' yield "Hello "
yield request.args['name'] yield request.args["name"]
yield '!' yield "!"
return Response(generate()) return Response(generate())
Alternatively it can also be used around a specific generator:: Or use it as a wrapper around a created generator:
.. code-block:: python
from flask import stream_with_context, request, Response from flask import stream_with_context, request, Response
@app.route('/stream') @app.get("/stream")
def streamed_response(): def streamed_response():
def generate(): def generate():
yield 'Hello ' yield "Hello "
yield request.args['name'] yield request.args["name"]
yield '!' yield "!"
return Response(stream_with_context(generate())) return Response(stream_with_context(generate()))
.. versionadded:: 0.9 .. versionadded:: 0.9
@ -105,35 +123,29 @@ def stream_with_context(
return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type]
def generator() -> t.Iterator[t.AnyStr | None]: def generator() -> t.Iterator[t.AnyStr]:
ctx = _cv_request.get(None) if (ctx := _cv_app.get(None)) is None:
if ctx is None:
raise RuntimeError( raise RuntimeError(
"'stream_with_context' can only be used when a request" "'stream_with_context' can only be used when a request"
" context is active, such as in a view function." " context is active, such as in a view function."
) )
with ctx:
# Dummy sentinel. Has to be inside the context block or we're
# not actually keeping the context around.
yield None
# The try/finally is here so that if someone passes a WSGI level with ctx:
# iterator in we're still running the cleanup logic. Generators yield None # type: ignore[misc]
# don't need that because they are closed on their destruction
# automatically.
try: try:
yield from gen yield from gen
finally: finally:
# Clean up in case the user wrapped a WSGI iterator.
if hasattr(gen, "close"): if hasattr(gen, "close"):
gen.close() gen.close()
# The trick is to start the generator. Then the code execution runs until # Execute the generator to the sentinel value. This captures the current
# the first dummy None is yielded at which point the context was already # context and pushes it to preserve it. Further iteration will yield from
# pushed. This item is discarded. Then when the iteration continues the # the original iterator.
# real generator is executed.
wrapped_g = generator() wrapped_g = generator()
next(wrapped_g) next(wrapped_g)
return wrapped_g # type: ignore[return-value] return wrapped_g
def make_response(*args: t.Any) -> Response: def make_response(*args: t.Any) -> Response:
@ -240,7 +252,7 @@ def url_for(
def redirect( def redirect(
location: str, code: int = 302, Response: type[BaseResponse] | None = None location: str, code: int = 303, Response: type[BaseResponse] | None = None
) -> BaseResponse: ) -> BaseResponse:
"""Create a redirect response object. """Create a redirect response object.
@ -253,12 +265,15 @@ def redirect(
:param Response: The response class to use. Not used when :param Response: The response class to use. Not used when
``current_app`` is active, which uses ``app.response_class``. ``current_app`` is active, which uses ``app.response_class``.
.. versionchanged:: 3.2
``code`` defaults to ``303`` instead of ``302``.
.. versionadded:: 2.2 .. versionadded:: 2.2
Calls ``current_app.redirect`` if available instead of always Calls ``current_app.redirect`` if available instead of always
using Werkzeug's default ``redirect``. using Werkzeug's default ``redirect``.
""" """
if current_app: if (ctx := _cv_app.get(None)) is not None:
return current_app.redirect(location, code=code) return ctx.app.redirect(location, code=code)
return _wz_redirect(location, code=code, Response=Response) return _wz_redirect(location, code=code, Response=Response)
@ -280,8 +295,8 @@ def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn
Calls ``current_app.aborter`` if available instead of always Calls ``current_app.aborter`` if available instead of always
using Werkzeug's default ``abort``. using Werkzeug's default ``abort``.
""" """
if current_app: if (ctx := _cv_app.get(None)) is not None:
current_app.aborter(code, *args, **kwargs) ctx.app.aborter(code, *args, **kwargs)
_wz_abort(code, *args, **kwargs) _wz_abort(code, *args, **kwargs)
@ -333,7 +348,7 @@ def flash(message: str, category: str = "message") -> None:
flashes = session.get("_flashes", []) flashes = session.get("_flashes", [])
flashes.append((category, message)) flashes.append((category, message))
session["_flashes"] = flashes session["_flashes"] = flashes
app = current_app._get_current_object() # type: ignore app = current_app._get_current_object()
message_flashed.send( message_flashed.send(
app, app,
_async_wrapper=app.ensure_sync, _async_wrapper=app.ensure_sync,
@ -373,10 +388,10 @@ def get_flashed_messages(
:param category_filter: filter of categories to limit return values. Only :param category_filter: filter of categories to limit return values. Only
categories in the list will be returned. categories in the list will be returned.
""" """
flashes = request_ctx.flashes flashes = app_ctx._flashes
if flashes is None: if flashes is None:
flashes = session.pop("_flashes") if "_flashes" in session else [] flashes = session.pop("_flashes") if "_flashes" in session else []
request_ctx.flashes = flashes app_ctx._flashes = flashes
if category_filter: if category_filter:
flashes = list(filter(lambda f: f[0] in category_filter, flashes)) flashes = list(filter(lambda f: f[0] in category_filter, flashes))
if not with_categories: if not with_categories:
@ -385,20 +400,22 @@ def get_flashed_messages(
def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]:
ctx = app_ctx._get_current_object()
if kwargs.get("max_age") is None: if kwargs.get("max_age") is None:
kwargs["max_age"] = current_app.get_send_file_max_age kwargs["max_age"] = ctx.app.get_send_file_max_age
kwargs.update( kwargs.update(
environ=request.environ, environ=ctx.request.environ,
use_x_sendfile=current_app.config["USE_X_SENDFILE"], use_x_sendfile=ctx.app.config["USE_X_SENDFILE"],
response_class=current_app.response_class, response_class=ctx.app.response_class,
_root_path=current_app.root_path, # type: ignore _root_path=ctx.app.root_path,
) )
return kwargs return kwargs
def send_file( def send_file(
path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes],
mimetype: str | None = None, mimetype: str | None = None,
as_attachment: bool = False, as_attachment: bool = False,
download_name: str | None = None, download_name: str | None = None,
@ -632,3 +649,34 @@ def _split_blueprint_path(name: str) -> list[str]:
out.extend(_split_blueprint_path(name.rpartition(".")[0])) out.extend(_split_blueprint_path(name.rpartition(".")[0]))
return out return out
class _CollectErrors:
"""A context manager that records and silences an error raised within it.
Used to run all teardown functions, then raise any errors afterward.
"""
def __init__(self) -> None:
self.errors: list[BaseException] = []
def __enter__(self) -> None:
pass
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> bool:
if exc_val is not None:
self.errors.append(exc_val)
return True
def raise_any(self, message: str) -> None:
"""Raise if any errors were collected."""
if self.errors:
if sys.version_info >= (3, 11):
raise BaseExceptionGroup(message, self.errors) # noqa: F821
else:
raise self.errors[0]

View file

@ -141,7 +141,7 @@ def jsonify(*args: t.Any, **kwargs: t.Any) -> Response:
mimetype. A dict or list returned from a view will be converted to a mimetype. A dict or list returned from a view will be converted to a
JSON response automatically without needing to call this. JSON response automatically without needing to call this.
This requires an active request or application context, and calls This requires an active app context, and calls
:meth:`app.json.response() <flask.json.provider.JSONProvider.response>`. :meth:`app.json.response() <flask.json.provider.JSONProvider.response>`.
In debug mode, the output is formatted with indentation to make it In debug mode, the output is formatted with indentation to make it

View file

@ -135,7 +135,7 @@ class DefaultJSONProvider(JSONProvider):
method) will call the ``__html__`` method to get a string. method) will call the ``__html__`` method to get a string.
""" """
default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] default: t.Callable[[t.Any], t.Any] = staticmethod(_default)
"""Apply this function to any object that :meth:`json.dumps` does """Apply this function to any object that :meth:`json.dumps` does
not know how to serialize. It should return a valid JSON type or not know how to serialize. It should return a valid JSON type or
raise a ``TypeError``. raise a ``TypeError``.

View file

@ -177,11 +177,8 @@ class App(Scaffold):
#: 3. Return None instead of AttributeError on unexpected attributes. #: 3. Return None instead of AttributeError on unexpected attributes.
#: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g.
#: #:
#: In Flask 0.9 this property was called `request_globals_class` but it
#: was changed in 0.10 to :attr:`app_ctx_globals_class` because the
#: flask.g object is now application context scoped.
#:
#: .. versionadded:: 0.10 #: .. versionadded:: 0.10
#: Renamed from ``request_globals_class`.
app_ctx_globals_class = _AppCtxGlobals app_ctx_globals_class = _AppCtxGlobals
#: The class that is used for the ``config`` attribute of this app. #: The class that is used for the ``config`` attribute of this app.
@ -213,7 +210,7 @@ class App(Scaffold):
#: #:
#: This attribute can also be configured from the config with the #: This attribute can also be configured from the config with the
#: :data:`SECRET_KEY` configuration key. Defaults to ``None``. #: :data:`SECRET_KEY` configuration key. Defaults to ``None``.
secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") secret_key = ConfigAttribute[str | bytes | None]("SECRET_KEY")
#: A :class:`~datetime.timedelta` which is used to set the expiration #: A :class:`~datetime.timedelta` which is used to set the expiration
#: date of a permanent session. The default is 31 days which makes a #: date of a permanent session. The default is 31 days which makes a
@ -423,7 +420,7 @@ class App(Scaffold):
) )
@cached_property @cached_property
def name(self) -> str: # type: ignore def name(self) -> str:
"""The name of the application. This is usually the import name """The name of the application. This is usually the import name
with the difference that it's guessed from the run file if the with the difference that it's guessed from the run file if the
import name is main. This name is used as a display name when import name is main. This name is used as a display name when
@ -521,7 +518,7 @@ class App(Scaffold):
return os.path.join(prefix, "var", f"{self.name}-instance") return os.path.join(prefix, "var", f"{self.name}-instance")
def create_global_jinja_loader(self) -> DispatchingJinjaLoader: def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
"""Creates the loader for the Jinja2 environment. Can be used to """Creates the loader for the Jinja environment. Can be used to
override just the loader and keeping the rest unchanged. It's override just the loader and keeping the rest unchanged. It's
discouraged to override this function. Instead one should override discouraged to override this function. Instead one should override
the :meth:`jinja_loader` function instead. the :meth:`jinja_loader` function instead.
@ -533,10 +530,13 @@ class App(Scaffold):
""" """
return DispatchingJinjaLoader(self) return DispatchingJinjaLoader(self)
def select_jinja_autoescape(self, filename: str) -> bool: def select_jinja_autoescape(self, filename: str | None) -> bool:
"""Returns ``True`` if autoescaping should be active for the given """Returns ``True`` if autoescaping should be active for the given
template name. If no template name is given, returns `True`. template name. If no template name is given, returns `True`.
.. versionchanged:: 3.2
Use case-insensitive comparison instead of only lower case.
.. versionchanged:: 2.2 .. versionchanged:: 2.2
Autoescaping is now enabled by default for ``.svg`` files. Autoescaping is now enabled by default for ``.svg`` files.
@ -544,7 +544,7 @@ class App(Scaffold):
""" """
if filename is None: if filename is None:
return True return True
return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) return filename.lower().endswith((".html", ".htm", ".xml", ".xhtml", ".svg"))
@property @property
def debug(self) -> bool: def debug(self) -> bool:
@ -630,19 +630,19 @@ class App(Scaffold):
# Methods that should always be added # Methods that should always be added
required_methods: set[str] = set(getattr(view_func, "required_methods", ())) required_methods: set[str] = set(getattr(view_func, "required_methods", ()))
# starting with Flask 0.8 the view_func object can disable and
# force-enable the automatic options handling.
if provide_automatic_options is None: if provide_automatic_options is None:
provide_automatic_options = getattr( provide_automatic_options = getattr(
view_func, "provide_automatic_options", None view_func, "provide_automatic_options", None
) )
if provide_automatic_options is None: if provide_automatic_options is None:
if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]: provide_automatic_options = (
provide_automatic_options = True "OPTIONS" not in methods
and self.config["PROVIDE_AUTOMATIC_OPTIONS"]
)
if provide_automatic_options:
required_methods.add("OPTIONS") required_methods.add("OPTIONS")
else:
provide_automatic_options = False
# Add the required methods now. # Add the required methods now.
methods |= required_methods methods |= required_methods
@ -660,21 +660,34 @@ class App(Scaffold):
) )
self.view_functions[endpoint] = view_func self.view_functions[endpoint] = view_func
@setupmethod @t.overload
def template_filter(self, name: T_template_filter) -> T_template_filter: ...
@t.overload
def template_filter( def template_filter(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_filter], T_template_filter]: ) -> t.Callable[[T_template_filter], T_template_filter]: ...
"""A decorator that is used to register custom template filter. @setupmethod
You can specify a name for the filter, otherwise the function def template_filter(
name will be used. Example:: self, name: T_template_filter | str | None = None
) -> T_template_filter | t.Callable[[T_template_filter], T_template_filter]:
"""Decorate a function to register it as a custom Jinja filter. The name
is optional. The decorator may be used without parentheses.
@app.template_filter() .. code-block:: python
def reverse(s):
return s[::-1]
:param name: the optional name of the filter, otherwise the @app.template_filter("reverse")
function name will be used. def reverse_filter(s):
return reversed(s)
The :meth:`add_template_filter` method may be used to register a
function later rather than decorating.
:param name: The name to register the filter as. If not given, uses the
function's name.
""" """
if callable(name):
self.add_template_filter(name)
return name
def decorator(f: T_template_filter) -> T_template_filter: def decorator(f: T_template_filter) -> T_template_filter:
self.add_template_filter(f, name=name) self.add_template_filter(f, name=name)
@ -686,24 +699,34 @@ class App(Scaffold):
def add_template_filter( def add_template_filter(
self, f: ft.TemplateFilterCallable, name: str | None = None self, f: ft.TemplateFilterCallable, name: str | None = None
) -> None: ) -> None:
"""Register a custom template filter. Works exactly like the """Register a function to use as a custom Jinja filter.
:meth:`template_filter` decorator.
:param name: the optional name of the filter, otherwise the The :meth:`template_filter` decorator can be used to register a function
function name will be used. by decorating instead.
:param f: The function to register.
:param name: The name to register the filter as. If not given, uses the
function's name.
""" """
self.jinja_env.filters[name or f.__name__] = f self.jinja_env.filters[name or f.__name__] = f
@setupmethod @t.overload
def template_test(self, name: T_template_test) -> T_template_test: ...
@t.overload
def template_test( def template_test(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_test], T_template_test]: ) -> t.Callable[[T_template_test], T_template_test]: ...
"""A decorator that is used to register custom template test. @setupmethod
You can specify a name for the test, otherwise the function def template_test(
name will be used. Example:: self, name: T_template_test | str | None = None
) -> T_template_test | t.Callable[[T_template_test], T_template_test]:
"""Decorate a function to register it as a custom Jinja test. The name
is optional. The decorator may be used without parentheses.
@app.template_test() .. code-block:: python
def is_prime(n):
@app.template_test("prime")
def is_prime_test(n):
if n == 2: if n == 2:
return True return True
for i in range(2, int(math.ceil(math.sqrt(n))) + 1): for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
@ -711,11 +734,17 @@ class App(Scaffold):
return False return False
return True return True
.. versionadded:: 0.10 The :meth:`add_template_test` method may be used to register a function
later rather than decorating.
:param name: the optional name of the test, otherwise the :param name: The name to register the filter as. If not given, uses the
function name will be used. function's name.
.. versionadded:: 0.10
""" """
if callable(name):
self.add_template_test(name)
return name
def decorator(f: T_template_test) -> T_template_test: def decorator(f: T_template_test) -> T_template_test:
self.add_template_test(f, name=name) self.add_template_test(f, name=name)
@ -727,33 +756,49 @@ class App(Scaffold):
def add_template_test( def add_template_test(
self, f: ft.TemplateTestCallable, name: str | None = None self, f: ft.TemplateTestCallable, name: str | None = None
) -> None: ) -> None:
"""Register a custom template test. Works exactly like the """Register a function to use as a custom Jinja test.
:meth:`template_test` decorator.
The :meth:`template_test` decorator can be used to register a function
by decorating instead.
:param f: The function to register.
:param name: The name to register the test as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the test, otherwise the
function name will be used.
""" """
self.jinja_env.tests[name or f.__name__] = f self.jinja_env.tests[name or f.__name__] = f
@setupmethod @t.overload
def template_global(self, name: T_template_global) -> T_template_global: ...
@t.overload
def template_global( def template_global(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_global], T_template_global]: ) -> t.Callable[[T_template_global], T_template_global]: ...
"""A decorator that is used to register a custom template global function. @setupmethod
You can specify a name for the global function, otherwise the function def template_global(
name will be used. Example:: self, name: T_template_global | str | None = None
) -> T_template_global | t.Callable[[T_template_global], T_template_global]:
"""Decorate a function to register it as a custom Jinja global. The name
is optional. The decorator may be used without parentheses.
@app.template_global() .. code-block:: python
@app.template_global
def double(n): def double(n):
return 2 * n return 2 * n
.. versionadded:: 0.10 The :meth:`add_template_global` method may be used to register a
function later rather than decorating.
:param name: the optional name of the global function, otherwise the :param name: The name to register the global as. If not given, uses the
function name will be used. function's name.
.. versionadded:: 0.10
""" """
if callable(name):
self.add_template_global(name)
return name
def decorator(f: T_template_global) -> T_template_global: def decorator(f: T_template_global) -> T_template_global:
self.add_template_global(f, name=name) self.add_template_global(f, name=name)
@ -765,22 +810,24 @@ class App(Scaffold):
def add_template_global( def add_template_global(
self, f: ft.TemplateGlobalCallable, name: str | None = None self, f: ft.TemplateGlobalCallable, name: str | None = None
) -> None: ) -> None:
"""Register a custom template global function. Works exactly like the """Register a function to use as a custom Jinja global.
:meth:`template_global` decorator.
The :meth:`template_global` decorator can be used to register a function
by decorating instead.
:param f: The function to register.
:param name: The name to register the global as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the global function, otherwise the
function name will be used.
""" """
self.jinja_env.globals[name or f.__name__] = f self.jinja_env.globals[name or f.__name__] = f
@setupmethod @setupmethod
def teardown_appcontext(self, f: T_teardown) -> T_teardown: def teardown_appcontext(self, f: T_teardown) -> T_teardown:
"""Registers a function to be called when the application """Registers a function to be called when the app context is popped. The
context is popped. The application context is typically popped context is popped at the end of a request, CLI command, or manual ``with``
after the request context for each request, at the end of CLI block.
commands, or after a manually pushed context ends.
.. code-block:: python .. code-block:: python
@ -789,9 +836,7 @@ class App(Scaffold):
When the ``with`` block exits (or ``ctx.pop()`` is called), the When the ``with`` block exits (or ``ctx.pop()`` is called), the
teardown functions are called just before the app context is teardown functions are called just before the app context is
made inactive. Since a request context typically also manages an made inactive.
application context it would also be called when you pop a
request context.
When a teardown function was called because of an unhandled When a teardown function was called because of an unhandled
exception it will be passed an error object. If an exception it will be passed an error object. If an
@ -880,17 +925,18 @@ class App(Scaffold):
return False return False
def should_ignore_error(self, error: BaseException | None) -> bool: should_ignore_error: None = None
"""This is called to figure out if an error should be ignored """If this method returns ``True``, the error will not be passed to
or not as far as the teardown system is concerned. If this teardown handlers, and the context will not be preserved for
function returns ``True`` then the teardown handlers will not be debugging.
passed the error.
.. deprecated:: 3.2
Handle errors as needed in teardown handlers instead.
.. versionadded:: 0.10 .. versionadded:: 0.10
""" """
return False
def redirect(self, location: str, code: int = 302) -> BaseResponse: def redirect(self, location: str, code: int = 303) -> BaseResponse:
"""Create a redirect response object. """Create a redirect response object.
This is called by :func:`flask.redirect`, and can be called This is called by :func:`flask.redirect`, and can be called
@ -899,6 +945,9 @@ class App(Scaffold):
:param location: The URL to redirect to. :param location: The URL to redirect to.
:param code: The status code for the redirect. :param code: The status code for the redirect.
.. versionchanged:: 3.2
``code`` defaults to ``303`` instead of ``302``.
.. versionadded:: 2.2 .. versionadded:: 2.2
Moved from ``flask.redirect``, which calls this method. Moved from ``flask.redirect``, which calls this method.
""" """

View file

@ -440,16 +440,31 @@ class Blueprint(Scaffold):
) )
) )
@setupmethod @t.overload
def app_template_filter(self, name: T_template_filter) -> T_template_filter: ...
@t.overload
def app_template_filter( def app_template_filter(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_filter], T_template_filter]: ) -> t.Callable[[T_template_filter], T_template_filter]: ...
"""Register a template filter, available in any template rendered by the @setupmethod
application. Equivalent to :meth:`.Flask.template_filter`. def app_template_filter(
self, name: T_template_filter | str | None = None
) -> T_template_filter | t.Callable[[T_template_filter], T_template_filter]:
"""Decorate a function to register it as a custom Jinja filter. The name
is optional. The decorator may be used without parentheses.
:param name: the optional name of the filter, otherwise the The :meth:`add_app_template_filter` method may be used to register a
function name will be used. function later rather than decorating.
The filter is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.template_filter`.
:param name: The name to register the filter as. If not given, uses the
function's name.
""" """
if callable(name):
self.add_app_template_filter(name)
return name
def decorator(f: T_template_filter) -> T_template_filter: def decorator(f: T_template_filter) -> T_template_filter:
self.add_app_template_filter(f, name=name) self.add_app_template_filter(f, name=name)
@ -461,31 +476,51 @@ class Blueprint(Scaffold):
def add_app_template_filter( def add_app_template_filter(
self, f: ft.TemplateFilterCallable, name: str | None = None self, f: ft.TemplateFilterCallable, name: str | None = None
) -> None: ) -> None:
"""Register a template filter, available in any template rendered by the """Register a function to use as a custom Jinja filter.
application. Works like the :meth:`app_template_filter` decorator. Equivalent to
:meth:`.Flask.add_template_filter`.
:param name: the optional name of the filter, otherwise the The :meth:`app_template_filter` decorator can be used to register a
function name will be used. function by decorating instead.
The filter is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.add_template_filter`.
:param f: The function to register.
:param name: The name to register the filter as. If not given, uses the
function's name.
""" """
def register_template(state: BlueprintSetupState) -> None: def register_template_filter(state: BlueprintSetupState) -> None:
state.app.jinja_env.filters[name or f.__name__] = f state.app.add_template_filter(f, name=name)
self.record_once(register_template) self.record_once(register_template_filter)
@setupmethod @t.overload
def app_template_test(self, name: T_template_test) -> T_template_test: ...
@t.overload
def app_template_test( def app_template_test(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_test], T_template_test]: ) -> t.Callable[[T_template_test], T_template_test]: ...
"""Register a template test, available in any template rendered by the @setupmethod
application. Equivalent to :meth:`.Flask.template_test`. def app_template_test(
self, name: T_template_test | str | None = None
) -> T_template_test | t.Callable[[T_template_test], T_template_test]:
"""Decorate a function to register it as a custom Jinja test. The name
is optional. The decorator may be used without parentheses.
The :meth:`add_app_template_test` method may be used to register a
function later rather than decorating.
The test is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.template_test`.
:param name: The name to register the filter as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the test, otherwise the
function name will be used.
""" """
if callable(name):
self.add_app_template_test(name)
return name
def decorator(f: T_template_test) -> T_template_test: def decorator(f: T_template_test) -> T_template_test:
self.add_app_template_test(f, name=name) self.add_app_template_test(f, name=name)
@ -497,33 +532,53 @@ class Blueprint(Scaffold):
def add_app_template_test( def add_app_template_test(
self, f: ft.TemplateTestCallable, name: str | None = None self, f: ft.TemplateTestCallable, name: str | None = None
) -> None: ) -> None:
"""Register a template test, available in any template rendered by the """Register a function to use as a custom Jinja test.
application. Works like the :meth:`app_template_test` decorator. Equivalent to
:meth:`.Flask.add_template_test`. The :meth:`app_template_test` decorator can be used to register a
function by decorating instead.
The test is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.add_template_test`.
:param f: The function to register.
:param name: The name to register the test as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the test, otherwise the
function name will be used.
""" """
def register_template(state: BlueprintSetupState) -> None: def register_template_test(state: BlueprintSetupState) -> None:
state.app.jinja_env.tests[name or f.__name__] = f state.app.add_template_test(f, name=name)
self.record_once(register_template) self.record_once(register_template_test)
@setupmethod @t.overload
def app_template_global(self, name: T_template_global) -> T_template_global: ...
@t.overload
def app_template_global( def app_template_global(
self, name: str | None = None self, name: str | None = None
) -> t.Callable[[T_template_global], T_template_global]: ) -> t.Callable[[T_template_global], T_template_global]: ...
"""Register a template global, available in any template rendered by the @setupmethod
application. Equivalent to :meth:`.Flask.template_global`. def app_template_global(
self, name: T_template_global | str | None = None
) -> T_template_global | t.Callable[[T_template_global], T_template_global]:
"""Decorate a function to register it as a custom Jinja global. The name
is optional. The decorator may be used without parentheses.
The :meth:`add_app_template_global` method may be used to register a
function later rather than decorating.
The global is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.template_global`.
:param name: The name to register the global as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the global, otherwise the
function name will be used.
""" """
if callable(name):
self.add_app_template_global(name)
return name
def decorator(f: T_template_global) -> T_template_global: def decorator(f: T_template_global) -> T_template_global:
self.add_app_template_global(f, name=name) self.add_app_template_global(f, name=name)
@ -535,20 +590,25 @@ class Blueprint(Scaffold):
def add_app_template_global( def add_app_template_global(
self, f: ft.TemplateGlobalCallable, name: str | None = None self, f: ft.TemplateGlobalCallable, name: str | None = None
) -> None: ) -> None:
"""Register a template global, available in any template rendered by the """Register a function to use as a custom Jinja global.
application. Works like the :meth:`app_template_global` decorator. Equivalent to
:meth:`.Flask.add_template_global`. The :meth:`app_template_global` decorator can be used to register a function
by decorating instead.
The global is available in all templates, not only those under this
blueprint. Equivalent to :meth:`.Flask.add_template_global`.
:param f: The function to register.
:param name: The name to register the global as. If not given, uses the
function's name.
.. versionadded:: 0.10 .. versionadded:: 0.10
:param name: the optional name of the global, otherwise the
function name will be used.
""" """
def register_template(state: BlueprintSetupState) -> None: def register_template_global(state: BlueprintSetupState) -> None:
state.app.jinja_env.globals[name or f.__name__] = f state.app.add_template_global(f, name=name)
self.record_once(register_template) self.record_once(register_template_global)
@setupmethod @setupmethod
def before_app_request(self, f: T_before_request) -> T_before_request: def before_app_request(self, f: T_before_request) -> T_before_request:

View file

@ -84,7 +84,7 @@ class Scaffold:
#: to. Do not change this once it is set by the constructor. #: to. Do not change this once it is set by the constructor.
self.import_name = import_name self.import_name = import_name
self.static_folder = static_folder # type: ignore self.static_folder = static_folder
self.static_url_path = static_url_path self.static_url_path = static_url_path
#: The path to the templates folder, relative to #: The path to the templates folder, relative to
@ -507,8 +507,8 @@ class Scaffold:
@setupmethod @setupmethod
def teardown_request(self, f: T_teardown) -> T_teardown: def teardown_request(self, f: T_teardown) -> T_teardown:
"""Register a function to be called when the request context is """Register a function to be called when the request context is
popped. Typically this happens at the end of each request, but popped. Typically, this happens at the end of each request, but
contexts may be pushed manually as well during testing. contexts may be pushed manually during testing.
.. code-block:: python .. code-block:: python

View file

@ -27,7 +27,7 @@ class SessionMixin(MutableMapping[str, t.Any]):
@property @property
def permanent(self) -> bool: def permanent(self) -> bool:
"""This reflects the ``'_permanent'`` key in the dict.""" """This reflects the ``'_permanent'`` key in the dict."""
return self.get("_permanent", False) return self.get("_permanent", False) # type: ignore[no-any-return]
@permanent.setter @permanent.setter
def permanent(self, value: bool) -> None: def permanent(self, value: bool) -> None:
@ -43,10 +43,15 @@ class SessionMixin(MutableMapping[str, t.Any]):
#: ``True``. #: ``True``.
modified = True modified = True
#: Some implementations can detect when session data is read or accessed = False
#: written and set this when that happens. The mixin default is hard """Indicates if the session was accessed, even if it was not modified. This
#: coded to ``True``. is set when the session object is accessed through the request context,
accessed = True including the global :data:`.session` proxy. A ``Vary: cookie`` header will
be added if this is ``True``.
.. versionchanged:: 3.1.3
This is tracked by the request context.
"""
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
#: will only be written to the response if this is ``True``. #: will only be written to the response if this is ``True``.
modified = False modified = False
#: When data is read or written, this is set to ``True``. Used by
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
#: header, which allows caching proxies to cache different pages for
#: different users.
accessed = False
def __init__( def __init__(
self, self,
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, initial: c.Mapping[str, t.Any] | None = None,
) -> None: ) -> None:
def on_update(self: te.Self) -> None: def on_update(self: te.Self) -> None:
self.modified = True self.modified = True
self.accessed = True
super().__init__(initial, on_update) super().__init__(initial, on_update)
def __getitem__(self, key: str) -> t.Any:
self.accessed = True
return super().__getitem__(key)
def get(self, key: str, default: t.Any = None) -> t.Any:
self.accessed = True
return super().get(key, default)
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
self.accessed = True
return super().setdefault(key, default)
class NullSession(SecureCookieSession): class NullSession(SecureCookieSession):
"""Class used to generate nicer error messages if sessions are not """Class used to generate nicer error messages if sessions are not
@ -107,7 +93,7 @@ class NullSession(SecureCookieSession):
"application to something unique and secret." "application to something unique and secret."
) )
__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail
del _fail del _fail
@ -318,11 +304,12 @@ class SecureCookieSessionInterface(SessionInterface):
if not app.secret_key: if not app.secret_key:
return None return None
keys: list[str | bytes] = [app.secret_key] keys: list[str | bytes] = []
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
keys.extend(fallbacks) keys.extend(fallbacks)
keys.append(app.secret_key) # itsdangerous expects current key at top
return URLSafeTimedSerializer( return URLSafeTimedSerializer(
keys, # type: ignore[arg-type] keys, # type: ignore[arg-type]
salt=self.salt, salt=self.salt,

View file

@ -7,37 +7,34 @@ from jinja2 import Environment as BaseEnvironment
from jinja2 import Template from jinja2 import Template
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
from .globals import _cv_app from .ctx import AppContext
from .globals import _cv_request from .globals import app_ctx
from .globals import current_app
from .globals import request
from .helpers import stream_with_context from .helpers import stream_with_context
from .signals import before_render_template from .signals import before_render_template
from .signals import template_rendered from .signals import template_rendered
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask
from .sansio.app import App from .sansio.app import App
from .sansio.scaffold import Scaffold from .sansio.scaffold import Scaffold
def _default_template_ctx_processor() -> dict[str, t.Any]: def _default_template_ctx_processor() -> dict[str, t.Any]:
"""Default template context processor. Injects `request`, """Default template context processor. Replaces the ``request`` and ``g``
`session` and `g`. proxies with their concrete objects for faster access.
""" """
appctx = _cv_app.get(None) ctx = app_ctx._get_current_object()
reqctx = _cv_request.get(None) rv: dict[str, t.Any] = {"g": ctx.g}
rv: dict[str, t.Any] = {}
if appctx is not None: if ctx.has_request:
rv["g"] = appctx.g rv["request"] = ctx.request
if reqctx is not None: # The session proxy cannot be replaced, accessing it gets
rv["request"] = reqctx.request # RequestContext.session, which sets session.accessed.
rv["session"] = reqctx.session
return rv return rv
class Environment(BaseEnvironment): class Environment(BaseEnvironment):
"""Works like a regular Jinja2 environment but has some additional """Works like a regular Jinja environment but has some additional
knowledge of how Flask's blueprint works so that it can prepend the knowledge of how Flask's blueprint works so that it can prepend the
name of the blueprint to referenced templates if necessary. name of the blueprint to referenced templates if necessary.
""" """
@ -123,8 +120,9 @@ class DispatchingJinjaLoader(BaseLoader):
return list(result) return list(result)
def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: def _render(ctx: AppContext, template: Template, context: dict[str, t.Any]) -> str:
app.update_template_context(context) app = ctx.app
app.update_template_context(ctx, context)
before_render_template.send( before_render_template.send(
app, _async_wrapper=app.ensure_sync, template=template, context=context app, _async_wrapper=app.ensure_sync, template=template, context=context
) )
@ -145,9 +143,9 @@ def render_template(
a list is given, the first name to exist will be rendered. a list is given, the first name to exist will be rendered.
:param context: The variables to make available in the template. :param context: The variables to make available in the template.
""" """
app = current_app._get_current_object() # type: ignore[attr-defined] ctx = app_ctx._get_current_object()
template = app.jinja_env.get_or_select_template(template_name_or_list) template = ctx.app.jinja_env.get_or_select_template(template_name_or_list)
return _render(app, template, context) return _render(ctx, template, context)
def render_template_string(source: str, **context: t.Any) -> str: def render_template_string(source: str, **context: t.Any) -> str:
@ -157,15 +155,16 @@ def render_template_string(source: str, **context: t.Any) -> str:
:param source: The source code of the template to render. :param source: The source code of the template to render.
:param context: The variables to make available in the template. :param context: The variables to make available in the template.
""" """
app = current_app._get_current_object() # type: ignore[attr-defined] ctx = app_ctx._get_current_object()
template = app.jinja_env.from_string(source) template = ctx.app.jinja_env.from_string(source)
return _render(app, template, context) return _render(ctx, template, context)
def _stream( def _stream(
app: Flask, template: Template, context: dict[str, t.Any] ctx: AppContext, template: Template, context: dict[str, t.Any]
) -> t.Iterator[str]: ) -> t.Iterator[str]:
app.update_template_context(context) app = ctx.app
app.update_template_context(ctx, context)
before_render_template.send( before_render_template.send(
app, _async_wrapper=app.ensure_sync, template=template, context=context app, _async_wrapper=app.ensure_sync, template=template, context=context
) )
@ -176,13 +175,7 @@ def _stream(
app, _async_wrapper=app.ensure_sync, template=template, context=context app, _async_wrapper=app.ensure_sync, template=template, context=context
) )
rv = generate() return stream_with_context(generate())
# If a request context is active, keep it while generating.
if request:
rv = stream_with_context(rv)
return rv
def stream_template( def stream_template(
@ -199,9 +192,9 @@ def stream_template(
.. versionadded:: 2.2 .. versionadded:: 2.2
""" """
app = current_app._get_current_object() # type: ignore[attr-defined] ctx = app_ctx._get_current_object()
template = app.jinja_env.get_or_select_template(template_name_or_list) template = ctx.app.jinja_env.get_or_select_template(template_name_or_list)
return _stream(app, template, context) return _stream(ctx, template, context)
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
@ -214,6 +207,6 @@ def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
.. versionadded:: 2.2 .. versionadded:: 2.2
""" """
app = current_app._get_current_object() # type: ignore[attr-defined] ctx = app_ctx._get_current_object()
template = app.jinja_env.from_string(source) template = ctx.app.jinja_env.from_string(source)
return _stream(app, template, context) return _stream(ctx, template, context)

View file

@ -58,9 +58,9 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
) -> None: ) -> None:
assert not (base_url or subdomain or url_scheme) or ( assert not (base_url or subdomain or url_scheme) or (
base_url is not None base_url is not None
) != bool( ) != bool(subdomain or url_scheme), (
subdomain or url_scheme 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' )
if base_url is None: if base_url is None:
http_host = app.config.get("SERVER_NAME") or "localhost" http_host = app.config.get("SERVER_NAME") or "localhost"
@ -85,7 +85,7 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
self.app = app self.app = app
super().__init__(path, base_url, *args, **kwargs) super().__init__(path, base_url, *args, **kwargs)
def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
"""Serialize ``obj`` to a JSON-formatted string. """Serialize ``obj`` to a JSON-formatted string.
The serialization will be configured according to the config associated The serialization will be configured according to the config associated
@ -107,10 +107,10 @@ def _get_werkzeug_version() -> str:
class FlaskClient(Client): class FlaskClient(Client):
"""Works like a regular Werkzeug test client but has knowledge about """Works like a regular Werkzeug test client, with additional behavior for
Flask's contexts to defer the cleanup of the request context until Flask. Can defer the cleanup of the request context until the end of a
the end of a ``with`` block. For general information about how to ``with`` block. For general information about how to use this class refer to
use this class refer to :class:`werkzeug.test.Client`. :class:`werkzeug.test.Client`.
.. versionchanged:: 0.12 .. versionchanged:: 0.12
`app.test_client()` includes preset default environment, which can be `app.test_client()` includes preset default environment, which can be
@ -240,10 +240,10 @@ class FlaskClient(Client):
response.json_module = self.application.json # type: ignore[assignment] response.json_module = self.application.json # type: ignore[assignment]
# Re-push contexts that were preserved during the request. # Re-push contexts that were preserved during the request.
while self._new_contexts: for cm in self._new_contexts:
cm = self._new_contexts.pop()
self._context_stack.enter_context(cm) self._context_stack.enter_context(cm)
self._new_contexts.clear()
return response return response
def __enter__(self) -> FlaskClient: def __enter__(self) -> FlaskClient:

View file

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import collections.abc as cabc
import typing as t import typing as t
if t.TYPE_CHECKING: # pragma: no cover if t.TYPE_CHECKING: # pragma: no cover
@ -17,11 +18,12 @@ ResponseValue = t.Union[
t.Mapping[str, t.Any], t.Mapping[str, t.Any],
t.Iterator[str], t.Iterator[str],
t.Iterator[bytes], t.Iterator[bytes],
cabc.AsyncIterable[str], # for Quart, until App is generic.
cabc.AsyncIterable[bytes],
] ]
# the possible types for an individual HTTP header # the possible types for an individual HTTP header
# This should be a Union, but mypy doesn't pass unless it's a TypeVar. HeaderValue = str | list[str] | tuple[str, ...]
HeaderValue = t.Union[str, list[str], tuple[str, ...]]
# the possible types for HTTP headers # the possible types for HTTP headers
HeadersValue = t.Union[ HeadersValue = t.Union[
@ -44,34 +46,29 @@ ResponseReturnValue = t.Union[
# callback annotated with flask.Response fail type checking. # callback annotated with flask.Response fail type checking.
ResponseClass = t.TypeVar("ResponseClass", bound="Response") ResponseClass = t.TypeVar("ResponseClass", bound="Response")
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Union[ AfterRequestCallable = (
t.Callable[[ResponseClass], ResponseClass], t.Callable[[ResponseClass], ResponseClass]
t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], | t.Callable[[ResponseClass], t.Awaitable[ResponseClass]]
] )
BeforeFirstRequestCallable = t.Union[ BeforeFirstRequestCallable = t.Callable[[], None] | t.Callable[[], t.Awaitable[None]]
t.Callable[[], None], t.Callable[[], t.Awaitable[None]] BeforeRequestCallable = (
] t.Callable[[], ResponseReturnValue | None]
BeforeRequestCallable = t.Union[ | t.Callable[[], t.Awaitable[ResponseReturnValue | None]]
t.Callable[[], t.Optional[ResponseReturnValue]], )
t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
]
ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]] ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
TeardownCallable = t.Union[ TeardownCallable = (
t.Callable[[t.Optional[BaseException]], None], t.Callable[[BaseException | None], None]
t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], | t.Callable[[BaseException | None], t.Awaitable[None]]
] )
TemplateContextProcessorCallable = t.Union[ TemplateContextProcessorCallable = (
t.Callable[[], dict[str, t.Any]], t.Callable[[], dict[str, t.Any]] | t.Callable[[], t.Awaitable[dict[str, t.Any]]]
t.Callable[[], t.Awaitable[dict[str, t.Any]]], )
]
TemplateFilterCallable = t.Callable[..., t.Any] TemplateFilterCallable = t.Callable[..., t.Any]
TemplateGlobalCallable = t.Callable[..., t.Any] TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool] TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None] URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
URLValuePreprocessorCallable = t.Callable[ URLValuePreprocessorCallable = t.Callable[[str | None, dict[str, t.Any] | None], None]
[t.Optional[str], t.Optional[dict[str, t.Any]]], None
]
# This should take Exception, but that either breaks typing the argument # This should take Exception, but that either breaks typing the argument
# with a specific exception, or decorating multiple times with different # with a specific exception, or decorating multiple times with different
@ -79,12 +76,12 @@ URLValuePreprocessorCallable = t.Callable[
# https://github.com/pallets/flask/issues/4095 # https://github.com/pallets/flask/issues/4095
# https://github.com/pallets/flask/issues/4295 # https://github.com/pallets/flask/issues/4295
# https://github.com/pallets/flask/issues/4297 # https://github.com/pallets/flask/issues/4297
ErrorHandlerCallable = t.Union[ ErrorHandlerCallable = (
t.Callable[[t.Any], ResponseReturnValue], t.Callable[[t.Any], ResponseReturnValue]
t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], | t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]]
] )
RouteCallable = t.Union[ RouteCallable = (
t.Callable[..., ResponseReturnValue], t.Callable[..., ResponseReturnValue]
t.Callable[..., t.Awaitable[ResponseReturnValue]], | t.Callable[..., t.Awaitable[ResponseReturnValue]]
] )

View file

@ -1,12 +1,11 @@
import os import os
import pkgutil
import sys import sys
import pytest import pytest
from _pytest import monkeypatch from _pytest import monkeypatch
from flask import Flask from flask import Flask
from flask.globals import request_ctx from flask.globals import app_ctx as _app_ctx
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
@ -84,47 +83,17 @@ def test_apps(monkeypatch):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def leak_detector(): def leak_detector():
yield """Fails if any app contexts are still pushed when a test ends. Pops all
contexts so subsequent tests are not affected.
# make sure we're not leaking a request context since we are
# testing flask internally in debug mode in a few cases
leaks = []
while request_ctx:
leaks.append(request_ctx._get_current_object())
request_ctx.pop()
assert leaks == []
@pytest.fixture(params=(True, False))
def limit_loader(request, monkeypatch):
"""Patch pkgutil.get_loader to give loader without get_filename or archive.
This provides for tests where a system has custom loaders, e.g. Google App
Engine's HardenedModulesHook, which have neither the `get_filename` method
nor the `archive` attribute.
This fixture will run the testcase twice, once with and once without the
limitation/mock.
""" """
if not request.param: yield
return leaks = []
class LimitedLoader: while _app_ctx:
def __init__(self, loader): leaks.append(_app_ctx._get_current_object())
self.loader = loader _app_ctx.pop()
def __getattr__(self, name): assert not leaks
if name in {"archive", "get_filename"}:
raise AttributeError(f"Mocking a loader which does not have {name!r}.")
return getattr(self.loader, name)
old_get_loader = pkgutil.get_loader
def get_loader(*args, **kwargs):
return LimitedLoader(old_get_loader(*args, **kwargs))
monkeypatch.setattr(pkgutil, "get_loader", get_loader)
@pytest.fixture @pytest.fixture

View file

@ -1,8 +1,10 @@
import sys
import pytest import pytest
import flask import flask
from flask.globals import app_ctx from flask.globals import app_ctx
from flask.globals import request_ctx from flask.testing import FlaskClient
def test_basic_url_generation(app): def test_basic_url_generation(app):
@ -107,7 +109,8 @@ def test_app_tearing_down_with_handled_exception_by_app_handler(app, client):
with app.app_context(): with app.app_context():
client.get("/") client.get("/")
assert cleanup_stuff == [None] # teardown request context, and with block context
assert cleanup_stuff == [None, None]
def test_app_tearing_down_with_unhandled_exception(app, client): def test_app_tearing_down_with_unhandled_exception(app, client):
@ -126,9 +129,11 @@ def test_app_tearing_down_with_unhandled_exception(app, client):
with app.app_context(): with app.app_context():
client.get("/") client.get("/")
assert len(cleanup_stuff) == 1 assert len(cleanup_stuff) == 2
assert isinstance(cleanup_stuff[0], ValueError) assert isinstance(cleanup_stuff[0], ValueError)
assert str(cleanup_stuff[0]) == "dummy" assert str(cleanup_stuff[0]) == "dummy"
# exception propagated, seen by request context and with block context
assert cleanup_stuff[0] is cleanup_stuff[1]
def test_app_ctx_globals_methods(app, app_ctx): def test_app_ctx_globals_methods(app, app_ctx):
@ -178,7 +183,6 @@ def test_context_refcounts(app, client):
@app.route("/") @app.route("/")
def index(): def index():
with app_ctx: with app_ctx:
with request_ctx:
pass pass
assert flask.request.environ["werkzeug.request"] is not None assert flask.request.environ["werkzeug.request"] is not None
@ -207,3 +211,55 @@ def test_clean_pop(app):
assert called == ["flask_test", "TEARDOWN"] assert called == ["flask_test", "TEARDOWN"]
assert not flask.current_app assert not flask.current_app
def test_robust_teardown(app: flask.Flask, client: FlaskClient) -> None:
count = 0
@app.teardown_request
def request_teardown(e: Exception | None) -> None:
nonlocal count
count += 1
raise ValueError("request_teardown")
@app.teardown_appcontext
def app_teardown(e: Exception | None) -> None:
nonlocal count
count += 1
raise ValueError("app_teardown")
@app.get("/")
def index() -> str:
return ""
def request_signal(sender: flask.Flask, exc: Exception | None) -> None:
nonlocal count
count += 1
raise ValueError("request_signal")
def app_signal(sender: flask.Flask, exc: Exception | None) -> None:
nonlocal count
count += 1
raise ValueError("app_signal")
with (
flask.request_tearing_down.connected_to(request_signal, app),
flask.appcontext_tearing_down.connected_to(app_signal, app),
):
if sys.version_info >= (3, 11):
with pytest.raises(ExceptionGroup, match="context teardown") as exc_info: # noqa: F821
client.get()
assert len(exc_info.value.exceptions) == 2
eg1, eg2 = exc_info.value.exceptions
assert isinstance(eg1, ExceptionGroup) # noqa: F821
assert "request teardown" in eg1.message
assert len(eg1.exceptions) == 2
assert isinstance(eg2, ExceptionGroup) # noqa: F821
assert "app teardown" in eg2.message
assert len(eg2.exceptions) == 2
else:
with pytest.raises(ValueError, match="request_teardown"):
client.get()
assert count == 4

View file

@ -1,8 +1,8 @@
import gc import gc
import importlib.metadata
import re import re
import typing as t import typing as t
import uuid import uuid
import warnings
import weakref import weakref
from contextlib import nullcontext from contextlib import nullcontext
from datetime import datetime from datetime import datetime
@ -20,6 +20,8 @@ from werkzeug.routing import BuildError
from werkzeug.routing import RequestRedirect from werkzeug.routing import RequestRedirect
import flask import flask
from flask.globals import app_ctx
from flask.testing import FlaskClient
require_cpython_gc = pytest.mark.skipif( require_cpython_gc = pytest.mark.skipif(
python_implementation() != "CPython", python_implementation() != "CPython",
@ -67,63 +69,61 @@ def test_method_route_no_methods(app):
app.get("/", methods=["GET", "POST"]) app.get("/", methods=["GET", "POST"])
def test_provide_automatic_options_attr(): def test_provide_automatic_options_attr_disable(
app = flask.Flask(__name__) app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the view func attribute."""
def index(): def index():
return "Hello World!" return "Hello World!"
index.provide_automatic_options = False index.provide_automatic_options = False
app.route("/")(index) app.add_url_rule("/", view_func=index)
rv = app.test_client().open("/", method="OPTIONS") rv = client.options()
assert rv.status_code == 405 assert rv.status_code == 405
app = flask.Flask(__name__)
def index2(): def test_provide_automatic_options_attr_enable(
app: flask.Flask, client: FlaskClient
) -> None:
"""When default automatic options is disabled in config, it can still be
enabled by the view function attribute.
"""
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
def index():
return "Hello World!" return "Hello World!"
index2.provide_automatic_options = True index.provide_automatic_options = True
app.route("/", methods=["OPTIONS"])(index2) app.add_url_rule("/", view_func=index)
rv = app.test_client().open("/", method="OPTIONS") rv = client.options()
assert sorted(rv.allow) == ["OPTIONS"] assert rv.allow == {"GET", "HEAD", "OPTIONS"}
def test_provide_automatic_options_kwarg(app, client): def test_provide_automatic_options_arg_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the route argument."""
@app.get("/", provide_automatic_options=False)
def index(): def index():
return flask.request.method return "Hello World!"
def more(): rv = client.options()
return flask.request.method
app.add_url_rule("/", view_func=index, provide_automatic_options=False)
app.add_url_rule(
"/more",
view_func=more,
methods=["GET", "POST"],
provide_automatic_options=False,
)
assert client.get("/").data == b"GET"
rv = client.post("/")
assert rv.status_code == 405
assert sorted(rv.allow) == ["GET", "HEAD"]
rv = client.open("/", method="OPTIONS")
assert rv.status_code == 405 assert rv.status_code == 405
rv = client.head("/")
assert rv.status_code == 200
assert not rv.data # head truncates
assert client.post("/more").data == b"POST"
assert client.get("/more").data == b"GET"
rv = client.delete("/more") def test_provide_automatic_options_method_disable(
assert rv.status_code == 405 app: flask.Flask, client: FlaskClient
assert sorted(rv.allow) == ["GET", "HEAD", "POST"] ) -> None:
"""Automatic options is ignored if the route handles options."""
rv = client.open("/more", method="OPTIONS") @app.route("/", methods=["OPTIONS"])
assert rv.status_code == 405 def index():
return "", {"X-Test": "test"}
rv = client.options()
assert rv.headers["X-Test"] == "test"
def test_request_dispatching(app, client): def test_request_dispatching(app, client):
@ -231,27 +231,46 @@ def test_endpoint_decorator(app, client):
assert client.get("/foo/bar").data == b"bar" assert client.get("/foo/bar").data == b"bar"
def test_session(app, client): def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
@app.route("/set", methods=["POST"]) @app.post("/")
def set(): def do_set():
assert not flask.session.accessed
assert not flask.session.modified
flask.session["value"] = flask.request.form["value"] flask.session["value"] = flask.request.form["value"]
assert flask.session.accessed
assert flask.session.modified
return "value set" return "value set"
@app.route("/get") @app.get("/")
def get(): def do_get():
assert not flask.session.accessed return flask.session.get("value", "None")
assert not flask.session.modified
v = flask.session.get("value", "None")
assert flask.session.accessed
assert not flask.session.modified
return v
assert client.post("/set", data={"value": "42"}).data == b"value set" @app.get("/nothing")
assert client.get("/get").data == b"42" def do_nothing() -> str:
return ""
with client:
rv = client.get("/nothing")
assert "cookie" not in rv.vary
assert not app_ctx._session.accessed
assert not app_ctx._session.modified
with client:
rv = client.post(data={"value": "42"})
assert rv.text == "value set"
assert "cookie" in rv.vary
assert app_ctx._session.accessed
assert app_ctx._session.modified
with client:
rv = client.get()
assert rv.text == "42"
assert "cookie" in rv.vary
assert app_ctx._session.accessed
assert not app_ctx._session.modified
with client:
rv = client.get("/nothing")
assert rv.text == ""
assert "cookie" not in rv.vary
assert not app_ctx._session.accessed
assert not app_ctx._session.modified
def test_session_path(app, client): def test_session_path(app, client):
@ -381,14 +400,21 @@ def test_session_secret_key_fallbacks(app, client) -> None:
def get_session() -> dict[str, t.Any]: def get_session() -> dict[str, t.Any]:
return dict(flask.session) return dict(flask.session)
# Set session with initial secret key # Set session with initial secret key, and two valid expiring keys
app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
"0 key",
["-1 key", "-2 key"],
)
client.post() client.post()
assert client.get().json == {"a": 1} assert client.get().json == {"a": 1}
# Change secret key, session can't be loaded and appears empty # Change secret key, session can't be loaded and appears empty
app.secret_key = "new test key" app.secret_key = "? key"
assert client.get().json == {} assert client.get().json == {}
# Add initial secret key as fallback, session can be loaded # Rotate the valid keys, session can be loaded
app.config["SECRET_KEY_FALLBACKS"] = ["test key"] app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
"+1 key",
["0 key", "-1 key"],
)
assert client.get().json == {"a": 1} assert client.get().json == {"a": 1}
@ -1394,20 +1420,21 @@ def test_url_for_passes_special_values_to_build_error_handler(app):
def test_static_files(app, client): def test_static_files(app, client):
rv = client.get("/static/index.html") with client.get("/static/index.html") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
assert rv.data.strip() == b"<h1>Hello World!</h1>" assert rv.data.strip() == b"<h1>Hello World!</h1>"
with app.test_request_context(): with app.test_request_context():
assert flask.url_for("static", filename="index.html") == "/static/index.html" assert (
rv.close() flask.url_for("static", filename="index.html") == "/static/index.html"
)
def test_static_url_path(): def test_static_url_path():
app = flask.Flask(__name__, static_url_path="/foo") app = flask.Flask(__name__, static_url_path="/foo")
app.testing = True app.testing = True
rv = app.test_client().get("/foo/index.html")
with app.test_client().get("/foo/index.html") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
with app.test_request_context(): with app.test_request_context():
assert flask.url_for("static", filename="index.html") == "/foo/index.html" assert flask.url_for("static", filename="index.html") == "/foo/index.html"
@ -1416,9 +1443,9 @@ def test_static_url_path():
def test_static_url_path_with_ending_slash(): def test_static_url_path_with_ending_slash():
app = flask.Flask(__name__, static_url_path="/foo/") app = flask.Flask(__name__, static_url_path="/foo/")
app.testing = True app.testing = True
rv = app.test_client().get("/foo/index.html")
with app.test_client().get("/foo/index.html") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
with app.test_request_context(): with app.test_request_context():
assert flask.url_for("static", filename="index.html") == "/foo/index.html" assert flask.url_for("static", filename="index.html") == "/foo/index.html"
@ -1426,25 +1453,25 @@ def test_static_url_path_with_ending_slash():
def test_static_url_empty_path(app): def test_static_url_empty_path(app):
app = flask.Flask(__name__, static_folder="", static_url_path="") app = flask.Flask(__name__, static_folder="", static_url_path="")
rv = app.test_client().open("/static/index.html", method="GET")
with app.test_client().open("/static/index.html", method="GET") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
def test_static_url_empty_path_default(app): def test_static_url_empty_path_default(app):
app = flask.Flask(__name__, static_folder="") app = flask.Flask(__name__, static_folder="")
rv = app.test_client().open("/static/index.html", method="GET")
with app.test_client().open("/static/index.html", method="GET") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
def test_static_folder_with_pathlib_path(app): def test_static_folder_with_pathlib_path(app):
from pathlib import Path from pathlib import Path
app = flask.Flask(__name__, static_folder=Path("static")) app = flask.Flask(__name__, static_folder=Path("static"))
rv = app.test_client().open("/static/index.html", method="GET")
with app.test_client().open("/static/index.html", method="GET") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
def test_static_folder_with_ending_slash(): def test_static_folder_with_ending_slash():
@ -1461,9 +1488,10 @@ def test_static_folder_with_ending_slash():
def test_static_route_with_host_matching(): def test_static_route_with_host_matching():
app = flask.Flask(__name__, host_matching=True, static_host="example.com") app = flask.Flask(__name__, host_matching=True, static_host="example.com")
c = app.test_client() c = app.test_client()
rv = c.get("http://example.com/static/index.html")
with c.get("http://example.com/static/index.html") as rv:
assert rv.status_code == 200 assert rv.status_code == 200
rv.close()
with app.test_request_context(): with app.test_request_context():
rv = flask.url_for("static", filename="index.html", _external=True) rv = flask.url_for("static", filename="index.html", _external=True)
assert rv == "http://example.com/static/index.html" assert rv == "http://example.com/static/index.html"
@ -1484,20 +1512,22 @@ def test_request_locals():
assert not flask.g assert not flask.g
werkzeug_3_2 = importlib.metadata.version("werkzeug") >= "3.2."
@pytest.mark.parametrize( @pytest.mark.parametrize(
("subdomain_matching", "host_matching", "expect_base", "expect_abc", "expect_xyz"), ("subdomain_matching", "host_matching", "expect_subdomain", "expect_host"),
[ [
(False, False, "default", "default", "default"), (False, False, "default", "default"),
(True, False, "default", "abc", "<invalid>"), (True, False, "abc", "<invalid>"),
(False, True, "default", "abc", "default"), (False, True, "abc", "default"),
], ],
) )
def test_server_name_matching( def test_server_name_matching(
subdomain_matching: bool, subdomain_matching: bool,
host_matching: bool, host_matching: bool,
expect_base: str, expect_subdomain: str,
expect_abc: str, expect_host: str,
expect_xyz: str,
) -> None: ) -> None:
app = flask.Flask( app = flask.Flask(
__name__, __name__,
@ -1515,15 +1545,18 @@ def test_server_name_matching(
client = app.test_client() client = app.test_client()
r = client.get(base_url="http://example.test") r = client.get(base_url="http://example.test")
assert r.text == expect_base assert r.text == "default"
r = client.get(base_url="http://abc.example.test") r = client.get(base_url="http://abc.example.test")
assert r.text == expect_abc assert r.text == expect_subdomain
with pytest.warns() if subdomain_matching else nullcontext(): with pytest.warns() if subdomain_matching else nullcontext():
r = client.get(base_url="http://xyz.other.test") r = client.get(base_url="http://xyz.other.test")
assert r.text == expect_xyz if werkzeug_3_2:
assert r.text == "default"
else:
assert r.text == expect_host
def test_server_name_subdomain(): def test_server_name_subdomain():
@ -1559,12 +1592,12 @@ def test_server_name_subdomain():
rv = client.get("/", "https://dev.local") rv = client.get("/", "https://dev.local")
assert rv.data == b"default" assert rv.data == b"default"
# suppress Werkzeug 0.15 warning about name mismatch with pytest.warns(match="Current server name"):
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", "Current server name", UserWarning, "flask.app"
)
rv = client.get("/", "http://foo.localhost") rv = client.get("/", "http://foo.localhost")
if werkzeug_3_2:
assert rv.status_code == 200
else:
assert rv.status_code == 404 assert rv.status_code == 404
rv = client.get("/", "http://foo.dev.local") rv = client.get("/", "http://foo.dev.local")
@ -1800,13 +1833,13 @@ def test_subdomain_matching_other_name(matching):
def index(): def index():
return "", 204 return "", 204
# suppress Werkzeug 0.15 warning about name mismatch with pytest.warns(match="Current server name") if matching else nullcontext():
with warnings.catch_warnings(): # ip address can't match name, but will fall back to default
warnings.filterwarnings(
"ignore", "Current server name", UserWarning, "flask.app"
)
# ip address can't match name
rv = client.get("/", "http://127.0.0.1:3000/") rv = client.get("/", "http://127.0.0.1:3000/")
if werkzeug_3_2:
assert rv.status_code == 204
else:
assert rv.status_code == 404 if matching else 204 assert rv.status_code == 404 if matching else 204
# allow all subdomains if matching is disabled # allow all subdomains if matching is disabled

View file

@ -184,12 +184,12 @@ def test_templates_and_static(test_apps):
assert rv.data == b"Hello from the Admin" assert rv.data == b"Hello from the Admin"
rv = client.get("/admin/index2") rv = client.get("/admin/index2")
assert rv.data == b"Hello from the Admin" assert rv.data == b"Hello from the Admin"
rv = client.get("/admin/static/test.txt")
with client.get("/admin/static/test.txt") as rv:
assert rv.data.strip() == b"Admin File" assert rv.data.strip() == b"Admin File"
rv.close()
rv = client.get("/admin/static/css/test.css") with client.get("/admin/static/css/test.css") as rv:
assert rv.data.strip() == b"/* nested file */" assert rv.data.strip() == b"/* nested file */"
rv.close()
# try/finally, in case other tests use this app for Blueprint tests. # try/finally, in case other tests use this app for Blueprint tests.
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"] max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
@ -198,10 +198,10 @@ def test_templates_and_static(test_apps):
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age: if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == expected_max_age:
expected_max_age = 7200 expected_max_age = 7200
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age app.config["SEND_FILE_MAX_AGE_DEFAULT"] = expected_max_age
rv = client.get("/admin/static/css/test.css")
with client.get("/admin/static/css/test.css") as rv:
cc = parse_cache_control_header(rv.headers["Cache-Control"]) cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == expected_max_age assert cc.max_age == expected_max_age
rv.close()
finally: finally:
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
@ -220,28 +220,19 @@ def test_templates_and_static(test_apps):
assert flask.render_template("nested/nested.txt") == "I'm nested" assert flask.render_template("nested/nested.txt") == "I'm nested"
def test_default_static_max_age(app): def test_default_static_max_age(app: flask.Flask) -> None:
class MyBlueprint(flask.Blueprint): class MyBlueprint(flask.Blueprint):
def get_send_file_max_age(self, filename): def get_send_file_max_age(self, filename):
return 100 return 100
blueprint = MyBlueprint("blueprint", __name__, static_folder="static") blueprint = MyBlueprint(
"blueprint", __name__, url_prefix="/bp", static_folder="static"
)
app.register_blueprint(blueprint) app.register_blueprint(blueprint)
# try/finally, in case other tests use this app for Blueprint tests. with app.test_request_context(), blueprint.send_static_file("index.html") as rv:
max_age_default = app.config["SEND_FILE_MAX_AGE_DEFAULT"]
try:
with app.test_request_context():
unexpected_max_age = 3600
if app.config["SEND_FILE_MAX_AGE_DEFAULT"] == unexpected_max_age:
unexpected_max_age = 7200
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = unexpected_max_age
rv = blueprint.send_static_file("index.html")
cc = parse_cache_control_header(rv.headers["Cache-Control"]) cc = parse_cache_control_header(rv.headers["Cache-Control"])
assert cc.max_age == 100 assert cc.max_age == 100
rv.close()
finally:
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = max_age_default
def test_templates_list(test_apps): def test_templates_list(test_apps):
@ -366,11 +357,35 @@ def test_template_filter(app):
def my_reverse(s): def my_reverse(s):
return s[::-1] return s[::-1]
@bp.app_template_filter
def my_reverse_2(s):
return s[::-1]
@bp.app_template_filter("my_reverse_custom_name_3")
def my_reverse_3(s):
return s[::-1]
@bp.app_template_filter(name="my_reverse_custom_name_4")
def my_reverse_4(s):
return s[::-1]
app.register_blueprint(bp, url_prefix="/py") app.register_blueprint(bp, url_prefix="/py")
assert "my_reverse" in app.jinja_env.filters.keys() assert "my_reverse" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"] == my_reverse
assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
assert "my_reverse_2" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2
assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba"
assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3
assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba"
assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4
assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba"
def test_add_template_filter(app): def test_add_template_filter(app):
bp = flask.Blueprint("bp", __name__) bp = flask.Blueprint("bp", __name__)
@ -502,11 +517,35 @@ def test_template_test(app):
def is_boolean(value): def is_boolean(value):
return isinstance(value, bool) return isinstance(value, bool)
@bp.app_template_test
def boolean_2(value):
return isinstance(value, bool)
@bp.app_template_test("my_boolean_custom_name")
def boolean_3(value):
return isinstance(value, bool)
@bp.app_template_test(name="my_boolean_custom_name_2")
def boolean_4(value):
return isinstance(value, bool)
app.register_blueprint(bp, url_prefix="/py") app.register_blueprint(bp, url_prefix="/py")
assert "is_boolean" in app.jinja_env.tests.keys() assert "is_boolean" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["is_boolean"] == is_boolean assert app.jinja_env.tests["is_boolean"] == is_boolean
assert app.jinja_env.tests["is_boolean"](False) assert app.jinja_env.tests["is_boolean"](False)
assert "boolean_2" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["boolean_2"] == boolean_2
assert app.jinja_env.tests["boolean_2"](False)
assert "my_boolean_custom_name" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3
assert app.jinja_env.tests["my_boolean_custom_name"](False)
assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4
assert app.jinja_env.tests["my_boolean_custom_name_2"](False)
def test_add_template_test(app): def test_add_template_test(app):
bp = flask.Blueprint("bp", __name__) bp = flask.Blueprint("bp", __name__)
@ -679,6 +718,18 @@ def test_template_global(app):
def get_answer(): def get_answer():
return 42 return 42
@bp.app_template_global
def get_stuff_1():
return "get_stuff_1"
@bp.app_template_global("my_get_stuff_custom_name_2")
def get_stuff_2():
return "get_stuff_2"
@bp.app_template_global(name="my_get_stuff_custom_name_3")
def get_stuff_3():
return "get_stuff_3"
# Make sure the function is not in the jinja_env already # Make sure the function is not in the jinja_env already
assert "get_answer" not in app.jinja_env.globals.keys() assert "get_answer" not in app.jinja_env.globals.keys()
app.register_blueprint(bp) app.register_blueprint(bp)
@ -688,10 +739,31 @@ def test_template_global(app):
assert app.jinja_env.globals["get_answer"] is get_answer assert app.jinja_env.globals["get_answer"] is get_answer
assert app.jinja_env.globals["get_answer"]() == 42 assert app.jinja_env.globals["get_answer"]() == 42
assert "get_stuff_1" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1
assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1"
assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2
assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2"
assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3
assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3"
with app.app_context(): with app.app_context():
rv = flask.render_template_string("{{ get_answer() }}") rv = flask.render_template_string("{{ get_answer() }}")
assert rv == "42" assert rv == "42"
rv = flask.render_template_string("{{ get_stuff_1() }}")
assert rv == "get_stuff_1"
rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}")
assert rv == "get_stuff_2"
rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}")
assert rv == "get_stuff_3"
def test_request_processing(app, client): def test_request_processing(app, client):
bp = flask.Blueprint("bp", __name__) bp = flask.Blueprint("bp", __name__)

View file

@ -462,7 +462,7 @@ class TestRoutes:
def expect_order(self, order, output): def expect_order(self, order, output):
# skip the header and match the start of each row # skip the header and match the start of each row
for expect, line in zip(order, output.splitlines()[2:]): for expect, line in zip(order, output.splitlines()[2:], strict=False):
# do this instead of startswith for nicer pytest output # do this instead of startswith for nicer pytest output
assert line[: len(expect)] == expect assert line[: len(expect)] == expect
@ -489,6 +489,7 @@ class TestRoutes:
def test_all_methods(self, invoke): def test_all_methods(self, invoke):
output = invoke(["routes"]).output output = invoke(["routes"]).output
assert "GET, HEAD, OPTIONS, POST" not in output assert "GET, HEAD, OPTIONS, POST" not in output
output = invoke(["routes", "--all-methods"]).output output = invoke(["routes", "--all-methods"]).output
assert "GET, HEAD, OPTIONS, POST" in output assert "GET, HEAD, OPTIONS, POST" in output

View file

@ -32,45 +32,39 @@ class PyBytesIO:
class TestSendfile: class TestSendfile:
def test_send_file(self, app, req_ctx): def test_send_file(self, app, req_ctx):
rv = flask.send_file("static/index.html") with app.open_resource("static/index.html") as f:
expect = f.read()
with flask.send_file("static/index.html") as rv:
assert rv.direct_passthrough assert rv.direct_passthrough
assert rv.mimetype == "text/html" assert rv.mimetype == "text/html"
with app.open_resource("static/index.html") as f:
rv.direct_passthrough = False rv.direct_passthrough = False
assert rv.data == f.read() assert rv.data == expect
rv.close()
def test_static_file(self, app, req_ctx): def test_static_file(self, app, req_ctx):
# Default max_age is None. # Default max_age is None.
# Test with static file handler. # Test with static file handler.
rv = app.send_static_file("index.html") with app.send_static_file("index.html") as rv:
assert rv.cache_control.max_age is None assert rv.cache_control.max_age is None
rv.close()
# Test with direct use of send_file. # Test with direct use of send_file.
rv = flask.send_file("static/index.html") with flask.send_file("static/index.html") as rv:
assert rv.cache_control.max_age is None assert rv.cache_control.max_age is None
rv.close()
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600 app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 3600
# Test with static file handler. # Test with static file handler.
rv = app.send_static_file("index.html") with app.send_static_file("index.html") as rv:
assert rv.cache_control.max_age == 3600 assert rv.cache_control.max_age == 3600
rv.close()
# Test with direct use of send_file. # Test with direct use of send_file.
rv = flask.send_file("static/index.html") with flask.send_file("static/index.html") as rv:
assert rv.cache_control.max_age == 3600 assert rv.cache_control.max_age == 3600
rv.close()
# Test with pathlib.Path. # Test with pathlib.Path.
rv = app.send_static_file(FakePath("index.html")) with app.send_static_file(FakePath("index.html")) as rv:
assert rv.cache_control.max_age == 3600 assert rv.cache_control.max_age == 3600
rv.close()
class StaticFileApp(flask.Flask): class StaticFileApp(flask.Flask):
def get_send_file_max_age(self, filename): def get_send_file_max_age(self, filename):
@ -80,23 +74,21 @@ class TestSendfile:
with app.test_request_context(): with app.test_request_context():
# Test with static file handler. # Test with static file handler.
rv = app.send_static_file("index.html") with app.send_static_file("index.html") as rv:
assert rv.cache_control.max_age == 10 assert rv.cache_control.max_age == 10
rv.close()
# Test with direct use of send_file. # Test with direct use of send_file.
rv = flask.send_file("static/index.html") with flask.send_file("static/index.html") as rv:
assert rv.cache_control.max_age == 10 assert rv.cache_control.max_age == 10
rv.close()
def test_send_from_directory(self, app, req_ctx): def test_send_from_directory(self, app, req_ctx):
app.root_path = os.path.join( app.root_path = os.path.join(
os.path.dirname(__file__), "test_apps", "subdomaintestmodule" os.path.dirname(__file__), "test_apps", "subdomaintestmodule"
) )
rv = flask.send_from_directory("static", "hello.txt")
with flask.send_from_directory("static", "hello.txt") as rv:
rv.direct_passthrough = False rv.direct_passthrough = False
assert rv.data.strip() == b"Hello Subdomain" assert rv.data.strip() == b"Hello Subdomain"
rv.close()
class TestUrlFor: class TestUrlFor:
@ -306,6 +298,31 @@ class TestStreaming:
rv = client.get("/") rv = client.get("/")
assert rv.data == b"flask" assert rv.data == b"flask"
def test_async_view(self, app, client):
@app.route("/")
async def index():
flask.session["test"] = "flask"
@flask.stream_with_context
def gen():
yield flask.session["test"]
return flask.Response(gen())
# response is closed without reading stream
client.get().close()
# response stream is read
with client.get() as rv:
assert rv.text == "flask"
# same as above, but with client context preservation
with client:
client.get().close()
with client, client.get() as rv:
assert rv.text == "flask"
class TestHelpers: class TestHelpers:
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -63,7 +63,7 @@ def test_uninstalled_namespace_paths(tmp_path, monkeypatch, purge_module):
def test_installed_module_paths( def test_installed_module_paths(
modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages, limit_loader modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages
): ):
(site_packages / "site_app.py").write_text( (site_packages / "site_app.py").write_text(
"import flask\napp = flask.Flask(__name__)\n" "import flask\napp = flask.Flask(__name__)\n"
@ -78,7 +78,7 @@ def test_installed_module_paths(
def test_installed_package_paths( def test_installed_package_paths(
limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch modules_tmp_path, modules_tmp_path_prefix, purge_module, monkeypatch
): ):
installed_path = modules_tmp_path / "path" installed_path = modules_tmp_path / "path"
installed_path.mkdir() installed_path.mkdir()
@ -97,7 +97,7 @@ def test_installed_package_paths(
def test_prefix_package_paths( def test_prefix_package_paths(
limit_loader, modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages modules_tmp_path, modules_tmp_path_prefix, purge_module, site_packages
): ):
app = site_packages / "site_package" app = site_packages / "site_package"
app.mkdir() app.mkdir()

View file

@ -1,16 +1,15 @@
from __future__ import annotations
import collections.abc as cabc
import warnings import warnings
from concurrent import futures
import pytest import pytest
import flask import flask
from flask.globals import request_ctx
from flask.sessions import SecureCookieSessionInterface from flask.sessions import SecureCookieSessionInterface
from flask.sessions import SessionInterface from flask.sessions import SessionInterface
from flask.testing import FlaskClient
try:
from greenlet import greenlet
except ImportError:
greenlet = None
def test_teardown_on_pop(app): def test_teardown_on_pop(app):
@ -145,61 +144,34 @@ def test_manual_context_binding(app):
index() index()
@pytest.mark.skipif(greenlet is None, reason="greenlet not installed") def test_copy_context_thread(
class TestGreenletContextCopying: request: pytest.FixtureRequest, app: flask.Flask, client: FlaskClient
def test_greenlet_context_copying(self, app, client): ) -> None:
greenlets = [] executor = futures.ThreadPoolExecutor(max_workers=2)
request.addfinalizer(lambda: executor.shutdown(cancel_futures=True))
@app.route("/") result: cabc.Iterator[int] | None = None
def index():
flask.session["fizz"] = "buzz"
reqctx = request_ctx.copy()
def g():
assert not flask.request
assert not flask.current_app
with reqctx:
assert flask.request
assert flask.current_app == app
assert flask.request.path == "/"
assert flask.request.args["foo"] == "bar"
assert flask.session.get("fizz") == "buzz"
assert not flask.request
return 42
greenlets.append(greenlet(g))
return "Hello World!"
rv = client.get("/?foo=bar")
assert rv.data == b"Hello World!"
result = greenlets[0].run()
assert result == 42
def test_greenlet_context_copying_api(self, app, client):
greenlets = []
@app.route("/") @app.route("/")
def index(): def index():
flask.session["fizz"] = "buzz" flask.session["fizz"] = "buzz"
@flask.copy_current_request_context @flask.copy_current_request_context
def g(): def work(n: int) -> int:
assert flask.request
assert flask.current_app == app assert flask.current_app == app
assert flask.request.path == "/" assert flask.request.path == "/"
assert flask.request.args["foo"] == "bar" assert flask.request.args["foo"] == "bar"
assert flask.session.get("fizz") == "buzz" assert flask.session["fizz"] == "buzz"
return 42 return n
greenlets.append(greenlet(g)) nonlocal result
result = executor.map(work, range(10))
return "Hello World!" return "Hello World!"
rv = client.get("/?foo=bar") rv = client.get(query_string={"foo": "bar"})
assert rv.data == b"Hello World!" assert rv.text == "Hello World!"
result = greenlets[0].run() assert result is not None
assert result == 42 assert set(result) == set(range(10))
def test_session_error_pops_context(): def test_session_error_pops_context():
@ -275,51 +247,3 @@ def test_session_dynamic_cookie_name():
# cookies are being used for the urls that end with "dynamic cookie" # cookies are being used for the urls that end with "dynamic cookie"
assert test_client.get("/get").data == b"42" assert test_client.get("/get").data == b"42"
assert test_client.get("/get_dynamic_cookie").data == b"616" assert test_client.get("/get_dynamic_cookie").data == b"616"
def test_bad_environ_raises_bad_request():
app = flask.Flask(__name__)
from flask.testing import EnvironBuilder
builder = EnvironBuilder(app)
environ = builder.get_environ()
# use a non-printable character in the Host - this is key to this test
environ["HTTP_HOST"] = "\x8a"
with app.request_context(environ):
response = app.full_dispatch_request()
assert response.status_code == 400
def test_environ_for_valid_idna_completes():
app = flask.Flask(__name__)
@app.route("/")
def index():
return "Hello World!"
from flask.testing import EnvironBuilder
builder = EnvironBuilder(app)
environ = builder.get_environ()
# these characters are all IDNA-compatible
environ["HTTP_HOST"] = "ąśźäüжŠßя.com"
with app.request_context(environ):
response = app.full_dispatch_request()
assert response.status_code == 200
def test_normal_environ_completes():
app = flask.Flask(__name__)
@app.route("/")
def index():
return "Hello World!"
response = app.test_client().get("/", headers={"host": "xn--on-0ia.com"})
assert response.status_code == 200

View file

@ -1,5 +1,5 @@
import flask import flask
from flask.globals import request_ctx from flask.globals import app_ctx
from flask.sessions import SessionInterface from flask.sessions import SessionInterface
@ -14,7 +14,7 @@ def test_open_session_with_endpoint():
pass pass
def open_session(self, app, request): def open_session(self, app, request):
request_ctx.match_request() app_ctx.match_request()
assert request.endpoint is not None assert request.endpoint is not None
app = flask.Flask(__name__) app = flask.Flask(__name__)

View file

@ -5,7 +5,7 @@ import flask
def test_suppressed_exception_logging(): def test_suppressed_exception_logging():
class SuppressedFlask(flask.Flask): class SuppressedFlask(flask.Flask):
def log_exception(self, exc_info): def log_exception(self, ctx, exc_info):
pass pass
out = StringIO() out = StringIO()

View file

@ -129,6 +129,30 @@ def test_template_filter(app):
assert app.jinja_env.filters["my_reverse"] == my_reverse assert app.jinja_env.filters["my_reverse"] == my_reverse
assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba" assert app.jinja_env.filters["my_reverse"]("abcd") == "dcba"
@app.template_filter
def my_reverse_2(s):
return s[::-1]
assert "my_reverse_2" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_2"] == my_reverse_2
assert app.jinja_env.filters["my_reverse_2"]("abcd") == "dcba"
@app.template_filter("my_reverse_custom_name_3")
def my_reverse_3(s):
return s[::-1]
assert "my_reverse_custom_name_3" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_custom_name_3"] == my_reverse_3
assert app.jinja_env.filters["my_reverse_custom_name_3"]("abcd") == "dcba"
@app.template_filter(name="my_reverse_custom_name_4")
def my_reverse_4(s):
return s[::-1]
assert "my_reverse_custom_name_4" in app.jinja_env.filters.keys()
assert app.jinja_env.filters["my_reverse_custom_name_4"] == my_reverse_4
assert app.jinja_env.filters["my_reverse_custom_name_4"]("abcd") == "dcba"
def test_add_template_filter(app): def test_add_template_filter(app):
def my_reverse(s): def my_reverse(s):
@ -223,6 +247,30 @@ def test_template_test(app):
assert app.jinja_env.tests["boolean"] == boolean assert app.jinja_env.tests["boolean"] == boolean
assert app.jinja_env.tests["boolean"](False) assert app.jinja_env.tests["boolean"](False)
@app.template_test
def boolean_2(value):
return isinstance(value, bool)
assert "boolean_2" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["boolean_2"] == boolean_2
assert app.jinja_env.tests["boolean_2"](False)
@app.template_test("my_boolean_custom_name")
def boolean_3(value):
return isinstance(value, bool)
assert "my_boolean_custom_name" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["my_boolean_custom_name"] == boolean_3
assert app.jinja_env.tests["my_boolean_custom_name"](False)
@app.template_test(name="my_boolean_custom_name_2")
def boolean_4(value):
return isinstance(value, bool)
assert "my_boolean_custom_name_2" in app.jinja_env.tests.keys()
assert app.jinja_env.tests["my_boolean_custom_name_2"] == boolean_4
assert app.jinja_env.tests["my_boolean_custom_name_2"](False)
def test_add_template_test(app): def test_add_template_test(app):
def boolean(value): def boolean(value):
@ -320,6 +368,39 @@ def test_add_template_global(app, app_ctx):
rv = flask.render_template_string("{{ get_stuff() }}") rv = flask.render_template_string("{{ get_stuff() }}")
assert rv == "42" assert rv == "42"
@app.template_global
def get_stuff_1():
return "get_stuff_1"
assert "get_stuff_1" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["get_stuff_1"] == get_stuff_1
assert app.jinja_env.globals["get_stuff_1"](), "get_stuff_1"
rv = flask.render_template_string("{{ get_stuff_1() }}")
assert rv == "get_stuff_1"
@app.template_global("my_get_stuff_custom_name_2")
def get_stuff_2():
return "get_stuff_2"
assert "my_get_stuff_custom_name_2" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["my_get_stuff_custom_name_2"] == get_stuff_2
assert app.jinja_env.globals["my_get_stuff_custom_name_2"](), "get_stuff_2"
rv = flask.render_template_string("{{ my_get_stuff_custom_name_2() }}")
assert rv == "get_stuff_2"
@app.template_global(name="my_get_stuff_custom_name_3")
def get_stuff_3():
return "get_stuff_3"
assert "my_get_stuff_custom_name_3" in app.jinja_env.globals.keys()
assert app.jinja_env.globals["my_get_stuff_custom_name_3"] == get_stuff_3
assert app.jinja_env.globals["my_get_stuff_custom_name_3"](), "get_stuff_3"
rv = flask.render_template_string("{{ my_get_stuff_custom_name_3() }}")
assert rv == "get_stuff_3"
def test_custom_template_loader(client): def test_custom_template_loader(client):
class MyFlask(flask.Flask): class MyFlask(flask.Flask):

View file

@ -6,7 +6,7 @@ import pytest
import flask import flask
from flask import appcontext_popped from flask import appcontext_popped
from flask.cli import ScriptInfo from flask.cli import ScriptInfo
from flask.globals import _cv_request from flask.globals import _cv_app
from flask.json import jsonify from flask.json import jsonify
from flask.testing import EnvironBuilder from flask.testing import EnvironBuilder
from flask.testing import FlaskCliRunner from flask.testing import FlaskCliRunner
@ -76,7 +76,6 @@ def test_client_open_environ(app, client, request):
return flask.request.remote_addr return flask.request.remote_addr
builder = EnvironBuilder(app, path="/index", method="GET") builder = EnvironBuilder(app, path="/index", method="GET")
request.addfinalizer(builder.close)
rv = client.open(builder) rv = client.open(builder)
assert rv.data == b"127.0.0.1" assert rv.data == b"127.0.0.1"
@ -138,32 +137,21 @@ def test_blueprint_with_subdomain():
assert rv.data == b"http://xxx.example.com:1234/foo/" assert rv.data == b"http://xxx.example.com:1234/foo/"
def test_redirect_keep_session(app, client, app_ctx): def test_redirect_session(app, client, app_ctx):
@app.route("/", methods=["GET", "POST"]) @app.route("/redirect")
def index(): def index():
if flask.request.method == "POST": flask.session["redirect"] = True
return flask.redirect("/getsession") return flask.redirect("/target")
flask.session["data"] = "foo"
return "index"
@app.route("/getsession") @app.route("/target")
def get_session(): def get_session():
return flask.session.get("data", "<missing>") flask.session["target"] = True
return ""
with client: with client:
rv = client.get("/getsession") client.get("/redirect", follow_redirects=True)
assert rv.data == b"<missing>" assert flask.session["redirect"] is True
assert flask.session["target"] is True
rv = client.get("/")
assert rv.data == b"index"
assert flask.session.get("data") == "foo"
rv = client.post("/", data={}, follow_redirects=True)
assert rv.data == b"foo"
assert flask.session.get("data") == "foo"
rv = client.get("/getsession")
assert rv.data == b"foo"
def test_session_transactions(app, client): def test_session_transactions(app, client):
@ -393,4 +381,4 @@ def test_client_pop_all_preserved(app, req_ctx, client):
# close the response, releasing the context held by stream_with_context # close the response, releasing the context held by stream_with_context
rv.close() rv.close()
# only req_ctx fixture should still be pushed # only req_ctx fixture should still be pushed
assert _cv_request.get(None) is req_ctx assert _cv_app.get(None) is req_ctx

View file

@ -2,6 +2,7 @@ import pytest
from werkzeug.http import parse_set_header from werkzeug.http import parse_set_header
import flask.views import flask.views
from flask.testing import FlaskClient
def common_test(app): def common_test(app):
@ -98,44 +99,55 @@ def test_view_decorators(app, client):
assert rv.data == b"Awesome" assert rv.data == b"Awesome"
def test_view_provide_automatic_options_attr(): def test_view_provide_automatic_options_attr_disable(
app = flask.Flask(__name__) app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options can be disabled by the view class attribute."""
class Index1(flask.views.View): class Index(flask.views.View):
provide_automatic_options = False provide_automatic_options = False
def dispatch_request(self): def dispatch_request(self):
return "Hello World!" return "Hello World!"
app.add_url_rule("/", view_func=Index1.as_view("index")) app.add_url_rule("/", view_func=Index.as_view("index"))
c = app.test_client() rv = client.options()
rv = c.open("/", method="OPTIONS")
assert rv.status_code == 405 assert rv.status_code == 405
app = flask.Flask(__name__)
class Index2(flask.views.View): def test_view_provide_automatic_options_attr_enable(
methods = ["OPTIONS"] app: flask.Flask, client: FlaskClient
) -> None:
"""When default automatic options is disabled in config, it can still be
enabled by the view class attribute.
"""
app.config["PROVIDE_AUTOMATIC_OPTIONS"] = False
class Index(flask.views.View):
provide_automatic_options = True provide_automatic_options = True
def dispatch_request(self): def dispatch_request(self):
return "Hello World!" return "Hello World!"
app.add_url_rule("/", view_func=Index2.as_view("index")) app.add_url_rule("/", view_func=Index.as_view("index"))
c = app.test_client() rv = client.options("/")
rv = c.open("/", method="OPTIONS") assert rv.allow == {"GET", "HEAD", "OPTIONS"}
assert sorted(rv.allow) == ["OPTIONS"]
app = flask.Flask(__name__)
class Index3(flask.views.View): def test_provide_automatic_options_method_disable(
app: flask.Flask, client: FlaskClient
) -> None:
"""Automatic options is ignored if the route handles options."""
class Index(flask.views.View):
methods = ["OPTIONS"]
def dispatch_request(self): def dispatch_request(self):
return "Hello World!" return "", {"X-Test": "test"}
app.add_url_rule("/", view_func=Index3.as_view("index")) app.add_url_rule("/", view_func=Index.as_view("index"))
c = app.test_client() rv = client.options()
rv = c.open("/", method="OPTIONS") assert rv.headers["X-Test"] == "test"
assert "OPTIONS" in rv.allow
def test_implicit_head(app, client): def test_implicit_head(app, client):
@ -180,7 +192,7 @@ def test_endpoint_override(app):
app.add_url_rule("/", view_func=Index.as_view("index")) app.add_url_rule("/", view_func=Index.as_view("index"))
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
app.add_url_rule("/", view_func=Index.as_view("index")) app.add_url_rule("/other", view_func=Index.as_view("index"))
# But these tests should still pass. We just log a warning. # But these tests should still pass. We just log a warning.
common_test(app) common_test(app)

62
tox.ini
View file

@ -1,62 +0,0 @@
[tox]
envlist =
py3{13,12,11,10,9}
pypy310
py313-min
py39-dev
style
typing
docs
skip_missing_interpreters = true
[testenv]
package = wheel
wheel_build_env = .pkg
envtmpdir = {toxworkdir}/tmp/{envname}
constrain_package_deps = true
use_frozen_constraints = true
deps =
-r requirements/tests.txt
min: -r requirements/tests-min.txt
dev: -r requirements/tests-dev.txt
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
[testenv:style]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files
[testenv:typing]
deps = -r requirements/typing.txt
commands =
mypy
pyright
[testenv:docs]
deps = -r requirements/docs.txt
commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
[testenv:update-actions]
labels = update
deps = gha-update
skip_install = true
commands = gha-update
[testenv:update-pre_commit]
labels = update
deps = pre-commit
skip_install = true
commands = pre-commit autoupdate -j4
[testenv:update-requirements]
labels = update
deps = pip-tools
skip_install = true
change_dir = requirements
commands =
pip-compile build.in -q {posargs:-U}
pip-compile docs.in -q {posargs:-U}
pip-compile tests.in -q {posargs:-U}
pip-compile tests-min.in -q
pip-compile typing.in -q {posargs:-U}
pip-compile dev.in -q {posargs:-U}

Some files were not shown because too many files have changed in this diff Show more