diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml
index 533151a8..22228a1c 100644
--- a/.github/workflows/lock.yaml
+++ b/.github/workflows/lock.yaml
@@ -7,19 +7,16 @@ name: Lock inactive closed issues
on:
schedule:
- cron: '0 0 * * *'
-permissions: {}
+permissions:
+ issues: write
+ pull-requests: write
concurrency:
group: lock
- cancel-in-progress: true
jobs:
lock:
runs-on: ubuntu-latest
- permissions:
- issues: write
- pull-requests: write
- discussions: write
steps:
- - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
+ - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with:
issue-inactive-days: 14
pr-inactive-days: 14
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
index eff10995..263d42b3 100644
--- a/.github/workflows/pre-commit.yaml
+++ b/.github/workflows/pre-commit.yaml
@@ -3,27 +3,14 @@ on:
pull_request:
push:
branches: [main, stable]
-permissions: {}
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
jobs:
main:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
- 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
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
+ with:
+ python-version: 3.x
+ - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
+ - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
+ if: ${{ !cancelled() }}
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 0c4f301a..e6206667 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -1,51 +1,61 @@
name: Publish
on:
push:
- tags: ['*']
-permissions: {}
-concurrency:
- group: publish-${{ github.event.push.ref }}
- cancel-in-progress: true
+ tags:
+ - '*'
jobs:
build:
runs-on: ubuntu-latest
outputs:
- artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
+ hash: ${{ steps.hash.outputs.hash }}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.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: false
- prune-cache: false
- - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- with:
- python-version-file: pyproject.toml
+ python-version: '3.x'
+ cache: pip
+ cache-dependency-path: requirements*/*.txt
+ - run: pip install -r requirements/build.txt
+ # Use the commit date instead of the current date during the build.
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
- - run: uv build
- - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- id: upload-artifact
+ - run: python -m build
+ # Generate hashes used for provenance.
+ - name: generate hash
+ id: hash
+ run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
+ - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
- name: dist
- path: dist/
- if-no-files-found: error
- create-release:
+ path: ./dist
+ provenance:
needs: [build]
+ 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:
+ # Upload the sdist, wheels, and provenance to a GitHub release. They remain
+ # available as build artifacts for a while as well.
+ needs: [provenance]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- with:
- artifact-ids: ${{ needs.build.outputs.artifact-id }}
- path: dist/
+ - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
- name: create release
- run: gh release create --draft --repo ${GITHUB_REPOSITORY} ${GITHUB_REF_NAME} dist/*
+ run: >
+ gh release create --draft --repo ${{ github.repository }}
+ ${{ github.ref_name }}
+ *.intoto.jsonl/* artifact/*
env:
GH_TOKEN: ${{ github.token }}
publish-pypi:
- needs: [build]
+ needs: [provenance]
+ # Wait for approval before attempting to upload to PyPI. This allows reviewing the
+ # files in the draft release.
environment:
name: publish
url: https://pypi.org/project/Flask/${{ github.ref_name }}
@@ -53,10 +63,7 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+ - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
with:
- artifact-ids: ${{ needs.build.outputs.artifact-id }}
- path: dist/
- - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
- with:
- packages-dir: "dist/"
+ packages-dir: artifact/
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 97064c8c..42effa11 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,14 +1,10 @@
name: Tests
on:
- pull_request:
- paths-ignore: ['docs/**', 'README.md']
push:
branches: [main, stable]
- paths-ignore: ['docs/**', 'README.md']
-permissions: {}
-concurrency:
- group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
- cancel-in-progress: true
+ paths-ignore: ['docs/**', '*.md', '*.rst']
+ pull_request:
+ paths-ignore: [ 'docs/**', '*.md', '*.rst' ]
jobs:
tests:
name: ${{ matrix.name || matrix.python }}
@@ -17,47 +13,29 @@ jobs:
fail-fast: false
matrix:
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.12'}
+ - {name: Windows, python: '3.12', os: windows-latest}
+ - {name: Mac, python: '3.12', os: macos-latest}
- {python: '3.11'}
- {python: '3.10'}
- - {name: PyPy, python: 'pypy-3.11', tox: pypy3.11}
- - {name: Minimum Versions, python: '3.14', tox: tests-min}
- - {name: Development Versions, python: '3.10', tox: tests-dev}
+ - {python: '3.9'}
+ - {name: PyPy, python: 'pypy-3.10', tox: pypy310}
+ - {name: Minimum Versions, python: '3.12', tox: py-min}
+ - {name: Development Versions, python: '3.9', tox: py-dev}
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- 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
+ - uses: pallets/actions/tox@5c46d4abb052877fa7e84db0ceec21b33673559e
with:
+ environment: ${{ format('py{0}', matrix.python) }}
python-version: ${{ matrix.python }}
- - run: uv run --locked --no-default-groups --group dev tox run
- env:
- TOX_ENV: ${{ matrix.tox || format('py{0}', matrix.python) }}
typing:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: pallets/actions/tox@5c46d4abb052877fa7e84db0ceec21b33673559e
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:
- python-version-file: pyproject.toml
- - name: cache mypy
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
- with:
- path: ./.mypy_cache
- key: mypy|${{ hashFiles('pyproject.toml') }}
- - run: uv run --locked --no-default-groups --group dev tox run -e typing
+ environment: 'typing'
+ python-version: '3.x'
+ workflow:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: pallets/actions/zizmor@5c46d4abb052877fa7e84db0ceec21b33673559e
diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml
deleted file mode 100644
index 04082427..00000000
--- a/.github/workflows/zizmor.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/.gitignore b/.gitignore
index 8441e5a6..62c1b887 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
.idea/
.vscode/
+.venv*/
+venv*/
__pycache__/
dist/
.coverage*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5d1c89cb..7db182a0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,20 +1,11 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13
+ rev: v0.7.3
hooks:
- - id: ruff-check
+ - id: ruff
- 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
- rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0
+ rev: v5.0.0
hooks:
- id: check-merge-conflict
- id: debug-statements
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index acbd83f9..865c6859 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -1,10 +1,13 @@
version: 2
build:
- os: ubuntu-24.04
+ os: ubuntu-22.04
tools:
- python: '3.13'
- commands:
- - asdf plugin add uv
- - asdf install uv latest
- - asdf global uv latest
- - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html
+ python: '3.12'
+python:
+ install:
+ - requirements: requirements/docs.txt
+ - method: pip
+ path: .
+sphinx:
+ builder: dirhtml
+ fail_on_warning: true
diff --git a/CHANGES.rst b/CHANGES.rst
index 232b144a..d2b4cfe1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,66 +3,15 @@ Version 3.2.0
Unreleased
-- Drop support for Python 3.9. :pr:`5730`
- 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
-------------
-Released 2025-05-13
+Unreleased
-- 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`
+- Fix type hint for `cli_runner.invoke`. :issue:`5645`
Version 3.1.0
@@ -164,7 +113,6 @@ Released 2023-05-01
- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed.
- Update Werkzeug requirement to >=2.3.3 to apply recent bug fixes.
- :ghsa:`m2qf-hxjv-5gpq`
Version 2.3.1
@@ -1417,7 +1365,7 @@ Released 2011-09-29, codename Rakija
of Flask itself and no longer of the test client. This cleaned up
some internal logic and lowers the odds of runaway request contexts
in unittests.
-- Fixed the Jinja environment's ``list_templates`` method not
+- Fixed the Jinja2 environment's ``list_templates`` method not
returning the correct names when blueprints or modules were
involved.
@@ -1503,7 +1451,7 @@ Released 2010-12-31
- Fixed an issue where the default ``OPTIONS`` response was not
exposing all valid methods in the ``Allow`` header.
-- Jinja template loading syntax now allows "./" in front of a
+- Jinja2 template loading syntax now allows "./" in front of a
template load path. Previously this caused issues with module
setups.
- Fixed an issue where the subdomain setting for modules was ignored
diff --git a/README.md b/README.md
index 64f56cac..29312c65 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-
-
# Flask
Flask is a lightweight [WSGI] web application framework. It is designed
diff --git a/docs/_static/flask-horizontal.png b/docs/_static/flask-horizontal.png
new file mode 100644
index 00000000..a0df2c61
Binary files /dev/null and b/docs/_static/flask-horizontal.png differ
diff --git a/docs/_static/flask-icon.svg b/docs/_static/flask-icon.svg
deleted file mode 100644
index c802da9a..00000000
--- a/docs/_static/flask-icon.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
diff --git a/docs/_static/flask-logo.svg b/docs/_static/flask-logo.svg
deleted file mode 100644
index c216b617..00000000
--- a/docs/_static/flask-logo.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
diff --git a/docs/_static/flask-name.svg b/docs/_static/flask-name.svg
deleted file mode 100644
index b46782d2..00000000
--- a/docs/_static/flask-name.svg
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
diff --git a/docs/_static/flask-vertical.png b/docs/_static/flask-vertical.png
new file mode 100644
index 00000000..d1fd1499
Binary files /dev/null and b/docs/_static/flask-vertical.png differ
diff --git a/docs/_static/shortcut-icon.png b/docs/_static/shortcut-icon.png
new file mode 100644
index 00000000..4d3e6c37
Binary files /dev/null and b/docs/_static/shortcut-icon.png differ
diff --git a/docs/api.rst b/docs/api.rst
index 52b25376..1aa8048f 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -31,15 +31,17 @@ Incoming Request Data
:inherited-members:
:exclude-members: json_module
-.. data:: request
+.. attribute:: request
- A proxy to the request data for the current request, an instance of
- :class:`.Request`.
+ To access incoming request data, you can use the global `request`
+ object. Flask parses incoming request data for you and gives you
+ 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 only available when a :doc:`request context ` is
- active.
+ This is a proxy. See :ref:`notes-on-proxies` for more information.
- This is a proxy. See :ref:`context-visibility` for more information.
+ The request object is an instance of a :class:`~flask.Request`.
Response Objects
@@ -60,33 +62,40 @@ 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
set that to something complex and unguessable.
-To access the current session you can use the :data:`.session` proxy.
+To access the current session you can use the :class:`session` object:
-.. data:: session
+.. class:: session
- A proxy to the session data for the current request, an instance of
- :class:`.SessionMixin`.
+ The session object works pretty much like an ordinary dict, with the
+ difference that it keeps track of modifications.
- This is only available when a :doc:`request context ` is
- active.
+ This is a proxy. See :ref:`notes-on-proxies` for more information.
- This is a proxy. See :ref:`context-visibility` for more information.
+ The following attributes are interesting:
- 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.
+ .. attribute:: new
- .. code-block:: python
+ ``True`` if the session is new, ``False`` otherwise.
- # appending to a list is not detected
- session["numbers"].append(42)
+ .. attribute:: modified
+
+ ``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
session.modified = True
- The session is persisted across requests using a cookie. By default the
- users's browser will clear the cookie when it is closed. Set
- :attr:`~.SessionMixin.permanent` to ``True`` to persist the cookie for
- :data:`PERMANENT_SESSION_LIFETIME`.
+ .. attribute:: permanent
+
+ If set to ``True`` the session lives for
+ :attr:`~flask.Flask.permanent_session_lifetime` seconds. The
+ 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
@@ -149,21 +158,20 @@ another, a global variable is not good enough because it would break in
threaded environments. Flask provides you with a special object that
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
-thing, like it does for :data:`.request` and :data:`.session`.
+thing, like it does for :class:`request` and :class:`session`.
.. data:: g
- A proxy to a namespace object used to store data during a single request or
- app context. An instance of :attr:`.Flask.app_ctx_globals_class`, which
- defaults to :class:`._AppCtxGlobals`.
+ A namespace object that can store data during an
+ :doc:`application context `. This is an instance of
+ :attr:`Flask.app_ctx_globals_class`, which defaults to
+ :class:`ctx._AppCtxGlobals`.
- This is a good place to store resources during a request. For example, a
- :meth:`~.Flask.before_request` function could load a user object from a
- session id, then set ``g.user`` to be used in the view function.
+ This is a good place to store resources during a request. For
+ example, a ``before_request`` function could load a user object from
+ a session id, then set ``g.user`` to be used in the view function.
- This is only available when an :doc:`app context ` is active.
-
- This is a proxy. See :ref:`context-visibility` for more information.
+ This is a proxy. See :ref:`notes-on-proxies` for more information.
.. versionchanged:: 0.10
Bound to the application context instead of the request context.
@@ -177,16 +185,17 @@ Useful Functions and Classes
.. data:: current_app
- A proxy to the :class:`.Flask` application handling the current request or
- other activity.
+ A proxy to the application handling the current request. This is
+ useful to access the application without needing to import it, or if
+ it can't be imported, such as when using the application factory
+ pattern or in blueprints and extensions.
- This is useful to access the application without needing to import it, or if
- it can't be imported, such as when using the application factory pattern or
- in blueprints and extensions.
+ This is only available when an
+ :doc:`application context ` is pushed. This happens
+ automatically during requests and CLI commands. It can be controlled
+ manually with :meth:`~flask.Flask.app_context`.
- This is only available when an :doc:`app context ` is active.
-
- This is a proxy. See :ref:`context-visibility` for more information.
+ This is a proxy. See :ref:`notes-on-proxies` for more information.
.. autofunction:: has_request_context
@@ -290,31 +299,31 @@ Stream Helpers
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
:members:
.. data:: flask.globals.app_ctx
- A proxy to the active :class:`.AppContext`.
+ The current :class:`~flask.ctx.AppContext`. If an app 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:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session` instead.
-
- This is only available when a :doc:`request context ` 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.
+ 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:`current_app` and :data:`g` instead.
.. autoclass:: flask.blueprints.BlueprintSetupState
:members:
@@ -596,7 +605,7 @@ This specifies that ``/users/`` will be the URL for page one and
``/users/page/N`` will be the URL for page ``N``.
If a URL contains a default value, it will be redirected to its simpler
-form with a 308 redirect. In the above example, ``/users/page/1`` will
+form with a 301 redirect. In the above example, ``/users/page/1`` will
be redirected to ``/users/``. If your route handles ``GET`` and ``POST``
requests, make sure the default route only handles ``GET``, as redirects
can't preserve form data. ::
diff --git a/docs/appcontext.rst b/docs/appcontext.rst
index 883d8cd2..5509a9a7 100644
--- a/docs/appcontext.rst
+++ b/docs/appcontext.rst
@@ -1,63 +1,74 @@
-The App and Request Context
-===========================
+.. currentmodule:: flask
-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
+=======================
-When handling a request, the context is referred to as the "request context"
-because it contains request data in addition to application data. Otherwise,
-such as during a CLI command, it is referred to as the "app context". During an
-app context, :data:`.current_app` and :data:`.g` are available, while during a
-request context :data:`.request` and :data:`.session` are also available.
+The application context keeps track of the application-level data during
+a request, CLI command, or other activity. Rather than passing the
+application around to each function, the :data:`current_app` and
+:data:`g` proxies are accessed instead.
+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
----------------------
-The context and proxies help solve two development issues: circular imports, and
-passing around global data during a request.
+The :class:`Flask` application object has attributes, such as
+:attr:`~Flask.config`, that are useful to access within views and
+:doc:`CLI commands `. However, importing the ``app`` instance
+within the modules in your project is prone to circular import issues.
+When using the :doc:`app factory pattern ` or
+writing reusable :doc:`blueprints ` or
+:doc:`extensions ` there won't be an ``app`` instance to
+import at all.
-The :class:`.Flask` application object has attributes, such as
-:attr:`~.Flask.config`, that are useful to access within views and other
-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 ` or writing reusable
-:doc:`blueprints ` or :doc:`extensions ` there won't
-be an ``app`` instance to import at all.
+Flask solves this issue with the *application context*. Rather than
+referring to an ``app`` directly, you use the :data:`current_app`
+proxy, which points to the application handling the current activity.
-When the application handles a request, it creates a :class:`.Request` object.
-Because a *worker* handles only one request at a time, the request data can be
-considered global to that worker during that request. Passing it as an argument
-through every function during the request becomes verbose and redundant.
+Flask automatically *pushes* an application context when handling a
+request. View functions, error handlers, and other functions that run
+during a request will have access to :data:`current_app`.
-Flask solves these issues with the *active context* pattern. Rather than
-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.
+Flask will also automatically push an app context when running CLI
+commands registered with :attr:`Flask.cli` using ``@app.cli.command()``.
-Context During Setup
---------------------
+Lifetime of the Context
+-----------------------
-If you try to access :data:`.current_app`, :data:`.g`, or anything that uses it,
-outside an app context, you'll get this error message:
+The application context is created and destroyed as necessary. When a
+Flask application begins handling a request, it pushes an application
+context and a :doc:`request context `. 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
RuntimeError: Working outside of application context.
- Attempted to use functionality that expected a current application to be
- set. To solve this, set up an app context using 'with app.app_context()'.
- See the documentation on app context for more information.
+ This typically means that you attempted to use functionality that
+ needed to interface with the current application object in some way.
+ To solve this, set up an application context with app.app_context().
If you see that error while configuring your application, such as when
-initializing an extension, you can push a context manually since you have direct
-access to the ``app``. Use :meth:`.Flask.app_context` in a ``with`` block.
-
-.. code-block:: python
+initializing an extension, you can push a context manually since you
+have direct access to the ``app``. Use :meth:`~Flask.app_context` in a
+``with`` block, and everything that runs in the block will have access
+to :data:`current_app`. ::
def create_app():
app = Flask(__name__)
@@ -67,121 +78,70 @@ access to the ``app``. Use :meth:`.Flask.app_context` in a ``with`` block.
return app
-If you see that error somewhere else in your code not related to setting up the
-application, it most likely indicates that you should move that code into a view
-function or CLI command.
+If you see that error somewhere else in your code not related to
+configuring the application, it most likely indicates that you should
+move that code into a view function or CLI command.
-Context During Testing
-----------------------
+Storing Data
+------------
-See :doc:`/testing` for detailed information about managing the context during
-tests.
+The application context is a good place to store common data during a
+request or CLI command. Flask provides the :data:`g object ` for this
+purpose. It is a simple namespace object that has the same lifetime as
+an application context.
-If you try to access :data:`.request`, :data:`.session`, or anything that uses
-it, outside a request context, you'll get this error message:
+.. note::
+ The ``g`` name stands for "global", but that is referring to the
+ 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.
-.. code-block:: pytb
+A common use for :data:`g` is to manage resources during a request.
- RuntimeError: Working outside of request context.
+1. ``get_X()`` creates resource ``X`` if it does not exist, caching it
+ 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.
- Attempted to use functionality that expected an active HTTP request. See the
- documentation on request context for more information.
+For example, you can manage a database connection using this pattern::
-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.
+ from flask import g
-The primary way to solve this is to use :meth:`.Flask.test_client` to simulate
-a full request.
+ def get_db():
+ if 'db' not in g:
+ g.db = connect_to_database()
-If you only want to unit test one function, rather than a full request, use
-:meth:`.Flask.test_request_context` in a ``with`` block.
+ return g.db
-.. code-block:: python
+ @app.teardown_appcontext
+ def teardown_db(exception):
+ db = g.pop('db', None)
- def generate_report(year):
- format = request.args.get("format")
- ...
+ if db is not None:
+ db.close()
- with app.test_request_context(
- "/make_report/2017", query_string={"format": "short"}
- ):
- generate_report()
+During a request, every call to ``get_db()`` will return the same
+connection, and it will be closed automatically at the end of the
+request.
+
+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.
-.. _context-visibility:
+Events and Signals
+------------------
-Visibility of the Context
--------------------------
+The application will call functions registered with :meth:`~Flask.teardown_appcontext`
+when the application context is popped.
-The context will have the same lifetime as an activity, such as a request, CLI
-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.
+The following signals are sent: :data:`appcontext_pushed`,
+:data:`appcontext_tearing_down`, and :data:`appcontext_popped`.
diff --git a/docs/async-await.rst b/docs/async-await.rst
index bb333802..16b61945 100644
--- a/docs/async-await.rst
+++ b/docs/async-await.rst
@@ -23,6 +23,12 @@ 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
: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
-----------
@@ -72,15 +78,15 @@ Flask based on the `ASGI`_ standard instead of WSGI. This allows it to
handle many concurrent requests, long running requests, and websockets
without requiring multiple worker processes or threads.
-It has also already been possible to :doc:`run Flask with Gevent ` to
-get many of the benefits of async request handling. Gevent patches low-level
-Python functions to accomplish this, whereas ``async``/``await`` and ASGI use
-standard, modern Python capabilities. Deciding whether you should use gevent
-with Flask, or Quart, or something else is ultimately up to understanding the
-specific needs of your project.
+It has also already been possible to run Flask with Gevent or Eventlet
+to get many of the benefits of async request handling. These libraries
+patch low-level Python functions to accomplish this, whereas ``async``/
+``await`` and ASGI use standard, modern Python capabilities. Deciding
+whether you should use Flask, Quart, or something else is ultimately up
+to understanding the specific needs of your project.
-.. _Quart: https://quart.palletsprojects.com
-.. _ASGI: https://asgi.readthedocs.io
+.. _Quart: https://github.com/pallets/quart
+.. _ASGI: https://asgi.readthedocs.io/en/latest/
Extensions
@@ -114,6 +120,6 @@ implemented async support, or make a feature request or PR to them.
Other event loops
-----------------
-At the moment Flask only supports :mod:`asyncio`. It's possible to override
-:meth:`flask.Flask.ensure_sync` to change how async functions are wrapped to use
-a different library. See :ref:`gevent-asyncio` for an example.
+At the moment Flask only supports :mod:`asyncio`. It's possible to
+override :meth:`flask.Flask.ensure_sync` to change how async functions
+are wrapped to use a different library.
diff --git a/docs/conf.py b/docs/conf.py
index af8adf01..a7b8f919 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,7 +26,6 @@ autodoc_preserve_defaults = True
extlinks = {
"issue": ("https://github.com/pallets/flask/issues/%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 = {
"python": ("https://docs.python.org/3/", None),
@@ -58,8 +57,8 @@ html_sidebars = {
}
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
html_static_path = ["_static"]
-html_favicon = "_static/flask-icon.svg"
-html_logo = "_static/flask-logo.svg"
+html_favicon = "_static/shortcut-icon.png"
+html_logo = "_static/flask-vertical.png"
html_title = f"Flask Documentation ({version})"
html_show_sourcelink = False
diff --git a/docs/config.rst b/docs/config.rst
index 7138222d..5695bbd0 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -79,7 +79,7 @@ The following configuration values are used internally by Flask:
.. py:data:: TESTING
- Enable testing mode. Exceptions are propagated rather than handled by
+ Enable testing mode. Exceptions are propagated rather than handled by the
the app's error handlers. Extensions may also change their behavior to
facilitate easier testing. You should enable this in your own tests.
@@ -127,16 +127,13 @@ The following configuration values are used internally by Flask:
.. py:data:: SECRET_KEY_FALLBACKS
- A list of old secret keys that can still be used for unsigning. This allows
- a project to implement key rotation without invalidating active sessions or
- other recently-signed secrets.
+ A list of old secret keys that can still be used for unsigning, most recent
+ first. This allows a project to implement key rotation without invalidating
+ active sessions or other recently-signed secrets.
Keys should be removed after an appropriate period of time, as checking each
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
:data:`SECRET_KEY` may not support this yet.
@@ -445,7 +442,7 @@ The following configuration values are used internally by Flask:
.. versionchanged:: 2.3
``ENV`` was removed.
-.. versionadded:: 3.1
+.. versionadded:: 3.10
Added :data:`PROVIDE_AUTOMATIC_OPTIONS` to control the default
addition of autogenerated OPTIONS responses.
diff --git a/docs/contributing.rst b/docs/contributing.rst
index ca4b3aee..d44f865f 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -1,7 +1,7 @@
Contributing
============
-See the Pallets `detailed contributing documentation `_ for many ways
+See the Pallets `detailed contributing documentation <_contrib>`_ for many ways
to contribute, including reporting issues, requesting features, asking or
answering questions, and making PRs.
diff --git a/docs/deploying/eventlet.rst b/docs/deploying/eventlet.rst
index d4c9e817..8a718b22 100644
--- a/docs/deploying/eventlet.rst
+++ b/docs/deploying/eventlet.rst
@@ -1,8 +1,80 @@
-:orphan:
-
eventlet
========
-`Eventlet is no longer maintained.`__ Use :doc:`/deploying/gevent` instead.
+Prefer using :doc:`gunicorn` with eventlet workers rather than using
+`eventlet`_ directly. Gunicorn provides a much more configurable and
+production-tested server.
-__ https://eventlet.readthedocs.io
+`eventlet`_ allows writing asynchronous, coroutine-based code that looks
+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.
diff --git a/docs/deploying/gevent.rst b/docs/deploying/gevent.rst
index 8810d21c..448b93e7 100644
--- a/docs/deploying/gevent.rst
+++ b/docs/deploying/gevent.rst
@@ -7,12 +7,15 @@ configurable and production-tested servers.
`gevent`_ allows writing asynchronous, coroutine-based code that looks
like standard synchronous Python. It uses `greenlet`_ to enable task
-switching without writing ``async/await`` or using ``asyncio``. This is
-not the same as Python's ``async/await``, or the ASGI server spec.
+switching without writing ``async/await`` or using ``asyncio``.
+
+: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
-instead of one per worker process. See :doc:`/gevent` for more
-information about enabling it in your application.
+instead of one per worker process. You must actually use gevent in your
+own code to see any benefit to using the server.
.. _gevent: https://www.gevent.org/
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
@@ -21,7 +24,8 @@ information about enabling it in your application.
Installing
----------
-When using gevent, greenlet>=1.0 is required. When using PyPy,
+When using gevent, 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 ``gevent``.
diff --git a/docs/deploying/gunicorn.rst b/docs/deploying/gunicorn.rst
index 089cb914..c50edc23 100644
--- a/docs/deploying/gunicorn.rst
+++ b/docs/deploying/gunicorn.rst
@@ -8,7 +8,7 @@ multiple worker implementations for performance tuning.
* It does not support Windows (but does run on WSL).
* It is easy to install as it does not require additional dependencies
or compilation.
-* It has built-in async worker support using gevent.
+* It has built-in async worker support using gevent or eventlet.
This page outlines the basics of running Gunicorn. Be sure to read its
`documentation`_ and use ``gunicorn --help`` to understand what features
@@ -93,19 +93,20 @@ otherwise it will be possible to bypass the proxy.
IP address in your browser.
-Async with gevent
------------------
+Async with gevent or eventlet
+-----------------------------
-The default sync worker is appropriate for most use cases. If you need numerous,
-long running, concurrent connections, Gunicorn provides an asynchronous worker
-using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI
-server spec. See :doc:`/gevent` for more information about enabling it in your
-application.
+The default sync worker is appropriate for many use cases. If you need
+asynchronous support, Gunicorn provides workers using either `gevent`_
+or `eventlet`_. This is not the same as Python's ``async/await``, or the
+ASGI server spec. You must actually use gevent/eventlet in your own code
+to see any benefit to using the workers.
-.. _gevent: https://www.gevent.org/
+When using either gevent or 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.
-When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
-required.
+To use gevent:
.. code-block:: text
@@ -114,3 +115,16 @@ required.
Listening at: http://127.0.0.1:8000 (x)
Using worker: gevent
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/
diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst
index dcef8abe..4135596a 100644
--- a/docs/deploying/index.rst
+++ b/docs/deploying/index.rst
@@ -36,6 +36,7 @@ discusses platforms that can manage this for you.
mod_wsgi
uwsgi
gevent
+ eventlet
asgi
WSGI servers have HTTP servers built-in. However, a dedicated HTTP
diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst
index aa80b6fb..1f9d5eca 100644
--- a/docs/deploying/uwsgi.rst
+++ b/docs/deploying/uwsgi.rst
@@ -119,16 +119,15 @@ IP address in your browser.
Async with gevent
-----------------
-The default sync worker is appropriate for most use cases. If you need numerous,
-long running, concurrent connections, uWSGI provides an asynchronous worker
-using `gevent`_. This is not the same as Python's ``async/await``, or the ASGI
-server spec. See :doc:`/gevent` for more information about enabling it in your
-application.
+The default sync worker is appropriate for many use cases. If you need
+asynchronous support, uWSGI provides a `gevent`_ worker. This is not the
+same as Python's ``async/await``, or the ASGI server spec. You must
+actually use gevent in your own code to see any benefit to using the
+worker.
-.. _gevent: https://www.gevent.org/
-
-When using gevent, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
-required.
+When using gevent, 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.
.. code-block:: text
@@ -141,3 +140,6 @@ required.
spawned uWSGI worker 1 (pid: x, cores: 100)
spawned uWSGI http 1 (pid: x)
*** running gevent loop engine [addr:x] ***
+
+
+.. _gevent: https://www.gevent.org/
diff --git a/docs/design.rst b/docs/design.rst
index d8776a90..066cf107 100644
--- a/docs/design.rst
+++ b/docs/design.rst
@@ -96,10 +96,10 @@ is ambiguous.
One Template Engine
-------------------
-Flask decides on one template engine: Jinja. Why doesn't Flask have a
+Flask decides on one template engine: Jinja2. Why doesn't Flask have a
pluggable template engine interface? You can obviously use a different
-template engine, but Flask will still configure Jinja for you. While
-that limitation that Jinja is *always* configured will probably go away,
+template engine, but Flask will still configure Jinja2 for you. While
+that limitation that Jinja2 is *always* configured will probably go away,
the decision to bundle one template engine and use that will not.
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
of variables and take the return value as string.
-But that's about where similarities end. Jinja for example has an
+But that's about where similarities end. Jinja2 for example has an
extensive filter system, a certain way to do template inheritance,
support for reusable blocks (macros) that can be used from inside
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
framework there is more than just rendering templates. For instance,
-Flask uses Jinja's extensive autoescaping support. Also it provides
-ways to access macros from Jinja templates.
+Flask uses Jinja2's extensive autoescaping support. Also it provides
+ways to access macros from Jinja2 templates.
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
@@ -150,7 +150,7 @@ authentication technologies, and more. Flask may be "micro", but it's ready for
production use on a variety of needs.
Why does Flask call itself a microframework and yet it depends on two
-libraries (namely Werkzeug and Jinja). Why shouldn't it? If we look
+libraries (namely Werkzeug and Jinja2). Why shouldn't it? If we look
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
looks very much like a WSGI rendition for Ruby. But nearly all
@@ -169,20 +169,19 @@ infrastructure, packages with dependencies are no longer an issue and
there are very few reasons against having libraries that depend on others.
-Context Locals
---------------
+Thread Locals
+-------------
-Flask uses special context locals and proxies to provide access to the
-current app and request data to any code running during a request, CLI command,
-etc. Context locals are specific to the worker handling the activity, such as a
-thread, process, coroutine, or greenlet.
+Flask uses thread local objects (context local objects in fact, they
+support greenlet contexts as well) for request, session and an extra
+object you can put your own things on (:data:`~flask.g`). Why is that and
+isn't that a bad idea?
-The context and proxies help solve two development issues: circular imports, and
-passing around global data. :data:`.current_app` can be used to access the
-application object without needing to import the app object directly, avoiding
-circular import issues. :data:`.request`, :data:`.session`, and :data:`.g` can
-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.
+Yes it is usually not such a bright idea to use thread locals. They cause
+troubles for servers that are not based on the concept of threads and make
+large applications harder to maintain. However Flask is just not designed
+for large applications or asynchronous servers. Flask wants to make it
+quick and easy to write a traditional web application.
Async/await and ASGI support
@@ -209,7 +208,7 @@ What Flask is, What Flask is Not
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
-to implement a proper WSGI application and to Jinja to handle templating.
+to implement a proper WSGI application and to Jinja2 to handle templating.
It also binds to a few common standard library packages such as logging.
Everything else is up for extensions.
diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst
index ae5ed330..0c74ad92 100644
--- a/docs/extensiondev.rst
+++ b/docs/extensiondev.rst
@@ -67,7 +67,7 @@ application instance.
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
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,
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
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
-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
the data or connection. For example, a ``ext.get_db`` method could
@@ -179,12 +179,13 @@ name as a prefix, or as a namespace.
g._hello = SimpleNamespace()
g._hello.user_id = 2
-The data in ``g`` lasts for an application context. An application context is
-active during a request, CLI command, or ``with app.app_context()`` block. If
-you're storing something that should be closed, use
-:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed when the
-app context ends. If it should only be valid during a request, or would not be
-used in the CLI outside a request, use :meth:`~flask.Flask.teardown_request`.
+The data in ``g`` lasts for an application context. An application
+context is active when a request context is, or when a CLI command is
+run. If you're storing something that should be closed, use
+:meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed
+when the application context ends. If it should only be valid during a
+request, or would not be used in the CLI outside a request, use
+:meth:`~flask.Flask.teardown_request`.
Views and Models
@@ -293,13 +294,11 @@ ecosystem remain consistent and compatible.
indicate minimum compatibility support. For example,
``sqlalchemy>=1.4``.
9. Indicate the versions of Python supported using ``python_requires=">=version"``.
- Flask and Pallets policy is to support all Python versions that are not
- within six months of end of life (EOL). See Python's `EOL calendar`_ for
- timing.
+ Flask itself supports Python >=3.9 as of October 2024, and this will update
+ over time.
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
.. _Discord Chat: https://discord.gg/pallets
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/
.. _Pallets-Eco: https://github.com/pallets-eco
-.. _EOL calendar: https://devguide.python.org/versions/
diff --git a/docs/gevent.rst b/docs/gevent.rst
deleted file mode 100644
index 3ead05c5..00000000
--- a/docs/gevent.rst
+++ /dev/null
@@ -1,125 +0,0 @@
-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()
diff --git a/docs/index.rst b/docs/index.rst
index 12e4d39e..f9ab9bd9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,9 +3,8 @@
Welcome to Flask
================
-.. image:: _static/flask-name.svg
+.. image:: _static/flask-horizontal.png
:align: center
- :height: 200px
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
@@ -52,6 +51,7 @@ community-maintained extensions to add even more functionality.
views
lifecycle
appcontext
+ reqcontext
blueprints
extensions
cli
@@ -60,7 +60,6 @@ community-maintained extensions to add even more functionality.
patterns/index
web-security
deploying/index
- gevent
async-await
diff --git a/docs/installation.rst b/docs/installation.rst
index f3a8ab24..90a314bf 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -5,7 +5,7 @@ Installation
Python Version
--------------
-We recommend using the latest version of Python. Flask supports Python 3.10 and newer.
+We recommend using the latest version of Python. Flask supports Python 3.9 and newer.
Dependencies
@@ -51,8 +51,9 @@ use them if you install them.
greenlet
~~~~~~~~
-You may choose to use :doc:`/gevent` with your application. In this case,
-greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is required.
+You may choose to use gevent or eventlet with your application. In this
+case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
+required.
These are not minimum supported versions, they only indicate the first
versions that added necessary features. You should use the latest
diff --git a/docs/lifecycle.rst b/docs/lifecycle.rst
index 37d45cd9..2344d98a 100644
--- a/docs/lifecycle.rst
+++ b/docs/lifecycle.rst
@@ -117,12 +117,15 @@ 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.
#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`.
-#. An :class:`.AppContext` object is created. This converts the WSGI ``environ``
- dict into a :class:`.Request` object.
-#. The :doc:`app context ` is pushed, which makes
- :data:`.current_app`, :data:`.g`, :data:`.request`, and :data:`.session`
- available.
+#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ``
+ dict into a :class:`.Request` object. It also creates an :class:`AppContext` object.
+#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and
+ :data:`.g` available.
#. The :data:`.appcontext_pushed` signal is sent.
+#. The :doc:`request context ` 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`
decorator during application setup. If there is no match, the error - usually a 404,
405, or redirect - is stored to be handled later.
@@ -138,8 +141,7 @@ parts that you can use to customize its behavior.
called to handle the error and return a response.
#. Whatever returned a response value - a before request function, the view, or an
error handler, that value is converted to a :class:`.Response` object.
-#. Any :func:`~.after_this_request` decorated functions are called, which can modify
- the response object. They are then cleared.
+#. Any :func:`~.after_this_request` decorated functions are called, then cleared.
#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify
the response object.
#. The session is saved, persisting any modified session data using the app's
@@ -152,19 +154,14 @@ parts that you can use to customize its behavior.
#. The response object's status, headers, and body are returned to the WSGI server.
#. Any :meth:`~.Flask.teardown_request` decorated functions are called.
#. 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.
#. The :data:`.appcontext_tearing_down` signal is sent.
-#. The app context is popped, :data:`.current_app`, :data:`.g`, :data:`.request`,
- and :data:`.session` are no longer available.
+#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer
+ available.
#. 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
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
diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst
index 0f248783..32fd062b 100644
--- a/docs/patterns/appfactories.rst
+++ b/docs/patterns/appfactories.rst
@@ -99,9 +99,9 @@ to the factory like this:
.. 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 ``hello`` is called with the keyword
+Then the ``create_app`` factory in ``myapp`` is called with the keyword
argument ``local_auth=True``. See :doc:`/cli` for more detail.
Factory Improvements
diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst
index b867854f..21ea767f 100644
--- a/docs/patterns/favicon.rst
+++ b/docs/patterns/favicon.rst
@@ -24,11 +24,8 @@ 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
however your application is the root you can simply route a redirect::
- app.add_url_rule(
- "/favicon.ico",
- endpoint="favicon",
- redirect_to=url_for("static", filename="favicon.ico"),
- )
+ app.add_url_rule('/favicon.ico',
+ redirect_to=url_for('static', filename='favicon.ico'))
If you want to save the extra redirect request you can also write a view
using :func:`~flask.send_from_directory`::
diff --git a/docs/patterns/javascript.rst b/docs/patterns/javascript.rst
index b97ffc6d..d58a3eb6 100644
--- a/docs/patterns/javascript.rst
+++ b/docs/patterns/javascript.rst
@@ -136,8 +136,7 @@ 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
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
-sent as well, otherwise Flask will return a 415 Unsupported Media Type
-error.
+sent as well, otherwise Flask will return a 400 error.
.. code-block:: javascript
@@ -245,9 +244,8 @@ Receiving JSON in Views
Use the :attr:`~flask.Request.json` property of the
:data:`~flask.request` object to decode the request's body as JSON. If
-the body is not valid JSON, a 400 Bad Request error will be raised. If
-the ``Content-Type`` header is not set to ``application/json``, a 415
-Unsupported Media Type error will be raised.
+the body is not valid JSON, or the ``Content-Type`` header is not set to
+``application/json``, a 400 Bad Request error will be raised.
.. code-block:: python
diff --git a/docs/patterns/mongoengine.rst b/docs/patterns/mongoengine.rst
index 8d49de7c..015e7b61 100644
--- a/docs/patterns/mongoengine.rst
+++ b/docs/patterns/mongoengine.rst
@@ -10,7 +10,8 @@ A running MongoDB server and `Flask-MongoEngine`_ are required. ::
pip install flask-mongoengine
.. _MongoEngine: http://mongoengine.org
-.. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/
+.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io
+
Configuration
-------------
@@ -79,7 +80,7 @@ Queries
Use the class ``objects`` attribute to make queries. A keyword argument
looks for an equal value on the field. ::
- bttf = Movie.objects(title="Back To The Future").get_or_404()
+ bttf = Movies.objects(title="Back To The Future").get_or_404()
Query operators may be used by concatenating them with the field name
using a double-underscore. ``objects``, and queries returned by
diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst
index 9e9afe48..7e4108d0 100644
--- a/docs/patterns/sqlalchemy.rst
+++ b/docs/patterns/sqlalchemy.rst
@@ -131,8 +131,9 @@ Here is an example :file:`database.py` module for your application::
def init_db():
metadata.create_all(bind=engine)
-As in the declarative approach, you need to close the session after each app
-context. Put this into your application module::
+As in the declarative approach, you need to close the session after
+each request or application context shutdown. Put this into your
+application module::
from yourapplication.database import db_session
diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst
index f42e0f8e..5932589f 100644
--- a/docs/patterns/sqlite3.rst
+++ b/docs/patterns/sqlite3.rst
@@ -1,9 +1,9 @@
Using SQLite 3 with Flask
=========================
-You can implement a few functions to work with a SQLite connection during a
-request context. The connection is created the first time it's accessed,
-reused on subsequent access, until it is closed when the request context ends.
+In Flask you can easily implement the opening of database connections on
+demand and closing them when the context dies (usually at the end of the
+request).
Here is a simple example of how you can use SQLite 3 with Flask::
diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst
index fc2f1739..c9e6ef22 100644
--- a/docs/patterns/streaming.rst
+++ b/docs/patterns/streaming.rst
@@ -8,21 +8,6 @@ roundtrip to the filesystem?
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
-----------
@@ -44,7 +29,7 @@ debug environments with profilers and other things you might have enabled.
Streaming from Templates
------------------------
-The Jinja template engine supports rendering a template piece by
+The Jinja2 template engine supports rendering a template piece by
piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use.
@@ -64,13 +49,13 @@ the template.
Streaming with Context
----------------------
-The :data:`.request` proxy will not be active while the generator is
-running, because the app has already returned control to the WSGI server at that
-point. If you try to access ``request``, you'll get a ``RuntimeError``.
+The :data:`~flask.request` will not be active while the generator is
+running, because the view has already returned at that point. If you try
+to access ``request``, you'll get a ``RuntimeError``.
If your generator function relies on data in ``request``, use the
-:func:`.stream_with_context` wrapper. This will keep the request context active
-during the generator.
+:func:`~flask.stream_with_context` wrapper. This will keep the request
+context active during the generator.
.. code-block:: python
diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst
index cb1208fe..3d626f50 100644
--- a/docs/patterns/wtforms.rst
+++ b/docs/patterns/wtforms.rst
@@ -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
call ``render_field(form.username, class='username')`` to add a class to
the input element. Note that WTForms returns standard Python strings,
-so we have to tell Jinja that this data is already HTML-escaped with
+so we have to tell Jinja2 that this data is already HTML-escaped with
the ``|safe`` filter.
Here is the :file:`register.html` template for the function we used above, which
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 712ba977..f763bb1e 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -139,16 +139,18 @@ how you're using untrusted data.
.. code-block:: python
- from flask import request
from markupsafe import escape
- @app.route("/hello")
- def hello():
- name = request.args.get("name", "Flask")
+ @app.route("/")
+ def hello(name):
return f"Hello, {escape(name)}!"
-If a user submits ``/hello?name=``, escaping causes
-it to be rendered as text, rather than running the script in the user's browser.
+If a user managed to submit the name ````,
+escaping causes it to be rendered as text, rather than running the
+script in the user's browser.
+
+```` in the route captures a value from the URL and passes it to
+the view function. These variable rules are explained below.
Routing
@@ -258,7 +260,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
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
-Python shell. See :doc:`/appcontext`.
+Python shell. See :ref:`context-locals`.
.. code-block:: python
@@ -352,7 +354,7 @@ Rendering Templates
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
-the application secure. Because of that Flask configures the `Jinja
+the application secure. Because of that Flask configures the `Jinja2
`_ template engine for you automatically.
Templates can be used to generate any type of text file. For web applications, you'll
@@ -392,8 +394,8 @@ package it's actually inside your package:
/templates
/hello.html
-For templates you can use the full power of Jinja templates. Head over
-to the official `Jinja Template Documentation
+For templates you can use the full power of Jinja2 templates. Head over
+to the official `Jinja2 Template Documentation
`_ for more information.
Here is an example template:
@@ -449,58 +451,105 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:
Accessing Request Data
----------------------
-For web applications it's crucial to react to the data a client sends to the
-server. In Flask this information is provided by the global :data:`.request`
-object, which is an instance of :class:`.Request`. This object has many
-attributes and methods to work with the incoming request data, but here is a
-broad overview. First it needs to be imported.
+For web applications it's crucial to react to the data a client sends to
+the server. In Flask this information is provided by the global
+:class:`~flask.request` object. If you have some experience with Python
+you might be wondering how that object can be global and how Flask
+manages to still be threadsafe. The answer is context locals:
-.. 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
-If you have some experience with Python you might be wondering how that object
-can be global when Flask handles multiple requests at a time. The answer is
-that :data:`.request` is actually a proxy, pointing at whatever request is
-currently being handled by a given worker, which is managed internally by Flask
-and Python. See :doc:`/appcontext` for much more information.
+ with app.test_request_context('/hello', method='POST'):
+ # now you can do something with the request until the
+ # end of the with block, such as basic assertions:
+ assert request.path == '/hello'
+ assert request.method == 'POST'
-The current request method is available in the :attr:`~.Request.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.
+The other possibility is passing a whole WSGI environment to the
+:meth:`~flask.Flask.request_context` method::
-.. code-block:: python
+ with app.request_context(environ):
+ assert request.method == 'POST'
- @app.route("/login", methods=["GET", "POST"])
+The Request Object
+``````````````````
+
+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():
error = None
-
- if request.method == "POST":
- if valid_login(request.form["username"], request.form["password"]):
- return store_login(request.form["username"])
+ if request.method == 'POST':
+ if valid_login(request.form['username'],
+ request.form['password']):
+ return log_the_user_in(request.form['username'])
else:
- error = "Invalid username or password"
+ error = 'Invalid username/password'
+ # the code below is executed if the request method
+ # was GET or the credentials were invalid
+ return render_template('login.html', error=error)
- # 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
+case a special :exc:`KeyError` is raised. You can catch it like a
+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.
-If the key does not exist in ``form``, a special :exc:`KeyError` is raised. You
-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
+To access parameters submitted in the URL (``?key=value``) you can use the
+:attr:`~flask.Request.args` attribute::
searchword = request.args.get('key', '')
-For a full list of methods and attributes of the request object, see the
-:class:`~.Request` documentation.
+We recommend accessing URL parameters with `get` or by catching the
+:exc:`KeyError` because users might change the URL and presenting them a 400
+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
diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst
index 6660671e..4f1846a3 100644
--- a/docs/reqcontext.rst
+++ b/docs/reqcontext.rst
@@ -1,6 +1,243 @@
-:orphan:
+.. currentmodule:: flask
The Request Context
===================
-Obsolete, see :doc:`/appcontext` instead.
+The request context keeps track of the request-level data during a
+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 `. 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 ` 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)
diff --git a/docs/server.rst b/docs/server.rst
index d6beb1d8..11e976bc 100644
--- a/docs/server.rst
+++ b/docs/server.rst
@@ -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
5000. You can choose to disable this service instead of using a different port by
-searching for "AirPlay Receiver" in System Settings and toggling it off.
+searching for "AirPlay Receiver" in System Preferences and toggling it off.
Deferred Errors on Reload
diff --git a/docs/shell.rst b/docs/shell.rst
index d8821e23..7e42e285 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -1,37 +1,56 @@
Working with the Shell
======================
-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.
+.. versionadded:: 0.3
-.. code-block:: text
+One of the reasons everybody loves Python is the interactive shell. It
+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.
- $ flask shell
+There are however some handy helpers to make playing around in the shell a
+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
--------------------------
-``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
using the :attr:`~flask.Flask.test_request_context` method which creates
us a :class:`~flask.ctx.RequestContext`:
>>> ctx = app.test_request_context()
-Normally you would use the ``with`` statement to make this context active, but
-in the shell it's easier to call :meth:`~.RequestContext.push` and
-:meth:`~.RequestContext.pop` manually:
+Normally you would use the ``with`` statement to make this request object
+active, but in the shell it's easier to use the
+:meth:`~flask.ctx.RequestContext.push` and
+:meth:`~flask.ctx.RequestContext.pop` methods by hand:
>>> ctx.push()
-From that point onwards you can work with the request object until you call
-``pop``:
+From that point onwards you can work with the request object until you
+call `pop`:
>>> ctx.pop()
diff --git a/docs/signals.rst b/docs/signals.rst
index 7ca81a9d..739bb0b5 100644
--- a/docs/signals.rst
+++ b/docs/signals.rst
@@ -144,10 +144,11 @@ function, you can pass ``current_app._get_current_object()`` as sender.
Signals and Flask's Request Context
-----------------------------------
-Context-local proxies are available between :data:`~flask.request_started` and
-:data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others
-as needed. Note the limitations described in :ref:`signals-sending` and the
-:data:`~flask.request_tearing_down` signal.
+Signals fully support :doc:`reqcontext` when receiving signals.
+Context-local variables are consistently available between
+:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can
+rely on :class:`flask.g` and others as needed. Note the limitations described
+in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal.
Decorator Based Signal Subscriptions
diff --git a/docs/templating.rst b/docs/templating.rst
index ed4a52ee..23cfee4c 100644
--- a/docs/templating.rst
+++ b/docs/templating.rst
@@ -1,21 +1,21 @@
Templates
=========
-Flask leverages Jinja as its template engine. You are obviously free to use
-a different template engine, but you still have to install Jinja to run
+Flask leverages Jinja2 as its template engine. You are obviously free to use
+a different template engine, but you still have to install Jinja2 to run
Flask itself. This requirement is necessary to enable rich extensions.
-An extension can depend on Jinja being present.
+An extension can depend on Jinja2 being present.
-This section only gives a very quick introduction into how Jinja
+This section only gives a very quick introduction into how Jinja2
is integrated into Flask. If you want information on the template
-engine's syntax itself, head over to the official `Jinja Template
+engine's syntax itself, head over to the official `Jinja2 Template
Documentation `_ for
more information.
Jinja Setup
-----------
-Unless customized, Jinja is configured by Flask as follows:
+Unless customized, Jinja2 is configured by Flask as follows:
- autoescaping is enabled for all templates ending in ``.html``,
``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using
@@ -25,13 +25,13 @@ Unless customized, Jinja is configured by Flask as follows:
- a template has the ability to opt in/out autoescaping with the
``{% autoescape %}`` tag.
- Flask inserts a couple of global functions and helpers into the
- Jinja context, additionally to the values that are present by
+ Jinja2 context, additionally to the values that are present by
default.
Standard Context
----------------
-The following global variables are available within Jinja templates
+The following global variables are available within Jinja2 templates
by default:
.. data:: config
@@ -137,58 +137,32 @@ using in this block.
.. _registering-filters:
-Registering Filters, Tests, and Globals
----------------------------------------
+Registering Filters
+-------------------
-The Flask app and blueprints provide decorators and methods to register your own
-filters, tests, and global functions for use in Jinja templates. They all follow
-the same pattern, so the following examples only discuss filters.
+If you want to register your own filters in Jinja2 you have two ways to do
+that. You can either put them by hand into the
+:attr:`~flask.Flask.jinja_env` of the application or use the
+:meth:`~flask.Flask.template_filter` decorator.
-Decorate a function with :meth:`~.Flask.template_filter` to register it as a
-template filter.
+The two following examples work the same and both reverse an object::
-.. code-block:: python
+ @app.template_filter('reverse')
+ def reverse_filter(s):
+ return s[::-1]
- @app.template_filter
- def reverse(s):
- return reversed(s)
+ def reverse_filter(s):
+ return s[::-1]
+ app.jinja_env.filters['reverse'] = reverse_filter
-.. code-block:: jinja
+In case of the decorator the argument is optional if you want to use the
+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 item in data | reverse %}
+ {% for x in mylist | reverse %}
{% 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
------------------
@@ -237,7 +211,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
template.
-The Jinja template engine supports rendering a template piece
+The Jinja2 template engine supports rendering a template piece
by piece, returning an iterator of strings. Flask provides the
:func:`~flask.stream_template` and :func:`~flask.stream_template_string`
functions to make this easier to use.
@@ -251,11 +225,5 @@ functions to make this easier to use.
return stream_template("timeline.html")
These functions automatically apply the
-:func:`~flask.stream_with_context` wrapper if a request is active, so that
-: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.
+:func:`~flask.stream_with_context` wrapper if a request is active, so
+that it remains available in the template.
diff --git a/docs/testing.rst b/docs/testing.rst
index c171abd6..b1d52f9a 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -275,10 +275,11 @@ command from the command line.
Tests that depend on an Active Context
--------------------------------------
-You may have functions that are called from views or commands, that expect an
-active :doc:`app context ` because they access :data:`.request`,
-:data:`.session`, :data:`.g`, or :data:`.current_app`. Rather than testing them by
-making a request or invoking the command, you can create and activate a context
+You may have functions that are called from views or commands, that
+expect an active :doc:`application context ` or
+:doc:`request context ` because they access ``request``,
+``session``, or ``current_app``. Rather than testing them by making a
+request or invoking the command, you can create and activate a context
directly.
Use ``with app.app_context()`` to push an application context. For
diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst
index 6418f5ff..b06329ea 100644
--- a/docs/tutorial/blog.rst
+++ b/docs/tutorial/blog.rst
@@ -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
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
-``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.
diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst
index cf132603..93abf93a 100644
--- a/docs/tutorial/database.rst
+++ b/docs/tutorial/database.rst
@@ -60,17 +60,17 @@ response is sent.
if db is not None:
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
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
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,
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
-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
by the ``DATABASE`` configuration key. This file doesn't have to exist
diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst
index 381477f9..39febd13 100644
--- a/docs/tutorial/factory.rst
+++ b/docs/tutorial/factory.rst
@@ -56,7 +56,10 @@ directory should be treated as a package.
app.config.from_mapping(test_config)
# ensure the instance folder exists
- os.makedirs(app.instance_path, exist_ok=True)
+ try:
+ os.makedirs(app.instance_path)
+ except OSError:
+ pass
# a simple page that says hello
@app.route('/hello')
diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst
index 9f510927..6f8e59f4 100644
--- a/docs/tutorial/layout.rst
+++ b/docs/tutorial/layout.rst
@@ -81,7 +81,8 @@ By the end, your project layout will look like this:
│ ├── test_auth.py
│ └── test_blog.py
├── .venv/
- └── pyproject.toml
+ ├── pyproject.toml
+ └── MANIFEST.in
If you're using version control, the following files that are generated
while running your project should be ignored. There may be other files
@@ -102,4 +103,8 @@ write. For example, with git:
.coverage
htmlcov/
+ dist/
+ build/
+ *.egg-info/
+
Continue to :doc:`factory`.
diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst
index ca9d4b32..1a5535cc 100644
--- a/docs/tutorial/templates.rst
+++ b/docs/tutorial/templates.rst
@@ -71,7 +71,7 @@ specific sections.
{% block content %}{% endblock %}
-: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
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
diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst
index 8958e773..f4744cda 100644
--- a/docs/tutorial/tests.rst
+++ b/docs/tutorial/tests.rst
@@ -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
``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
: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
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.
-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.
.. code-block:: python
diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst
index 6626628a..7092dbc2 100644
--- a/docs/tutorial/views.rst
+++ b/docs/tutorial/views.rst
@@ -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
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
session. The data is stored in a *cookie* that is sent to the
browser, and the browser then sends it back with subsequent requests.
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
a user is logged in their information should be loaded and made
available to other views.
@@ -236,7 +236,7 @@ available to other views.
:meth:`bp.before_app_request() ` registers
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
-: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 `, 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
``None``.
@@ -245,7 +245,7 @@ there is no user id, or if the id doesn't exist, ``g.user`` will be
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.
.. code-block:: python
diff --git a/docs/web-security.rst b/docs/web-security.rst
index 4118b5ec..f13bb7b8 100644
--- a/docs/web-security.rst
+++ b/docs/web-security.rst
@@ -51,12 +51,12 @@ tags. For more information on that have a look at the Wikipedia article
on `Cross-Site Scripting
`_.
-Flask configures Jinja to automatically escape all values unless
+Flask configures Jinja2 to automatically escape all values unless
explicitly told otherwise. This should rule out all XSS problems caused
in templates, but there are still other places where you have to be
careful:
-- generating HTML without the help of Jinja
+- generating HTML without the help of Jinja2
- calling :class:`~markupsafe.Markup` on data submitted by users
- sending out HTML from uploaded files, never do that, use the
``Content-Disposition: attachment`` header to prevent that problem.
@@ -65,7 +65,7 @@ careful:
trick a browser to execute HTML.
Another thing that is very important are unquoted attributes. While
-Jinja can protect you from XSS issues by escaping HTML, there is one
+Jinja2 can protect you from XSS issues by escaping HTML, there is one
thing it cannot protect you from: XSS by attribute injection. To counter
this possible attack vector, be sure to always quote your attributes with
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
headers for you.
-.. _Flask-Talisman: https://github.com/wntrblm/flask-talisman
+.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman
HTTP Strict Transport Security (HSTS)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -269,25 +269,17 @@ values (or any values that need secure signatures).
.. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute
-Host Header Validation
-----------------------
+HTTP Public Key Pinning (HPKP)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The ``Host`` header is used by the client to indicate what host name the request
-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.
+This tells the browser to authenticate with the server using only the specific
+certificate key to prevent MITM attacks.
-By default the app doesn't know what host(s) it is allowed to be accessed
-through, and assumes any host is valid. Although browsers do not allow setting
-the ``Host`` header, requests made by attackers in other scenarios could set
-the ``Host`` header to a value they want.
+.. warning::
+ Be careful when enabling this, as it is very difficult to undo if you set up
+ or upgrade your key incorrectly.
-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.
+- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning
Copy/Paste to Terminal
diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py
index ab96e719..e35934d6 100644
--- a/examples/tutorial/flaskr/__init__.py
+++ b/examples/tutorial/flaskr/__init__.py
@@ -21,7 +21,10 @@ def create_app(test_config=None):
app.config.update(test_config)
# ensure the instance folder exists
- os.makedirs(app.instance_path, exist_ok=True)
+ try:
+ os.makedirs(app.instance_path)
+ except OSError:
+ pass
@app.route("/hello")
def hello():
diff --git a/pyproject.toml b/pyproject.toml
index 1bc3e0e1..f11add65 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,14 +3,14 @@ name = "Flask"
version = "3.2.0.dev"
description = "A simple framework for building complex web applications."
readme = "README.md"
-license = "BSD-3-Clause"
-license-files = ["LICENSE.txt"]
+license = {file = "LICENSE.txt"}
maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Flask",
"Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
@@ -19,70 +19,32 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Typing :: Typed",
]
-requires-python = ">=3.10"
+requires-python = ">=3.9"
dependencies = [
- "blinker>=1.9.0",
+ "Werkzeug>=3.1",
+ "Jinja2>=3.1.2",
+ "itsdangerous>=2.2",
"click>=8.1.3",
- "itsdangerous>=2.2.0",
- "jinja2>=3.1.2",
- "markupsafe>=2.1.1",
- "werkzeug>=3.1.0",
-]
-
-[project.optional-dependencies]
-async = ["asgiref>=3.2"]
-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",
+ "blinker>=1.9",
+ "importlib-metadata>=3.6; python_version < '3.10'",
]
[project.urls]
Donate = "https://palletsprojects.com/donate"
Documentation = "https://flask.palletsprojects.com/"
-Changes = "https://flask.palletsprojects.com/page/changes/"
+Changes = "https://flask.palletsprojects.com/changes/"
Source = "https://github.com/pallets/flask/"
Chat = "https://discord.gg/pallets"
+[project.optional-dependencies]
+async = ["asgiref>=3.2"]
+dotenv = ["python-dotenv"]
+
[project.scripts]
flask = "flask.cli:main"
[build-system]
-requires = ["flit_core>=3.11,<4"]
+requires = ["flit_core<4"]
build-backend = "flit_core.buildapi"
[tool.flit.module]
@@ -92,17 +54,16 @@ name = "flask"
include = [
"docs/",
"examples/",
+ "requirements/",
"tests/",
"CHANGES.rst",
- "uv.lock"
+ "CONTRIBUTING.rst",
+ "tox.ini",
]
exclude = [
"docs/_build/",
]
-[tool.uv]
-default-groups = ["dev", "pre-commit", "tests", "typing"]
-
[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = [
@@ -116,16 +77,9 @@ source = ["flask", "tests"]
[tool.coverage.paths]
source = ["src", "*/site-packages"]
-[tool.coverage.report]
-exclude_also = [
- "if t.TYPE_CHECKING",
- "raise NotImplementedError",
- ": \\.{3}",
-]
-
[tool.mypy]
-python_version = "3.10"
-files = ["src", "tests/type_check"]
+python_version = "3.9"
+files = ["src/flask", "tests/type_check"]
show_error_codes = true
pretty = true
strict = true
@@ -140,8 +94,8 @@ module = [
ignore_missing_imports = true
[tool.pyright]
-pythonVersion = "3.10"
-include = ["src", "tests/type_check"]
+pythonVersion = "3.9"
+include = ["src/flask", "tests/type_check"]
typeCheckingMode = "basic"
[tool.ruff]
@@ -164,114 +118,7 @@ select = [
force-single-line = true
order-by-type = false
-[tool.codespell]
-ignore-words-list = "te"
-
-[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.gha-update]
+tag-only = [
+ "slsa-framework/slsa-github-generator",
]
-
-[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}]]
diff --git a/requirements/build.in b/requirements/build.in
new file mode 100644
index 00000000..378eac25
--- /dev/null
+++ b/requirements/build.in
@@ -0,0 +1 @@
+build
diff --git a/requirements/build.txt b/requirements/build.txt
new file mode 100644
index 00000000..9d6dd104
--- /dev/null
+++ b/requirements/build.txt
@@ -0,0 +1,12 @@
+#
+# 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
diff --git a/requirements/dev.in b/requirements/dev.in
new file mode 100644
index 00000000..1efde82b
--- /dev/null
+++ b/requirements/dev.in
@@ -0,0 +1,5 @@
+-r docs.txt
+-r tests.txt
+-r typing.txt
+pre-commit
+tox
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 00000000..e1056206
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,198 @@
+#
+# 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
diff --git a/requirements/docs.in b/requirements/docs.in
new file mode 100644
index 00000000..fd5708f7
--- /dev/null
+++ b/requirements/docs.in
@@ -0,0 +1,4 @@
+pallets-sphinx-themes
+sphinx
+sphinxcontrib-log-cabinet
+sphinx-tabs
diff --git a/requirements/docs.txt b/requirements/docs.txt
new file mode 100644
index 00000000..47c22099
--- /dev/null
+++ b/requirements/docs.txt
@@ -0,0 +1,67 @@
+#
+# 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
diff --git a/requirements/tests-dev.txt b/requirements/tests-dev.txt
new file mode 100644
index 00000000..3e7f028e
--- /dev/null
+++ b/requirements/tests-dev.txt
@@ -0,0 +1,6 @@
+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
diff --git a/requirements/tests-min.in b/requirements/tests-min.in
new file mode 100644
index 00000000..e0c2fedd
--- /dev/null
+++ b/requirements/tests-min.in
@@ -0,0 +1,6 @@
+werkzeug==3.1.0
+jinja2==3.1.2
+markupsafe==2.1.1
+itsdangerous==2.2.0
+click==8.1.3
+blinker==1.9.0
diff --git a/requirements/tests-min.txt b/requirements/tests-min.txt
new file mode 100644
index 00000000..47857609
--- /dev/null
+++ b/requirements/tests-min.txt
@@ -0,0 +1,21 @@
+#
+# 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
diff --git a/requirements/tests.in b/requirements/tests.in
new file mode 100644
index 00000000..f4b3dad8
--- /dev/null
+++ b/requirements/tests.in
@@ -0,0 +1,4 @@
+pytest
+asgiref
+greenlet ; python_version < "3.11"
+python-dotenv
diff --git a/requirements/tests.txt b/requirements/tests.txt
new file mode 100644
index 00000000..cc8b9a2e
--- /dev/null
+++ b/requirements/tests.txt
@@ -0,0 +1,18 @@
+#
+# 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
diff --git a/requirements/typing.in b/requirements/typing.in
new file mode 100644
index 00000000..59128f34
--- /dev/null
+++ b/requirements/typing.in
@@ -0,0 +1,8 @@
+mypy
+pyright
+pytest
+types-contextvars
+types-dataclasses
+asgiref
+cryptography
+python-dotenv
diff --git a/requirements/typing.txt b/requirements/typing.txt
new file mode 100644
index 00000000..7eddb5f1
--- /dev/null
+++ b/requirements/typing.txt
@@ -0,0 +1,40 @@
+#
+# 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
diff --git a/src/flask/app.py b/src/flask/app.py
index 652b9bbf..905b2477 100644
--- a/src/flask/app.py
+++ b/src/flask/app.py
@@ -1,13 +1,11 @@
from __future__ import annotations
import collections.abc as cabc
-import inspect
import os
import sys
import typing as t
import weakref
from datetime import timedelta
-from functools import update_wrapper
from inspect import iscoroutinefunction
from itertools import chain
from types import TracebackType
@@ -31,17 +29,20 @@ from werkzeug.wsgi import get_host
from . import cli
from . import typing as ft
from .ctx import AppContext
+from .ctx import RequestContext
from .globals import _cv_app
-from .globals import app_ctx
+from .globals import _cv_request
+from .globals import current_app
from .globals import g
from .globals import request
+from .globals import request_ctx
from .globals import session
-from .helpers import _CollectErrors
from .helpers import get_debug_flag
from .helpers import get_flashed_messages
from .helpers import get_load_dotenv
from .helpers import send_from_directory
from .sansio.app import App
+from .sansio.scaffold import _sentinel
from .sessions import SecureCookieSessionInterface
from .sessions import SessionInterface
from .signals import appcontext_tearing_down
@@ -77,35 +78,6 @@ def _make_timedelta(value: timedelta | int | None) -> timedelta | None:
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):
"""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
@@ -251,62 +223,6 @@ class Flask(App):
#: .. versionadded:: 0.8
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__(
self,
import_name: str,
@@ -349,9 +265,9 @@ class Flask(App):
# For one, it might be created while the server is running (e.g. during
# development). Also, Google App Engine stores static files somewhere
if self.has_static_folder:
- assert bool(static_host) == host_matching, (
- "Invalid static_host/host_matching combination"
- )
+ assert (
+ bool(static_host) == host_matching
+ ), "Invalid static_host/host_matching combination"
# Use a weakref to avoid creating a reference cycle between the app
# and the view function (see #3761).
self_ref = weakref.ref(self)
@@ -359,7 +275,7 @@ class Flask(App):
f"{self.static_url_path}/",
endpoint="static",
host=static_host,
- view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore
+ view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950
)
def get_send_file_max_age(self, filename: str | None) -> int | None:
@@ -379,7 +295,7 @@ class Flask(App):
.. versionadded:: 0.9
"""
- value = self.config["SEND_FILE_MAX_AGE_DEFAULT"]
+ value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"]
if value is None:
return None
@@ -587,9 +503,7 @@ class Flask(App):
raise FormDataRoutingRedirect(request)
- def update_template_context(
- self, ctx: AppContext, context: dict[str, t.Any]
- ) -> None:
+ def update_template_context(self, context: dict[str, t.Any]) -> None:
"""Update the template context with some commonly used variables.
This injects request, session, config and g into the template
context as well as everything template context processors want
@@ -603,8 +517,8 @@ class Flask(App):
names: t.Iterable[str | None] = (None,)
# A template may be rendered outside a request context.
- if ctx.has_request:
- names = chain(names, reversed(ctx.request.blueprints))
+ if request:
+ names = chain(names, reversed(request.blueprints))
# The values passed to render_template take precedence. Keep a
# copy to re-apply after all context functions.
@@ -828,7 +742,7 @@ class Flask(App):
return cls(self, **kwargs) # type: ignore
def handle_http_exception(
- self, ctx: AppContext, e: HTTPException
+ self, e: HTTPException
) -> HTTPException | ft.ResponseReturnValue:
"""Handles an HTTP exception. By default this will invoke the
registered error handlers and fall back to returning the
@@ -857,13 +771,13 @@ class Flask(App):
if isinstance(e, RoutingException):
return e
- handler = self._find_error_handler(e, ctx.request.blueprints)
+ handler = self._find_error_handler(e, request.blueprints)
if handler is None:
return e
return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
def handle_user_exception(
- self, ctx: AppContext, e: Exception
+ self, e: Exception
) -> HTTPException | ft.ResponseReturnValue:
"""This method is called whenever an exception occurs that
should be handled. A special case is :class:`~werkzeug
@@ -885,16 +799,16 @@ class Flask(App):
e.show_exception = True
if isinstance(e, HTTPException) and not self.trap_http_exception(e):
- return self.handle_http_exception(ctx, e)
+ return self.handle_http_exception(e)
- handler = self._find_error_handler(e, ctx.request.blueprints)
+ handler = self._find_error_handler(e, request.blueprints)
if handler is None:
raise
return self.ensure_sync(handler)(e) # type: ignore[no-any-return]
- def handle_exception(self, ctx: AppContext, e: Exception) -> Response:
+ def handle_exception(self, e: Exception) -> Response:
"""Handle an exception that did not have an error handler
associated with it, or that was raised from an error handler.
This always causes a 500 ``InternalServerError``.
@@ -937,20 +851,19 @@ class Flask(App):
raise e
- self.log_exception(ctx, exc_info)
+ self.log_exception(exc_info)
server_error: InternalServerError | ft.ResponseReturnValue
server_error = InternalServerError(original_exception=e)
- handler = self._find_error_handler(server_error, ctx.request.blueprints)
+ handler = self._find_error_handler(server_error, request.blueprints)
if handler is not None:
server_error = self.ensure_sync(handler)(server_error)
- return self.finalize_request(ctx, server_error, from_error_handler=True)
+ return self.finalize_request(server_error, from_error_handler=True)
def log_exception(
self,
- ctx: AppContext,
- exc_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None],
+ exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]),
) -> None:
"""Logs an exception. This is called by :meth:`handle_exception`
if debugging is disabled and right before the handler is called.
@@ -960,10 +873,10 @@ class Flask(App):
.. versionadded:: 0.8
"""
self.logger.error(
- f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info
+ f"Exception on {request.path} [{request.method}]", exc_info=exc_info
)
- def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
+ def dispatch_request(self) -> ft.ResponseReturnValue:
"""Does the request dispatching. Matches the URL and returns the
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
@@ -973,8 +886,7 @@ class Flask(App):
This no longer does the exception handling, this code was
moved to the new :meth:`full_dispatch_request`.
"""
- req = ctx.request
-
+ req = request_ctx.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
rule: Rule = req.url_rule # type: ignore[assignment]
@@ -984,43 +896,31 @@ class Flask(App):
getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS"
):
- return self.make_default_options_response(ctx)
+ return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint
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]
- def full_dispatch_request(self, ctx: AppContext) -> Response:
+ def full_dispatch_request(self) -> Response:
"""Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and
error handling.
.. 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
try:
request_started.send(self, _async_wrapper=self.ensure_sync)
- rv = self.preprocess_request(ctx)
+ rv = self.preprocess_request()
if rv is None:
- rv = self.dispatch_request(ctx)
+ rv = self.dispatch_request()
except Exception as e:
- rv = self.handle_user_exception(ctx, e)
- return self.finalize_request(ctx, rv)
+ rv = self.handle_user_exception(e)
+ return self.finalize_request(rv)
def finalize_request(
self,
- ctx: AppContext,
rv: ft.ResponseReturnValue | HTTPException,
from_error_handler: bool = False,
) -> Response:
@@ -1038,7 +938,7 @@ class Flask(App):
"""
response = self.make_response(rv)
try:
- response = self.process_response(ctx, response)
+ response = self.process_response(response)
request_finished.send(
self, _async_wrapper=self.ensure_sync, response=response
)
@@ -1050,14 +950,15 @@ class Flask(App):
)
return response
- def make_default_options_response(self, ctx: AppContext) -> Response:
+ def make_default_options_response(self) -> Response:
"""This method is called to create the default ``OPTIONS`` response.
This can be changed through subclassing to change the default
behavior of ``OPTIONS`` responses.
.. versionadded:: 0.7
"""
- methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr]
+ adapter = request_ctx.url_adapter
+ methods = adapter.allowed_methods() # type: ignore[union-attr]
rv = self.response_class()
rv.allow.update(methods)
return rv
@@ -1156,9 +1057,11 @@ class Flask(App):
.. versionadded:: 2.2
Moved from ``flask.url_for``, which calls this method.
"""
- if (ctx := _cv_app.get(None)) is not None and ctx.has_request:
- url_adapter = ctx.url_adapter
- blueprint_name = ctx.request.blueprint
+ req_ctx = _cv_request.get(None)
+
+ if req_ctx is not None:
+ url_adapter = req_ctx.url_adapter
+ blueprint_name = req_ctx.request.blueprint
# If the endpoint starts with "." and the request matches a
# blueprint, the endpoint is relative to the blueprint.
@@ -1173,11 +1076,13 @@ class Flask(App):
if _external is None:
_external = _scheme is not None
else:
+ app_ctx = _cv_app.get(None)
+
# If called by helpers.url_for, an app context is active,
# use its url_adapter. Otherwise, app.url_for was called
# directly, build an adapter.
- if ctx is not None:
- url_adapter = ctx.url_adapter
+ if app_ctx is not None:
+ url_adapter = app_ctx.url_adapter
else:
url_adapter = self.create_url_adapter(None)
@@ -1317,7 +1222,7 @@ class Flask(App):
# waiting to do it manually, so that the class can handle any
# special logic
rv = self.response_class(
- rv, # pyright: ignore
+ rv,
status=status,
headers=headers, # type: ignore[arg-type]
)
@@ -1363,7 +1268,7 @@ class Flask(App):
return rv
- def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None:
+ def preprocess_request(self) -> ft.ResponseReturnValue | None:
"""Called before the request is dispatched. Calls
:attr:`url_value_preprocessors` registered with the app and the
current blueprint (if any). Then calls :attr:`before_request_funcs`
@@ -1373,13 +1278,12 @@ class Flask(App):
value is handled as if it was the return value from the view, and
further request handling is stopped.
"""
- req = ctx.request
- names = (None, *reversed(req.blueprints))
+ names = (None, *reversed(request.blueprints))
for name in names:
if name in self.url_value_preprocessors:
for url_func in self.url_value_preprocessors[name]:
- url_func(req.endpoint, req.view_args)
+ url_func(request.endpoint, request.view_args)
for name in names:
if name in self.before_request_funcs:
@@ -1391,7 +1295,7 @@ class Flask(App):
return None
- def process_response(self, ctx: AppContext, response: Response) -> Response:
+ def process_response(self, response: Response) -> Response:
"""Can be overridden in order to modify the response object
before it's sent to the WSGI server. By default this will
call all the :meth:`after_request` decorated functions.
@@ -1404,90 +1308,92 @@ class Flask(App):
:return: a new response object or the same, has to be an
instance of :attr:`response_class`.
"""
+ ctx = request_ctx._get_current_object() # type: ignore[attr-defined]
+
for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response)
- for name in chain(ctx.request.blueprints, (None,)):
+ for name in chain(request.blueprints, (None,)):
if name in self.after_request_funcs:
for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(func)(response)
- if not self.session_interface.is_null_session(ctx._get_session()):
- self.session_interface.save_session(self, ctx._get_session(), response)
+ if not self.session_interface.is_null_session(ctx.session):
+ self.session_interface.save_session(self, ctx.session, response)
return response
def do_teardown_request(
- self, ctx: AppContext, exc: BaseException | None = None
+ self,
+ exc: BaseException | None = _sentinel, # type: ignore[assignment]
) -> None:
- """Called after the request is dispatched and the response is finalized,
- right before the request context is popped. Called by
- :meth:`.AppContext.pop`.
+ """Called after the request is dispatched and the response is
+ returned, right before the request context is popped.
- This calls all functions decorated with :meth:`teardown_request`, and
- :meth:`Blueprint.teardown_request` if a blueprint handled the request.
- Finally, the :data:`request_tearing_down` signal is sent.
+ This calls all functions decorated with
+ :meth:`teardown_request`, and :meth:`Blueprint.teardown_request`
+ if a blueprint handled the request. Finally, the
+ :data:`request_tearing_down` signal is sent.
- :param exc: An unhandled exception raised while dispatching the request.
- Passed to each teardown function.
+ This is called by
+ :meth:`RequestContext.pop() `,
+ which may be delayed during testing to maintain access to
+ resources.
- .. versionchanged:: 3.2
- All callbacks are called rather than stopping on the first error.
+ :param exc: An unhandled exception raised while dispatching the
+ request. Detected from the current exception information if
+ not passed. Passed to each teardown function.
.. versionchanged:: 0.9
Added the ``exc`` argument.
"""
- collect_errors = _CollectErrors()
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
- for name in chain(ctx.request.blueprints, (None,)):
+ for name in chain(request.blueprints, (None,)):
if name in self.teardown_request_funcs:
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)
-
- collect_errors.raise_any("Errors during request teardown")
+ request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
def do_teardown_appcontext(
- self, ctx: AppContext, exc: BaseException | None = None
+ self,
+ exc: BaseException | None = _sentinel, # type: ignore[assignment]
) -> None:
- """Called right before the application context is popped. Called by
- :meth:`.AppContext.pop`.
+ """Called right before the application context is popped.
- This calls all functions decorated with :meth:`teardown_appcontext`.
- Then the :data:`appcontext_tearing_down` signal is sent.
+ When handling a request, the application context is popped
+ after the request context. See :meth:`do_teardown_request`.
- :param exc: An unhandled exception raised while the context was active.
- Passed to each teardown function.
+ This calls all functions decorated with
+ :meth:`teardown_appcontext`. Then the
+ :data:`appcontext_tearing_down` signal is sent.
- .. versionchanged:: 3.2
- All callbacks are called rather than stopping on the first error.
+ This is called by
+ :meth:`AppContext.pop() `.
.. versionadded:: 0.9
"""
- collect_errors = _CollectErrors()
+ if exc is _sentinel:
+ exc = sys.exc_info()[1]
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)
-
- collect_errors.raise_any("Errors during app teardown")
+ appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
def app_context(self) -> AppContext:
- """Create an :class:`.AppContext`. When the context is pushed,
- :data:`.current_app` and :data:`.g` become available.
+ """Create an :class:`~flask.ctx.AppContext`. Use as a ``with``
+ block to push the context, which will make :data:`current_app`
+ point at this application.
- A context is automatically pushed when handling each request, and when
- running any ``flask`` CLI command. Use this as a ``with`` block to
- manually push a context outside of those situations, such as during
- setup or testing.
+ An application context is automatically pushed by
+ :meth:`RequestContext.push() `
+ when handling a request, and when running a CLI command. Use
+ this to manually create a context outside of these situations.
- .. code-block:: python
+ ::
with app.app_context():
init_db()
@@ -1498,37 +1404,44 @@ class Flask(App):
"""
return AppContext(self)
- def request_context(self, environ: WSGIEnvironment) -> AppContext:
- """Create an :class:`.AppContext` with request information representing
- the given WSGI environment. A context is automatically pushed when
- handling each request. When the context is pushed, :data:`.request`,
- :data:`.session`, :data:`g:, and :data:`.current_app` become available.
+ def request_context(self, environ: WSGIEnvironment) -> RequestContext:
+ """Create a :class:`~flask.ctx.RequestContext` representing a
+ WSGI environment. Use a ``with`` block to push the context,
+ which will make :data:`request` point at this request.
- 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.
+ See :doc:`/reqcontext`.
- See :doc:`/appcontext`.
+ Typically you should not call this from your own code. A request
+ 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 AppContext.from_environ(self, environ)
+ return RequestContext(self, environ)
- def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> AppContext:
- """Create an :class:`.AppContext` with request information created from
- the given arguments. When the context is pushed, :data:`.request`,
- :data:`.session`, :data:`g:, and :data:`.current_app` become available.
+ def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext:
+ """Create a :class:`~flask.ctx.RequestContext` for a WSGI
+ environment created from the given values. This is mostly useful
+ during testing, where you may want to run a function that uses
+ request data without dispatching a full request.
- 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.
+ See :doc:`/reqcontext`.
- .. code-block:: python
+ Use a ``with`` block to push the context, which will make
+ :data:`request` point at the request for the created
+ environment. ::
with app.test_request_context(...):
generate_report()
- See :doc:`/appcontext`.
+ When using the shell, it may be easier to push and pop the
+ context manually to avoid indentation. ::
+
+ ctx = app.test_request_context(...)
+ ctx.push()
+ ...
+ ctx.pop()
Takes the same arguments as Werkzeug's
:class:`~werkzeug.test.EnvironBuilder`, with some defaults from
@@ -1538,18 +1451,20 @@ class Flask(App):
:param path: URL path being requested.
:param base_url: Base URL where the app is being served, which
``path`` is relative to. If not given, built from
- :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`,
- and :data:`APPLICATION_ROOT`.
- :param subdomain: Subdomain name to prepend to :data:`SERVER_NAME`.
+ :data:`PREFERRED_URL_SCHEME`, ``subdomain``,
+ :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`.
+ :param subdomain: Subdomain name to append to
+ :data:`SERVER_NAME`.
:param url_scheme: Scheme to use instead of
:data:`PREFERRED_URL_SCHEME`.
- :param data: The request body text or bytes,or a dict of form data.
+ :param data: The request body, either as a string or a dict of
+ form keys and values.
:param json: If given, this is serialized as JSON and passed as
``data``. Also defaults ``content_type`` to
``application/json``.
- :param args: Other positional arguments passed to
+ :param args: other positional arguments passed to
:class:`~werkzeug.test.EnvironBuilder`.
- :param kwargs: Other keyword arguments passed to
+ :param kwargs: other keyword arguments passed to
:class:`~werkzeug.test.EnvironBuilder`.
"""
from .testing import EnvironBuilder
@@ -1557,12 +1472,10 @@ class Flask(App):
builder = EnvironBuilder(self, *args, **kwargs)
try:
- environ = builder.get_environ()
+ return self.request_context(builder.get_environ())
finally:
builder.close()
- return self.request_context(environ)
-
def wsgi_app(
self, environ: WSGIEnvironment, start_response: StartResponse
) -> cabc.Iterable[bytes]:
@@ -1583,6 +1496,7 @@ class Flask(App):
Teardown events for the request and app contexts are called
even if an unhandled error occurs. Other events may not be
called depending on when an error occurs during dispatch.
+ See :ref:`callbacks-and-errors`.
:param environ: A WSGI environment.
:param start_response: A callable accepting a status code,
@@ -1594,23 +1508,20 @@ class Flask(App):
try:
try:
ctx.push()
- response = self.full_dispatch_request(ctx)
+ response = self.full_dispatch_request()
except Exception as e:
error = e
- response = self.handle_exception(ctx, e)
- except:
+ response = self.handle_exception(e)
+ except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if "werkzeug.debug.preserve_context" in environ:
- environ["werkzeug.debug.preserve_context"](ctx)
+ environ["werkzeug.debug.preserve_context"](_cv_app.get())
+ environ["werkzeug.debug.preserve_context"](_cv_request.get())
- if (
- error is not None
- and self.should_ignore_error is not None
- and self.should_ignore_error(error)
- ):
+ if error is not None and self.should_ignore_error(error):
error = None
ctx.pop(error)
diff --git a/src/flask/cli.py b/src/flask/cli.py
index 1a9159ec..dd03f3c5 100644
--- a/src/flask/cli.py
+++ b/src/flask/cli.py
@@ -468,7 +468,7 @@ _app_option = click.Option(
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
# that, let debug be set by env in that case.
- source = ctx.get_parameter_source(param.name)
+ source = ctx.get_parameter_source(param.name) # type: ignore[arg-type]
if source is not None and source in (
ParameterSource.DEFAULT,
@@ -601,7 +601,15 @@ class FlaskGroup(AppGroup):
if self._loaded_plugin_commands:
return
- for ep in importlib.metadata.entry_points(group="flask.commands"):
+ if sys.version_info >= (3, 10):
+ 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._loaded_plugin_commands = True
@@ -628,7 +636,7 @@ class FlaskGroup(AppGroup):
# Push an app context for the loaded app unless it is already
# active somehow. This makes the context available to parameter
# and command callbacks without needing @with_appcontext.
- if not current_app or current_app._get_current_object() is not app:
+ if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined]
ctx.with_resource(app.app_context())
return app.cli.get_command(ctx, name)
@@ -676,9 +684,7 @@ class FlaskGroup(AppGroup):
return super().make_context(info_name, args, parent=parent, **extra)
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
- if (not args and self.no_args_is_help) or (
- len(args) == 1 and args[0] in self.get_help_option_names(ctx)
- ):
+ if not args and self.no_args_is_help:
# Attempt to load --env-file and --app early in case they
# were given as env vars. Otherwise no_args_is_help will not
# see commands from app.cli.
@@ -777,7 +783,7 @@ def show_server_banner(debug: bool, app_import_path: str | None) -> None:
click.echo(f" * Debug mode: {'on' if debug else 'off'}")
-class CertParamType(click.ParamType[t.Any]):
+class CertParamType(click.ParamType):
"""Click option type for the ``--cert`` option. Allows either an
existing file, the string ``'adhoc'``, or an import for a
:class:`~ssl.SSLContext` object.
@@ -803,7 +809,7 @@ class CertParamType(click.ParamType[t.Any]):
try:
return self.path_type(value, param, ctx)
except click.BadParameter:
- value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr]
+ value = click.STRING(value, param, ctx).lower()
if value == "adhoc":
try:
diff --git a/src/flask/ctx.py b/src/flask/ctx.py
index d4d0de65..9b164d39 100644
--- a/src/flask/ctx.py
+++ b/src/flask/ctx.py
@@ -1,21 +1,20 @@
from __future__ import annotations
import contextvars
+import sys
import typing as t
from functools import update_wrapper
from types import TracebackType
from werkzeug.exceptions import HTTPException
-from werkzeug.routing import MapAdapter
from . import typing as ft
from .globals import _cv_app
-from .helpers import _CollectErrors
+from .globals import _cv_request
from .signals import appcontext_popped
from .signals import appcontext_pushed
-if t.TYPE_CHECKING:
- import typing_extensions as te
+if t.TYPE_CHECKING: # pragma: no cover
from _typeshed.wsgi import WSGIEnvironment
from .app import Flask
@@ -32,7 +31,7 @@ class _AppCtxGlobals:
application context.
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
@@ -118,27 +117,29 @@ class _AppCtxGlobals:
def after_this_request(
f: ft.AfterRequestCallable[t.Any],
) -> ft.AfterRequestCallable[t.Any]:
- """Decorate a function to run after the current request. The behavior is the
- same as :meth:`.Flask.after_request`, except it only applies to the current
- request, rather than every request. Therefore, it must be used within a
- request context, rather than during setup.
+ """Executes a function after this request. This is useful to modify
+ response objects. The function is passed the response object and has
+ to return the same or a new one.
- .. code-block:: python
+ Example::
- @app.route("/")
+ @app.route('/')
def index():
@after_this_request
def add_header(response):
- response.headers["X-Foo"] = "Parachute"
+ response.headers['X-Foo'] = 'Parachute'
return response
+ return 'Hello World!'
- return "Hello, World!"
+ This is more useful if a function other than the view function wants to
+ 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
"""
- ctx = _cv_app.get(None)
+ ctx = _cv_request.get(None)
- if ctx is None or not ctx.has_request:
+ if ctx is None:
raise RuntimeError(
"'after_this_request' can only be used when a request"
" context is active, such as in a view function."
@@ -152,27 +153,13 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def copy_current_request_context(f: F) -> F:
- """Decorate a function to run inside the current request context. This can
- be used when starting a background task, otherwise it will not see the app
- and request objects that were active in the parent.
+ """A helper function that decorates a function to retain the current
+ request context. This is useful when working with greenlets. The moment
+ the function is decorated a copy of the request context is created and
+ then pushed when the function is called. The current session is also
+ included in the copied request context.
- .. 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
+ Example::
import gevent
from flask import copy_current_request_context
@@ -189,68 +176,59 @@ def copy_current_request_context(f: F) -> F:
.. versionadded:: 0.10
"""
- # Store the context that was active when the decorator was applied.
- original = _cv_app.get(None)
+ ctx = _cv_request.get(None)
- if original is None:
+ if ctx is None:
raise RuntimeError(
"'copy_current_request_context' can only be used when a"
" request context is active, such as in a view function."
)
+ ctx = ctx.copy()
+
def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any:
- # Copy the context before pushing, so each worker acts independently.
- with original.copy() as ctx:
- return ctx.app.ensure_sync(f)(*args, **kwargs)
+ with ctx: # type: ignore[union-attr]
+ return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr]
return update_wrapper(wrapper, f) # type: ignore[return-value]
def has_request_context() -> bool:
- """Test if an app context is active and if it has request information.
+ """If you have code that wants to test if a request context is there or
+ 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
+ ::
- from flask import has_request_context, request
+ class User(db.Model):
- if has_request_context():
- remote_addr = request.remote_addr
+ def __init__(self, username, remote_addr=None):
+ self.username = username
+ if remote_addr is None and has_request_context():
+ remote_addr = request.remote_addr
+ self.remote_addr = remote_addr
- If a request context is active, the :data:`.request` and :data:`.session`
- context proxies will available and ``True``, otherwise ``False``. You can
- use that to test the data you use, rather than using this function.
+ Alternatively you can also just test any of the context bound objects
+ (such as :class:`request` or :class:`g`) for truthness::
- .. code-block:: python
+ class User(db.Model):
- from flask import request
-
- if request:
- remote_addr = request.remote_addr
+ def __init__(self, username, remote_addr=None):
+ self.username = username
+ if remote_addr is None and request:
+ remote_addr = request.remote_addr
+ self.remote_addr = remote_addr
.. versionadded:: 0.7
"""
- return (ctx := _cv_app.get(None)) is not None and ctx.has_request
+ return _cv_request.get(None) is not None
def has_app_context() -> bool:
- """Test if an app context is active. Unlike :func:`has_request_context`
- this can be true outside a request, such as in a CLI command.
-
- .. 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 = ...
+ """Works like :func:`has_request_context` but for the application
+ context. You can also just do a boolean check on the
+ :data:`current_app` object instead.
.. versionadded:: 0.9
"""
@@ -258,283 +236,214 @@ def has_app_context() -> bool:
class AppContext:
- """An app context contains information about an app, and about the request
- when handling a request. A context is pushed at the beginning of each
- request and CLI command, and popped at the end. The context is referred to
- as a "request context" if it has request information, and an "app context"
- if not.
-
- Do not use this class directly. Use :meth:`.Flask.app_context` to create an
- app context if needed during setup, and :meth:`.Flask.test_request_context`
- to create a request context if needed during tests.
-
- When the context is popped, it will evaluate all the teardown functions
- registered with :meth:`~flask.Flask.teardown_request` (if handling a
- request) then :meth:`.Flask.teardown_appcontext`.
-
- 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, and will run again when
- the restored context is popped.
-
- :param app: The application this context represents.
- :param request: The request data this context represents.
- :param session: The session data this context represents. If not given,
- loaded from the request on first access.
-
- .. versionchanged:: 3.2
- Merged with ``RequestContext``. The ``RequestContext`` alias will be
- removed in Flask 4.0.
-
- .. versionchanged:: 3.2
- A combined app and request context is pushed for every request and CLI
- command, rather than trying to detect if an app context is already
- pushed.
-
- .. versionchanged:: 3.2
- The session is loaded the first time it is accessed, rather than when
- the context is pushed.
+ """The app context contains application-specific information. An app
+ context is created and pushed at the beginning of each request if
+ one is not already active. An app context is also pushed when
+ running CLI commands.
"""
- def __init__(
- self,
- app: Flask,
- *,
- request: Request | None = None,
- session: SessionMixin | None = None,
- ) -> None:
+ def __init__(self, app: Flask) -> None:
self.app = app
- """The application represented by this context. Accessed through
- :data:`.current_app`.
- """
-
+ self.url_adapter = app.create_url_adapter(None)
self.g: _AppCtxGlobals = app.app_ctx_globals_class()
- """The global data for this context. Accessed through :data:`.g`."""
-
- self.url_adapter: MapAdapter | None = None
- """The URL adapter bound to the request, or the app if not in a request.
- May be ``None`` if binding failed.
- """
-
- self._request: Request | None = request
- self._session: SessionMixin | None = session
- self._flashes: list[tuple[str, str]] | None = None
- self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
-
- try:
- self.url_adapter = app.create_url_adapter(self._request)
- except HTTPException as e:
- if self._request is not None:
- self._request.routing_exception = e
-
- self._cv_token: contextvars.Token[AppContext] | None = None
- """The previous state to restore when popping."""
-
- 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
- The current session data is used instead of reloading the original data.
-
- .. versionadded:: 0.10
- """
- return self.__class__(
- self.app,
- request=self._request,
- 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:
- """Apply routing to the current request, storing either the matched
- endpoint and args, or a routing exception.
- """
- try:
- result = self.url_adapter.match(return_rule=True) # type: ignore[union-attr]
- except HTTPException as e:
- self._request.routing_exception = e # type: ignore[union-attr]
- else:
- self._request.url_rule, self._request.view_args = result # type: ignore[union-attr]
+ self._cv_tokens: list[contextvars.Token[AppContext]] = []
def push(self) -> None:
- """Push this context so that it is the active context. If this is a
- request context, calls :meth:`match_request` to perform routing with
- the context active.
-
- Typically, this is not used directly. Instead, use a ``with`` block
- to manage the context.
-
- 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)
+ """Binds the app context to the current context."""
+ self._cv_tokens.append(_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.
- # This allows a custom open_session method to use the request context.
- self._get_session()
-
- # Match the request URL after loading the session, so that the
- # session is available in custom URL converters.
- if self.url_adapter is not None:
- self.match_request()
-
- def pop(self, exc: BaseException | None = None) -> None:
- """Pop this context so that it is no longer the active context. Then
- call teardown functions and signals.
-
- 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
- Added the ``exc`` argument.
- """
- if self._cv_token is None:
- raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.")
-
- ctx = _cv_app.get(None)
-
- if ctx is None or self._cv_token is None:
- raise RuntimeError(
- f"Cannot pop this context ({self!r}), there is no active context."
- )
+ def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
+ """Pops the app context."""
+ try:
+ if len(self._cv_tokens) == 1:
+ if exc is _sentinel:
+ 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:
- raise RuntimeError(
- f"Cannot pop this context ({self!r}), it is not the active"
- f" context ({ctx!r})."
+ raise AssertionError(
+ f"Popped wrong app context. ({ctx!r} instead of {self!r})"
)
- self._push_count -= 1
+ appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync)
- 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:
+ def __enter__(self) -> AppContext:
self.push()
return self
def __exit__(
self,
- exc_type: type[BaseException] | None,
+ exc_type: type | None,
+ 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__(
+ self,
+ app: Flask,
+ environ: WSGIEnvironment,
+ request: Request | None = None,
+ session: SessionMixin | None = None,
+ ) -> None:
+ self.app = app
+ if request is None:
+ request = app.request_class(environ)
+ request.json_module = app.json
+ self.request: Request = request
+ self.url_adapter = None
+ try:
+ self.url_adapter = app.create_url_adapter(self.request)
+ except HTTPException as e:
+ self.request.routing_exception = e
+ self.flashes: list[tuple[str, str]] | None = None
+ 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"
+ # functions.
+ self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = []
+
+ self._cv_tokens: list[
+ tuple[contextvars.Token[RequestContext], AppContext | None]
+ ] = []
+
+ def copy(self) -> RequestContext:
+ """Creates a copy of this request context with the same request object.
+ 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
+
+ .. versionchanged:: 1.1
+ The current session object is used instead of reloading the original
+ data. This prevents `flask.session` pointing to an out-of-date object.
+ """
+ return self.__class__(
+ self.app,
+ environ=self.request.environ,
+ request=self.request,
+ session=self.session,
+ )
+
+ def match_request(self) -> None:
+ """Can be overridden by a subclass to hook into the matching
+ of the request.
+ """
+ try:
+ result = self.url_adapter.match(return_rule=True) # type: ignore
+ self.request.url_rule, self.request.view_args = result # type: ignore
+ except HTTPException as e:
+ self.request.routing_exception = e
+
+ def push(self) -> None:
+ # Before we push the request context we have to ensure that there
+ # is an application context.
+ app_ctx = _cv_app.get(None)
+
+ if app_ctx is None or app_ctx.app is not self.app:
+ app_ctx = self.app.app_context()
+ app_ctx.push()
+ else:
+ app_ctx = None
+
+ self._cv_tokens.append((_cv_request.set(self), app_ctx))
+
+ # Open the session at the moment that the request context is available.
+ # 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
+ # 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
+ # session is available in custom URL converters.
+ if self.url_adapter is not None:
+ self.match_request()
+
+ def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore
+ """Pops the request context and unbinds it by doing that. This will
+ also trigger the execution of functions registered by the
+ :meth:`~flask.Flask.teardown_request` decorator.
+
+ .. versionchanged:: 0.9
+ Added the `exc` argument.
+ """
+ clear_request = len(self._cv_tokens) == 1
+
+ try:
+ 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 request_close is not None:
+ request_close()
+ 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:
+ self.push()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type | None,
exc_value: BaseException | None,
tb: TracebackType | None,
) -> None:
self.pop(exc_value)
def __repr__(self) -> str:
- if self._request is not None:
- return (
- f"<{type(self).__name__} {id(self)} 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 (
+ f"<{type(self).__name__} {self.request.url!r}"
+ f" [{self.request.method}] of {self.app.name}>"
)
- return AppContext
-
- raise AttributeError(name)
diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py
index 61884e1a..2c8c4c48 100644
--- a/src/flask/debughelpers.py
+++ b/src/flask/debughelpers.py
@@ -6,7 +6,7 @@ from jinja2.loaders import BaseLoader
from werkzeug.routing import RequestRedirect
from .blueprints import Blueprint
-from .globals import _cv_app
+from .globals import request_ctx
from .sansio.app import App
if t.TYPE_CHECKING:
@@ -136,9 +136,8 @@ def explain_template_loading_attempts(
info = [f"Locating template {template!r}:"]
total_found = 0
blueprint = None
-
- if (ctx := _cv_app.get(None)) is not None and ctx.has_request:
- blueprint = ctx.request.blueprint
+ if request_ctx and request_ctx.request.blueprint is not None:
+ blueprint = request_ctx.request.blueprint
for idx, (loader, srcobj, triple) in enumerate(attempts):
if isinstance(srcobj, App):
diff --git a/src/flask/globals.py b/src/flask/globals.py
index f4a7298e..e2c410cc 100644
--- a/src/flask/globals.py
+++ b/src/flask/globals.py
@@ -9,69 +9,43 @@ if t.TYPE_CHECKING: # pragma: no cover
from .app import Flask
from .ctx import _AppCtxGlobals
from .ctx import AppContext
+ from .ctx import RequestContext
from .sessions import SessionMixin
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 = """\
Working outside of application context.
-Attempted to use functionality that expected a current application to be set. To
-solve this, set up an app context using 'with app.app_context()'. See the
-documentation on app context for more information.\
+This typically means that you attempted to use functionality that needed
+the current application. To solve this, set up an application context
+with app.app_context(). See the documentation for more information.\
"""
_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
-app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment]
+app_ctx: AppContext = LocalProxy( # type: ignore[assignment]
_cv_app, unbound_message=_no_app_msg
)
-current_app: FlaskProxy = LocalProxy( # type: ignore[assignment]
+current_app: Flask = LocalProxy( # type: ignore[assignment]
_cv_app, "app", unbound_message=_no_app_msg
)
-g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment]
+g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment]
_cv_app, "g", unbound_message=_no_app_msg
)
_no_req_msg = """\
Working outside of request context.
-Attempted to use functionality that expected an active HTTP request. See the
-documentation on request context for more information.\
+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.\
"""
-request: RequestProxy = LocalProxy( # type: ignore[assignment]
- _cv_app, "request", unbound_message=_no_req_msg
+_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
+request_ctx: RequestContext = LocalProxy( # type: ignore[assignment]
+ _cv_request, unbound_message=_no_req_msg
)
-session: SessionMixinProxy = LocalProxy( # type: ignore[assignment]
- _cv_app, "session", unbound_message=_no_req_msg
+request: Request = LocalProxy( # type: ignore[assignment]
+ _cv_request, "request", 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)
diff --git a/src/flask/helpers.py b/src/flask/helpers.py
index fb7f6eba..a6b7e150 100644
--- a/src/flask/helpers.py
+++ b/src/flask/helpers.py
@@ -7,17 +7,16 @@ import typing as t
from datetime import datetime
from functools import cache
from functools import update_wrapper
-from types import TracebackType
import werkzeug.utils
from werkzeug.exceptions import abort as _wz_abort
from werkzeug.utils import redirect as _wz_redirect
from werkzeug.wrappers import Response as BaseResponse
-from .globals import _cv_app
-from .globals import app_ctx
+from .globals import _cv_request
from .globals import current_app
from .globals import request
+from .globals import request_ctx
from .globals import session
from .signals import message_flashed
@@ -63,52 +62,35 @@ def stream_with_context(
def stream_with_context(
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]]:
- """Wrap a response generator function so that it runs inside the current
- request context. This keeps :data:`.request`, :data:`.session`, and :data:`.g`
- available, even though at the point the generator runs the request context
- will typically have ended.
+ """Request contexts disappear when the response is started on the server.
+ This is done for efficiency reasons and to make it less likely to encounter
+ memory leaks with badly written WSGI middlewares. The downside is that if
+ you are using streamed responses, the generator cannot access request bound
+ information any more.
- .. 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
+ This function however can help you keep the context around for longer::
from flask import stream_with_context, request, Response
- @app.get("/stream")
+ @app.route('/stream')
def streamed_response():
@stream_with_context
def generate():
- yield "Hello "
- yield request.args["name"]
- yield "!"
-
+ yield 'Hello '
+ yield request.args['name']
+ yield '!'
return Response(generate())
- Or use it as a wrapper around a created generator:
-
- .. code-block:: python
+ Alternatively it can also be used around a specific generator::
from flask import stream_with_context, request, Response
- @app.get("/stream")
+ @app.route('/stream')
def streamed_response():
def generate():
- yield "Hello "
- yield request.args["name"]
- yield "!"
-
+ yield 'Hello '
+ yield request.args['name']
+ yield '!'
return Response(stream_with_context(generate()))
.. versionadded:: 0.9
@@ -123,29 +105,35 @@ def stream_with_context(
return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type]
- def generator() -> t.Iterator[t.AnyStr]:
- if (ctx := _cv_app.get(None)) is None:
+ def generator() -> t.Iterator[t.AnyStr | None]:
+ ctx = _cv_request.get(None)
+ if ctx is None:
raise RuntimeError(
"'stream_with_context' can only be used when a request"
" context is active, such as in a view function."
)
-
with ctx:
- yield None # type: ignore[misc]
+ # 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
+ # iterator in we're still running the cleanup logic. Generators
+ # don't need that because they are closed on their destruction
+ # automatically.
try:
yield from gen
finally:
- # Clean up in case the user wrapped a WSGI iterator.
if hasattr(gen, "close"):
gen.close()
- # Execute the generator to the sentinel value. This captures the current
- # context and pushes it to preserve it. Further iteration will yield from
- # the original iterator.
+ # The trick is to start the generator. Then the code execution runs until
+ # the first dummy None is yielded at which point the context was already
+ # pushed. This item is discarded. Then when the iteration continues the
+ # real generator is executed.
wrapped_g = generator()
next(wrapped_g)
- return wrapped_g
+ return wrapped_g # type: ignore[return-value]
def make_response(*args: t.Any) -> Response:
@@ -252,7 +240,7 @@ def url_for(
def redirect(
- location: str, code: int = 303, Response: type[BaseResponse] | None = None
+ location: str, code: int = 302, Response: type[BaseResponse] | None = None
) -> BaseResponse:
"""Create a redirect response object.
@@ -265,15 +253,12 @@ def redirect(
:param Response: The response class to use. Not used when
``current_app`` is active, which uses ``app.response_class``.
- .. versionchanged:: 3.2
- ``code`` defaults to ``303`` instead of ``302``.
-
.. versionadded:: 2.2
Calls ``current_app.redirect`` if available instead of always
using Werkzeug's default ``redirect``.
"""
- if (ctx := _cv_app.get(None)) is not None:
- return ctx.app.redirect(location, code=code)
+ if current_app:
+ return current_app.redirect(location, code=code)
return _wz_redirect(location, code=code, Response=Response)
@@ -295,8 +280,8 @@ def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn
Calls ``current_app.aborter`` if available instead of always
using Werkzeug's default ``abort``.
"""
- if (ctx := _cv_app.get(None)) is not None:
- ctx.app.aborter(code, *args, **kwargs)
+ if current_app:
+ current_app.aborter(code, *args, **kwargs)
_wz_abort(code, *args, **kwargs)
@@ -348,7 +333,7 @@ def flash(message: str, category: str = "message") -> None:
flashes = session.get("_flashes", [])
flashes.append((category, message))
session["_flashes"] = flashes
- app = current_app._get_current_object()
+ app = current_app._get_current_object() # type: ignore
message_flashed.send(
app,
_async_wrapper=app.ensure_sync,
@@ -388,10 +373,10 @@ def get_flashed_messages(
:param category_filter: filter of categories to limit return values. Only
categories in the list will be returned.
"""
- flashes = app_ctx._flashes
+ flashes = request_ctx.flashes
if flashes is None:
flashes = session.pop("_flashes") if "_flashes" in session else []
- app_ctx._flashes = flashes
+ request_ctx.flashes = flashes
if category_filter:
flashes = list(filter(lambda f: f[0] in category_filter, flashes))
if not with_categories:
@@ -400,22 +385,20 @@ def get_flashed_messages(
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:
- kwargs["max_age"] = ctx.app.get_send_file_max_age
+ kwargs["max_age"] = current_app.get_send_file_max_age
kwargs.update(
- environ=ctx.request.environ,
- use_x_sendfile=ctx.app.config["USE_X_SENDFILE"],
- response_class=ctx.app.response_class,
- _root_path=ctx.app.root_path,
+ environ=request.environ,
+ use_x_sendfile=current_app.config["USE_X_SENDFILE"],
+ response_class=current_app.response_class,
+ _root_path=current_app.root_path, # type: ignore
)
return kwargs
def send_file(
- path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes],
+ path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO,
mimetype: str | None = None,
as_attachment: bool = False,
download_name: str | None = None,
@@ -649,34 +632,3 @@ def _split_blueprint_path(name: str) -> list[str]:
out.extend(_split_blueprint_path(name.rpartition(".")[0]))
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]
diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py
index 742812f2..c0941d04 100644
--- a/src/flask/json/__init__.py
+++ b/src/flask/json/__init__.py
@@ -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
JSON response automatically without needing to call this.
- This requires an active app context, and calls
+ This requires an active request or application context, and calls
:meth:`app.json.response() `.
In debug mode, the output is formatted with indentation to make it
diff --git a/src/flask/json/provider.py b/src/flask/json/provider.py
index f37cb7b2..ea7e4753 100644
--- a/src/flask/json/provider.py
+++ b/src/flask/json/provider.py
@@ -135,7 +135,7 @@ class DefaultJSONProvider(JSONProvider):
method) will call the ``__html__`` method to get a string.
"""
- default: t.Callable[[t.Any], t.Any] = staticmethod(_default)
+ default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment]
"""Apply this function to any object that :meth:`json.dumps` does
not know how to serialize. It should return a valid JSON type or
raise a ``TypeError``.
diff --git a/src/flask/sansio/app.py b/src/flask/sansio/app.py
index 74c5b323..36201714 100644
--- a/src/flask/sansio/app.py
+++ b/src/flask/sansio/app.py
@@ -177,8 +177,11 @@ class App(Scaffold):
#: 3. Return None instead of AttributeError on unexpected attributes.
#: 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
- #: Renamed from ``request_globals_class`.
app_ctx_globals_class = _AppCtxGlobals
#: The class that is used for the ``config`` attribute of this app.
@@ -210,7 +213,7 @@ class App(Scaffold):
#:
#: This attribute can also be configured from the config with the
#: :data:`SECRET_KEY` configuration key. Defaults to ``None``.
- secret_key = ConfigAttribute[str | bytes | None]("SECRET_KEY")
+ secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY")
#: A :class:`~datetime.timedelta` which is used to set the expiration
#: date of a permanent session. The default is 31 days which makes a
@@ -420,7 +423,7 @@ class App(Scaffold):
)
@cached_property
- def name(self) -> str:
+ def name(self) -> str: # type: ignore
"""The name of the application. This is usually the import name
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
@@ -518,7 +521,7 @@ class App(Scaffold):
return os.path.join(prefix, "var", f"{self.name}-instance")
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
- """Creates the loader for the Jinja environment. Can be used to
+ """Creates the loader for the Jinja2 environment. Can be used to
override just the loader and keeping the rest unchanged. It's
discouraged to override this function. Instead one should override
the :meth:`jinja_loader` function instead.
@@ -530,13 +533,10 @@ class App(Scaffold):
"""
return DispatchingJinjaLoader(self)
- def select_jinja_autoescape(self, filename: str | None) -> bool:
+ def select_jinja_autoescape(self, filename: str) -> bool:
"""Returns ``True`` if autoescaping should be active for the given
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
Autoescaping is now enabled by default for ``.svg`` files.
@@ -544,7 +544,7 @@ class App(Scaffold):
"""
if filename is None:
return True
- return filename.lower().endswith((".html", ".htm", ".xml", ".xhtml", ".svg"))
+ return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg"))
@property
def debug(self) -> bool:
@@ -630,19 +630,19 @@ class App(Scaffold):
# Methods that should always be added
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:
provide_automatic_options = getattr(
view_func, "provide_automatic_options", None
)
- if provide_automatic_options is None:
- provide_automatic_options = (
- "OPTIONS" not in methods
- and self.config["PROVIDE_AUTOMATIC_OPTIONS"]
- )
-
- if provide_automatic_options:
- required_methods.add("OPTIONS")
+ if provide_automatic_options is None:
+ if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]:
+ provide_automatic_options = True
+ required_methods.add("OPTIONS")
+ else:
+ provide_automatic_options = False
# Add the required methods now.
methods |= required_methods
@@ -660,34 +660,21 @@ class App(Scaffold):
)
self.view_functions[endpoint] = view_func
- @t.overload
- def template_filter(self, name: T_template_filter) -> T_template_filter: ...
- @t.overload
- def template_filter(
- self, name: str | None = None
- ) -> t.Callable[[T_template_filter], T_template_filter]: ...
@setupmethod
def 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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_filter], T_template_filter]:
+ """A decorator that is used to register custom template filter.
+ You can specify a name for the filter, otherwise the function
+ name will be used. Example::
- .. code-block:: python
+ @app.template_filter()
+ def reverse(s):
+ return s[::-1]
- @app.template_filter("reverse")
- 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.
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
"""
- if callable(name):
- self.add_template_filter(name)
- return name
def decorator(f: T_template_filter) -> T_template_filter:
self.add_template_filter(f, name=name)
@@ -699,52 +686,36 @@ class App(Scaffold):
def add_template_filter(
self, f: ft.TemplateFilterCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja filter.
+ """Register a custom template filter. Works exactly like the
+ :meth:`template_filter` decorator.
- The :meth:`template_filter` decorator can be used to register a function
- 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.
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
"""
self.jinja_env.filters[name or f.__name__] = f
- @t.overload
- def template_test(self, name: T_template_test) -> T_template_test: ...
- @t.overload
- def template_test(
- self, name: str | None = None
- ) -> t.Callable[[T_template_test], T_template_test]: ...
@setupmethod
def 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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_test], T_template_test]:
+ """A decorator that is used to register custom template test.
+ You can specify a name for the test, otherwise the function
+ name will be used. Example::
- .. code-block:: python
-
- @app.template_test("prime")
- def is_prime_test(n):
- if n == 2:
- return True
- for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
- if n % i == 0:
- return False
+ @app.template_test()
+ def is_prime(n):
+ if n == 2:
+ return True
+ for i in range(2, int(math.ceil(math.sqrt(n))) + 1):
+ if n % i == 0:
+ return False
return True
- The :meth:`add_template_test` 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.
-
.. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
"""
- if callable(name):
- self.add_template_test(name)
- return name
def decorator(f: T_template_test) -> T_template_test:
self.add_template_test(f, name=name)
@@ -756,49 +727,33 @@ class App(Scaffold):
def add_template_test(
self, f: ft.TemplateTestCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja test.
-
- 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.
+ """Register a custom template test. Works exactly like the
+ :meth:`template_test` decorator.
.. 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
- @t.overload
- def template_global(self, name: T_template_global) -> T_template_global: ...
- @t.overload
- def template_global(
- self, name: str | None = None
- ) -> t.Callable[[T_template_global], T_template_global]: ...
@setupmethod
def 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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_global], T_template_global]:
+ """A decorator that is used to register a custom template global function.
+ You can specify a name for the global function, otherwise the function
+ name will be used. Example::
- .. code-block:: python
-
- @app.template_global
+ @app.template_global()
def double(n):
return 2 * n
- The :meth:`add_template_global` method may be used to register a
- function later rather than decorating.
-
- :param name: The name to register the global as. If not given, uses the
- function's name.
-
.. versionadded:: 0.10
+
+ :param name: the optional name of the global function, otherwise the
+ function name will be used.
"""
- if callable(name):
- self.add_template_global(name)
- return name
def decorator(f: T_template_global) -> T_template_global:
self.add_template_global(f, name=name)
@@ -810,24 +765,22 @@ class App(Scaffold):
def add_template_global(
self, f: ft.TemplateGlobalCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja global.
-
- 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.
+ """Register a custom template global function. Works exactly like the
+ :meth:`template_global` decorator.
.. 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
@setupmethod
def teardown_appcontext(self, f: T_teardown) -> T_teardown:
- """Registers a function to be called when the app context is popped. The
- context is popped at the end of a request, CLI command, or manual ``with``
- block.
+ """Registers a function to be called when the application
+ context is popped. The application context is typically popped
+ after the request context for each request, at the end of CLI
+ commands, or after a manually pushed context ends.
.. code-block:: python
@@ -836,7 +789,9 @@ class App(Scaffold):
When the ``with`` block exits (or ``ctx.pop()`` is called), the
teardown functions are called just before the app context is
- made inactive.
+ made inactive. Since a request context typically also manages an
+ application context it would also be called when you pop a
+ request context.
When a teardown function was called because of an unhandled
exception it will be passed an error object. If an
@@ -925,18 +880,17 @@ class App(Scaffold):
return False
- should_ignore_error: None = None
- """If this method returns ``True``, the error will not be passed to
- teardown handlers, and the context will not be preserved for
- debugging.
+ def should_ignore_error(self, error: BaseException | None) -> bool:
+ """This is called to figure out if an error should be ignored
+ or not as far as the teardown system is concerned. If this
+ function returns ``True`` then the teardown handlers will not be
+ passed the error.
- .. deprecated:: 3.2
- Handle errors as needed in teardown handlers instead.
+ .. versionadded:: 0.10
+ """
+ return False
- .. versionadded:: 0.10
- """
-
- def redirect(self, location: str, code: int = 303) -> BaseResponse:
+ def redirect(self, location: str, code: int = 302) -> BaseResponse:
"""Create a redirect response object.
This is called by :func:`flask.redirect`, and can be called
@@ -945,9 +899,6 @@ class App(Scaffold):
:param location: The URL to redirect to.
:param code: The status code for the redirect.
- .. versionchanged:: 3.2
- ``code`` defaults to ``303`` instead of ``302``.
-
.. versionadded:: 2.2
Moved from ``flask.redirect``, which calls this method.
"""
diff --git a/src/flask/sansio/blueprints.py b/src/flask/sansio/blueprints.py
index 665816e5..4f912cca 100644
--- a/src/flask/sansio/blueprints.py
+++ b/src/flask/sansio/blueprints.py
@@ -440,31 +440,16 @@ class Blueprint(Scaffold):
)
)
- @t.overload
- def app_template_filter(self, name: T_template_filter) -> T_template_filter: ...
- @t.overload
- def app_template_filter(
- self, name: str | None = None
- ) -> t.Callable[[T_template_filter], T_template_filter]: ...
@setupmethod
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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_filter], T_template_filter]:
+ """Register a template filter, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_filter`.
- The :meth:`add_app_template_filter` method may be used to register a
- 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.
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
"""
- if callable(name):
- self.add_app_template_filter(name)
- return name
def decorator(f: T_template_filter) -> T_template_filter:
self.add_app_template_filter(f, name=name)
@@ -476,51 +461,31 @@ class Blueprint(Scaffold):
def add_app_template_filter(
self, f: ft.TemplateFilterCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja filter.
+ """Register a template filter, available in any template rendered by the
+ application. Works like the :meth:`app_template_filter` decorator. Equivalent to
+ :meth:`.Flask.add_template_filter`.
- The :meth:`app_template_filter` decorator can be used to register a
- 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.
+ :param name: the optional name of the filter, otherwise the
+ function name will be used.
"""
- def register_template_filter(state: BlueprintSetupState) -> None:
- state.app.add_template_filter(f, name=name)
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.filters[name or f.__name__] = f
- self.record_once(register_template_filter)
+ self.record_once(register_template)
- @t.overload
- def app_template_test(self, name: T_template_test) -> T_template_test: ...
- @t.overload
- def app_template_test(
- self, name: str | None = None
- ) -> t.Callable[[T_template_test], T_template_test]: ...
@setupmethod
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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_test], T_template_test]:
+ """Register a template test, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_test`.
.. 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:
self.add_app_template_test(f, name=name)
@@ -532,53 +497,33 @@ class Blueprint(Scaffold):
def add_app_template_test(
self, f: ft.TemplateTestCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja 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.
+ """Register a template test, available in any template rendered by the
+ application. Works like the :meth:`app_template_test` decorator. Equivalent to
+ :meth:`.Flask.add_template_test`.
.. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
"""
- def register_template_test(state: BlueprintSetupState) -> None:
- state.app.add_template_test(f, name=name)
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.tests[name or f.__name__] = f
- self.record_once(register_template_test)
+ self.record_once(register_template)
- @t.overload
- def app_template_global(self, name: T_template_global) -> T_template_global: ...
- @t.overload
- def app_template_global(
- self, name: str | None = None
- ) -> t.Callable[[T_template_global], T_template_global]: ...
@setupmethod
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.
+ self, name: str | None = None
+ ) -> t.Callable[[T_template_global], T_template_global]:
+ """Register a template global, available in any template rendered by the
+ application. Equivalent to :meth:`.Flask.template_global`.
.. 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:
self.add_app_template_global(f, name=name)
@@ -590,25 +535,20 @@ class Blueprint(Scaffold):
def add_app_template_global(
self, f: ft.TemplateGlobalCallable, name: str | None = None
) -> None:
- """Register a function to use as a custom Jinja 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.
+ """Register a template global, available in any template rendered by the
+ application. Works like the :meth:`app_template_global` decorator. Equivalent to
+ :meth:`.Flask.add_template_global`.
.. versionadded:: 0.10
+
+ :param name: the optional name of the global, otherwise the
+ function name will be used.
"""
- def register_template_global(state: BlueprintSetupState) -> None:
- state.app.add_template_global(f, name=name)
+ def register_template(state: BlueprintSetupState) -> None:
+ state.app.jinja_env.globals[name or f.__name__] = f
- self.record_once(register_template_global)
+ self.record_once(register_template)
@setupmethod
def before_app_request(self, f: T_before_request) -> T_before_request:
diff --git a/src/flask/sansio/scaffold.py b/src/flask/sansio/scaffold.py
index a04c38ad..3a839f5a 100644
--- a/src/flask/sansio/scaffold.py
+++ b/src/flask/sansio/scaffold.py
@@ -84,7 +84,7 @@ class Scaffold:
#: to. Do not change this once it is set by the constructor.
self.import_name = import_name
- self.static_folder = static_folder
+ self.static_folder = static_folder # type: ignore
self.static_url_path = static_url_path
#: The path to the templates folder, relative to
@@ -507,8 +507,8 @@ class Scaffold:
@setupmethod
def teardown_request(self, f: T_teardown) -> T_teardown:
"""Register a function to be called when the request context is
- popped. Typically, this happens at the end of each request, but
- contexts may be pushed manually during testing.
+ popped. Typically this happens at the end of each request, but
+ contexts may be pushed manually as well during testing.
.. code-block:: python
diff --git a/src/flask/sessions.py b/src/flask/sessions.py
index ad357706..375de065 100644
--- a/src/flask/sessions.py
+++ b/src/flask/sessions.py
@@ -27,7 +27,7 @@ class SessionMixin(MutableMapping[str, t.Any]):
@property
def permanent(self) -> bool:
"""This reflects the ``'_permanent'`` key in the dict."""
- return self.get("_permanent", False) # type: ignore[no-any-return]
+ return self.get("_permanent", False)
@permanent.setter
def permanent(self, value: bool) -> None:
@@ -43,15 +43,10 @@ class SessionMixin(MutableMapping[str, t.Any]):
#: ``True``.
modified = True
- accessed = False
- """Indicates if the session was accessed, even if it was not modified. This
- is set when the session object is accessed through the request context,
- 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.
- """
+ #: Some implementations can detect when session data is read or
+ #: written and set this when that happens. The mixin default is hard
+ #: coded to ``True``.
+ accessed = True
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
@@ -70,15 +65,34 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
#: will only be written to the response if this is ``True``.
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__(
self,
- initial: c.Mapping[str, t.Any] | None = None,
+ initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
) -> None:
def on_update(self: te.Self) -> None:
self.modified = True
+ self.accessed = True
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 used to generate nicer error messages if sessions are not
@@ -93,7 +107,7 @@ class NullSession(SecureCookieSession):
"application to something unique and secret."
)
- __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail
+ __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
del _fail
@@ -304,12 +318,11 @@ class SecureCookieSessionInterface(SessionInterface):
if not app.secret_key:
return None
- keys: list[str | bytes] = []
+ keys: list[str | bytes] = [app.secret_key]
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:
keys.extend(fallbacks)
- keys.append(app.secret_key) # itsdangerous expects current key at top
return URLSafeTimedSerializer(
keys, # type: ignore[arg-type]
salt=self.salt,
diff --git a/src/flask/templating.py b/src/flask/templating.py
index 005108cc..618a3b35 100644
--- a/src/flask/templating.py
+++ b/src/flask/templating.py
@@ -7,34 +7,37 @@ from jinja2 import Environment as BaseEnvironment
from jinja2 import Template
from jinja2 import TemplateNotFound
-from .ctx import AppContext
-from .globals import app_ctx
+from .globals import _cv_app
+from .globals import _cv_request
+from .globals import current_app
+from .globals import request
from .helpers import stream_with_context
from .signals import before_render_template
from .signals import template_rendered
if t.TYPE_CHECKING: # pragma: no cover
+ from .app import Flask
from .sansio.app import App
from .sansio.scaffold import Scaffold
def _default_template_ctx_processor() -> dict[str, t.Any]:
- """Default template context processor. Replaces the ``request`` and ``g``
- proxies with their concrete objects for faster access.
+ """Default template context processor. Injects `request`,
+ `session` and `g`.
"""
- ctx = app_ctx._get_current_object()
- rv: dict[str, t.Any] = {"g": ctx.g}
-
- if ctx.has_request:
- rv["request"] = ctx.request
- # The session proxy cannot be replaced, accessing it gets
- # RequestContext.session, which sets session.accessed.
-
+ appctx = _cv_app.get(None)
+ reqctx = _cv_request.get(None)
+ rv: dict[str, t.Any] = {}
+ if appctx is not None:
+ rv["g"] = appctx.g
+ if reqctx is not None:
+ rv["request"] = reqctx.request
+ rv["session"] = reqctx.session
return rv
class Environment(BaseEnvironment):
- """Works like a regular Jinja environment but has some additional
+ """Works like a regular Jinja2 environment but has some additional
knowledge of how Flask's blueprint works so that it can prepend the
name of the blueprint to referenced templates if necessary.
"""
@@ -120,9 +123,8 @@ class DispatchingJinjaLoader(BaseLoader):
return list(result)
-def _render(ctx: AppContext, template: Template, context: dict[str, t.Any]) -> str:
- app = ctx.app
- app.update_template_context(ctx, context)
+def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str:
+ app.update_template_context(context)
before_render_template.send(
app, _async_wrapper=app.ensure_sync, template=template, context=context
)
@@ -143,9 +145,9 @@ def render_template(
a list is given, the first name to exist will be rendered.
:param context: The variables to make available in the template.
"""
- ctx = app_ctx._get_current_object()
- template = ctx.app.jinja_env.get_or_select_template(template_name_or_list)
- return _render(ctx, template, context)
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.get_or_select_template(template_name_or_list)
+ return _render(app, template, context)
def render_template_string(source: str, **context: t.Any) -> str:
@@ -155,16 +157,15 @@ def render_template_string(source: str, **context: t.Any) -> str:
:param source: The source code of the template to render.
:param context: The variables to make available in the template.
"""
- ctx = app_ctx._get_current_object()
- template = ctx.app.jinja_env.from_string(source)
- return _render(ctx, template, context)
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.from_string(source)
+ return _render(app, template, context)
def _stream(
- ctx: AppContext, template: Template, context: dict[str, t.Any]
+ app: Flask, template: Template, context: dict[str, t.Any]
) -> t.Iterator[str]:
- app = ctx.app
- app.update_template_context(ctx, context)
+ app.update_template_context(context)
before_render_template.send(
app, _async_wrapper=app.ensure_sync, template=template, context=context
)
@@ -175,7 +176,13 @@ def _stream(
app, _async_wrapper=app.ensure_sync, template=template, context=context
)
- return stream_with_context(generate())
+ rv = generate()
+
+ # If a request context is active, keep it while generating.
+ if request:
+ rv = stream_with_context(rv)
+
+ return rv
def stream_template(
@@ -192,9 +199,9 @@ def stream_template(
.. versionadded:: 2.2
"""
- ctx = app_ctx._get_current_object()
- template = ctx.app.jinja_env.get_or_select_template(template_name_or_list)
- return _stream(ctx, template, context)
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.get_or_select_template(template_name_or_list)
+ return _stream(app, template, context)
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
@@ -207,6 +214,6 @@ def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
.. versionadded:: 2.2
"""
- ctx = app_ctx._get_current_object()
- template = ctx.app.jinja_env.from_string(source)
- return _stream(ctx, template, context)
+ app = current_app._get_current_object() # type: ignore[attr-defined]
+ template = app.jinja_env.from_string(source)
+ return _stream(app, template, context)
diff --git a/src/flask/testing.py b/src/flask/testing.py
index 68b1ab48..a62c4836 100644
--- a/src/flask/testing.py
+++ b/src/flask/testing.py
@@ -58,9 +58,9 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
) -> None:
assert not (base_url or subdomain or url_scheme) or (
base_url is not None
- ) != bool(subdomain or url_scheme), (
- 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
- )
+ ) != bool(
+ subdomain or url_scheme
+ ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
if base_url is None:
http_host = app.config.get("SERVER_NAME") or "localhost"
@@ -85,7 +85,7 @@ class EnvironBuilder(werkzeug.test.EnvironBuilder):
self.app = app
super().__init__(path, base_url, *args, **kwargs)
- def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str:
+ def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore
"""Serialize ``obj`` to a JSON-formatted string.
The serialization will be configured according to the config associated
@@ -107,10 +107,10 @@ def _get_werkzeug_version() -> str:
class FlaskClient(Client):
- """Works like a regular Werkzeug test client, with additional behavior for
- Flask. Can defer the cleanup of the request context until the end of a
- ``with`` block. For general information about how to use this class refer to
- :class:`werkzeug.test.Client`.
+ """Works like a regular Werkzeug test client but has knowledge about
+ Flask's contexts to defer the cleanup of the request context until
+ the end of a ``with`` block. For general information about how to
+ use this class refer to :class:`werkzeug.test.Client`.
.. versionchanged:: 0.12
`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]
# Re-push contexts that were preserved during the request.
- for cm in self._new_contexts:
+ while self._new_contexts:
+ cm = self._new_contexts.pop()
self._context_stack.enter_context(cm)
- self._new_contexts.clear()
return response
def __enter__(self) -> FlaskClient:
diff --git a/src/flask/typing.py b/src/flask/typing.py
index 54950616..e7234e96 100644
--- a/src/flask/typing.py
+++ b/src/flask/typing.py
@@ -1,6 +1,5 @@
from __future__ import annotations
-import collections.abc as cabc
import typing as t
if t.TYPE_CHECKING: # pragma: no cover
@@ -18,12 +17,11 @@ ResponseValue = t.Union[
t.Mapping[str, t.Any],
t.Iterator[str],
t.Iterator[bytes],
- cabc.AsyncIterable[str], # for Quart, until App is generic.
- cabc.AsyncIterable[bytes],
]
# the possible types for an individual HTTP header
-HeaderValue = str | list[str] | tuple[str, ...]
+# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
+HeaderValue = t.Union[str, list[str], tuple[str, ...]]
# the possible types for HTTP headers
HeadersValue = t.Union[
@@ -46,29 +44,34 @@ ResponseReturnValue = t.Union[
# callback annotated with flask.Response fail type checking.
ResponseClass = t.TypeVar("ResponseClass", bound="Response")
-AppOrBlueprintKey = str | None # The App key is None, whereas blueprints are named
-AfterRequestCallable = (
- t.Callable[[ResponseClass], ResponseClass]
- | t.Callable[[ResponseClass], t.Awaitable[ResponseClass]]
-)
-BeforeFirstRequestCallable = t.Callable[[], None] | t.Callable[[], t.Awaitable[None]]
-BeforeRequestCallable = (
- t.Callable[[], ResponseReturnValue | None]
- | t.Callable[[], t.Awaitable[ResponseReturnValue | None]]
-)
+AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
+AfterRequestCallable = t.Union[
+ t.Callable[[ResponseClass], ResponseClass],
+ t.Callable[[ResponseClass], t.Awaitable[ResponseClass]],
+]
+BeforeFirstRequestCallable = t.Union[
+ t.Callable[[], None], t.Callable[[], t.Awaitable[None]]
+]
+BeforeRequestCallable = t.Union[
+ t.Callable[[], t.Optional[ResponseReturnValue]],
+ t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]],
+]
ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]]
-TeardownCallable = (
- t.Callable[[BaseException | None], None]
- | t.Callable[[BaseException | None], t.Awaitable[None]]
-)
-TemplateContextProcessorCallable = (
- t.Callable[[], dict[str, t.Any]] | t.Callable[[], t.Awaitable[dict[str, t.Any]]]
-)
+TeardownCallable = t.Union[
+ t.Callable[[t.Optional[BaseException]], None],
+ t.Callable[[t.Optional[BaseException]], t.Awaitable[None]],
+]
+TemplateContextProcessorCallable = t.Union[
+ t.Callable[[], dict[str, t.Any]],
+ t.Callable[[], t.Awaitable[dict[str, t.Any]]],
+]
TemplateFilterCallable = t.Callable[..., t.Any]
TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None]
-URLValuePreprocessorCallable = t.Callable[[str | None, dict[str, t.Any] | None], None]
+URLValuePreprocessorCallable = t.Callable[
+ [t.Optional[str], t.Optional[dict[str, t.Any]]], None
+]
# This should take Exception, but that either breaks typing the argument
# with a specific exception, or decorating multiple times with different
@@ -76,12 +79,12 @@ URLValuePreprocessorCallable = t.Callable[[str | None, dict[str, t.Any] | None],
# https://github.com/pallets/flask/issues/4095
# https://github.com/pallets/flask/issues/4295
# https://github.com/pallets/flask/issues/4297
-ErrorHandlerCallable = (
- t.Callable[[t.Any], ResponseReturnValue]
- | t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]]
-)
+ErrorHandlerCallable = t.Union[
+ t.Callable[[t.Any], ResponseReturnValue],
+ t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]],
+]
-RouteCallable = (
- t.Callable[..., ResponseReturnValue]
- | t.Callable[..., t.Awaitable[ResponseReturnValue]]
-)
+RouteCallable = t.Union[
+ t.Callable[..., ResponseReturnValue],
+ t.Callable[..., t.Awaitable[ResponseReturnValue]],
+]
diff --git a/tests/conftest.py b/tests/conftest.py
index 0414b9e2..58cf85d8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,11 +1,12 @@
import os
+import pkgutil
import sys
import pytest
from _pytest import monkeypatch
from flask import Flask
-from flask.globals import app_ctx as _app_ctx
+from flask.globals import request_ctx
@pytest.fixture(scope="session", autouse=True)
@@ -83,17 +84,47 @@ def test_apps(monkeypatch):
@pytest.fixture(autouse=True)
def leak_detector():
- """Fails if any app contexts are still pushed when a test ends. Pops all
- contexts so subsequent tests are not affected.
- """
yield
+
+ # 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()
- while _app_ctx:
- leaks.append(_app_ctx._get_current_object())
- _app_ctx.pop()
+ assert leaks == []
- assert not 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:
+ return
+
+ class LimitedLoader:
+ def __init__(self, loader):
+ self.loader = loader
+
+ def __getattr__(self, name):
+ 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
diff --git a/tests/test_appctx.py b/tests/test_appctx.py
index 2d4eecc5..ca9e079e 100644
--- a/tests/test_appctx.py
+++ b/tests/test_appctx.py
@@ -1,10 +1,8 @@
-import sys
-
import pytest
import flask
from flask.globals import app_ctx
-from flask.testing import FlaskClient
+from flask.globals import request_ctx
def test_basic_url_generation(app):
@@ -109,8 +107,7 @@ def test_app_tearing_down_with_handled_exception_by_app_handler(app, client):
with app.app_context():
client.get("/")
- # teardown request context, and with block context
- assert cleanup_stuff == [None, None]
+ assert cleanup_stuff == [None]
def test_app_tearing_down_with_unhandled_exception(app, client):
@@ -129,11 +126,9 @@ def test_app_tearing_down_with_unhandled_exception(app, client):
with app.app_context():
client.get("/")
- assert len(cleanup_stuff) == 2
+ assert len(cleanup_stuff) == 1
assert isinstance(cleanup_stuff[0], ValueError)
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):
@@ -183,7 +178,8 @@ def test_context_refcounts(app, client):
@app.route("/")
def index():
with app_ctx:
- pass
+ with request_ctx:
+ pass
assert flask.request.environ["werkzeug.request"] is not None
return ""
@@ -211,55 +207,3 @@ def test_clean_pop(app):
assert called == ["flask_test", "TEARDOWN"]
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
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 1d9d83f8..c737dc5f 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -1,8 +1,8 @@
import gc
-import importlib.metadata
import re
import typing as t
import uuid
+import warnings
import weakref
from contextlib import nullcontext
from datetime import datetime
@@ -20,8 +20,6 @@ from werkzeug.routing import BuildError
from werkzeug.routing import RequestRedirect
import flask
-from flask.globals import app_ctx
-from flask.testing import FlaskClient
require_cpython_gc = pytest.mark.skipif(
python_implementation() != "CPython",
@@ -69,61 +67,63 @@ def test_method_route_no_methods(app):
app.get("/", methods=["GET", "POST"])
-def test_provide_automatic_options_attr_disable(
- app: flask.Flask, client: FlaskClient
-) -> None:
- """Automatic options can be disabled by the view func attribute."""
+def test_provide_automatic_options_attr():
+ app = flask.Flask(__name__)
def index():
return "Hello World!"
index.provide_automatic_options = False
- app.add_url_rule("/", view_func=index)
- rv = client.options()
+ app.route("/")(index)
+ rv = app.test_client().open("/", method="OPTIONS")
assert rv.status_code == 405
+ app = flask.Flask(__name__)
-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():
+ def index2():
return "Hello World!"
- index.provide_automatic_options = True
- app.add_url_rule("/", view_func=index)
- rv = client.options()
- assert rv.allow == {"GET", "HEAD", "OPTIONS"}
+ index2.provide_automatic_options = True
+ app.route("/", methods=["OPTIONS"])(index2)
+ rv = app.test_client().open("/", method="OPTIONS")
+ assert sorted(rv.allow) == ["OPTIONS"]
-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 test_provide_automatic_options_kwarg(app, client):
def index():
- return "Hello World!"
+ return flask.request.method
- rv = client.options()
+ def more():
+ 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
+ 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"
-def test_provide_automatic_options_method_disable(
- app: flask.Flask, client: FlaskClient
-) -> None:
- """Automatic options is ignored if the route handles options."""
+ rv = client.delete("/more")
+ assert rv.status_code == 405
+ assert sorted(rv.allow) == ["GET", "HEAD", "POST"]
- @app.route("/", methods=["OPTIONS"])
- def index():
- return "", {"X-Test": "test"}
-
- rv = client.options()
- assert rv.headers["X-Test"] == "test"
+ rv = client.open("/more", method="OPTIONS")
+ assert rv.status_code == 405
def test_request_dispatching(app, client):
@@ -231,46 +231,27 @@ def test_endpoint_decorator(app, client):
assert client.get("/foo/bar").data == b"bar"
-def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
- @app.post("/")
- def do_set():
+def test_session(app, client):
+ @app.route("/set", methods=["POST"])
+ def set():
+ assert not flask.session.accessed
+ assert not flask.session.modified
flask.session["value"] = flask.request.form["value"]
+ assert flask.session.accessed
+ assert flask.session.modified
return "value set"
- @app.get("/")
- def do_get():
- return flask.session.get("value", "None")
+ @app.route("/get")
+ def get():
+ assert not flask.session.accessed
+ assert not flask.session.modified
+ v = flask.session.get("value", "None")
+ assert flask.session.accessed
+ assert not flask.session.modified
+ return v
- @app.get("/nothing")
- 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
+ assert client.post("/set", data={"value": "42"}).data == b"value set"
+ assert client.get("/get").data == b"42"
def test_session_path(app, client):
@@ -400,21 +381,14 @@ def test_session_secret_key_fallbacks(app, client) -> None:
def get_session() -> dict[str, t.Any]:
return dict(flask.session)
- # 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"],
- )
+ # Set session with initial secret key
client.post()
assert client.get().json == {"a": 1}
# Change secret key, session can't be loaded and appears empty
- app.secret_key = "? key"
+ app.secret_key = "new test key"
assert client.get().json == {}
- # Rotate the valid keys, session can be loaded
- app.secret_key, app.config["SECRET_KEY_FALLBACKS"] = (
- "+1 key",
- ["0 key", "-1 key"],
- )
+ # Add initial secret key as fallback, session can be loaded
+ app.config["SECRET_KEY_FALLBACKS"] = ["test key"]
assert client.get().json == {"a": 1}
@@ -1420,21 +1394,20 @@ def test_url_for_passes_special_values_to_build_error_handler(app):
def test_static_files(app, client):
- with client.get("/static/index.html") as rv:
- assert rv.status_code == 200
- assert rv.data.strip() == b"