diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..45198266 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "pallets/flask", + "image": "mcr.microsoft.com/devcontainers/python:3", + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.launchArgs": [ + "-X", + "dev" + ] + } + } + }, + "onCreateCommand": ".devcontainer/on-create-command.sh" +} diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh new file mode 100755 index 00000000..eaebea61 --- /dev/null +++ b/.devcontainer/on-create-command.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +python3 -m venv --upgrade-deps .venv +. .venv/bin/activate +pip install -r requirements/dev.txt +pip install -e . +pre-commit install --install-hooks diff --git a/.editorconfig b/.editorconfig index e32c8029..2ff985a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,5 @@ end_of_line = lf charset = utf-8 max_line_length = 88 -[*.{yml,yaml,json,js,css,html}] +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] indent_size = 2 diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index c2a15eee..0917c797 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -5,7 +5,7 @@ about: Report a bug in Flask (not other projects which depend on Flask) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index abe39156..a8f9f0b7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,11 @@ blank_issues_enabled: false contact_links: - name: Security issue - url: security@palletsprojects.com - about: Do not report security issues publicly. Email our security contact. - - name: Questions - url: https://stackoverflow.com/questions/tagged/flask?tab=Frequent - about: Search for and ask questions about your code on Stack Overflow. - - name: Questions and discussions + url: https://github.com/pallets/flask/security/advisories/new + about: Do not report security issues publicly. Create a private advisory. + - name: Questions on GitHub Discussions + url: https://github.com/pallets/flask/discussions/ + about: Ask questions about your own code on the Discussions tab. + - name: Questions on Discord url: https://discord.gg/pallets - about: Discuss questions about your code on our Discord chat. + about: Ask questions about your own code on our Discord chat. diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index fcfac71b..00000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,19 +0,0 @@ -# Security Policy - -If you believe you have identified a security issue with a Pallets -project, **do not open a public issue**. To responsibly report a -security issue, please email security@palletsprojects.com. A security -team member will contact you acknowledging the report and how to -continue. - -Be sure to include as much detail as necessary in your report. As with -reporting normal issues, a minimal reproducible example will help the -maintainers address the issue faster. If you are able, you may also -include a fix for the issue generated with `git format-patch`. - -The current and previous release will receive security patches, with -older versions evaluated based on usage information and severity. - -After fixing an issue, we will make a security release along with an -announcement on our blog. We may obtain a CVE id as well. You may -include a name and link if you would like to be credited for the report. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 90f94bc3..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 -updates: -- package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - day: "monday" - time: "16:00" - timezone: "UTC" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 29fd35f8..eb124d25 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,7 @@ -- fixes # +fixes # +--> - -Checklist: - -- [ ] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change. -- [ ] Add or update relevant docs, in the docs folder and in code. -- [ ] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue. -- [ ] Add `.. versionchanged::` entries in any relevant code docs. -- [ ] Run `pre-commit` hooks and fix any issues. -- [ ] Run `pytest` and `tox`, no tests failed. diff --git a/.github/workflows/lock.yaml b/.github/workflows/lock.yaml index cd89f67c..533151a8 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,18 +1,26 @@ -# This does not automatically close "stale" issues. Instead, it locks closed issues after 2 weeks of no activity. -# If there's a new issue related to an old one, we've found it's much easier to work on as a new issue. - -name: 'Lock threads' +name: Lock inactive closed issues +# Lock closed issues that have not received any further activity for two weeks. +# This does not close open issues, only humans may do that. It is easier to +# respond to new issues with fresh examples rather than continuing discussions +# on old issues. on: schedule: - cron: '0 0 * * *' - +permissions: {} +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@v3 + - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 with: - github-token: ${{ github.token }} issue-inactive-days: 14 pr-inactive-days: 14 + discussion-inactive-days: 14 diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 00000000..eff10995 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,29 @@ +name: pre-commit +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 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..0c4f301a --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,62 @@ +name: Publish +on: + push: + tags: ['*'] +permissions: {} +concurrency: + group: publish-${{ github.event.push.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 + - 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 + with: + name: dist + path: dist/ + if-no-files-found: error + create-release: + needs: [build] + 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/ + - name: create release + run: gh release create --draft --repo ${GITHUB_REPOSITORY} ${GITHUB_REF_NAME} dist/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + needs: [build] + environment: + name: publish + url: https://pypi.org/project/Flask/${{ github.ref_name }} + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.build.outputs.artifact-id }} + path: dist/ + - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + packages-dir: "dist/" diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 674fb8b7..97064c8c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,51 +1,63 @@ name: Tests on: - push: - branches: - - main - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' pull_request: - branches: - - main - - '*.x' - paths-ignore: - - 'docs/**' - - '*.md' - - '*.rst' + 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 jobs: tests: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} + name: ${{ matrix.name || matrix.python }} + runs-on: ${{ matrix.os || 'ubuntu-latest' }} strategy: fail-fast: false matrix: include: - - {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310} - - {name: Windows, python: '3.10', os: windows-latest, tox: py310} - - {name: Mac, python: '3.10', os: macos-latest, tox: py310} - - {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311} - - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} - - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - - {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37} - - {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min} - - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev} - - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} + - {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'} + - {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} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - 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 with: python-version: ${{ matrix.python }} - cache: 'pip' - cache-dependency-path: 'requirements/*.txt' - - name: update pip - run: | - pip install -U wheel - pip install -U setuptools - python -m pip install -U pip - - run: pip install tox - - run: tox -e ${{ matrix.tox }} + - 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 + 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 diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 00000000..04082427 --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,22 @@ +name: GitHub Actions security analysis with zizmor +on: + pull_request: + paths: ["**/*.yaml?"] + push: + branches: [main, stable] + paths: ["**/*.yaml?"] +permissions: {} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + zizmor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 + with: + advanced-security: false + annotations: true diff --git a/.gitignore b/.gitignore index e6713351..8441e5a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,8 @@ -.DS_Store -.env -.flaskenv -*.pyc -*.pyo -env/ -venv/ -.venv/ -env* -dist/ -build/ -*.egg -*.egg-info/ -.tox/ -.cache/ -.pytest_cache/ .idea/ -docs/_build/ -.vscode - -# Coverage reports +.vscode/ +__pycache__/ +dist/ +.coverage* htmlcov/ -.coverage -.coverage.* -*,cover +.tox/ +docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d767e60..5d1c89cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,38 +1,23 @@ -ci: - autoupdate_branch: "2.1.x" - autoupdate_schedule: monthly repos: - - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13 hooks: - - id: pyupgrade - args: ["--py36-plus"] - - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.2 + - id: ruff-check + - id: ruff-format + - repo: https://github.com/astral-sh/uv-pre-commit + rev: fa60a193803535a9e2accdb3ca4b1b584b1150cb # frozen: 0.11.15 hooks: - - id: reorder-python-imports - name: Reorder Python imports (src, tests) - files: "^(?!examples/)" - args: ["--application-directories", "src"] - additional_dependencies: ["setuptools>60.9"] - - repo: https://github.com/psf/black - rev: 22.6.0 + - id: uv-lock + - repo: https://github.com/codespell-project/codespell + rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-implicit-str-concat - - repo: https://github.com/peterdemin/pip-compile-multi - rev: v2.4.6 - hooks: - - id: pip-compile-multi-verify + - id: codespell + args: ['--write-changes'] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: + - id: check-merge-conflict + - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 346900b2..acbd83f9 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,13 +1,10 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: "3.10" -python: - install: - - requirements: requirements/docs.txt - - method: pip - path: . -sphinx: - builder: dirhtml - fail_on_warning: true + 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 diff --git a/CHANGES.rst b/CHANGES.rst index 70adbca9..232b144a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,273 @@ +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 + +- Fix signing key selection order when key rotation is enabled via + ``SECRET_KEY_FALLBACKS``. :ghsa:`4grg-w6v8-c28g` +- Fix type hint for ``cli_runner.invoke``. :issue:`5645` +- ``flask --help`` loads the app and plugins first to make sure all commands + are shown. :issue:`5673` +- Mark sans-io base class as being able to handle views that return + ``AsyncIterable``. This is not accurate for Flask, but makes typing easier + for Quart. :pr:`5659` + + +Version 3.1.0 +------------- + +Released 2024-11-13 + +- Drop support for Python 3.8. :pr:`5623` +- Update minimum dependency versions to latest feature releases. + Werkzeug >= 3.1, ItsDangerous >= 2.2, Blinker >= 1.9. :pr:`5624,5633` +- Provide a configuration option to control automatic option + responses. :pr:`5496` +- ``Flask.open_resource``/``open_instance_resource`` and + ``Blueprint.open_resource`` take an ``encoding`` parameter to use when + opening in text mode. It defaults to ``utf-8``. :issue:`5504` +- ``Request.max_content_length`` can be customized per-request instead of only + through the ``MAX_CONTENT_LENGTH`` config. Added + ``MAX_FORM_MEMORY_SIZE`` and ``MAX_FORM_PARTS`` config. Added documentation + about resource limits to the security page. :issue:`5625` +- Add support for the ``Partitioned`` cookie attribute (CHIPS), with the + ``SESSION_COOKIE_PARTITIONED`` config. :issue:`5472` +- ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. + ``load_dotenv`` loads default files in addition to a path unless + ``load_defaults=False`` is passed. :issue:`5628` +- Support key rotation with the ``SECRET_KEY_FALLBACKS`` config, a list of old + secret keys that can still be used for unsigning. Extensions will need to + add support. :issue:`5621` +- Fix how setting ``host_matching=True`` or ``subdomain_matching=False`` + interacts with ``SERVER_NAME``. Setting ``SERVER_NAME`` no longer restricts + requests to only that domain. :issue:`5553` +- ``Request.trusted_hosts`` is checked during routing, and can be set through + the ``TRUSTED_HOSTS`` config. :issue:`5636` + + +Version 3.0.3 +------------- + +Released 2024-04-07 + +- The default ``hashlib.sha1`` may not be available in FIPS builds. Don't + access it at import time so the developer has time to change the default. + :issue:`5448` +- Don't initialize the ``cli`` attribute in the sansio scaffold, but rather in + the ``Flask`` concrete class. :pr:`5270` + + +Version 3.0.2 +------------- + +Released 2024-02-03 + +- Correct type for ``jinja_loader`` property. :issue:`5388` +- Fix error with ``--extra-files`` and ``--exclude-patterns`` CLI options. + :issue:`5391` + + +Version 3.0.1 +------------- + +Released 2024-01-18 + +- Correct type for ``path`` argument to ``send_file``. :issue:`5336` +- Fix a typo in an error message for the ``flask run --key`` option. :pr:`5344` +- Session data is untagged without relying on the built-in ``json.loads`` + ``object_hook``. This allows other JSON providers that don't implement that. + :issue:`5381` +- Address more type findings when using mypy strict mode. :pr:`5383` + + +Version 3.0.0 +------------- + +Released 2023-09-30 + +- Remove previously deprecated code. :pr:`5223` +- Deprecate the ``__version__`` attribute. Use feature detection, or + ``importlib.metadata.version("flask")``, instead. :issue:`5230` +- Restructure the code such that the Flask (app) and Blueprint + classes have Sans-IO bases. :pr:`5127` +- Allow self as an argument to url_for. :pr:`5264` +- Require Werkzeug >= 3.0.0. + + +Version 2.3.3 +------------- + +Released 2023-08-21 + +- Python 3.12 compatibility. +- Require Werkzeug >= 2.3.7. +- Use ``flit_core`` instead of ``setuptools`` as build backend. +- Refactor how an app's root and instance paths are determined. :issue:`5160` + + +Version 2.3.2 +------------- + +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 +------------- + +Released 2023-04-25 + +- Restore deprecated ``from flask import Markup``. :issue:`5084` + + +Version 2.3.0 +------------- + +Released 2023-04-25 + +- Drop support for Python 3.7. :pr:`5072` +- Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, + itsdangerous>=2.1.2, click>=8.1.3. +- Remove previously deprecated code. :pr:`4995` + + - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and + ``_request_ctx_stack`` objects are removed. ``top`` still exists to give + extensions more time to update, but it will be removed. + - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` + property are removed. + - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, + ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` + are removed. + - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and + ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. + - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators + are removed. + - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the + corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. + - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. + - Calling setup methods on blueprints after registration is an error instead of a + warning. :pr:`4997` + +- Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them + directly from ``markupsafe`` instead. :pr:`4996` +- The ``app.got_first_request`` property is deprecated. :pr:`4997` +- The ``locked_cached_property`` decorator is deprecated. Use a lock inside the + decorated function if locking is needed. :issue:`4993` +- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The + ``signals_available`` attribute is deprecated. :issue:`5056` +- Signals support ``async`` subscriber functions. :pr:`5049` +- Remove uses of locks that could cause requests to block each other very briefly. + :issue:`4993` +- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. + :pr:`4947` +- Ensure subdomains are applied with nested blueprints. :issue:`4834` +- ``config.from_file`` can use ``text=False`` to indicate that the parser wants a + binary file instead. :issue:`4989` +- If a blueprint is created with an empty name it raises a ``ValueError``. + :issue:`5010` +- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not + to set the domain, which modern browsers interpret as an exact match rather than + a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. + :issue:`5051` +- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain + matching is in use. :issue:`5004` +- Use postponed evaluation of annotations. :pr:`5071` + + +Version 2.2.5 +------------- + +Released 2023-05-02 + +- Update for compatibility with Werkzeug 2.3.3. +- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. + + +Version 2.2.4 +------------- + +Released 2023-04-25 + +- Update for compatibility with Werkzeug 2.3. + + +Version 2.2.3 +------------- + +Released 2023-02-15 + +- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` +- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` +- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` + + +Version 2.2.2 +------------- + +Released 2022-08-08 + +- Update Werkzeug dependency to >= 2.2.2. This includes fixes related + to the new faster router, header parsing, and the development + server. :pr:`4754` +- Fix the default value for ``app.env`` to be ``"production"``. This + attribute remains deprecated. :issue:`4740` + + Version 2.2.1 ------------- @@ -12,7 +282,7 @@ Version 2.2.0 Released 2022-08-01 -- Remove previously deprecated code. :pr:`4337` +- Remove previously deprecated code. :pr:`4667` - Old names for some ``send_file`` parameters have been removed. ``download_name`` replaces ``attachment_filename``, ``max_age`` @@ -188,7 +458,7 @@ Released 2022-03-28 or ``AppContext.g`` instead. :issue:`3898` - ``copy_current_request_context`` can decorate async functions. :pr:`4303` -- The CLI uses ``importlib.metadata`` instead of ``setuptools`` to +- The CLI uses ``importlib.metadata`` instead of ``pkg_resources`` to load command entry points. :issue:`4419` - Overriding ``FlaskClient.open`` will not cause an error on redirect. :issue:`3396` @@ -665,7 +935,7 @@ Released 2018-04-26 explicitly for each exception if you want to avoid traversing the MRO. :pr:`2362` - Fix incorrect JSON encoding of aware, non-UTC datetimes. :pr:`2374` -- Template auto reloading will honor debug mode even even if +- Template auto reloading will honor debug mode even if ``Flask.jinja_env`` was already accessed. :pr:`2373` - The following old deprecated code was removed. :issue:`2385` @@ -1147,7 +1417,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 Jinja2 environment's ``list_templates`` method not +- Fixed the Jinja environment's ``list_templates`` method not returning the correct names when blueprints or modules were involved. @@ -1233,7 +1503,7 @@ Released 2010-12-31 - Fixed an issue where the default ``OPTIONS`` response was not exposing all valid methods in the ``Allow`` header. -- Jinja2 template loading syntax now allows "./" in front of a +- Jinja template loading syntax now allows "./" in front of a template load path. Previously this caused issues with module setups. - Fixed an issue where the subdomain setting for modules was ignored diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f4ba197d..00000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at report@palletsprojects.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index d5e3a3f7..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,227 +0,0 @@ -How to contribute to Flask -========================== - -Thank you for considering contributing to Flask! - - -Support questions ------------------ - -Please don't use the issue tracker for this. The issue tracker is a tool -to address bugs and feature requests in Flask itself. Use one of the -following resources for questions about using Flask or issues with your -own code: - -- The ``#questions`` channel on our Discord chat: - https://discord.gg/pallets -- Ask on `Stack Overflow`_. Search with Google first using: - ``site:stackoverflow.com flask {search term, exception message, etc.}`` -- Ask on our `GitHub Discussions`_ for long term discussion or larger - questions. - -.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent -.. _GitHub Discussions: https://github.com/pallets/flask/discussions - - -Reporting issues ----------------- - -Include the following information in your post: - -- Describe what you expected to happen. -- If possible, include a `minimal reproducible example`_ to help us - identify the issue. This also helps check that the issue is not with - your own code. -- Describe what actually happened. Include the full traceback if there - was an exception. -- List your Python and Flask versions. If possible, check if this - issue is already fixed in the latest releases or the latest code in - the repository. - -.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example - - -Submitting patches ------------------- - -If there is not an open issue for what you want to submit, prefer -opening one for discussion before working on a PR. You can work on any -issue that doesn't have an open PR linked to it or a maintainer assigned -to it. These show up in the sidebar. No need to ask if you can work on -an issue that interests you. - -Include the following in your patch: - -- Use `Black`_ to format your code. This and other tools will run - automatically if you install `pre-commit`_ using the instructions - below. -- Include tests if your patch adds or changes code. Make sure the test - fails without your patch. -- Update any relevant docs pages and docstrings. Docs pages and - docstrings should be wrapped at 72 characters. -- Add an entry in ``CHANGES.rst``. Use the same style as other - entries. Also include ``.. versionchanged::`` inline changelogs in - relevant docstrings. - -.. _Black: https://black.readthedocs.io -.. _pre-commit: https://pre-commit.com - - -First time setup -~~~~~~~~~~~~~~~~ - -- Download and install the `latest version of git`_. -- Configure git with your `username`_ and `email`_. - - .. code-block:: text - - $ git config --global user.name 'your name' - $ git config --global user.email 'your email' - -- Make sure you have a `GitHub account`_. -- Fork Flask to your GitHub account by clicking the `Fork`_ button. -- `Clone`_ the main repository locally. - - .. code-block:: text - - $ git clone https://github.com/pallets/flask - $ cd flask - -- Add your fork as a remote to push your work to. Replace - ``{username}`` with your username. This names the remote "fork", the - default Pallets remote is "origin". - - .. code-block:: text - - $ git remote add fork https://github.com/{username}/flask - -- Create a virtualenv. - - - - Linux/macOS - - .. code-block:: text - - $ python3 -m venv env - $ . env/bin/activate - - - Windows - - .. code-block:: text - - > py -3 -m venv env - > env\Scripts\activate - -- Upgrade pip and setuptools. - - .. code-block:: text - - $ python -m pip install --upgrade pip setuptools - -- Install the development dependencies, then install Flask in editable - mode. - - .. code-block:: text - - $ pip install -r requirements/dev.txt && pip install -e . - -- Install the pre-commit hooks. - - .. code-block:: text - - $ pre-commit install - -.. _latest version of git: https://git-scm.com/downloads -.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git -.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address -.. _GitHub account: https://github.com/join -.. _Fork: https://github.com/pallets/flask/fork -.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork - - -Start coding -~~~~~~~~~~~~ - -- Create a branch to identify the issue you would like to work on. If - you're submitting a bug or documentation fix, branch off of the - latest ".x" branch. - - .. code-block:: text - - $ git fetch origin - $ git checkout -b your-branch-name origin/2.0.x - - If you're submitting a feature addition or change, branch off of the - "main" branch. - - .. code-block:: text - - $ git fetch origin - $ git checkout -b your-branch-name origin/main - -- Using your favorite editor, make your changes, - `committing as you go`_. -- Include tests that cover any code changes you make. Make sure the - test fails without your patch. Run the tests as described below. -- Push your commits to your fork on GitHub and - `create a pull request`_. Link to the issue being addressed with - ``fixes #123`` in the pull request. - - .. code-block:: text - - $ git push --set-upstream fork your-branch-name - -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes -.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request - - -Running the tests -~~~~~~~~~~~~~~~~~ - -Run the basic test suite with pytest. - -.. code-block:: text - - $ pytest - -This runs the tests for the current environment, which is usually -sufficient. CI will run the full suite when you submit your pull -request. You can run the full test suite with tox if you don't want to -wait. - -.. code-block:: text - - $ tox - - -Running test coverage -~~~~~~~~~~~~~~~~~~~~~ - -Generating a report of lines that do not have test coverage can indicate -where to start contributing. Run ``pytest`` using ``coverage`` and -generate a report. - -.. code-block:: text - - $ pip install coverage - $ coverage run -m pytest - $ coverage html - -Open ``htmlcov/index.html`` in your browser to explore the report. - -Read more about `coverage `__. - - -Building the docs -~~~~~~~~~~~~~~~~~ - -Build the docs in the ``docs`` directory using Sphinx. - -.. code-block:: text - - $ cd docs - $ make html - -Open ``_build/html/index.html`` in your browser to view the docs. - -Read more about `Sphinx `__. diff --git a/LICENSE.rst b/LICENSE.txt similarity index 100% rename from LICENSE.rst rename to LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 65a97749..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include CHANGES.rst -include CONTRIBUTING.rst -include tox.ini -include requirements/*.txt -graft artwork -graft docs -prune docs/_build -graft examples -graft tests -include src/flask/py.typed -global-exclude *.pyc diff --git a/README.md b/README.md new file mode 100644 index 00000000..64f56cac --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +
+ +# Flask + +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 +complex applications. It began as a simple wrapper around [Werkzeug] +and [Jinja], and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +[WSGI]: https://wsgi.readthedocs.io/ +[Werkzeug]: https://werkzeug.palletsprojects.com/ +[Jinja]: https://jinja.palletsprojects.com/ + +## A Simple Example + +```python +# save this as app.py +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" +``` + +``` +$ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + +## Donate + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ diff --git a/README.rst b/README.rst deleted file mode 100644 index 3d1c3882..00000000 --- a/README.rst +++ /dev/null @@ -1,82 +0,0 @@ -Flask -===== - -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 -complex applications. It began as a simple wrapper around `Werkzeug`_ -and `Jinja`_ and has become one of the most popular Python web -application frameworks. - -Flask offers suggestions, but doesn't enforce any dependencies or -project layout. It is up to the developer to choose the tools and -libraries they want to use. There are many extensions provided by the -community that make adding new functionality easy. - -.. _WSGI: https://wsgi.readthedocs.io/ -.. _Werkzeug: https://werkzeug.palletsprojects.com/ -.. _Jinja: https://jinja.palletsprojects.com/ - - -Installing ----------- - -Install and update using `pip`_: - -.. code-block:: text - - $ pip install -U Flask - -.. _pip: https://pip.pypa.io/en/stable/getting-started/ - - -A Simple Example ----------------- - -.. code-block:: python - - # save this as app.py - from flask import Flask - - app = Flask(__name__) - - @app.route("/") - def hello(): - return "Hello, World!" - -.. code-block:: text - - $ flask run - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) - - -Contributing ------------- - -For guidance on setting up a development environment and how to make a -contribution to Flask, see the `contributing guidelines`_. - -.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst - - -Donate ------- - -The Pallets organization develops and supports Flask and the libraries -it uses. In order to grow the community of contributors and users, and -allow the maintainers to devote more time to the projects, `please -donate today`_. - -.. _please donate today: https://palletsprojects.com/donate - - -Links ------ - -- Documentation: https://flask.palletsprojects.com/ -- Changes: https://flask.palletsprojects.com/changes/ -- PyPI Releases: https://pypi.org/project/Flask/ -- Source Code: https://github.com/pallets/flask/ -- Issue Tracker: https://github.com/pallets/flask/issues/ -- Website: https://palletsprojects.com/p/flask/ -- Twitter: https://twitter.com/PalletsTeam -- Chat: https://discord.gg/pallets diff --git a/artwork/LICENSE.rst b/artwork/LICENSE.rst deleted file mode 100644 index 99c58a21..00000000 --- a/artwork/LICENSE.rst +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 2010 Pallets - -This logo or a modified version may be used by anyone to refer to the -Flask project, but does not indicate endorsement by the project. - -Redistribution and use in source (SVG) and binary (renders in PNG, etc.) -forms, with or without modification, are permitted provided that the -following conditions are met: - -1. Redistributions of source code must retain the above copyright - notice and this list of conditions. - -2. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -We would appreciate that you make the image a link to -https://palletsprojects.com/p/flask/ if you use it in a medium that -supports links. diff --git a/artwork/logo-full.svg b/artwork/logo-full.svg deleted file mode 100644 index 8c0748a2..00000000 --- a/artwork/logo-full.svg +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/artwork/logo-lineart.svg b/artwork/logo-lineart.svg deleted file mode 100644 index 615260dc..00000000 --- a/artwork/logo-lineart.svg +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/docs/_static/flask-icon.png b/docs/_static/flask-icon.png deleted file mode 100644 index 55cb8478..00000000 Binary files a/docs/_static/flask-icon.png and /dev/null differ diff --git a/docs/_static/flask-icon.svg b/docs/_static/flask-icon.svg new file mode 100644 index 00000000..c802da9a --- /dev/null +++ b/docs/_static/flask-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/_static/flask-logo.png b/docs/_static/flask-logo.png deleted file mode 100644 index ce236061..00000000 Binary files a/docs/_static/flask-logo.png and /dev/null differ diff --git a/docs/_static/flask-logo.svg b/docs/_static/flask-logo.svg new file mode 100644 index 00000000..c216b617 --- /dev/null +++ b/docs/_static/flask-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/_static/flask-name.svg b/docs/_static/flask-name.svg new file mode 100644 index 00000000..b46782d2 --- /dev/null +++ b/docs/_static/flask-name.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/docs/_static/no.png b/docs/_static/no.png deleted file mode 100644 index 644c3f70..00000000 Binary files a/docs/_static/no.png and /dev/null differ diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png index 3f789157..ad025545 100644 Binary files a/docs/_static/pycharm-run-config.png and b/docs/_static/pycharm-run-config.png differ diff --git a/docs/_static/yes.png b/docs/_static/yes.png deleted file mode 100644 index 56917ab2..00000000 Binary files a/docs/_static/yes.png and /dev/null differ diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst deleted file mode 100644 index 9c36158a..00000000 --- a/docs/advanced_foreword.rst +++ /dev/null @@ -1,46 +0,0 @@ -Foreword for Experienced Programmers -==================================== - -Thread-Locals in Flask ----------------------- - -One of the design decisions in Flask was that simple tasks should be simple; -they should not take a lot of code and yet they should not limit you. Because -of that, Flask has a few design choices that some people might find -surprising or unorthodox. For example, Flask uses thread-local objects -internally so that you don’t have to pass objects around from -function to function within a request in order to stay threadsafe. -This approach is convenient, but requires a valid -request context for dependency injection or when attempting to reuse code which -uses a value pegged to the request. The Flask project is honest about -thread-locals, does not hide them, and calls out in the code and documentation -where they are used. - -Develop for the Web with Caution --------------------------------- - -Always keep security in mind when building web applications. - -If you write a web application, you are probably allowing users to register -and leave their data on your server. The users are entrusting you with data. -And even if you are the only user that might leave data in your application, -you still want that data to be stored securely. - -Unfortunately, there are many ways the security of a web application can be -compromised. Flask protects you against one of the most common security -problems of modern web applications: cross-site scripting (XSS). Unless you -deliberately mark insecure HTML as secure, Flask and the underlying Jinja2 -template engine have you covered. But there are many more ways to cause -security problems. - -The documentation will warn you about aspects of web development that require -attention to security. Some of these security concerns are far more complex -than one might think, and we all sometimes underestimate the likelihood that a -vulnerability will be exploited - until a clever attacker figures out a way to -exploit our applications. And don't think that your application is not -important enough to attract an attacker. Depending on the kind of attack, -chances are that automated bots are probing for ways to fill your database with -spam, links to malicious software, and the like. - -Flask is no different from any other framework in that you the developer must -build with caution, watching for exploits when building to your requirements. diff --git a/docs/api.rst b/docs/api.rst index 880720b4..52b25376 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,7 +3,7 @@ API .. module:: flask -This part of the documentation covers all the interfaces of Flask. For +This part of the documentation covers all the interfaces of Flask. For parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. @@ -31,17 +31,15 @@ Incoming Request Data :inherited-members: :exclude-members: json_module -.. attribute:: request +.. data:: 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. + A proxy to the request data for the current request, an instance of + :class:`.Request`. - This is a proxy. See :ref:`notes-on-proxies` for more information. + This is only available when a :doc:`request context ` is + active. - The request object is an instance of a :class:`~flask.Request`. + This is a proxy. See :ref:`context-visibility` for more information. Response Objects @@ -62,40 +60,33 @@ does this is by using a signed cookie. The user can look at the session contents, but can't modify it unless they know the secret key, so make sure to set that to something complex and unguessable. -To access the current session you can use the :class:`session` object: +To access the current session you can use the :data:`.session` proxy. -.. class:: session +.. data:: session - The session object works pretty much like an ordinary dict, with the - difference that it keeps track of modifications. + A proxy to the session data for the current request, an instance of + :class:`.SessionMixin`. - This is a proxy. See :ref:`notes-on-proxies` for more information. + This is only available when a :doc:`request context ` is + active. - The following attributes are interesting: + This is a proxy. See :ref:`context-visibility` for more information. - .. attribute:: new + The session object works like a dict but tracks assignment and access to its + keys. It cannot track modifications to mutable values, you need to set + :attr:`~.SessionMixin.modified` manually when modifying a list, dict, etc. - ``True`` if the session is new, ``False`` otherwise. + .. code-block:: python - .. attribute:: modified - - ``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) + # appending to a list is not detected + session["numbers"].append(42) # so mark it as modified yourself session.modified = True - .. 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. + 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`. Session Interface @@ -155,23 +146,24 @@ Application Globals To share data that is valid for one request only from one function to another, a global variable is not good enough because it would break in -threaded environments. Flask provides you with a special object that +threaded environments. Flask provides you with a special object that ensures it is only valid for the active request and that will return -different values for each request. In a nutshell: it does the right -thing, like it does for :class:`request` and :class:`session`. +different values for each request. In a nutshell: it does the right +thing, like it does for :data:`.request` and :data:`.session`. .. data:: g - 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`. + 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`. - 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 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 proxy. See :ref:`notes-on-proxies` for more information. + This is only available when an :doc:`app context ` is active. + + This is a proxy. See :ref:`context-visibility` for more information. .. versionchanged:: 0.10 Bound to the application context instead of the request context. @@ -185,17 +177,16 @@ Useful Functions and Classes .. data:: current_app - 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. + A proxy to the :class:`.Flask` application handling the current request or + other activity. - 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 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 a proxy. See :ref:`notes-on-proxies` for more information. + This is only available when an :doc:`app context ` is active. + + This is a proxy. See :ref:`context-visibility` for more information. .. autofunction:: has_request_context @@ -217,10 +208,6 @@ Useful Functions and Classes .. autofunction:: send_from_directory -.. autofunction:: escape - -.. autoclass:: Markup - :members: escape, unescape, striptags Message Flashing ---------------- @@ -248,7 +235,7 @@ HTML `` @@ -270,12 +257,6 @@ HTML `` @@ -244,8 +245,9 @@ 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, or the ``Content-Type`` header is not set to -``application/json``, a 400 Bad Request error will be raised. +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. .. code-block:: python diff --git a/docs/patterns/mongoengine.rst b/docs/patterns/mongoengine.rst index 015e7b61..8d49de7c 100644 --- a/docs/patterns/mongoengine.rst +++ b/docs/patterns/mongoengine.rst @@ -10,8 +10,7 @@ A running MongoDB server and `Flask-MongoEngine`_ are required. :: pip install flask-mongoengine .. _MongoEngine: http://mongoengine.org -.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io - +.. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/ Configuration ------------- @@ -80,7 +79,7 @@ Queries Use the class ``objects`` attribute to make queries. A keyword argument looks for an equal value on the field. :: - bttf = Movies.objects(title="Back To The Future").get_or_404() + bttf = Movie.objects(title="Back To The Future").get_or_404() Query operators may be used by concatenating them with the field name using a double-underscore. ``objects``, and queries returned by diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 13f8270e..90fa8a8f 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -42,19 +42,20 @@ You should then end up with something like that:: But how do you run your application now? The naive ``python yourapplication/__init__.py`` will not work. Let's just say that Python does not want modules in packages to be the startup file. But that is not -a big problem, just add a new file called :file:`setup.py` next to the inner -:file:`yourapplication` folder with the following contents:: +a big problem, just add a new file called :file:`pyproject.toml` next to the inner +:file:`yourapplication` folder with the following contents: - from setuptools import setup +.. code-block:: toml - setup( - name='yourapplication', - packages=['yourapplication'], - include_package_data=True, - install_requires=[ - 'flask', - ], - ) + [project] + name = "yourapplication" + dependencies = [ + "flask", + ] + + [build-system] + requires = ["flit_core<4"] + build-backend = "flit_core.buildapi" Install your application so it is importable: @@ -66,6 +67,8 @@ To use the ``flask`` command and run your application you need to set the ``--app`` option that tells Flask where to find the application instance: +.. code-block:: text + $ flask --app yourapplication run What did we gain from this? Now we can restructure the application a bit @@ -98,7 +101,7 @@ And this is what :file:`views.py` would look like:: You should then end up with something like that:: /yourapplication - setup.py + pyproject.toml /yourapplication __init__.py views.py diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 734d550c..9e9afe48 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -34,8 +34,7 @@ official documentation on the `declarative`_ extension. Here's the example :file:`database.py` module for your application:: from sqlalchemy import create_engine - from sqlalchemy.orm import scoped_session, sessionmaker - from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base engine = create_engine('sqlite:////tmp/test.db') db_session = scoped_session(sessionmaker(autocommit=False, @@ -132,9 +131,8 @@ 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 request or application context shutdown. Put this into your -application module:: +As in the declarative approach, you need to close the session after each app +context. 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 5932589f..f42e0f8e 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -1,9 +1,9 @@ Using SQLite 3 with Flask ========================= -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). +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. 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 e35ac4ab..fc2f1739 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -8,6 +8,21 @@ 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 ----------- @@ -20,7 +35,7 @@ data and to then invoke that function and pass it to a response object:: def generate(): for row in iter_all_rows(): yield f"{','.join(row)}\n" - return generate(), {"Content-Type": "text/csv") + return generate(), {"Content-Type": "text/csv"} Each ``yield`` expression is directly sent to the browser. Note though that some WSGI middlewares might break streaming, so be careful there in @@ -29,7 +44,7 @@ debug environments with profilers and other things you might have enabled. Streaming from Templates ------------------------ -The Jinja2 template engine supports rendering a template piece by +The Jinja template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. @@ -49,13 +64,13 @@ the template. Streaming with Context ---------------------- -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``. +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``. If your generator function relies on data in ``request``, use the -:func:`~flask.stream_with_context` wrapper. This will keep the request -context active during the generator. +:func:`.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 3d626f50..cb1208fe 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 Jinja2 that this data is already HTML-escaped with +so we have to tell Jinja that this data is already HTML-escaped with the ``|safe`` filter. 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 85668aa1..712ba977 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -41,7 +41,7 @@ itself. To run the application, use the ``flask`` command or ``python -m flask``. You need to tell the Flask where your application -is with the ``-app`` option. +is with the ``--app`` option. .. code-block:: text @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) @@ -139,18 +139,16 @@ how you're using untrusted data. .. code-block:: python + from flask import request from markupsafe import escape - @app.route("/") - def hello(name): + @app.route("/hello") + def hello(): + name = request.args.get("name", "Flask") return f"Hello, {escape(name)}!" -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. +If a user submits ``/hello?name=``, escaping causes +it to be rendered as text, rather than running the script in the user's browser. Routing @@ -260,7 +258,7 @@ Why would you want to build URLs using the URL reversing function For example, here we use the :meth:`~flask.Flask.test_request_context` method 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 :ref:`context-locals`. +Python shell. See :doc:`/appcontext`. .. code-block:: python @@ -354,9 +352,17 @@ 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 `Jinja2 +the application secure. Because of that Flask configures the `Jinja `_ template engine for you automatically. +Templates can be used to generate any type of text file. For web applications, you'll +primarily be generating HTML pages, but you can also generate markdown, plain text for +emails, and anything else. + +For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. + +.. _MDN Web Docs: https://developer.mozilla.org/ + To render a template you can use the :func:`~flask.render_template` method. All you have to do is provide the name of the template and the variables you want to pass to the template engine as keyword arguments. @@ -367,7 +373,7 @@ Here's a simple example of how to render a template:: @app.route('/hello/') @app.route('/hello/') def hello(name=None): - return render_template('hello.html', name=name) + return render_template('hello.html', person=name) Flask will look for templates in the :file:`templates` folder. So if your application is a module, this folder is next to that module, if it's a @@ -386,8 +392,8 @@ package it's actually inside your package: /templates /hello.html -For templates you can use the full power of Jinja2 templates. Head over -to the official `Jinja2 Template Documentation +For templates you can use the full power of Jinja templates. Head over +to the official `Jinja Template Documentation `_ for more information. Here is an example template: @@ -396,8 +402,8 @@ Here is an example template: Hello from Flask - {% if name %} -

