diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 45198266..00000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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 deleted file mode 100755 index eaebea61..00000000 --- a/.devcontainer/on-create-command.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/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 2ff985a6..e32c8029 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,5 @@ end_of_line = lf charset = utf-8 max_line_length = 88 -[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] +[*.{yml,yaml,json,js,css,html}] indent_size = 2 diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..8f3b4fd4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,25 @@ +[flake8] +extend-select = + # bugbear + B + # bugbear opinions + B9 + # implicit str concat + ISC +extend-ignore = + # slice notation whitespace, invalid + E203 + # line length, handled by bugbear B950 + E501 + # bare except, handled by bugbear B001 + E722 + # zip with strict=, requires python >= 3.10 + B905 + # string formatting opinion, B028 renamed to B907 + B028 + B907 +# up to 88 allowed by bugbear B950 +max-line-length = 80 +per-file-ignores = + # __init__ exports names + src/flask/__init__.py: F401 diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 0917c797..c2a15eee 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 a8f9f0b7..abe39156 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: 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: 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://discord.gg/pallets - about: Ask questions about your own code on our Discord chat. + about: Discuss questions about your code on our Discord chat. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..fcfac71b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,19 @@ +# 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 new file mode 100644 index 00000000..90f94bc3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +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 eb124d25..29fd35f8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,6 @@ +- 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 533151a8..c790fae5 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,26 +1,25 @@ -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. +name: 'Lock threads' +# Lock closed issues that have not received any further activity for +# two weeks. This does not close open issues, only humans may do that. +# We find 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: {} + +permissions: + issues: write + pull-requests: write + concurrency: group: lock - cancel-in-progress: true + jobs: lock: runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - discussions: write steps: - - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 + - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836 with: 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 deleted file mode 100644 index eff10995..00000000 --- a/.github/workflows/pre-commit.yaml +++ /dev/null @@ -1,29 +0,0 @@ -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 index 0c4f301a..0ed49559 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,62 +1,72 @@ name: Publish on: push: - tags: ['*'] -permissions: {} -concurrency: - group: publish-${{ github.event.push.ref }} - cancel-in-progress: true + tags: + - '*' jobs: build: runs-on: ubuntu-latest outputs: - artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} + hash: ${{ steps.hash.outputs.hash }} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: false - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version-file: pyproject.toml + python-version: '3.x' + cache: 'pip' + cache-dependency-path: 'requirements/*.txt' + - run: pip install -r requirements/build.txt + # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - - run: uv build - - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - id: upload-artifact + - run: python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce with: - name: dist - path: dist/ - if-no-files-found: error + path: ./dist + provenance: + needs: ['build'] + permissions: + actions: read + id-token: write + contents: write + # Can't pin with hash due to how this workflow works. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.4.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} create-release: - needs: [build] + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: ['provenance'] runs-on: ubuntu-latest permissions: contents: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - artifact-ids: ${{ needs.build.outputs.artifact-id }} - path: dist/ + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a - name: create release - run: gh release create --draft --repo ${GITHUB_REPOSITORY} ${GITHUB_REF_NAME} dist/* + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* artifact/* env: GH_TOKEN: ${{ github.token }} publish-pypi: - needs: [build] - environment: - name: publish - url: https://pypi.org/project/Flask/${{ github.ref_name }} + needs: ['provenance'] + # Wait for approval before attempting to upload to PyPI. This allows reviewing the + # files in the draft release. + environment: 'publish' runs-on: ubuntu-latest - permissions: - id-token: write steps: - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + # Try uploading to Test PyPI first, in case something fails. + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: - artifact-ids: ${{ needs.build.outputs.artifact-id }} - path: dist/ - - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + packages_dir: artifact/ + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc with: - packages-dir: "dist/" + password: ${{ secrets.PYPI_TOKEN }} + packages_dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 97064c8c..c2482fd6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,63 +1,58 @@ name: Tests on: - pull_request: - paths-ignore: ['docs/**', 'README.md'] push: - branches: [main, stable] - paths-ignore: ['docs/**', 'README.md'] -permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + branches: + - main + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' + pull_request: + branches: + - main + - '*.x' + paths-ignore: + - 'docs/**' + - '*.md' + - '*.rst' jobs: tests: - name: ${{ matrix.name || matrix.python }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - - {python: '3.14'} - - {python: '3.14t'} - - {name: Windows, python: '3.14', os: windows-latest} - - {name: Mac, python: '3.14', os: macos-latest} - - {python: '3.13'} - - {python: '3.12'} - - {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} + - {name: Linux, python: '3.11', os: ubuntu-latest, tox: py311} + - {name: Windows, python: '3.11', os: windows-latest, tox: py311} + - {name: Mac, python: '3.11', os: macos-latest, tox: py311} + - {name: '3.12-dev', python: '3.12-dev', os: ubuntu-latest, tox: py312} + - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} + - {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.9', os: ubuntu-latest, tox: pypy39} + - {name: 'Pallets Minimum Versions', python: '3.11', os: ubuntu-latest, tox: py311-min} + - {name: 'Pallets Development Versions', python: '3.8', os: ubuntu-latest, tox: py38-dev} + - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - with: - enable-cache: true - prune-cache: false - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: ${{ matrix.python }} - - run: uv run --locked --no-default-groups --group dev tox run - env: - TOX_ENV: ${{ matrix.tox || format('py{0}', matrix.python) }} - typing: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 + 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 - name: cache mypy - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 with: path: ./.mypy_cache - key: mypy|${{ hashFiles('pyproject.toml') }} - - run: uv run --locked --no-default-groups --group dev tox run -e typing + key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }} + if: matrix.tox == 'typing' + - run: pip install tox + - run: tox run -e ${{ matrix.tox }} diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml deleted file mode 100644 index 04082427..00000000 --- a/.github/workflows/zizmor.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: GitHub Actions security analysis with zizmor -on: - pull_request: - paths: ["**/*.yaml?"] - push: - branches: [main, stable] - paths: ["**/*.yaml?"] -permissions: {} -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true -jobs: - zizmor: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 - with: - advanced-security: false - annotations: true diff --git a/.gitignore b/.gitignore index 8441e5a6..e6713351 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,25 @@ -.idea/ -.vscode/ -__pycache__/ +.DS_Store +.env +.flaskenv +*.pyc +*.pyo +env/ +venv/ +.venv/ +env* dist/ -.coverage* -htmlcov/ +build/ +*.egg +*.egg-info/ .tox/ +.cache/ +.pytest_cache/ +.idea/ docs/_build/ +.vscode + +# Coverage reports +htmlcov/ +.coverage +.coverage.* +*,cover diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d1c89cb..d2b6324d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,23 +1,37 @@ +ci: + autoupdate_branch: "2.1.x" + autoupdate_schedule: monthly repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 5e2fb545eba1ea9dc051f6f962d52fe8f76a9794 # frozen: v0.15.13 + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.2 hooks: - - id: ruff-check - - id: ruff-format - - repo: https://github.com/astral-sh/uv-pre-commit - rev: fa60a193803535a9e2accdb3ca4b1b584b1150cb # frozen: 0.11.15 + - id: pyupgrade + args: ["--py37-plus"] + - repo: https://github.com/asottile/reorder_python_imports + rev: v3.9.0 hooks: - - id: uv-lock - - repo: https://github.com/codespell-project/codespell - rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2 + - id: reorder-python-imports + name: Reorder Python imports (src, tests) + files: "^(?!examples/)" + args: ["--application-directories", "src"] + - repo: https://github.com/psf/black + rev: 23.3.0 hooks: - - id: codespell - args: ['--write-changes'] + - id: black + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-implicit-str-concat + - repo: https://github.com/peterdemin/pip-compile-multi + rev: v2.6.2 + hooks: + - id: pip-compile-multi-verify - repo: https://github.com/pre-commit/pre-commit-hooks - rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 + rev: v4.4.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 acbd83f9..346900b2 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,10 +1,13 @@ version: 2 build: - os: ubuntu-24.04 + os: ubuntu-20.04 tools: - python: '3.13' - commands: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html + python: "3.10" +python: + install: + - requirements: requirements/docs.txt + - method: pip + path: . +sphinx: + builder: dirhtml + fail_on_warning: true diff --git a/CHANGES.rst b/CHANGES.rst index 232b144a..71493abf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,234 +1,3 @@ -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 ------------- @@ -458,7 +227,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 ``pkg_resources`` to +- The CLI uses ``importlib.metadata`` instead of ``setuptools`` to load command entry points. :issue:`4419` - Overriding ``FlaskClient.open`` will not cause an error on redirect. :issue:`3396` @@ -935,7 +704,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 if +- Template auto reloading will honor debug mode even even if ``Flask.jinja_env`` was already accessed. :pr:`2373` - The following old deprecated code was removed. :issue:`2385` @@ -1417,7 +1186,7 @@ Released 2011-09-29, codename Rakija of Flask itself and no longer of the test client. This cleaned up some internal logic and lowers the odds of runaway request contexts in unittests. -- Fixed the Jinja environment's ``list_templates`` method not +- Fixed the Jinja2 environment's ``list_templates`` method not returning the correct names when blueprints or modules were involved. @@ -1503,7 +1272,7 @@ Released 2010-12-31 - Fixed an issue where the default ``OPTIONS`` response was not exposing all valid methods in the ``Allow`` header. -- Jinja template loading syntax now allows "./" in front of a +- Jinja2 template loading syntax now allows "./" in front of a template load path. Previously this caused issues with module setups. - Fixed an issue where the subdomain setting for modules was ignored diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f4ba197d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# 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 new file mode 100644 index 00000000..8d209048 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,227 @@ +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://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.txt b/LICENSE.rst similarity index 100% rename from LICENSE.txt rename to LICENSE.rst diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..65a97749 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +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 deleted file mode 100644 index 64f56cac..00000000 --- a/README.md +++ /dev/null @@ -1,53 +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/ - -## 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 new file mode 100644 index 00000000..3d1c3882 --- /dev/null +++ b/README.rst @@ -0,0 +1,82 @@ +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 new file mode 100644 index 00000000..99c58a21 --- /dev/null +++ b/artwork/LICENSE.rst @@ -0,0 +1,19 @@ +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 new file mode 100644 index 00000000..8c0748a2 --- /dev/null +++ b/artwork/logo-full.svg @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/artwork/logo-lineart.svg b/artwork/logo-lineart.svg new file mode 100644 index 00000000..615260dc --- /dev/null +++ b/artwork/logo-lineart.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/docs/_static/flask-icon.png b/docs/_static/flask-icon.png new file mode 100644 index 00000000..55cb8478 Binary files /dev/null and b/docs/_static/flask-icon.png differ diff --git a/docs/_static/flask-icon.svg b/docs/_static/flask-icon.svg deleted file mode 100644 index c802da9a..00000000 --- a/docs/_static/flask-icon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/_static/flask-logo.png b/docs/_static/flask-logo.png new file mode 100644 index 00000000..ce236061 Binary files /dev/null and b/docs/_static/flask-logo.png differ diff --git a/docs/_static/flask-logo.svg b/docs/_static/flask-logo.svg deleted file mode 100644 index c216b617..00000000 --- a/docs/_static/flask-logo.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/docs/_static/flask-name.svg b/docs/_static/flask-name.svg deleted file mode 100644 index b46782d2..00000000 --- a/docs/_static/flask-name.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/docs/_static/no.png b/docs/_static/no.png new file mode 100644 index 00000000..644c3f70 Binary files /dev/null and b/docs/_static/no.png differ diff --git a/docs/_static/yes.png b/docs/_static/yes.png new file mode 100644 index 00000000..56917ab2 Binary files /dev/null and b/docs/_static/yes.png differ diff --git a/docs/api.rst b/docs/api.rst index 52b25376..afbe0b79 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -31,15 +31,17 @@ Incoming Request Data :inherited-members: :exclude-members: json_module -.. data:: request +.. attribute:: request - A proxy to the request data for the current request, an instance of - :class:`.Request`. + To access incoming request data, you can use the global `request` + object. Flask parses incoming request data for you and gives you + access to it through that global object. Internally Flask makes + sure that you always get the correct data for the active thread if you + are in a multithreaded environment. - This is only available when a :doc:`request context ` is - active. + This is a proxy. See :ref:`notes-on-proxies` for more information. - This is a proxy. See :ref:`context-visibility` for more information. + The request object is an instance of a :class:`~flask.Request`. Response Objects @@ -60,33 +62,40 @@ does this is by using a signed cookie. The user can look at the session contents, but can't modify it unless they know the secret key, so make sure to set that to something complex and unguessable. -To access the current session you can use the :data:`.session` proxy. +To access the current session you can use the :class:`session` object: -.. data:: session +.. class:: session - A proxy to the session data for the current request, an instance of - :class:`.SessionMixin`. + The session object works pretty much like an ordinary dict, with the + difference that it keeps track of modifications. - This is only available when a :doc:`request context ` is - active. + This is a proxy. See :ref:`notes-on-proxies` for more information. - This is a proxy. See :ref:`context-visibility` for more information. + The following attributes are interesting: - The session object works like a dict but tracks assignment and access to its - keys. It cannot track modifications to mutable values, you need to set - :attr:`~.SessionMixin.modified` manually when modifying a list, dict, etc. + .. attribute:: new - .. code-block:: python + ``True`` if the session is new, ``False`` otherwise. - # appending to a list is not detected - session["numbers"].append(42) + .. attribute:: modified + + ``True`` if the session object detected a modification. Be advised + that modifications on mutable structures are not picked up + automatically, in that situation you have to explicitly set the + attribute to ``True`` yourself. Here an example:: + + # this change is not picked up because a mutable object (here + # a list) is changed. + session['objects'].append(42) # so mark it as modified yourself session.modified = True - The session is persisted across requests using a cookie. By default the - users's browser will clear the cookie when it is closed. Set - :attr:`~.SessionMixin.permanent` to ``True`` to persist the cookie for - :data:`PERMANENT_SESSION_LIFETIME`. + .. attribute:: permanent + + If set to ``True`` the session lives for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to ``False`` (which is the default) the + session will be deleted when the user closes the browser. Session Interface @@ -149,21 +158,20 @@ another, a global variable is not good enough because it would break in threaded environments. Flask provides you with a special object that ensures it is only valid for the active request and that will return different values for each request. In a nutshell: it does the right -thing, like it does for :data:`.request` and :data:`.session`. +thing, like it does for :class:`request` and :class:`session`. .. data:: g - A proxy to a namespace object used to store data during a single request or - app context. An instance of :attr:`.Flask.app_ctx_globals_class`, which - defaults to :class:`._AppCtxGlobals`. + A namespace object that can store data during an + :doc:`application context `. This is an instance of + :attr:`Flask.app_ctx_globals_class`, which defaults to + :class:`ctx._AppCtxGlobals`. - This is a good place to store resources during a request. For example, a - :meth:`~.Flask.before_request` function could load a user object from a - session id, then set ``g.user`` to be used in the view function. + This is a good place to store resources during a request. For + example, a ``before_request`` function could load a user object from + a session id, then set ``g.user`` to be used in the view function. - This is only available when an :doc:`app context ` is active. - - This is a proxy. See :ref:`context-visibility` for more information. + This is a proxy. See :ref:`notes-on-proxies` for more information. .. versionchanged:: 0.10 Bound to the application context instead of the request context. @@ -177,16 +185,17 @@ Useful Functions and Classes .. data:: current_app - A proxy to the :class:`.Flask` application handling the current request or - other activity. + A proxy to the application handling the current request. This is + useful to access the application without needing to import it, or if + it can't be imported, such as when using the application factory + pattern or in blueprints and extensions. - This is useful to access the application without needing to import it, or if - it can't be imported, such as when using the application factory pattern or - in blueprints and extensions. + This is only available when an + :doc:`application context ` is pushed. This happens + automatically during requests and CLI commands. It can be controlled + manually with :meth:`~flask.Flask.app_context`. - This is only available when an :doc:`app context ` is active. - - This is a proxy. See :ref:`context-visibility` for more information. + This is a proxy. See :ref:`notes-on-proxies` for more information. .. autofunction:: has_request_context @@ -208,6 +217,10 @@ Useful Functions and Classes .. autofunction:: send_from_directory +.. autofunction:: escape + +.. autoclass:: Markup + :members: escape, unescape, striptags Message Flashing ---------------- @@ -257,6 +270,12 @@ HTML `` @@ -245,9 +244,8 @@ Receiving JSON in Views Use the :attr:`~flask.Request.json` property of the :data:`~flask.request` object to decode the request's body as JSON. If -the body is not valid JSON, a 400 Bad Request error will be raised. If -the ``Content-Type`` header is not set to ``application/json``, a 415 -Unsupported Media Type error will be raised. +the body is not valid JSON, or the ``Content-Type`` header is not set to +``application/json``, a 400 Bad Request error will be raised. .. code-block:: python diff --git a/docs/patterns/mongoengine.rst b/docs/patterns/mongoengine.rst index 8d49de7c..015e7b61 100644 --- a/docs/patterns/mongoengine.rst +++ b/docs/patterns/mongoengine.rst @@ -10,7 +10,8 @@ A running MongoDB server and `Flask-MongoEngine`_ are required. :: pip install flask-mongoengine .. _MongoEngine: http://mongoengine.org -.. _Flask-MongoEngine: https://docs.mongoengine.org/projects/flask-mongoengine/en/latest/ +.. _Flask-MongoEngine: https://flask-mongoengine.readthedocs.io + Configuration ------------- @@ -79,7 +80,7 @@ Queries Use the class ``objects`` attribute to make queries. A keyword argument looks for an equal value on the field. :: - bttf = Movie.objects(title="Back To The Future").get_or_404() + bttf = Movies.objects(title="Back To The Future").get_or_404() Query operators may be used by concatenating them with the field name using a double-underscore. ``objects``, and queries returned by diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 90fa8a8f..3c7ae425 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -42,20 +42,19 @@ 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:`pyproject.toml` next to the inner -:file:`yourapplication` folder with the following contents: +a big problem, just add a new file called :file:`setup.py` next to the inner +:file:`yourapplication` folder with the following contents:: -.. code-block:: toml + from setuptools import setup - [project] - name = "yourapplication" - dependencies = [ - "flask", - ] - - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" + setup( + name='yourapplication', + packages=['yourapplication'], + include_package_data=True, + install_requires=[ + 'flask', + ], + ) Install your application so it is importable: @@ -101,7 +100,7 @@ And this is what :file:`views.py` would look like:: You should then end up with something like that:: /yourapplication - pyproject.toml + setup.py /yourapplication __init__.py views.py diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 9e9afe48..734d550c 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -34,7 +34,8 @@ 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, declarative_base + from sqlalchemy.orm import scoped_session, sessionmaker + from sqlalchemy.ext.declarative import declarative_base engine = create_engine('sqlite:////tmp/test.db') db_session = scoped_session(sessionmaker(autocommit=False, @@ -131,8 +132,9 @@ Here is an example :file:`database.py` module for your application:: def init_db(): metadata.create_all(bind=engine) -As in the declarative approach, you need to close the session after each app -context. Put this into your application module:: +As in the declarative approach, you need to close the session after +each request or application context shutdown. Put this into your +application module:: from yourapplication.database import db_session diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index f42e0f8e..5932589f 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -1,9 +1,9 @@ Using SQLite 3 with Flask ========================= -You can implement a few functions to work with a SQLite connection during a -request context. The connection is created the first time it's accessed, -reused on subsequent access, until it is closed when the request context ends. +In Flask you can easily implement the opening of database connections on +demand and closing them when the context dies (usually at the end of the +request). Here is a simple example of how you can use SQLite 3 with Flask:: diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index fc2f1739..c9e6ef22 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -8,21 +8,6 @@ roundtrip to the filesystem? The answer is by using generators and direct responses. -HTTP Response Behavior ----------------------- - -**Headers cannot be changed after the streaming response starts.** - -When using streaming, it's important to be aware of the order than an HTTP -response is sent. All headers must be sent first, then the body. More headers -cannot be sent after the body has begun. Therefore, you must make sure all -headers are set before starting the response, outside the generator. - -In particular, if the generator will access ``session``, be sure to do so in the -view as well so that the ``Vary: cookie`` header will be set. Do not modify the -session in the generator, as the ``Set-Cookie`` header will already be sent. - - Basic Usage ----------- @@ -44,7 +29,7 @@ debug environments with profilers and other things you might have enabled. Streaming from Templates ------------------------ -The Jinja template engine supports rendering a template piece by +The Jinja2 template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. @@ -64,13 +49,13 @@ the template. Streaming with Context ---------------------- -The :data:`.request` proxy will not be active while the generator is -running, because the app has already returned control to the WSGI server at that -point. If you try to access ``request``, you'll get a ``RuntimeError``. +The :data:`~flask.request` will not be active while the generator is +running, because the view has already returned at that point. If you try +to access ``request``, you'll get a ``RuntimeError``. If your generator function relies on data in ``request``, use the -:func:`.stream_with_context` wrapper. This will keep the request context active -during the generator. +:func:`~flask.stream_with_context` wrapper. This will keep the request +context active during the generator. .. code-block:: python diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index cb1208fe..3d626f50 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -99,7 +99,7 @@ WTForm's field function, which renders the field for us. The keyword arguments will be inserted as HTML attributes. So, for example, you can call ``render_field(form.username, class='username')`` to add a class to the input element. Note that WTForms returns standard Python strings, -so we have to tell Jinja that this data is already HTML-escaped with +so we have to tell Jinja2 that this data is already HTML-escaped with the ``|safe`` filter. Here is the :file:`register.html` template for the function we used above, which diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 712ba977..ad9e3bc4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -139,16 +139,18 @@ how you're using untrusted data. .. code-block:: python - from flask import request from markupsafe import escape - @app.route("/hello") - def hello(): - name = request.args.get("name", "Flask") + @app.route("/") + def hello(name): return f"Hello, {escape(name)}!" -If a user submits ``/hello?name=``, escaping causes -it to be rendered as text, rather than running the script in the user's browser. +If a user managed to submit the name ````, +escaping causes it to be rendered as text, rather than running the +script in the user's browser. + +```` in the route captures a value from the URL and passes it to +the view function. These variable rules are explained below. Routing @@ -258,7 +260,7 @@ Why would you want to build URLs using the URL reversing function For example, here we use the :meth:`~flask.Flask.test_request_context` method to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` tells Flask to behave as though it's handling a request even while we use a -Python shell. See :doc:`/appcontext`. +Python shell. See :ref:`context-locals`. .. code-block:: python @@ -352,12 +354,12 @@ Rendering Templates Generating HTML from within Python is not fun, and actually pretty cumbersome because you have to do the HTML escaping on your own to keep -the application secure. Because of that Flask configures the `Jinja +the application secure. Because of that Flask configures the `Jinja2 `_ template engine for you automatically. Templates can be used to generate any type of text file. For web applications, you'll primarily be generating HTML pages, but you can also generate markdown, plain text for -emails, and anything else. +emails, any anything else. For a reference to HTML, CSS, and other web APIs, use the `MDN Web Docs`_. @@ -373,7 +375,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', person=name) + return render_template('hello.html', name=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 @@ -392,8 +394,8 @@ package it's actually inside your package: /templates /hello.html -For templates you can use the full power of Jinja templates. Head over -to the official `Jinja Template Documentation +For templates you can use the full power of Jinja2 templates. Head over +to the official `Jinja2 Template Documentation `_ for more information. Here is an example template: @@ -402,8 +404,8 @@ Here is an example template: Hello from Flask - {% if person %} -

