diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..45198266c6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "pallets/flask", + "image": "mcr.microsoft.com/devcontainers/python:3", + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.launchArgs": [ + "-X", + "dev" + ] + } + } + }, + "onCreateCommand": ".devcontainer/on-create-command.sh" +} diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh new file mode 100755 index 0000000000..deffa37bfe --- /dev/null +++ b/.devcontainer/on-create-command.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +python3 -m venv .venv +. .venv/bin/activate +pip install -U pip setuptools wheel +pip install -r requirements/dev.txt +pip install -e . +pre-commit install --install-hooks diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..8f3b4fd4bc --- /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/workflows/lock.yaml b/.github/workflows/lock.yaml index cd89f67c9d..c790fae5cb 100644 --- a/.github/workflows/lock.yaml +++ b/.github/workflows/lock.yaml @@ -1,18 +1,25 @@ -# This does not automatically close "stale" issues. Instead, it locks closed issues after 2 weeks of no activity. -# If there's a new issue related to an old one, we've found it's much easier to work on as a new issue. - name: 'Lock threads' +# 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: + issues: write + pull-requests: write + +concurrency: + group: lock + jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@c1b35aecc5cdb1a34539d14196df55838bb2f836 with: - github-token: ${{ github.token }} issue-inactive-days: 14 pr-inactive-days: 14 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000000..82285542df --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,72 @@ +name: Publish +on: + push: + tags: + - '*' +jobs: + build: + runs-on: ubuntu-latest + outputs: + hash: ${{ steps.hash.outputs.hash }} + steps: + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b + with: + 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: 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: + 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.5.0 + with: + base64-subjects: ${{ needs.build.outputs.hash }} + create-release: + # Upload the sdist, wheels, and provenance to a GitHub release. They remain + # available as build artifacts for a while as well. + needs: ['provenance'] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + - name: create release + run: > + gh release create --draft --repo ${{ github.repository }} + ${{ github.ref_name }} + *.intoto.jsonl/* artifact/* + env: + GH_TOKEN: ${{ github.token }} + publish-pypi: + 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@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + # Try uploading to Test PyPI first, in case something fails. + - uses: pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: artifact/ + - uses: pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d + with: + packages-dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 674fb8b73b..d138c7b90b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,28 +24,29 @@ jobs: fail-fast: false matrix: include: - - {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310} - - {name: Windows, python: '3.10', os: windows-latest, tox: py310} - - {name: Mac, python: '3.10', os: macos-latest, tox: py310} - - {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311} + - {name: 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.7', os: ubuntu-latest, tox: pypy37} - - {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min} - - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev} - - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing} + - {name: 'PyPy', python: 'pypy-3.9', os: ubuntu-latest, tox: pypy39} + - {name: 'Minimum Versions', python: '3.11', os: ubuntu-latest, tox: py311-min} + - {name: '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@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - uses: actions/setup-python@57ded4d7d5e986d7296eab16560982c6dd7c923b with: python-version: ${{ matrix.python }} cache: 'pip' cache-dependency-path: 'requirements/*.txt' - - name: update pip - run: | - pip install -U wheel - pip install -U setuptools - python -m pip install -U pip + - name: cache mypy + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + with: + path: ./.mypy_cache + key: mypy|${{ matrix.python }}|${{ hashFiles('pyproject.toml') }} + if: matrix.tox == 'typing' - run: pip install tox - - run: tox -e ${{ matrix.tox }} + - run: tox run -e ${{ matrix.tox }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a1a225f0e..79b632ae84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,35 +3,34 @@ ci: autoupdate_schedule: monthly repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.3.2 hooks: - id: pyupgrade - args: ["--py36-plus"] + args: ["--py38-plus"] - repo: https://github.com/asottile/reorder_python_imports - rev: v3.8.2 + rev: v3.9.0 hooks: - id: reorder-python-imports name: Reorder Python imports (src, tests) files: "^(?!examples/)" args: ["--application-directories", "src"] - additional_dependencies: ["setuptools>60.9"] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + 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.4.6 + rev: v2.6.2 hooks: - id: pip-compile-multi-verify - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: fix-byte-order-marker - id: trailing-whitespace diff --git a/CHANGES.rst b/CHANGES.rst index 6c3ff32c97..4d651204c3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,13 +1,99 @@ +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. + + +Version 2.3.1 +------------- + +Released 2023-04-25 + +- Restore deprecated ``from flask import Markup``. :issue:`5084` + + Version 2.3.0 ------------- -Unreleased +Released 2023-04-25 + +- Drop support for Python 3.7. :pr:`5072` +- Update minimum requirements to the latest versions: Werkzeug>=2.3.0, Jinja2>3.1.2, + itsdangerous>=2.1.2, click>=8.1.3. +- Remove previously deprecated code. :pr:`4995` + + - The ``push`` and ``pop`` methods of the deprecated ``_app_ctx_stack`` and + ``_request_ctx_stack`` objects are removed. ``top`` still exists to give + extensions more time to update, but it will be removed. + - The ``FLASK_ENV`` environment variable, ``ENV`` config key, and ``app.env`` + property are removed. + - The ``session_cookie_name``, ``send_file_max_age_default``, ``use_x_sendfile``, + ``propagate_exceptions``, and ``templates_auto_reload`` properties on ``app`` + are removed. + - The ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_MIMETYPE``, and + ``JSONIFY_PRETTYPRINT_REGULAR`` config keys are removed. + - The ``app.before_first_request`` and ``bp.before_app_first_request`` decorators + are removed. + - ``json_encoder`` and ``json_decoder`` attributes on app and blueprint, and the + corresponding ``json.JSONEncoder`` and ``JSONDecoder`` classes, are removed. + - The ``json.htmlsafe_dumps`` and ``htmlsafe_dump`` functions are removed. + - Calling setup methods on blueprints after registration is an error instead of a + warning. :pr:`4997` + +- Importing ``escape`` and ``Markup`` from ``flask`` is deprecated. Import them + directly from ``markupsafe`` instead. :pr:`4996` +- The ``app.got_first_request`` property is deprecated. :pr:`4997` +- The ``locked_cached_property`` decorator is deprecated. Use a lock inside the + decorated function if locking is needed. :issue:`4993` +- Signals are always available. ``blinker>=1.6.2`` is a required dependency. The + ``signals_available`` attribute is deprecated. :issue:`5056` +- Signals support ``async`` subscriber functions. :pr:`5049` +- Remove uses of locks that could cause requests to block each other very briefly. + :issue:`4993` +- Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``. + :pr:`4947` +- Ensure subdomains are applied with nested blueprints. :issue:`4834` +- ``config.from_file`` can use ``text=False`` to indicate that the parser wants a + binary file instead. :issue:`4989` +- If a blueprint is created with an empty name it raises a ``ValueError``. + :issue:`5010` +- ``SESSION_COOKIE_DOMAIN`` does not fall back to ``SERVER_NAME``. The default is not + to set the domain, which modern browsers interpret as an exact match rather than + a subdomain match. Warnings about ``localhost`` and IP addresses are also removed. + :issue:`5051` +- The ``routes`` command shows each rule's ``subdomain`` or ``host`` when domain + matching is in use. :issue:`5004` +- Use postponed evaluation of annotations. :pr:`5071` + + +Version 2.2.5 +------------- + +Released 2023-05-02 + +- Update for compatibility with Werkzeug 2.3.3. +- Set ``Vary: Cookie`` header when the session is accessed, modified, or refreshed. + + +Version 2.2.4 +------------- + +Released 2023-04-25 + +- Update for compatibility with Werkzeug 2.3. Version 2.2.3 ------------- -Unreleased +Released 2023-02-15 + +- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` +- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` +- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` Version 2.2.2 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5e3a3f776..24daa729d0 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -67,9 +67,29 @@ Include the following in your patch: .. _pre-commit: https://pre-commit.com -First time setup -~~~~~~~~~~~~~~~~ +First time setup using GitHub Codespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`GitHub Codespaces`_ creates a development environment that is already set up for the +project. By default it opens in Visual Studio Code for the Web, but this can +be changed in your GitHub profile settings to use Visual Studio Code or JetBrains +PyCharm on your local computer. + +- Make sure you have a `GitHub account`_. +- From the project's repository page, click the green "Code" button and then "Create + codespace on main". +- The codespace will be set up, then Visual Studio Code will open. However, you'll + need to wait a bit longer for the Python extension to be installed. You'll know it's + ready when the terminal at the bottom shows that the virtualenv was activated. +- Check out a branch and `start coding`_. + +.. _GitHub Codespaces: https://docs.github.com/en/codespaces +.. _devcontainer: https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers + +First time setup in your local environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure you have a `GitHub account`_. - Download and install the `latest version of git`_. - Configure git with your `username`_ and `email`_. @@ -78,102 +98,90 @@ First time setup $ 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. +- `Clone`_ your fork locally, replacing ``your-username`` in the command below with + your actual username. .. code-block:: text - $ git clone https://github.com/pallets/flask + $ git clone https://github.com/your-username/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. - +- Create a virtualenv. Use the latest version of Python. - Linux/macOS .. code-block:: text - $ python3 -m venv env - $ . env/bin/activate + $ python3 -m venv .venv + $ . .venv/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 + > py -3 -m venv .venv + > .venv\Scripts\activate -- Install the development dependencies, then install Flask in editable - mode. +- Install the development dependencies, then install Flask in editable mode. .. code-block:: text + $ python -m pip install -U pip setuptools wheel $ pip install -r requirements/dev.txt && pip install -e . - Install the pre-commit hooks. .. code-block:: text - $ pre-commit install + $ pre-commit install --install-hooks +.. _GitHub account: https://github.com/join .. _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: 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. +- 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. + 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. +- Using your favorite editor, make your changes, `committing as you go`_. + + - If you are in a codespace, you will be prompted to `create a fork`_ the first + time you make a commit. Enter ``Y`` to continue. + +- 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 description. .. code-block:: text - $ git push --set-upstream fork your-branch-name + $ git push --set-upstream origin your-branch-name -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _create a fork: https://docs.github.com/en/codespaces/developing-in-codespaces/using-source-control-in-your-codespace#about-automatic-forking .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request +.. _Running the tests: Running the tests ~~~~~~~~~~~~~~~~~ @@ -201,6 +209,9 @@ 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. +If you are using GitHub Codespaces, ``coverage`` is already installed +so you can skip the installation command. + .. code-block:: text $ pip install coverage diff --git a/README.rst b/README.rst index 3d1c3882af..4b7ff42ad1 100644 --- a/README.rst +++ b/README.rst @@ -77,6 +77,4 @@ Links - PyPI Releases: https://pypi.org/project/Flask/ - Source Code: https://github.com/pallets/flask/ - Issue Tracker: https://github.com/pallets/flask/issues/ -- Website: https://palletsprojects.com/p/flask/ -- Twitter: https://twitter.com/PalletsTeam - Chat: https://discord.gg/pallets diff --git a/artwork/LICENSE.rst b/artwork/LICENSE.rst deleted file mode 100644 index 99c58a2127..0000000000 --- a/artwork/LICENSE.rst +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 2010 Pallets - -This logo or a modified version may be used by anyone to refer to the -Flask project, but does not indicate endorsement by the project. - -Redistribution and use in source (SVG) and binary (renders in PNG, etc.) -forms, with or without modification, are permitted provided that the -following conditions are met: - -1. Redistributions of source code must retain the above copyright - notice and this list of conditions. - -2. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -We would appreciate that you make the image a link to -https://palletsprojects.com/p/flask/ if you use it in a medium that -supports links. diff --git a/artwork/logo-full.svg b/artwork/logo-full.svg deleted file mode 100644 index 8c0748a286..0000000000 --- a/artwork/logo-full.svg +++ /dev/null @@ -1,290 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/artwork/logo-lineart.svg b/artwork/logo-lineart.svg deleted file mode 100644 index 615260dce6..0000000000 --- a/artwork/logo-lineart.svg +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/docs/_static/flask-horizontal.png b/docs/_static/flask-horizontal.png new file mode 100644 index 0000000000..a0df2c6109 Binary files /dev/null and b/docs/_static/flask-horizontal.png differ diff --git a/docs/_static/flask-icon.png b/docs/_static/flask-icon.png deleted file mode 100644 index 55cb8478ef..0000000000 Binary files a/docs/_static/flask-icon.png and /dev/null differ diff --git a/docs/_static/flask-logo.png b/docs/_static/flask-logo.png deleted file mode 100644 index ce23606157..0000000000 Binary files a/docs/_static/flask-logo.png and /dev/null differ diff --git a/docs/_static/flask-vertical.png b/docs/_static/flask-vertical.png new file mode 100644 index 0000000000..d1fd149907 Binary files /dev/null and b/docs/_static/flask-vertical.png differ diff --git a/docs/_static/no.png b/docs/_static/no.png deleted file mode 100644 index 644c3f70f9..0000000000 Binary files a/docs/_static/no.png and /dev/null differ diff --git a/docs/_static/pycharm-run-config.png b/docs/_static/pycharm-run-config.png index 3f78915796..ad025545a7 100644 Binary files a/docs/_static/pycharm-run-config.png and b/docs/_static/pycharm-run-config.png differ diff --git a/docs/_static/shortcut-icon.png b/docs/_static/shortcut-icon.png new file mode 100644 index 0000000000..4d3e6c3774 Binary files /dev/null and b/docs/_static/shortcut-icon.png differ diff --git a/docs/_static/yes.png b/docs/_static/yes.png deleted file mode 100644 index 56917ab2c6..0000000000 Binary files a/docs/_static/yes.png and /dev/null differ diff --git a/docs/api.rst b/docs/api.rst index 880720b408..043beb0775 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -3,7 +3,7 @@ API .. module:: flask -This part of the documentation covers all the interfaces of Flask. For +This part of the documentation covers all the interfaces of Flask. For parts where Flask depends on external libraries, we document the most important right here and provide links to the canonical documentation. @@ -34,12 +34,12 @@ Incoming Request Data .. attribute:: 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 + 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 a proxy. See :ref:`notes-on-proxies` for more information. + This is a proxy. See :ref:`notes-on-proxies` for more information. The request object is an instance of a :class:`~flask.Request`. @@ -69,7 +69,7 @@ To access the current session you can use the :class:`session` object: The session object works pretty much like an ordinary dict, with the difference that it keeps track of modifications. - This is a proxy. See :ref:`notes-on-proxies` for more information. + This is a proxy. See :ref:`notes-on-proxies` for more information. The following attributes are interesting: @@ -79,10 +79,10 @@ To access the current session you can use the :class:`session` object: .. attribute:: modified - ``True`` if the session object detected a modification. Be advised + ``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:: + attribute to ``True`` yourself. Here an example:: # this change is not picked up because a mutable object (here # a list) is changed. @@ -93,8 +93,8 @@ To access the current session you can use the :class:`session` object: .. 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 + :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. @@ -155,9 +155,9 @@ Application Globals To share data that is valid for one request only from one function to another, a global variable is not good enough because it would break in -threaded environments. Flask provides you with a special object that +threaded environments. Flask provides you with a special object that ensures it is only valid for the active request and that will return -different values for each request. In a nutshell: it does the right +different values for each request. In a nutshell: it does the right thing, like it does for :class:`request` and :class:`session`. .. data:: g @@ -217,10 +217,6 @@ Useful Functions and Classes .. autofunction:: send_from_directory -.. autofunction:: escape - -.. autoclass:: Markup - :members: escape, unescape, striptags Message Flashing ---------------- @@ -248,7 +244,7 @@ HTML `` @@ -270,12 +266,6 @@ HTML `` + + diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py new file mode 100644 index 0000000000..99cf92dc20 --- /dev/null +++ b/examples/celery/src/task_app/views.py @@ -0,0 +1,38 @@ +from celery.result import AsyncResult +from flask import Blueprint +from flask import request + +from . import tasks + +bp = Blueprint("tasks", __name__, url_prefix="/tasks") + + +@bp.get("/result/") +def result(id: str) -> dict[str, object]: + result = AsyncResult(id) + ready = result.ready() + return { + "ready": ready, + "successful": result.successful() if ready else None, + "value": result.get() if ready else result.result, + } + + +@bp.post("/add") +def add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = tasks.add.delay(a, b) + return {"result_id": result.id} + + +@bp.post("/block") +def block() -> dict[str, object]: + result = tasks.block.delay() + return {"result_id": result.id} + + +@bp.post("/process") +def process() -> dict[str, object]: + result = tasks.process.delay(total=request.form.get("total", type=int)) + return {"result_id": result.id} diff --git a/examples/javascript/.gitignore b/examples/javascript/.gitignore index 85a35845ad..a306afbc08 100644 --- a/examples/javascript/.gitignore +++ b/examples/javascript/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/javascript/README.rst b/examples/javascript/README.rst index 23c7ce436d..697bb21760 100644 --- a/examples/javascript/README.rst +++ b/examples/javascript/README.rst @@ -23,8 +23,8 @@ Install .. code-block:: text - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate $ pip install -e . diff --git a/examples/javascript/js_example/__init__.py b/examples/javascript/js_example/__init__.py index 068b2d98ed..0ec3ca215a 100644 --- a/examples/javascript/js_example/__init__.py +++ b/examples/javascript/js_example/__init__.py @@ -2,4 +2,4 @@ app = Flask(__name__) -from js_example import views # noqa: F401 +from js_example import views # noqa: E402, F401 diff --git a/examples/javascript/pyproject.toml b/examples/javascript/pyproject.toml new file mode 100644 index 0000000000..e74415b827 --- /dev/null +++ b/examples/javascript/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "js_example" +version = "1.1.0" +description = "Demonstrates making AJAX requests to Flask." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = ["flask"] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/patterns/jquery/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["js_example", "tests"] diff --git a/examples/javascript/setup.cfg b/examples/javascript/setup.cfg deleted file mode 100644 index f509ddfe50..0000000000 --- a/examples/javascript/setup.cfg +++ /dev/null @@ -1,29 +0,0 @@ -[metadata] -name = js_example -version = 1.1.0 -url = https://flask.palletsprojects.com/patterns/jquery/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = Demonstrates making AJAX requests to Flask. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - blinker - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - js_example diff --git a/examples/javascript/setup.py b/examples/javascript/setup.py deleted file mode 100644 index 606849326a..0000000000 --- a/examples/javascript/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore index 85a35845ad..a306afbc08 100644 --- a/examples/tutorial/.gitignore +++ b/examples/tutorial/.gitignore @@ -1,4 +1,4 @@ -venv/ +.venv/ *.pyc __pycache__/ instance/ diff --git a/examples/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca250..653c216729 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -23,13 +23,13 @@ default Git version is the main branch. :: Create a virtualenv and activate it:: - $ python3 -m venv venv - $ . venv/bin/activate + $ python3 -m venv .venv + $ . .venv/bin/activate Or on Windows cmd:: - $ py -3 -m venv venv - $ venv\Scripts\activate.bat + $ py -3 -m venv .venv + $ .venv\Scripts\activate.bat Install Flaskr:: @@ -48,7 +48,7 @@ Run .. code-block:: text $ flask --app flaskr init-db - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug Open http://127.0.0.1:5000 in a browser. diff --git a/examples/tutorial/pyproject.toml b/examples/tutorial/pyproject.toml new file mode 100644 index 0000000000..c86eb61f19 --- /dev/null +++ b/examples/tutorial/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "flaskr" +version = "1.0.0" +description = "The basic blog app built in the Flask tutorial." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +dependencies = [ + "flask", +] + +[project.urls] +Documentation = "https://flask.palletsprojects.com/tutorial/" + +[project.optional-dependencies] +test = ["pytest"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = ["error"] + +[tool.coverage.run] +branch = true +source = ["flaskr", "tests"] diff --git a/examples/tutorial/setup.cfg b/examples/tutorial/setup.cfg deleted file mode 100644 index d001093b48..0000000000 --- a/examples/tutorial/setup.cfg +++ /dev/null @@ -1,28 +0,0 @@ -[metadata] -name = flaskr -version = 1.0.0 -url = https://flask.palletsprojects.com/tutorial/ -license = BSD-3-Clause -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = The basic blog app built in the Flask tutorial. -long_description = file: README.rst -long_description_content_type = text/x-rst - -[options] -packages = find: -include_package_data = true -install_requires = - Flask - -[options.extras_require] -test = - pytest - -[tool:pytest] -testpaths = tests - -[coverage:run] -branch = True -source = - flaskr diff --git a/examples/tutorial/setup.py b/examples/tutorial/setup.py deleted file mode 100644 index 606849326a..0000000000 --- a/examples/tutorial/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..400ed59f85 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,97 @@ +[project] +name = "Flask" +description = "A simple framework for building complex web applications." +readme = "README.rst" +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +authors = [{name = "Armin Ronacher", email = "armin.ronacher@active-4.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks", +] +requires-python = ">=3.8" +dependencies = [ + "Werkzeug>=2.3.3", + "Jinja2>=3.1.2", + "itsdangerous>=2.1.2", + "click>=8.1.3", + "blinker>=1.6.2", + "importlib-metadata>=3.6.0; python_version < '3.10'", +] +dynamic = ["version"] + +[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/" +Chat = "https://discord.gg/pallets" + +[project.optional-dependencies] +async = ["asgiref>=3.2"] +dotenv = ["python-dotenv"] + +[project.scripts] +flask = "flask.cli:main" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +version = {attr = "flask.__version__"} + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", + # change in Python 3.12 alpha causes warning from inside pytest + "ignore:onerror argument:DeprecationWarning", +] + +[tool.coverage.run] +branch = true +source = ["flask", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +python_version = "3.8" +files = ["src/flask"] +show_error_codes = true +pretty = true +#strict = 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 + +[[tool.mypy.overrides]] +module = [ + "asgiref.*", + "dotenv.*", + "cryptography.*", + "importlib_metadata", +] +ignore_missing_imports = true diff --git a/requirements/build.in b/requirements/build.in new file mode 100644 index 0000000000..378eac25d3 --- /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 0000000000..196545d0e0 --- /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.txt b/requirements/dev.txt index 4796a63359..5e5bc43b73 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,49 +8,55 @@ -r docs.txt -r tests.txt -r typing.txt -build==0.8.0 +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.8.0 +filelock==3.12.0 # via # tox # virtualenv -identify==2.5.5 +identify==2.5.23 # via pre-commit nodeenv==1.7.0 # via pre-commit -pep517==0.13.0 - # via build -pip-compile-multi==2.4.6 +pip-compile-multi==2.6.2 # via -r requirements/dev.in -pip-tools==6.8.0 +pip-tools==6.13.0 # via pip-compile-multi -platformdirs==2.5.2 - # via virtualenv -pre-commit==2.20.0 +platformdirs==3.5.0 + # via + # tox + # virtualenv +pre-commit==3.3.1 # via -r requirements/dev.in -pyyaml==6.0 - # via pre-commit -six==1.16.0 +pyproject-api==1.5.1 # via tox -toml==0.10.2 +pyproject-hooks==1.0.0 + # via build +pyyaml==6.0 # via pre-commit -toposort==1.7 +toposort==1.10 # via pip-compile-multi -tox==3.26.0 +tox==4.5.1 # via -r requirements/dev.in -virtualenv==20.16.5 +virtualenv==20.23.0 # via # pre-commit # tox -wheel==0.37.1 +wheel==0.40.0 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/docs.txt b/requirements/docs.txt index db4c099a5d..c56dde85a3 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -5,41 +5,37 @@ # # pip-compile-multi # -alabaster==0.7.12 +alabaster==0.7.13 # via sphinx -babel==2.10.3 +babel==2.12.1 # via sphinx -certifi==2022.6.15.1 +certifi==2022.12.7 # via requests -charset-normalizer==2.1.1 +charset-normalizer==3.1.0 # via requests docutils==0.17.1 # via # sphinx # sphinx-tabs -idna==3.3 +idna==3.4 # via requests imagesize==1.4.1 # via sphinx jinja2==3.1.2 # via sphinx -markupsafe==2.1.1 +markupsafe==2.1.2 # via jinja2 -packaging==21.3 +packaging==23.1 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.0.2 +pallets-sphinx-themes==2.1.0 # via -r requirements/docs.in -pygments==2.13.0 +pygments==2.15.1 # via # sphinx # sphinx-tabs -pyparsing==3.0.9 - # via packaging -pytz==2022.2.1 - # via babel -requests==2.28.1 +requests==2.29.0 # via sphinx snowballstemmer==2.2.0 # via sphinx @@ -54,11 +50,11 @@ sphinx-issues==3.0.1 # via -r requirements/docs.in sphinx-tabs==3.3.1 # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==2.0.0 +sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx @@ -68,5 +64,5 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.12 +urllib3==1.26.15 # via requests diff --git a/requirements/tests-pallets-dev.in b/requirements/tests-pallets-dev.in deleted file mode 100644 index dddbe48a41..0000000000 --- a/requirements/tests-pallets-dev.in +++ /dev/null @@ -1,5 +0,0 @@ -https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz -https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz -https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz -https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz -https://github.com/pallets/click/archive/refs/heads/main.tar.gz diff --git a/requirements/tests-pallets-dev.txt b/requirements/tests-pallets-dev.txt deleted file mode 100644 index a74f556b17..0000000000 --- a/requirements/tests-pallets-dev.txt +++ /dev/null @@ -1,20 +0,0 @@ -# SHA1:692b640e7f835e536628f76de0afff1296524122 -# -# This file is autogenerated by pip-compile-multi -# To update, run: -# -# pip-compile-multi -# -click @ https://github.com/pallets/click/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -itsdangerous @ https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -jinja2 @ https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in -markupsafe @ https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz - # via - # -r requirements/tests-pallets-dev.in - # jinja2 - # werkzeug -werkzeug @ https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz - # via -r requirements/tests-pallets-dev.in diff --git a/requirements/tests-pallets-min.in b/requirements/tests-pallets-min.in index 6c8a55d9e6..5b2a6ff713 100644 --- a/requirements/tests-pallets-min.in +++ b/requirements/tests-pallets-min.in @@ -1,5 +1,6 @@ -Werkzeug==2.0.0 -Jinja2==3.0.0 -MarkupSafe==2.0.0 -itsdangerous==2.0.0 -click==8.0.0 +Werkzeug==2.3.3 +Jinja2==3.1.2 +MarkupSafe==2.1.1 +itsdangerous==2.1.2 +click==8.1.3 +blinker==1.6.2 diff --git a/requirements/tests-pallets-min.txt b/requirements/tests-pallets-min.txt index 64f0e1ce4f..1a79a37b88 100644 --- a/requirements/tests-pallets-min.txt +++ b/requirements/tests-pallets-min.txt @@ -1,19 +1,22 @@ -# SHA1:4de7d9e6254a945fd97ec10880dd23b6cd43b70d +# SHA1:0b58503b99aabc227b7f39357216d676d9987a12 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -click==8.0.0 +blinker==1.6.2 # via -r requirements/tests-pallets-min.in -itsdangerous==2.0.0 +click==8.1.3 # via -r requirements/tests-pallets-min.in -jinja2==3.0.0 +itsdangerous==2.1.2 # via -r requirements/tests-pallets-min.in -markupsafe==2.0.0 +jinja2==3.1.2 + # via -r requirements/tests-pallets-min.in +markupsafe==2.1.1 # via # -r requirements/tests-pallets-min.in # jinja2 -werkzeug==2.0.0 + # werkzeug +werkzeug==2.3.3 # via -r requirements/tests-pallets-min.in diff --git a/requirements/tests.in b/requirements/tests.in index 41936997e0..f4b3dad8b7 100644 --- a/requirements/tests.in +++ b/requirements/tests.in @@ -1,5 +1,4 @@ pytest asgiref -blinker greenlet ; python_version < "3.11" python-dotenv diff --git a/requirements/tests.txt b/requirements/tests.txt index 1292951717..29044fb5e1 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,31 +1,19 @@ -# SHA1:69cf1e101a60350e9933c6f1f3b129bd9ed1ea7c +# SHA1:42d37aff22e2f1fc447e20d483e13d6d4e066b10 # # This file is autogenerated by pip-compile-multi # To update, run: # # pip-compile-multi # -asgiref==3.5.2 +asgiref==3.6.0 # via -r requirements/tests.in -attrs==22.1.0 +iniconfig==2.0.0 # via pytest -blinker==1.5 - # via -r requirements/tests.in -greenlet==1.1.3 ; python_version < "3.11" - # via -r requirements/tests.in -iniconfig==1.1.1 - # via pytest -packaging==21.3 +packaging==23.1 # via pytest pluggy==1.0.0 # via pytest -py==1.11.0 - # via pytest -pyparsing==3.0.9 - # via packaging -pytest==7.1.3 +pytest==7.3.1 # via -r requirements/tests.in -python-dotenv==0.21.0 +python-dotenv==1.0.0 # via -r requirements/tests.in -tomli==2.0.1 - # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index 5c15669e45..82b3e7e732 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,21 +7,19 @@ # cffi==1.15.1 # via cryptography -cryptography==38.0.1 +cryptography==40.0.2 # via -r requirements/typing.in -mypy==0.971 +mypy==1.2.0 # via -r requirements/typing.in -mypy-extensions==0.4.3 +mypy-extensions==1.0.0 # via mypy pycparser==2.21 # via cffi -tomli==2.0.1 - # via mypy -types-contextvars==2.4.7 +types-contextvars==2.4.7.2 # via -r requirements/typing.in types-dataclasses==0.6.6 # via -r requirements/typing.in -types-setuptools==65.3.0 +types-setuptools==67.7.0.1 # via -r requirements/typing.in -typing-extensions==4.3.0 +typing-extensions==4.5.0 # via mypy diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e858d13a20..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,121 +0,0 @@ -[metadata] -name = Flask -version = attr: flask.__version__ -url = https://palletsprojects.com/p/flask -project_urls = - Donate = https://palletsprojects.com/donate - Documentation = https://flask.palletsprojects.com/ - Changes = https://flask.palletsprojects.com/changes/ - Source Code = https://github.com/pallets/flask/ - Issue Tracker = https://github.com/pallets/flask/issues/ - Twitter = https://twitter.com/PalletsTeam - Chat = https://discord.gg/pallets -license = BSD-3-Clause -author = Armin Ronacher -author_email = armin.ronacher@active-4.com -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = A simple framework for building complex web applications. -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Web Environment - Framework :: Flask - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Topic :: Internet :: WWW/HTTP :: Dynamic Content - Topic :: Internet :: WWW/HTTP :: WSGI - Topic :: Internet :: WWW/HTTP :: WSGI :: Application - Topic :: Software Development :: Libraries :: Application Frameworks - -[options] -packages = find: -package_dir = = src -include_package_data = True -python_requires = >= 3.7 -# Dependencies are in setup.py for GitHub's dependency graph. - -[options.packages.find] -where = src - -[options.entry_points] -console_scripts = - flask = flask.cli:main - -[tool:pytest] -testpaths = tests -filterwarnings = - error - -[coverage:run] -branch = True -source = - flask - tests - -[coverage:paths] -source = - src - */site-packages - -[flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = - # slice notation whitespace, invalid - E203 - # import at top, too many circular import fixes - E402 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # bin op line break, invalid - W503 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/flask/__init__.py: F401 - -[mypy] -files = src/flask, tests/typing -python_version = 3.7 -show_error_codes = True -allow_redefinition = True -disallow_subclassing_any = True -# disallow_untyped_calls = True -# disallow_untyped_defs = True -# disallow_incomplete_defs = True -no_implicit_optional = True -local_partial_types = True -# no_implicit_reexport = True -strict_equality = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unused_ignores = True -# warn_return_any = True -# warn_unreachable = True - -[mypy-asgiref.*] -ignore_missing_imports = True - -[mypy-blinker.*] -ignore_missing_imports = True - -[mypy-dotenv.*] -ignore_missing_imports = True - -[mypy-cryptography.*] -ignore_missing_imports = True - -[mypy-importlib_metadata] -ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 6717546774..0000000000 --- a/setup.py +++ /dev/null @@ -1,17 +0,0 @@ -from setuptools import setup - -# Metadata goes in setup.cfg. These are here for GitHub's dependency graph. -setup( - name="Flask", - install_requires=[ - "Werkzeug >= 2.2.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 185a465a51..0bef2213bf 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -1,6 +1,3 @@ -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 @@ -35,14 +32,13 @@ from .signals import request_finished as request_finished from .signals import request_started as request_started from .signals import request_tearing_down as request_tearing_down -from .signals import signals_available as signals_available from .signals import template_rendered as template_rendered from .templating import render_template as render_template from .templating import render_template_string as render_template_string from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string -__version__ = "2.3.0.dev" +__version__ = "2.3.2" def __getattr__(name): @@ -51,7 +47,7 @@ def __getattr__(name): from .globals import __app_ctx_stack warnings.warn( - "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.4.", DeprecationWarning, stacklevel=2, ) @@ -62,10 +58,45 @@ def __getattr__(name): from .globals import __request_ctx_stack warnings.warn( - "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.4.", DeprecationWarning, stacklevel=2, ) return __request_ctx_stack + if name == "escape": + import warnings + from markupsafe import escape + + warnings.warn( + "'flask.escape' is deprecated and will be removed in Flask 2.4. Import" + " 'markupsafe.escape' instead.", + DeprecationWarning, + stacklevel=2, + ) + return escape + + if name == "Markup": + import warnings + from markupsafe import Markup + + warnings.warn( + "'flask.Markup' is deprecated and will be removed in Flask 2.4. Import" + " 'markupsafe.Markup' instead.", + DeprecationWarning, + stacklevel=2, + ) + return Markup + + if name == "signals_available": + import warnings + + warnings.warn( + "'signals_available' is deprecated and will be removed in Flask 2.4." + " Signals are always available", + DeprecationWarning, + stacklevel=2, + ) + return True + raise AttributeError(name) diff --git a/src/flask/app.py b/src/flask/app.py index ce4dcf6a7d..3b6b38d8ad 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1,6 +1,5 @@ -import functools -import inspect -import json +from __future__ import annotations + import logging import os import sys @@ -8,9 +7,10 @@ import weakref from collections.abc import Iterator as _abc_Iterator from datetime import timedelta +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 @@ -27,7 +27,7 @@ from werkzeug.routing import RoutingException from werkzeug.routing import Rule from werkzeug.serving import is_running_from_reloader -from werkzeug.urls import url_quote +from werkzeug.utils import cached_property from werkzeug.utils import redirect as _wz_redirect from werkzeug.wrappers import Response as BaseResponse @@ -48,7 +48,6 @@ from .helpers import get_debug_flag from .helpers import get_flashed_messages from .helpers import get_load_dotenv -from .helpers import locked_cached_property from .json.provider import DefaultJSONProvider from .json.provider import JSONProvider from .logging import create_logger @@ -70,14 +69,10 @@ from .wrappers import Response if t.TYPE_CHECKING: # pragma: no cover - import typing_extensions as te from .blueprints import Blueprint from .testing import FlaskClient from .testing import FlaskCliRunner -T_before_first_request = t.TypeVar( - "T_before_first_request", bound=ft.BeforeFirstRequestCallable -) T_shell_context_processor = t.TypeVar( "T_shell_context_processor", bound=ft.ShellContextProcessorCallable ) @@ -86,21 +81,8 @@ T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) -if sys.version_info >= (3, 8): - iscoroutinefunction = inspect.iscoroutinefunction -else: - - def iscoroutinefunction(func: t.Any) -> bool: - while inspect.ismethod(func): - func = func.__func__ - - while isinstance(func, functools.partial): - func = func.func - - return inspect.iscoroutinefunction(func) - -def _make_timedelta(value: t.Union[timedelta, int, None]) -> t.Optional[timedelta]: +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: if value is None or isinstance(value, timedelta): return value @@ -274,36 +256,6 @@ class Flask(Scaffold): #: :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. @@ -315,153 +267,7 @@ def session_cookie_name(self, value: str) -> None: "PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta ) - @property - def send_file_max_age_default(self) -> t.Optional[timedelta]: - """The default value for ``max_age`` for :func:`~flask.send_file`. The default - is ``None``, which tells the browser to use conditional requests instead of a - timed cache. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use - ``app.config["SEND_FILE_MAX_AGE_DEFAULT"]`` instead. - - .. versionchanged:: 2.0 - Defaults to ``None`` instead of 12 hours. - """ - import warnings - - warnings.warn( - "'send_file_max_age_default' is deprecated and will be removed in Flask" - " 2.3. Use 'SEND_FILE_MAX_AGE_DEFAULT' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - return _make_timedelta(self.config["SEND_FILE_MAX_AGE_DEFAULT"]) - - @send_file_max_age_default.setter - def send_file_max_age_default(self, value: t.Union[int, timedelta, None]) -> None: - import warnings - - warnings.warn( - "'send_file_max_age_default' is deprecated and will be removed in Flask" - " 2.3. Use 'SEND_FILE_MAX_AGE_DEFAULT' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["SEND_FILE_MAX_AGE_DEFAULT"] = _make_timedelta(value) - - @property - def use_x_sendfile(self) -> bool: - """Enable this to use the ``X-Sendfile`` feature, assuming the server supports - it, from :func:`~flask.send_file`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.config["USE_X_SENDFILE"]`` instead. - """ - import warnings - - warnings.warn( - "'use_x_sendfile' is deprecated and will be removed in Flask 2.3. Use" - " 'USE_X_SENDFILE' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.config["USE_X_SENDFILE"] - - @use_x_sendfile.setter - def use_x_sendfile(self, value: bool) -> None: - import warnings - - warnings.warn( - "'use_x_sendfile' is deprecated and will be removed in Flask 2.3. Use" - " 'USE_X_SENDFILE' in 'app.config' instead.", - DeprecationWarning, - stacklevel=2, - ) - self.config["USE_X_SENDFILE"] = value - - _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None - _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None - - @property # type: ignore[override] - def json_encoder(self) -> t.Type[json.JSONEncoder]: # type: ignore[override] - """The JSON encoder class to use. Defaults to - :class:`~flask.json.JSONEncoder`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'app.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - - if self._json_encoder is None: - from . import json - - return json.JSONEncoder - - return self._json_encoder - - @json_encoder.setter - def json_encoder(self, value: t.Type[json.JSONEncoder]) -> None: - import warnings - - warnings.warn( - "'app.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_encoder = value - - @property # type: ignore[override] - def json_decoder(self) -> t.Type[json.JSONDecoder]: # type: ignore[override] - """The JSON decoder class to use. Defaults to - :class:`~flask.json.JSONDecoder`. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'app.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - - if self._json_decoder is None: - from . import json - - return json.JSONDecoder - - return self._json_decoder - - @json_decoder.setter - def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: - import warnings - - warnings.warn( - "'app.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_decoder = value - - json_provider_class: t.Type[JSONProvider] = DefaultJSONProvider + json_provider_class: 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. @@ -487,7 +293,6 @@ def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: #: Default configuration parameters. default_config = ImmutableDict( { - "ENV": None, "DEBUG": None, "TESTING": False, "PROPAGATE_EXCEPTIONS": None, @@ -509,10 +314,6 @@ def json_decoder(self, value: t.Type[json.JSONDecoder]) -> 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, } @@ -534,7 +335,7 @@ def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: #: client class. Defaults to :class:`~flask.testing.FlaskClient`. #: #: .. versionadded:: 0.7 - test_client_class: t.Optional[t.Type["FlaskClient"]] = None + test_client_class: type[FlaskClient] | None = None #: The :class:`~click.testing.CliRunner` subclass, by default #: :class:`~flask.testing.FlaskCliRunner` that is used by @@ -542,7 +343,7 @@ def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: #: Flask app object as the first argument. #: #: .. versionadded:: 1.0 - test_cli_runner_class: t.Optional[t.Type["FlaskCliRunner"]] = None + test_cli_runner_class: type[FlaskCliRunner] | None = None #: the session interface to use. By default an instance of #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. @@ -553,15 +354,15 @@ def json_decoder(self, value: t.Type[json.JSONDecoder]) -> None: def __init__( self, import_name: str, - static_url_path: t.Optional[str] = None, - static_folder: t.Optional[t.Union[str, os.PathLike]] = "static", - static_host: t.Optional[str] = None, + static_url_path: str | None = None, + static_folder: str | os.PathLike | None = "static", + static_host: str | None = None, host_matching: bool = False, subdomain_matching: bool = False, - template_folder: t.Optional[str] = "templates", - instance_path: t.Optional[str] = None, + template_folder: str | os.PathLike | None = "templates", + instance_path: str | None = None, instance_relative_config: bool = False, - root_path: t.Optional[str] = None, + root_path: str | None = None, ): super().__init__( import_name=import_name, @@ -621,34 +422,23 @@ def __init__( #: 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] + self.url_build_error_handlers: list[ + t.Callable[[Exception, str, 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] = [] + self.teardown_appcontext_funcs: 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] = [] + self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] #: Maps registered blueprint names to blueprint objects. The #: dict retains the order the blueprints were registered in. @@ -656,7 +446,7 @@ def __init__( #: not track how often they were attached. #: #: .. versionadded:: 0.7 - self.blueprints: t.Dict[str, "Blueprint"] = {} + self.blueprints: dict[str, Blueprint] = {} #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and @@ -692,7 +482,6 @@ def __init__( # 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. @@ -729,7 +518,7 @@ def _check_setup_finished(self, f_name: str) -> None: " running it." ) - @locked_cached_property + @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 @@ -746,29 +535,7 @@ def name(self) -> str: # type: ignore return os.path.splitext(os.path.basename(fn))[0] return self.import_name - @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. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - - .. versionadded:: 0.7 - """ - import warnings - - 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 - - @locked_cached_property + @cached_property def logger(self) -> logging.Logger: """A standard Python :class:`~logging.Logger` for the app, with the same name as :attr:`name`. @@ -795,7 +562,7 @@ def logger(self) -> logging.Logger: """ return create_logger(self) - @locked_cached_property + @cached_property def jinja_env(self) -> Environment: """The Jinja environment used to load templates. @@ -810,8 +577,18 @@ def got_first_request(self) -> bool: """This attribute is set to ``True`` if the application started handling the first request. + .. deprecated:: 2.3 + Will be removed in Flask 2.4. + .. versionadded:: 0.8 """ + import warnings + + warnings.warn( + "'got_first_request' is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) return self._got_first_request def make_config(self, instance_relative: bool = False) -> Config: @@ -827,7 +604,6 @@ def make_config(self, instance_relative: bool = False) -> Config: 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) @@ -868,42 +644,6 @@ def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyS """ 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` and the various Jinja-related methods of the app. Changing @@ -961,11 +701,14 @@ def select_jinja_autoescape(self, filename: str) -> bool: """Returns ``True`` if autoescaping should be active for the given template name. If no template name is given, returns `True`. + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + .. versionadded:: 0.5 """ if filename is None: return True - return filename.endswith((".html", ".htm", ".xml", ".xhtml")) + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) def update_template_context(self, context: dict) -> None: """Update the template context with some commonly used variables. @@ -978,7 +721,7 @@ def update_template_context(self, context: dict) -> None: :param context: the context as a dictionary that is updated in place to add extra variables. """ - names: t.Iterable[t.Optional[str]] = (None,) + names: t.Iterable[str | None] = (None,) # A template may be rendered outside a request context. if request: @@ -1007,40 +750,6 @@ def make_shell_context(self) -> dict: 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 @@ -1063,9 +772,9 @@ def debug(self, value: bool) -> None: def run( self, - host: t.Optional[str] = None, - port: t.Optional[int] = None, - debug: t.Optional[bool] = None, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, load_dotenv: bool = True, **options: t.Any, ) -> None: @@ -1141,16 +850,8 @@ def run( if get_load_dotenv(load_dotenv): cli.load_dotenv() - # if set, let env vars override previous values - if "FLASK_ENV" in os.environ: - print( - "'FLASK_ENV' is deprecated and will not be used in" - " Flask 2.3. Use 'FLASK_DEBUG' instead.", - file=sys.stderr, - ) - self.config["ENV"] = os.environ.get("FLASK_ENV") or "production" - self.debug = get_debug_flag() - elif "FLASK_DEBUG" in os.environ: + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: self.debug = get_debug_flag() # debug passed to method overrides all other sources @@ -1192,7 +893,7 @@ def run( # without reloader and that stuff from an interactive shell. self._got_first_request = False - def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> "FlaskClient": + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: """Creates a test client for this application. For information about unit testing head over to :doc:`/testing`. @@ -1245,12 +946,12 @@ def __init__(self, *args, **kwargs): """ cls = self.test_client_class if cls is None: - from .testing import FlaskClient as cls # type: ignore + from .testing import FlaskClient as cls return cls( # type: ignore self, self.response_class, use_cookies=use_cookies, **kwargs ) - def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: """Create a CLI runner for testing CLI commands. See :ref:`testing-cli`. @@ -1263,12 +964,12 @@ def test_cli_runner(self, **kwargs: t.Any) -> "FlaskCliRunner": cls = self.test_cli_runner_class if cls is None: - from .testing import FlaskCliRunner as cls # type: ignore + from .testing import FlaskCliRunner as cls return cls(self, **kwargs) # type: ignore @setupmethod - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: + 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. @@ -1295,7 +996,7 @@ def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: """ blueprint.register(self, options) - def iter_blueprints(self) -> t.ValuesView["Blueprint"]: + def iter_blueprints(self) -> t.ValuesView[Blueprint]: """Iterates over all blueprints by the order they were registered. .. versionadded:: 0.11 @@ -1306,9 +1007,9 @@ def iter_blueprints(self) -> t.ValuesView["Blueprint"]: 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, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, **options: t.Any, ) -> None: if endpoint is None: @@ -1363,7 +1064,7 @@ def add_url_rule( @setupmethod def template_filter( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_filter], T_template_filter]: """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function @@ -1385,7 +1086,7 @@ def decorator(f: T_template_filter) -> T_template_filter: @setupmethod def add_template_filter( - self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None + self, f: ft.TemplateFilterCallable, name: str | None = None ) -> None: """Register a custom template filter. Works exactly like the :meth:`template_filter` decorator. @@ -1397,7 +1098,7 @@ def add_template_filter( @setupmethod def template_test( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_test], T_template_test]: """A decorator that is used to register custom template test. You can specify a name for the test, otherwise the function @@ -1426,7 +1127,7 @@ def decorator(f: T_template_test) -> T_template_test: @setupmethod def add_template_test( - self, f: ft.TemplateTestCallable, name: t.Optional[str] = None + self, f: ft.TemplateTestCallable, name: str | None = None ) -> None: """Register a custom template test. Works exactly like the :meth:`template_test` decorator. @@ -1440,7 +1141,7 @@ def add_template_test( @setupmethod def template_global( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_global], T_template_global]: """A decorator that is used to register a custom template global function. You can specify a name for the global function, otherwise the function @@ -1464,7 +1165,7 @@ def decorator(f: T_template_global) -> T_template_global: @setupmethod def add_template_global( - self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None + self, f: ft.TemplateGlobalCallable, name: str | None = None ) -> None: """Register a custom template global function. Works exactly like the :meth:`template_global` decorator. @@ -1476,32 +1177,6 @@ def add_template_global( """ 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 @@ -1547,7 +1222,7 @@ def shell_context_processor( self.shell_context_processors.append(f) return f - def _find_error_handler(self, e: Exception) -> t.Optional[ft.ErrorHandlerCallable]: + def _find_error_handler(self, e: Exception) -> ft.ErrorHandlerCallable | None: """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 @@ -1572,7 +1247,7 @@ def _find_error_handler(self, e: Exception) -> t.Optional[ft.ErrorHandlerCallabl def handle_http_exception( self, e: HTTPException - ) -> t.Union[HTTPException, ft.ResponseReturnValue]: + ) -> 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. @@ -1642,7 +1317,7 @@ def trap_http_exception(self, e: Exception) -> bool: def handle_user_exception( self, e: Exception - ) -> t.Union[HTTPException, ft.ResponseReturnValue]: + ) -> 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 @@ -1679,7 +1354,7 @@ def handle_exception(self, e: Exception) -> Response: Always sends the :data:`got_request_exception` signal. - If :attr:`propagate_exceptions` is ``True``, such as in debug + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug mode, the error will be re-raised so that the debugger can display it. Otherwise, the original exception is logged, and an :exc:`~werkzeug.exceptions.InternalServerError` is returned. @@ -1701,7 +1376,7 @@ def handle_exception(self, e: Exception) -> Response: .. versionadded:: 0.3 """ exc_info = sys.exc_info() - got_request_exception.send(self, exception=e) + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) propagate = self.config["PROPAGATE_EXCEPTIONS"] if propagate is None: @@ -1716,7 +1391,7 @@ def handle_exception(self, e: Exception) -> Response: raise e self.log_exception(exc_info) - server_error: t.Union[InternalServerError, ft.ResponseReturnValue] + server_error: InternalServerError | ft.ResponseReturnValue server_error = InternalServerError(original_exception=e) handler = self._find_error_handler(server_error) @@ -1727,9 +1402,7 @@ def handle_exception(self, e: Exception) -> Response: def log_exception( self, - exc_info: t.Union[ - t.Tuple[type, BaseException, TracebackType], t.Tuple[None, None, None] - ], + exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), ) -> None: """Logs an exception. This is called by :meth:`handle_exception` if debugging is disabled and right before the handler is called. @@ -1742,7 +1415,7 @@ def log_exception( f"Exception on {request.path} [{request.method}]", exc_info=exc_info ) - def raise_routing_exception(self, request: Request) -> "te.NoReturn": + 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 @@ -1792,7 +1465,7 @@ def dispatch_request(self) -> ft.ResponseReturnValue: ): return self.make_default_options_response() # otherwise dispatch to the handler for that endpoint - view_args: t.Dict[str, t.Any] = req.view_args # type: ignore[assignment] + view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) def full_dispatch_request(self) -> Response: @@ -1802,19 +1475,10 @@ def full_dispatch_request(self) -> Response: .. versionadded:: 0.7 """ - # Run before_first_request functions if this is the thread's first request. - # Inlined to avoid a method call on subsequent requests. - # This is deprecated, will be removed in Flask 2.3. - if not self._got_first_request: - with self._before_request_lock: - if not self._got_first_request: - for func in self.before_first_request_funcs: - self.ensure_sync(func)() - - self._got_first_request = True + self._got_first_request = True try: - request_started.send(self) + request_started.send(self, _async_wrapper=self.ensure_sync) rv = self.preprocess_request() if rv is None: rv = self.dispatch_request() @@ -1824,7 +1488,7 @@ def full_dispatch_request(self) -> Response: def finalize_request( self, - rv: t.Union[ft.ResponseReturnValue, HTTPException], + rv: ft.ResponseReturnValue | HTTPException, from_error_handler: bool = False, ) -> Response: """Given the return value from a view function this finalizes @@ -1842,7 +1506,9 @@ def finalize_request( response = self.make_response(rv) try: response = self.process_response(response) - request_finished.send(self, response=response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) except Exception: if not from_error_handler: raise @@ -1864,7 +1530,7 @@ def make_default_options_response(self) -> Response: rv.allow.update(methods) return rv - def should_ignore_error(self, error: t.Optional[BaseException]) -> bool: + def should_ignore_error(self, error: BaseException | None) -> bool: """This is called to figure out if an error should be ignored or not as far as the teardown system is concerned. If this function returns ``True`` then the teardown handlers will not be @@ -1915,10 +1581,10 @@ def url_for( self, endpoint: str, *, - _anchor: t.Optional[str] = None, - _method: t.Optional[str] = None, - _scheme: t.Optional[str] = None, - _external: t.Optional[bool] = None, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -2031,7 +1697,8 @@ def url_for( return self.handle_url_build_error(error, endpoint, values) if _anchor is not None: - rv = f"{rv}#{url_quote(_anchor)}" + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" return rv @@ -2189,9 +1856,7 @@ def make_response(self, rv: ft.ResponseReturnValue) -> Response: return rv - def create_url_adapter( - self, request: t.Optional[Request] - ) -> t.Optional[MapAdapter]: + 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. @@ -2238,7 +1903,7 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None: .. versionadded:: 0.7 """ - names: t.Iterable[t.Optional[str]] = (None,) + names: t.Iterable[str | None] = (None,) # url_for may be called outside a request context, parse the # passed endpoint instead of using request.blueprints. @@ -2253,7 +1918,7 @@ def inject_url_defaults(self, endpoint: str, values: dict) -> None: func(endpoint, values) def handle_url_build_error( - self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any] + self, error: BuildError, endpoint: str, values: dict[str, t.Any] ) -> str: """Called by :meth:`.url_for` if a :exc:`~werkzeug.routing.BuildError` was raised. If this returns @@ -2286,7 +1951,7 @@ def handle_url_build_error( raise error - def preprocess_request(self) -> t.Optional[ft.ResponseReturnValue]: + def preprocess_request(self) -> ft.ResponseReturnValue | None: """Called before the request is dispatched. Calls :attr:`url_value_preprocessors` registered with the app and the current blueprint (if any). Then calls :attr:`before_request_funcs` @@ -2342,7 +2007,7 @@ def process_response(self, response: Response) -> Response: return response def do_teardown_request( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore + self, exc: BaseException | None = _sentinel # type: ignore ) -> None: """Called after the request is dispatched and the response is returned, right before the request context is popped. @@ -2372,10 +2037,10 @@ def do_teardown_request( for func in reversed(self.teardown_request_funcs[name]): self.ensure_sync(func)(exc) - request_tearing_down.send(self, exc=exc) + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) def do_teardown_appcontext( - self, exc: t.Optional[BaseException] = _sentinel # type: ignore + self, exc: BaseException | None = _sentinel # type: ignore ) -> None: """Called right before the application context is popped. @@ -2397,7 +2062,7 @@ def do_teardown_appcontext( for func in reversed(self.teardown_appcontext_funcs): self.ensure_sync(func)(exc) - appcontext_tearing_down.send(self, exc=exc) + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) def app_context(self) -> AppContext: """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` @@ -2518,7 +2183,7 @@ def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any: start the response. """ ctx = self.request_context(environ) - error: t.Optional[BaseException] = None + error: BaseException | None = None try: try: ctx.push() diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 104f8acf0d..0407f86fef 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -1,4 +1,5 @@ -import json +from __future__ import annotations + import os import typing as t from collections import defaultdict @@ -15,9 +16,6 @@ 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) @@ -42,8 +40,8 @@ class BlueprintSetupState: def __init__( self, - blueprint: "Blueprint", - app: "Flask", + blueprint: Blueprint, + app: Flask, options: t.Any, first_registration: bool, ) -> None: @@ -89,8 +87,8 @@ def __init__( def add_url_rule( self, rule: str, - endpoint: t.Optional[str] = None, - view_func: t.Optional[t.Callable] = None, + endpoint: str | None = None, + view_func: t.Callable | None = None, **options: t.Any, ) -> None: """A helper method to register a rule (and optionally a view function) @@ -173,89 +171,18 @@ class Blueprint(Scaffold): _got_registered_once = False - _json_encoder: t.Union[t.Type[json.JSONEncoder], None] = None - _json_decoder: t.Union[t.Type[json.JSONDecoder], None] = None - - @property # type: ignore[override] - def json_encoder( # type: ignore[override] - self, - ) -> t.Union[t.Type[json.JSONEncoder], None]: - """Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'bp.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._json_encoder - - @json_encoder.setter - def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None: - import warnings - - warnings.warn( - "'bp.json_encoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_encoder = value - - @property # type: ignore[override] - def json_decoder( # type: ignore[override] - self, - ) -> t.Union[t.Type[json.JSONDecoder], None]: - """Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Customize - :attr:`json_provider_class` instead. - - .. versionadded:: 0.10 - """ - import warnings - - warnings.warn( - "'bp.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - return self._json_decoder - - @json_decoder.setter - def json_decoder(self, value: t.Union[t.Type[json.JSONDecoder], None]) -> None: - import warnings - - warnings.warn( - "'bp.json_decoder' is deprecated and will be removed in Flask 2.3." - " Customize 'app.json_provider_class' or 'app.json' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._json_decoder = value - def __init__( self, name: str, import_name: str, - static_folder: t.Optional[t.Union[str, os.PathLike]] = None, - static_url_path: t.Optional[str] = None, - template_folder: t.Optional[str] = None, - url_prefix: t.Optional[str] = None, - subdomain: t.Optional[str] = None, - url_defaults: t.Optional[dict] = None, - root_path: t.Optional[str] = None, - cli_group: t.Optional[str] = _sentinel, # type: ignore + static_folder: str | os.PathLike | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore ): super().__init__( import_name=import_name, @@ -265,36 +192,32 @@ def __init__( root_path=root_path, ) + if not name: + raise ValueError("'name' may not be empty.") + if "." in name: raise ValueError("'name' may not contain a dot '.' character.") self.name = name self.url_prefix = url_prefix self.subdomain = subdomain - self.deferred_functions: t.List[DeferredSetupFunction] = [] + self.deferred_functions: list[DeferredSetupFunction] = [] if url_defaults is None: url_defaults = {} self.url_values_defaults = url_defaults self.cli_group = cli_group - self._blueprints: t.List[t.Tuple["Blueprint", dict]] = [] + self._blueprints: list[tuple[Blueprint, dict]] = [] def _check_setup_finished(self, f_name: str) -> None: if self._got_registered_once: - import warnings - - 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, + raise AssertionError( + f"The setup method '{f_name}' can no longer be called on the blueprint" + f" '{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." ) @setupmethod @@ -321,7 +244,7 @@ def wrapper(state: BlueprintSetupState) -> None: self.record(update_wrapper(wrapper, func)) def make_setup_state( - self, app: "Flask", options: dict, first_registration: bool = False + 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. @@ -330,7 +253,7 @@ def make_setup_state( return BlueprintSetupState(self, app, options, first_registration) @setupmethod - def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: + 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. @@ -347,7 +270,7 @@ def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None: raise ValueError("Cannot register a blueprint on itself") self._blueprints.append((blueprint, options)) - def register(self, app: "Flask", options: dict) -> None: + 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 @@ -358,6 +281,13 @@ def register(self, app: "Flask", options: dict) -> None: :param options: Keyword arguments forwarded from :meth:`~Flask.register_blueprint`. + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + + .. versionchanged:: 2.1 + Registering the same blueprint with the same name multiple + times is an error. + .. versionchanged:: 2.0.1 Nested blueprints are registered with their dotted name. This allows different blueprints with the same name to be @@ -368,10 +298,6 @@ def register(self, app: "Flask", options: dict) -> None: 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) @@ -453,6 +379,17 @@ def extend(bp_dict, parent_dict): for blueprint, bp_options in self._blueprints: bp_options = bp_options.copy() bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain if bp_url_prefix is None: bp_url_prefix = blueprint.url_prefix @@ -473,13 +410,16 @@ def extend(bp_dict, parent_dict): 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, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, **options: t.Any, ) -> None: - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. + """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.") @@ -499,10 +439,10 @@ def add_url_rule( @setupmethod def app_template_filter( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_filter], T_template_filter]: - """Register a custom template filter, available application wide. Like - :meth:`Flask.template_filter` but for a blueprint. + """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. @@ -516,11 +456,11 @@ def decorator(f: T_template_filter) -> T_template_filter: @setupmethod def add_app_template_filter( - self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None + self, f: ft.TemplateFilterCallable, name: str | None = None ) -> None: - """Register a custom template filter, available application wide. Like - :meth:`Flask.add_template_filter` but for a blueprint. Works exactly - like the :meth:`app_template_filter` decorator. + """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. @@ -533,10 +473,10 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def app_template_test( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_test], T_template_test]: - """Register a custom template test, available application wide. Like - :meth:`Flask.template_test` but for a blueprint. + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. .. versionadded:: 0.10 @@ -552,11 +492,11 @@ def decorator(f: T_template_test) -> T_template_test: @setupmethod def add_app_template_test( - self, f: ft.TemplateTestCallable, name: t.Optional[str] = None + self, f: ft.TemplateTestCallable, name: str | None = None ) -> None: - """Register a custom template test, available application wide. Like - :meth:`Flask.add_template_test` but for a blueprint. Works exactly - like the :meth:`app_template_test` decorator. + """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 @@ -571,10 +511,10 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def app_template_global( - self, name: t.Optional[str] = None + self, name: str | None = None ) -> t.Callable[[T_template_global], T_template_global]: - """Register a custom template global, available application wide. Like - :meth:`Flask.template_global` but for a blueprint. + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. .. versionadded:: 0.10 @@ -590,11 +530,11 @@ def decorator(f: T_template_global) -> T_template_global: @setupmethod def add_app_template_global( - self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None + self, f: ft.TemplateGlobalCallable, name: str | None = None ) -> None: - """Register a custom template global, available application wide. Like - :meth:`Flask.add_template_global` but for a blueprint. Works exactly - like the :meth:`app_template_global` decorator. + """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 @@ -609,41 +549,18 @@ def register_template(state: BlueprintSetupState) -> None: @setupmethod def before_app_request(self, f: T_before_request) -> T_before_request: - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a blueprint. + """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: - """Like :meth:`Flask.before_first_request`. Such a function is - executed before the first request to the application. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Run setup code when creating - the application instead. - """ - import warnings - - warnings.warn( - "'before_app_first_request' is deprecated and will be" - " removed in Flask 2.3. Use 'record_once' instead to run" - " setup code when registering the blueprint.", - DeprecationWarning, - stacklevel=2, - ) - self.record_once(lambda s: s.app.before_first_request_funcs.append(f)) - return f - @setupmethod def after_app_request(self, f: T_after_request) -> T_after_request: - """Like :meth:`Flask.after_request` but for a blueprint. Such a function - is executed after each request, even if outside of the blueprint. + """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) @@ -652,9 +569,8 @@ def after_app_request(self, f: T_after_request) -> T_after_request: @setupmethod def teardown_app_request(self, f: T_teardown) -> T_teardown: - """Like :meth:`Flask.teardown_request` but for a blueprint. Such a - function is executed when tearing down each request, even if outside of - the blueprint. + """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) @@ -665,8 +581,8 @@ def teardown_app_request(self, f: T_teardown) -> T_teardown: def app_context_processor( self, f: T_template_context_processor ) -> T_template_context_processor: - """Like :meth:`Flask.context_processor` but for a blueprint. Such a - function is executed each request, even if outside of the blueprint. + """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) @@ -675,10 +591,10 @@ def app_context_processor( @setupmethod def app_errorhandler( - self, code: t.Union[t.Type[Exception], int] + self, code: type[Exception] | int ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`Flask.errorhandler` but for a blueprint. This - handler is used for all requests, even if outside of the blueprint. + """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: @@ -691,7 +607,9 @@ def decorator(f: T_error_handler) -> T_error_handler: def app_url_value_preprocessor( self, f: T_url_value_preprocessor ) -> T_url_value_preprocessor: - """Same as :meth:`url_value_preprocessor` but application wide.""" + """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) ) @@ -699,7 +617,9 @@ def app_url_value_preprocessor( @setupmethod def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: - """Same as :meth:`url_defaults` but application wide.""" + """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) ) diff --git a/src/flask/cli.py b/src/flask/cli.py index 82fe8194eb..f7e1f29353 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -9,7 +9,7 @@ import traceback import typing as t from functools import update_wrapper -from operator import attrgetter +from operator import itemgetter import click from click.core import ParameterSource @@ -285,7 +285,7 @@ def __init__( self.create_app = create_app #: A dictionary with arbitrary data that can be associated with #: this script info. - self.data: t.Dict[t.Any, t.Any] = {} + self.data: dict[t.Any, t.Any] = {} self.set_debug_flag = set_debug_flag self._loaded_app: Flask | None = None @@ -933,6 +933,9 @@ def app(environ, start_response): ) +run_command.params.insert(0, _debug_option) + + @click.command("shell", short_help="Run a shell in the app context.") @with_appcontext def shell_command() -> None: @@ -986,49 +989,62 @@ def shell_command() -> None: @click.option( "--sort", "-s", - type=click.Choice(("endpoint", "methods", "rule", "match")), + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), default="endpoint", help=( - 'Method to sort routes by. "match" is the order that Flask will match ' - "routes when dispatching a request." + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." ), ) @click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") @with_appcontext def routes_command(sort: str, all_methods: bool) -> None: """Show all registered routes with endpoints and methods.""" - rules = list(current_app.url_map.iter_rules()) + if not rules: click.echo("No routes were registered.") return - ignored_methods = set(() if all_methods else ("HEAD", "OPTIONS")) + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] - if sort in ("endpoint", "rule"): - rules = sorted(rules, key=attrgetter(sort)) - elif sort == "methods": - rules = sorted(rules, key=lambda rule: sorted(rule.methods)) # type: ignore + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] - rule_methods = [ - ", ".join(sorted(rule.methods - ignored_methods)) # type: ignore - for rule in rules - ] + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") - headers = ("Endpoint", "Methods", "Rule") - widths = ( - max(len(rule.endpoint) for rule in rules), - max(len(methods) for methods in rule_methods), - max(len(rule.rule) for rule in rules), - ) - widths = [max(len(h), w) for h, w in zip(headers, widths)] - row = "{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}".format(*widths) + row.append(rule.rule) + rows.append(row) + + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] + + 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 - click.echo(row.format(*headers).strip()) - click.echo(row.format(*("-" * width for width in widths))) + 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 rule, methods in zip(rules, rule_methods): - click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) + for row in rows: + click.echo(template.format(*row)) cli = FlaskGroup( diff --git a/src/flask/config.py b/src/flask/config.py index d4fc310fe3..a73dd786b7 100644 --- a/src/flask/config.py +++ b/src/flask/config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import errno import json import os @@ -10,7 +12,7 @@ class ConfigAttribute: """Makes an attribute forward to the config""" - def __init__(self, name: str, get_converter: t.Optional[t.Callable] = None) -> None: + def __init__(self, name: str, get_converter: t.Callable | None = None) -> None: self.__name__ = name self.get_converter = get_converter @@ -70,7 +72,7 @@ class Config(dict): :param defaults: an optional dictionary of default values """ - def __init__(self, root_path: str, defaults: t.Optional[dict] = None) -> None: + def __init__(self, root_path: str, defaults: dict | None = None) -> None: super().__init__(defaults or {}) self.root_path = root_path @@ -191,7 +193,7 @@ def from_pyfile(self, filename: str, silent: bool = False) -> bool: self.from_object(d) return True - def from_object(self, obj: t.Union[object, str]) -> None: + def from_object(self, obj: object | str) -> None: """Updates the values from the given object. An object can be of one of the following two types: @@ -234,6 +236,7 @@ def from_file( 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 @@ -244,8 +247,8 @@ def from_file( import json app.config.from_file("config.json", load=json.load) - import toml - app.config.from_file("config.toml", load=toml.load) + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) :param filename: The path to the data file. This can be an absolute path or relative to the config root path. @@ -254,14 +257,18 @@ def from_file( :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` implements a ``read`` method. :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. :return: ``True`` if the file was loaded successfully. + .. versionchanged:: 2.3 + The ``text`` parameter was added. + .. versionadded:: 2.0 """ filename = os.path.join(self.root_path, filename) try: - with open(filename) as f: + with open(filename, "r" if text else "rb") as f: obj = load(f) except OSError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): @@ -273,7 +280,7 @@ def from_file( return self.from_mapping(obj) def from_mapping( - self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any ) -> bool: """Updates the config like :meth:`update` ignoring items with non-upper keys. @@ -282,7 +289,7 @@ def from_mapping( .. versionadded:: 0.11 """ - mappings: t.Dict[str, t.Any] = {} + mappings: dict[str, t.Any] = {} if mapping is not None: mappings.update(mapping) mappings.update(kwargs) @@ -293,7 +300,7 @@ def from_mapping( def get_namespace( self, namespace: str, lowercase: bool = True, trim_namespace: bool = True - ) -> t.Dict[str, t.Any]: + ) -> dict[str, t.Any]: """Returns a dictionary containing a subset of configuration options that match the specified namespace/prefix. Example usage:: diff --git a/src/flask/ctx.py b/src/flask/ctx.py index ca2844944c..b37e4e04a6 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextvars import sys import typing as t @@ -60,7 +62,7 @@ def __delattr__(self, name: str) -> None: except KeyError: raise AttributeError(name) from None - def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any: + def get(self, name: str, default: t.Any | None = None) -> t.Any: """Get an attribute by name, or a default value. Like :meth:`dict.get`. @@ -233,18 +235,18 @@ class AppContext: running CLI commands. """ - def __init__(self, app: "Flask") -> None: + def __init__(self, app: Flask) -> None: self.app = app self.url_adapter = app.create_url_adapter(None) self.g: _AppCtxGlobals = app.app_ctx_globals_class() - self._cv_tokens: t.List[contextvars.Token] = [] + self._cv_tokens: list[contextvars.Token] = [] def push(self) -> None: """Binds the app context to the current context.""" self._cv_tokens.append(_cv_app.set(self)) - appcontext_pushed.send(self.app) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) - def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore """Pops the app context.""" try: if len(self._cv_tokens) == 1: @@ -260,17 +262,17 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno f"Popped wrong app context. ({ctx!r} instead of {self!r})" ) - appcontext_popped.send(self.app) + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) - def __enter__(self) -> "AppContext": + def __enter__(self) -> AppContext: self.push() return self def __exit__( self, - exc_type: t.Optional[type], - exc_value: t.Optional[BaseException], - tb: t.Optional[TracebackType], + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, ) -> None: self.pop(exc_value) @@ -299,31 +301,31 @@ class RequestContext: def __init__( self, - app: "Flask", + app: Flask, environ: dict, - request: t.Optional["Request"] = None, - session: t.Optional["SessionMixin"] = None, + request: Request | None = None, + session: SessionMixin | None = None, ) -> None: self.app = app if request is None: request = app.request_class(environ) - request.json_module = app.json # type: ignore[misc] + 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 + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session # Functions that should be executed after the request on the response # object. These will be called before the regular "after_request" # functions. - self._after_request_functions: t.List[ft.AfterRequestCallable] = [] + self._after_request_functions: list[ft.AfterRequestCallable] = [] - self._cv_tokens: t.List[t.Tuple[contextvars.Token, t.Optional[AppContext]]] = [] + self._cv_tokens: list[tuple[contextvars.Token, AppContext | None]] = [] - def copy(self) -> "RequestContext": + 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 @@ -382,7 +384,7 @@ def push(self) -> None: if self.url_adapter is not None: self.match_request() - def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore """Pops the request context and unbinds it by doing that. This will also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. @@ -419,15 +421,15 @@ def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: igno f"Popped wrong request context. ({ctx!r} instead of {self!r})" ) - def __enter__(self) -> "RequestContext": + 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], + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, ) -> None: self.pop(exc_value) diff --git a/src/flask/debughelpers.py b/src/flask/debughelpers.py index b0639892c4..6061441a89 100644 --- a/src/flask/debughelpers.py +++ b/src/flask/debughelpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing as t from .app import Flask diff --git a/src/flask/globals.py b/src/flask/globals.py index 254da42b98..e9cd4acfcd 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import typing as t from contextvars import ContextVar @@ -17,30 +19,17 @@ def __init__(self, name: str, cv: ContextVar[t.Any]) -> None: self.name = name self.cv = cv - def _warn(self): + @property + def top(self) -> t.Any | None: import warnings 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.", + f"'_{self.name}_ctx_stack' is deprecated and will be removed in Flask 2.4." + f" Use 'g' to store data, or '{self.name}_ctx' to access the current" + " context.", DeprecationWarning, - stacklevel=3, + stacklevel=2, ) - - def push(self, obj: t.Any) -> None: - self._warn() - self.cv.set(obj) - - def pop(self) -> t.Any: - self._warn() - ctx = self.cv.get(None) - self.cv.set(None) - return ctx - - @property - def top(self) -> t.Optional[t.Any]: - self._warn() return self.cv.get(None) @@ -51,15 +40,15 @@ def top(self) -> t.Optional[t.Any]: 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") +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") __app_ctx_stack = _FakeStack("app", _cv_app) -app_ctx: "AppContext" = LocalProxy( # type: ignore[assignment] +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] _cv_app, unbound_message=_no_app_msg ) -current_app: "Flask" = LocalProxy( # type: ignore[assignment] +current_app: Flask = LocalProxy( # type: ignore[assignment] _cv_app, "app", unbound_message=_no_app_msg ) -g: "_AppCtxGlobals" = LocalProxy( # type: ignore[assignment] +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] _cv_app, "g", unbound_message=_no_app_msg ) @@ -70,15 +59,15 @@ def top(self) -> t.Optional[t.Any]: an active HTTP request. Consult the documentation on testing for information about how to avoid this problem.\ """ -_cv_request: ContextVar["RequestContext"] = ContextVar("flask.request_ctx") +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") __request_ctx_stack = _FakeStack("request", _cv_request) -request_ctx: "RequestContext" = LocalProxy( # type: ignore[assignment] +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] _cv_request, unbound_message=_no_req_msg ) -request: "Request" = LocalProxy( # type: ignore[assignment] +request: Request = LocalProxy( # type: ignore[assignment] _cv_request, "request", unbound_message=_no_req_msg ) -session: "SessionMixin" = LocalProxy( # type: ignore[assignment] +session: SessionMixin = LocalProxy( # type: ignore[assignment] _cv_request, "session", unbound_message=_no_req_msg ) @@ -88,7 +77,7 @@ def __getattr__(name: str) -> t.Any: import warnings warnings.warn( - "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.4.", DeprecationWarning, stacklevel=2, ) @@ -98,7 +87,7 @@ def __getattr__(name: str) -> t.Any: import warnings warnings.warn( - "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.4.", DeprecationWarning, stacklevel=2, ) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 15990d0e82..61a0f818f3 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import os import pkgutil import socket import sys import typing as t +import warnings from datetime import datetime from functools import lru_cache from functools import update_wrapper @@ -22,26 +25,6 @@ if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response as BaseResponse from .wrappers import Response - import typing_extensions as te - - -def get_env() -> str: - """Get the environment the app is running in, indicated by the - :envvar:`FLASK_ENV` environment variable. The default is - ``'production'``. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. - """ - import warnings - - warnings.warn( - "'FLASK_ENV' and 'get_env' are deprecated and will be removed" - " in Flask 2.3. Use 'FLASK_DEBUG' instead.", - DeprecationWarning, - stacklevel=2, - ) - return os.environ.get("FLASK_ENV") or "production" def get_debug_flag() -> bool: @@ -49,21 +32,7 @@ def get_debug_flag() -> bool: :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. """ val = os.environ.get("FLASK_DEBUG") - - if not val: - env = os.environ.get("FLASK_ENV") - - if env is not None: - print( - "'FLASK_ENV' is deprecated and will not be used in" - " Flask 2.3. Use 'FLASK_DEBUG' instead.", - file=sys.stderr, - ) - return env == "development" - - return False - - return val.lower() not in {"0", "false", "no"} + return bool(val and val.lower() not in {"0", "false", "no"}) def get_load_dotenv(default: bool = True) -> bool: @@ -82,9 +51,9 @@ def get_load_dotenv(default: bool = True) -> bool: def stream_with_context( - generator_or_function: t.Union[ - t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]] - ] + generator_or_function: ( + t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]] + ) ) -> t.Iterator[t.AnyStr]: """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 @@ -149,7 +118,7 @@ def generator() -> t.Generator: yield from gen finally: if hasattr(gen, "close"): - gen.close() # type: ignore + gen.close() # The trick is to start the generator. Then the code execution runs until # the first dummy None is yielded at which point the context was already @@ -160,7 +129,7 @@ def generator() -> t.Generator: 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 @@ -212,10 +181,10 @@ def index(): def url_for( endpoint: str, *, - _anchor: t.Optional[str] = None, - _method: t.Optional[str] = None, - _scheme: t.Optional[str] = None, - _external: t.Optional[bool] = None, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, **values: t.Any, ) -> str: """Generate a URL to the given endpoint with the given values. @@ -264,8 +233,8 @@ def url_for( def redirect( - location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None -) -> "BaseResponse": + location: str, code: int = 302, Response: type[BaseResponse] | None = None +) -> BaseResponse: """Create a redirect response object. If :data:`~flask.current_app` is available, it will use its @@ -287,9 +256,7 @@ def redirect( return _wz_redirect(location, code=code, Response=Response) -def abort( # type: ignore[misc] - code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any -) -> "te.NoReturn": +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given status code. @@ -359,8 +326,10 @@ def flash(message: str, category: str = "message") -> None: flashes = session.get("_flashes", []) flashes.append((category, message)) session["_flashes"] = flashes + app = current_app._get_current_object() # type: ignore message_flashed.send( - current_app._get_current_object(), # type: ignore + app, + _async_wrapper=app.ensure_sync, message=message, category=category, ) @@ -368,7 +337,7 @@ def flash(message: str, category: str = "message") -> None: def get_flashed_messages( with_categories: bool = False, category_filter: t.Iterable[str] = () -) -> t.Union[t.List[str], t.List[t.Tuple[str, str]]]: +) -> list[str] | list[tuple[str, str]]: """Pulls all flashed messages from the session and returns them. Further calls in the same request to the function will return the same messages. By default just the messages are returned, @@ -408,7 +377,7 @@ def get_flashed_messages( return flashes -def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]: +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: if kwargs.get("max_age") is None: kwargs["max_age"] = current_app.get_send_file_max_age @@ -422,17 +391,15 @@ def _prepare_send_file_kwargs(**kwargs: t.Any) -> t.Dict[str, t.Any]: def send_file( - path_or_file: t.Union[os.PathLike, str, t.BinaryIO], - mimetype: t.Optional[str] = None, + path_or_file: os.PathLike | str | t.BinaryIO, + mimetype: str | None = None, as_attachment: bool = False, - download_name: t.Optional[str] = None, + download_name: str | None = None, conditional: bool = True, - etag: t.Union[bool, str] = True, - last_modified: t.Optional[t.Union[datetime, int, float]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, -) -> "Response": + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths @@ -550,10 +517,10 @@ def send_file( def send_from_directory( - directory: t.Union[os.PathLike, str], - path: t.Union[os.PathLike, str], + directory: os.PathLike | str, + path: os.PathLike | str, **kwargs: t.Any, -) -> "Response": +) -> Response: """Send a file from within a directory using :func:`send_file`. .. code-block:: python @@ -617,7 +584,7 @@ def get_root_path(import_name: str) -> str: return os.getcwd() if hasattr(loader, "get_filename"): - filepath = loader.get_filename(import_name) # type: ignore + filepath = loader.get_filename(import_name) else: # Fall back to imports. __import__(import_name) @@ -646,6 +613,10 @@ class locked_cached_property(werkzeug.utils.cached_property): :class:`werkzeug.utils.cached_property` except access uses a lock for thread safety. + .. deprecated:: 2.3 + Will be removed in Flask 2.4. Use a lock inside the decorated function if + locking is needed. + .. versionchanged:: 2.0 Inherits from Werkzeug's ``cached_property`` (and ``property``). """ @@ -653,9 +624,17 @@ class locked_cached_property(werkzeug.utils.cached_property): def __init__( self, fget: t.Callable[[t.Any], t.Any], - name: t.Optional[str] = None, - doc: t.Optional[str] = None, + name: str | None = None, + doc: str | None = None, ) -> None: + import warnings + + warnings.warn( + "'locked_cached_property' is deprecated and will be removed in Flask 2.4." + " Use a lock inside the decorated function if locking is needed.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(fget, name=name, doc=doc) self.lock = RLock() @@ -683,7 +662,16 @@ def is_ip(value: str) -> bool: :return: True if string is an IP address :rtype: bool + + .. deprecated:: 2.3 + Will be removed in Flask 2.4. """ + warnings.warn( + "The 'is_ip' function is deprecated and will be removed in Flask 2.4.", + DeprecationWarning, + stacklevel=2, + ) + for family in (socket.AF_INET, socket.AF_INET6): try: socket.inet_pton(family, value) @@ -696,8 +684,8 @@ def is_ip(value: str) -> bool: @lru_cache(maxsize=None) -def _split_blueprint_path(name: str) -> t.List[str]: - out: t.List[str] = [name] +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] if "." in name: out.extend(_split_blueprint_path(name.rpartition(".")[0])) diff --git a/src/flask/json/__init__.py b/src/flask/json/__init__.py index 65d8829acb..f15296fed0 100644 --- a/src/flask/json/__init__.py +++ b/src/flask/json/__init__.py @@ -3,85 +3,14 @@ import json as _json import typing as t -from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps - from ..globals import current_app from .provider import _default if t.TYPE_CHECKING: # pragma: no cover - from ..app import Flask from ..wrappers import Response -class JSONEncoder(_json.JSONEncoder): - """The default JSON encoder. Handles extra types compared to the - built-in :class:`json.JSONEncoder`. - - - :class:`datetime.datetime` and :class:`datetime.date` are - serialized to :rfc:`822` strings. This is the same as the HTTP - date format. - - :class:`decimal.Decimal` is serialized to a string. - - :class:`uuid.UUID` is serialized to a string. - - :class:`dataclasses.dataclass` is passed to - :func:`dataclasses.asdict`. - - :class:`~markupsafe.Markup` (or any object with a ``__html__`` - method) will call the ``__html__`` method to get a string. - - Assign a subclass of this to :attr:`flask.Flask.json_encoder` or - :attr:`flask.Blueprint.json_encoder` to override the default. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.json`` instead. - """ - - def __init__(self, **kwargs) -> None: - import warnings - - warnings.warn( - "'JSONEncoder' is deprecated and will be removed in" - " Flask 2.3. Use 'Flask.json' to provide an alternate" - " JSON implementation instead.", - DeprecationWarning, - stacklevel=3, - ) - super().__init__(**kwargs) - - def default(self, o: t.Any) -> t.Any: - """Convert ``o`` to a JSON serializable type. See - :meth:`json.JSONEncoder.default`. Python does not support - overriding how basic types like ``str`` or ``list`` are - serialized, they are handled before this method. - """ - return _default(o) - - -class JSONDecoder(_json.JSONDecoder): - """The default JSON decoder. - - This does not change any behavior from the built-in - :class:`json.JSONDecoder`. - - Assign a subclass of this to :attr:`flask.Flask.json_decoder` or - :attr:`flask.Blueprint.json_decoder` to override the default. - - .. deprecated:: 2.2 - Will be removed in Flask 2.3. Use ``app.json`` instead. - """ - - def __init__(self, **kwargs) -> None: - import warnings - - warnings.warn( - "'JSONDecoder' is deprecated and will be removed in" - " Flask 2.3. Use 'Flask.json' to provide an alternate" - " JSON implementation instead.", - DeprecationWarning, - stacklevel=3, - ) - super().__init__(**kwargs) - - -def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: +def dumps(obj: t.Any, **kwargs: t.Any) -> str: """Serialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -91,13 +20,13 @@ def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: :param obj: The data to serialize. :param kwargs: Arguments passed to the ``dumps`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.dumps``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0.2 :class:`decimal.Decimal` is supported by converting to a string. @@ -108,28 +37,14 @@ def dumps(obj: t.Any, *, app: Flask | None = None, **kwargs: t.Any) -> str: ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.dumps' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.dumps(obj, **kwargs) + if current_app: + return current_app.json.dumps(obj, **kwargs) kwargs.setdefault("default", _default) return _json.dumps(obj, **kwargs) -def dump( - obj: t.Any, fp: t.IO[str], *, app: Flask | None = None, **kwargs: t.Any -) -> None: +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: """Serialize data as JSON and write to a file. If :data:`~flask.current_app` is available, it will use its @@ -141,37 +56,25 @@ def dump( encoding to be valid JSON. :param kwargs: Arguments passed to the ``dump`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.dump``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0 Writing to a binary file, and the ``encoding`` argument, will be removed in Flask 2.1. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.dump' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - app.json.dump(obj, fp, **kwargs) + if current_app: + current_app.json.dump(obj, fp, **kwargs) else: kwargs.setdefault("default", _default) _json.dump(obj, fp, **kwargs) -def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: """Deserialize data as JSON. If :data:`~flask.current_app` is available, it will use its @@ -181,13 +84,13 @@ def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any :param s: Text or UTF-8 bytes. :param kwargs: Arguments passed to the ``loads`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.loads``, allowing an app to override the behavior. - .. versionchanged:: 2.2 - The ``app`` parameter will be removed in Flask 2.3. - .. versionchanged:: 2.0 ``encoding`` will be removed in Flask 2.1. The data must be a string or UTF-8 bytes. @@ -196,25 +99,13 @@ def loads(s: str | bytes, *, app: Flask | None = None, **kwargs: t.Any) -> t.Any ``app`` can be passed directly, rather than requiring an app context for configuration. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.loads' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.loads(s, **kwargs) + if current_app: + return current_app.json.loads(s, **kwargs) return _json.loads(s, **kwargs) -def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t.Any: +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: """Deserialize data as JSON read from a file. If :data:`~flask.current_app` is available, it will use its @@ -224,6 +115,9 @@ def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t. :param fp: A file opened for reading text or UTF-8 bytes. :param kwargs: Arguments passed to the ``load`` implementation. + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + .. versionchanged:: 2.2 Calls ``current_app.json.load``, allowing an app to override the behavior. @@ -235,78 +129,12 @@ def load(fp: t.IO[t.AnyStr], *, app: Flask | None = None, **kwargs: t.Any) -> t. ``encoding`` will be removed in Flask 2.1. The file must be text mode, or binary mode with UTF-8 bytes. """ - if app is not None: - import warnings - - warnings.warn( - "The 'app' parameter is deprecated and will be removed in" - " Flask 2.3. Call 'app.json.load' directly instead.", - DeprecationWarning, - stacklevel=2, - ) - else: - app = current_app - - if app: - return app.json.load(fp, **kwargs) + if current_app: + return current_app.json.load(fp, **kwargs) return _json.load(fp, **kwargs) -def htmlsafe_dumps(obj: t.Any, **kwargs: t.Any) -> str: - """Serialize an object to a string of JSON with :func:`dumps`, then - replace HTML-unsafe characters with Unicode escapes and mark the - result safe with :class:`~markupsafe.Markup`. - - This is available in templates as the ``|tojson`` filter. - - The returned string is safe to render in HTML documents and - ``