Hello {{ name }}!

+ {% if person %} +

Hello {{ person }}!

{% else %}

Hello, World!

{% endif %} @@ -411,7 +417,7 @@ know how that works, see :doc:`patterns/templateinheritance`. Basically template inheritance makes it possible to keep certain elements on each page (like header, navigation and footer). -Automatic escaping is enabled, so if ``name`` contains HTML it will be escaped +Automatic escaping is enabled, so if ``person`` contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be safe HTML (for example because it came from a module that converts wiki markup to HTML) you can mark it as safe by using the @@ -443,105 +449,58 @@ 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 -: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: +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. - -.. _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:: +.. code-block:: python from flask import request - 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' +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. -The other possibility is passing a whole WSGI environment to the -:meth:`~flask.Flask.request_context` method:: +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. - with app.request_context(environ): - assert request.method == 'POST' +.. code-block:: python -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']) + @app.route("/login", methods=["GET", "POST"]) def login(): error = None - if request.method == 'POST': - if valid_login(request.form['username'], - request.form['password']): - return log_the_user_in(request.form['username']) + + if request.method == "POST": + if valid_login(request.form["username"], request.form["password"]): + return store_login(request.form["username"]) else: - 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) + error = "Invalid username or password" -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. + # Executed if the request method was GET or the credentials were invalid. + return render_template("login.html", error=error) -To access parameters submitted in the URL (``?key=value``) you can use the -:attr:`~flask.Request.args` attribute:: +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 searchword = request.args.get('key', '') -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. +For a full list of methods and attributes of the request object, see the +:class:`~.Request` documentation. File Uploads diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 2b109770..6660671e 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -1,247 +1,6 @@ -.. currentmodule:: flask +:orphan: The Request Context =================== -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', data={'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 -~~~~~~~ - -If :data:`~signals.signals_available` is true, 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) +Obsolete, see :doc:`/appcontext` instead. diff --git a/docs/server.rst b/docs/server.rst index a34dfab5..d6beb1d8 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the @@ -76,8 +76,8 @@ following example shows that process id 6847 is using port 5000. TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847 macOS Monterey and later automatically starts a service that uses port -5000. To disable the service, go to System Preferences, Sharing, and -disable "AirPlay Receiver". +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. Deferred Errors on Reload diff --git a/docs/shell.rst b/docs/shell.rst index 7e42e285..d8821e23 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,56 +1,37 @@ Working with the Shell ====================== -.. versionadded:: 0.3 +One of the reasons everybody loves Python is the interactive shell. It allows +you to play around with code in real time and immediately get results back. +Flask provides the ``flask shell`` CLI command to start an interactive Python +shell with some setup done to make working with the Flask app easier. -One of the reasons everybody loves Python is the interactive shell. It -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. +.. code-block:: text -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`. + $ flask shell 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 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: +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: >>> 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 27630de6..7ca81a9d 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -1,33 +1,28 @@ Signals ======= -.. versionadded:: 0.6 +Signals are a lightweight way to notify subscribers of certain events during the +lifecycle of the application and each request. When an event occurs, it emits the +signal, which calls each subscriber. -Starting with Flask 0.6, there is integrated support for signalling in -Flask. This support is provided by the excellent `blinker`_ library and -will gracefully fall back if it is not available. +Signals are implemented by the `Blinker`_ library. See its documentation for detailed +information. Flask provides some built-in signals. Extensions may provide their own. -What are signals? Signals help you decouple applications by sending -notifications when actions occur elsewhere in the core framework or -another Flask extensions. In short, signals allow certain senders to -notify subscribers that something happened. +Many signals mirror Flask's decorator-based callbacks with similar names. For example, +the :data:`.request_started` signal is similar to the :meth:`~.Flask.before_request` +decorator. The advantage of signals over handlers is that they can be subscribed to +temporarily, and can't directly affect the application. This is useful for testing, +metrics, auditing, and more. For example, if you want to know what templates were +rendered at what parts of what requests, there is a signal that will notify you of that +information. -Flask comes with a couple of signals and other extensions might provide -more. Also keep in mind that signals are intended to notify subscribers -and should not encourage subscribers to modify data. You will notice that -there are signals that appear to do the same thing like some of the -builtin decorators do (eg: :data:`~flask.request_started` is very similar -to :meth:`~flask.Flask.before_request`). However, there are differences in -how they work. The core :meth:`~flask.Flask.before_request` handler, for -example, is executed in a specific order and is able to abort the request -early by returning a response. In contrast all signal handlers are -executed in undefined order and do not modify any data. -The big advantage of signals over handlers is that you can safely -subscribe to them for just a split second. These temporary -subscriptions are helpful for unit testing for example. Say you want to -know what templates were rendered as part of a request: signals allow you -to do exactly that. +Core Signals +------------ + +See :ref:`core-signals-list` for a list of all built-in signals. The :doc:`lifecycle` +page also describes the order that signals and decorators execute. + Subscribing to Signals ---------------------- @@ -99,17 +94,12 @@ The example above would then look like this:: ... template, context = templates[0] -.. admonition:: Blinker API Changes - - The :meth:`~blinker.base.Signal.connected_to` method arrived in Blinker - with version 1.1. - Creating Signals ---------------- If you want to use signals in your own application, you can use the blinker library directly. The most common use case are named signals in a -custom :class:`~blinker.base.Namespace`.. This is what is recommended +custom :class:`~blinker.base.Namespace`. This is what is recommended most of the time:: from blinker import Namespace @@ -123,12 +113,6 @@ The name for the signal here makes it unique and also simplifies debugging. You can access the name of the signal with the :attr:`~blinker.base.NamedSignal.name` attribute. -.. admonition:: For Extension Developers - - If you are writing a Flask extension and you want to gracefully degrade for - missing blinker installations, you can do so by using the - :class:`flask.signals.Namespace` class. - .. _signals-sending: Sending Signals @@ -160,17 +144,16 @@ function, you can pass ``current_app._get_current_object()`` as sender. Signals and Flask's Request Context ----------------------------------- -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. +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. Decorator Based Signal Subscriptions ------------------------------------ -With Blinker 1.1 you can also easily subscribe to signals by using the new +You can also easily subscribe to signals by using the :meth:`~blinker.base.NamedSignal.connect_via` decorator:: from flask import template_rendered @@ -179,10 +162,5 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new def when_template_rendered(sender, template, context, **extra): print(f'Template {template.name} is rendered with {context}') -Core Signals ------------- - -Take a look at :ref:`core-signals-list` for a list of all builtin signals. - .. _blinker: https://pypi.org/project/blinker/ diff --git a/docs/templating.rst b/docs/templating.rst index 3cda995e..ed4a52ee 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -1,37 +1,37 @@ Templates ========= -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 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 itself. This requirement is necessary to enable rich extensions. -An extension can depend on Jinja2 being present. +An extension can depend on Jinja being present. -This section only gives a very quick introduction into how Jinja2 +This section only gives a very quick introduction into how Jinja is integrated into Flask. If you want information on the template -engine's syntax itself, head over to the official `Jinja2 Template +engine's syntax itself, head over to the official `Jinja Template Documentation `_ for more information. Jinja Setup ----------- -Unless customized, Jinja2 is configured by Flask as follows: +Unless customized, Jinja is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, - ``.htm``, ``.xml`` as well as ``.xhtml`` when using + ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using :func:`~flask.templating.render_template`. - autoescaping is enabled for all strings when using :func:`~flask.templating.render_template_string`. - 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 - Jinja2 context, additionally to the values that are present by + Jinja context, additionally to the values that are present by default. Standard Context ---------------- -The following global variables are available within Jinja2 templates +The following global variables are available within Jinja templates by default: .. data:: config @@ -115,7 +115,7 @@ markdown to HTML converter. There are three ways to accomplish that: -- In the Python code, wrap the HTML string in a :class:`~flask.Markup` +- In the Python code, wrap the HTML string in a :class:`~markupsafe.Markup` object before passing it to the template. This is in general the recommended way. - Inside the template, use the ``|safe`` filter to explicitly mark a @@ -137,32 +137,58 @@ using in this block. .. _registering-filters: -Registering Filters -------------------- +Registering Filters, Tests, and Globals +--------------------------------------- -If you want to register your own filters in Jinja2 you have two ways to do -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. +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. -The two following examples work the same and both reverse an object:: +Decorate a function with :meth:`~.Flask.template_filter` to register it as a +template filter. - @app.template_filter('reverse') - def reverse_filter(s): - return s[::-1] +.. code-block:: python - def reverse_filter(s): - return s[::-1] - app.jinja_env.filters['reverse'] = reverse_filter + @app.template_filter + def reverse(s): + return reversed(s) -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`:: +.. code-block:: jinja - {% for x in mylist | reverse %} + {% for item in data | 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 ------------------ @@ -211,7 +237,7 @@ strings. This can be used for streaming HTML in chunks to speed up initial page load, or to save memory when rendering a very large template. -The Jinja2 template engine supports rendering a template piece +The Jinja template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. @@ -225,5 +251,11 @@ 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 it remains available in the template. +: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. diff --git a/docs/testing.rst b/docs/testing.rst index 8545bd39..c171abd6 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -192,7 +192,7 @@ which records the request that produced that response. .. code-block:: python def test_logout_redirect(client): - response = client.get("/logout") + response = client.get("/logout", follow_redirects=True) # Check that there was one redirect response. assert len(response.history) == 1 # Check that the second request was to the index page. @@ -275,11 +275,10 @@ 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:`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 +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 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 b06329ea..6418f5ff 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 934f6008..cf132603 100644 --- a/docs/tutorial/database.rst +++ b/docs/tutorial/database.rst @@ -37,6 +37,7 @@ response is sent. :caption: ``flaskr/db.py`` import sqlite3 + from datetime import datetime import click from flask import current_app, g @@ -59,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 @@ -132,6 +133,11 @@ Add the Python functions that will run these SQL commands to the init_db() click.echo('Initialized the database.') + + sqlite3.register_converter( + "timestamp", lambda v: datetime.fromisoformat(v.decode()) + ) + :meth:`open_resource() ` opens a file relative to the ``flaskr`` package, which is useful since you won't necessarily know where that location is when deploying the application later. ``get_db`` @@ -142,6 +148,10 @@ read from the file. that calls the ``init_db`` function and shows a success message to the user. You can read :doc:`/cli` to learn more about writing commands. +The call to :func:`sqlite3.register_converter` tells Python how to +interpret timestamp values in the database. We convert the value to a +:class:`datetime.datetime`. + Register with the Application ----------------------------- diff --git a/docs/tutorial/deploy.rst b/docs/tutorial/deploy.rst index 436ed5e8..eb3a53ac 100644 --- a/docs/tutorial/deploy.rst +++ b/docs/tutorial/deploy.rst @@ -14,22 +14,13 @@ application. Build and Install ----------------- -When you want to deploy your application elsewhere, you build a -distribution file. The current standard for Python distribution is the -*wheel* format, with the ``.whl`` extension. Make sure the wheel library -is installed first: +When you want to deploy your application elsewhere, you build a *wheel* +(``.whl``) file. Install and use the ``build`` tool to do this. .. code-block:: none - $ pip install wheel - -Running ``setup.py`` with Python gives you a command line tool to issue -build-related commands. The ``bdist_wheel`` command will build a wheel -distribution file. - -.. code-block:: none - - $ python setup.py bdist_wheel + $ pip install build + $ python -m build --wheel You can find the file in ``dist/flaskr-1.0.0-py3-none-any.whl``. The file name is in the format of {project name}-{version}-{python tag} @@ -54,7 +45,7 @@ create the database in the instance folder. When Flask detects that it's installed (not in editable mode), it uses a different directory for the instance folder. You can find it at -``venv/var/flaskr-instance`` instead. +``.venv/var/flaskr-instance`` instead. Configure the Secret Key @@ -77,7 +68,7 @@ Create the ``config.py`` file in the instance folder, which the factory will read from if it exists. Copy the generated value into it. .. code-block:: python - :caption: ``venv/var/flaskr-instance/config.py`` + :caption: ``.venv/var/flaskr-instance/config.py`` SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf' diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index c8e2c5f4..381477f9 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -56,10 +56,7 @@ directory should be treated as a package. app.config.from_mapping(test_config) # ensure the instance folder exists - try: - os.makedirs(app.instance_path) - except OSError: - pass + os.makedirs(app.instance_path, exist_ok=True) # a simple page that says hello @app.route('/hello') @@ -137,7 +134,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug You'll see output similar to this: diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index 7380b302..db83e106 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -1,11 +1,10 @@ Make the Project Installable ============================ -Making your project installable means that you can build a -*distribution* file and install that in another environment, just like -you installed Flask in your project's environment. This makes deploying -your project the same as installing any other library, so you're using -all the standard Python tools to manage everything. +Making your project installable means that you can build a *wheel* file and install that +in another environment, just like you installed Flask in your project's environment. +This makes deploying your project the same as installing any other library, so you're +using all the standard Python tools to manage everything. Installing also comes with other benefits that might not be obvious from the tutorial or as a new Python user, including: @@ -28,51 +27,27 @@ the tutorial or as a new Python user, including: Describe the Project -------------------- -The ``setup.py`` file describes your project and the files that belong -to it. +The ``pyproject.toml`` file describes your project and how to build it. -.. code-block:: python - :caption: ``setup.py`` +.. code-block:: toml + :caption: ``pyproject.toml`` - from setuptools import find_packages, setup + [project] + name = "flaskr" + version = "1.0.0" + description = "The basic blog app built in the Flask tutorial." + dependencies = [ + "flask", + ] - setup( - name='flaskr', - version='1.0.0', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=[ - 'flask', - ], - ) + [build-system] + requires = ["flit_core<4"] + build-backend = "flit_core.buildapi" - -``packages`` tells Python what package directories (and the Python files -they contain) to include. ``find_packages()`` finds these directories -automatically so you don't have to type them out. To include other -files, such as the static and templates directories, -``include_package_data`` is set. Python needs another file named -``MANIFEST.in`` to tell what this other data is. - -.. code-block:: none - :caption: ``MANIFEST.in`` - - include flaskr/schema.sql - graft flaskr/static - graft flaskr/templates - global-exclude *.pyc - -This tells Python to copy everything in the ``static`` and ``templates`` -directories, and the ``schema.sql`` file, but to exclude all bytecode -files. - -See the official `Packaging tutorial `_ and -`detailed guide `_ for more explanation of the files -and options used. +See the official `Packaging tutorial `_ for more +explanation of the files and options used. .. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ -.. _packaging guide: https://packaging.python.org/guides/distributing-packages-using-setuptools/ Install the Project @@ -84,10 +59,10 @@ Use ``pip`` to install your project in the virtual environment. $ pip install -e . -This tells pip to find ``setup.py`` in the current directory and install -it in *editable* or *development* mode. Editable mode means that as you -make changes to your local code, you'll only need to re-install if you -change the metadata about the project, such as its dependencies. +This tells pip to find ``pyproject.toml`` in the current directory and install the +project in *editable* or *development* mode. Editable mode means that as you make +changes to your local code, you'll only need to re-install if you change the metadata +about the project, such as its dependencies. You can observe that the project is now installed with ``pip list``. @@ -104,9 +79,7 @@ You can observe that the project is now installed with ``pip list``. Jinja2 2.10 MarkupSafe 1.0 pip 9.0.3 - setuptools 39.0.1 Werkzeug 0.14.1 - wheel 0.30.0 Nothing changes from how you've been running your project so far. ``--app`` is still set to ``flaskr`` and ``flask run`` still runs diff --git a/docs/tutorial/layout.rst b/docs/tutorial/layout.rst index b6a09f03..9f510927 100644 --- a/docs/tutorial/layout.rst +++ b/docs/tutorial/layout.rst @@ -41,7 +41,7 @@ The project directory will contain: * ``flaskr/``, a Python package containing your application code and files. * ``tests/``, a directory containing test modules. -* ``venv/``, a Python virtual environment where Flask and other +* ``.venv/``, a Python virtual environment where Flask and other dependencies are installed. * Installation files telling Python how to install your project. * Version control config, such as `git`_. You should make a habit of @@ -80,9 +80,8 @@ By the end, your project layout will look like this: │ ├── test_db.py │ ├── test_auth.py │ └── test_blog.py - ├── venv/ - ├── setup.py - └── MANIFEST.in + ├── .venv/ + └── pyproject.toml If you're using version control, the following files that are generated while running your project should be ignored. There may be other files @@ -92,7 +91,7 @@ write. For example, with git: .. code-block:: none :caption: ``.gitignore`` - venv/ + .venv/ *.pyc __pycache__/ @@ -103,8 +102,4 @@ 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 1a5535cc..ca9d4b32 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 cb60790c..8958e773 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 @@ -490,20 +490,18 @@ no longer exist in the database. Running the Tests ----------------- -Some extra configuration, which is not required but makes running -tests with coverage less verbose, can be added to the project's -``setup.cfg`` file. +Some extra configuration, which is not required but makes running tests with coverage +less verbose, can be added to the project's ``pyproject.toml`` file. -.. code-block:: none - :caption: ``setup.cfg`` +.. code-block:: toml + :caption: ``pyproject.toml`` - [tool:pytest] - testpaths = tests + [tool.pytest.ini_options] + testpaths = ["tests"] - [coverage:run] - branch = True - source = - flaskr + [tool.coverage.run] + branch = true + source = ["flaskr"] To run the tests, use the ``pytest`` command. It will find and run all the test functions you've written. @@ -514,7 +512,7 @@ the test functions you've written. ========================= test session starts ========================== platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0 - rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg + rootdir: /home/user/Projects/flask-tutorial collected 23 items tests/test_auth.py ........ [ 34%] diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 7092dbc2..6626628a 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/views.rst b/docs/views.rst index b7c5ba20..f2210270 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -116,7 +116,10 @@ function. item = self.model.query.get_or_404(id) return render_template(self.template, item=item) - app.add_url_rule("/users/", view_func=DetailView.as_view("user_detail")) + app.add_url_rule( + "/users/", + view_func=DetailView.as_view("user_detail", User) + ) View Lifetime and ``self`` @@ -246,14 +249,14 @@ provide get (list) and post (create) methods. init_every_request = False def __init__(self, model): - self.model + self.model = model self.validator = generate_validator(model) def _get_item(self, id): return self.model.query.get_or_404(id) def get(self, id): - user = self._get_item(id) + item = self._get_item(id) return jsonify(item.to_json()) def patch(self, id): @@ -294,9 +297,11 @@ provide get (list) and post (create) methods. db.session.commit() return jsonify(item.to_json()) - def register_api(app, model, url): - app.add_url_rule(f"/{name}/", view_func=ItemAPI(f"{name}-item", model)) - app.add_url_rule(f"/{name}/", view_func=GroupAPI(f"{name}-group", model)) + def register_api(app, model, name): + item = ItemAPI.as_view(f"{name}-item", model) + group = GroupAPI.as_view(f"{name}-group", model) + app.add_url_rule(f"/{name}/", view_func=item) + app.add_url_rule(f"/{name}/", view_func=group) register_api(app, User, "users") register_api(app, Story, "stories") diff --git a/docs/security.rst b/docs/web-security.rst similarity index 74% rename from docs/security.rst rename to docs/web-security.rst index 777e5112..4118b5ec 100644 --- a/docs/security.rst +++ b/docs/web-security.rst @@ -1,9 +1,43 @@ Security Considerations ======================= -Web applications usually face all kinds of security problems and it's very -hard to get everything right. Flask tries to solve a few of these things -for you, but there are a couple more you have to take care of yourself. +Web applications face many types of potential security problems, and it can be +hard to get everything right, or even to know what "right" is in general. Flask +tries to solve a few of these things by default, but there are other parts you +may have to take care of yourself. Many of these solutions are tradeoffs, and +will depend on each application's specific needs and threat model. Many hosting +platforms may take care of certain types of problems without the need for the +Flask application to handle them. + +Resource Use +------------ + +A common category of attacks is "Denial of Service" (DoS or DDoS). This is a +very broad category, and different variants target different layers in a +deployed application. In general, something is done to increase how much +processing time or memory is used to handle each request, to the point where +there are not enough resources to handle legitimate requests. + +Flask provides a few configuration options to handle resource use. They can +also be set on individual requests to customize only that request. The +documentation for each goes into more detail. + +- :data:`MAX_CONTENT_LENGTH` or :attr:`.Request.max_content_length` controls + how much data will be read from a request. It is not set by default, + although it will still block truly unlimited streams unless the WSGI server + indicates support. +- :data:`MAX_FORM_MEMORY_SIZE` or :attr:`.Request.max_form_memory_size` + controls how large any non-file ``multipart/form-data`` field can be. It is + set to 500kB by default. +- :data:`MAX_FORM_PARTS` or :attr:`.Request.max_form_parts` controls how many + ``multipart/form-data`` fields can be parsed. It is set to 1000 by default. + Combined with the default `max_form_memory_size`, this means that a form + will occupy at most 500MB of memory. + +Regardless of these settings, you should also review what settings are available +from your operating system, container deployment (Docker etc), WSGI server, HTTP +server, and hosting platform. They typically have ways to set process resource +limits, timeouts, and other checks regardless of how Flask is configured. .. _security-xss: @@ -17,13 +51,13 @@ tags. For more information on that have a look at the Wikipedia article on `Cross-Site Scripting `_. -Flask configures Jinja2 to automatically escape all values unless +Flask configures Jinja to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused in templates, but there are still other places where you have to be careful: -- generating HTML without the help of Jinja2 -- calling :class:`~flask.Markup` on data submitted by users +- generating HTML without the help of Jinja +- 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. - sending out textfiles from uploaded files. Some browsers are using @@ -31,7 +65,7 @@ careful: trick a browser to execute HTML. Another thing that is very important are unquoted attributes. While -Jinja2 can protect you from XSS issues by escaping HTML, there is one +Jinja can protect you from XSS issues by escaping HTML, there is one thing it cannot protect you from: XSS by attribute injection. To counter this possible attack vector, be sure to always quote your attributes with either double or single quotes when using Jinja expressions in them: @@ -124,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/GoogleCloudPlatform/flask-talisman +.. _Flask-Talisman: https://github.com/wntrblm/flask-talisman HTTP Strict Transport Security (HSTS) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -235,17 +269,25 @@ values (or any values that need secure signatures). .. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute -HTTP Public Key Pinning (HPKP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Host Header Validation +---------------------- -This tells the browser to authenticate with the server using only the specific -certificate key to prevent MITM attacks. +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. -.. warning:: - Be careful when enabling this, as it is very difficult to undo if you set up - or upgrade your key incorrectly. +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. -- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning +When deploying your application, set :data:`TRUSTED_HOSTS` to restrict what +values the ``Host`` header may be. + +The ``Host`` header may be modified by proxies in between the client and your +application. See :doc:`deploying/proxy_fix` to tell your app which proxy values +to trust. Copy/Paste to Terminal diff --git a/examples/celery/README.md b/examples/celery/README.md new file mode 100644 index 00000000..038eb51e --- /dev/null +++ b/examples/celery/README.md @@ -0,0 +1,27 @@ +Background Tasks with Celery +============================ + +This example shows how to configure Celery with Flask, how to set up an API for +submitting tasks and polling results, and how to use that API with JavaScript. See +[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). + +From this directory, create a virtualenv and install the application into it. Then run a +Celery worker. + +```shell +$ python3 -m venv .venv +$ . ./.venv/bin/activate +$ pip install -r requirements.txt && pip install -e . +$ celery -A make_celery worker --loglevel INFO +``` + +In a separate terminal, activate the virtualenv and run the Flask development server. + +```shell +$ . ./.venv/bin/activate +$ flask -A task_app run --debug +``` + +Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling +requests in the browser dev tools and the Flask logs. You can see the tasks submitting +and completing in the Celery logs. diff --git a/examples/celery/make_celery.py b/examples/celery/make_celery.py new file mode 100644 index 00000000..f7d138e6 --- /dev/null +++ b/examples/celery/make_celery.py @@ -0,0 +1,4 @@ +from task_app import create_app + +flask_app = create_app() +celery_app = flask_app.extensions["celery"] diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml new file mode 100644 index 00000000..cca36d8c --- /dev/null +++ b/examples/celery/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "flask-example-celery" +version = "1.0.0" +description = "Example Flask application with Celery background tasks." +readme = "README.md" +classifiers = ["Private :: Do Not Upload"] +dependencies = ["flask", "celery[redis]"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "task_app" + +[tool.ruff] +src = ["src"] diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt new file mode 100644 index 00000000..29075ab5 --- /dev/null +++ b/examples/celery/requirements.txt @@ -0,0 +1,58 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --resolver=backtracking pyproject.toml +# +amqp==5.1.1 + # via kombu +async-timeout==4.0.2 + # via redis +billiard==3.6.4.0 + # via celery +blinker==1.6.2 + # via flask +celery[redis]==5.2.7 + # via flask-example-celery (pyproject.toml) +click==8.1.3 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # flask +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.2.0 + # via celery +flask==2.3.2 + # via flask-example-celery (pyproject.toml) +itsdangerous==2.1.2 + # via flask +jinja2==3.1.2 + # via flask +kombu==5.2.4 + # via celery +markupsafe==2.1.2 + # via + # jinja2 + # werkzeug +prompt-toolkit==3.0.38 + # via click-repl +pytz==2023.3 + # via celery +redis==4.5.4 + # via celery +six==1.16.0 + # via click-repl +vine==5.0.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.6 + # via prompt-toolkit +werkzeug==2.3.3 + # via flask diff --git a/examples/celery/src/task_app/__init__.py b/examples/celery/src/task_app/__init__.py new file mode 100644 index 00000000..dafff8aa --- /dev/null +++ b/examples/celery/src/task_app/__init__.py @@ -0,0 +1,39 @@ +from celery import Celery +from celery import Task +from flask import Flask +from flask import render_template + + +def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + + @app.route("/") + def index() -> str: + return render_template("index.html") + + from . import views + + app.register_blueprint(views.bp) + return app + + +def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: + with app.app_context(): + return self.run(*args, **kwargs) + + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app diff --git a/examples/celery/src/task_app/tasks.py b/examples/celery/src/task_app/tasks.py new file mode 100644 index 00000000..b6b3595d --- /dev/null +++ b/examples/celery/src/task_app/tasks.py @@ -0,0 +1,23 @@ +import time + +from celery import shared_task +from celery import Task + + +@shared_task(ignore_result=False) +def add(a: int, b: int) -> int: + return a + b + + +@shared_task() +def block() -> None: + time.sleep(5) + + +@shared_task(bind=True, ignore_result=False) +def process(self: Task, total: int) -> object: + for i in range(total): + self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) + time.sleep(1) + + return {"current": total, "total": total} diff --git a/examples/celery/src/task_app/templates/index.html b/examples/celery/src/task_app/templates/index.html new file mode 100644 index 00000000..4e1145cb --- /dev/null +++ b/examples/celery/src/task_app/templates/index.html @@ -0,0 +1,108 @@ + + + + + Celery Example + + +