Hello {{ person }}!

+ {% if name %} +

Hello {{ name }}!

{% else %}

Hello, World!

{% endif %} @@ -417,7 +419,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 ``person`` contains HTML it will be escaped +Automatic escaping is enabled, so if ``name`` 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 @@ -449,58 +451,105 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works: Accessing Request Data ---------------------- -For web applications it's crucial to react to the data a client sends to the -server. In Flask this information is provided by the global :data:`.request` -object, which is an instance of :class:`.Request`. This object has many -attributes and methods to work with the incoming request data, but here is a -broad overview. First it needs to be imported. +For web applications it's crucial to react to the data a client sends to +the server. In Flask this information is provided by the global +:class:`~flask.request` object. If you have some experience with Python +you might be wondering how that object can be global and how Flask +manages to still be threadsafe. The answer is context locals: -.. code-block:: python + +.. _context-locals: + +Context Locals +`````````````` + +.. admonition:: Insider Information + + If you want to understand how that works and how you can implement + tests with context locals, read this section, otherwise just skip it. + +Certain objects in Flask are global objects, but not of the usual kind. +These objects are actually proxies to objects that are local to a specific +context. What a mouthful. But that is actually quite easy to understand. + +Imagine the context being the handling thread. A request comes in and the +web server decides to spawn a new thread (or something else, the +underlying object is capable of dealing with concurrency systems other +than threads). When Flask starts its internal request handling it +figures out that the current thread is the active context and binds the +current application and the WSGI environments to that context (thread). +It does that in an intelligent way so that one application can invoke another +application without breaking. + +So what does this mean to you? Basically you can completely ignore that +this is the case unless you are doing something like unit testing. You +will notice that code which depends on a request object will suddenly break +because there is no request object. The solution is creating a request +object yourself and binding it to the context. The easiest solution for +unit testing is to use the :meth:`~flask.Flask.test_request_context` +context manager. In combination with the ``with`` statement it will bind a +test request so that you can interact with it. Here is an example:: from flask import request -If you have some experience with Python you might be wondering how that object -can be global when Flask handles multiple requests at a time. The answer is -that :data:`.request` is actually a proxy, pointing at whatever request is -currently being handled by a given worker, which is managed internally by Flask -and Python. See :doc:`/appcontext` for much more information. + with app.test_request_context('/hello', method='POST'): + # now you can do something with the request until the + # end of the with block, such as basic assertions: + assert request.path == '/hello' + assert request.method == 'POST' -The current request method is available in the :attr:`~.Request.method` -attribute. To access form data (data transmitted in a ``POST`` or ``PUT`` -request), use the :attr:`~flask.Request.form` attribute, which behaves like a -dict. +The other possibility is passing a whole WSGI environment to the +:meth:`~flask.Flask.request_context` method:: -.. code-block:: python + with app.request_context(environ): + assert request.method == 'POST' - @app.route("/login", methods=["GET", "POST"]) +The Request Object +`````````````````` + +The request object is documented in the API section and we will not cover +it here in detail (see :class:`~flask.Request`). Here is a broad overview of +some of the most common operations. First of all you have to import it from +the ``flask`` module:: + + from flask import request + +The current request method is available by using the +:attr:`~flask.Request.method` attribute. To access form data (data +transmitted in a ``POST`` or ``PUT`` request) you can use the +:attr:`~flask.Request.form` attribute. Here is a full example of the two +attributes mentioned above:: + + @app.route('/login', methods=['POST', 'GET']) def login(): error = None - - if request.method == "POST": - if valid_login(request.form["username"], request.form["password"]): - return store_login(request.form["username"]) + if request.method == 'POST': + if valid_login(request.form['username'], + request.form['password']): + return log_the_user_in(request.form['username']) else: - error = "Invalid username or password" + error = 'Invalid username/password' + # the code below is executed if the request method + # was GET or the credentials were invalid + return render_template('login.html', error=error) - # Executed if the request method was GET or the credentials were invalid. - return render_template("login.html", error=error) +What happens if the key does not exist in the ``form`` attribute? In that +case a special :exc:`KeyError` is raised. You can catch it like a +standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request +error page is shown instead. So for many situations you don't have to +deal with that problem. -If the key does not exist in ``form``, a special :exc:`KeyError` is raised. You -can catch it like a normal ``KeyError``, otherwise it will return a HTTP 400 -Bad Request error page. You can also use the -:meth:`~werkzeug.datastructures.MultiDict.get` method to get a default -instead of an error. - -To access parameters submitted in the URL (``?key=value``), use the -:attr:`~.Request.args` attribute. Key errors behave the same as ``form``, -returning a 400 response if not caught. - -.. code-block:: python +To access parameters submitted in the URL (``?key=value``) you can use the +:attr:`~flask.Request.args` attribute:: searchword = request.args.get('key', '') -For a full list of methods and attributes of the request object, see the -:class:`~.Request` documentation. +We recommend accessing URL parameters with `get` or by catching the +:exc:`KeyError` because users might change the URL and presenting them a 400 +bad request page in that case is not user friendly. + +For a full list of methods and attributes of the request object, head over +to the :class:`~flask.Request` documentation. File Uploads diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 6660671e..70ea13e3 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -1,6 +1,248 @@ -:orphan: +.. currentmodule:: flask The Request Context =================== -Obsolete, see :doc:`/appcontext` instead. +The request context keeps track of the request-level data during a +request. Rather than passing the request object to each function that +runs during a request, the :data:`request` and :data:`session` proxies +are accessed instead. + +This is similar to :doc:`/appcontext`, which keeps track of the +application-level data independent of a request. A corresponding +application context is pushed when a request context is pushed. + + +Purpose of the Context +---------------------- + +When the :class:`Flask` application handles a request, it creates a +:class:`Request` object based on the environment it received from the +WSGI server. Because a *worker* (thread, process, or coroutine depending +on the server) handles only one request at a time, the request data can +be considered global to that worker during that request. Flask uses the +term *context local* for this. + +Flask automatically *pushes* a request context when handling a request. +View functions, error handlers, and other functions that run during a +request will have access to the :data:`request` proxy, which points to +the request object for the current request. + + +Lifetime of the Context +----------------------- + +When a Flask application begins handling a request, it pushes a request +context, which also pushes an :doc:`app context `. When the +request ends it pops the request context then the application context. + +The context is unique to each thread (or other worker type). +:data:`request` cannot be passed to another thread, the other thread has +a different context space and will not know about the request the parent +thread was pointing to. + +Context locals are implemented using Python's :mod:`contextvars` and +Werkzeug's :class:`~werkzeug.local.LocalProxy`. Python manages the +lifetime of context vars automatically, and local proxy wraps that +low-level interface to make the data easier to work with. + + +Manually Push a Context +----------------------- + +If you try to access :data:`request`, or anything that uses it, outside +a request context, you'll get this error message: + +.. code-block:: pytb + + RuntimeError: Working outside of request context. + + This typically means that you attempted to use functionality that + needed an active HTTP request. Consult the documentation on testing + for information about how to avoid this problem. + +This should typically only happen when testing code that expects an +active request. One option is to use the +:meth:`test client ` to simulate a full request. Or +you can use :meth:`~Flask.test_request_context` in a ``with`` block, and +everything that runs in the block will have access to :data:`request`, +populated with your test data. :: + + def generate_report(year): + format = request.args.get("format") + ... + + with app.test_request_context( + "/make_report/2017", query_string={"format": "short"} + ): + generate_report() + +If you see that error somewhere else in your code not related to +testing, it most likely indicates that you should move that code into a +view function. + +For information on how to use the request context from the interactive +Python shell, see :doc:`/shell`. + + +How the Context Works +--------------------- + +The :meth:`Flask.wsgi_app` method is called to handle each request. It +manages the contexts during the request. Internally, the request and +application contexts work like stacks. When contexts are pushed, the +proxies that depend on them are available and point at information from +the top item. + +When the request starts, a :class:`~ctx.RequestContext` is created and +pushed, which creates and pushes an :class:`~ctx.AppContext` first if +a context for that application is not already the top context. While +these contexts are pushed, the :data:`current_app`, :data:`g`, +:data:`request`, and :data:`session` proxies are available to the +original thread handling the request. + +Other contexts may be pushed to change the proxies during a request. +While this is not a common pattern, it can be used in advanced +applications to, for example, do internal redirects or chain different +applications together. + +After the request is dispatched and a response is generated and sent, +the request context is popped, which then pops the application context. +Immediately before they are popped, the :meth:`~Flask.teardown_request` +and :meth:`~Flask.teardown_appcontext` functions are executed. These +execute even if an unhandled exception occurred during dispatch. + + +.. _callbacks-and-errors: + +Callbacks and Errors +-------------------- + +Flask dispatches a request in multiple stages which can affect the +request, response, and how errors are handled. The contexts are active +during all of these stages. + +A :class:`Blueprint` can add handlers for these events that are specific +to the blueprint. The handlers for a blueprint will run if the blueprint +owns the route that matches the request. + +#. Before each request, :meth:`~Flask.before_request` functions are + called. If one of these functions return a value, the other + functions are skipped. The return value is treated as the response + and the view function is not called. + +#. If the :meth:`~Flask.before_request` functions did not return a + response, the view function for the matched route is called and + returns a response. + +#. The return value of the view is converted into an actual response + object and passed to the :meth:`~Flask.after_request` + functions. Each function returns a modified or new response object. + +#. After the response is returned, the contexts are popped, which calls + the :meth:`~Flask.teardown_request` and + :meth:`~Flask.teardown_appcontext` functions. These functions are + called even if an unhandled exception was raised at any point above. + +If an exception is raised before the teardown functions, Flask tries to +match it with an :meth:`~Flask.errorhandler` function to handle the +exception and return a response. If no error handler is found, or the +handler itself raises an exception, Flask returns a generic +``500 Internal Server Error`` response. The teardown functions are still +called, and are passed the exception object. + +If debug mode is enabled, unhandled exceptions are not converted to a +``500`` response and instead are propagated to the WSGI server. This +allows the development server to present the interactive debugger with +the traceback. + + +Teardown Callbacks +~~~~~~~~~~~~~~~~~~ + +The teardown callbacks are independent of the request dispatch, and are +instead called by the contexts when they are popped. The functions are +called even if there is an unhandled exception during dispatch, and for +manually pushed contexts. This means there is no guarantee that any +other parts of the request dispatch have run first. Be sure to write +these functions in a way that does not depend on other callbacks and +will not fail. + +During testing, it can be useful to defer popping the contexts after the +request ends, so that their data can be accessed in the test function. +Use the :meth:`~Flask.test_client` as a ``with`` block to preserve the +contexts until the ``with`` block exits. + +.. code-block:: python + + from flask import Flask, request + + app = Flask(__name__) + + @app.route('/') + def hello(): + print('during view') + return 'Hello, World!' + + @app.teardown_request + def show_teardown(exception): + print('after with block') + + with app.test_request_context(): + print('during with block') + + # teardown functions are called after the context with block exits + + with app.test_client() as client: + client.get('/') + # the contexts are not popped even though the request ended + print(request.path) + + # the contexts are popped and teardown functions are called after + # the client with block exits + +Signals +~~~~~~~ + +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) diff --git a/docs/web-security.rst b/docs/security.rst similarity index 74% rename from docs/web-security.rst rename to docs/security.rst index 4118b5ec..777e5112 100644 --- a/docs/web-security.rst +++ b/docs/security.rst @@ -1,43 +1,9 @@ Security Considerations ======================= -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. +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. .. _security-xss: @@ -51,13 +17,13 @@ tags. For more information on that have a look at the Wikipedia article on `Cross-Site Scripting `_. -Flask configures Jinja to automatically escape all values unless +Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused in templates, but there are still other places where you have to be careful: -- generating HTML without the help of Jinja -- calling :class:`~markupsafe.Markup` on data submitted by users +- generating HTML without the help of Jinja2 +- calling :class:`~flask.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 @@ -65,7 +31,7 @@ careful: trick a browser to execute HTML. Another thing that is very important are unquoted attributes. While -Jinja can protect you from XSS issues by escaping HTML, there is one +Jinja2 can protect you from XSS issues by escaping HTML, there is one thing it cannot protect you from: XSS by attribute injection. To counter this possible attack vector, be sure to always quote your attributes with either double or single quotes when using Jinja expressions in them: @@ -158,7 +124,7 @@ recommend reviewing each of the headers below for use in your application. The `Flask-Talisman`_ extension can be used to manage HTTPS and the security headers for you. -.. _Flask-Talisman: https://github.com/wntrblm/flask-talisman +.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman HTTP Strict Transport Security (HSTS) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -269,25 +235,17 @@ values (or any values that need secure signatures). .. _samesite_support: https://caniuse.com/#feat=same-site-cookie-attribute -Host Header Validation ----------------------- +HTTP Public Key Pinning (HPKP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``Host`` header is used by the client to indicate what host name the request -was made to. This is used, for example, by ``url_for(..., _external=True)`` to -generate full URLs, for use in email or other messages outside the browser -window. +This tells the browser to authenticate with the server using only the specific +certificate key to prevent MITM attacks. -By default the app doesn't know what host(s) it is allowed to be accessed -through, and assumes any host is valid. Although browsers do not allow setting -the ``Host`` header, requests made by attackers in other scenarios could set -the ``Host`` header to a value they want. +.. warning:: + Be careful when enabling this, as it is very difficult to undo if you set up + or upgrade your key incorrectly. -When deploying your application, set :data:`TRUSTED_HOSTS` to restrict what -values the ``Host`` header may be. - -The ``Host`` header may be modified by proxies in between the client and your -application. See :doc:`deploying/proxy_fix` to tell your app which proxy values -to trust. +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning Copy/Paste to Terminal diff --git a/docs/server.rst b/docs/server.rst index d6beb1d8..d38aa120 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -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. 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. +5000. To disable the service, go to System Preferences, Sharing, and +disable "AirPlay Receiver". Deferred Errors on Reload diff --git a/docs/shell.rst b/docs/shell.rst index d8821e23..7e42e285 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -1,37 +1,56 @@ Working with the Shell ====================== -One of the reasons everybody loves Python is the interactive shell. It allows -you to play around with code in real time and immediately get results back. -Flask provides the ``flask shell`` CLI command to start an interactive Python -shell with some setup done to make working with the Flask app easier. +.. versionadded:: 0.3 -.. code-block:: text +One of the reasons everybody loves Python is the interactive shell. It +basically allows you to execute Python commands in real time and +immediately get results back. Flask itself does not come with an +interactive shell, because it does not require any specific setup upfront, +just import your application and start playing around. - $ flask shell +There are however some handy helpers to make playing around in the shell a +more pleasant experience. The main issue with interactive console +sessions is that you're not triggering a request like a browser does which +means that :data:`~flask.g`, :data:`~flask.request` and others are not +available. But the code you want to test might depend on them, so what +can you do? + +This is where some helper functions come in handy. Keep in mind however +that these functions are not only there for interactive shell usage, but +also for unit testing and other situations that require a faked request +context. + +Generally it's recommended that you read :doc:`reqcontext` first. + +Command Line Interface +---------------------- + +Starting with Flask 0.11 the recommended way to work with the shell is the +``flask shell`` command which does a lot of this automatically for you. +For instance the shell is automatically initialized with a loaded +application context. + +For more information see :doc:`/cli`. Creating a Request Context -------------------------- -``flask shell`` pushes an app context automatically, so :data:`.current_app` and -:data:`.g` are already available. However, there is no HTTP request being -handled in the shell, so :data:`.request` and :data:`.session` are not yet -available. - The easiest way to create a proper request context from the shell is by using the :attr:`~flask.Flask.test_request_context` method which creates us a :class:`~flask.ctx.RequestContext`: >>> ctx = app.test_request_context() -Normally you would use the ``with`` statement to make this context active, but -in the shell it's easier to call :meth:`~.RequestContext.push` and -:meth:`~.RequestContext.pop` manually: +Normally you would use the ``with`` statement to make this request object +active, but in the shell it's easier to use the +:meth:`~flask.ctx.RequestContext.push` and +:meth:`~flask.ctx.RequestContext.pop` methods by hand: >>> ctx.push() -From that point onwards you can work with the request object until you call -``pop``: +From that point onwards you can work with the request object until you +call `pop`: >>> ctx.pop() diff --git a/docs/signals.rst b/docs/signals.rst index 7ca81a9d..27630de6 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -1,28 +1,33 @@ Signals ======= -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. +.. versionadded:: 0.6 -Signals are implemented by the `Blinker`_ library. See its documentation for detailed -information. Flask provides some built-in signals. Extensions may provide their own. +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. -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. +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. +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. -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. - +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. Subscribing to Signals ---------------------- @@ -94,12 +99,17 @@ 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 @@ -113,6 +123,12 @@ 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 @@ -144,16 +160,17 @@ function, you can pass ``current_app._get_current_object()`` as sender. Signals and Flask's Request Context ----------------------------------- -Context-local proxies are available between :data:`~flask.request_started` and -:data:`~flask.request_finished`, so you can rely on :class:`flask.g` and others -as needed. Note the limitations described in :ref:`signals-sending` and the -:data:`~flask.request_tearing_down` signal. +Signals fully support :doc:`reqcontext` when receiving signals. +Context-local variables are consistently available between +:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can +rely on :class:`flask.g` and others as needed. Note the limitations described +in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. Decorator Based Signal Subscriptions ------------------------------------ -You can also easily subscribe to signals by using the +With Blinker 1.1 you can also easily subscribe to signals by using the new :meth:`~blinker.base.NamedSignal.connect_via` decorator:: from flask import template_rendered @@ -162,5 +179,10 @@ You can also easily subscribe to signals by using the 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 ed4a52ee..f497de73 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -1,21 +1,21 @@ Templates ========= -Flask leverages Jinja as its template engine. You are obviously free to use -a different template engine, but you still have to install Jinja to run +Flask leverages Jinja2 as its template engine. You are obviously free to use +a different template engine, but you still have to install Jinja2 to run Flask itself. This requirement is necessary to enable rich extensions. -An extension can depend on Jinja being present. +An extension can depend on Jinja2 being present. -This section only gives a very quick introduction into how Jinja +This section only gives a very quick introduction into how Jinja2 is integrated into Flask. If you want information on the template -engine's syntax itself, head over to the official `Jinja Template +engine's syntax itself, head over to the official `Jinja2 Template Documentation `_ for more information. Jinja Setup ----------- -Unless customized, Jinja is configured by Flask as follows: +Unless customized, Jinja2 is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using @@ -25,13 +25,13 @@ Unless customized, Jinja is configured by Flask as follows: - a template has the ability to opt in/out autoescaping with the ``{% autoescape %}`` tag. - Flask inserts a couple of global functions and helpers into the - Jinja context, additionally to the values that are present by + Jinja2 context, additionally to the values that are present by default. Standard Context ---------------- -The following global variables are available within Jinja templates +The following global variables are available within Jinja2 templates by default: .. data:: config @@ -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:`~markupsafe.Markup` +- In the Python code, wrap the HTML string in a :class:`~flask.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,58 +137,32 @@ using in this block. .. _registering-filters: -Registering Filters, Tests, and Globals ---------------------------------------- +Registering Filters +------------------- -The Flask app and blueprints provide decorators and methods to register your own -filters, tests, and global functions for use in Jinja templates. They all follow -the same pattern, so the following examples only discuss filters. +If you want to register your own filters in Jinja2 you have two ways to do +that. You can either put them by hand into the +:attr:`~flask.Flask.jinja_env` of the application or use the +:meth:`~flask.Flask.template_filter` decorator. -Decorate a function with :meth:`~.Flask.template_filter` to register it as a -template filter. +The two following examples work the same and both reverse an object:: -.. code-block:: python + @app.template_filter('reverse') + def reverse_filter(s): + return s[::-1] - @app.template_filter - def reverse(s): - return reversed(s) + def reverse_filter(s): + return s[::-1] + app.jinja_env.filters['reverse'] = reverse_filter -.. code-block:: jinja +In case of the decorator the argument is optional if you want to use the +function name as name of the filter. Once registered, you can use the filter +in your templates in the same way as Jinja2's builtin filters, for example if +you have a Python list in context called `mylist`:: - {% for item in data | reverse %} + {% for x in mylist | reverse %} {% endfor %} -By default it will use the name of the function as the name of the filter, but -that can be changed by passing a name to the decorator. - -.. code-block:: python - - @app.template_filter("reverse") - def reverse_filter(s): - return reversed(s) - -A filter can be registered separately using :meth:`~.Flask.add_template_filter`. -The name is optional and will use the function name if not given. - -.. code-block:: python - - def reverse_filter(s): - return reversed(s) - - app.add_template_filter(reverse_filter, "reverse") - -For template tests, use the :meth:`~.Flask.template_test` decorator or -:meth:`~.Flask.add_template_test` method. For template global functions, use the -:meth:`~.Flask.template_global` decorator or :meth:`~.Flask.add_template_global` -method. - -The same methods also exist on :class:`.Blueprint`, prefixed with ``app_`` to -indicate that the registered functions will be available to all templates, not -only when rendering from within the blueprint. - -The Jinja environment is also available as :attr:`~.Flask.jinja_env`. It may be -modified directly, as you would when using Jinja outside Flask. - Context Processors ------------------ @@ -237,7 +211,7 @@ strings. This can be used for streaming HTML in chunks to speed up initial page load, or to save memory when rendering a very large template. -The Jinja template engine supports rendering a template piece +The Jinja2 template engine supports rendering a template piece by piece, returning an iterator of strings. Flask provides the :func:`~flask.stream_template` and :func:`~flask.stream_template_string` functions to make this easier to use. @@ -251,11 +225,5 @@ functions to make this easier to use. return stream_template("timeline.html") These functions automatically apply the -:func:`~flask.stream_with_context` wrapper if a request is active, so that -:data:`.request`, :data:`.session`, and :data:`.g` remain available in the -template. - -More headers cannot be sent after the body has begun. Therefore, you must -make sure all headers are set before starting the response. In particular, -if the template will access ``session``, be sure to do so in the view as -well so that the ``Vary: cookie`` header will be set. +:func:`~flask.stream_with_context` wrapper if a request is active, so +that it remains available in the template. diff --git a/docs/testing.rst b/docs/testing.rst index c171abd6..8545bd39 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", follow_redirects=True) + response = client.get("/logout") # Check that there was one redirect response. assert len(response.history) == 1 # Check that the second request was to the index page. @@ -275,10 +275,11 @@ command from the command line. Tests that depend on an Active Context -------------------------------------- -You may have functions that are called from views or commands, that expect an -active :doc:`app context ` because they access :data:`.request`, -:data:`.session`, :data:`.g`, or :data:`.current_app`. Rather than testing them by -making a request or invoking the command, you can create and activate a context +You may have functions that are called from views or commands, that +expect an active :doc:`application context ` or +:doc:`request context ` because they access ``request``, +``session``, or ``current_app``. Rather than testing them by making a +request or invoking the command, you can create and activate a context directly. Use ``with app.app_context()`` to push an application context. For diff --git a/docs/tutorial/blog.rst b/docs/tutorial/blog.rst index 6418f5ff..b06329ea 100644 --- a/docs/tutorial/blog.rst +++ b/docs/tutorial/blog.rst @@ -305,7 +305,7 @@ The pattern ``{{ request.form['title'] or post['title'] }}`` is used to choose what data appears in the form. When the form hasn't been submitted, the original ``post`` data appears, but if invalid form data was posted you want to display that so the user can fix the error, so -``request.form`` is used instead. :data:`.request` is another variable +``request.form`` is used instead. :data:`request` is another variable that's automatically available in templates. diff --git a/docs/tutorial/database.rst b/docs/tutorial/database.rst index cf132603..934f6008 100644 --- a/docs/tutorial/database.rst +++ b/docs/tutorial/database.rst @@ -37,7 +37,6 @@ response is sent. :caption: ``flaskr/db.py`` import sqlite3 - from datetime import datetime import click from flask import current_app, g @@ -60,17 +59,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 @@ -133,11 +132,6 @@ 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`` @@ -148,10 +142,6 @@ 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 eb3a53ac..436ed5e8 100644 --- a/docs/tutorial/deploy.rst +++ b/docs/tutorial/deploy.rst @@ -14,13 +14,22 @@ application. Build and Install ----------------- -When you want to deploy your application elsewhere, you build a *wheel* -(``.whl``) file. Install and use the ``build`` tool to do this. +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: .. code-block:: none - $ pip install build - $ python -m build --wheel + $ 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 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} @@ -45,7 +54,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 @@ -68,7 +77,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 381477f9..39febd13 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -56,7 +56,10 @@ directory should be treated as a package. app.config.from_mapping(test_config) # ensure the instance folder exists - os.makedirs(app.instance_path, exist_ok=True) + try: + os.makedirs(app.instance_path) + except OSError: + pass # a simple page that says hello @app.route('/hello') diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index db83e106..f6820ebd 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -1,10 +1,11 @@ Make the Project Installable ============================ -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. +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. Installing also comes with other benefits that might not be obvious from the tutorial or as a new Python user, including: @@ -27,27 +28,50 @@ the tutorial or as a new Python user, including: Describe the Project -------------------- -The ``pyproject.toml`` file describes your project and how to build it. +The ``setup.py`` file describes your project and the files that belong +to it. -.. code-block:: toml - :caption: ``pyproject.toml`` +.. code-block:: python + :caption: ``setup.py`` - [project] - name = "flaskr" - version = "1.0.0" - description = "The basic blog app built in the Flask tutorial." - dependencies = [ - "flask", - ] + from setuptools import find_packages, setup - [build-system] - requires = ["flit_core<4"] - build-backend = "flit_core.buildapi" + setup( + name='flaskr', + version='1.0.0', + packages=find_packages(), + include_package_data=True, + install_requires=[ + 'flask', + ], + ) -See the official `Packaging tutorial `_ for more -explanation of the files and options used. + +``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. .. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/ +.. _packaging guide: https://packaging.python.org/guides/distributing-packages-using-setuptools/ Install the Project @@ -59,10 +83,10 @@ Use ``pip`` to install your project in the virtual environment. $ pip install -e . -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. +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. You can observe that the project is now installed with ``pip list``. @@ -79,7 +103,9 @@ 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 9f510927..b6a09f03 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,8 +80,9 @@ By the end, your project layout will look like this: │ ├── test_db.py │ ├── test_auth.py │ └── test_blog.py - ├── .venv/ - └── pyproject.toml + ├── venv/ + ├── setup.py + └── MANIFEST.in If you're using version control, the following files that are generated while running your project should be ignored. There may be other files @@ -91,7 +92,7 @@ write. For example, with git: .. code-block:: none :caption: ``.gitignore`` - .venv/ + venv/ *.pyc __pycache__/ @@ -102,4 +103,8 @@ write. For example, with git: .coverage htmlcov/ + dist/ + build/ + *.egg-info/ + Continue to :doc:`factory`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index ca9d4b32..1a5535cc 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -71,7 +71,7 @@ specific sections. {% block content %}{% endblock %} -:data:`.g` is automatically available in templates. Based on if +:data:`g` is automatically available in templates. Based on if ``g.user`` is set (from ``load_logged_in_user``), either the username and a log out link are displayed, or links to register and log in are displayed. :func:`url_for` is also automatically available, and is diff --git a/docs/tutorial/tests.rst b/docs/tutorial/tests.rst index 8958e773..cb60790c 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,18 +490,20 @@ 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 ``pyproject.toml`` file. +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. -.. code-block:: toml - :caption: ``pyproject.toml`` +.. code-block:: none + :caption: ``setup.cfg`` - [tool.pytest.ini_options] - testpaths = ["tests"] + [tool:pytest] + testpaths = tests - [tool.coverage.run] - branch = true - source = ["flaskr"] + [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. @@ -512,7 +514,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 + rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg collected 23 items tests/test_auth.py ........ [ 34%] diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 6626628a..7092dbc2 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -208,13 +208,13 @@ There are a few differences from the ``register`` view: password in the same way as the stored hash and securely compares them. If they match, the password is valid. -#. :data:`.session` is a :class:`dict` that stores data across requests. +#. :data:`session` is a :class:`dict` that stores data across requests. When validation succeeds, the user's ``id`` is stored in a new session. The data is stored in a *cookie* that is sent to the browser, and the browser then sends it back with subsequent requests. Flask securely *signs* the data so that it can't be tampered with. -Now that the user's ``id`` is stored in the :data:`.session`, it will be +Now that the user's ``id`` is stored in the :data:`session`, it will be available on subsequent requests. At the beginning of each request, if a user is logged in their information should be loaded and made available to other views. @@ -236,7 +236,7 @@ available to other views. :meth:`bp.before_app_request() ` registers a function that runs before the view function, no matter what URL is requested. ``load_logged_in_user`` checks if a user id is stored in the -:data:`.session` and gets that user's data from the database, storing it +:data:`session` and gets that user's data from the database, storing it on :data:`g.user `, which lasts for the length of the request. If there is no user id, or if the id doesn't exist, ``g.user`` will be ``None``. @@ -245,7 +245,7 @@ there is no user id, or if the id doesn't exist, ``g.user`` will be Logout ------ -To log out, you need to remove the user id from the :data:`.session`. +To log out, you need to remove the user id from the :data:`session`. Then ``load_logged_in_user`` won't load a user on subsequent requests. .. code-block:: python diff --git a/docs/views.rst b/docs/views.rst index f2210270..68a3462a 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -249,7 +249,7 @@ provide get (list) and post (create) methods. init_every_request = False def __init__(self, model): - self.model = model + self.model self.validator = generate_validator(model) def _get_item(self, id): diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml index cca36d8c..88ba6b96 100644 --- a/examples/celery/pyproject.toml +++ b/examples/celery/pyproject.toml @@ -3,15 +3,9 @@ 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]"] +requires-python = ">=3.7" +dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] [build-system] -requires = ["flit_core<4"] -build-backend = "flit_core.buildapi" - -[tool.flit.module] -name = "task_app" - -[tool.ruff] -src = ["src"] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt index 29075ab5..ce9ae72c 100644 --- a/examples/celery/requirements.txt +++ b/examples/celery/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.10 # by the following command: # # pip-compile --resolver=backtracking pyproject.toml @@ -10,8 +10,6 @@ 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 @@ -27,7 +25,7 @@ click-plugins==1.1.1 # via celery click-repl==0.2.0 # via celery -flask==2.3.2 +flask==2.2.3 # via flask-example-celery (pyproject.toml) itsdangerous==2.1.2 # via flask @@ -39,11 +37,11 @@ markupsafe==2.1.2 # via # jinja2 # werkzeug -prompt-toolkit==3.0.38 +prompt-toolkit==3.0.37 # via click-repl -pytz==2023.3 +pytz==2022.7.1 # via celery -redis==4.5.4 +redis==4.5.1 # via celery six==1.16.0 # via click-repl @@ -54,5 +52,5 @@ vine==5.0.0 # kombu wcwidth==0.2.6 # via prompt-toolkit -werkzeug==2.3.3 +werkzeug==2.2.3 # via flask diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore index a306afbc..85a35845 100644 --- a/examples/javascript/.gitignore +++ b/examples/javascript/.gitignore @@ -1,4 +1,4 @@ -.venv/ +venv/ *.pyc __pycache__/ instance/ diff --git a/examples/javascript/LICENSE.txt b/examples/javascript/LICENSE.rst similarity index 100% rename from examples/javascript/LICENSE.txt rename to examples/javascript/LICENSE.rst diff --git a/examples/javascript/MANIFEST.in b/examples/javascript/MANIFEST.in new file mode 100644 index 00000000..c730a34e --- /dev/null +++ b/examples/javascript/MANIFEST.in @@ -0,0 +1,4 @@ +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 f5f66912..23c7ce43 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/javascript/ +.. _Flask docs: https://flask.palletsprojects.com/patterns/jquery/ 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/views.py b/examples/javascript/js_example/views.py index 9f0d26c5..0d4b6561 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 . import app +from js_example import app @app.route("/", defaults={"js": "fetch"}) diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml deleted file mode 100644 index ea0efabd..00000000 --- a/examples/javascript/pyproject.toml +++ /dev/null @@ -1,33 +0,0 @@ -[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 new file mode 100644 index 00000000..f509ddfe --- /dev/null +++ b/examples/javascript/setup.cfg @@ -0,0 +1,29 @@ +[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 new file mode 100644 index 00000000..60684932 --- /dev/null +++ b/examples/javascript/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/examples/javascript/tests/test_js_example.py b/examples/javascript/tests/test_js_example.py index 856f5f77..d155ad5c 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"), ( - ("/", "fetch.html"), + ("/", "xhr.html"), ("/plain", "xhr.html"), ("/fetch", "fetch.html"), ("/jquery", "jquery.html"), diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore index a306afbc..85a35845 100644 --- a/examples/tutorial/.gitignore +++ b/examples/tutorial/.gitignore @@ -1,4 +1,4 @@ -.venv/ +venv/ *.pyc __pycache__/ instance/ diff --git a/examples/tutorial/LICENSE.txt b/examples/tutorial/LICENSE.rst similarity index 100% rename from examples/tutorial/LICENSE.txt rename to examples/tutorial/LICENSE.rst diff --git a/examples/tutorial/MANIFEST.in b/examples/tutorial/MANIFEST.in new file mode 100644 index 00000000..97d55d51 --- /dev/null +++ b/examples/tutorial/MANIFEST.in @@ -0,0 +1,6 @@ +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 653c2167..1c745078 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:: diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py index ab96e719..bb9cce5a 100644 --- a/examples/tutorial/flaskr/__init__.py +++ b/examples/tutorial/flaskr/__init__.py @@ -21,20 +21,22 @@ def create_app(test_config=None): app.config.update(test_config) # ensure the instance folder exists - os.makedirs(app.instance_path, exist_ok=True) + try: + os.makedirs(app.instance_path) + except OSError: + pass @app.route("/hello") def hello(): return "Hello, World!" # register the database commands - from . import db + from flaskr import db db.init_app(app) # apply the blueprints to the app - from . import auth - from . import blog + from flaskr import auth, 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 34c03a20..b423e6ae 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 .db import get_db +from flaskr.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 be0d92c4..3704626b 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 .auth import login_required -from .db import get_db +from flaskr.auth import login_required +from flaskr.db import get_db bp = Blueprint("blog", __name__) diff --git a/examples/tutorial/flaskr/db.py b/examples/tutorial/flaskr/db.py index dec22fde..acaa4ae3 100644 --- a/examples/tutorial/flaskr/db.py +++ b/examples/tutorial/flaskr/db.py @@ -1,5 +1,4 @@ import sqlite3 -from datetime import datetime import click from flask import current_app @@ -45,9 +44,6 @@ 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 deleted file mode 100644 index ca31e53d..00000000 --- a/examples/tutorial/pyproject.toml +++ /dev/null @@ -1,40 +0,0 @@ -[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 new file mode 100644 index 00000000..d001093b --- /dev/null +++ b/examples/tutorial/setup.cfg @@ -0,0 +1,28 @@ +[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 new file mode 100644 index 00000000..60684932 --- /dev/null +++ b/examples/tutorial/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1bc3e0e1..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,277 +0,0 @@ -[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/build.in b/requirements/build.in new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/requirements/build.in @@ -0,0 +1 @@ +build diff --git a/requirements/build.txt b/requirements/build.txt new file mode 100644 index 00000000..196545d0 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,13 @@ +# SHA1:80754af91bfb6d1073585b046fe0a474ce868509 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +build==0.10.0 + # via -r requirements/build.in +packaging==23.1 + # via build +pyproject-hooks==1.0.0 + # via build diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 00000000..99f5942f --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,6 @@ +-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 new file mode 100644 index 00000000..f9732cc9 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,64 @@ +# 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.10.0 + # via pip-tools +cachetools==5.3.0 + # via tox +cfgv==3.3.1 + # via pre-commit +chardet==5.1.0 + # via tox +click==8.1.3 + # via + # pip-compile-multi + # pip-tools +colorama==0.4.6 + # via tox +distlib==0.3.6 + # via virtualenv +filelock==3.12.0 + # via + # tox + # virtualenv +identify==2.5.22 + # via pre-commit +nodeenv==1.7.0 + # via pre-commit +pip-compile-multi==2.6.2 + # via -r requirements/dev.in +pip-tools==6.13.0 + # via pip-compile-multi +platformdirs==3.3.0 + # via + # tox + # virtualenv +pre-commit==3.2.2 + # via -r requirements/dev.in +pyproject-api==1.5.1 + # via tox +pyproject-hooks==1.0.0 + # via build +pyyaml==6.0 + # via pre-commit +toposort==1.10 + # via pip-compile-multi +tox==4.5.0 + # via -r requirements/dev.in +virtualenv==20.22.0 + # via + # pre-commit + # tox +wheel==0.40.0 + # 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 new file mode 100644 index 00000000..3a389e2b --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..7ee48f68 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,68 @@ +# SHA1:323f1c1134d78952ea63131c187303def63b56bd +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +alabaster==0.7.13 + # via sphinx +babel==2.12.1 + # via sphinx +certifi==2022.12.7 + # via requests +charset-normalizer==3.1.0 + # via requests +docutils==0.17.1 + # via + # sphinx + # sphinx-tabs +idna==3.4 + # via requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.2 + # via sphinx +markupsafe==2.1.2 + # via jinja2 +packaging==23.1 + # via + # pallets-sphinx-themes + # sphinx +pallets-sphinx-themes==2.1.0 + # via -r requirements/docs.in +pygments==2.15.1 + # via + # sphinx + # sphinx-tabs +requests==2.28.2 + # 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.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # 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.15 + # via requests diff --git a/requirements/tests-pallets-min.in b/requirements/tests-pallets-min.in new file mode 100644 index 00000000..fdc4e0cd --- /dev/null +++ b/requirements/tests-pallets-min.in @@ -0,0 +1,5 @@ +Werkzeug==2.2.2 +Jinja2==3.0.0 +MarkupSafe==2.1.1 +itsdangerous==2.0.0 +click==8.0.0 diff --git a/requirements/tests-pallets-min.txt b/requirements/tests-pallets-min.txt new file mode 100644 index 00000000..e091af77 --- /dev/null +++ b/requirements/tests-pallets-min.txt @@ -0,0 +1,20 @@ +# SHA1:f7109e66098c9e4cb68dc5f0d8e14f429ceed12c +# +# 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.1.1 + # via + # -r requirements/tests-pallets-min.in + # jinja2 + # werkzeug +werkzeug==2.2.2 + # via -r requirements/tests-pallets-min.in diff --git a/requirements/tests.in b/requirements/tests.in new file mode 100644 index 00000000..87f72802 --- /dev/null +++ b/requirements/tests.in @@ -0,0 +1,5 @@ +pytest +asgiref +blinker +greenlet ; python_version < "3.11" +python-dotenv>=1; python_version >= "3.8" diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 00000000..9c2c6fb7 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,21 @@ +# SHA1:30698f5f4f9cba5088318306829a15b0dc123b38 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +asgiref==3.6.0 + # via -r requirements/tests.in +blinker==1.6.2 + # via -r requirements/tests.in +iniconfig==2.0.0 + # via pytest +packaging==23.1 + # via pytest +pluggy==1.0.0 + # via pytest +pytest==7.3.1 + # via -r requirements/tests.in +python-dotenv==1.0.0 ; python_version >= "3.8" + # via -r requirements/tests.in diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 00000000..2c589ea0 --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1,5 @@ +mypy +types-contextvars +types-dataclasses +types-setuptools +cryptography diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 00000000..7b40becb --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,25 @@ +# SHA1:7cc3f64d4e78db89d81680ac81503d5ac35d31a9 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +cffi==1.15.1 + # via cryptography +cryptography==40.0.2 + # via -r requirements/typing.in +mypy==1.2.0 + # via -r requirements/typing.in +mypy-extensions==1.0.0 + # via mypy +pycparser==2.21 + # via cffi +types-contextvars==2.4.7.2 + # via -r requirements/typing.in +types-dataclasses==0.6.6 + # via -r requirements/typing.in +types-setuptools==67.7.0.0 + # via -r requirements/typing.in +typing-extensions==4.5.0 + # via mypy diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..736bd50f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,96 @@ +[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 + +[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 new file mode 100644 index 00000000..67175467 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +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.2", + "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 30dce6fd..19993402 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,5 +1,10 @@ +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 @@ -30,10 +35,37 @@ 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 -from .wrappers import Request as Request -from .wrappers import Response as Response + +__version__ = "2.2.5" + + +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) diff --git a/src/flask/app.py b/src/flask/app.py index 652b9bbf..d904d6ba 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1,47 +1,62 @@ -from __future__ import annotations - -import collections.abc as cabc +import functools 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.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 app_ctx +from .globals import _cv_request from .globals import g from .globals import request +from .globals import request_ctx from .globals import session -from .helpers import _CollectErrors +from .helpers import _split_blueprint_path from .helpers import get_debug_flag from .helpers import get_flashed_messages from .helpers import get_load_dotenv -from .helpers import send_from_directory -from .sansio.app import App +from .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 .sessions import SecureCookieSessionInterface from .sessions import SessionInterface from .signals import appcontext_tearing_down @@ -49,18 +64,20 @@ 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 - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIEnvironment - + import typing_extensions as te + from .blueprints import Blueprint 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 ) @@ -69,44 +86,28 @@ 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 _make_timedelta(value: timedelta | int | None) -> timedelta | None: + 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]: if value is None or isinstance(value, timedelta): return value return timedelta(seconds=value) -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) - - -# Other methods may call the overridden method with the new ctx arg. Remove it -# and call the method with the remaining args. -def remove_ctx(f: F) -> F: - def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any: - if args and isinstance(args[0], AppContext): - args = args[1:] - - return f(self, *args, **kwargs) - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -# The overridden method may call super().base_method without the new ctx arg. -# Add it to the args for the call. -def add_ctx(f: F) -> F: - def wrapper(self: Flask, *args: t.Any, **kwargs: t.Any) -> t.Any: - if not args: - args = (app_ctx._get_current_object(),) - elif not isinstance(args[0], AppContext): - args = (app_ctx._get_current_object(), *args) - - return f(self, *args, **kwargs) - - return update_wrapper(wrapper, f) # type: ignore[return-value] - - -class Flask(App): +class Flask(Scaffold): """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 @@ -203,16 +204,296 @@ class Flask(App): 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]: + """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]: + """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", @@ -220,30 +501,48 @@ class Flask(App): "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 class that is used for request objects. See :class:`~flask.Request` - #: for more information. - request_class: type[Request] = Request + #: 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 response objects. See - #: :class:`~flask.Response` for more information. - response_class: type[Response] = Response + #: 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 session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. @@ -251,97 +550,149 @@ class Flask(App): #: .. versionadded:: 0.8 session_interface: SessionInterface = SecureCookieSessionInterface() - def __init_subclass__(cls, **kwargs: t.Any) -> None: - import warnings - - # These method signatures were updated to take a ctx param. Detect - # overridden methods in subclasses that still have the old signature. - # Show a deprecation warning and wrap to call with correct args. - for method in ( - cls.handle_http_exception, - cls.handle_user_exception, - cls.handle_exception, - cls.log_exception, - cls.dispatch_request, - cls.full_dispatch_request, - cls.finalize_request, - cls.make_default_options_response, - cls.preprocess_request, - cls.process_response, - cls.do_teardown_request, - cls.do_teardown_appcontext, - ): - base_method = getattr(Flask, method.__name__) - - if method is base_method: - # not overridden - continue - - # get the second parameter (first is self) - iter_params = iter(inspect.signature(method).parameters.values()) - next(iter_params) - param = next(iter_params, None) - - # must have second parameter named ctx or annotated AppContext - if param is None or not ( - # no annotation, match name - (param.annotation is inspect.Parameter.empty and param.name == "ctx") - or ( - # string annotation, access path ends with AppContext - isinstance(param.annotation, str) - and param.annotation.rpartition(".")[2] == "AppContext" - ) - or ( - # class annotation - inspect.isclass(param.annotation) - and issubclass(param.annotation, AppContext) - ) - ): - warnings.warn( - f"The '{method.__name__}' method now takes 'ctx: AppContext'" - " as the first parameter. The old signature is deprecated" - " and will not be supported in Flask 4.0.", - DeprecationWarning, - stacklevel=2, - ) - setattr(cls, method.__name__, remove_ctx(method)) - setattr(Flask, method.__name__, add_ctx(base_method)) - def __init__( self, import_name: str, - static_url_path: str | None = None, - static_folder: str | os.PathLike[str] | None = "static", - static_host: str | None = None, + static_url_path: t.Optional[str] = None, + static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", + static_host: t.Optional[str] = None, host_matching: bool = False, subdomain_matching: bool = False, - template_folder: str | os.PathLike[str] | None = "templates", - instance_path: str | None = None, + template_folder: t.Optional[t.Union[str, os.PathLike]] = "templates", + instance_path: t.Optional[str] = None, instance_relative_config: bool = False, - root_path: str | None = None, + root_path: t.Optional[str] = None, ): super().__init__( import_name=import_name, - static_url_path=static_url_path, static_folder=static_folder, - static_host=static_host, - host_matching=host_matching, - subdomain_matching=subdomain_matching, + static_url_path=static_url_path, template_folder=template_folder, - instance_path=instance_path, - instance_relative_config=instance_relative_config, root_path=root_path, ) - #: 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() + 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." + ) - # 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 + #: 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() # Add a static route using the provided static_url_path, static_host, # and static_folder if there is a configured static_folder. @@ -349,9 +700,9 @@ class Flask(App): # For one, it might be created while the server is running (e.g. during # development). Also, Google App Engine stores static files somewhere if self.has_static_folder: - assert bool(static_host) == host_matching, ( - "Invalid static_host/host_matching combination" - ) + assert ( + bool(static_host) == host_matching + ), "Invalid static_host/host_matching combination" # Use a weakref to avoid creating a reference cycle between the app # and the view function (see #3761). self_ref = weakref.ref(self) @@ -359,112 +710,199 @@ class Flask(App): f"{self.static_url_path}/", endpoint="static", host=static_host, - view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 ) - def get_send_file_max_age(self, filename: str | None) -> int | None: - """Used by :func:`send_file` to determine the ``max_age`` cache - value for a given file path if it wasn't passed. + # 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 - 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_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." + ) - Note this is a duplicate of the same method in the Flask - class. + @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. - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. - - .. versionadded:: 0.9 + .. versionadded:: 0.8 """ - value = self.config["SEND_FILE_MAX_AGE_DEFAULT"] + 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 - if value is None: - return None + @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 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 + .. deprecated:: 2.2 + Will be removed in Flask 2.3. + .. versionadded:: 0.7 """ - if not self.has_static_folder: - raise RuntimeError("'static_folder' must be set to serve static_files.") + import warnings - # 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 + warnings.warn( + "'propagate_exceptions' is deprecated and will be removed in Flask 2.3.", + DeprecationWarning, + stacklevel=2, ) + rv = self.config["PROPAGATE_EXCEPTIONS"] + if rv is not None: + return rv + return self.testing or self.debug - 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. + @locked_cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. - 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: + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. - .. code-block:: python + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. - with app.open_resource("schema.sql") as f: - conn.executescript(f.read()) + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. - :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:: 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. - .. versionchanged:: 3.1 - Added the ``encoding`` parameter. + .. versionadded:: 0.3 """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") + return create_logger(self) - path = os.path.join(self.root_path, resource) + @locked_cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. - 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. + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. """ - path = os.path.join(self.instance_path, resource) + return self.create_jinja_environment() - if "b" in mode: - return open(path, mode) + @property + def got_first_request(self) -> bool: + """This attribute is set to ``True`` if the application started + handling the first request. - return open(path, mode, encoding=encoding) + .. 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 "production" + 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 def create_jinja_environment(self) -> Environment: """Create the Jinja environment based on :attr:`jinja_options` @@ -506,90 +944,33 @@ class Flask(App): rv.policies["json.dumps_function"] = self.json.dumps return rv - 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. + 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. - .. 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``. + The global loader dispatches between the loaders of the application + and the individual blueprints. - .. 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 + .. versionadded:: 0.7 """ - if request is not None: - if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: - request.trusted_hosts = trusted_hosts + return DispatchingJinjaLoader(self) - # 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"] + 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`. - 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 "" + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. - 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: + .. versionadded:: 0.5 """ - 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] + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) - from .debughelpers import FormDataRoutingRedirect - - raise FormDataRoutingRedirect(request) - - def update_template_context( - self, ctx: AppContext, context: dict[str, t.Any] - ) -> None: + def update_template_context(self, context: dict) -> 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 @@ -600,11 +981,11 @@ class Flask(App): :param context: the context as a dictionary that is updated in place to add extra variables. """ - names: t.Iterable[str | None] = (None,) + names: t.Iterable[t.Optional[str]] = (None,) # A template may be rendered outside a request context. - if ctx.has_request: - names = chain(names, reversed(ctx.request.blueprints)) + if request: + names = chain(names, reversed(request.blueprints)) # The values passed to render_template take precedence. Keep a # copy to re-apply after all context functions. @@ -613,11 +994,11 @@ class Flask(App): for name in names: if name in self.template_context_processors: for func in self.template_context_processors[name]: - context.update(self.ensure_sync(func)()) + context.update(func()) context.update(orig_ctx) - def make_shell_context(self) -> dict[str, t.Any]: + def make_shell_context(self) -> dict: """Returns the shell context for an interactive shell for this application. This runs all the registered shell context processors. @@ -629,11 +1010,65 @@ class Flask(App): 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: str | None = None, - port: int | None = None, - debug: bool | None = None, + host: t.Optional[str] = None, + port: t.Optional[int] = None, + debug: t.Optional[bool] = None, load_dotenv: bool = True, **options: t.Any, ) -> None: @@ -709,8 +1144,16 @@ class Flask(App): if get_load_dotenv(load_dotenv): cli.load_dotenv() - # if set, env var overrides existing value - if "FLASK_DEBUG" in os.environ: + # 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: self.debug = get_debug_flag() # debug passed to method overrides all other sources @@ -752,7 +1195,7 @@ class Flask(App): # 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`. @@ -810,7 +1253,7 @@ class Flask(App): 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`. @@ -827,9 +1270,312 @@ class Flask(App): 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, ctx: AppContext, e: HTTPException - ) -> HTTPException | ft.ResponseReturnValue: + self, e: HTTPException + ) -> t.Union[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. @@ -857,14 +1603,49 @@ class Flask(App): if isinstance(e, RoutingException): return e - handler = self._find_error_handler(e, ctx.request.blueprints) + handler = self._find_error_handler(e) if handler is None: return e - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + 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 def handle_user_exception( - self, ctx: AppContext, e: Exception - ) -> HTTPException | ft.ResponseReturnValue: + self, e: Exception + ) -> t.Union[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 @@ -885,23 +1666,23 @@ class Flask(App): e.show_exception = True if isinstance(e, HTTPException) and not self.trap_http_exception(e): - return self.handle_http_exception(ctx, e) + return self.handle_http_exception(e) - handler = self._find_error_handler(e, ctx.request.blueprints) + handler = self._find_error_handler(e) if handler is None: raise - return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + return self.ensure_sync(handler)(e) - def handle_exception(self, ctx: AppContext, e: Exception) -> Response: + def handle_exception(self, e: Exception) -> Response: """Handle an exception that did not have an error handler associated with it, or that was raised from an error handler. This always causes a 500 ``InternalServerError``. Always sends the :data:`got_request_exception` signal. - If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug + If :attr:`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. @@ -923,7 +1704,7 @@ class Flask(App): .. versionadded:: 0.3 """ exc_info = sys.exc_info() - got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) + got_request_exception.send(self, exception=e) propagate = self.config["PROPAGATE_EXCEPTIONS"] if propagate is None: @@ -937,20 +1718,21 @@ class Flask(App): raise e - self.log_exception(ctx, exc_info) - server_error: InternalServerError | ft.ResponseReturnValue + self.log_exception(exc_info) + server_error: t.Union[InternalServerError, ft.ResponseReturnValue] server_error = InternalServerError(original_exception=e) - handler = self._find_error_handler(server_error, ctx.request.blueprints) + handler = self._find_error_handler(server_error) if handler is not None: server_error = self.ensure_sync(handler)(server_error) - return self.finalize_request(ctx, server_error, from_error_handler=True) + return self.finalize_request(server_error, from_error_handler=True) def log_exception( self, - ctx: AppContext, - exc_info: tuple[type, BaseException, TracebackType] | tuple[None, None, None], + exc_info: t.Union[ + t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] + ], ) -> None: """Logs an exception. This is called by :meth:`handle_exception` if debugging is disabled and right before the handler is called. @@ -960,10 +1742,38 @@ class Flask(App): .. versionadded:: 0.8 """ self.logger.error( - f"Exception on {ctx.request.path} [{ctx.request.method}]", exc_info=exc_info + f"Exception on {request.path} [{request.method}]", exc_info=exc_info ) - def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue: + def 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: """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a @@ -973,8 +1783,7 @@ class Flask(App): This no longer does the exception handling, this code was moved to the new :meth:`full_dispatch_request`. """ - req = ctx.request - + req = request_ctx.request if req.routing_exception is not None: self.raise_routing_exception(req) rule: Rule = req.url_rule # type: ignore[assignment] @@ -984,44 +1793,41 @@ class Flask(App): getattr(rule, "provide_automatic_options", False) and req.method == "OPTIONS" ): - return self.make_default_options_response(ctx) + return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint - view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] - return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) - def full_dispatch_request(self, ctx: AppContext) -> Response: + def full_dispatch_request(self) -> Response: """Dispatches the request and on top of that performs request pre and postprocessing as well as HTTP exception catching and error handling. .. versionadded:: 0.7 """ - if not self._got_first_request and self.should_ignore_error is not None: - import warnings + # 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)() - warnings.warn( - "The 'should_ignore_error' method is deprecated and will" - " be removed in Flask 3.3. Handle errors as needed in" - " teardown handlers instead.", - DeprecationWarning, - stacklevel=1, - ) - - self._got_first_request = True + self._got_first_request = True try: - request_started.send(self, _async_wrapper=self.ensure_sync) - rv = self.preprocess_request(ctx) + request_started.send(self) + rv = self.preprocess_request() if rv is None: - rv = self.dispatch_request(ctx) + rv = self.dispatch_request() except Exception as e: - rv = self.handle_user_exception(ctx, e) - return self.finalize_request(ctx, rv) + rv = self.handle_user_exception(e) + return self.finalize_request(rv) def finalize_request( self, - ctx: AppContext, - rv: ft.ResponseReturnValue | HTTPException, + rv: t.Union[ft.ResponseReturnValue, HTTPException], from_error_handler: bool = False, ) -> Response: """Given the return value from a view function this finalizes @@ -1038,10 +1844,8 @@ class Flask(App): """ response = self.make_response(rv) try: - response = self.process_response(ctx, response) - request_finished.send( - self, _async_wrapper=self.ensure_sync, response=response - ) + response = self.process_response(response) + request_finished.send(self, response=response) except Exception: if not from_error_handler: raise @@ -1050,19 +1854,30 @@ class Flask(App): ) return response - def make_default_options_response(self, ctx: AppContext) -> Response: + def make_default_options_response(self) -> Response: """This method is called to create the default ``OPTIONS`` response. This can be changed through subclassing to change the default behavior of ``OPTIONS`` responses. .. versionadded:: 0.7 """ - methods = ctx.url_adapter.allowed_methods() # type: ignore[union-attr] + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] rv = self.response_class() rv.allow.update(methods) return rv - def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + 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: """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. @@ -1077,7 +1892,7 @@ class Flask(App): return func def async_to_sync( - self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] + self, func: t.Callable[..., t.Coroutine] ) -> t.Callable[..., t.Any]: """Return a sync function that will run the coroutine function. @@ -1101,13 +1916,12 @@ class Flask(App): def url_for( self, - /, endpoint: str, *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, + _anchor: t.Optional[str] = None, + _method: t.Optional[str] = None, + _scheme: t.Optional[str] = None, + _external: t.Optional[bool] = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -1156,9 +1970,11 @@ class Flask(App): .. versionadded:: 2.2 Moved from ``flask.url_for``, which calls this method. """ - if (ctx := _cv_app.get(None)) is not None and ctx.has_request: - url_adapter = ctx.url_adapter - blueprint_name = ctx.request.blueprint + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint # If the endpoint starts with "." and the request matches a # blueprint, the endpoint is relative to the blueprint. @@ -1173,11 +1989,13 @@ class Flask(App): if _external is None: _external = _scheme is not None else: + app_ctx = _cv_app.get(None) + # If called by helpers.url_for, an app context is active, # use its url_adapter. Otherwise, app.url_for was called # directly, build an adapter. - if ctx is not None: - url_adapter = ctx.url_adapter + if app_ctx is not None: + url_adapter = app_ctx.url_adapter else: url_adapter = self.create_url_adapter(None) @@ -1221,6 +2039,20 @@ class Flask(App): 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`. @@ -1278,8 +2110,7 @@ class Flask(App): response object. """ - status: int | None = None - headers: HeadersValue | None = None + status = headers = None # unpack tuple returns if isinstance(rv, tuple): @@ -1291,7 +2122,7 @@ class Flask(App): # decide if a 2-tuple has status or headers elif len_rv == 2: if isinstance(rv[1], (Headers, dict, tuple, list)): - rv, headers = rv # pyright: ignore + rv, headers = rv else: rv, status = rv # type: ignore[assignment,misc] # other sized tuples are not allowed @@ -1312,12 +2143,12 @@ class Flask(App): # 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, cabc.Iterator): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_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, # pyright: ignore + rv, status=status, headers=headers, # type: ignore[arg-type] ) @@ -1329,8 +2160,7 @@ class Flask(App): # class to the correct type try: rv = self.response_class.force_type( - rv, # type: ignore[arg-type] - request.environ, + rv, request.environ # type: ignore[arg-type] ) except TypeError as e: raise TypeError( @@ -1359,11 +2189,108 @@ class Flask(App): # extend existing headers with provided headers if headers: - rv.headers.update(headers) + rv.headers.update(headers) # type: ignore[arg-type] return rv - def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None: + 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]: """Called before the request is dispatched. Calls :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` @@ -1373,13 +2300,12 @@ class Flask(App): value is handled as if it was the return value from the view, and further request handling is stopped. """ - req = ctx.request - names = (None, *reversed(req.blueprints)) + names = (None, *reversed(request.blueprints)) for name in names: if name in self.url_value_preprocessors: for url_func in self.url_value_preprocessors[name]: - url_func(req.endpoint, req.view_args) + url_func(request.endpoint, request.view_args) for name in names: if name in self.before_request_funcs: @@ -1387,11 +2313,11 @@ class Flask(App): rv = self.ensure_sync(before_func)() if rv is not None: - return rv # type: ignore[no-any-return] + return rv return None - def process_response(self, ctx: AppContext, response: Response) -> Response: + def process_response(self, response: Response) -> Response: """Can be overridden in order to modify the response object before it's sent to the WSGI server. By default this will call all the :meth:`after_request` decorated functions. @@ -1404,90 +2330,90 @@ class Flask(App): :return: a new response object or the same, has to be an instance of :attr:`response_class`. """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + for func in ctx._after_request_functions: response = self.ensure_sync(func)(response) - for name in chain(ctx.request.blueprints, (None,)): + for name in chain(request.blueprints, (None,)): if name in self.after_request_funcs: for func in reversed(self.after_request_funcs[name]): response = self.ensure_sync(func)(response) - if not self.session_interface.is_null_session(ctx._get_session()): - self.session_interface.save_session(self, ctx._get_session(), response) + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) return response def do_teardown_request( - self, ctx: AppContext, exc: BaseException | None = None + self, exc: t.Optional[BaseException] = _sentinel # type: ignore ) -> None: - """Called after the request is dispatched and the response is finalized, - right before the request context is popped. Called by - :meth:`.AppContext.pop`. + """Called after the request is dispatched and the response is + returned, right before the request context is popped. - This calls all functions decorated with :meth:`teardown_request`, and - :meth:`Blueprint.teardown_request` if a blueprint handled the request. - Finally, the :data:`request_tearing_down` signal is sent. + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. - :param exc: An unhandled exception raised while dispatching the request. - Passed to each teardown function. + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. - .. versionchanged:: 3.2 - All callbacks are called rather than stopping on the first error. + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. .. versionchanged:: 0.9 Added the ``exc`` argument. """ - collect_errors = _CollectErrors() + if exc is _sentinel: + exc = sys.exc_info()[1] - for name in chain(ctx.request.blueprints, (None,)): + for name in chain(request.blueprints, (None,)): if name in self.teardown_request_funcs: for func in reversed(self.teardown_request_funcs[name]): - with collect_errors: - self.ensure_sync(func)(exc) + self.ensure_sync(func)(exc) - with collect_errors: - request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - collect_errors.raise_any("Errors during request teardown") + request_tearing_down.send(self, exc=exc) def do_teardown_appcontext( - self, ctx: AppContext, exc: BaseException | None = None + self, exc: t.Optional[BaseException] = _sentinel # type: ignore ) -> None: - """Called right before the application context is popped. Called by - :meth:`.AppContext.pop`. + """Called right before the application context is popped. - This calls all functions decorated with :meth:`teardown_appcontext`. - Then the :data:`appcontext_tearing_down` signal is sent. + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. - :param exc: An unhandled exception raised while the context was active. - Passed to each teardown function. + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. - .. versionchanged:: 3.2 - All callbacks are called rather than stopping on the first error. + This is called by + :meth:`AppContext.pop() `. .. versionadded:: 0.9 """ - collect_errors = _CollectErrors() + if exc is _sentinel: + exc = sys.exc_info()[1] for func in reversed(self.teardown_appcontext_funcs): - with collect_errors: - self.ensure_sync(func)(exc) + self.ensure_sync(func)(exc) - with collect_errors: - appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) - - collect_errors.raise_any("Errors during app teardown") + appcontext_tearing_down.send(self, exc=exc) def app_context(self) -> AppContext: - """Create an :class:`.AppContext`. When the context is pushed, - :data:`.current_app` and :data:`.g` become available. + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. - A context is automatically pushed when handling each request, and when - running any ``flask`` CLI command. Use this as a ``with`` block to - manually push a context outside of those situations, such as during - setup or testing. + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. - .. code-block:: python + :: with app.app_context(): init_db() @@ -1498,37 +2424,44 @@ class Flask(App): """ return AppContext(self) - def request_context(self, environ: WSGIEnvironment) -> AppContext: - """Create an :class:`.AppContext` with request information representing - the given WSGI environment. A context is automatically pushed when - handling each request. When the context is pushed, :data:`.request`, - :data:`.session`, :data:`g:, and :data:`.current_app` become available. + def request_context(self, environ: 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. - This method should not be used in your own code. Creating a valid WSGI - environ is not trivial. Use :meth:`test_request_context` to correctly - create a WSGI environ and request context instead. + See :doc:`/reqcontext`. - See :doc:`/appcontext`. + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. - :param environ: A WSGI environment. + :param environ: a WSGI environment """ - return AppContext.from_environ(self, environ) + return RequestContext(self, environ) - def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> AppContext: - """Create an :class:`.AppContext` with request information created from - the given arguments. When the context is pushed, :data:`.request`, - :data:`.session`, :data:`g:, and :data:`.current_app` become available. + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. - This is useful during testing to run a function that uses request data - without dispatching a full request. Use this as a ``with`` block to push - a context. + See :doc:`/reqcontext`. - .. code-block:: python + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: - with app.test_request_context(...): + with test_request_context(...): generate_report() - See :doc:`/appcontext`. + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() Takes the same arguments as Werkzeug's :class:`~werkzeug.test.EnvironBuilder`, with some defaults from @@ -1538,18 +2471,20 @@ class Flask(App): :param path: URL path being requested. :param base_url: Base URL where the app is being served, which ``path`` is relative to. If not given, built from - :data:`PREFERRED_URL_SCHEME`, ``subdomain``, :data:`SERVER_NAME`, - and :data:`APPLICATION_ROOT`. - :param subdomain: Subdomain name to prepend to :data:`SERVER_NAME`. + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. :param url_scheme: Scheme to use instead of :data:`PREFERRED_URL_SCHEME`. - :param data: The request body text or bytes,or a dict of form data. + :param data: The request body, either as a string or a dict of + form keys and values. :param json: If given, this is serialized as JSON and passed as ``data``. Also defaults ``content_type`` to ``application/json``. - :param args: Other positional arguments passed to + :param args: other positional arguments passed to :class:`~werkzeug.test.EnvironBuilder`. - :param kwargs: Other keyword arguments passed to + :param kwargs: other keyword arguments passed to :class:`~werkzeug.test.EnvironBuilder`. """ from .testing import EnvironBuilder @@ -1557,15 +2492,11 @@ class Flask(App): builder = EnvironBuilder(self, *args, **kwargs) try: - environ = builder.get_environ() + return self.request_context(builder.get_environ()) finally: builder.close() - return self.request_context(environ) - - def wsgi_app( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: + def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: """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:: @@ -1583,6 +2514,7 @@ class Flask(App): Teardown events for the request and app contexts are called even if an unhandled error occurs. Other events may not be called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. :param environ: A WSGI environment. :param start_response: A callable accepting a status code, @@ -1590,34 +2522,29 @@ class Flask(App): start the response. """ ctx = self.request_context(environ) - error: BaseException | None = None + error: t.Optional[BaseException] = None try: try: ctx.push() - response = self.full_dispatch_request(ctx) + response = self.full_dispatch_request() except Exception as e: error = e - response = self.handle_exception(ctx, e) - except: + response = self.handle_exception(e) + except: # noqa: B001 error = sys.exc_info()[1] raise return response(environ, start_response) finally: if "werkzeug.debug.preserve_context" in environ: - environ["werkzeug.debug.preserve_context"](ctx) + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) - if ( - error is not None - and self.should_ignore_error is not None - and self.should_ignore_error(error) - ): + if error is not None and self.should_ignore_error(error): error = None ctx.pop(error) - def __call__( - self, environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: + def __call__(self, environ: dict, start_response: t.Callable) -> t.Any: """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 b6d4e433..eb664235 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -1,128 +1,712 @@ -from __future__ import annotations - +import json import os import typing as t -from datetime import timedelta +from collections import defaultdict +from functools import update_wrapper -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 +from . import typing as ft +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod if t.TYPE_CHECKING: # pragma: no cover - from .wrappers import Response + 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 +) -class Blueprint(SansioBlueprint): +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 + def json_encoder( + 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 + def json_decoder( + 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 + def __init__( self, name: str, import_name: str, - 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: + static_folder: t.Optional[t.Union[str, os.PathLike]] = None, + static_url_path: t.Optional[str] = None, + template_folder: t.Optional[t.Union[str, os.PathLike]] = 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 + ): super().__init__( - name, - import_name, - static_folder, - static_url_path, - template_folder, - url_prefix, - subdomain, - url_defaults, - root_path, - cli_group, + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, ) - #: 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() + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") - # 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 + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: t.List[DeferredSetupFunction] = [] - 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. + if url_defaults is None: + url_defaults = {} - 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. + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] - Note this is a duplicate of the same method in the Flask - class. + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + import warnings - .. versionchanged:: 2.0 - The default configuration is ``None`` instead of 12 hours. + 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, + ) - .. 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` + @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` 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. """ - if mode not in {"r", "rt", "rb"}: - raise ValueError("Resources can only be opened for reading.") + self.deferred_functions.append(func) - path = os.path.join(self.root_path, resource) + @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. + """ - if mode == "rb": - return open(path, mode) # pyright: ignore + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) - return open(path, mode, encoding=encoding) + 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: + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. + """ + 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 template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :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 template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :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 template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + 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 template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(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 template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + 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 template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(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:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. + """ + 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: + """Register a function to run before the first request to the application is + handled by the worker. Equivalent to :meth:`.Flask.before_first_request`. + + .. 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:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. + """ + 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:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. + """ + 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:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. + """ + 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:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. + """ + + 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: + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ + 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: + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/src/flask/cli.py b/src/flask/cli.py index 1a9159ec..37a15ff2 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -1,8 +1,6 @@ from __future__ import annotations import ast -import collections.abc as cabc -import importlib.metadata import inspect import os import platform @@ -11,8 +9,7 @@ import sys import traceback import typing as t from functools import update_wrapper -from operator import itemgetter -from types import ModuleType +from operator import attrgetter import click from click.core import ParameterSource @@ -25,12 +22,6 @@ 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 @@ -38,7 +29,7 @@ class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" -def find_best_app(module: ModuleType) -> Flask: +def find_best_app(module): """Given a module instance this tries to find the best possible application in the module or raises an exception. """ @@ -91,7 +82,7 @@ def find_best_app(module: ModuleType) -> Flask: ) -def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: +def _called_with_wrong_args(f): """Check whether calling a function raised a ``TypeError`` because the call failed or because something in the factory raised the error. @@ -117,7 +108,7 @@ def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: del tb -def find_app_by_string(module: ModuleType, app_name: str) -> Flask: +def find_app_by_string(module, app_name): """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. """ @@ -148,11 +139,7 @@ def find_app_by_string(module: ModuleType, app_name: str) -> Flask: # 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 - if kw.arg is not None - } + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expr.keywords} except ValueError: # literal_eval gives cryptic error messages, show a generic # message with the full expression instead. @@ -197,7 +184,7 @@ def find_app_by_string(module: ModuleType, app_name: str) -> Flask: ) -def prepare_import(path: str) -> str: +def prepare_import(path): """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. """ @@ -226,27 +213,13 @@ def prepare_import(path: str) -> str: return ".".join(module_name[::-1]) -@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: +def locate_app(module_name, app_name, raise_if_not_found=True): 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: # type: ignore[union-attr] + if sys.exc_info()[2].tb_next: raise NoAppException( f"While importing {module_name!r}, an ImportError was" f" raised:\n\n{traceback.format_exc()}" @@ -254,7 +227,7 @@ def locate_app( elif raise_if_not_found: raise NoAppException(f"Could not import {module_name!r}.") from None else: - return None + return module = sys.modules[module_name] @@ -264,17 +237,17 @@ def locate_app( return find_app_by_string(module, app_name) -def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: +def get_version(ctx, param, value): if not value or ctx.resilient_parsing: return - flask_version = importlib.metadata.version("flask") - werkzeug_version = importlib.metadata.version("werkzeug") + import werkzeug + from . import __version__ click.echo( f"Python {platform.python_version()}\n" - f"Flask {flask_version}\n" - f"Werkzeug {werkzeug_version}", + f"Flask {__version__}\n" + f"Werkzeug {werkzeug.__version__}", color=ctx.color, ) ctx.exit() @@ -297,9 +270,6 @@ 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__( @@ -307,7 +277,6 @@ 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 @@ -316,18 +285,8 @@ class ScriptInfo: self.create_app = create_app #: A dictionary with arbitrary data that can be associated with #: this script info. - self.data: dict[t.Any, t.Any] = {} + self.data: t.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: @@ -337,13 +296,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, maxsplit=1) + [None] + re.split(r":(?![\\/])", self.app_import_path, 1) + [None] )[:2] import_name = prepare_import(path) app = locate_app(import_name, name) @@ -352,10 +311,10 @@ class ScriptInfo: import_name = prepare_import(path) app = locate_app(import_name, None, raise_if_not_found=False) - if app is not None: + if app: break - if app is None: + if not app: raise NoAppException( "Could not locate a Flask application. Use the" " 'flask --app' option, 'FLASK_APP' environment" @@ -374,10 +333,8 @@ 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: F) -> F: +def with_appcontext(f): """Wraps a callback so that it's guaranteed to be executed with the script's application context. @@ -392,14 +349,14 @@ def with_appcontext(f: F) -> F: """ @click.pass_context - def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + def decorator(__ctx, *args, **kwargs): 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) # type: ignore[return-value] + return update_wrapper(decorator, f) class AppGroup(click.Group): @@ -410,31 +367,27 @@ class AppGroup(click.Group): Not to be confused with :class:`FlaskGroup`. """ - def command( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: + def command(self, *args, **kwargs): """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: t.Callable[..., t.Any]) -> click.Command: + def decorator(f): if wrap_for_ctx: f = with_appcontext(f) - return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] + return click.Group.command(self, *args, **kwargs)(f) return decorator - def group( # type: ignore[override] - self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: + def group(self, *args, **kwargs): """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 super().group(*args, **kwargs) # type: ignore[no-any-return] + return click.Group.group(self, *args, **kwargs) def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: @@ -468,7 +421,7 @@ _app_option = click.Option( def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: # If the flag isn't provided, it will default to False. Don't use # that, let debug be set by env in that case. - source = ctx.get_parameter_source(param.name) + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] if source is not None and source in ( ParameterSource.DEFAULT, @@ -493,22 +446,23 @@ _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: - import dotenv # noqa: F401 + importlib.import_module("dotenv") except ImportError: - # 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) + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) return value @@ -517,11 +471,7 @@ 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, taking precedence over" - " those set by '.env' and '.flaskenv'. Variables set directly in the" - " environment take highest precedence. python-dotenv must be installed." - ), + help="Load environment variables from this file. python-dotenv must be installed.", is_eager=True, expose_value=False, callback=_env_file_callback, @@ -545,9 +495,6 @@ 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. @@ -569,7 +516,7 @@ class FlaskGroup(AppGroup): set_debug_flag: bool = True, **extra: t.Any, ) -> None: - params: list[click.Parameter] = list(extra.pop("params", None) or ()) + params = 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 @@ -597,16 +544,24 @@ class FlaskGroup(AppGroup): self._loaded_plugin_commands = False - def _load_plugin_commands(self) -> None: + def _load_plugin_commands(self): if self._loaded_plugin_commands: return - for ep in importlib.metadata.entry_points(group="flask.commands"): + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + + for ep in metadata.entry_points(group="flask.commands"): self.add_command(ep.load(), ep.name) self._loaded_plugin_commands = True - def get_command(self, ctx: click.Context, name: str) -> click.Command | None: + def get_command(self, ctx, name): self._load_plugin_commands() # Look up built-in and plugin commands, which should be # available even if the app fails to load. @@ -633,7 +588,7 @@ class FlaskGroup(AppGroup): return app.cli.get_command(ctx, name) - def list_commands(self, ctx: click.Context) -> list[str]: + def list_commands(self, ctx): self._load_plugin_commands() # Start with the built-in and plugin commands. rv = set(super().list_commands(ctx)) @@ -666,19 +621,20 @@ 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, - load_dotenv_defaults=self.load_dotenv, + create_app=self.create_app, set_debug_flag=self.set_debug_flag ) return super().make_context(info_name, args, parent=parent, **extra) def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: - if (not args and self.no_args_is_help) or ( - len(args) == 1 and args[0] in self.get_help_option_names(ctx) - ): + if not args and self.no_args_is_help: # Attempt to load --env-file and --app early in case they # were given as env vars. Otherwise no_args_is_help will not # see commands from app.cli. @@ -688,33 +644,25 @@ class FlaskGroup(AppGroup): return super().parse_args(ctx, args) -def _path_is_ancestor(path: str, other: str) -> bool: +def _path_is_ancestor(path, other): """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[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``. +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. 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. - :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. + :param path: Load the file at this location instead of searching. + :return: ``True`` if a file was loaded. .. versionchanged:: 2.0 The current directory is not changed to the location of the @@ -734,36 +682,37 @@ def load_dotenv( except ImportError: if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): click.secho( - " * Tip: There are .env files present. Install python-dotenv" - " to use them.", + " * Tip: There are .env or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', fg="yellow", err=True, ) return False - data: dict[str, str | None] = {} + # 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") - if load_defaults: - for default_name in (".flaskenv", ".env"): - if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): - continue + return False - data |= dotenv.dotenv_values(default_path, encoding="utf-8") + loaded = False - if path is not None and os.path.isfile(path): - data |= dotenv.dotenv_values(path, encoding="utf-8") + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) - for key, value in data.items(): - if key in os.environ or value is None: + if not path: continue - os.environ[key] = value + dotenv.load_dotenv(path, encoding="utf-8") + loaded = True - return bool(data) # True if at least one env var was loaded. + return loaded # True if at least one file was located and loaded. -def show_server_banner(debug: bool, app_import_path: str | None) -> None: +def show_server_banner(debug, app_import_path): """Show extra startup messages the first time the server is run, ignoring the reloader. """ @@ -777,7 +726,7 @@ def show_server_banner(debug: bool, app_import_path: str | None) -> None: click.echo(f" * Debug mode: {'on' if debug else 'off'}") -class CertParamType(click.ParamType[t.Any]): +class CertParamType(click.ParamType): """Click option type for the ``--cert`` option. Allows either an existing file, the string ``'adhoc'``, or an import for a :class:`~ssl.SSLContext` object. @@ -785,12 +734,10 @@ class CertParamType(click.ParamType[t.Any]): name = "path" - def __init__(self) -> None: + def __init__(self): self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: + def convert(self, value, param, ctx): try: import ssl except ImportError: @@ -803,7 +750,7 @@ class CertParamType(click.ParamType[t.Any]): try: return self.path_type(value, param, ctx) except click.BadParameter: - value = click.STRING(value, param, ctx).lower() # type: ignore[union-attr] + value = click.STRING(value, param, ctx).lower() if value == "adhoc": try: @@ -825,7 +772,7 @@ class CertParamType(click.ParamType[t.Any]): raise -def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: +def _validate_key(ctx, param, value): """The ``--key`` option must be specified when ``--cert`` is a file. Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. """ @@ -847,9 +794,7 @@ def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t 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: @@ -870,11 +815,8 @@ class SeparatedPathType(click.Path): validated as a :class:`click.Path` type. """ - def convert( - self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None - ) -> t.Any: + def convert(self, value, param, ctx): 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] @@ -933,16 +875,16 @@ class SeparatedPathType(click.Path): ) @pass_script_info def run_command( - 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: + info, + host, + port, + reload, + debugger, + with_threads, + cert, + extra_files, + exclude_patterns, +): """Run a local development server. This server is for development purposes only. It does not provide @@ -952,7 +894,7 @@ def run_command( option. """ try: - app: WSGIApplication = info.load_app() # pyright: ignore + app = info.load_app() except Exception as e: if is_running_from_reloader(): # When reloading, print out the error immediately, but raise @@ -960,9 +902,7 @@ def run_command( traceback.print_exc() err = e - def app( - environ: WSGIEnvironment, start_response: StartResponse - ) -> cabc.Iterable[bytes]: + def app(environ, start_response): raise err from None else: @@ -1013,7 +953,7 @@ def shell_command() -> None: f"App: {current_app.import_name}\n" f"Instance: {current_app.instance_path}" ) - ctx: dict[str, t.Any] = {} + ctx: dict = {} # Support the regular Python interpreter startup script if someone # is using it. @@ -1049,62 +989,49 @@ def shell_command() -> None: @click.option( "--sort", "-s", - type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), + type=click.Choice(("endpoint", "methods", "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()) + 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"} - host_matching = current_app.url_map.host_matching - has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) - rows = [] + ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) - for rule in rules: - row = [ - rule.endpoint, - ", ".join(sorted((rule.methods or set()) - ignored_methods)), - ] + 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 - if has_domain: - row.append((rule.host if host_matching else rule.subdomain) or "") + rule_methods = [ + ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore + for rule in rules + ] - row.append(rule.rule) - rows.append(row) + 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) - headers = ["Endpoint", "Methods"] - sorts = ["endpoint", "methods"] + click.echo(row.format(*headers).strip()) + click.echo(row.format(*("-" * width for width in widths))) - 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)) + for rule, methods in zip(rules, rule_methods): + click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) cli = FlaskGroup( diff --git a/src/flask/config.py b/src/flask/config.py index 34ef1a57..d4fc310f 100644 --- a/src/flask/config.py +++ b/src/flask/config.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import errno import json import os @@ -8,46 +6,27 @@ import typing as t from werkzeug.utils import import_string -if t.TYPE_CHECKING: - import typing_extensions as te - from .sansio.app import App - - -T = t.TypeVar("T") - - -class ConfigAttribute(t.Generic[T]): +class ConfigAttribute: """Makes an attribute forward to the config""" - def __init__( - self, name: str, get_converter: t.Callable[[t.Any], T] | None = None - ) -> None: + def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: self.__name__ = name self.get_converter = get_converter - @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: + def __get__(self, obj: t.Any, owner: t.Any = None) -> t.Any: 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 - return rv # type: ignore[no-any-return] - - def __set__(self, obj: App, value: t.Any) -> None: + def __set__(self, obj: t.Any, value: t.Any) -> None: obj.config[self.__name__] = value -class Config(dict): # type: ignore[type-arg] +class Config(dict): """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. @@ -91,11 +70,7 @@ class Config(dict): # type: ignore[type-arg] :param defaults: an optional dictionary of default values """ - def __init__( - self, - root_path: str | os.PathLike[str], - defaults: dict[str, t.Any] | None = None, - ) -> None: + def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: super().__init__(defaults or {}) self.root_path = root_path @@ -150,13 +125,13 @@ class Config(dict): # type: ignore[type-arg] .. 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) @@ -164,6 +139,9 @@ class Config(dict): # type: ignore[type-arg] # 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 @@ -184,9 +162,7 @@ class Config(dict): # type: ignore[type-arg] return True - def from_pyfile( - self, filename: str | os.PathLike[str], silent: bool = False - ) -> bool: + def from_pyfile(self, filename: 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. @@ -215,7 +191,7 @@ class Config(dict): # type: ignore[type-arg] self.from_object(d) return True - def from_object(self, obj: object | str) -> None: + def from_object(self, obj: t.Union[object, str]) -> None: """Updates the values from the given object. An object can be of one of the following two types: @@ -255,10 +231,9 @@ class Config(dict): # type: ignore[type-arg] def from_file( self, - filename: str | os.PathLike[str], - load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], + filename: str, + load: t.Callable[[t.IO[t.Any]], t.Mapping], 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 @@ -269,8 +244,8 @@ class Config(dict): # type: ignore[type-arg] import json app.config.from_file("config.json", load=json.load) - import tomllib - app.config.from_file("config.toml", load=tomllib.load, text=False) + import toml + app.config.from_file("config.toml", load=toml.load) :param filename: The path to the data file. This can be an absolute path or relative to the config root path. @@ -279,18 +254,14 @@ class Config(dict): # type: ignore[type-arg] :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, "r" if text else "rb") as f: + with open(filename) as f: obj = load(f) except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): @@ -302,7 +273,7 @@ class Config(dict): # type: ignore[type-arg] return self.from_mapping(obj) def from_mapping( - self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any + self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any ) -> bool: """Updates the config like :meth:`update` ignoring items with non-upper keys. @@ -311,7 +282,7 @@ class Config(dict): # type: ignore[type-arg] .. versionadded:: 0.11 """ - mappings: dict[str, t.Any] = {} + mappings: t.Dict[str, t.Any] = {} if mapping is not None: mappings.update(mapping) mappings.update(kwargs) @@ -322,7 +293,7 @@ class Config(dict): # type: ignore[type-arg] def get_namespace( self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> dict[str, t.Any]: + ) -> t.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 d4d0de65..c79c26dc 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -1,23 +1,18 @@ -from __future__ import annotations - import contextvars +import sys import typing as t from functools import update_wrapper from types import TracebackType from werkzeug.exceptions import HTTPException -from werkzeug.routing import MapAdapter from . import typing as ft from .globals import _cv_app -from .helpers import _CollectErrors +from .globals import _cv_request from .signals import appcontext_popped from .signals import appcontext_pushed -if t.TYPE_CHECKING: - import typing_extensions as te - from _typeshed.wsgi import WSGIEnvironment - +if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .sessions import SessionMixin from .wrappers import Request @@ -32,7 +27,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 @@ -65,7 +60,7 @@ class _AppCtxGlobals: except KeyError: raise AttributeError(name) from None - def get(self, name: str, default: t.Any | None = None) -> t.Any: + def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: """Get an attribute by name, or a default value. Like :meth:`dict.get`. @@ -115,30 +110,30 @@ class _AppCtxGlobals: return object.__repr__(self) -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. +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. - .. code-block:: python + Example:: - @app.route("/") + @app.route('/') def index(): @after_this_request def add_header(response): - response.headers["X-Foo"] = "Parachute" + response.headers['X-Foo'] = 'Parachute' return response + return 'Hello World!' - return "Hello, World!" + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. .. versionadded:: 0.9 """ - ctx = _cv_app.get(None) + ctx = _cv_request.get(None) - if ctx is None or not ctx.has_request: + if ctx is None: raise RuntimeError( "'after_this_request' can only be used when a request" " context is active, such as in a view function." @@ -148,31 +143,14 @@ def after_this_request( return f -F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +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. - -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 + Example:: import gevent from flask import copy_current_request_context @@ -189,68 +167,59 @@ def copy_current_request_context(f: F) -> F: .. versionadded:: 0.10 """ - # Store the context that was active when the decorator was applied. - original = _cv_app.get(None) + ctx = _cv_request.get(None) - if original is None: + if ctx is None: raise RuntimeError( "'copy_current_request_context' can only be used when a" " request context is active, such as in a view function." ) - 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: + ctx = ctx.copy() + + def wrapper(*args, **kwargs): + with ctx: return ctx.app.ensure_sync(f)(*args, **kwargs) - return update_wrapper(wrapper, f) # type: ignore[return-value] + return update_wrapper(wrapper, f) def has_request_context() -> bool: - """Test if an app context is active and if it has request information. + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. - .. code-block:: python + :: - from flask import has_request_context, request + class User(db.Model): - if has_request_context(): - remote_addr = request.remote_addr + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr - If a request context is active, the :data:`.request` and :data:`.session` - context proxies will available and ``True``, otherwise ``False``. You can - use that to test the data you use, rather than using this function. + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: - .. code-block:: python + class User(db.Model): - from flask import request - - if request: - remote_addr = request.remote_addr + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr .. versionadded:: 0.7 """ - return (ctx := _cv_app.get(None)) is not None and ctx.has_request + return _cv_request.get(None) is not None def has_app_context() -> bool: - """Test if an app context is active. Unlike :func:`has_request_context` - this can be true outside a request, such as in a CLI command. - - .. code-block:: python - - from flask import has_app_context, g - - if has_app_context(): - g.cached_data = ... - - If an app context is active, the :data:`.g` and :data:`.current_app` context - proxies will available and ``True``, otherwise ``False``. You can use that - to test the data you use, rather than using this function. - - from flask import g - - if g: - g.cached_data = ... + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. .. versionadded:: 0.9 """ @@ -258,283 +227,212 @@ def has_app_context() -> bool: class AppContext: - """An app context contains information about an app, and about the request - when handling a request. A context is pushed at the beginning of each - request and CLI command, and popped at the end. The context is referred to - as a "request context" if it has request information, and an "app context" - if not. - - Do not use this class directly. Use :meth:`.Flask.app_context` to create an - app context if needed during setup, and :meth:`.Flask.test_request_context` - to create a request context if needed during tests. - - When the context is popped, it will evaluate all the teardown functions - registered with :meth:`~flask.Flask.teardown_request` (if handling a - request) then :meth:`.Flask.teardown_appcontext`. - - When using the interactive debugger, the context will be restored so - ``request`` is still accessible. Similarly, the test client can preserve the - context after the request ends. However, teardown functions may already have - closed some resources such as database connections, and will run again when - the restored context is popped. - - :param app: The application this context represents. - :param request: The request data this context represents. - :param session: The session data this context represents. If not given, - loaded from the request on first access. - - .. versionchanged:: 3.2 - Merged with ``RequestContext``. The ``RequestContext`` alias will be - removed in Flask 4.0. - - .. versionchanged:: 3.2 - A combined app and request context is pushed for every request and CLI - command, rather than trying to detect if an app context is already - pushed. - - .. versionchanged:: 3.2 - The session is loaded the first time it is accessed, rather than when - the context is pushed. + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. """ - def __init__( - self, - app: Flask, - *, - request: Request | None = None, - session: SessionMixin | None = None, - ) -> None: + def __init__(self, app: "Flask") -> None: self.app = app - """The application represented by this context. Accessed through - :data:`.current_app`. - """ - + self.url_adapter = app.create_url_adapter(None) self.g: _AppCtxGlobals = app.app_ctx_globals_class() - """The global data for this context. Accessed through :data:`.g`.""" - - self.url_adapter: MapAdapter | None = None - """The URL adapter bound to the request, or the app if not in a request. - May be ``None`` if binding failed. - """ - - self._request: Request | None = request - self._session: SessionMixin | None = session - self._flashes: list[tuple[str, str]] | None = None - self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] - - try: - self.url_adapter = app.create_url_adapter(self._request) - except HTTPException as e: - if self._request is not None: - self._request.routing_exception = e - - self._cv_token: contextvars.Token[AppContext] | None = None - """The previous state to restore when popping.""" - - self._push_count: int = 0 - """Track nested pushes of this context. Cleanup will only run once the - original push has been popped. - """ - - @classmethod - def from_environ(cls, app: Flask, environ: WSGIEnvironment, /) -> te.Self: - """Create an app context with request data from the given WSGI environ. - - :param app: The application this context represents. - :param environ: The request data this context represents. - """ - request = app.request_class(environ) - request.json_module = app.json - return cls(app, request=request) - - @property - def has_request(self) -> bool: - """True if this context was created with request data.""" - return self._request is not None - - def copy(self) -> te.Self: - """Create a new context with the same data objects as this context. See - :func:`.copy_current_request_context`. - - .. versionchanged:: 1.1 - The current session data is used instead of reloading the original data. - - .. versionadded:: 0.10 - """ - return self.__class__( - self.app, - request=self._request, - session=self._session, - ) - - @property - def request(self) -> Request: - """The request object associated with this context. Accessed through - :data:`.request`. Only available in request contexts, otherwise raises - :exc:`RuntimeError`. - """ - if self._request is None: - raise RuntimeError("There is no request in this context.") - - return self._request - - def _get_session(self) -> SessionMixin: - """Open the session if it is not already open for this request context.""" - if self._request is None: - raise RuntimeError("There is no request in this context.") - - if self._session is None: - si = self.app.session_interface - self._session = si.open_session(self.app, self.request) - - if self._session is None: - self._session = si.make_null_session(self.app) - - return self._session - - @property - def session(self) -> SessionMixin: - """The session object associated with this context. Accessed through - :data:`.session`. Only available in request contexts, otherwise raises - :exc:`RuntimeError`. Accessing this sets :attr:`.SessionMixin.accessed`. - """ - session = self._get_session() - session.accessed = True - return session - - def match_request(self) -> None: - """Apply routing to the current request, storing either the matched - endpoint and args, or a routing exception. - """ - try: - result = self.url_adapter.match(return_rule=True) # type: ignore[union-attr] - except HTTPException as e: - self._request.routing_exception = e # type: ignore[union-attr] - else: - self._request.url_rule, self._request.view_args = result # type: ignore[union-attr] + self._cv_tokens: t.List[contextvars.Token] = [] def push(self) -> None: - """Push this context so that it is the active context. If this is a - request context, calls :meth:`match_request` to perform routing with - the context active. + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app) - Typically, this is not used directly. Instead, use a ``with`` block - to manage the context. - - In some situations, such as streaming or testing, the context may be - pushed multiple times. It will only trigger matching and signals if it - is not currently pushed. - """ - self._push_count += 1 - - if self._cv_token is not None: - return - - self._cv_token = _cv_app.set(self) - appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) - - if self._request is not None: - # Open the session at the moment that the request context is available. - # This allows a custom open_session method to use the request context. - self._get_session() - - # Match the request URL after loading the session, so that the - # session is available in custom URL converters. - if self.url_adapter is not None: - self.match_request() - - def pop(self, exc: BaseException | None = None) -> None: - """Pop this context so that it is no longer the active context. Then - call teardown functions and signals. - - Typically, this is not used directly. Instead, use a ``with`` block - to manage the context. - - This context must currently be the active context, otherwise a - :exc:`RuntimeError` is raised. In some situations, such as streaming or - testing, the context may have been pushed multiple times. It will only - trigger cleanup once it has been popped as many times as it was pushed. - Until then, it will remain the active context. - - :param exc: An unhandled exception that was raised while the context was - active. Passed to teardown functions. - - .. versionchanged:: 0.9 - Added the ``exc`` argument. - """ - if self._cv_token is None: - raise RuntimeError(f"Cannot pop this context ({self!r}), it is not pushed.") - - ctx = _cv_app.get(None) - - if ctx is None or self._cv_token is None: - raise RuntimeError( - f"Cannot pop this context ({self!r}), there is no active context." - ) + def pop(self, exc: 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()) if ctx is not self: - raise RuntimeError( - f"Cannot pop this context ({self!r}), it is not the active" - f" context ({ctx!r})." + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" ) - self._push_count -= 1 + appcontext_popped.send(self.app) - if self._push_count > 0: - return - - collect_errors = _CollectErrors() - - if self._request is not None: - with collect_errors: - self.app.do_teardown_request(self, exc) - - with collect_errors: - self._request.close() - - with collect_errors: - self.app.do_teardown_appcontext(self, exc) - - _cv_app.reset(self._cv_token) - self._cv_token = None - - with collect_errors: - appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) - - collect_errors.raise_any("Errors during context teardown") - - def __enter__(self) -> te.Self: + def __enter__(self) -> "AppContext": self.push() return self def __exit__( self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - tb: TracebackType | None, + 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. + """ + + def __init__( + self, + app: "Flask", + environ: dict, + request: t.Optional["Request"] = None, + session: t.Optional["SessionMixin"] = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: 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] = [] + + self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = [] + + def copy(self) -> "RequestContext": + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: 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. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> "RequestContext": + self.push() + return self + + def __exit__( + self, + exc_type: t.Optional[type], + exc_value: t.Optional[BaseException], + tb: t.Optional[TracebackType], ) -> None: self.pop(exc_value) def __repr__(self) -> str: - if self._request is not None: - return ( - f"<{type(self).__name__} {id(self)} of {self.app.name}," - f" {self.request.method} {self.request.url!r}>" - ) - - return f"<{type(self).__name__} {id(self)} of {self.app.name}>" - - -def __getattr__(name: str) -> t.Any: - import warnings - - if name == "RequestContext": - warnings.warn( - "'RequestContext' has merged with 'AppContext', and will be removed" - " in Flask 4.0. Use 'AppContext' instead.", - DeprecationWarning, - stacklevel=2, + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" ) - return AppContext - - raise AttributeError(name) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index 61884e1a..b0639892 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -1,17 +1,8 @@ -from __future__ import annotations - import typing as t -from jinja2.loaders import BaseLoader -from werkzeug.routing import RequestRedirect - +from .app import Flask from .blueprints import Blueprint -from .globals import _cv_app -from .sansio.app import App - -if t.TYPE_CHECKING: - from .sansio.scaffold import Scaffold - from .wrappers import Request +from .globals import request_ctx class UnexpectedUnicodeError(AssertionError, UnicodeError): @@ -25,7 +16,7 @@ class DebugFilesKeyError(KeyError, AssertionError): provide a better error message than just a generic KeyError/BadRequest. """ - def __init__(self, request: Request, key: str) -> None: + def __init__(self, request, key): form_matches = request.form.getlist(key) buf = [ f"You tried to access the file {key!r} in the request.files" @@ -43,7 +34,7 @@ class DebugFilesKeyError(KeyError, AssertionError): ) self.msg = "".join(buf) - def __str__(self) -> str: + def __str__(self): return self.msg @@ -54,9 +45,8 @@ class FormDataRoutingRedirect(AssertionError): 307 or 308. """ - def __init__(self, request: Request) -> None: + def __init__(self, request): 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}'." @@ -78,7 +68,7 @@ class FormDataRoutingRedirect(AssertionError): super().__init__("".join(buf)) -def attach_enctype_error_multidict(request: Request) -> None: +def attach_enctype_error_multidict(request): """Patch ``request.files.__getitem__`` to raise a descriptive error about ``enctype=multipart/form-data``. @@ -87,8 +77,8 @@ def attach_enctype_error_multidict(request: Request) -> None: """ oldcls = request.files.__class__ - class newcls(oldcls): # type: ignore[valid-type, misc] - def __getitem__(self, key: str) -> t.Any: + class newcls(oldcls): + def __getitem__(self, key): try: return super().__getitem__(key) except KeyError as e: @@ -104,7 +94,7 @@ def attach_enctype_error_multidict(request: Request) -> None: request.files.__class__ = newcls -def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: +def _dump_loader_info(loader) -> t.Generator: yield f"class: {type(loader).__module__}.{type(loader).__name__}" for key, value in sorted(loader.__dict__.items()): if key.startswith("_"): @@ -121,27 +111,16 @@ def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: yield f"{key}: {value!r}" -def explain_template_loading_attempts( - app: App, - template: str, - attempts: list[ - tuple[ - BaseLoader, - Scaffold, - tuple[str, str | None, t.Callable[[], bool] | None] | None, - ] - ], -) -> None: +def explain_template_loading_attempts(app: Flask, template, attempts) -> None: """This should help developers understand what failed""" info = [f"Locating template {template!r}:"] total_found = 0 blueprint = None - - if (ctx := _cv_app.get(None)) is not None and ctx.has_request: - blueprint = ctx.request.blueprint + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint for idx, (loader, srcobj, triple) in enumerate(attempts): - if isinstance(srcobj, App): + if isinstance(srcobj, Flask): 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 f4a7298e..254da42b 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import typing as t from contextvars import ContextVar @@ -9,69 +7,101 @@ if t.TYPE_CHECKING: # pragma: no cover from .app import Flask from .ctx import _AppCtxGlobals from .ctx import AppContext + from .ctx import RequestContext from .sessions import SessionMixin from .wrappers import Request - T = t.TypeVar("T", covariant=True) - class ProxyMixin(t.Protocol[T]): - def _get_current_object(self) -> T: ... +class _FakeStack: + def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: + self.name = name + self.cv = cv - # 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): ... + def _warn(self): + import warnings - class AppContextProxy(ProxyMixin[AppContext], AppContext): ... + 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 _AppCtxGlobalsProxy(ProxyMixin[_AppCtxGlobals], _AppCtxGlobals): ... + def push(self, obj: t.Any) -> None: + self._warn() + self.cv.set(obj) - class RequestProxy(ProxyMixin[Request], Request): ... + def pop(self) -> t.Any: + self._warn() + ctx = self.cv.get(None) + self.cv.set(None) + return ctx - class SessionMixinProxy(ProxyMixin[SessionMixin], SessionMixin): ... + @property + def top(self) -> t.Optional[t.Any]: + self._warn() + return self.cv.get(None) _no_app_msg = """\ Working outside of application context. -Attempted to use functionality that expected a current application to be set. To -solve this, set up an app context using 'with app.app_context()'. See the -documentation on app context for more information.\ +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ """ -_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") -app_ctx: AppContextProxy = LocalProxy( # type: ignore[assignment] +_cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx") +__app_ctx_stack = _FakeStack("app", _cv_app) +app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment] _cv_app, unbound_message=_no_app_msg ) -current_app: FlaskProxy = LocalProxy( # type: ignore[assignment] +current_app: "Flask" = LocalProxy( # type: ignore[assignment] _cv_app, "app", unbound_message=_no_app_msg ) -g: _AppCtxGlobalsProxy = LocalProxy( # type: ignore[assignment] +g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment] _cv_app, "g", unbound_message=_no_app_msg ) _no_req_msg = """\ Working outside of request context. -Attempted to use functionality that expected an active HTTP request. See the -documentation on request context for more information.\ +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ """ -request: RequestProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "request", unbound_message=_no_req_msg +_cv_request: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") +__request_ctx_stack = _FakeStack("request", _cv_request) +request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg ) -session: SessionMixinProxy = LocalProxy( # type: ignore[assignment] - _cv_app, "session", unbound_message=_no_req_msg +request: "Request" = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: "SessionMixin" = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg ) def __getattr__(name: str) -> t.Any: - import warnings + if name == "_app_ctx_stack": + import warnings - if name == "request_ctx": warnings.warn( - "'request_ctx' has merged with 'app_ctx', and will be removed" - " in Flask 4.0. Use 'app_ctx' instead.", + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", DeprecationWarning, stacklevel=2, ) - return app_ctx + return __app_ctx_stack + + if name == "_request_ctx_stack": + import warnings + + 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) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index fb7f6eba..3833cb8a 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -1,28 +1,47 @@ -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 cache +from functools import lru_cache from functools import update_wrapper -from types import TracebackType +from threading import RLock import werkzeug.utils from werkzeug.exceptions import abort as _wz_abort from werkzeug.utils import redirect as _wz_redirect -from werkzeug.wrappers import Response as BaseResponse -from .globals import _cv_app -from .globals import app_ctx +from .globals import _cv_request from .globals import current_app from .globals import request +from .globals import request_ctx from .globals import session from .signals import message_flashed 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: @@ -30,7 +49,21 @@ def get_debug_flag() -> bool: :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. """ val = os.environ.get("FLASK_DEBUG") - return bool(val and val.lower() not in {"0", "false", "no"}) + + 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"} def get_load_dotenv(default: bool = True) -> bool: @@ -48,107 +81,86 @@ 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.Iterator[t.AnyStr], -) -> t.Iterator[t.AnyStr]: ... + 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. - -@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 + This function however can help you keep the context around for longer:: from flask import stream_with_context, request, Response - @app.get("/stream") + @app.route('/stream') def streamed_response(): @stream_with_context def generate(): - yield "Hello " - yield request.args["name"] - yield "!" - + yield 'Hello ' + yield request.args['name'] + yield '!' return Response(generate()) - Or use it as a wrapper around a created generator: - - .. code-block:: python + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response - @app.get("/stream") + @app.route('/stream') def streamed_response(): def generate(): - yield "Hello " - yield request.args["name"] - yield "!" - + yield 'Hello ' + yield request.args['name'] + yield '!' return Response(stream_with_context(generate())) .. versionadded:: 0.9 """ try: - gen = iter(generator_or_function) # type: ignore[arg-type] + gen = iter(generator_or_function) # type: ignore except TypeError: def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: - gen = generator_or_function(*args, **kwargs) # type: ignore[operator] + gen = generator_or_function(*args, **kwargs) # type: ignore return stream_with_context(gen) - return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] + return update_wrapper(decorator, generator_or_function) # type: ignore - def generator() -> t.Iterator[t.AnyStr]: - if (ctx := _cv_app.get(None)) is None: + def generator() -> t.Generator: + ctx = _cv_request.get(None) + if ctx is None: raise RuntimeError( "'stream_with_context' can only be used when a request" " context is active, such as in a view function." ) - with ctx: - yield None # type: ignore[misc] + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. try: yield from gen finally: - # Clean up in case the user wrapped a WSGI iterator. if hasattr(gen, "close"): gen.close() - # Execute the generator to the sentinel value. This captures the current - # context and pushes it to preserve it. Further iteration will yield from - # the original iterator. + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. wrapped_g = generator() next(wrapped_g) return wrapped_g -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 @@ -194,16 +206,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) + return current_app.make_response(args) # type: ignore def url_for( endpoint: str, *, - _anchor: str | None = None, - _method: str | None = None, - _scheme: str | None = None, - _external: bool | None = None, + _anchor: t.Optional[str] = None, + _method: t.Optional[str] = None, + _scheme: t.Optional[str] = None, + _external: t.Optional[bool] = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -252,8 +264,8 @@ def url_for( def redirect( - location: str, code: int = 303, Response: type[BaseResponse] | None = None -) -> BaseResponse: + location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None +) -> "BaseResponse": """Create a redirect response object. If :data:`~flask.current_app` is available, it will use its @@ -265,20 +277,19 @@ def redirect( :param Response: The response class to use. Not used when ``current_app`` is active, which uses ``app.response_class``. - .. versionchanged:: 3.2 - ``code`` defaults to ``303`` instead of ``302``. - .. versionadded:: 2.2 Calls ``current_app.redirect`` if available instead of always using Werkzeug's default ``redirect``. """ - if (ctx := _cv_app.get(None)) is not None: - return ctx.app.redirect(location, code=code) + if current_app: + return current_app.redirect(location, code=code) return _wz_redirect(location, code=code, Response=Response) -def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: +def abort( + code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any +) -> "te.NoReturn": """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given status code. @@ -295,8 +306,8 @@ def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn Calls ``current_app.aborter`` if available instead of always using Werkzeug's default ``abort``. """ - if (ctx := _cv_app.get(None)) is not None: - ctx.app.aborter(code, *args, **kwargs) + if current_app: + current_app.aborter(code, *args, **kwargs) _wz_abort(code, *args, **kwargs) @@ -348,10 +359,8 @@ 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( - app, - _async_wrapper=app.ensure_sync, + current_app._get_current_object(), # type: ignore message=message, category=category, ) @@ -359,7 +368,7 @@ def flash(message: str, category: str = "message") -> None: def get_flashed_messages( with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> list[str] | list[tuple[str, str]]: +) -> t.Union[t.List[str], t.List[t.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, @@ -388,10 +397,10 @@ def get_flashed_messages( :param category_filter: filter of categories to limit return values. Only categories in the list will be returned. """ - flashes = app_ctx._flashes + flashes = request_ctx.flashes if flashes is None: flashes = session.pop("_flashes") if "_flashes" in session else [] - app_ctx._flashes = flashes + request_ctx.flashes = flashes if category_filter: flashes = list(filter(lambda f: f[0] in category_filter, flashes)) if not with_categories: @@ -399,31 +408,31 @@ def get_flashed_messages( return flashes -def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: - ctx = app_ctx._get_current_object() - +def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]: if kwargs.get("max_age") is None: - kwargs["max_age"] = ctx.app.get_send_file_max_age + kwargs["max_age"] = current_app.get_send_file_max_age kwargs.update( - environ=ctx.request.environ, - use_x_sendfile=ctx.app.config["USE_X_SENDFILE"], - response_class=ctx.app.response_class, - _root_path=ctx.app.root_path, + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore ) return kwargs def send_file( - path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes], - mimetype: str | None = None, + path_or_file: t.Union[os.PathLike, str, t.BinaryIO], + mimetype: t.Optional[str] = None, as_attachment: bool = False, - download_name: str | None = None, + download_name: t.Optional[str] = None, conditional: bool = True, - etag: bool | str = True, - last_modified: datetime | int | float | None = None, - max_age: None | (int | t.Callable[[str | None], int | None]) = None, -) -> Response: + 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": """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths @@ -516,7 +525,7 @@ def send_file( .. versionchanged:: 0.7 MIME guessing and etag support for file-like objects was - removed because it was unreliable. Pass a filename if you are + deprecated because it was unreliable. Pass a filename if you are able to, otherwise attach an etag yourself. .. versionchanged:: 0.5 @@ -541,10 +550,10 @@ def send_file( def send_from_directory( - directory: os.PathLike[str] | str, - path: os.PathLike[str] | str, + directory: t.Union[os.PathLike, str], + path: t.Union[os.PathLike, str], **kwargs: t.Any, -) -> Response: +) -> "Response": """Send a file from within a directory using :func:`send_file`. .. code-block:: python @@ -564,8 +573,7 @@ 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. This *must not* - be a value provided by the client, otherwise it becomes insecure. + relative to the current application's root path. :param path: The path to the file to send, relative to ``directory``. :param kwargs: Arguments to pass to :func:`send_file`. @@ -600,24 +608,16 @@ def get_root_path(import_name: str) -> str: return os.path.dirname(os.path.abspath(mod.__file__)) # Next attempt: check the loader. - try: - spec = importlib.util.find_spec(import_name) - - if spec is None: - raise ValueError - except (ImportError, ValueError): - loader = None - else: - loader = spec.loader + loader = pkgutil.get_loader(import_name) # 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: + if loader is None or import_name == "__main__": return os.getcwd() if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) # pyright: ignore + filepath = loader.get_filename(import_name) else: # Fall back to imports. __import__(import_name) @@ -638,45 +638,68 @@ 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)) # type: ignore[no-any-return] + return os.path.dirname(os.path.abspath(filepath)) -@cache -def _split_blueprint_path(name: str) -> list[str]: - out: list[str] = [name] +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] 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 742812f2..65d8829a 100644 --- a/src/flask/json/__init__.py +++ b/src/flask/json/__init__.py @@ -3,14 +3,85 @@ 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 -def dumps(obj: t.Any, **kwargs: t.Any) -> str: +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: """Serialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -20,13 +91,13 @@ def dumps(obj: t.Any, **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. @@ -37,14 +108,28 @@ def dumps(obj: t.Any, **kwargs: t.Any) -> str: ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if current_app: - return current_app.json.dumps(obj, **kwargs) + 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) kwargs.setdefault("default", _default) return _json.dumps(obj, **kwargs) -def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: +def dump( + obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any +) -> None: """Serialize data as JSON and write to a file. If :data:`~flask.current_app` is available, it will use its @@ -56,25 +141,37 @@ def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: 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 current_app: - current_app.json.dump(obj, fp, **kwargs) + 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) else: kwargs.setdefault("default", _default) _json.dump(obj, fp, **kwargs) -def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: +def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -84,13 +181,13 @@ def loads(s: str | bytes, **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. @@ -99,13 +196,25 @@ def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if current_app: - return current_app.json.loads(s, **kwargs) + 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) return _json.loads(s, **kwargs) -def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: +def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON read from a file. If :data:`~flask.current_app` is available, it will use its @@ -115,9 +224,6 @@ def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: :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. @@ -129,19 +235,85 @@ def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: ``encoding`` will be removed in Flask 2.1. The file must be text mode, or binary mode with UTF-8 bytes. """ - if current_app: - return current_app.json.load(fp, **kwargs) + 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) 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 + ``