Celery Example

+Execute background tasks with Celery. Submits tasks and shows results using JavaScript. + +
+

Add

+

Start a task to add two numbers, then poll for the result. +

+
+
+ +
+

Result:

+ +
+

Block

+

Start a task that takes 5 seconds. However, the response will return immediately. +

+ +
+

+ +
+

Process

+

Start a task that counts, waiting one second each time, showing progress. +

+
+ +
+

+ + + + diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py new file mode 100644 index 00000000..99cf92dc --- /dev/null +++ b/examples/celery/src/task_app/views.py @@ -0,0 +1,38 @@ +from celery.result import AsyncResult +from flask import Blueprint +from flask import request + +from . import tasks + +bp = Blueprint("tasks", __name__, url_prefix="/tasks") + + +@bp.get("/result/") +def result(id: str) -> dict[str, object]: + result = AsyncResult(id) + ready = result.ready() + return { + "ready": ready, + "successful": result.successful() if ready else None, + "value": result.get() if ready else result.result, + } + + +@bp.post("/add") +def add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = tasks.add.delay(a, b) + return {"result_id": result.id} + + +@bp.post("/block") +def block() -> dict[str, object]: + result = tasks.block.delay() + return {"result_id": result.id} + + +@bp.post("/process") +def process() -> dict[str, object]: + result = tasks.process.delay(total=request.form.get("total", type=int)) + return {"result_id": result.id} diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore index 85a35845..a306afbc 100644 --- a/examples/javascript/.gitignore +++ b/examples/javascript/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/javascript/LICENSE.rst b/examples/javascript/LICENSE.txt similarity index 100% rename from examples/javascript/LICENSE.rst rename to examples/javascript/LICENSE.txt diff --git a/examples/javascript/MANIFEST.in b/examples/javascript/MANIFEST.in deleted file mode 100644 index c730a34e..00000000 --- a/examples/javascript/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE.rst -graft js_example/templates -graft tests -global-exclude *.pyc diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst index 23c7ce43..f5f66912 100644 --- a/examples/javascript/README.rst +++ b/examples/javascript/README.rst @@ -15,7 +15,7 @@ page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and .. |jQuery.ajax| replace:: ``jQuery.ajax`` .. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/ -.. _Flask docs: https://flask.palletsprojects.com/patterns/jquery/ +.. _Flask docs: https://flask.palletsprojects.com/patterns/javascript/ Install @@ -23,8 +23,8 @@ Install .. code-block:: text - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate $ pip install -e . diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py index 068b2d98..0ec3ca21 100644 --- a/examples/javascript/js_example/__init__.py +++ b/examples/javascript/js_example/__init__.py @@ -2,4 +2,4 @@ from flask import Flask app = Flask(__name__) -from js_example import views # noqa: F401 +from js_example import views # noqa: E402, F401 diff --git a/examples/javascript/js_example/views.py b/examples/javascript/js_example/views.py index 0d4b6561..9f0d26c5 100644 --- a/examples/javascript/js_example/views.py +++ b/examples/javascript/js_example/views.py @@ -2,7 +2,7 @@ from flask import jsonify from flask import render_template from flask import request -from js_example import app +from . import app @app.route("/", defaults={"js": "fetch"}) diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml new file mode 100644 index 00000000..ea0efabd --- /dev/null +++ b/examples/javascript/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "js_example" +version = "1.1.0" +description = "Demonstrates making AJAX requests to Flask." +readme = "README.rst" +license = {file = "LICENSE.txt"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = ["Private :: Do Not Upload"] +dependencies = ["flask"] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/patterns/javascript/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "js_example" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["js_example", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/examples/javascript/setup.cfg b/examples/javascript/setup.cfg deleted file mode 100644 index f509ddfe..00000000 --- a/examples/javascript/setup.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[metadata] -name = js_example -version = 1.1.0 -url = https://flask.palletsprojects.com/patterns/jquery/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = Demonstrates making AJAX requests to Flask. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - blinker - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - js_example diff --git a/examples/javascript/setup.py b/examples/javascript/setup.py deleted file mode 100644 index 60684932..00000000 --- a/examples/javascript/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/examples/javascript/tests/test_js_example.py b/examples/javascript/tests/test_js_example.py index d155ad5c..856f5f77 100644 --- a/examples/javascript/tests/test_js_example.py +++ b/examples/javascript/tests/test_js_example.py @@ -5,7 +5,7 @@ from flask import template_rendered @pytest.mark.parametrize( ("path", "template_name"), ( - ("/", "xhr.html"), + ("/", "fetch.html"), ("/plain", "xhr.html"), ("/fetch", "fetch.html"), ("/jquery", "jquery.html"), diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore index 85a35845..a306afbc 100644 --- a/examples/tutorial/.gitignore +++ b/examples/tutorial/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/tutorial/LICENSE.rst b/examples/tutorial/LICENSE.txt similarity index 100% rename from examples/tutorial/LICENSE.rst rename to examples/tutorial/LICENSE.txt diff --git a/examples/tutorial/MANIFEST.in b/examples/tutorial/MANIFEST.in deleted file mode 100644 index 97d55d51..00000000 --- a/examples/tutorial/MANIFEST.in +++ /dev/null @@ -1,6 +0,0 @@ -include LICENSE.rst -include flaskr/schema.sql -graft flaskr/static -graft flaskr/templates -graft tests -global-exclude *.pyc diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca2..653c2167 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -23,13 +23,13 @@ default Git version is the main branch. :: Create a virtualenv and activate it:: - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate Or on Windows cmd:: - $ py -3 -m venv venv - $ venv\Scripts\activate.bat + $ py -3 -m venv .venv + $ .venv\Scripts\activate.bat Install Flaskr:: @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py index bb9cce5a..ab96e719 100644 --- a/examples/tutorial/flaskr/__init__.py +++ b/examples/tutorial/flaskr/__init__.py @@ -21,22 +21,20 @@ def create_app(test_config=None): app.config.update(test_config) # ensure the instance folder exists - try: - os.makedirs(app.instance_path) - except OSError: - pass + os.makedirs(app.instance_path, exist_ok=True) @app.route("/hello") def hello(): return "Hello, World!" # register the database commands - from flaskr import db + from . import db db.init_app(app) # apply the blueprints to the app - from flaskr import auth, blog + from . import auth + from . import blog app.register_blueprint(auth.bp) app.register_blueprint(blog.bp) diff --git a/examples/tutorial/flaskr/auth.py b/examples/tutorial/flaskr/auth.py index b423e6ae..34c03a20 100644 --- a/examples/tutorial/flaskr/auth.py +++ b/examples/tutorial/flaskr/auth.py @@ -11,7 +11,7 @@ from flask import url_for from werkzeug.security import check_password_hash from werkzeug.security import generate_password_hash -from flaskr.db import get_db +from .db import get_db bp = Blueprint("auth", __name__, url_prefix="/auth") diff --git a/examples/tutorial/flaskr/blog.py b/examples/tutorial/flaskr/blog.py index 3704626b..be0d92c4 100644 --- a/examples/tutorial/flaskr/blog.py +++ b/examples/tutorial/flaskr/blog.py @@ -7,8 +7,8 @@ from flask import request from flask import url_for from werkzeug.exceptions import abort -from flaskr.auth import login_required -from flaskr.db import get_db +from .auth import login_required +from .db import get_db bp = Blueprint("blog", __name__) diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py index acaa4ae3..dec22fde 100644 --- a/examples/tutorial/flaskr/db.py +++ b/examples/tutorial/flaskr/db.py @@ -1,4 +1,5 @@ import sqlite3 +from datetime import datetime import click from flask import current_app @@ -44,6 +45,9 @@ def init_db_command(): click.echo("Initialized the database.") +sqlite3.register_converter("timestamp", lambda v: datetime.fromisoformat(v.decode())) + + def init_app(app): """Register database functions with the Flask app. This is called by the application factory. diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml new file mode 100644 index 00000000..ca31e53d --- /dev/null +++ b/examples/tutorial/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {file = "LICENSE.txt"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = ["Private :: Do Not Upload"] +dependencies = [ + "flask", +] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/tutorial/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flaskr" + +[tool.flit.sdist] +include = [ + "tests/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["flaskr", "tests"] + +[tool.ruff] +src = ["src"] diff --git a/examples/tutorial/setup.cfg b/examples/tutorial/setup.cfg deleted file mode 100644 index d001093b..00000000 --- a/examples/tutorial/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[metadata] -name = flaskr -version = 1.0.0 -url = https://flask.palletsprojects.com/tutorial/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = The basic blog app built in the Flask tutorial. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - flaskr diff --git a/examples/tutorial/setup.py b/examples/tutorial/setup.py deleted file mode 100644 index 60684932..00000000 --- a/examples/tutorial/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..1bc3e0e1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,277 @@ +[project] +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"] +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Typing :: Typed", +] +requires-python = ">=3.10" +dependencies = [ + "blinker>=1.9.0", + "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", +] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://flask.palletsprojects.com/" +Changes = "https://flask.palletsprojects.com/page/changes/" +Source = "https://github.com/pallets/flask/" +Chat = "https://discord.gg/pallets" + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["flit_core>=3.11,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "flask" + +[tool.flit.sdist] +include = [ + "docs/", + "examples/", + "tests/", + "CHANGES.rst", + "uv.lock" +] +exclude = [ + "docs/_build/", +] + +[tool.uv] +default-groups = ["dev", "pre-commit", "tests", "typing"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +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"] +show_error_codes = true +pretty = true +strict = true + +[[tool.mypy.overrides]] +module = [ + "asgiref.*", + "dotenv.*", + "cryptography.*", + "importlib_metadata", +] +ignore_missing_imports = true + +[tool.pyright] +pythonVersion = "3.10" +include = ["src", "tests/type_check"] +typeCheckingMode = "basic" + +[tool.ruff] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] + +[tool.ruff.lint.isort] +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.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/dev.in b/requirements/dev.in deleted file mode 100644 index 99f5942f..00000000 --- a/requirements/dev.in +++ /dev/null @@ -1,6 +0,0 @@ --r docs.in --r tests.in --r typing.in -pip-compile-multi -pre-commit -tox diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 03d224c1..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,62 +0,0 @@ -# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# --r docs.txt --r tests.txt --r typing.txt -build==0.8.0 - # via pip-tools -cfgv==3.3.1 - # via pre-commit -click==8.1.3 - # via - # pip-compile-multi - # pip-tools -distlib==0.3.5 - # via virtualenv -filelock==3.7.1 - # via - # tox - # virtualenv -greenlet==1.1.2 ; python_version < "3.11" - # via -r requirements/tests.in -identify==2.5.3 - # via pre-commit -nodeenv==1.7.0 - # via pre-commit -pep517==0.13.0 - # via build -pip-compile-multi==2.4.6 - # via -r requirements/dev.in -pip-tools==6.8.0 - # via pip-compile-multi -platformdirs==2.5.2 - # via virtualenv -pre-commit==2.20.0 - # via -r requirements/dev.in -pyyaml==6.0 - # via pre-commit -six==1.16.0 - # via tox -toml==0.10.2 - # via - # pre-commit - # tox -toposort==1.7 - # via pip-compile-multi -tox==3.25.1 - # via -r requirements/dev.in -virtualenv==20.16.2 - # via - # pre-commit - # tox -wheel==0.37.1 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements/docs.in b/requirements/docs.in deleted file mode 100644 index 3a389e2b..00000000 --- a/requirements/docs.in +++ /dev/null @@ -1,7 +0,0 @@ -Pallets-Sphinx-Themes -# sphinx 5 introduced error in references from werkzeug in docstrings -Sphinx < 5 -sphinx-issues -sphinxcontrib-log-cabinet -# sphinx-tabs 3.4 requires docutils 0.18, sphinx < 5 requires < 0.18 -sphinx-tabs < 3.4 diff --git a/requirements/docs.txt b/requirements/docs.txt deleted file mode 100644 index 2ac5f547..00000000 --- a/requirements/docs.txt +++ /dev/null @@ -1,72 +0,0 @@ -# SHA1:323f1c1134d78952ea63131c187303def63b56bd -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -alabaster==0.7.12 - # via sphinx -babel==2.10.3 - # via sphinx -certifi==2022.6.15 - # via requests -charset-normalizer==2.1.0 - # via requests -docutils==0.17.1 - # via - # sphinx - # sphinx-tabs -idna==3.3 - # via requests -imagesize==1.4.1 - # via sphinx -jinja2==3.1.2 - # via sphinx -markupsafe==2.1.1 - # via jinja2 -packaging==21.3 - # via - # pallets-sphinx-themes - # sphinx -pallets-sphinx-themes==2.0.2 - # via -r requirements/docs.in -pygments==2.12.0 - # via - # sphinx - # sphinx-tabs -pyparsing==3.0.9 - # via packaging -pytz==2022.1 - # via babel -requests==2.28.1 - # via sphinx -snowballstemmer==2.2.0 - # via sphinx -sphinx==4.5.0 - # via - # -r requirements/docs.in - # pallets-sphinx-themes - # sphinx-issues - # sphinx-tabs - # sphinxcontrib-log-cabinet -sphinx-issues==3.0.1 - # via -r requirements/docs.in -sphinx-tabs==3.3.1 - # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.2 - # via sphinx -sphinxcontrib-devhelp==1.0.2 - # via sphinx -sphinxcontrib-htmlhelp==2.0.0 - # via sphinx -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-log-cabinet==1.0.1 - # via -r requirements/docs.in -sphinxcontrib-qthelp==1.0.3 - # via sphinx -sphinxcontrib-serializinghtml==1.1.5 - # via sphinx -urllib3==1.26.11 - # via requests diff --git a/requirements/tests-pallets-dev.in b/requirements/tests-pallets-dev.in deleted file mode 100644 index dddbe48a..00000000 --- a/requirements/tests-pallets-dev.in +++ /dev/null @@ -1,5 +0,0 @@ -https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz -https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz -https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz -https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz -https://github.com/pallets/click/archive/refs/heads/main.tar.gz diff --git a/requirements/tests-pallets-dev.txt b/requirements/tests-pallets-dev.txt deleted file mode 100644 index a74f556b..00000000 --- a/requirements/tests-pallets-dev.txt +++ /dev/null @@ -1,20 +0,0 @@ -# SHA1:692b640e7f835e536628f76de0afff1296524122 -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -click @ https://github.com/pallets/click/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -itsdangerous @ https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -jinja2 @ https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -markupsafe @ https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz - # via - # -r requirements/tests-pallets-dev.in - # jinja2 - # werkzeug -werkzeug @ https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in diff --git a/requirements/tests-pallets-min.in b/requirements/tests-pallets-min.in deleted file mode 100644 index 6c8a55d9..00000000 --- a/requirements/tests-pallets-min.in +++ /dev/null @@ -1,5 +0,0 @@ -Werkzeug==2.0.0 -Jinja2==3.0.0 -MarkupSafe==2.0.0 -itsdangerous==2.0.0 -click==8.0.0 diff --git a/requirements/tests-pallets-min.txt b/requirements/tests-pallets-min.txt deleted file mode 100644 index 64f0e1ce..00000000 --- a/requirements/tests-pallets-min.txt +++ /dev/null @@ -1,19 +0,0 @@ -# SHA1:4de7d9e6254a945fd97ec10880dd23b6cd43b70d -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -click==8.0.0 - # via -r requirements/tests-pallets-min.in -itsdangerous==2.0.0 - # via -r requirements/tests-pallets-min.in -jinja2==3.0.0 - # via -r requirements/tests-pallets-min.in -markupsafe==2.0.0 - # via - # -r requirements/tests-pallets-min.in - # jinja2 -werkzeug==2.0.0 - # via -r requirements/tests-pallets-min.in diff --git a/requirements/tests.in b/requirements/tests.in deleted file mode 100644 index 41936997..00000000 --- a/requirements/tests.in +++ /dev/null @@ -1,5 +0,0 @@ -pytest -asgiref -blinker -greenlet ; python_version < "3.11" -python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt deleted file mode 100644 index d9b7513c..00000000 --- a/requirements/tests.txt +++ /dev/null @@ -1,31 +0,0 @@ -# SHA1:69cf1e101a60350e9933c6f1f3b129bd9ed1ea7c -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -asgiref==3.5.2 - # via -r requirements/tests.in -attrs==22.1.0 - # via pytest -blinker==1.5 - # via -r requirements/tests.in -greenlet==1.1.2 ; python_version < "3.11" - # via -r requirements/tests.in -iniconfig==1.1.1 - # via pytest -packaging==21.3 - # via pytest -pluggy==1.0.0 - # via pytest -py==1.11.0 - # via pytest -pyparsing==3.0.9 - # via packaging -pytest==7.1.2 - # via -r requirements/tests.in -python-dotenv==0.20.0 - # via -r requirements/tests.in -tomli==2.0.1 - # via pytest diff --git a/requirements/typing.in b/requirements/typing.in deleted file mode 100644 index 2c589ea0..00000000 --- a/requirements/typing.in +++ /dev/null @@ -1,5 +0,0 @@ -mypy -types-contextvars -types-dataclasses -types-setuptools -cryptography diff --git a/requirements/typing.txt b/requirements/typing.txt deleted file mode 100644 index a842e1cd..00000000 --- a/requirements/typing.txt +++ /dev/null @@ -1,27 +0,0 @@ -# SHA1:7cc3f64d4e78db89d81680ac81503d5ac35d31a9 -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -cffi==1.15.1 - # via cryptography -cryptography==37.0.4 - # via -r requirements/typing.in -mypy==0.971 - # via -r requirements/typing.in -mypy-extensions==0.4.3 - # via mypy -pycparser==2.21 - # via cffi -tomli==2.0.1 - # via mypy -types-contextvars==2.4.7 - # via -r requirements/typing.in -types-dataclasses==0.6.6 - # via -r requirements/typing.in -types-setuptools==63.2.3 - # via -r requirements/typing.in -typing-extensions==4.3.0 - # via mypy diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e858d13a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,121 +0,0 @@ -[metadata] -name = Flask -version = attr: flask.__version__ -url = https://palletsprojects.com/p/flask -project_urls = - Donate = https://palletsprojects.com/donate - Documentation = https://flask.palletsprojects.com/ - Changes = https://flask.palletsprojects.com/changes/ - Source Code = https://github.com/pallets/flask/ - Issue Tracker = https://github.com/pallets/flask/issues/ - Twitter = https://twitter.com/PalletsTeam - Chat = https://discord.gg/pallets -license = BSD-3-Clause -author = Armin Ronacher -author_email = armin.ronacher@active-4.com -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = A simple framework for building complex web applications. -long_description = file: README.rst -long_description_content_type = text/x-rst -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 - Topic :: Internet :: WWW/HTTP :: WSGI - Topic :: Internet :: WWW/HTTP :: WSGI :: Application - Topic :: Software Development :: Libraries :: Application Frameworks - -[options] -packages = find: -package_dir = = src -include_package_data = True -python_requires = >= 3.7 -# Dependencies are in setup.py for GitHub's dependency graph. - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - flask = flask.cli:main - -[tool:pytest] -testpaths = tests -filterwarnings = - error - -[coverage:run] -branch = True -source = - flask - tests - -[coverage:paths] -source = - src - */site-packages - -[flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = - # slice notation whitespace, invalid - E203 - # import at top, too many circular import fixes - E402 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # bin op line break, invalid - W503 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/flask/__init__.py: F401 - -[mypy] -files = src/flask, tests/typing -python_version = 3.7 -show_error_codes = True -allow_redefinition = True -disallow_subclassing_any = True -# disallow_untyped_calls = True -# disallow_untyped_defs = True -# disallow_incomplete_defs = True -no_implicit_optional = True -local_partial_types = True -# no_implicit_reexport = True -strict_equality = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unused_ignores = True -# warn_return_any = True -# warn_unreachable = True - -[mypy-asgiref.*] -ignore_missing_imports = True - -[mypy-blinker.*] -ignore_missing_imports = True - -[mypy-dotenv.*] -ignore_missing_imports = True - -[mypy-cryptography.*] -ignore_missing_imports = True - -[mypy-importlib_metadata] -ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 7c53630d..00000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -from setuptools import setup - -# Metadata goes in setup.cfg. These are here for GitHub's dependency graph. -setup( - name="Flask", - install_requires=[ - "Werkzeug >= 2.2.0", - "Jinja2 >= 3.0", - "itsdangerous >= 2.0", - "click >= 8.0", - "importlib-metadata >= 3.6.0; python_version < '3.10'", - ], - extras_require={ - "async": ["asgiref >= 3.2"], - "dotenv": ["python-dotenv"], - }, -) diff --git a/src/flask/__init__.py b/src/flask/__init__.py index 6acd1e0b..30dce6fd 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,10 +1,5 @@ -from markupsafe import escape -from markupsafe import Markup - from . import json as json from .app import Flask as Flask -from .app import Request as Request -from .app import Response as Response from .blueprints import Blueprint as Blueprint from .config import Config as Config from .ctx import after_this_request as after_this_request @@ -35,37 +30,10 @@ from .signals import message_flashed as message_flashed from .signals import request_finished as request_finished from .signals import request_started as request_started from .signals import request_tearing_down as request_tearing_down -from .signals import signals_available as signals_available from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string - -__version__ = "2.2.1" - - -def __getattr__(name): - if name == "_app_ctx_stack": - import warnings - from .globals import __app_ctx_stack - - warnings.warn( - "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", - DeprecationWarning, - stacklevel=2, - ) - return __app_ctx_stack - - if name == "_request_ctx_stack": - import warnings - from .globals import __request_ctx_stack - - warnings.warn( - "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", - DeprecationWarning, - stacklevel=2, - ) - return __request_ctx_stack - - raise AttributeError(name) +from .wrappers import Request as Request +from .wrappers import Response as Response diff --git a/src/flask/app.py b/src/flask/app.py index 965865f2..652b9bbf 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1,62 +1,47 @@ -import functools +from __future__ import annotations + +import collections.abc as cabc import inspect -import json -import logging import os import sys import typing as t import weakref -from collections.abc import Iterator as _abc_Iterator from datetime import timedelta +from functools import update_wrapper +from inspect import iscoroutinefunction from itertools import chain -from threading import Lock from types import TracebackType +from urllib.parse import quote as _url_quote import click from werkzeug.datastructures import Headers from werkzeug.datastructures import ImmutableDict -from werkzeug.exceptions import Aborter -from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import HTTPException from werkzeug.exceptions import InternalServerError from werkzeug.routing import BuildError -from werkzeug.routing import Map from werkzeug.routing import MapAdapter from werkzeug.routing import RequestRedirect from werkzeug.routing import RoutingException from werkzeug.routing import Rule from werkzeug.serving import is_running_from_reloader -from werkzeug.urls import url_quote -from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse +from werkzeug.wsgi import get_host from . import cli from . import typing as ft -from .config import Config -from .config import ConfigAttribute -from .ctx import _AppCtxGlobals from .ctx import AppContext -from .ctx import RequestContext from .globals import _cv_app -from .globals import _cv_request +from .globals import app_ctx from .globals import g from .globals import request -from .globals import request_ctx from .globals import session -from .helpers import _split_blueprint_path +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 locked_cached_property -from .json.provider import DefaultJSONProvider -from .json.provider import JSONProvider -from .logging import create_logger -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import find_package -from .scaffold import Scaffold -from .scaffold import setupmethod +from .helpers import send_from_directory +from .sansio.app import App from .sessions import SecureCookieSessionInterface from .sessions import SessionInterface from .signals import appcontext_tearing_down @@ -64,20 +49,18 @@ from .signals import got_request_exception from .signals import request_finished from .signals import request_started from .signals import request_tearing_down -from .templating import DispatchingJinjaLoader from .templating import Environment from .wrappers import Request from .wrappers import Response if t.TYPE_CHECKING: # pragma: no cover - import typing_extensions as te - from .blueprints import Blueprint + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + from .testing import FlaskClient from .testing import FlaskCliRunner + from .typing import HeadersValue -T_before_first_request = t.TypeVar( - "T_before_first_request", bound=ft.BeforeFirstRequestCallable -) T_shell_context_processor = t.TypeVar( "T_shell_context_processor", bound=ft.ShellContextProcessorCallable ) @@ -86,28 +69,44 @@ T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallab T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) -if sys.version_info >= (3, 8): - iscoroutinefunction = inspect.iscoroutinefunction -else: - def iscoroutinefunction(func: t.Any) -> bool: - while inspect.ismethod(func): - func = func.__func__ - - while isinstance(func, functools.partial): - func = func.func - - return inspect.iscoroutinefunction(func) - - -def _make_timedelta(value: t.Union[timedelta, int, None]) -> t.Optional[timedelta]: +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: if value is None or isinstance(value, timedelta): return value return timedelta(seconds=value) -class Flask(Scaffold): +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 application. Once it is created it will act as a central registry for @@ -204,296 +203,16 @@ class Flask(Scaffold): automatically, such as for namespace packages. """ - #: The class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class = Request - - #: The class that is used for response objects. See - #: :class:`~flask.Response` for more information. - response_class = Response - - #: The class of the object assigned to :attr:`aborter`, created by - #: :meth:`create_aborter`. That object is called by - #: :func:`flask.abort` to raise HTTP errors, and can be - #: called directly as well. - #: - #: Defaults to :class:`werkzeug.exceptions.Aborter`. - #: - #: .. versionadded:: 2.2 - aborter_class = Aborter - - #: The class that is used for the Jinja environment. - #: - #: .. versionadded:: 0.11 - jinja_environment = Environment - - #: The class that is used for the :data:`~flask.g` instance. - #: - #: Example use cases for a custom class: - #: - #: 1. Store arbitrary attributes on flask.g. - #: 2. Add a property for lazy per-request database connectors. - #: 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 - app_ctx_globals_class = _AppCtxGlobals - - #: The class that is used for the ``config`` attribute of this app. - #: Defaults to :class:`~flask.Config`. - #: - #: Example use cases for a custom class: - #: - #: 1. Default values for certain config options. - #: 2. Access to config values through attributes in addition to keys. - #: - #: .. versionadded:: 0.11 - config_class = Config - - #: The testing flag. Set this to ``True`` to enable the test mode of - #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate test helpers that have an - #: additional runtime cost which should not be enabled by default. - #: - #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the - #: default it's implicitly enabled. - #: - #: This attribute can also be configured from the config with the - #: ``TESTING`` configuration key. Defaults to ``False``. - testing = ConfigAttribute("TESTING") - - #: If a secret key is set, cryptographic components can use this to - #: sign cookies and other things. Set this to a complex random value - #: when you want to use the secure cookie for instance. - #: - #: This attribute can also be configured from the config with the - #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. - secret_key = ConfigAttribute("SECRET_KEY") - - @property - def session_cookie_name(self) -> str: - """The name of the cookie set by the session interface. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.config["SESSION_COOKIE_NAME"]`` - instead. - """ - import warnings - - warnings.warn( - "'session_cookie_name' is deprecated and will be removed in Flask 2.3. Use" - " 'SESSION_COOKIE_NAME' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.config["SESSION_COOKIE_NAME"] - - @session_cookie_name.setter - def session_cookie_name(self, value: str) -> None: - import warnings - - warnings.warn( - "'session_cookie_name' is deprecated and will be removed in Flask 2.3. Use" - " 'SESSION_COOKIE_NAME' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["SESSION_COOKIE_NAME"] = value - - #: A :class:`~datetime.timedelta` which is used to set the expiration - #: date of a permanent session. The default is 31 days which makes a - #: permanent session survive for roughly one month. - #: - #: This attribute can also be configured from the config with the - #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to - #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute( - "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta - ) - - @property - def send_file_max_age_default(self) -> t.Optional[timedelta]: - """The default value for ``max_age`` for :func:`~flask.send_file`. The default - is ``None``, which tells the browser to use conditional requests instead of a - timed cache. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use - ``app.config["SEND_FILE_MAX_AGE_DEFAULT"]`` instead. - - .. versionchanged:: 2.0 - Defaults to ``None`` instead of 12 hours. - """ - import warnings - - warnings.warn( - "'send_file_max_age_default' is deprecated and will be removed in Flask" - " 2.3. Use 'SEND_FILE_MAX_AGE_DEFAULT' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - return _make_timedelta(self.config["SEND_FILE_MAX_AGE_DEFAULT"]) - - @send_file_max_age_default.setter - def send_file_max_age_default(self, value: t.Union[int, timedelta, None]) -> None: - import warnings - - warnings.warn( - "'send_file_max_age_default' is deprecated and will be removed in Flask" - " 2.3. Use 'SEND_FILE_MAX_AGE_DEFAULT' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["SEND_FILE_MAX_AGE_DEFAULT"] = _make_timedelta(value) - - @property - def use_x_sendfile(self) -> bool: - """Enable this to use the ``X-Sendfile`` feature, assuming the server supports - it, from :func:`~flask.send_file`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.config["USE_X_SENDFILE"]`` instead. - """ - import warnings - - warnings.warn( - "'use_x_sendfile' is deprecated and will be removed in Flask 2.3. Use" - " 'USE_X_SENDFILE' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.config["USE_X_SENDFILE"] - - @use_x_sendfile.setter - def use_x_sendfile(self, value: bool) -> None: - import warnings - - warnings.warn( - "'use_x_sendfile' is deprecated and will be removed in Flask 2.3. Use" - " 'USE_X_SENDFILE' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["USE_X_SENDFILE"] = value - - _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None - _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None - - @property # type: ignore[override] - def json_encoder(self) -> t.Type[json.JSONEncoder]: # type: ignore[override] - """The JSON encoder class to use. Defaults to - :class:`~flask.json.JSONEncoder`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'app.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - - if self._json_encoder is None: - from . import json - - return json.JSONEncoder - - return self._json_encoder - - @json_encoder.setter - def json_encoder(self, value: t.Type[json.JSONEncoder]) -> None: - import warnings - - warnings.warn( - "'app.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_encoder = value - - @property # type: ignore[override] - def json_decoder(self) -> t.Type[json.JSONDecoder]: # type: ignore[override] - """The JSON decoder class to use. Defaults to - :class:`~flask.json.JSONDecoder`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'app.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - - if self._json_decoder is None: - from . import json - - return json.JSONDecoder - - return self._json_decoder - - @json_decoder.setter - def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: - import warnings - - warnings.warn( - "'app.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_decoder = value - - json_provider_class: t.Type[JSONProvider] = DefaultJSONProvider - """A subclass of :class:`~flask.json.provider.JSONProvider`. An - instance is created and assigned to :attr:`app.json` when creating - the app. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses - Python's built-in :mod:`json` library. A different provider can use - a different JSON library. - - .. versionadded:: 2.2 - """ - - #: Options that are passed to the Jinja environment in - #: :meth:`create_jinja_environment`. Changing these options after - #: the environment is created (accessing :attr:`jinja_env`) will - #: have no effect. - #: - #: .. versionchanged:: 1.1.0 - #: This is a ``dict`` instead of an ``ImmutableDict`` to allow - #: easier configuration. - #: - jinja_options: dict = {} - - #: Default configuration parameters. default_config = ImmutableDict( { - "ENV": None, "DEBUG": None, "TESTING": False, "PROPAGATE_EXCEPTIONS": None, "SECRET_KEY": None, + "SECRET_KEY_FALLBACKS": None, "PERMANENT_SESSION_LIFETIME": timedelta(days=31), "USE_X_SENDFILE": False, + "TRUSTED_HOSTS": None, "SERVER_NAME": None, "APPLICATION_ROOT": "/", "SESSION_COOKIE_NAME": "session", @@ -501,48 +220,30 @@ class Flask(Scaffold): "SESSION_COOKIE_PATH": None, "SESSION_COOKIE_HTTPONLY": True, "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_PARTITIONED": False, "SESSION_COOKIE_SAMESITE": None, "SESSION_REFRESH_EACH_REQUEST": True, "MAX_CONTENT_LENGTH": None, + "MAX_FORM_MEMORY_SIZE": 500_000, + "MAX_FORM_PARTS": 1_000, "SEND_FILE_MAX_AGE_DEFAULT": None, "TRAP_BAD_REQUEST_ERRORS": None, "TRAP_HTTP_EXCEPTIONS": False, "EXPLAIN_TEMPLATE_LOADING": False, "PREFERRED_URL_SCHEME": "http", - "JSON_AS_ASCII": None, - "JSON_SORT_KEYS": None, - "JSONIFY_PRETTYPRINT_REGULAR": None, - "JSONIFY_MIMETYPE": None, "TEMPLATES_AUTO_RELOAD": None, "MAX_COOKIE_SIZE": 4093, + "PROVIDE_AUTOMATIC_OPTIONS": True, } ) - #: The rule object to use for URL rules created. This is used by - #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. - #: - #: .. versionadded:: 0.7 - url_rule_class = Rule + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class: type[Request] = Request - #: The map object to use for storing the URL rules and routing - #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. - #: - #: .. versionadded:: 1.1.0 - url_map_class = Map - - #: The :meth:`test_client` method creates an instance of this test - #: client class. Defaults to :class:`~flask.testing.FlaskClient`. - #: - #: .. versionadded:: 0.7 - test_client_class: t.Optional[t.Type["FlaskClient"]] = None - - #: The :class:`~click.testing.CliRunner` subclass, by default - #: :class:`~flask.testing.FlaskCliRunner` that is used by - #: :meth:`test_cli_runner`. Its ``__init__`` method should take a - #: Flask app object as the first argument. - #: - #: .. versionadded:: 1.0 - test_cli_runner_class: t.Optional[t.Type["FlaskCliRunner"]] = None + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class: type[Response] = Response #: the session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. @@ -550,149 +251,97 @@ class Flask(Scaffold): #: .. 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, - static_url_path: t.Optional[str] = None, - static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", - static_host: t.Optional[str] = None, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, host_matching: bool = False, subdomain_matching: bool = False, - template_folder: t.Optional[str] = "templates", - instance_path: t.Optional[str] = None, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, instance_relative_config: bool = False, - root_path: t.Optional[str] = None, + root_path: str | None = None, ): super().__init__( import_name=import_name, - static_folder=static_folder, static_url_path=static_url_path, + static_folder=static_folder, + static_host=static_host, + host_matching=host_matching, + subdomain_matching=subdomain_matching, template_folder=template_folder, + instance_path=instance_path, + instance_relative_config=instance_relative_config, root_path=root_path, ) - if instance_path is None: - instance_path = self.auto_find_instance_path() - elif not os.path.isabs(instance_path): - raise ValueError( - "If an instance path is provided it must be absolute." - " A relative path was given instead." - ) + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = cli.AppGroup() - #: Holds the path to the instance folder. - #: - #: .. versionadded:: 0.8 - self.instance_path = instance_path - - #: The configuration dictionary as :class:`Config`. This behaves - #: exactly like a regular dictionary but supports additional methods - #: to load a config from files. - self.config = self.make_config(instance_relative_config) - - #: An instance of :attr:`aborter_class` created by - #: :meth:`make_aborter`. This is called by :func:`flask.abort` - #: to raise HTTP errors, and can be called directly as well. - #: - #: .. versionadded:: 2.2 - #: Moved from ``flask.abort``, which calls this object. - self.aborter = self.make_aborter() - - self.json: JSONProvider = self.json_provider_class(self) - """Provides access to JSON methods. Functions in ``flask.json`` - will call methods on this provider when the application context - is active. Used for handling JSON requests and responses. - - An instance of :attr:`json_provider_class`. Can be customized by - changing that attribute on a subclass, or by assigning to this - attribute afterwards. - - The default, :class:`~flask.json.provider.DefaultJSONProvider`, - uses Python's built-in :mod:`json` library. A different provider - can use a different JSON library. - - .. versionadded:: 2.2 - """ - - #: A list of functions that are called by - #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a - #: :exc:`~werkzeug.routing.BuildError`. Each function is called - #: with ``error``, ``endpoint`` and ``values``. If a function - #: returns ``None`` or raises a ``BuildError``, it is skipped. - #: Otherwise, its return value is returned by ``url_for``. - #: - #: .. versionadded:: 0.9 - self.url_build_error_handlers: t.List[ - t.Callable[[Exception, str, t.Dict[str, t.Any]], str] - ] = [] - - #: A list of functions that will be called at the beginning of the - #: first request to this instance. To register a function, use the - #: :meth:`before_first_request` decorator. - #: - #: .. deprecated:: 2.2 - #: Will be removed in Flask 2.3. Run setup code when - #: creating the application instead. - #: - #: .. versionadded:: 0.8 - self.before_first_request_funcs: t.List[ft.BeforeFirstRequestCallable] = [] - - #: A list of functions that are called when the application context - #: is destroyed. Since the application context is also torn down - #: if the request ends this is the place to store code that disconnects - #: from databases. - #: - #: .. versionadded:: 0.9 - self.teardown_appcontext_funcs: t.List[ft.TeardownCallable] = [] - - #: A list of shell context processor functions that should be run - #: when a shell context is created. - #: - #: .. versionadded:: 0.11 - self.shell_context_processors: t.List[ft.ShellContextProcessorCallable] = [] - - #: Maps registered blueprint names to blueprint objects. The - #: dict retains the order the blueprints were registered in. - #: Blueprints can be registered multiple times, this dict does - #: not track how often they were attached. - #: - #: .. versionadded:: 0.7 - self.blueprints: t.Dict[str, "Blueprint"] = {} - - #: a place where extensions can store application specific state. For - #: example this is where an extension could store database engines and - #: similar things. - #: - #: The key must match the name of the extension module. For example in - #: case of a "Flask-Foo" extension in `flask_foo`, the key would be - #: ``'foo'``. - #: - #: .. versionadded:: 0.7 - self.extensions: dict = {} - - #: The :class:`~werkzeug.routing.Map` for this instance. You can use - #: this to change the routing converters after the class was created - #: but before any routes are connected. Example:: - #: - #: from werkzeug.routing import BaseConverter - #: - #: class ListConverter(BaseConverter): - #: def to_python(self, value): - #: return value.split(',') - #: def to_url(self, values): - #: return ','.join(super(ListConverter, self).to_url(value) - #: for value in values) - #: - #: app = Flask(__name__) - #: app.url_map.converters['list'] = ListConverter - self.url_map = self.url_map_class() - - self.url_map.host_matching = host_matching - self.subdomain_matching = subdomain_matching - - # tracks internally if the application already handled at least one - # request. - self._got_first_request = False - self._before_request_lock = Lock() + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name # Add a static route using the provided static_url_path, static_host, # and static_folder if there is a configured static_folder. @@ -700,9 +349,9 @@ class Flask(Scaffold): # 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) @@ -710,199 +359,112 @@ class Flask(Scaffold): f"{self.static_url_path}/", endpoint="static", host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore ) - # Set the name of the Click group in case someone wants to add - # the app's commands to another CLI tool. - self.cli.name = self.name + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. - def _check_setup_finished(self, f_name: str) -> None: - if self._got_first_request: - raise AssertionError( - f"The setup method '{f_name}' can no longer be called" - " on the application. It has already handled its first" - " request, any changes will not be applied" - " consistently.\n" - "Make sure all imports, decorators, functions, etc." - " needed to set up the application are done before" - " running it." - ) + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. - @locked_cached_property - 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 - Flask needs the name of the application. It can be set and overridden - to change the value. + Note this is a duplicate of the same method in the Flask + class. - .. versionadded:: 0.8 + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 """ - if self.import_name == "__main__": - fn = getattr(sys.modules["__main__"], "__file__", None) - if fn is None: - return "__main__" - return os.path.splitext(os.path.basename(fn))[0] - return self.import_name + value = self.config["SEND_FILE_MAX_AGE_DEFAULT"] - @property - def propagate_exceptions(self) -> bool: - """Returns the value of the ``PROPAGATE_EXCEPTIONS`` configuration - value in case it's set, otherwise a sensible default is returned. + if value is None: + return None - .. deprecated:: 2.2 - Will be removed in Flask 2.3. + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 - .. versionadded:: 0.7 """ - import warnings + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") - warnings.warn( - "'propagate_exceptions' is deprecated and will be removed in Flask 2.3.", - DeprecationWarning, - stacklevel=2, + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age ) - rv = self.config["PROPAGATE_EXCEPTIONS"] - if rv is not None: - return rv - return self.testing or self.debug - @locked_cached_property - def logger(self) -> logging.Logger: - """A standard Python :class:`~logging.Logger` for the app, with - the same name as :attr:`name`. + def open_resource( + self, resource: str, mode: str = "rb", encoding: str | None = None + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for reading. - In debug mode, the logger's :attr:`~logging.Logger.level` will - be set to :data:`~logging.DEBUG`. + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: - If there are no handlers configured, a default handler will be - added. See :doc:`/logging` for more information. + .. code-block:: python - .. versionchanged:: 1.1.0 - The logger takes the same name as :attr:`name` rather than - hard-coding ``"flask.app"``. + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) - .. versionchanged:: 1.0.0 - Behavior was simplified. The logger is always named - ``"flask.app"``. The level is only set during configuration, - it doesn't check ``app.debug`` each time. Only one format is - used, not different ones depending on ``app.debug``. No - handlers are removed, and a handler is only added if no - handlers are already configured. + :param resource: Path to the resource relative to :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is supported, + valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. - .. versionadded:: 0.3 + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. """ - return create_logger(self) + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") - @locked_cached_property - def jinja_env(self) -> Environment: - """The Jinja environment used to load templates. + path = os.path.join(self.root_path, resource) - The environment is created the first time this property is - accessed. Changing :attr:`jinja_options` after that will have no - effect. + if mode == "rb": + return open(path, mode) # pyright: ignore + + return open(path, mode, encoding=encoding) + + def open_instance_resource( + self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to the application's instance folder + :attr:`instance_path`. Unlike :meth:`open_resource`, files in the + instance folder can be opened for writing. + + :param resource: Path to the resource relative to :attr:`instance_path`. + :param mode: Open the file in this mode. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. """ - return self.create_jinja_environment() + path = os.path.join(self.instance_path, resource) - @property - def got_first_request(self) -> bool: - """This attribute is set to ``True`` if the application started - handling the first request. + if "b" in mode: + return open(path, mode) - .. versionadded:: 0.8 - """ - return self._got_first_request - - def make_config(self, instance_relative: bool = False) -> Config: - """Used to create the config attribute by the Flask constructor. - The `instance_relative` parameter is passed in from the constructor - of Flask (there named `instance_relative_config`) and indicates if - the config should be relative to the instance path or the root path - of the application. - - .. versionadded:: 0.8 - """ - root_path = self.root_path - if instance_relative: - root_path = self.instance_path - defaults = dict(self.default_config) - defaults["ENV"] = os.environ.get("FLASK_ENV") or "development" - defaults["DEBUG"] = get_debug_flag() - return self.config_class(root_path, defaults) - - def make_aborter(self) -> Aborter: - """Create the object to assign to :attr:`aborter`. That object - is called by :func:`flask.abort` to raise HTTP errors, and can - be called directly as well. - - By default, this creates an instance of :attr:`aborter_class`, - which defaults to :class:`werkzeug.exceptions.Aborter`. - - .. versionadded:: 2.2 - """ - return self.aborter_class() - - def auto_find_instance_path(self) -> str: - """Tries to locate the instance path if it was not provided to the - constructor of the application class. It will basically calculate - the path to a folder named ``instance`` next to your main file or - the package. - - .. versionadded:: 0.8 - """ - prefix, package_path = find_package(self.import_name) - if prefix is None: - return os.path.join(package_path, "instance") - return os.path.join(prefix, "var", f"{self.name}-instance") - - def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: - """Opens a resource from the application's instance folder - (:attr:`instance_path`). Otherwise works like - :meth:`open_resource`. Instance resources can also be opened for - writing. - - :param resource: the name of the resource. To access resources within - subfolders use forward slashes as separator. - :param mode: resource file opening mode, default is 'rb'. - """ - return open(os.path.join(self.instance_path, resource), mode) - - @property - def templates_auto_reload(self) -> bool: - """Reload templates when they are changed. Used by - :meth:`create_jinja_environment`. It is enabled by default in debug mode. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.config["TEMPLATES_AUTO_RELOAD"]`` - instead. - - .. versionadded:: 1.0 - This property was added but the underlying config and behavior - already existed. - """ - import warnings - - warnings.warn( - "'templates_auto_reload' is deprecated and will be removed in Flask 2.3." - " Use 'TEMPLATES_AUTO_RELOAD' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - rv = self.config["TEMPLATES_AUTO_RELOAD"] - return rv if rv is not None else self.debug - - @templates_auto_reload.setter - def templates_auto_reload(self, value: bool) -> None: - import warnings - - warnings.warn( - "'templates_auto_reload' is deprecated and will be removed in Flask 2.3." - " Use 'TEMPLATES_AUTO_RELOAD' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["TEMPLATES_AUTO_RELOAD"] = value + return open(path, mode, encoding=encoding) def create_jinja_environment(self) -> Environment: """Create the Jinja environment based on :attr:`jinja_options` @@ -944,30 +506,90 @@ class Flask(Scaffold): rv.policies["json.dumps_function"] = self.json.dumps return rv - def create_global_jinja_loader(self) -> DispatchingJinjaLoader: - """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. + def create_url_adapter(self, request: Request | None) -> MapAdapter | None: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. - The global loader dispatches between the loaders of the application - and the individual blueprints. + .. versionchanged:: 3.1 + If :data:`SERVER_NAME` is set, it does not restrict requests to + only that domain, for both ``subdomain_matching`` and + ``host_matching``. - .. versionadded:: 0.7 + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + + .. versionchanged:: 0.9 + This can be called outside a request when the URL adapter is created + for an application context. + + .. versionadded:: 0.6 """ - return DispatchingJinjaLoader(self) + if request is not None: + if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: + request.trusted_hosts = trusted_hosts - 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`. + # Check trusted_hosts here until bind_to_environ does. + request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore + subdomain = None + server_name = self.config["SERVER_NAME"] - .. versionadded:: 0.5 + if self.url_map.host_matching: + # Don't pass SERVER_NAME, otherwise it's used and the actual + # host is ignored, which breaks host matching. + server_name = None + elif not self.subdomain_matching: + # Werkzeug doesn't implement subdomain matching yet. Until then, + # disable it by forcing the current subdomain to the default, or + # the empty string. + subdomain = self.url_map.default_subdomain or "" + + return self.url_map.bind_to_environ( + request.environ, server_name=server_name, subdomain=subdomain + ) + + # Need at least SERVER_NAME to match/build outside a request. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def raise_routing_exception(self, request: Request) -> t.NoReturn: + """Intercept routing exceptions and possibly do something else. + + In debug mode, intercept a routing redirect and replace it with + an error if the body will be discarded. + + With modern Werkzeug this shouldn't occur, since it now uses a + 308 status which tells the browser to resend the method and + body. + + .. versionchanged:: 2.1 + Don't intercept 307 and 308 redirects. + + :meta private: + :internal: """ - if filename is None: - return True - return filename.endswith((".html", ".htm", ".xml", ".xhtml")) + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.routing_exception.code in {307, 308} + or request.method in {"GET", "HEAD", "OPTIONS"} + ): + raise request.routing_exception # type: ignore[misc] - def update_template_context(self, context: dict) -> None: + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def update_template_context( + self, ctx: AppContext, 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 @@ -978,11 +600,11 @@ class Flask(Scaffold): :param context: the context as a dictionary that is updated in place to add extra variables. """ - names: t.Iterable[t.Optional[str]] = (None,) + names: t.Iterable[str | None] = (None,) # A template may be rendered outside a request context. - if request: - names = chain(names, reversed(request.blueprints)) + if ctx.has_request: + names = chain(names, reversed(ctx.request.blueprints)) # The values passed to render_template take precedence. Keep a # copy to re-apply after all context functions. @@ -991,11 +613,11 @@ class Flask(Scaffold): for name in names: if name in self.template_context_processors: for func in self.template_context_processors[name]: - context.update(func()) + context.update(self.ensure_sync(func)()) context.update(orig_ctx) - def make_shell_context(self) -> dict: + def make_shell_context(self) -> dict[str, t.Any]: """Returns the shell context for an interactive shell for this application. This runs all the registered shell context processors. @@ -1007,65 +629,11 @@ class Flask(Scaffold): rv.update(processor()) return rv - @property - def env(self) -> str: - """What environment the app is running in. This maps to the :data:`ENV` config - key. - - **Do not enable development when deploying in production.** - - Default: ``'production'`` - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - """ - import warnings - - warnings.warn( - "'app.env' is deprecated and will be removed in Flask 2.3." - " Use 'app.debug' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.config["ENV"] - - @env.setter - def env(self, value: str) -> None: - import warnings - - warnings.warn( - "'app.env' is deprecated and will be removed in Flask 2.3." - " Use 'app.debug' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["ENV"] = value - - @property - def debug(self) -> bool: - """Whether debug mode is enabled. When using ``flask run`` to start the - development server, an interactive debugger will be shown for unhandled - exceptions, and the server will be reloaded when code changes. This maps to the - :data:`DEBUG` config key. It may not behave as expected if set late. - - **Do not enable debug mode when deploying in production.** - - Default: ``False`` - """ - return self.config["DEBUG"] - - @debug.setter - def debug(self, value: bool) -> None: - self.config["DEBUG"] = value - - if self.config["TEMPLATES_AUTO_RELOAD"] is None: - self.jinja_env.auto_reload = value - def run( self, - host: t.Optional[str] = None, - port: t.Optional[int] = None, - debug: t.Optional[bool] = None, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, load_dotenv: bool = True, **options: t.Any, ) -> None: @@ -1141,16 +709,8 @@ class Flask(Scaffold): if get_load_dotenv(load_dotenv): cli.load_dotenv() - # if set, let env vars override previous values - if "FLASK_ENV" in os.environ: - print( - "'FLASK_ENV' is deprecated and will not be used in" - " Flask 2.3. Use 'FLASK_DEBUG' instead.", - file=sys.stderr, - ) - self.config["ENV"] = os.environ.get("FLASK_ENV") or "production" - self.debug = get_debug_flag() - elif "FLASK_DEBUG" in os.environ: + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: self.debug = get_debug_flag() # debug passed to method overrides all other sources @@ -1192,7 +752,7 @@ class Flask(Scaffold): # without reloader and that stuff from an interactive shell. self._got_first_request = False - def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> "FlaskClient": + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: """Creates a test client for this application. For information about unit testing head over to :doc:`/testing`. @@ -1245,12 +805,12 @@ class Flask(Scaffold): """ cls = self.test_client_class if cls is None: - from .testing import FlaskClient as cls # type: ignore + from .testing import FlaskClient as cls return cls( # type: ignore self, self.response_class, use_cookies=use_cookies, **kwargs ) - def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: """Create a CLI runner for testing CLI commands. See :ref:`testing-cli`. @@ -1263,316 +823,13 @@ class Flask(Scaffold): cls = self.test_cli_runner_class if cls is None: - from .testing import FlaskCliRunner as cls # type: ignore + from .testing import FlaskCliRunner as cls return cls(self, **kwargs) # type: ignore - @setupmethod - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on the application. Keyword - arguments passed to this method will override the defaults set on the - blueprint. - - Calls the blueprint's :meth:`~flask.Blueprint.register` method after - recording the blueprint in the application's :attr:`blueprints`. - - :param blueprint: The blueprint to register. - :param url_prefix: Blueprint routes will be prefixed with this. - :param subdomain: Blueprint routes will match on this subdomain. - :param url_defaults: Blueprint routes will use these default values for - view arguments. - :param options: Additional keyword arguments are passed to - :class:`~flask.blueprints.BlueprintSetupState`. They can be - accessed in :meth:`~flask.Blueprint.record` callbacks. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 0.7 - """ - blueprint.register(self, options) - - def iter_blueprints(self) -> t.ValuesView["Blueprint"]: - """Iterates over all blueprints by the order they were registered. - - .. versionadded:: 0.11 - """ - return self.blueprints.values() - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[ft.RouteCallable] = None, - provide_automatic_options: t.Optional[bool] = None, - **options: t.Any, - ) -> None: - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - options["endpoint"] = endpoint - methods = options.pop("methods", None) - - # if the methods are not given and the view_func object knows its - # methods we can use that instead. If neither exists, we go with - # a tuple of only ``GET`` as default. - if methods is None: - methods = getattr(view_func, "methods", None) or ("GET",) - if isinstance(methods, str): - raise TypeError( - "Allowed methods must be a list of strings, for" - ' example: @app.route(..., methods=["POST"])' - ) - methods = {item.upper() for item in methods} - - # Methods that should always be added - required_methods = 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: - if "OPTIONS" not in methods: - provide_automatic_options = True - required_methods.add("OPTIONS") - else: - provide_automatic_options = False - - # Add the required methods now. - methods |= required_methods - - rule = self.url_rule_class(rule, methods=methods, **options) - rule.provide_automatic_options = provide_automatic_options # type: ignore - - self.url_map.add(rule) - if view_func is not None: - old_func = self.view_functions.get(endpoint) - if old_func is not None and old_func != view_func: - raise AssertionError( - "View function mapping is overwriting an existing" - f" endpoint function: {endpoint}" - ) - self.view_functions[endpoint] = view_func - - @setupmethod - def template_filter( - self, name: t.Optional[str] = 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:: - - @app.template_filter() - def reverse(s): - return s[::-1] - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_filter( - self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template filter. Works exactly like the - :meth:`template_filter` decorator. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - self.jinja_env.filters[name or f.__name__] = f - - @setupmethod - def template_test( - self, name: t.Optional[str] = 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:: - - @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 - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: T_template_test) -> T_template_test: - self.add_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_test( - self, f: ft.TemplateTestCallable, name: t.Optional[str] = None - ) -> None: - """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 - - @setupmethod - def template_global( - self, name: t.Optional[str] = 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:: - - @app.template_global() - def double(n): - return 2 * n - - .. versionadded:: 0.10 - - :param name: the optional name of the global function, otherwise the - function name will be used. - """ - - def decorator(f: T_template_global) -> T_template_global: - self.add_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_template_global( - self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None - ) -> None: - """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 before_first_request(self, f: T_before_first_request) -> T_before_first_request: - """Registers a function to be run before the first request to this - instance of the application. - - The function will be called without any arguments and its return - value is ignored. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Run setup code when creating - the application instead. - - .. versionadded:: 0.8 - """ - import warnings - - warnings.warn( - "'before_first_request' is deprecated and will be removed" - " in Flask 2.3. Run setup code while creating the" - " application instead.", - DeprecationWarning, - stacklevel=2, - ) - self.before_first_request_funcs.append(f) - return f - - @setupmethod - def teardown_appcontext(self, f: T_teardown) -> T_teardown: - """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 - - with app.app_context(): - ... - - When the ``with`` block exits (or ``ctx.pop()`` is called), the - teardown functions are called just before the app context is - 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 - :meth:`errorhandler` is registered, it will handle the exception - and the teardown will not receive it. - - Teardown functions must avoid raising exceptions. If they - execute code that might fail they must surround that code with a - ``try``/``except`` block and log any errors. - - The return values of teardown functions are ignored. - - .. versionadded:: 0.9 - """ - self.teardown_appcontext_funcs.append(f) - return f - - @setupmethod - def shell_context_processor( - self, f: T_shell_context_processor - ) -> T_shell_context_processor: - """Registers a shell context processor function. - - .. versionadded:: 0.11 - """ - self.shell_context_processors.append(f) - return f - - def _find_error_handler(self, e: Exception) -> t.Optional[ft.ErrorHandlerCallable]: - """Return a registered error handler for an exception in this order: - blueprint handler for a specific code, app handler for a specific code, - blueprint handler for an exception class, app handler for an exception - class, or ``None`` if a suitable handler is not found. - """ - exc_class, code = self._get_exc_class_and_code(type(e)) - names = (*request.blueprints, None) - - for c in (code, None) if code is not None else (None,): - for name in names: - handler_map = self.error_handler_spec[name][c] - - if not handler_map: - continue - - for cls in exc_class.__mro__: - handler = handler_map.get(cls) - - if handler is not None: - return handler - return None - def handle_http_exception( - self, e: HTTPException - ) -> t.Union[HTTPException, ft.ResponseReturnValue]: + self, ctx: AppContext, e: HTTPException + ) -> HTTPException | ft.ResponseReturnValue: """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the exception as response. @@ -1600,49 +857,14 @@ class Flask(Scaffold): if isinstance(e, RoutingException): return e - handler = self._find_error_handler(e) + handler = self._find_error_handler(e, ctx.request.blueprints) if handler is None: return e - return self.ensure_sync(handler)(e) - - def trap_http_exception(self, e: Exception) -> bool: - """Checks if an HTTP exception should be trapped or not. By default - this will return ``False`` for all exceptions except for a bad request - key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It - also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. - - This is called for all HTTP exceptions raised by a view function. - If it returns ``True`` for any exception the error handler for this - exception is not called and it shows up as regular exception in the - traceback. This is helpful for debugging implicitly raised HTTP - exceptions. - - .. versionchanged:: 1.0 - Bad request errors are not trapped by default in debug mode. - - .. versionadded:: 0.8 - """ - if self.config["TRAP_HTTP_EXCEPTIONS"]: - return True - - trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] - - # if unset, trap key errors in debug mode - if ( - trap_bad_request is None - and self.debug - and isinstance(e, BadRequestKeyError) - ): - return True - - if trap_bad_request: - return isinstance(e, BadRequest) - - return False + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] def handle_user_exception( - self, e: Exception - ) -> t.Union[HTTPException, ft.ResponseReturnValue]: + self, ctx: AppContext, e: Exception + ) -> HTTPException | ft.ResponseReturnValue: """This method is called whenever an exception occurs that should be handled. A special case is :class:`~werkzeug .exceptions.HTTPException` which is forwarded to the @@ -1663,23 +885,23 @@ class Flask(Scaffold): e.show_exception = True if isinstance(e, HTTPException) and not self.trap_http_exception(e): - return self.handle_http_exception(e) + return self.handle_http_exception(ctx, e) - handler = self._find_error_handler(e) + handler = self._find_error_handler(e, ctx.request.blueprints) if handler is None: raise - return self.ensure_sync(handler)(e) + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] - def handle_exception(self, e: Exception) -> Response: + def handle_exception(self, ctx: AppContext, e: Exception) -> Response: """Handle an exception that did not have an error handler associated with it, or that was raised from an error handler. This always causes a 500 ``InternalServerError``. Always sends the :data:`got_request_exception` signal. - If :attr:`propagate_exceptions` is ``True``, such as in debug + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug mode, the error will be re-raised so that the debugger can display it. Otherwise, the original exception is logged, and an :exc:`~werkzeug.exceptions.InternalServerError` is returned. @@ -1701,7 +923,7 @@ class Flask(Scaffold): .. versionadded:: 0.3 """ exc_info = sys.exc_info() - got_request_exception.send(self, exception=e) + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) propagate = self.config["PROPAGATE_EXCEPTIONS"] if propagate is None: @@ -1715,21 +937,20 @@ class Flask(Scaffold): raise e - self.log_exception(exc_info) - server_error: t.Union[InternalServerError, ft.ResponseReturnValue] + self.log_exception(ctx, exc_info) + server_error: InternalServerError | ft.ResponseReturnValue server_error = InternalServerError(original_exception=e) - handler = self._find_error_handler(server_error) + handler = self._find_error_handler(server_error, ctx.request.blueprints) if handler is not None: server_error = self.ensure_sync(handler)(server_error) - return self.finalize_request(server_error, from_error_handler=True) + return self.finalize_request(ctx, server_error, from_error_handler=True) def log_exception( self, - exc_info: t.Union[ - t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] - ], + ctx: AppContext, + 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. @@ -1739,38 +960,10 @@ class Flask(Scaffold): .. versionadded:: 0.8 """ self.logger.error( - f"Exception on {request.path} [{request.method}]", exc_info=exc_info + f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info ) - def raise_routing_exception(self, request: Request) -> "te.NoReturn": - """Intercept routing exceptions and possibly do something else. - - In debug mode, intercept a routing redirect and replace it with - an error if the body will be discarded. - - With modern Werkzeug this shouldn't occur, since it now uses a - 308 status which tells the browser to resend the method and - body. - - .. versionchanged:: 2.1 - Don't intercept 307 and 308 redirects. - - :meta private: - :internal: - """ - if ( - not self.debug - or not isinstance(request.routing_exception, RequestRedirect) - or request.routing_exception.code in {307, 308} - or request.method in {"GET", "HEAD", "OPTIONS"} - ): - raise request.routing_exception # type: ignore - - from .debughelpers import FormDataRoutingRedirect - - raise FormDataRoutingRedirect(request) - - def dispatch_request(self) -> ft.ResponseReturnValue: + def dispatch_request(self, ctx: AppContext) -> 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 @@ -1780,7 +973,8 @@ class Flask(Scaffold): This no longer does the exception handling, this code was moved to the new :meth:`full_dispatch_request`. """ - req = request_ctx.request + req = ctx.request + if req.routing_exception is not None: self.raise_routing_exception(req) rule: Rule = req.url_rule # type: ignore[assignment] @@ -1790,41 +984,44 @@ class Flask(Scaffold): getattr(rule, "provide_automatic_options", False) and req.method == "OPTIONS" ): - return self.make_default_options_response() + return self.make_default_options_response(ctx) # otherwise dispatch to the handler for that endpoint - view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment] - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) + 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) -> Response: + def full_dispatch_request(self, ctx: AppContext) -> 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 """ - # Run before_first_request functions if this is the thread's first request. - # Inlined to avoid a method call on subsequent requests. - # This is deprecated, will be removed in Flask 2.3. - if not self._got_first_request: - with self._before_request_lock: - if not self._got_first_request: - for func in self.before_first_request_funcs: - self.ensure_sync(func)() + if not self._got_first_request and self.should_ignore_error is not None: + import warnings - self._got_first_request = True + 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) - rv = self.preprocess_request() + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request(ctx) if rv is None: - rv = self.dispatch_request() + rv = self.dispatch_request(ctx) except Exception as e: - rv = self.handle_user_exception(e) - return self.finalize_request(rv) + rv = self.handle_user_exception(ctx, e) + return self.finalize_request(ctx, rv) def finalize_request( self, - rv: t.Union[ft.ResponseReturnValue, HTTPException], + ctx: AppContext, + rv: ft.ResponseReturnValue | HTTPException, from_error_handler: bool = False, ) -> Response: """Given the return value from a view function this finalizes @@ -1841,8 +1038,10 @@ class Flask(Scaffold): """ response = self.make_response(rv) try: - response = self.process_response(response) - request_finished.send(self, response=response) + response = self.process_response(ctx, response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) except Exception: if not from_error_handler: raise @@ -1851,30 +1050,19 @@ class Flask(Scaffold): ) return response - def make_default_options_response(self) -> Response: + def make_default_options_response(self, ctx: AppContext) -> Response: """This method is called to create the default ``OPTIONS`` response. This can be changed through subclassing to change the default behavior of ``OPTIONS`` responses. .. versionadded:: 0.7 """ - adapter = request_ctx.url_adapter - methods = adapter.allowed_methods() # type: ignore[union-attr] + methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr] rv = self.response_class() rv.allow.update(methods) return rv - def should_ignore_error(self, error: t.Optional[BaseException]) -> 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. - - .. versionadded:: 0.10 - """ - return False - - def ensure_sync(self, func: t.Callable) -> t.Callable: + def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: """Ensure that the function is synchronous for WSGI workers. Plain ``def`` functions are returned as-is. ``async def`` functions are wrapped to run and wait for the response. @@ -1889,7 +1077,7 @@ class Flask(Scaffold): return func def async_to_sync( - self, func: t.Callable[..., t.Coroutine] + self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] ) -> t.Callable[..., t.Any]: """Return a sync function that will run the coroutine function. @@ -1913,12 +1101,13 @@ class Flask(Scaffold): def url_for( self, + /, endpoint: str, *, - _anchor: t.Optional[str] = None, - _method: t.Optional[str] = None, - _scheme: t.Optional[str] = None, - _external: t.Optional[bool] = None, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -1967,11 +1156,9 @@ class Flask(Scaffold): .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ - 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 (ctx := _cv_app.get(None)) is not None and ctx.has_request: + url_adapter = ctx.url_adapter + blueprint_name = ctx.request.blueprint # If the endpoint starts with "." and the request matches a # blueprint, the endpoint is relative to the blueprint. @@ -1986,13 +1173,11 @@ class Flask(Scaffold): 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 app_ctx is not None: - url_adapter = app_ctx.url_adapter + if ctx is not None: + url_adapter = ctx.url_adapter else: url_adapter = self.create_url_adapter(None) @@ -2031,24 +1216,11 @@ class Flask(Scaffold): return self.handle_url_build_error(error, endpoint, values) if _anchor is not None: - rv = f"{rv}#{url_quote(_anchor)}" + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" return rv - 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 - directly as well. - - :param location: The URL to redirect to. - :param code: The status code for the redirect. - - .. versionadded:: 2.2 - Moved from ``flask.redirect``, which calls this method. - """ - return _wz_redirect(location, code=code, Response=self.response_class) - def make_response(self, rv: ft.ResponseReturnValue) -> Response: """Convert the return value from a view function to an instance of :attr:`response_class`. @@ -2106,7 +1278,8 @@ class Flask(Scaffold): response object. """ - status = headers = None + status: int | None = None + headers: HeadersValue | None = None # unpack tuple returns if isinstance(rv, tuple): @@ -2118,7 +1291,7 @@ class Flask(Scaffold): # decide if a 2-tuple has status or headers elif len_rv == 2: if isinstance(rv[1], (Headers, dict, tuple, list)): - rv, headers = rv + rv, headers = rv # pyright: ignore else: rv, status = rv # type: ignore[assignment,misc] # other sized tuples are not allowed @@ -2139,12 +1312,12 @@ class Flask(Scaffold): # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_Iterator): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic rv = self.response_class( - rv, + rv, # pyright: ignore status=status, headers=headers, # type: ignore[arg-type] ) @@ -2156,7 +1329,8 @@ class Flask(Scaffold): # class to the correct type try: rv = self.response_class.force_type( - rv, request.environ # type: ignore[arg-type] + rv, # type: ignore[arg-type] + request.environ, ) except TypeError as e: raise TypeError( @@ -2185,108 +1359,11 @@ class Flask(Scaffold): # extend existing headers with provided headers if headers: - rv.headers.update(headers) # type: ignore[arg-type] + rv.headers.update(headers) return rv - def create_url_adapter( - self, request: t.Optional[Request] - ) -> t.Optional[MapAdapter]: - """Creates a URL adapter for the given request. The URL adapter - is created at a point where the request context is not yet set - up so the request is passed explicitly. - - .. versionadded:: 0.6 - - .. versionchanged:: 0.9 - This can now also be called without a request object when the - URL adapter is created for the application context. - - .. versionchanged:: 1.0 - :data:`SERVER_NAME` no longer implicitly enables subdomain - matching. Use :attr:`subdomain_matching` instead. - """ - if request is not None: - # If subdomain matching is disabled (the default), use the - # default subdomain in all cases. This should be the default - # in Werkzeug but it currently does not have that feature. - if not self.subdomain_matching: - subdomain = self.url_map.default_subdomain or None - else: - subdomain = None - - return self.url_map.bind_to_environ( - request.environ, - server_name=self.config["SERVER_NAME"], - subdomain=subdomain, - ) - # We need at the very least the server name to be set for this - # to work. - if self.config["SERVER_NAME"] is not None: - return self.url_map.bind( - self.config["SERVER_NAME"], - script_name=self.config["APPLICATION_ROOT"], - url_scheme=self.config["PREFERRED_URL_SCHEME"], - ) - - return None - - def inject_url_defaults(self, endpoint: str, values: dict) -> None: - """Injects the URL defaults for the given endpoint directly into - the values dictionary passed. This is used internally and - automatically called on URL building. - - .. versionadded:: 0.7 - """ - names: t.Iterable[t.Optional[str]] = (None,) - - # url_for may be called outside a request context, parse the - # passed endpoint instead of using request.blueprints. - if "." in endpoint: - names = chain( - names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) - ) - - for name in names: - if name in self.url_default_functions: - for func in self.url_default_functions[name]: - func(endpoint, values) - - def handle_url_build_error( - self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any] - ) -> str: - """Called by :meth:`.url_for` if a - :exc:`~werkzeug.routing.BuildError` was raised. If this returns - a value, it will be returned by ``url_for``, otherwise the error - will be re-raised. - - Each function in :attr:`url_build_error_handlers` is called with - ``error``, ``endpoint`` and ``values``. If a function returns - ``None`` or raises a ``BuildError``, it is skipped. Otherwise, - its return value is returned by ``url_for``. - - :param error: The active ``BuildError`` being handled. - :param endpoint: The endpoint being built. - :param values: The keyword arguments passed to ``url_for``. - """ - for handler in self.url_build_error_handlers: - try: - rv = handler(error, endpoint, values) - except BuildError as e: - # make error available outside except block - error = e - else: - if rv is not None: - return rv - - # Re-raise if called with an active exception, otherwise raise - # the passed in exception. - if error is sys.exc_info()[1]: - raise - - raise error - - def preprocess_request(self) -> t.Optional[ft.ResponseReturnValue]: + def preprocess_request(self, ctx: AppContext) -> 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` @@ -2296,12 +1373,13 @@ class Flask(Scaffold): value is handled as if it was the return value from the view, and further request handling is stopped. """ - names = (None, *reversed(request.blueprints)) + req = ctx.request + names = (None, *reversed(req.blueprints)) for name in names: if name in self.url_value_preprocessors: for url_func in self.url_value_preprocessors[name]: - url_func(request.endpoint, request.view_args) + url_func(req.endpoint, req.view_args) for name in names: if name in self.before_request_funcs: @@ -2309,11 +1387,11 @@ class Flask(Scaffold): rv = self.ensure_sync(before_func)() if rv is not None: - return rv + return rv # type: ignore[no-any-return] return None - def process_response(self, response: Response) -> Response: + def process_response(self, ctx: AppContext, response: Response) -> Response: """Can be overridden in order to modify the response object before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. @@ -2326,90 +1404,90 @@ class Flask(Scaffold): :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(request.blueprints, (None,)): + for name in chain(ctx.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.session): - self.session_interface.save_session(self, ctx.session, response) + if not self.session_interface.is_null_session(ctx._get_session()): + self.session_interface.save_session(self, ctx._get_session(), response) return response def do_teardown_request( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore + self, ctx: AppContext, exc: BaseException | None = None ) -> None: - """Called after the request is dispatched and the response is - returned, right before the request context is popped. + """Called after the request is dispatched and the response is finalized, + right before the request context is popped. Called by + :meth:`.AppContext.pop`. - 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. - This is called by - :meth:`RequestContext.pop() `, - which may be delayed during testing to maintain access to - resources. + :param exc: An unhandled exception raised while dispatching the request. + Passed to each teardown function. - :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:: 3.2 + All callbacks are called rather than stopping on the first error. .. versionchanged:: 0.9 Added the ``exc`` argument. """ - if exc is _sentinel: - exc = sys.exc_info()[1] + collect_errors = _CollectErrors() - for name in chain(request.blueprints, (None,)): + for name in chain(ctx.request.blueprints, (None,)): if name in self.teardown_request_funcs: for func in reversed(self.teardown_request_funcs[name]): - self.ensure_sync(func)(exc) + with collect_errors: + self.ensure_sync(func)(exc) - request_tearing_down.send(self, exc=exc) + with collect_errors: + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + collect_errors.raise_any("Errors during request teardown") def do_teardown_appcontext( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore + self, ctx: AppContext, exc: BaseException | None = None ) -> None: - """Called right before the application context is popped. + """Called right before the application context is popped. Called by + :meth:`.AppContext.pop`. - When handling a request, the application context is popped - after the request context. See :meth:`do_teardown_request`. + This calls all functions decorated with :meth:`teardown_appcontext`. + Then the :data:`appcontext_tearing_down` signal is sent. - This calls all functions decorated with - :meth:`teardown_appcontext`. Then the - :data:`appcontext_tearing_down` signal is sent. + :param exc: An unhandled exception raised while the context was active. + Passed to each teardown function. - This is called by - :meth:`AppContext.pop() `. + .. versionchanged:: 3.2 + All callbacks are called rather than stopping on the first error. .. versionadded:: 0.9 """ - if exc is _sentinel: - exc = sys.exc_info()[1] + collect_errors = _CollectErrors() for func in reversed(self.teardown_appcontext_funcs): - self.ensure_sync(func)(exc) + with collect_errors: + self.ensure_sync(func)(exc) - appcontext_tearing_down.send(self, exc=exc) + with collect_errors: + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + collect_errors.raise_any("Errors during app teardown") def app_context(self) -> AppContext: - """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. + """Create an :class:`.AppContext`. When the context is pushed, + :data:`.current_app` and :data:`.g` become available. - 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. + 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. - :: + .. code-block:: python with app.app_context(): init_db() @@ -2420,44 +1498,37 @@ class Flask(Scaffold): """ return AppContext(self) - def request_context(self, environ: dict) -> 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. + 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. - See :doc:`/reqcontext`. + This method should not be used in your own code. Creating a valid WSGI + environ is not trivial. Use :meth:`test_request_context` to correctly + create a WSGI environ and request context instead. - Typically you should not call this from your own code. A request - 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. + See :doc:`/appcontext`. - :param environ: a WSGI environment + :param environ: A WSGI environment. """ - return RequestContext(self, environ) + return AppContext.from_environ(self, environ) - def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: - """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. + 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. - See :doc:`/reqcontext`. + This is useful during testing to run a function that uses request data + without dispatching a full request. Use this as a ``with`` block to push + a context. - Use a ``with`` block to push the context, which will make - :data:`request` point at the request for the created - environment. :: + .. code-block:: python - with test_request_context(...): + with app.test_request_context(...): generate_report() - 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() + See :doc:`/appcontext`. Takes the same arguments as Werkzeug's :class:`~werkzeug.test.EnvironBuilder`, with some defaults from @@ -2467,20 +1538,18 @@ class Flask(Scaffold): :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 append to - :data:`SERVER_NAME`. + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`, + and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to prepend to :data:`SERVER_NAME`. :param url_scheme: Scheme to use instead of :data:`PREFERRED_URL_SCHEME`. - :param data: The request body, either as a string or a dict of - form keys and values. + :param data: The request body text or bytes,or a dict of form data. :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 @@ -2488,11 +1557,15 @@ class Flask(Scaffold): builder = EnvironBuilder(self, *args, **kwargs) try: - return self.request_context(builder.get_environ()) + environ = builder.get_environ() finally: builder.close() - def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: + return self.request_context(environ) + + def wsgi_app( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: """The actual WSGI application. This is not implemented in :meth:`__call__` so that middlewares can be applied without losing a reference to the app object. Instead of doing this:: @@ -2510,7 +1583,6 @@ class Flask(Scaffold): 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, @@ -2518,29 +1590,34 @@ class Flask(Scaffold): start the response. """ ctx = self.request_context(environ) - error: t.Optional[BaseException] = None + error: BaseException | None = None try: try: ctx.push() - response = self.full_dispatch_request() + response = self.full_dispatch_request(ctx) except Exception as e: error = e - response = self.handle_exception(e) - except: # noqa: B001 + response = self.handle_exception(ctx, e) + except: error = sys.exc_info()[1] raise return response(environ, start_response) finally: if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](_cv_app.get()) - environ["werkzeug.debug.preserve_context"](_cv_request.get()) + environ["werkzeug.debug.preserve_context"](ctx) - if error is not None and self.should_ignore_error(error): + if ( + error is not None + and self.should_ignore_error is not None + and self.should_ignore_error(error) + ): error = None ctx.pop(error) - def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: + def __call__( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: """The WSGI server calls the Flask application object as the WSGI application. This calls :meth:`wsgi_app`, which can be wrapped to apply middleware. diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 104f8acf..b6d4e433 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -1,706 +1,128 @@ -import json +from __future__ import annotations + import os import typing as t -from collections import defaultdict -from functools import update_wrapper +from datetime import timedelta -from . import typing as ft -from .scaffold import _endpoint_from_view_func -from .scaffold import _sentinel -from .scaffold import Scaffold -from .scaffold import setupmethod +from .cli import AppGroup +from .globals import current_app +from .helpers import send_from_directory +from .sansio.blueprints import Blueprint as SansioBlueprint +from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa +from .sansio.scaffold import _sentinel if t.TYPE_CHECKING: # pragma: no cover - from .app import Flask - -DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] -T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable) -T_before_first_request = t.TypeVar( - "T_before_first_request", bound=ft.BeforeFirstRequestCallable -) -T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) -T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) -T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) -T_template_context_processor = t.TypeVar( - "T_template_context_processor", bound=ft.TemplateContextProcessorCallable -) -T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) -T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) -T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) -T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) -T_url_value_preprocessor = t.TypeVar( - "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable -) + from .wrappers import Response -class BlueprintSetupState: - """Temporary holder object for registering a blueprint with the - application. An instance of this class is created by the - :meth:`~flask.Blueprint.make_setup_state` method and later passed - to all register callback functions. - """ - - def __init__( - self, - blueprint: "Blueprint", - app: "Flask", - options: t.Any, - first_registration: bool, - ) -> None: - #: a reference to the current application - self.app = app - - #: a reference to the blueprint that created this setup state. - self.blueprint = blueprint - - #: a dictionary with all options that were passed to the - #: :meth:`~flask.Flask.register_blueprint` method. - self.options = options - - #: as blueprints can be registered multiple times with the - #: application and not everything wants to be registered - #: multiple times on it, this attribute can be used to figure - #: out if the blueprint was registered in the past already. - self.first_registration = first_registration - - subdomain = self.options.get("subdomain") - if subdomain is None: - subdomain = self.blueprint.subdomain - - #: The subdomain that the blueprint should be active for, ``None`` - #: otherwise. - self.subdomain = subdomain - - url_prefix = self.options.get("url_prefix") - if url_prefix is None: - url_prefix = self.blueprint.url_prefix - #: The prefix that should be used for all URLs defined on the - #: blueprint. - self.url_prefix = url_prefix - - self.name = self.options.get("name", blueprint.name) - self.name_prefix = self.options.get("name_prefix", "") - - #: A dictionary with URL defaults that is added to each and every - #: URL that was defined with the blueprint. - self.url_defaults = dict(self.blueprint.url_values_defaults) - self.url_defaults.update(self.options.get("url_defaults", ())) - - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, - **options: t.Any, - ) -> None: - """A helper method to register a rule (and optionally a view function) - to the application. The endpoint is automatically prefixed with the - blueprint's name. - """ - if self.url_prefix is not None: - if rule: - rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) - else: - rule = self.url_prefix - options.setdefault("subdomain", self.subdomain) - if endpoint is None: - endpoint = _endpoint_from_view_func(view_func) # type: ignore - defaults = self.url_defaults - if "defaults" in options: - defaults = dict(defaults, **options.pop("defaults")) - - self.app.add_url_rule( - rule, - f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), - view_func, - defaults=defaults, - **options, - ) - - -class Blueprint(Scaffold): - """Represents a blueprint, a collection of routes and other - app-related functions that can be registered on a real application - later. - - A blueprint is an object that allows defining application functions - without requiring an application object ahead of time. It uses the - same decorators as :class:`~flask.Flask`, but defers the need for an - application by recording them for later registration. - - Decorating a function with a blueprint creates a deferred function - that is called with :class:`~flask.blueprints.BlueprintSetupState` - when the blueprint is registered on an application. - - See :doc:`/blueprints` for more information. - - :param name: The name of the blueprint. Will be prepended to each - endpoint name. - :param import_name: The name of the blueprint package, usually - ``__name__``. This helps locate the ``root_path`` for the - blueprint. - :param static_folder: A folder with static files that should be - served by the blueprint's static route. The path is relative to - the blueprint's root path. Blueprint static files are disabled - by default. - :param static_url_path: The url to serve static files from. - Defaults to ``static_folder``. If the blueprint does not have - a ``url_prefix``, the app's static route will take precedence, - and the blueprint's static files won't be accessible. - :param template_folder: A folder with templates that should be added - to the app's template search path. The path is relative to the - blueprint's root path. Blueprint templates are disabled by - default. Blueprint templates have a lower precedence than those - in the app's templates folder. - :param url_prefix: A path to prepend to all of the blueprint's URLs, - to make them distinct from the rest of the app's routes. - :param subdomain: A subdomain that blueprint routes will match on by - default. - :param url_defaults: A dict of default values that blueprint routes - will receive by default. - :param root_path: By default, the blueprint will automatically set - this based on ``import_name``. In certain situations this - automatic detection can fail, so the path can be specified - manually instead. - - .. versionchanged:: 1.1.0 - Blueprints have a ``cli`` group to register nested CLI commands. - The ``cli_group`` parameter controls the name of the group under - the ``flask`` command. - - .. versionadded:: 0.7 - """ - - _got_registered_once = False - - _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None - _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None - - @property # type: ignore[override] - def json_encoder( # type: ignore[override] - self, - ) -> t.Union[t.Type[json.JSONEncoder], None]: - """Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'bp.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._json_encoder - - @json_encoder.setter - def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None: - import warnings - - warnings.warn( - "'bp.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_encoder = value - - @property # type: ignore[override] - def json_decoder( # type: ignore[override] - self, - ) -> t.Union[t.Type[json.JSONDecoder], None]: - """Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'bp.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._json_decoder - - @json_decoder.setter - def json_decoder(self, value: t.Union[t.Type[json.JSONDecoder], None]) -> None: - import warnings - - warnings.warn( - "'bp.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_decoder = value - +class Blueprint(SansioBlueprint): def __init__( self, name: str, import_name: str, - static_folder: t.Optional[t.Union[str, os.PathLike]] = None, - static_url_path: t.Optional[str] = None, - template_folder: t.Optional[str] = None, - url_prefix: t.Optional[str] = None, - subdomain: t.Optional[str] = None, - url_defaults: t.Optional[dict] = None, - root_path: t.Optional[str] = None, - cli_group: t.Optional[str] = _sentinel, # type: ignore - ): + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore + ) -> None: super().__init__( - import_name=import_name, - static_folder=static_folder, - static_url_path=static_url_path, - template_folder=template_folder, - root_path=root_path, + name, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_group, ) - if "." in name: - raise ValueError("'name' may not contain a dot '.' character.") + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = AppGroup() - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.deferred_functions: t.List[DeferredSetupFunction] = [] + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name - if url_defaults is None: - url_defaults = {} + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. - self.url_values_defaults = url_defaults - self.cli_group = cli_group - self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. - def _check_setup_finished(self, f_name: str) -> None: - if self._got_registered_once: - import warnings + Note this is a duplicate of the same method in the Flask + class. - warnings.warn( - f"The setup method '{f_name}' can no longer be called on" - f" the blueprint '{self.name}'. It has already been" - " registered at least once, any changes will not be" - " applied consistently.\n" - "Make sure all imports, decorators, functions, etc." - " needed to set up the blueprint are done before" - " registering it.\n" - "This warning will become an exception in Flask 2.3.", - UserWarning, - stacklevel=3, - ) + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. - @setupmethod - def record(self, func: t.Callable) -> None: - """Registers a function that is called when the blueprint is - registered on the application. This function is called with the - state as argument as returned by the :meth:`make_setup_state` + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource( + self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for reading. The + blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` method. + + :param resource: Path to the resource relative to :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is supported, + valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. """ - self.deferred_functions.append(func) + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") - @setupmethod - def record_once(self, func: t.Callable) -> None: - """Works like :meth:`record` but wraps the function in another - function that will ensure the function is only called once. If the - blueprint is registered a second time on the application, the - function passed is not called. - """ + path = os.path.join(self.root_path, resource) - def wrapper(state: BlueprintSetupState) -> None: - if state.first_registration: - func(state) + if mode == "rb": + return open(path, mode) # pyright: ignore - self.record(update_wrapper(wrapper, func)) - - def make_setup_state( - self, app: "Flask", options: dict, first_registration: bool = False - ) -> BlueprintSetupState: - """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` - object that is later passed to the register callback functions. - Subclasses can override this to return a subclass of the setup state. - """ - return BlueprintSetupState(self, app, options, first_registration) - - @setupmethod - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: - """Register a :class:`~flask.Blueprint` on this blueprint. Keyword - arguments passed to this method will override the defaults set - on the blueprint. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionadded:: 2.0 - """ - if blueprint is self: - raise ValueError("Cannot register a blueprint on itself") - self._blueprints.append((blueprint, options)) - - def register(self, app: "Flask", options: dict) -> None: - """Called by :meth:`Flask.register_blueprint` to register all - views and callbacks registered on the blueprint with the - application. Creates a :class:`.BlueprintSetupState` and calls - each :meth:`record` callback with it. - - :param app: The application this blueprint is being registered - with. - :param options: Keyword arguments forwarded from - :meth:`~Flask.register_blueprint`. - - .. versionchanged:: 2.0.1 - Nested blueprints are registered with their dotted name. - This allows different blueprints with the same name to be - nested at different locations. - - .. versionchanged:: 2.0.1 - The ``name`` option can be used to change the (pre-dotted) - name the blueprint is registered with. This allows the same - blueprint to be registered multiple times with unique names - for ``url_for``. - - .. versionchanged:: 2.0.1 - Registering the same blueprint with the same name multiple - times is deprecated and will become an error in Flask 2.1. - """ - name_prefix = options.get("name_prefix", "") - self_name = options.get("name", self.name) - name = f"{name_prefix}.{self_name}".lstrip(".") - - if name in app.blueprints: - bp_desc = "this" if app.blueprints[name] is self else "a different" - existing_at = f" '{name}'" if self_name != name else "" - - raise ValueError( - f"The name '{self_name}' is already registered for" - f" {bp_desc} blueprint{existing_at}. Use 'name=' to" - f" provide a unique name." - ) - - first_bp_registration = not any(bp is self for bp in app.blueprints.values()) - first_name_registration = name not in app.blueprints - - app.blueprints[name] = self - self._got_registered_once = True - state = self.make_setup_state(app, options, first_bp_registration) - - if self.has_static_folder: - state.add_url_rule( - f"{self.static_url_path}/", - view_func=self.send_static_file, - endpoint="static", - ) - - # Merge blueprint data into parent. - if first_bp_registration or first_name_registration: - - def extend(bp_dict, parent_dict): - for key, values in bp_dict.items(): - key = name if key is None else f"{name}.{key}" - parent_dict[key].extend(values) - - for key, value in self.error_handler_spec.items(): - key = name if key is None else f"{name}.{key}" - value = defaultdict( - dict, - { - code: { - exc_class: func for exc_class, func in code_values.items() - } - for code, code_values in value.items() - }, - ) - app.error_handler_spec[key] = value - - for endpoint, func in self.view_functions.items(): - app.view_functions[endpoint] = func - - extend(self.before_request_funcs, app.before_request_funcs) - extend(self.after_request_funcs, app.after_request_funcs) - extend( - self.teardown_request_funcs, - app.teardown_request_funcs, - ) - extend(self.url_default_functions, app.url_default_functions) - extend(self.url_value_preprocessors, app.url_value_preprocessors) - extend(self.template_context_processors, app.template_context_processors) - - for deferred in self.deferred_functions: - deferred(state) - - cli_resolved_group = options.get("cli_group", self.cli_group) - - if self.cli.commands: - if cli_resolved_group is None: - app.cli.commands.update(self.cli.commands) - elif cli_resolved_group is _sentinel: - self.cli.name = name - app.cli.add_command(self.cli) - else: - self.cli.name = cli_resolved_group - app.cli.add_command(self.cli) - - for blueprint, bp_options in self._blueprints: - bp_options = bp_options.copy() - bp_url_prefix = bp_options.get("url_prefix") - - if bp_url_prefix is None: - bp_url_prefix = blueprint.url_prefix - - if state.url_prefix is not None and bp_url_prefix is not None: - bp_options["url_prefix"] = ( - state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") - ) - elif bp_url_prefix is not None: - bp_options["url_prefix"] = bp_url_prefix - elif state.url_prefix is not None: - bp_options["url_prefix"] = state.url_prefix - - bp_options["name_prefix"] = name - blueprint.register(app, bp_options) - - @setupmethod - def add_url_rule( - self, - rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[ft.RouteCallable] = None, - provide_automatic_options: t.Optional[bool] = None, - **options: t.Any, - ) -> None: - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. - """ - if endpoint and "." in endpoint: - raise ValueError("'endpoint' may not contain a dot '.' character.") - - if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: - raise ValueError("'view_func' name may not contain a dot '.' character.") - - self.record( - lambda s: s.add_url_rule( - rule, - endpoint, - view_func, - provide_automatic_options=provide_automatic_options, - **options, - ) - ) - - @setupmethod - def app_template_filter( - self, name: t.Optional[str] = None - ) -> t.Callable[[T_template_filter], T_template_filter]: - """Register a custom template filter, available application wide. Like - :meth:`Flask.template_filter` but for a blueprint. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def decorator(f: T_template_filter) -> T_template_filter: - self.add_app_template_filter(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_filter( - self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template filter, available application wide. Like - :meth:`Flask.add_template_filter` but for a blueprint. Works exactly - like the :meth:`app_template_filter` decorator. - - :param name: the optional name of the filter, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.filters[name or f.__name__] = f - - self.record_once(register_template) - - @setupmethod - def app_template_test( - self, name: t.Optional[str] = None - ) -> t.Callable[[T_template_test], T_template_test]: - """Register a custom template test, available application wide. Like - :meth:`Flask.template_test` but for a blueprint. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def decorator(f: T_template_test) -> T_template_test: - self.add_app_template_test(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_test( - self, f: ft.TemplateTestCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template test, available application wide. Like - :meth:`Flask.add_template_test` but for a blueprint. Works exactly - like the :meth:`app_template_test` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the test, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.tests[name or f.__name__] = f - - self.record_once(register_template) - - @setupmethod - def app_template_global( - self, name: t.Optional[str] = None - ) -> t.Callable[[T_template_global], T_template_global]: - """Register a custom template global, available application wide. Like - :meth:`Flask.template_global` but for a blueprint. - - .. versionadded:: 0.10 - - :param name: the optional name of the global, otherwise the - function name will be used. - """ - - def decorator(f: T_template_global) -> T_template_global: - self.add_app_template_global(f, name=name) - return f - - return decorator - - @setupmethod - def add_app_template_global( - self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None - ) -> None: - """Register a custom template global, available application wide. Like - :meth:`Flask.add_template_global` but for a blueprint. Works exactly - like the :meth:`app_template_global` decorator. - - .. versionadded:: 0.10 - - :param name: the optional name of the global, otherwise the - function name will be used. - """ - - def register_template(state: BlueprintSetupState) -> None: - state.app.jinja_env.globals[name or f.__name__] = f - - self.record_once(register_template) - - @setupmethod - def before_app_request(self, f: T_before_request) -> T_before_request: - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a blueprint. - """ - self.record_once( - lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def before_app_first_request( - self, f: T_before_first_request - ) -> T_before_first_request: - """Like :meth:`Flask.before_first_request`. Such a function is - executed before the first request to the application. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Run setup code when creating - the application instead. - """ - import warnings - - warnings.warn( - "'before_app_first_request' is deprecated and will be" - " removed in Flask 2.3. Use 'record_once' instead to run" - " setup code when registering the blueprint.", - DeprecationWarning, - stacklevel=2, - ) - self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) - return f - - @setupmethod - def after_app_request(self, f: T_after_request) -> T_after_request: - """Like :meth:`Flask.after_request` but for a blueprint. Such a function - is executed after each request, even if outside of the blueprint. - """ - self.record_once( - lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def teardown_app_request(self, f: T_teardown) -> T_teardown: - """Like :meth:`Flask.teardown_request` but for a blueprint. Such a - function is executed when tearing down each request, even if outside of - the blueprint. - """ - self.record_once( - lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_context_processor( - self, f: T_template_context_processor - ) -> T_template_context_processor: - """Like :meth:`Flask.context_processor` but for a blueprint. Such a - function is executed each request, even if outside of the blueprint. - """ - self.record_once( - lambda s: s.app.template_context_processors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_errorhandler( - self, code: t.Union[t.Type[Exception], int] - ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`Flask.errorhandler` but for a blueprint. This - handler is used for all requests, even if outside of the blueprint. - """ - - def decorator(f: T_error_handler) -> T_error_handler: - self.record_once(lambda s: s.app.errorhandler(code)(f)) - return f - - return decorator - - @setupmethod - def app_url_value_preprocessor( - self, f: T_url_value_preprocessor - ) -> T_url_value_preprocessor: - """Same as :meth:`url_value_preprocessor` but application wide.""" - self.record_once( - lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) - ) - return f - - @setupmethod - def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Same as :meth:`url_defaults` but application wide.""" - self.record_once( - lambda s: s.app.url_default_functions.setdefault(None, []).append(f) - ) - return f + return open(path, mode, encoding=encoding) diff --git a/src/flask/cli.py b/src/flask/cli.py index c12311f6..1a9159ec 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1,6 +1,8 @@ from __future__ import annotations import ast +import collections.abc as cabc +import importlib.metadata import inspect import os import platform @@ -9,7 +11,8 @@ import sys import traceback import typing as t from functools import update_wrapper -from operator import attrgetter +from operator import itemgetter +from types import ModuleType import click from click.core import ParameterSource @@ -22,6 +25,12 @@ from .helpers import get_debug_flag from .helpers import get_load_dotenv if t.TYPE_CHECKING: + import ssl + + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + from .app import Flask @@ -29,7 +38,7 @@ class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" -def find_best_app(module): +def find_best_app(module: ModuleType) -> Flask: """Given a module instance this tries to find the best possible application in the module or raises an exception. """ @@ -82,7 +91,7 @@ def find_best_app(module): ) -def _called_with_wrong_args(f): +def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: """Check whether calling a function raised a ``TypeError`` because the call failed or because something in the factory raised the error. @@ -108,7 +117,7 @@ def _called_with_wrong_args(f): del tb -def find_app_by_string(module, app_name): +def find_app_by_string(module: ModuleType, app_name: str) -> Flask: """Check if the given string is a variable name or a function. Call a function to get the app instance, or return the variable directly. """ @@ -139,7 +148,11 @@ def find_app_by_string(module, app_name): # Parse the positional and keyword arguments as literals. try: args = [ast.literal_eval(arg) for arg in expr.args] - kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} + kwargs = { + kw.arg: ast.literal_eval(kw.value) + for kw in expr.keywords + if kw.arg is not None + } except ValueError: # literal_eval gives cryptic error messages, show a generic # message with the full expression instead. @@ -184,7 +197,7 @@ def find_app_by_string(module, app_name): ) -def prepare_import(path): +def prepare_import(path: str) -> str: """Given a filename this will try to calculate the python path, add it to the search path and return the actual module name that is expected. """ @@ -213,13 +226,27 @@ def prepare_import(path): return ".".join(module_name[::-1]) -def locate_app(module_name, app_name, raise_if_not_found=True): +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True +) -> Flask: ... + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... +) -> Flask | None: ... + + +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: bool = True +) -> Flask | None: try: __import__(module_name) except ImportError: # Reraise the ImportError if it occurred within the imported module. # Determine this by checking whether the trace has a depth > 1. - if sys.exc_info()[2].tb_next: + if sys.exc_info()[2].tb_next: # type: ignore[union-attr] raise NoAppException( f"While importing {module_name!r}, an ImportError was" f" raised:\n\n{traceback.format_exc()}" @@ -227,7 +254,7 @@ def locate_app(module_name, app_name, raise_if_not_found=True): elif raise_if_not_found: raise NoAppException(f"Could not import {module_name!r}.") from None else: - return + return None module = sys.modules[module_name] @@ -237,17 +264,17 @@ def locate_app(module_name, app_name, raise_if_not_found=True): return find_app_by_string(module, app_name) -def get_version(ctx, param, value): +def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: if not value or ctx.resilient_parsing: return - import werkzeug - from . import __version__ + flask_version = importlib.metadata.version("flask") + werkzeug_version = importlib.metadata.version("werkzeug") click.echo( f"Python {platform.python_version()}\n" - f"Flask {__version__}\n" - f"Werkzeug {werkzeug.__version__}", + f"Flask {flask_version}\n" + f"Werkzeug {werkzeug_version}", color=ctx.color, ) ctx.exit() @@ -270,6 +297,9 @@ class ScriptInfo: a bigger role. Typically it's created automatically by the :class:`FlaskGroup` but you can also manually create it and pass it onwards as click object. + + .. versionchanged:: 3.1 + Added the ``load_dotenv_defaults`` parameter and attribute. """ def __init__( @@ -277,6 +307,7 @@ class ScriptInfo: app_import_path: str | None = None, create_app: t.Callable[..., Flask] | None = None, set_debug_flag: bool = True, + load_dotenv_defaults: bool = True, ) -> None: #: Optionally the import path for the Flask application. self.app_import_path = app_import_path @@ -285,8 +316,18 @@ class ScriptInfo: self.create_app = create_app #: A dictionary with arbitrary data that can be associated with #: this script info. - self.data: t.Dict[t.Any, t.Any] = {} + self.data: dict[t.Any, t.Any] = {} self.set_debug_flag = set_debug_flag + + self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults) + """Whether default ``.flaskenv`` and ``.env`` files should be loaded. + + ``ScriptInfo`` doesn't load anything, this is for reference when doing + the load elsewhere during processing. + + .. versionadded:: 3.1 + """ + self._loaded_app: Flask | None = None def load_app(self) -> Flask: @@ -296,13 +337,13 @@ class ScriptInfo: """ if self._loaded_app is not None: return self._loaded_app - + app: Flask | None = None if self.create_app is not None: app = self.create_app() else: if self.app_import_path: path, name = ( - re.split(r":(?![\\/])", self.app_import_path, 1) + [None] + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] )[:2] import_name = prepare_import(path) app = locate_app(import_name, name) @@ -311,10 +352,10 @@ class ScriptInfo: import_name = prepare_import(path) app = locate_app(import_name, None, raise_if_not_found=False) - if app: + if app is not None: break - if not app: + if app is None: raise NoAppException( "Could not locate a Flask application. Use the" " 'flask --app' option, 'FLASK_APP' environment" @@ -333,8 +374,10 @@ class ScriptInfo: pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -def with_appcontext(f): + +def with_appcontext(f: F) -> F: """Wraps a callback so that it's guaranteed to be executed with the script's application context. @@ -349,14 +392,14 @@ def with_appcontext(f): """ @click.pass_context - def decorator(__ctx, *args, **kwargs): + def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: if not current_app: - app = __ctx.ensure_object(ScriptInfo).load_app() - __ctx.with_resource(app.app_context()) + app = ctx.ensure_object(ScriptInfo).load_app() + ctx.with_resource(app.app_context()) - return __ctx.invoke(f, *args, **kwargs) + return ctx.invoke(f, *args, **kwargs) - return update_wrapper(decorator, f) + return update_wrapper(decorator, f) # type: ignore[return-value] class AppGroup(click.Group): @@ -367,27 +410,31 @@ class AppGroup(click.Group): Not to be confused with :class:`FlaskGroup`. """ - def command(self, *args, **kwargs): + def command( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: """This works exactly like the method of the same name on a regular :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` unless it's disabled by passing ``with_appcontext=False``. """ wrap_for_ctx = kwargs.pop("with_appcontext", True) - def decorator(f): + def decorator(f: t.Callable[..., t.Any]) -> click.Command: if wrap_for_ctx: f = with_appcontext(f) - return click.Group.command(self, *args, **kwargs)(f) + return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] return decorator - def group(self, *args, **kwargs): + def group( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: """This works exactly like the method of the same name on a regular :class:`click.Group` but it defaults the group class to :class:`AppGroup`. """ kwargs.setdefault("cls", AppGroup) - return click.Group.group(self, *args, **kwargs) + return super().group(*args, **kwargs) # type: ignore[no-any-return] def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: @@ -421,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) # type: ignore[arg-type] + source = ctx.get_parameter_source(param.name) if source is not None and source in ( ParameterSource.DEFAULT, @@ -446,23 +493,22 @@ _debug_option = click.Option( def _env_file_callback( ctx: click.Context, param: click.Option, value: str | None ) -> str | None: - if value is None: - return None - - import importlib - try: - importlib.import_module("dotenv") + import dotenv # noqa: F401 except ImportError: - raise click.BadParameter( - "python-dotenv must be installed to load an env file.", - ctx=ctx, - param=param, - ) from None + # Only show an error if a value was passed, otherwise we still want to + # call load_dotenv and show a message without exiting. + if value is not None: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Load if a value was passed, or we want to load default files, or both. + if value is not None or ctx.obj.load_dotenv_defaults: + load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults) - # Don't check FLASK_SKIP_DOTENV, that only disables automatically - # loading .env and .flaskenv files. - load_dotenv(value) return value @@ -471,7 +517,11 @@ def _env_file_callback( _env_file_option = click.Option( ["-e", "--env-file"], type=click.Path(exists=True, dir_okay=False), - help="Load environment variables from this file. python-dotenv must be installed.", + help=( + "Load environment variables from this file, taking precedence over" + " those set by '.env' and '.flaskenv'. Variables set directly in the" + " environment take highest precedence. python-dotenv must be installed." + ), is_eager=True, expose_value=False, callback=_env_file_callback, @@ -495,6 +545,9 @@ class FlaskGroup(AppGroup): directory to the directory containing the first file found. :param set_debug_flag: Set the app's debug flag. + .. versionchanged:: 3.1 + ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. + .. versionchanged:: 2.2 Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. @@ -516,7 +569,7 @@ class FlaskGroup(AppGroup): set_debug_flag: bool = True, **extra: t.Any, ) -> None: - params = list(extra.pop("params", None) or ()) + params: list[click.Parameter] = list(extra.pop("params", None) or ()) # Processing is done with option callbacks instead of a group # callback. This allows users to make a custom group callback # without losing the behavior. --env-file must come first so @@ -544,24 +597,16 @@ class FlaskGroup(AppGroup): self._loaded_plugin_commands = False - def _load_plugin_commands(self): + def _load_plugin_commands(self) -> None: if self._loaded_plugin_commands: return - 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 - - for ep in metadata.entry_points(group="flask.commands"): + for ep in importlib.metadata.entry_points(group="flask.commands"): self.add_command(ep.load(), ep.name) self._loaded_plugin_commands = True - def get_command(self, ctx, name): + def get_command(self, ctx: click.Context, name: str) -> click.Command | None: self._load_plugin_commands() # Look up built-in and plugin commands, which should be # available even if the app fails to load. @@ -588,7 +633,7 @@ class FlaskGroup(AppGroup): return app.cli.get_command(ctx, name) - def list_commands(self, ctx): + def list_commands(self, ctx: click.Context) -> list[str]: self._load_plugin_commands() # Start with the built-in and plugin commands. rv = set(super().list_commands(ctx)) @@ -621,20 +666,19 @@ class FlaskGroup(AppGroup): # when importing, blocking whatever command is being called. os.environ["FLASK_RUN_FROM_CLI"] = "true" - # Attempt to load .env and .flask env files. The --env-file - # option can cause another file to be loaded. - if get_load_dotenv(self.load_dotenv): - load_dotenv() - if "obj" not in extra and "obj" not in self.context_settings: extra["obj"] = ScriptInfo( - create_app=self.create_app, set_debug_flag=self.set_debug_flag + create_app=self.create_app, + set_debug_flag=self.set_debug_flag, + load_dotenv_defaults=self.load_dotenv, ) 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: + if (not args and self.no_args_is_help) or ( + len(args) == 1 and args[0] in self.get_help_option_names(ctx) + ): # Attempt to load --env-file and --app early in case they # were given as env vars. Otherwise no_args_is_help will not # see commands from app.cli. @@ -644,25 +688,33 @@ class FlaskGroup(AppGroup): return super().parse_args(ctx, args) -def _path_is_ancestor(path, other): +def _path_is_ancestor(path: str, other: str) -> bool: """Take ``other`` and remove the length of ``path`` from it. Then join it to ``path``. If it is the original value, ``path`` is an ancestor of ``other``.""" return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other -def load_dotenv(path: str | os.PathLike | None = None) -> bool: - """Load "dotenv" files in order of precedence to set environment variables. - - If an env var is already set it is not overwritten, so earlier files in the - list are preferred over later files. +def load_dotenv( + path: str | os.PathLike[str] | None = None, load_defaults: bool = True +) -> bool: + """Load "dotenv" files to set environment variables. A given path takes + precedence over ``.env``, which takes precedence over ``.flaskenv``. After + loading and combining these files, values are only set if the key is not + already set in ``os.environ``. This is a no-op if `python-dotenv`_ is not installed. .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme - :param path: Load the file at this location instead of searching. - :return: ``True`` if a file was loaded. + :param path: Load the file at this location. + :param load_defaults: Search for and load the default ``.flaskenv`` and + ``.env`` files. + :return: ``True`` if at least one env var was loaded. + + .. versionchanged:: 3.1 + Added the ``load_defaults`` parameter. A given path takes precedence + over default files. .. versionchanged:: 2.0 The current directory is not changed to the location of the @@ -682,50 +734,42 @@ def load_dotenv(path: str | os.PathLike | None = None) -> bool: except ImportError: if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): click.secho( - " * Tip: There are .env or .flaskenv files present." - ' Do "pip install python-dotenv" to use them.', + " * Tip: There are .env files present. Install python-dotenv" + " to use them.", fg="yellow", err=True, ) return False - # Always return after attempting to load a given path, don't load - # the default files. - if path is not None: - if os.path.isfile(path): - return dotenv.load_dotenv(path, encoding="utf-8") + data: dict[str, str | None] = {} - return False + if load_defaults: + for default_name in (".flaskenv", ".env"): + if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): + continue - loaded = False + data |= dotenv.dotenv_values(default_path, encoding="utf-8") - for name in (".env", ".flaskenv"): - path = dotenv.find_dotenv(name, usecwd=True) + if path is not None and os.path.isfile(path): + data |= dotenv.dotenv_values(path, encoding="utf-8") - if not path: + for key, value in data.items(): + if key in os.environ or value is None: continue - dotenv.load_dotenv(path, encoding="utf-8") - loaded = True + os.environ[key] = value - return loaded # True if at least one file was located and loaded. + return bool(data) # True if at least one env var was loaded. -def show_server_banner(debug, app_import_path): +def show_server_banner(debug: bool, app_import_path: str | None) -> None: """Show extra startup messages the first time the server is run, ignoring the reloader. """ if is_running_from_reloader(): return - click.secho( - "WARNING: This is a development server. Do not use it in a production" - " deployment. Use a production WSGI server instead.", - fg="red", - bold=True, - ) - if app_import_path is not None: click.echo(f" * Serving Flask app '{app_import_path}'") @@ -733,7 +777,7 @@ def show_server_banner(debug, app_import_path): click.echo(f" * Debug mode: {'on' if debug else 'off'}") -class CertParamType(click.ParamType): +class CertParamType(click.ParamType[t.Any]): """Click option type for the ``--cert`` option. Allows either an existing file, the string ``'adhoc'``, or an import for a :class:`~ssl.SSLContext` object. @@ -741,10 +785,12 @@ class CertParamType(click.ParamType): name = "path" - def __init__(self): + def __init__(self) -> None: self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: try: import ssl except ImportError: @@ -757,7 +803,7 @@ class CertParamType(click.ParamType): try: return self.path_type(value, param, ctx) except click.BadParameter: - value = click.STRING(value, param, ctx).lower() + value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr] if value == "adhoc": try: @@ -779,7 +825,7 @@ class CertParamType(click.ParamType): raise -def _validate_key(ctx, param, value): +def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: """The ``--key`` option must be specified when ``--cert`` is a file. Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. """ @@ -801,7 +847,9 @@ def _validate_key(ctx, param, value): if is_context: raise click.BadParameter( - 'When "--cert" is an SSLContext object, "--key is not used.', ctx, param + 'When "--cert" is an SSLContext object, "--key" is not used.', + ctx, + param, ) if not cert: @@ -822,8 +870,11 @@ class SeparatedPathType(click.Path): validated as a :class:`click.Path` type. """ - def convert(self, value, param, ctx): + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: items = self.split_envvar_value(value) + # can't call no-arg super() inside list comprehension until Python 3.12 super_convert = super().convert return [super_convert(item, param, ctx) for item in items] @@ -882,16 +933,16 @@ class SeparatedPathType(click.Path): ) @pass_script_info def run_command( - info, - host, - port, - reload, - debugger, - with_threads, - cert, - extra_files, - exclude_patterns, -): + info: ScriptInfo, + host: str, + port: int, + reload: bool, + debugger: bool, + with_threads: bool, + cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, + extra_files: list[str] | None, + exclude_patterns: list[str] | None, +) -> None: """Run a local development server. This server is for development purposes only. It does not provide @@ -901,7 +952,7 @@ def run_command( option. """ try: - app = info.load_app() + app: WSGIApplication = info.load_app() # pyright: ignore except Exception as e: if is_running_from_reloader(): # When reloading, print out the error immediately, but raise @@ -909,7 +960,9 @@ def run_command( traceback.print_exc() err = e - def app(environ, start_response): + def app( + environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: raise err from None else: @@ -940,6 +993,9 @@ def run_command( ) +run_command.params.insert(0, _debug_option) + + @click.command("shell", short_help="Run a shell in the app context.") @with_appcontext def shell_command() -> None: @@ -954,10 +1010,10 @@ def shell_command() -> None: banner = ( f"Python {sys.version} on {sys.platform}\n" - f"App: {current_app.import_name} [{current_app.env}]\n" + f"App: {current_app.import_name}\n" f"Instance: {current_app.instance_path}" ) - ctx: dict = {} + ctx: dict[str, t.Any] = {} # Support the regular Python interpreter startup script if someone # is using it. @@ -993,49 +1049,62 @@ def shell_command() -> None: @click.option( "--sort", "-s", - type=click.Choice(("endpoint", "methods", "rule", "match")), + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), default="endpoint", help=( - 'Method to sort routes by. "match" is the order that Flask will match ' - "routes when dispatching a request." + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." ), ) @click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") @with_appcontext def routes_command(sort: str, all_methods: bool) -> None: """Show all registered routes with endpoints and methods.""" - rules = list(current_app.url_map.iter_rules()) + if not rules: click.echo("No routes were registered.") return - ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] - if sort in ("endpoint", "rule"): - rules = sorted(rules, key=attrgetter(sort)) - elif sort == "methods": - rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] - rule_methods = [ - ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore - for rule in rules - ] + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") - headers = ("Endpoint", "Methods", "Rule") - widths = ( - max(len(rule.endpoint) for rule in rules), - max(len(methods) for methods in rule_methods), - max(len(rule.rule) for rule in rules), - ) - widths = [max(len(h), w) for h, w in zip(headers, widths)] - row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths) + row.append(rule.rule) + rows.append(row) - click.echo(row.format(*headers).strip()) - click.echo(row.format(*("-" * width for width in widths))) + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] - for rule, methods in zip(rules, rule_methods): - click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) + if has_domain: + headers.append("Host" if host_matching else "Subdomain") + sorts.append("domain") + + headers.append("Rule") + sorts.append("rule") + + try: + rows.sort(key=itemgetter(sorts.index(sort))) + except ValueError: + pass + + rows.insert(0, headers) + widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] + rows.insert(1, ["-" * w for w in widths]) + template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) + + for row in rows: + click.echo(template.format(*row)) cli = FlaskGroup( diff --git a/src/flask/config.py b/src/flask/config.py index 7b6a137a..34ef1a57 100644 --- a/src/flask/config.py +++ b/src/flask/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import errno import json import os @@ -6,27 +8,46 @@ import typing as t from werkzeug.utils import import_string +if t.TYPE_CHECKING: + import typing_extensions as te -class ConfigAttribute: + from .sansio.app import App + + +T = t.TypeVar("T") + + +class ConfigAttribute(t.Generic[T]): """Makes an attribute forward to the config""" - def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: + def __init__( + self, name: str, get_converter: t.Callable[[t.Any], T] | None = None + ) -> None: self.__name__ = name self.get_converter = get_converter - def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: + @t.overload + def __get__(self, obj: None, owner: None) -> te.Self: ... + + @t.overload + def __get__(self, obj: App, owner: type[App]) -> T: ... + + def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: if obj is None: return self + rv = obj.config[self.__name__] + if self.get_converter is not None: rv = self.get_converter(rv) - return rv - def __set__(self, obj: t.Any, value: t.Any) -> None: + return rv # type: ignore[no-any-return] + + def __set__(self, obj: App, value: t.Any) -> None: obj.config[self.__name__] = value -class Config(dict): +class Config(dict): # type: ignore[type-arg] """Works exactly like a dict but provides ways to fill it from files or special dictionaries. There are two common patterns to populate the config. @@ -70,7 +91,11 @@ class Config(dict): :param defaults: an optional dictionary of default values """ - def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: + def __init__( + self, + root_path: str | os.PathLike[str], + defaults: dict[str, t.Any] | None = None, + ) -> None: super().__init__(defaults or {}) self.root_path = root_path @@ -125,13 +150,13 @@ class Config(dict): .. versionadded:: 2.1 """ prefix = f"{prefix}_" - len_prefix = len(prefix) for key in sorted(os.environ): if not key.startswith(prefix): continue value = os.environ[key] + key = key.removeprefix(prefix) try: value = loads(value) @@ -139,9 +164,6 @@ class Config(dict): # Keep the value as a string if loading failed. pass - # Change to key.removeprefix(prefix) on Python >= 3.9. - key = key[len_prefix:] - if "__" not in key: # A non-nested key, set directly. self[key] = value @@ -162,7 +184,9 @@ class Config(dict): return True - def from_pyfile(self, filename: str, silent: bool = False) -> bool: + def from_pyfile( + self, filename: str | os.PathLike[str], silent: bool = False + ) -> bool: """Updates the values in the config from a Python file. This function behaves as if the file was imported as module with the :meth:`from_object` function. @@ -191,7 +215,7 @@ class Config(dict): self.from_object(d) return True - def from_object(self, obj: t.Union[object, str]) -> None: + def from_object(self, obj: object | str) -> None: """Updates the values from the given object. An object can be of one of the following two types: @@ -231,9 +255,10 @@ class Config(dict): def from_file( self, - filename: str, - load: t.Callable[[t.IO[t.Any]], t.Mapping], + filename: str | os.PathLike[str], + load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], silent: bool = False, + text: bool = True, ) -> bool: """Update the values in the config from a file that is loaded using the ``load`` parameter. The loaded data is passed to the @@ -244,8 +269,8 @@ class Config(dict): import json app.config.from_file("config.json", load=json.load) - import toml - app.config.from_file("config.toml", load=toml.load) + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) :param filename: The path to the data file. This can be an absolute path or relative to the config root path. @@ -254,14 +279,18 @@ class Config(dict): :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` implements a ``read`` method. :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. :return: ``True`` if the file was loaded successfully. + .. versionchanged:: 2.3 + The ``text`` parameter was added. + .. versionadded:: 2.0 """ filename = os.path.join(self.root_path, filename) try: - with open(filename) as f: + with open(filename, "r" if text else "rb") as f: obj = load(f) except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): @@ -273,15 +302,16 @@ class Config(dict): return self.from_mapping(obj) def from_mapping( - self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any ) -> bool: - """Updates the config like :meth:`update` ignoring items with non-upper - keys. + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + :return: Always returns ``True``. .. versionadded:: 0.11 """ - mappings: t.Dict[str, t.Any] = {} + mappings: dict[str, t.Any] = {} if mapping is not None: mappings.update(mapping) mappings.update(kwargs) @@ -292,7 +322,7 @@ class Config(dict): def get_namespace( self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> t.Dict[str, t.Any]: + ) -> dict[str, t.Any]: """Returns a dictionary containing a subset of configuration options that match the specified namespace/prefix. Example usage:: diff --git a/src/flask/ctx.py b/src/flask/ctx.py index ca284494..d4d0de65 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -1,18 +1,23 @@ +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 .globals import _cv_request +from .helpers import _CollectErrors from .signals import appcontext_popped from .signals import appcontext_pushed -if t.TYPE_CHECKING: # pragma: no cover +if t.TYPE_CHECKING: + import typing_extensions as te + from _typeshed.wsgi import WSGIEnvironment + from .app import Flask from .sessions import SessionMixin from .wrappers import Request @@ -27,7 +32,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 @@ -60,7 +65,7 @@ class _AppCtxGlobals: except KeyError: raise AttributeError(name) from None - def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: + def get(self, name: str, default: t.Any | None = None) -> t.Any: """Get an attribute by name, or a default value. Like :meth:`dict.get`. @@ -110,30 +115,30 @@ class _AppCtxGlobals: return object.__repr__(self) -def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: - """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. +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. - Example:: + .. code-block:: python - @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!' - 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. + return "Hello, World!" .. versionadded:: 0.9 """ - ctx = _cv_request.get(None) + ctx = _cv_app.get(None) - if ctx is None: + if ctx is None or not ctx.has_request: raise RuntimeError( "'after_this_request' can only be used when a request" " context is active, such as in a view function." @@ -143,14 +148,31 @@ def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable: return f -def copy_current_request_context(f: t.Callable) -> t.Callable: - """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. +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - Example:: + +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. + + .. warning:: + + Due to the following caveats, it is often safer (and simpler) to pass + the data you need when starting the task, rather than using this and + relying on the context objects. + + In order to avoid execution switching partially though reading data, either + read the request body (access ``form``, ``json``, ``data``, etc) before + starting the task, or use a lock. This can be an issue when using threading, + but shouldn't be an issue when using greenlet/gevent or asyncio. + + If the task will access ``session``, be sure to do so in the parent as well + so that the ``Vary: cookie`` header will be set. Modifying ``session`` in + the task should be avoided, as it may execute after the response cookie has + already been written. + + .. code-block:: python import gevent from flask import copy_current_request_context @@ -167,59 +189,68 @@ def copy_current_request_context(f: t.Callable) -> t.Callable: .. versionadded:: 0.10 """ - ctx = _cv_request.get(None) + # Store the context that was active when the decorator was applied. + original = _cv_app.get(None) - if ctx is None: + if original is None: raise RuntimeError( "'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, **kwargs): - with ctx: + 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) - return update_wrapper(wrapper, f) + return update_wrapper(wrapper, f) # type: ignore[return-value] def has_request_context() -> bool: - """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. + """Test if an app context is active and if it has request information. - :: + .. code-block:: python - class User(db.Model): + from flask import has_request_context, request - 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 has_request_context(): + remote_addr = request.remote_addr - Alternatively you can also just test any of the context bound objects - (such as :class:`request` or :class:`g`) for truthness:: + 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. - class User(db.Model): + .. code-block:: python - 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 + from flask import request + + if request: + remote_addr = request.remote_addr .. versionadded:: 0.7 """ - return _cv_request.get(None) is not None + return (ctx := _cv_app.get(None)) is not None and ctx.has_request def has_app_context() -> bool: - """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. + """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 = ... .. versionadded:: 0.9 """ @@ -227,212 +258,283 @@ def has_app_context() -> bool: class AppContext: - """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. - """ + """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. - def __init__(self, app: "Flask") -> None: - self.app = app - self.url_adapter = app.create_url_adapter(None) - self.g: _AppCtxGlobals = app.app_ctx_globals_class() - self._cv_tokens: t.List[contextvars.Token] = [] + 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. - def push(self) -> None: - """Binds the app context to the current context.""" - self._cv_tokens.append(_cv_app.set(self)) - appcontext_pushed.send(self.app) + 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`. - def pop(self, exc: t.Optional[BaseException] = _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()) + 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. - if ctx is not self: - raise AssertionError( - f"Popped wrong app context. ({ctx!r} instead of {self!r})" - ) + :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. - appcontext_popped.send(self.app) + .. versionchanged:: 3.2 + Merged with ``RequestContext``. The ``RequestContext`` alias will be + removed in Flask 4.0. - def __enter__(self) -> "AppContext": - self.push() - return self + .. 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. - def __exit__( - self, - exc_type: t.Optional[type], - exc_value: t.Optional[BaseException], - tb: t.Optional[TracebackType], - ) -> 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. + .. versionchanged:: 3.2 + The session is loaded the first time it is accessed, rather than when + the context is pushed. """ def __init__( self, - app: "Flask", - environ: dict, - request: t.Optional["Request"] = None, - session: t.Optional["SessionMixin"] = None, + app: Flask, + *, + 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 # type: ignore[misc] - self.request: Request = request - self.url_adapter = None + """The application represented by this context. Accessed through + :data:`.current_app`. + """ + + 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) + self.url_adapter = app.create_url_adapter(self._request) except HTTPException as e: - self.request.routing_exception = e - self.flashes: t.Optional[t.List[t.Tuple[str, str]]] = None - self.session: t.Optional["SessionMixin"] = 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: t.List[ft.AfterRequestCallable] = [] + if self._request is not None: + self._request.routing_exception = e - self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = [] + self._cv_token: contextvars.Token[AppContext] | None = None + """The previous state to restore when popping.""" - 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. + self._push_count: int = 0 + """Track nested pushes of this context. Cleanup will only run once the + original push has been popped. + """ - .. versionadded:: 0.10 + @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 object is used instead of reloading the original - data. This prevents `flask.session` pointing to an out-of-date object. + The current session data is used instead of reloading the original data. + + .. versionadded:: 0.10 """ return self.__class__( self.app, - environ=self.request.environ, - request=self.request, - session=self.session, + 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: - """Can be overridden by a subclass to hook into the matching - of the request. + """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 - self.request.url_rule, self.request.view_args = result # type: ignore + result = self.url_adapter.match(return_rule=True) # type: ignore[union-attr] except HTTPException as e: - self.request.routing_exception = e + self._request.routing_exception = e # type: ignore[union-attr] + else: + self._request.url_rule, self._request.view_args = result # type: ignore[union-attr] def push(self) -> None: - # Before we push the request context we have to ensure that there - # is an application context. - app_ctx = _cv_app.get(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. - 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 + Typically, this is not used directly. Instead, use a ``with`` block + to manage the context. - self._cv_tokens.append((_cv_request.set(self), app_ctx)) + In some situations, such as streaming or testing, the context may be + pushed multiple times. It will only trigger matching and signals if it + is not currently pushed. + """ + self._push_count += 1 - # 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._cv_token is not None: + return - if self.session is None: - self.session = session_interface.make_null_session(self.app) + self._cv_token = _cv_app.set(self) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) - # 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() + 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() - def pop(self, exc: t.Optional[BaseException] = _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. + # 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. + Added the ``exc`` argument. """ - clear_request = len(self._cv_tokens) == 1 + if self._cv_token is None: + raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.") - try: - if clear_request: - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_request(exc) + ctx = _cv_app.get(None) - 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) + if ctx is None or self._cv_token is None: + raise RuntimeError( + f"Cannot pop this context ({self!r}), there is no active context." + ) - # 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 ctx is not self: + raise RuntimeError( + f"Cannot pop this context ({self!r}), it is not the active" + f" context ({ctx!r})." + ) - if app_ctx is not None: - app_ctx.pop(exc) + self._push_count -= 1 - if ctx is not self: - raise AssertionError( - f"Popped wrong request context. ({ctx!r} instead of {self!r})" - ) + if self._push_count > 0: + return - def __enter__(self) -> "RequestContext": + collect_errors = _CollectErrors() + + if self._request is not None: + with collect_errors: + self.app.do_teardown_request(self, exc) + + with collect_errors: + self._request.close() + + with collect_errors: + self.app.do_teardown_appcontext(self, exc) + + _cv_app.reset(self._cv_token) + self._cv_token = None + + with collect_errors: + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + collect_errors.raise_any("Errors during context teardown") + + def __enter__(self) -> te.Self: self.push() return self def __exit__( self, - exc_type: t.Optional[type], - exc_value: t.Optional[BaseException], - tb: t.Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, ) -> None: self.pop(exc_value) def __repr__(self) -> str: - return ( - f"<{type(self).__name__} {self.request.url!r}" - f" [{self.request.method}] of {self.app.name}>" + 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 AppContext + + raise AttributeError(name) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b0639892..61884e1a 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -1,8 +1,17 @@ +from __future__ import annotations + import typing as t -from .app import Flask +from jinja2.loaders import BaseLoader +from werkzeug.routing import RequestRedirect + from .blueprints import Blueprint -from .globals import request_ctx +from .globals import _cv_app +from .sansio.app import App + +if t.TYPE_CHECKING: + from .sansio.scaffold import Scaffold + from .wrappers import Request class UnexpectedUnicodeError(AssertionError, UnicodeError): @@ -16,7 +25,7 @@ class DebugFilesKeyError(KeyError, AssertionError): provide a better error message than just a generic KeyError/BadRequest. """ - def __init__(self, request, key): + def __init__(self, request: Request, key: str) -> None: form_matches = request.form.getlist(key) buf = [ f"You tried to access the file {key!r} in the request.files" @@ -34,7 +43,7 @@ class DebugFilesKeyError(KeyError, AssertionError): ) self.msg = "".join(buf) - def __str__(self): + def __str__(self) -> str: return self.msg @@ -45,8 +54,9 @@ class FormDataRoutingRedirect(AssertionError): 307 or 308. """ - def __init__(self, request): + def __init__(self, request: Request) -> None: exc = request.routing_exception + assert isinstance(exc, RequestRedirect) buf = [ f"A request was sent to '{request.url}', but routing issued" f" a redirect to the canonical URL '{exc.new_url}'." @@ -68,7 +78,7 @@ class FormDataRoutingRedirect(AssertionError): super().__init__("".join(buf)) -def attach_enctype_error_multidict(request): +def attach_enctype_error_multidict(request: Request) -> None: """Patch ``request.files.__getitem__`` to raise a descriptive error about ``enctype=multipart/form-data``. @@ -77,8 +87,8 @@ def attach_enctype_error_multidict(request): """ oldcls = request.files.__class__ - class newcls(oldcls): - def __getitem__(self, key): + class newcls(oldcls): # type: ignore[valid-type, misc] + def __getitem__(self, key: str) -> t.Any: try: return super().__getitem__(key) except KeyError as e: @@ -94,7 +104,7 @@ def attach_enctype_error_multidict(request): request.files.__class__ = newcls -def _dump_loader_info(loader) -> t.Generator: +def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: yield f"class: {type(loader).__module__}.{type(loader).__name__}" for key, value in sorted(loader.__dict__.items()): if key.startswith("_"): @@ -111,16 +121,27 @@ def _dump_loader_info(loader) -> t.Generator: yield f"{key}: {value!r}" -def explain_template_loading_attempts(app: Flask, template, attempts) -> None: +def explain_template_loading_attempts( + app: App, + template: str, + attempts: list[ + tuple[ + BaseLoader, + Scaffold, + tuple[str, str | None, t.Callable[[], bool] | None] | None, + ] + ], +) -> None: """This should help developers understand what failed""" info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None - if request_ctx and request_ctx.request.blueprint is not None: - blueprint = request_ctx.request.blueprint + + if (ctx := _cv_app.get(None)) is not None and ctx.has_request: + blueprint = ctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): - if isinstance(srcobj, Flask): + if isinstance(srcobj, App): src_info = f"application {srcobj.import_name!r}" elif isinstance(srcobj, Blueprint): src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" diff --git a/src/flask/globals.py b/src/flask/globals.py index b230ef7e..f4a7298e 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing as t from contextvars import ContextVar @@ -7,101 +9,69 @@ 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 _FakeStack: - def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: - self.name = name - self.cv = cv + class ProxyMixin(t.Protocol[T]): + def _get_current_object(self) -> T: ... - def _warn(self): - import warnings + # 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): ... - warnings.warn( - f"'_{self.name}_ctx_stack' is deprecated and will be" - " removed in Flask 2.3. Use 'g' to store data, or" - f" '{self.name}_ctx' to access the current context.", - DeprecationWarning, - stacklevel=3, - ) + class AppContextProxy(ProxyMixin[AppContext], AppContext): ... - def push(self, obj: t.Any) -> None: - self._warn() - self.cv.set(obj) + class _AppCtxGlobalsProxy(ProxyMixin[_AppCtxGlobals], _AppCtxGlobals): ... - def pop(self) -> t.Any: - self._warn() - ctx = self.cv.get(None) - self.cv.set(None) - return ctx + class RequestProxy(ProxyMixin[Request], Request): ... - @property - def top(self) -> t.Optional[t.Any]: - self._warn() - return self.cv.get(None) + class SessionMixinProxy(ProxyMixin[SessionMixin], SessionMixin): ... _no_app_msg = """\ Working outside of application context. -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.\ +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.\ """ -_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx") -__app_ctx_stack = _FakeStack("app", _cv_app) -app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment] +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment] _cv_app, unbound_message=_no_app_msg ) -current_app: "Flask" = LocalProxy( # type: ignore[assignment] +current_app: FlaskProxy = LocalProxy( # type: ignore[assignment] _cv_app, "app", unbound_message=_no_app_msg ) -g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment] +g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment] _cv_app, "g", unbound_message=_no_app_msg ) _no_req_msg = """\ 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.\ +Attempted to use functionality that expected an active HTTP request. See the +documentation on request context for more information.\ """ -_cv_request: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") -__request_ctx_stack = _FakeStack("request", _cv_request) -request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] - _cv_request, unbound_message=_no_req_msg +request: RequestProxy = LocalProxy( # type: ignore[assignment] + _cv_app, "request", 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 +session: SessionMixinProxy = LocalProxy( # type: ignore[assignment] + _cv_app, "session", unbound_message=_no_req_msg ) def __getattr__(name: str) -> t.Any: - if name == "_app_ctx_stack": - import warnings + import warnings + if name == "request_ctx": warnings.warn( - "'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + "'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_stack - - if name == "_request_ctx_stack": - import warnings - - warnings.warn( - "'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", - DeprecationWarning, - stacklevel=2, - ) - return __request_ctx_stack + return app_ctx raise AttributeError(name) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 15990d0e..fb7f6eba 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -1,47 +1,28 @@ +from __future__ import annotations + +import importlib.util import os -import pkgutil -import socket import sys import typing as t from datetime import datetime -from functools import lru_cache +from functools import cache from functools import update_wrapper -from threading import RLock +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_request +from .globals import _cv_app +from .globals import app_ctx from .globals import current_app from .globals import request -from .globals import request_ctx from .globals import session from .signals import message_flashed if t.TYPE_CHECKING: # pragma: no cover - from werkzeug.wrappers import Response as BaseResponse from .wrappers import Response - import typing_extensions as te - - -def get_env() -> str: - """Get the environment the app is running in, indicated by the - :envvar:`FLASK_ENV` environment variable. The default is - ``'production'``. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - """ - import warnings - - warnings.warn( - "'FLASK_ENV' and 'get_env' are deprecated and will be removed" - " in Flask 2.3. Use 'FLASK_DEBUG' instead.", - DeprecationWarning, - stacklevel=2, - ) - return os.environ.get("FLASK_ENV") or "production" def get_debug_flag() -> bool: @@ -49,21 +30,7 @@ def get_debug_flag() -> bool: :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. """ val = os.environ.get("FLASK_DEBUG") - - if not val: - env = os.environ.get("FLASK_ENV") - - if env is not None: - print( - "'FLASK_ENV' is deprecated and will not be used in" - " Flask 2.3. Use 'FLASK_DEBUG' instead.", - file=sys.stderr, - ) - return env == "development" - - return False - - return val.lower() not in {"0", "false", "no"} + return bool(val and val.lower() not in {"0", "false", "no"}) def get_load_dotenv(default: bool = True) -> bool: @@ -81,86 +48,107 @@ def get_load_dotenv(default: bool = True) -> bool: return val.lower() in ("0", "false", "no") +@t.overload def stream_with_context( - generator_or_function: t.Union[ - t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] - ] -) -> t.Iterator[t.AnyStr]: - """Request contexts disappear when the response is started on the server. - 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. + generator_or_function: t.Iterator[t.AnyStr], +) -> t.Iterator[t.AnyStr]: ... - This function however can help you keep the context around for longer:: + +@t.overload +def stream_with_context( + generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... + + +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. + + .. warning:: + + Due to the following caveat, it is often safer to pass the data you + need as arguments to the generator, rather than relying on the context + objects. + + More headers cannot be sent after the body has begun. Therefore, you must + make sure all headers are set before starting the response. In particular, + if the generator will access ``session``, be sure to do so in the view as + well so that the ``Vary: cookie`` header will be set. Do not modify the + session in the generator, as the ``Set-Cookie`` header will already be sent. + + Use it as a decorator on a generator function: + + .. code-block:: python from flask import stream_with_context, request, Response - @app.route('/stream') + @app.get("/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()) - Alternatively it can also be used around a specific generator:: + Or use it as a wrapper around a created generator: + + .. code-block:: python from flask import stream_with_context, request, Response - @app.route('/stream') + @app.get("/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 """ try: - gen = iter(generator_or_function) # type: ignore + gen = iter(generator_or_function) # type: ignore[arg-type] except TypeError: def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: - gen = generator_or_function(*args, **kwargs) # type: ignore + gen = generator_or_function(*args, **kwargs) # type: ignore[operator] return stream_with_context(gen) - return update_wrapper(decorator, generator_or_function) # type: ignore + return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] - def generator() -> t.Generator: - ctx = _cv_request.get(None) - if ctx is None: + def generator() -> t.Iterator[t.AnyStr]: + if (ctx := _cv_app.get(None)) 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: - # 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. + with ctx: + yield None # type: ignore[misc] + try: yield from gen finally: + # Clean up in case the user wrapped a WSGI iterator. if hasattr(gen, "close"): - gen.close() # type: ignore + gen.close() - # 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. + # 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. wrapped_g = generator() next(wrapped_g) return wrapped_g -def make_response(*args: t.Any) -> "Response": +def make_response(*args: t.Any) -> Response: """Sometimes it is necessary to set additional headers in a view. Because views do not have to return response objects but can return a value that is converted into a response object by Flask itself, it becomes tricky to @@ -206,16 +194,16 @@ def make_response(*args: t.Any) -> "Response": return current_app.response_class() if len(args) == 1: args = args[0] - return current_app.make_response(args) # type: ignore + return current_app.make_response(args) def url_for( endpoint: str, *, - _anchor: t.Optional[str] = None, - _method: t.Optional[str] = None, - _scheme: t.Optional[str] = None, - _external: t.Optional[bool] = None, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -264,8 +252,8 @@ def url_for( def redirect( - location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None -) -> "BaseResponse": + location: str, code: int = 303, Response: type[BaseResponse] | None = None +) -> BaseResponse: """Create a redirect response object. If :data:`~flask.current_app` is available, it will use its @@ -277,19 +265,20 @@ 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 current_app: - return current_app.redirect(location, code=code) + if (ctx := _cv_app.get(None)) is not None: + return ctx.app.redirect(location, code=code) return _wz_redirect(location, code=code, Response=Response) -def abort( # type: ignore[misc] - code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any -) -> "te.NoReturn": +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given status code. @@ -306,8 +295,8 @@ def abort( # type: ignore[misc] Calls ``current_app.aborter`` if available instead of always using Werkzeug's default ``abort``. """ - if current_app: - current_app.aborter(code, *args, **kwargs) + if (ctx := _cv_app.get(None)) is not None: + ctx.app.aborter(code, *args, **kwargs) _wz_abort(code, *args, **kwargs) @@ -359,8 +348,10 @@ 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() message_flashed.send( - current_app._get_current_object(), # type: ignore + app, + _async_wrapper=app.ensure_sync, message=message, category=category, ) @@ -368,7 +359,7 @@ def flash(message: str, category: str = "message") -> None: def get_flashed_messages( with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]: +) -> list[str] | list[tuple[str, str]]: """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return the same messages. By default just the messages are returned, @@ -397,10 +388,10 @@ def get_flashed_messages( :param category_filter: filter of categories to limit return values. Only categories in the list will be returned. """ - flashes = request_ctx.flashes + flashes = app_ctx._flashes if flashes is None: flashes = session.pop("_flashes") if "_flashes" in session else [] - request_ctx.flashes = flashes + app_ctx._flashes = flashes if category_filter: flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: @@ -408,31 +399,31 @@ def get_flashed_messages( return flashes -def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]: +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: + ctx = app_ctx._get_current_object() + if kwargs.get("max_age") is None: - kwargs["max_age"] = current_app.get_send_file_max_age + kwargs["max_age"] = ctx.app.get_send_file_max_age kwargs.update( - 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 + 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, ) return kwargs def send_file( - path_or_file: t.Union[os.PathLike, str, t.BinaryIO], - mimetype: t.Optional[str] = None, + path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes], + mimetype: str | None = None, as_attachment: bool = False, - download_name: t.Optional[str] = None, + download_name: str | None = None, conditional: bool = True, - etag: t.Union[bool, str] = True, - last_modified: t.Optional[t.Union[datetime, int, float]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, -) -> "Response": + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths @@ -525,7 +516,7 @@ def send_file( .. versionchanged:: 0.7 MIME guessing and etag support for file-like objects was - deprecated because it was unreliable. Pass a filename if you are + removed because it was unreliable. Pass a filename if you are able to, otherwise attach an etag yourself. .. versionchanged:: 0.5 @@ -550,10 +541,10 @@ def send_file( def send_from_directory( - directory: t.Union[os.PathLike, str], - path: t.Union[os.PathLike, str], + directory: os.PathLike[str] | str, + path: os.PathLike[str] | str, **kwargs: t.Any, -) -> "Response": +) -> Response: """Send a file from within a directory using :func:`send_file`. .. code-block:: python @@ -573,7 +564,8 @@ def send_from_directory( raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. :param directory: The directory that ``path`` must be located under, - relative to the current application's root path. + relative to the current application's root path. This *must not* + be a value provided by the client, otherwise it becomes insecure. :param path: The path to the file to send, relative to ``directory``. :param kwargs: Arguments to pass to :func:`send_file`. @@ -608,16 +600,24 @@ def get_root_path(import_name: str) -> str: return os.path.dirname(os.path.abspath(mod.__file__)) # Next attempt: check the loader. - loader = pkgutil.get_loader(import_name) + try: + spec = importlib.util.find_spec(import_name) + + if spec is None: + raise ValueError + except (ImportError, ValueError): + loader = None + else: + loader = spec.loader # Loader does not exist or we're referring to an unloaded main # module or a main module without path (interactive sessions), go # with the current working directory. - if loader is None or import_name == "__main__": + if loader is None: return os.getcwd() if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) # type: ignore + filepath = loader.get_filename(import_name) # pyright: ignore else: # Fall back to imports. __import__(import_name) @@ -638,68 +638,45 @@ def get_root_path(import_name: str) -> str: ) # filepath is import_name.py for a module, or __init__.py for a package. - return os.path.dirname(os.path.abspath(filepath)) + return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] -class locked_cached_property(werkzeug.utils.cached_property): - """A :func:`property` that is only evaluated once. Like - :class:`werkzeug.utils.cached_property` except access uses a lock - for thread safety. - - .. versionchanged:: 2.0 - Inherits from Werkzeug's ``cached_property`` (and ``property``). - """ - - def __init__( - self, - fget: t.Callable[[t.Any], t.Any], - name: t.Optional[str] = None, - doc: t.Optional[str] = None, - ) -> None: - super().__init__(fget, name=name, doc=doc) - self.lock = RLock() - - def __get__(self, obj: object, type: type = None) -> t.Any: # type: ignore - if obj is None: - return self - - with self.lock: - return super().__get__(obj, type=type) - - def __set__(self, obj: object, value: t.Any) -> None: - with self.lock: - super().__set__(obj, value) - - def __delete__(self, obj: object) -> None: - with self.lock: - super().__delete__(obj) - - -def is_ip(value: str) -> bool: - """Determine if the given string is an IP address. - - :param value: value to check - :type value: str - - :return: True if string is an IP address - :rtype: bool - """ - for family in (socket.AF_INET, socket.AF_INET6): - try: - socket.inet_pton(family, value) - except OSError: - pass - else: - return True - - return False - - -@lru_cache(maxsize=None) -def _split_blueprint_path(name: str) -> t.List[str]: - out: t.List[str] = [name] +@cache +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] if "." in name: 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 65d8829a..742812f2 100644 --- a/src/flask/json/__init__.py +++ b/src/flask/json/__init__.py @@ -3,85 +3,14 @@ from __future__ import annotations import json as _json import typing as t -from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps - from ..globals import current_app from .provider import _default if t.TYPE_CHECKING: # pragma: no cover - from ..app import Flask from ..wrappers import Response -class JSONEncoder(_json.JSONEncoder): - """The default JSON encoder. Handles extra types compared to the - built-in :class:`json.JSONEncoder`. - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`decimal.Decimal` is serialized to a string. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - - Assign a subclass of this to :attr:`flask.Flask.json_encoder` or - :attr:`flask.Blueprint.json_encoder` to override the default. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.json`` instead. - """ - - def __init__(self, **kwargs) -> None: - import warnings - - warnings.warn( - "'JSONEncoder' is deprecated and will be removed in" - " Flask 2.3. Use 'Flask.json' to provide an alternate" - " JSON implementation instead.", - DeprecationWarning, - stacklevel=3, - ) - super().__init__(**kwargs) - - def default(self, o: t.Any) -> t.Any: - """Convert ``o`` to a JSON serializable type. See - :meth:`json.JSONEncoder.default`. Python does not support - overriding how basic types like ``str`` or ``list`` are - serialized, they are handled before this method. - """ - return _default(o) - - -class JSONDecoder(_json.JSONDecoder): - """The default JSON decoder. - - This does not change any behavior from the built-in - :class:`json.JSONDecoder`. - - Assign a subclass of this to :attr:`flask.Flask.json_decoder` or - :attr:`flask.Blueprint.json_decoder` to override the default. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.json`` instead. - """ - - def __init__(self, **kwargs) -> None: - import warnings - - warnings.warn( - "'JSONDecoder' is deprecated and will be removed in" - " Flask 2.3. Use 'Flask.json' to provide an alternate" - " JSON implementation instead.", - DeprecationWarning, - stacklevel=3, - ) - super().__init__(**kwargs) - - -def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: +def dumps(obj: t.Any, **kwargs: t.Any) -> str: """Serialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -91,13 +20,13 @@ def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: :param obj: The data to serialize. :param kwargs: Arguments passed to the ``dumps`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.dumps``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0.2 :class:`decimal.Decimal` is supported by converting to a string. @@ -108,28 +37,14 @@ def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.dumps' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.dumps(obj, **kwargs) + if current_app: + return current_app.json.dumps(obj, **kwargs) kwargs.setdefault("default", _default) return _json.dumps(obj, **kwargs) -def dump( - obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any -) -> None: +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: """Serialize data as JSON and write to a file. If :data:`~flask.current_app` is available, it will use its @@ -141,37 +56,25 @@ def dump( encoding to be valid JSON. :param kwargs: Arguments passed to the ``dump`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.dump``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0 Writing to a binary file, and the ``encoding`` argument, will be removed in Flask 2.1. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.dump' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - app.json.dump(obj, fp, **kwargs) + if current_app: + current_app.json.dump(obj, fp, **kwargs) else: kwargs.setdefault("default", _default) _json.dump(obj, fp, **kwargs) -def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -181,13 +84,13 @@ def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any :param s: Text or UTF-8 bytes. :param kwargs: Arguments passed to the ``loads`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.loads``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0 ``encoding`` will be removed in Flask 2.1. The data must be a string or UTF-8 bytes. @@ -196,25 +99,13 @@ def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.loads' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.loads(s, **kwargs) + if current_app: + return current_app.json.loads(s, **kwargs) return _json.loads(s, **kwargs) -def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: """Deserialize data as JSON read from a file. If :data:`~flask.current_app` is available, it will use its @@ -224,6 +115,9 @@ def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t. :param fp: A file opened for reading text or UTF-8 bytes. :param kwargs: Arguments passed to the ``load`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.load``, allowing an app to override the behavior. @@ -235,85 +129,19 @@ def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t. ``encoding`` will be removed in Flask 2.1. The file must be text mode, or binary mode with UTF-8 bytes. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.load' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.load(fp, **kwargs) + if current_app: + return current_app.json.load(fp, **kwargs) return _json.load(fp, **kwargs) -def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str: - """Serialize an object to a string of JSON with :func:`dumps`, then - replace HTML-unsafe characters with Unicode escapes and mark the - result safe with :class:`~markupsafe.Markup`. - - This is available in templates as the ``|tojson`` filter. - - The returned string is safe to render in HTML documents and - ``