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..0ed4955916 --- /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@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 + 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.4.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 + steps: + - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a + # Try uploading to Test PyPI first, in case something fails. + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc + with: + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + packages_dir: artifact/ + - uses: pypa/gh-action-pypi-publish@c7f29f7adef1a245bd91520e94867e5c6eedddcc + with: + password: ${{ secrets.PYPI_TOKEN }} + packages_dir: artifact/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 674fb8b73b..79a56fcaa8 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,20 +24,21 @@ 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: 'Pallets Minimum Versions', python: '3.11', os: ubuntu-latest, tox: py311-min} + - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py37-dev} + - {name: Typing, python: '3.11', os: ubuntu-latest, tox: typing} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c + - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 with: python-version: ${{ matrix.python }} cache: 'pip' @@ -47,5 +48,11 @@ jobs: pip install -U wheel pip install -U setuptools python -m pip install -U pip + - name: cache mypy + uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12 + with: + path: ./.mypy_cache + key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }} + 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 5d767e605b..d5e7a9c8ec 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.1 hooks: - id: pyupgrade - args: ["--py36-plus"] + args: ["--py37-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.6.0 + rev: 23.1.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.1 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 e33abe8a43..cd1a04c489 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +Version 2.2.3 +------------- + +Released 2023-02-15 + +- Autoescape is enabled by default for ``.svg`` template files. :issue:`4831` +- Fix the type of ``template_folder`` to accept ``pathlib.Path``. :issue:`4892` +- Add ``--debug`` option to the ``flask run`` command. :issue:`4777` + + Version 2.2.2 ------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d5e3a3f776..8d209048b8 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -171,7 +171,7 @@ Start coding $ git push --set-upstream fork your-branch-name -.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _committing as you go: https://afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request 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/api.rst b/docs/api.rst index 880720b408..afbe0b79e6 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 @@ -248,7 +248,7 @@ HTML `` @@ -347,14 +347,14 @@ Signals .. data:: signals.signals_available - ``True`` if the signaling system is available. This is the case + ``True`` if the signaling system is available. This is the case when `blinker`_ is installed. The following signals exist in Flask: .. data:: template_rendered - This signal is sent when a template was successfully rendered. The + This signal is sent when a template was successfully rendered. The signal is invoked with the instance of the template as `template` and the context as dictionary (named `context`). @@ -388,7 +388,7 @@ The following signals exist in Flask: .. data:: request_started This signal is sent when the request context is set up, before - any request processing happens. Because the request context is already + any request processing happens. Because the request context is already bound, the subscriber can access the request with the standard global proxies such as :class:`~flask.request`. @@ -408,7 +408,7 @@ The following signals exist in Flask: Example subscriber:: def log_response(sender, response, **extra): - sender.logger.debug('Request context is about to close down. ' + sender.logger.debug('Request context is about to close down. ' 'Response: %s', response) from flask import request_finished @@ -445,8 +445,8 @@ The following signals exist in Flask: .. data:: request_tearing_down - This signal is sent when the request is tearing down. This is always - called, even if an exception is caused. Currently functions listening + This signal is sent when the request is tearing down. This is always + called, even if an exception is caused. Currently functions listening to this signal are called after the regular teardown handlers, but this is not something you can rely on. @@ -464,8 +464,8 @@ The following signals exist in Flask: .. data:: appcontext_tearing_down - This signal is sent when the app context is tearing down. This is always - called, even if an exception is caused. Currently functions listening + This signal is sent when the app context is tearing down. This is always + called, even if an exception is caused. Currently functions listening to this signal are called after the regular teardown handlers, but this is not something you can rely on. @@ -482,9 +482,9 @@ The following signals exist in Flask: .. data:: appcontext_pushed - This signal is sent when an application context is pushed. The sender - is the application. This is usually useful for unittests in order to - temporarily hook in information. For instance it can be used to + This signal is sent when an application context is pushed. The sender + is the application. This is usually useful for unittests in order to + temporarily hook in information. For instance it can be used to set a resource early onto the `g` object. Example usage:: @@ -511,8 +511,8 @@ The following signals exist in Flask: .. data:: appcontext_popped - This signal is sent when an application context is popped. The sender - is the application. This usually falls in line with the + This signal is sent when an application context is popped. The sender + is the application. This usually falls in line with the :data:`appcontext_tearing_down` signal. .. versionadded:: 0.10 @@ -520,7 +520,7 @@ The following signals exist in Flask: .. data:: message_flashed - This signal is sent when the application is flashing a message. The + This signal is sent when the application is flashing a message. The messages is sent as `message` keyword argument and the category as `category`. @@ -538,7 +538,7 @@ The following signals exist in Flask: .. class:: signals.Namespace An alias for :class:`blinker.base.Namespace` if blinker is available, - otherwise a dummy class that creates fake signals. This class is + otherwise a dummy class that creates fake signals. This class is available for Flask extensions that want to provide the same fallback system as Flask itself. @@ -579,7 +579,7 @@ Generally there are three ways to define rules for the routing system: which is exposed as :attr:`flask.Flask.url_map`. Variable parts in the route can be specified with angular brackets -(``/user/``). By default a variable part in the URL accepts any +(``/user/``). By default a variable part in the URL accepts any string without a slash however a different converter can be specified as well by using ````. @@ -613,7 +613,7 @@ Here are some examples:: pass An important detail to keep in mind is how Flask deals with trailing -slashes. The idea is to keep each URL unique so the following rules +slashes. The idea is to keep each URL unique so the following rules apply: 1. If a rule ends with a slash and is requested without a slash by the @@ -622,11 +622,11 @@ apply: 2. If a rule does not end with a trailing slash and the user requests the page with a trailing slash, a 404 not found is raised. -This is consistent with how web servers deal with static files. This +This is consistent with how web servers deal with static files. This also makes it possible to use relative link targets safely. -You can also define multiple rules for the same function. They have to be -unique however. Defaults can also be specified. Here for example is a +You can also define multiple rules for the same function. They have to be +unique however. Defaults can also be specified. Here for example is a definition for a URL that accepts an optional page:: @app.route('/users/', defaults={'page': 1}) @@ -649,33 +649,33 @@ can't preserve form data. :: pass Here are the parameters that :meth:`~flask.Flask.route` and -:meth:`~flask.Flask.add_url_rule` accept. The only difference is that +:meth:`~flask.Flask.add_url_rule` accept. The only difference is that with the route parameter the view function is defined with the decorator instead of the `view_func` parameter. =============== ========================================================== `rule` the URL rule as string -`endpoint` the endpoint for the registered URL rule. Flask itself +`endpoint` the endpoint for the registered URL rule. Flask itself assumes that the name of the view function is the name of the endpoint if not explicitly stated. `view_func` the function to call when serving a request to the - provided endpoint. If this is not provided one can + provided endpoint. If this is not provided one can specify the function later by storing it in the :attr:`~flask.Flask.view_functions` dictionary with the endpoint as key. -`defaults` A dictionary with defaults for this rule. See the +`defaults` A dictionary with defaults for this rule. See the example above for how defaults work. `subdomain` specifies the rule for the subdomain in case subdomain - matching is in use. If not specified the default + matching is in use. If not specified the default subdomain is assumed. `**options` the options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. A change to - Werkzeug is handling of method options. methods is a list + :class:`~werkzeug.routing.Rule` object. A change to + Werkzeug is handling of method options. methods is a list of methods this rule should be limited to (``GET``, ``POST`` - etc.). By default a rule just listens for ``GET`` (and - implicitly ``HEAD``). Starting with Flask 0.6, ``OPTIONS`` is + etc.). By default a rule just listens for ``GET`` (and + implicitly ``HEAD``). Starting with Flask 0.6, ``OPTIONS`` is implicitly added and handled by the standard request - handling. They have to be specified as keyword arguments. + handling. They have to be specified as keyword arguments. =============== ========================================================== @@ -687,19 +687,19 @@ customize behavior the view function would normally not have control over. The following attributes can be provided optionally to either override some defaults to :meth:`~flask.Flask.add_url_rule` or general behavior: -- `__name__`: The name of a function is by default used as endpoint. If - endpoint is provided explicitly this value is used. Additionally this +- `__name__`: The name of a function is by default used as endpoint. If + endpoint is provided explicitly this value is used. Additionally this will be prefixed with the name of the blueprint by default which cannot be customized from the function itself. - `methods`: If methods are not provided when the URL rule is added, Flask will look on the view function object itself if a `methods` - attribute exists. If it does, it will pull the information for the + attribute exists. If it does, it will pull the information for the methods from there. - `provide_automatic_options`: if this attribute is set Flask will either force enable or disable the automatic implementation of the - HTTP ``OPTIONS`` response. This can be useful when working with + HTTP ``OPTIONS`` response. This can be useful when working with decorators that want to customize the ``OPTIONS`` response on a per-view basis. diff --git a/docs/cli.rst b/docs/cli.rst index 22484f1731..be5a0b701b 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -95,7 +95,7 @@ the ``--debug`` option. .. code-block:: console - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app "hello" * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) @@ -103,6 +103,14 @@ the ``--debug`` option. * Debugger is active! * Debugger PIN: 223-456-919 +The ``--debug`` option can also be passed to the top level ``flask`` command to enable +debug mode for any command. The following two ``run`` calls are equivalent. + +.. code-block:: console + + $ flask --app hello --debug run + $ flask --app hello run --debug + Watch and Ignore Files with the Reloader ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -550,7 +558,7 @@ a name such as "flask run". Click the *Script path* dropdown and change it to *Module name*, then input ``flask``. The *Parameters* field is set to the CLI command to execute along with any arguments. -This example uses ``--app hello --debug run``, which will run the development server in +This example uses ``--app hello run --debug``, which will run the development server in debug mode. ``--app hello`` should be the import or file with your Flask app. If you installed your project as a package in your virtualenv, you may uncheck the diff --git a/docs/config.rst b/docs/config.rst index bdcbdcd43c..9db2045f7a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -47,17 +47,17 @@ Debug Mode The :data:`DEBUG` config value is special because it may behave inconsistently if changed after the app has begun setting up. In order to set debug mode reliably, use the -``--debug`` option on the ``flask`` command.``flask run`` will use the interactive -debugger and reloader by default in debug mode. +``--debug`` option on the ``flask`` or ``flask run`` command. ``flask run`` will use the +interactive debugger and reloader by default in debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug Using the option is recommended. While it is possible to set :data:`DEBUG` in your -config or code, this is strongly discouraged. It can't be read early by the ``flask`` -command, and some systems or extensions may have already configured themselves based on -a previous value. +config or code, this is strongly discouraged. It can't be read early by the +``flask run`` command, and some systems or extensions may have already configured +themselves based on a previous value. Builtin Configuration Values diff --git a/docs/debugging.rst b/docs/debugging.rst index fb3604b036..18f4286758 100644 --- a/docs/debugging.rst +++ b/docs/debugging.rst @@ -43,7 +43,7 @@ The debugger is enabled by default when the development server is run in debug m .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug When running from Python code, passing ``debug=True`` enables debug mode, which is mostly equivalent. @@ -72,7 +72,7 @@ which can interfere. .. code-block:: text - $ flask --app hello --debug run --no-debugger --no-reload + $ flask --app hello run --debug --no-debugger --no-reload When running from Python: diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 2e48682a5d..4135596a4f 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -66,7 +66,6 @@ interface. The links below are for some of the most common platforms, which have instructions for Flask, WSGI, or Python. - `PythonAnywhere `_ -- `Heroku `_ - `Google App Engine `_ - `Google Cloud Run `_ - `AWS Elastic Beanstalk `_ diff --git a/docs/deploying/waitress.rst b/docs/deploying/waitress.rst index 9b2fe13f93..eb70e058a8 100644 --- a/docs/deploying/waitress.rst +++ b/docs/deploying/waitress.rst @@ -45,10 +45,10 @@ pattern, use ``--call {module}:{factory}`` instead. .. code-block:: text # equivalent to 'from hello import app' - $ waitress-serve hello:app --host 127.0.0.1 + $ waitress-serve --host 127.0.0.1 hello:app # equivalent to 'from hello import create_app; create_app()' - $ waitress-serve --call hello:create_app --host 127.0.0.1 + $ waitress-serve --host 127.0.0.1 --call hello:create_app Serving on http://127.0.0.1:8080 diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 95745119d1..4ddb6da03b 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -184,7 +184,7 @@ context is active when a request context is, or when a CLI command is run. If you're storing something that should be closed, use :meth:`~flask.Flask.teardown_appcontext` to ensure that it gets closed when the application context ends. If it should only be valid during a -request, or would not be used in the CLI outside a reqeust, use +request, or would not be used in the CLI outside a request, use :meth:`~flask.Flask.teardown_request`. @@ -212,7 +212,7 @@ class's :meth:`~views.View.as_view` method. def __init__(self, model): self.model = model - def get(id): + def get(self, id): post = self.model.query.get(id) return jsonify(post.to_json()) diff --git a/docs/index.rst b/docs/index.rst index 983f612f3f..747749d053 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,6 +48,7 @@ community-maintained extensions to add even more functionality. config signals views + lifecycle appcontext reqcontext blueprints diff --git a/docs/lifecycle.rst b/docs/lifecycle.rst new file mode 100644 index 0000000000..2344d98ad7 --- /dev/null +++ b/docs/lifecycle.rst @@ -0,0 +1,168 @@ +Application Structure and Lifecycle +=================================== + +Flask makes it pretty easy to write a web application. But there are quite a few +different parts to an application and to each request it handles. Knowing what happens +during application setup, serving, and handling requests will help you know what's +possible in Flask and how to structure your application. + + +Application Setup +----------------- + +The first step in creating a Flask application is creating the application object. Each +Flask application is an instance of the :class:`.Flask` class, which collects all +configuration, extensions, and views. + +.. code-block:: python + + from flask import Flask + + app = Flask(__name__) + app.config.from_mapping( + SECRET_KEY="dev", + ) + app.config.from_prefixed_env() + + @app.route("/") + def index(): + return "Hello, World!" + +This is known as the "application setup phase", it's the code you write that's outside +any view functions or other handlers. It can be split up between different modules and +sub-packages, but all code that you want to be part of your application must be imported +in order for it to be registered. + +All application setup must be completed before you start serving your application and +handling requests. This is because WSGI servers divide work between multiple workers, or +can be distributed across multiple machines. If the configuration changed in one worker, +there's no way for Flask to ensure consistency between other workers. + +Flask tries to help developers catch some of these setup ordering issues by showing an +error if setup-related methods are called after requests are handled. In that case +you'll see this error: + + The setup method 'route' can no longer be called on the application. It has already + handled its first request, any changes will not be applied consistently. + Make sure all imports, decorators, functions, etc. needed to set up the application + are done before running it. + +However, it is not possible for Flask to detect all cases of out-of-order setup. In +general, don't do anything to modify the ``Flask`` app object and ``Blueprint`` objects +from within view functions that run during requests. This includes: + +- Adding routes, view functions, and other request handlers with ``@app.route``, + ``@app.errorhandler``, ``@app.before_request``, etc. +- Registering blueprints. +- Loading configuration with ``app.config``. +- Setting up the Jinja template environment with ``app.jinja_env``. +- Setting a session interface, instead of the default itsdangerous cookie. +- Setting a JSON provider with ``app.json``, instead of the default provider. +- Creating and initializing Flask extensions. + + +Serving the Application +----------------------- + +Flask is a WSGI application framework. The other half of WSGI is the WSGI server. During +development, Flask, through Werkzeug, provides a development WSGI server with the +``flask run`` CLI command. When you are done with development, use a production server +to serve your application, see :doc:`deploying/index`. + +Regardless of what server you're using, it will follow the :pep:`3333` WSGI spec. The +WSGI server will be told how to access your Flask application object, which is the WSGI +application. Then it will start listening for HTTP requests, translate the request data +into a WSGI environ, and call the WSGI application with that data. The WSGI application +will return data that is translated into an HTTP response. + +#. Browser or other client makes HTTP request. +#. WSGI server receives request. +#. WSGI server converts HTTP data to WSGI ``environ`` dict. +#. WSGI server calls WSGI application with the ``environ``. +#. Flask, the WSGI application, does all its internal processing to route the request + to a view function, handle errors, etc. +#. Flask translates View function return into WSGI response data, passes it to WSGI + server. +#. WSGI server creates and send an HTTP response. +#. Client receives the HTTP response. + + +Middleware +~~~~~~~~~~ + +The WSGI application above is a callable that behaves in a certain way. Middleware +is a WSGI application that wraps another WSGI application. It's a similar concept to +Python decorators. The outermost middleware will be called by the server. It can modify +the data passed to it, then call the WSGI application (or further middleware) that it +wraps, and so on. And it can take the return value of that call and modify it further. + +From the WSGI server's perspective, there is one WSGI application, the one it calls +directly. Typically, Flask is the "real" application at the end of the chain of +middleware. But even Flask can call further WSGI applications, although that's an +advanced, uncommon use case. + +A common middleware you'll see used with Flask is Werkzeug's +:class:`~werkzeug.middleware.proxy_fix.ProxyFix`, which modifies the request to look +like it came directly from a client even if it passed through HTTP proxies on the way. +There are other middleware that can handle serving static files, authentication, etc. + + +How a Request is Handled +------------------------ + +For us, the interesting part of the steps above is when Flask gets called by the WSGI +server (or middleware). At that point, it will do quite a lot to handle the request and +generate the response. At the most basic, it will match the URL to a view function, call +the view function, and pass the return value back to the server. But there are many more +parts that you can use to customize its behavior. + +#. WSGI server calls the Flask object, which calls :meth:`.Flask.wsgi_app`. +#. A :class:`.RequestContext` object is created. This converts the WSGI ``environ`` + dict into a :class:`.Request` object. It also creates an :class:`AppContext` object. +#. The :doc:`app context ` is pushed, which makes :data:`.current_app` and + :data:`.g` available. +#. The :data:`.appcontext_pushed` signal is sent. +#. The :doc:`request context ` is pushed, which makes :attr:`.request` and + :class:`.session` available. +#. The session is opened, loading any existing session data using the app's + :attr:`~.Flask.session_interface`, an instance of :class:`.SessionInterface`. +#. The URL is matched against the URL rules registered with the :meth:`~.Flask.route` + decorator during application setup. If there is no match, the error - usually a 404, + 405, or redirect - is stored to be handled later. +#. The :data:`.request_started` signal is sent. +#. Any :meth:`~.Flask.url_value_preprocessor` decorated functions are called. +#. Any :meth:`~.Flask.before_request` decorated functions are called. If any of + these function returns a value it is treated as the response immediately. +#. If the URL didn't match a route a few steps ago, that error is raised now. +#. The :meth:`~.Flask.route` decorated view function associated with the matched URL + is called and returns a value to be used as the response. +#. If any step so far raised an exception, and there is an :meth:`~.Flask.errorhandler` + decorated function that matches the exception class or HTTP error code, it is + called to handle the error and return a response. +#. Whatever returned a response value - a before request function, the view, or an + error handler, that value is converted to a :class:`.Response` object. +#. Any :func:`~.after_this_request` decorated functions are called, then cleared. +#. Any :meth:`~.Flask.after_request` decorated functions are called, which can modify + the response object. +#. The session is saved, persisting any modified session data using the app's + :attr:`~.Flask.session_interface`. +#. The :data:`.request_finished` signal is sent. +#. If any step so far raised an exception, and it was not handled by an error handler + function, it is handled now. HTTP exceptions are treated as responses with their + corresponding status code, other exceptions are converted to a generic 500 response. + The :data:`.got_request_exception` signal is sent. +#. The response object's status, headers, and body are returned to the WSGI server. +#. Any :meth:`~.Flask.teardown_request` decorated functions are called. +#. The :data:`.request_tearing_down` signal is sent. +#. The request context is popped, :attr:`.request` and :class:`.session` are no longer + available. +#. Any :meth:`~.Flask.teardown_appcontext` decorated functions are called. +#. The :data:`.appcontext_tearing_down` signal is sent. +#. The app context is popped, :data:`.current_app` and :data:`.g` are no longer + available. +#. The :data:`.appcontext_popped` signal is sent. + +There are even more decorators and customization points than this, but that aren't part +of every request lifecycle. They're more specific to certain things you might use during +a request, such as templates, building URLs, or handling JSON data. See the rest of this +documentation, as well as the :doc:`api` to explore further. diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 0c5e846efd..efa470a78e 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -93,7 +93,7 @@ exist yet, it is dynamically created and remembered:: from threading import Lock - class SubdomainDispatcher(object): + class SubdomainDispatcher: def __init__(self, domain, create_app): self.domain = domain @@ -148,7 +148,7 @@ request path up to the first slash:: from threading import Lock from werkzeug.wsgi import pop_path_info, peek_path_info - class PathDispatcher(object): + class PathDispatcher: def __init__(self, default_app, create_app): self.default_app = default_app diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index 415c10fa47..a76e676f32 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -91,7 +91,7 @@ To run such an application, you can use the :command:`flask` command: .. code-block:: text - $ flask run --app hello run + $ flask --app hello run Flask will automatically detect the factory if it is named ``create_app`` or ``make_app`` in ``hello``. You can also pass arguments @@ -99,7 +99,7 @@ to the factory like this: .. code-block:: text - $ flask run --app hello:create_app(local_auth=True)`` + $ flask --app hello:create_app(local_auth=True) run`` Then the ``create_app`` factory in ``myapp`` is called with the keyword argument ``local_auth=True``. See :doc:`/cli` for more detail. diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 228a04a8aa..2e9a43a731 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -1,105 +1,242 @@ -Celery Background Tasks -======================= +Background Tasks with Celery +============================ -If your application has a long running task, such as processing some uploaded -data or sending email, you don't want to wait for it to finish during a -request. Instead, use a task queue to send the necessary data to another -process that will run the task in the background while the request returns -immediately. +If your application has a long running task, such as processing some uploaded data or +sending email, you don't want to wait for it to finish during a request. Instead, use a +task queue to send the necessary data to another process that will run the task in the +background while the request returns immediately. + +`Celery`_ is a powerful task queue that can be used for simple background tasks as well +as complex multi-stage programs and schedules. This guide will show you how to configure +Celery using Flask. Read Celery's `First Steps with Celery`_ guide to learn how to use +Celery itself. + +.. _Celery: https://celery.readthedocs.io +.. _First Steps with Celery: https://celery.readthedocs.io/en/latest/getting-started/first-steps-with-celery.html + +The Flask repository contains `an example `_ +based on the information on this page, which also shows how to use JavaScript to submit +tasks and poll for progress and results. -Celery is a powerful task queue that can be used for simple background tasks -as well as complex multi-stage programs and schedules. This guide will show you -how to configure Celery using Flask, but assumes you've already read the -`First Steps with Celery `_ -guide in the Celery documentation. Install ------- -Celery is a separate Python package. Install it from PyPI using pip:: +Install Celery from PyPI, for example using pip: + +.. code-block:: text $ pip install celery -Configure ---------- -The first thing you need is a Celery instance, this is called the celery -application. It serves the same purpose as the :class:`~flask.Flask` -object in Flask, just for Celery. Since this instance is used as the -entry-point for everything you want to do in Celery, like creating tasks -and managing workers, it must be possible for other modules to import it. +Integrate Celery with Flask +--------------------------- -For instance you can place this in a ``tasks`` module. While you can use -Celery without any reconfiguration with Flask, it becomes a bit nicer by -subclassing tasks and adding support for Flask's application contexts and -hooking it up with the Flask configuration. +You can use Celery without any integration with Flask, but it's convenient to configure +it through Flask's config, and to let tasks access the Flask application. -This is all that is necessary to integrate Celery with Flask: +Celery uses similar ideas to Flask, with a ``Celery`` app object that has configuration +and registers tasks. While creating a Flask app, use the following code to create and +configure a Celery app as well. .. code-block:: python - from celery import Celery + from celery import Celery, Task - def make_celery(app): - celery = Celery(app.import_name) - celery.conf.update(app.config["CELERY_CONFIG"]) - - class ContextTask(celery.Task): - def __call__(self, *args, **kwargs): + def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: with app.app_context(): return self.run(*args, **kwargs) - celery.Task = ContextTask - return celery - -The function creates a new Celery object, configures it with the broker -from the application config, updates the rest of the Celery config from -the Flask config and then creates a subclass of the task that wraps the -task execution in an application context. + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app -.. note:: - Celery 5.x deprecated uppercase configuration keys, and 6.x will - remove them. See their official `migration guide`_. +This creates and returns a ``Celery`` app object. Celery `configuration`_ is taken from +the ``CELERY`` key in the Flask configuration. The Celery app is set as the default, so +that it is seen during each request. The ``Task`` subclass automatically runs task +functions with a Flask app context active, so that services like your database +connections are available. -.. _migration guide: https://docs.celeryproject.org/en/stable/userguide/configuration.html#conf-old-settings-map. +.. _configuration: https://celery.readthedocs.io/en/stable/userguide/configuration.html -An example task ---------------- +Here's a basic ``example.py`` that configures Celery to use Redis for communication. We +enable a result backend, but ignore results by default. This allows us to store results +only for tasks where we care about the result. -Let's write a task that adds two numbers together and returns the result. We -configure Celery's broker and backend to use Redis, create a ``celery`` -application using the factory from above, and then use it to define the task. :: +.. code-block:: python from flask import Flask - flask_app = Flask(__name__) - flask_app.config.update(CELERY_CONFIG={ - 'broker_url': 'redis://localhost:6379', - 'result_backend': 'redis://localhost:6379', - }) - celery = make_celery(flask_app) + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + celery_app = celery_init_app(app) + +Point the ``celery worker`` command at this and it will find the ``celery_app`` object. + +.. code-block:: text + + $ celery -A example worker --loglevel INFO + +You can also run the ``celery beat`` command to run tasks on a schedule. See Celery's +docs for more information about defining schedules. + +.. code-block:: text + + $ celery -A example beat --loglevel INFO + + +Application Factory +------------------- + +When using the Flask application factory pattern, call the ``celery_init_app`` function +inside the factory. It sets ``app.extensions["celery"]`` to the Celery app object, which +can be used to get the Celery app from the Flask app returned by the factory. + +.. code-block:: python + + def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + return app + +To use ``celery`` commands, Celery needs an app object, but that's no longer directly +available. Create a ``make_celery.py`` file that calls the Flask app factory and gets +the Celery app from the returned Flask app. + +.. code-block:: python + + from example import create_app + + flask_app = create_app() + celery_app = flask_app.extensions["celery"] + +Point the ``celery`` command to this file. + +.. code-block:: text + + $ celery -A make_celery worker --loglevel INFO + $ celery -A make_celery beat --loglevel INFO + - @celery.task() - def add_together(a, b): +Defining Tasks +-------------- + +Using ``@celery_app.task`` to decorate task functions requires access to the +``celery_app`` object, which won't be available when using the factory pattern. It also +means that the decorated tasks are tied to the specific Flask and Celery app instances, +which could be an issue during testing if you change configuration for a test. + +Instead, use Celery's ``@shared_task`` decorator. This creates task objects that will +access whatever the "current app" is, which is a similar concept to Flask's blueprints +and app context. This is why we called ``celery_app.set_default()`` above. + +Here's an example task that adds two numbers together and returns the result. + +.. code-block:: python + + from celery import shared_task + + @shared_task(ignore_result=False) + def add_together(a: int, b: int) -> int: return a + b -This task can now be called in the background:: +Earlier, we configured Celery to ignore task results by default. Since we want to know +the return value of this task, we set ``ignore_result=False``. On the other hand, a task +that didn't need a result, such as sending an email, wouldn't set this. + + +Calling Tasks +------------- + +The decorated function becomes a task object with methods to call it in the background. +The simplest way is to use the ``delay(*args, **kwargs)`` method. See Celery's docs for +more methods. + +A Celery worker must be running to run the task. Starting a worker is shown in the +previous sections. + +.. code-block:: python + + from flask import request - result = add_together.delay(23, 42) - result.wait() # 65 + @app.post("/add") + def start_add() -> dict[str, object]: + a = request.form.get("a", type=int) + b = request.form.get("b", type=int) + result = add_together.delay(a, b) + return {"result_id": result.id} -Run a worker ------------- +The route doesn't get the task's result immediately. That would defeat the purpose by +blocking the response. Instead, we return the running task's result id, which we can use +later to get the result. -If you jumped in and already executed the above code you will be -disappointed to learn that ``.wait()`` will never actually return. -That's because you also need to run a Celery worker to receive and execute the -task. :: - $ celery -A your_application.celery worker +Getting Results +--------------- + +To fetch the result of the task we started above, we'll add another route that takes the +result id we returned before. We return whether the task is finished (ready), whether it +finished successfully, and what the return value (or error) was if it is finished. + +.. code-block:: python + + from celery.result import AsyncResult + + @app.get("/result/") + def task_result(id: str) -> dict[str, object]: + result = AsyncResult(id) + return { + "ready": result.ready(), + "successful": result.successful(), + "value": result.result if result.ready() else None, + } + +Now you can start the task using the first route, then poll for the result using the +second route. This keeps the Flask request workers from being blocked waiting for tasks +to finish. + +The Flask repository contains `an example `_ +using JavaScript to submit tasks and poll for progress and results. + + +Passing Data to Tasks +--------------------- + +The "add" task above took two integers as arguments. To pass arguments to tasks, Celery +has to serialize them to a format that it can pass to other processes. Therefore, +passing complex objects is not recommended. For example, it would be impossible to pass +a SQLAlchemy model object, since that object is probably not serializable and is tied to +the session that queried it. + +Pass the minimal amount of data necessary to fetch or recreate any complex data within +the task. Consider a task that will run when the logged in user asks for an archive of +their data. The Flask request knows the logged in user, and has the user object queried +from the database. It got that by querying the database for a given id, so the task can +do the same thing. Pass the user's id rather than the user object. + +.. code-block:: python -The ``your_application`` string has to point to your application's package -or module that creates the ``celery`` object. + @shared_task + def generate_user_archive(user_id: str) -> None: + user = db.session.get(User, user_id) + ... -Now that the worker is running, ``wait`` will return the result once the task -is finished. + generate_user_archive.delay(current_user.id) diff --git a/docs/patterns/javascript.rst b/docs/patterns/javascript.rst index dd3bcb9b6c..4b1d7e0fb4 100644 --- a/docs/patterns/javascript.rst +++ b/docs/patterns/javascript.rst @@ -28,7 +28,7 @@ It is important to understand the difference between templates and JavaScript. Templates are rendered on the server, before the response is sent to the user's browser. JavaScript runs in the user's browser, after the template is rendered and sent. Therefore, it is impossible to use -JavaScript to affect how the Jinja template is rendered, but is is +JavaScript to affect how the Jinja template is rendered, but it is possible to render data into the JavaScript that will run. To provide data to JavaScript when rendering the template, use the diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 13f8270ee8..3c7ae425b1 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -66,6 +66,8 @@ To use the ``flask`` command and run your application you need to set the ``--app`` option that tells Flask where to find the application instance: +.. code-block:: text + $ flask --app yourapplication run What did we gain from this? Now we can restructure the application a bit diff --git a/docs/patterns/streaming.rst b/docs/patterns/streaming.rst index e35ac4ab52..c9e6ef22be 100644 --- a/docs/patterns/streaming.rst +++ b/docs/patterns/streaming.rst @@ -20,7 +20,7 @@ data and to then invoke that function and pass it to a response object:: def generate(): for row in iter_all_rows(): yield f"{','.join(row)}\n" - return generate(), {"Content-Type": "text/csv") + return generate(), {"Content-Type": "text/csv"} Each ``yield`` expression is directly sent to the browser. Note though that some WSGI middlewares might break streaming, so be careful there in diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 22c411eec2..ad9e3bc4e8 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -41,7 +41,7 @@ itself. To run the application, use the ``flask`` command or ``python -m flask``. You need to tell the Flask where your application -is with the ``-app`` option. +is with the ``--app`` option. .. code-block:: text @@ -108,7 +108,7 @@ To enable debug mode, use the ``--debug`` option. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug * Serving Flask app 'hello' * Debug mode: on * Running on http://127.0.0.1:5000 (Press CTRL+C to quit) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 2b109770e5..70ea13e366 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -69,11 +69,12 @@ everything that runs in the block will have access to :data:`request`, populated with your test data. :: def generate_report(year): - format = request.args.get('format') + format = request.args.get("format") ... with app.test_request_context( - '/make_report/2017', data={'format': 'short'}): + "/make_report/2017", query_string={"format": "short"} + ): generate_report() If you see that error somewhere else in your code not related to diff --git a/docs/server.rst b/docs/server.rst index a34dfab5dd..d38aa12089 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -24,7 +24,7 @@ debug mode. .. code-block:: text - $ flask --app hello --debug run + $ flask --app hello run --debug This enables debug mode, including the interactive debugger and reloader, and then starts the server on http://localhost:5000/. Use ``flask run --help`` to see the diff --git a/docs/templating.rst b/docs/templating.rst index 3cda995e44..f497de7333 100644 --- a/docs/templating.rst +++ b/docs/templating.rst @@ -18,7 +18,7 @@ Jinja Setup Unless customized, Jinja2 is configured by Flask as follows: - autoescaping is enabled for all templates ending in ``.html``, - ``.htm``, ``.xml`` as well as ``.xhtml`` when using + ``.htm``, ``.xml``, ``.xhtml``, as well as ``.svg`` when using :func:`~flask.templating.render_template`. - autoescaping is enabled for all strings when using :func:`~flask.templating.render_template_string`. diff --git a/docs/tutorial/factory.rst b/docs/tutorial/factory.rst index c8e2c5f4e0..39febd135f 100644 --- a/docs/tutorial/factory.rst +++ b/docs/tutorial/factory.rst @@ -137,7 +137,7 @@ follow the tutorial. .. code-block:: text - $ flask --app flaskr --debug run + $ flask --app flaskr run --debug You'll see output similar to this: diff --git a/docs/tutorial/install.rst b/docs/tutorial/install.rst index 7380b30269..f6820ebde9 100644 --- a/docs/tutorial/install.rst +++ b/docs/tutorial/install.rst @@ -41,7 +41,6 @@ to it. version='1.0.0', packages=find_packages(), include_package_data=True, - zip_safe=False, install_requires=[ 'flask', ], diff --git a/docs/views.rst b/docs/views.rst index b7c5ba200e..68a3462ad4 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -116,7 +116,10 @@ function. item = self.model.query.get_or_404(id) return render_template(self.template, item=item) - app.add_url_rule("/users/", view_func=DetailView.as_view("user_detail")) + app.add_url_rule( + "/users/", + view_func=DetailView.as_view("user_detail", User) + ) View Lifetime and ``self`` @@ -253,7 +256,7 @@ provide get (list) and post (create) methods. return self.model.query.get_or_404(id) def get(self, id): - user = self._get_item(id) + item = self._get_item(id) return jsonify(item.to_json()) def patch(self, id): @@ -294,9 +297,11 @@ provide get (list) and post (create) methods. db.session.commit() return jsonify(item.to_json()) - def register_api(app, model, url): - app.add_url_rule(f"/{name}/", view_func=ItemAPI(f"{name}-item", model)) - app.add_url_rule(f"/{name}/", view_func=GroupAPI(f"{name}-group", model)) + def register_api(app, model, name): + item = ItemAPI.as_view(f"{name}-item", model) + group = GroupAPI.as_view(f"{name}-group", model) + app.add_url_rule(f"/{name}/", view_func=item) + app.add_url_rule(f"/{name}/", view_func=group) register_api(app, User, "users") register_api(app, Story, "stories") diff --git a/examples/celery/README.md b/examples/celery/README.md new file mode 100644 index 0000000000..038eb51eb6 --- /dev/null +++ b/examples/celery/README.md @@ -0,0 +1,27 @@ +Background Tasks with Celery +============================ + +This example shows how to configure Celery with Flask, how to set up an API for +submitting tasks and polling results, and how to use that API with JavaScript. See +[Flask's documentation about Celery](https://flask.palletsprojects.com/patterns/celery/). + +From this directory, create a virtualenv and install the application into it. Then run a +Celery worker. + +```shell +$ python3 -m venv .venv +$ . ./.venv/bin/activate +$ pip install -r requirements.txt && pip install -e . +$ celery -A make_celery worker --loglevel INFO +``` + +In a separate terminal, activate the virtualenv and run the Flask development server. + +```shell +$ . ./.venv/bin/activate +$ flask -A task_app run --debug +``` + +Go to http://localhost:5000/ and use the forms to submit tasks. You can see the polling +requests in the browser dev tools and the Flask logs. You can see the tasks submitting +and completing in the Celery logs. diff --git a/examples/celery/make_celery.py b/examples/celery/make_celery.py new file mode 100644 index 0000000000..f7d138e642 --- /dev/null +++ b/examples/celery/make_celery.py @@ -0,0 +1,4 @@ +from task_app import create_app + +flask_app = create_app() +celery_app = flask_app.extensions["celery"] diff --git a/examples/celery/pyproject.toml b/examples/celery/pyproject.toml new file mode 100644 index 0000000000..88ba6b960c --- /dev/null +++ b/examples/celery/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "flask-example-celery" +version = "1.0.0" +description = "Example Flask application with Celery background tasks." +readme = "README.md" +requires-python = ">=3.7" +dependencies = ["flask>=2.2.2", "celery[redis]>=5.2.7"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/examples/celery/requirements.txt b/examples/celery/requirements.txt new file mode 100644 index 0000000000..b283401366 --- /dev/null +++ b/examples/celery/requirements.txt @@ -0,0 +1,56 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile pyproject.toml +# +amqp==5.1.1 + # via kombu +async-timeout==4.0.2 + # via redis +billiard==3.6.4.0 + # via celery +celery[redis]==5.2.7 + # via flask-example-celery (pyproject.toml) +click==8.1.3 + # via + # celery + # click-didyoumean + # click-plugins + # click-repl + # flask +click-didyoumean==0.3.0 + # via celery +click-plugins==1.1.1 + # via celery +click-repl==0.2.0 + # via celery +flask==2.2.2 + # via flask-example-celery (pyproject.toml) +itsdangerous==2.1.2 + # via flask +jinja2==3.1.2 + # via flask +kombu==5.2.4 + # via celery +markupsafe==2.1.2 + # via + # jinja2 + # werkzeug +prompt-toolkit==3.0.36 + # via click-repl +pytz==2022.7.1 + # via celery +redis==4.5.1 + # via celery +six==1.16.0 + # via click-repl +vine==5.0.0 + # via + # amqp + # celery + # kombu +wcwidth==0.2.6 + # via prompt-toolkit +werkzeug==2.2.2 + # via flask diff --git a/examples/celery/src/task_app/__init__.py b/examples/celery/src/task_app/__init__.py new file mode 100644 index 0000000000..dafff8aad8 --- /dev/null +++ b/examples/celery/src/task_app/__init__.py @@ -0,0 +1,39 @@ +from celery import Celery +from celery import Task +from flask import Flask +from flask import render_template + + +def create_app() -> Flask: + app = Flask(__name__) + app.config.from_mapping( + CELERY=dict( + broker_url="redis://localhost", + result_backend="redis://localhost", + task_ignore_result=True, + ), + ) + app.config.from_prefixed_env() + celery_init_app(app) + + @app.route("/") + def index() -> str: + return render_template("index.html") + + from . import views + + app.register_blueprint(views.bp) + return app + + +def celery_init_app(app: Flask) -> Celery: + class FlaskTask(Task): + def __call__(self, *args: object, **kwargs: object) -> object: + with app.app_context(): + return self.run(*args, **kwargs) + + celery_app = Celery(app.name, task_cls=FlaskTask) + celery_app.config_from_object(app.config["CELERY"]) + celery_app.set_default() + app.extensions["celery"] = celery_app + return celery_app diff --git a/examples/celery/src/task_app/tasks.py b/examples/celery/src/task_app/tasks.py new file mode 100644 index 0000000000..b6b3595d22 --- /dev/null +++ b/examples/celery/src/task_app/tasks.py @@ -0,0 +1,23 @@ +import time + +from celery import shared_task +from celery import Task + + +@shared_task(ignore_result=False) +def add(a: int, b: int) -> int: + return a + b + + +@shared_task() +def block() -> None: + time.sleep(5) + + +@shared_task(bind=True, ignore_result=False) +def process(self: Task, total: int) -> object: + for i in range(total): + self.update_state(state="PROGRESS", meta={"current": i + 1, "total": total}) + time.sleep(1) + + return {"current": total, "total": total} diff --git a/examples/celery/src/task_app/templates/index.html b/examples/celery/src/task_app/templates/index.html new file mode 100644 index 0000000000..4e1145cb8f --- /dev/null +++ b/examples/celery/src/task_app/templates/index.html @@ -0,0 +1,108 @@ + + + + + Codestin Search App + + +

Celery Example

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

Add

+

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

+
+
+ +
+

Result:

+ +
+

Block

+

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

+ +
+

+ +
+

Process

+

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

+
+ +
+

+ + + + diff --git a/examples/celery/src/task_app/views.py b/examples/celery/src/task_app/views.py new file mode 100644 index 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/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/tutorial/README.rst b/examples/tutorial/README.rst index a7e12ca250..1c745078bc 100644 --- a/examples/tutorial/README.rst +++ b/examples/tutorial/README.rst @@ -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/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..a735b3d0d1 --- /dev/null +++ b/requirements/build.txt @@ -0,0 +1,17 @@ +# SHA1:80754af91bfb6d1073585b046fe0a474ce868509 +# +# This file is autogenerated by pip-compile-multi +# To update, run: +# +# pip-compile-multi +# +build==0.9.0 + # via -r requirements/build.in +packaging==23.0 + # via build +pep517==0.13.0 + # via build +tomli==2.0.1 + # via + # build + # pep517 diff --git a/requirements/dev.txt b/requirements/dev.txt index 03d224c15c..41b2619ce3 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,53 +8,57 @@ -r docs.txt -r tests.txt -r typing.txt -build==0.8.0 +build==0.9.0 # via pip-tools +cachetools==5.2.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 -distlib==0.3.5 +colorama==0.4.6 + # via tox +distlib==0.3.6 # via virtualenv -filelock==3.7.1 +filelock==3.8.2 # via # tox # virtualenv -greenlet==1.1.2 ; python_version < "3.11" - # via -r requirements/tests.in -identify==2.5.3 +identify==2.5.11 # 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.1 # via -r requirements/dev.in -pip-tools==6.8.0 +pip-tools==6.12.1 # via pip-compile-multi -platformdirs==2.5.2 - # via virtualenv +platformdirs==2.6.0 + # via + # tox + # virtualenv pre-commit==2.20.0 # via -r requirements/dev.in +pyproject-api==1.2.1 + # via tox pyyaml==6.0 # via pre-commit -six==1.16.0 - # via tox toml==0.10.2 - # via - # pre-commit - # tox + # via pre-commit toposort==1.7 # via pip-compile-multi -tox==3.25.1 +tox==4.0.16 # via -r requirements/dev.in -virtualenv==20.16.2 +virtualenv==20.17.1 # via # pre-commit # tox -wheel==0.37.1 +wheel==0.38.4 # 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 2ac5f547e4..b1e46bde86 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -7,17 +7,17 @@ # alabaster==0.7.12 # via sphinx -babel==2.10.3 +babel==2.11.0 # via sphinx -certifi==2022.6.15 +certifi==2022.12.7 # via requests -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via requests docutils==0.17.1 # via # sphinx # sphinx-tabs -idna==3.3 +idna==3.4 # via requests imagesize==1.4.1 # via sphinx @@ -25,19 +25,17 @@ jinja2==3.1.2 # via sphinx markupsafe==2.1.1 # via jinja2 -packaging==21.3 +packaging==22.0 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.0.2 +pallets-sphinx-themes==2.0.3 # via -r requirements/docs.in -pygments==2.12.0 +pygments==2.13.0 # via # sphinx # sphinx-tabs -pyparsing==3.0.9 - # via packaging -pytz==2022.1 +pytz==2022.7 # via babel requests==2.28.1 # via sphinx @@ -68,5 +66,5 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.11 +urllib3==1.26.13 # 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.txt b/requirements/tests.txt index d9b7513c33..aff42de283 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -5,27 +5,19 @@ # # pip-compile-multi # -asgiref==3.5.2 +asgiref==3.6.0 # via -r requirements/tests.in -attrs==22.1.0 +attrs==22.2.0 # via pytest blinker==1.5 # via -r requirements/tests.in -greenlet==1.1.2 ; python_version < "3.11" - # via -r requirements/tests.in iniconfig==1.1.1 # via pytest -packaging==21.3 +packaging==22.0 # via pytest pluggy==1.0.0 # via pytest -py==1.11.0 - # via pytest -pyparsing==3.0.9 - # via packaging -pytest==7.1.2 +pytest==7.2.0 # via -r requirements/tests.in -python-dotenv==0.20.0 +python-dotenv==0.21.0 # via -r requirements/tests.in -tomli==2.0.1 - # via pytest diff --git a/requirements/typing.txt b/requirements/typing.txt index a842e1cdd4..ad8dd594df 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -7,21 +7,19 @@ # cffi==1.15.1 # via cryptography -cryptography==37.0.4 +cryptography==38.0.4 # via -r requirements/typing.in -mypy==0.971 +mypy==0.991 # via -r requirements/typing.in mypy-extensions==0.4.3 # via mypy pycparser==2.21 # via cffi -tomli==2.0.1 - # via mypy types-contextvars==2.4.7 # via -r requirements/typing.in types-dataclasses==0.6.6 # via -r requirements/typing.in -types-setuptools==63.2.3 +types-setuptools==65.6.0.2 # via -r requirements/typing.in -typing-extensions==4.3.0 +typing-extensions==4.4.0 # via mypy diff --git a/setup.cfg b/setup.cfg index e858d13a20..736bd50f27 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,31 +61,6 @@ 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 diff --git a/src/flask/__init__.py b/src/flask/__init__.py index e02531c578..463f55f255 100644 --- a/src/flask/__init__.py +++ b/src/flask/__init__.py @@ -42,7 +42,7 @@ from .templating import stream_template as stream_template from .templating import stream_template_string as stream_template_string -__version__ = "2.2.2" +__version__ = "2.2.3" def __getattr__(name): diff --git a/src/flask/app.py b/src/flask/app.py index db442c9edf..0ac4bbb5ae 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -384,7 +384,7 @@ def use_x_sendfile(self, value: bool) -> 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] + def json_encoder(self) -> t.Type[json.JSONEncoder]: """The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`. @@ -423,7 +423,7 @@ def json_encoder(self, value: t.Type[json.JSONEncoder]) -> None: self._json_encoder = value @property # type: ignore[override] - def json_decoder(self) -> t.Type[json.JSONDecoder]: # type: ignore[override] + def json_decoder(self) -> t.Type[json.JSONDecoder]: """The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`. @@ -558,7 +558,7 @@ def __init__( static_host: t.Optional[str] = None, host_matching: bool = False, subdomain_matching: bool = False, - template_folder: t.Optional[str] = "templates", + template_folder: t.Optional[t.Union[str, os.PathLike]] = "templates", instance_path: t.Optional[str] = None, instance_relative_config: bool = False, root_path: t.Optional[str] = None, @@ -961,11 +961,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. diff --git a/src/flask/blueprints.py b/src/flask/blueprints.py index 104f8acf0d..eb6642358d 100644 --- a/src/flask/blueprints.py +++ b/src/flask/blueprints.py @@ -176,8 +176,8 @@ class Blueprint(Scaffold): _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] + @property + def json_encoder( self, ) -> t.Union[t.Type[json.JSONEncoder], None]: """Blueprint-local JSON encoder class to use. Set to ``None`` to use the app's. @@ -210,8 +210,8 @@ def json_encoder(self, value: t.Union[t.Type[json.JSONEncoder], None]) -> None: ) self._json_encoder = value - @property # type: ignore[override] - def json_decoder( # type: ignore[override] + @property + def json_decoder( self, ) -> t.Union[t.Type[json.JSONDecoder], None]: """Blueprint-local JSON decoder class to use. Set to ``None`` to use the app's. @@ -250,7 +250,7 @@ def __init__( 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, + template_folder: t.Optional[t.Union[str, os.PathLike]] = None, url_prefix: t.Optional[str] = None, subdomain: t.Optional[str] = None, url_defaults: t.Optional[dict] = None, @@ -478,8 +478,11 @@ def add_url_rule( provide_automatic_options: t.Optional[bool] = None, **options: t.Any, ) -> None: - """Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for - the :func:`url_for` function is prefixed with the name of the blueprint. + """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.") @@ -501,8 +504,8 @@ def add_url_rule( def app_template_filter( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_filter], T_template_filter]: - """Register a custom template filter, available application wide. Like - :meth:`Flask.template_filter` but for a blueprint. + """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. @@ -518,9 +521,9 @@ def decorator(f: T_template_filter) -> T_template_filter: def add_app_template_filter( self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template filter, available application wide. Like - :meth:`Flask.add_template_filter` but for a blueprint. Works exactly - like the :meth:`app_template_filter` decorator. + """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. @@ -535,8 +538,8 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_test( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_test], T_template_test]: - """Register a custom template test, available application wide. Like - :meth:`Flask.template_test` but for a blueprint. + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. .. versionadded:: 0.10 @@ -554,9 +557,9 @@ def decorator(f: T_template_test) -> T_template_test: def add_app_template_test( self, f: ft.TemplateTestCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template test, available application wide. Like - :meth:`Flask.add_template_test` but for a blueprint. Works exactly - like the :meth:`app_template_test` decorator. + """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 @@ -573,8 +576,8 @@ def register_template(state: BlueprintSetupState) -> None: def app_template_global( self, name: t.Optional[str] = None ) -> t.Callable[[T_template_global], T_template_global]: - """Register a custom template global, available application wide. Like - :meth:`Flask.template_global` but for a blueprint. + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. .. versionadded:: 0.10 @@ -592,9 +595,9 @@ def decorator(f: T_template_global) -> T_template_global: def add_app_template_global( self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None ) -> None: - """Register a custom template global, available application wide. Like - :meth:`Flask.add_template_global` but for a blueprint. Works exactly - like the :meth:`app_template_global` decorator. + """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,8 +612,8 @@ 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) @@ -621,8 +624,8 @@ def before_app_request(self, f: T_before_request) -> T_before_request: 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. + """Register a function to run before the first request to the application is + handled by the worker. Equivalent to :meth:`.Flask.before_first_request`. .. deprecated:: 2.2 Will be removed in Flask 2.3. Run setup code when creating @@ -642,8 +645,8 @@ def before_app_first_request( @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 +655,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 +667,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) @@ -677,8 +679,8 @@ def app_context_processor( def app_errorhandler( self, code: t.Union[t.Type[Exception], int] ) -> t.Callable[[T_error_handler], T_error_handler]: - """Like :meth:`Flask.errorhandler` but for a blueprint. This - handler is used for all requests, even if outside of the blueprint. + """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 +693,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 +703,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..37a15ff2d8 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -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: diff --git a/src/flask/config.py b/src/flask/config.py index 7b6a137ada..d4fc310fe3 100644 --- a/src/flask/config.py +++ b/src/flask/config.py @@ -275,8 +275,9 @@ def from_file( def from_mapping( self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any ) -> bool: - """Updates the config like :meth:`update` ignoring items with non-upper - keys. + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + :return: Always returns ``True``. .. versionadded:: 0.11 diff --git a/src/flask/ctx.py b/src/flask/ctx.py index ca2844944c..c79c26dc96 100644 --- a/src/flask/ctx.py +++ b/src/flask/ctx.py @@ -307,7 +307,7 @@ def __init__( 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: diff --git a/src/flask/globals.py b/src/flask/globals.py index b230ef7e06..254da42b98 100644 --- a/src/flask/globals.py +++ b/src/flask/globals.py @@ -88,7 +88,7 @@ def __getattr__(name: str) -> t.Any: import warnings warnings.warn( - "'_app_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + "'_app_ctx_stack' is deprecated and will be removed in Flask 2.3.", DeprecationWarning, stacklevel=2, ) @@ -98,7 +98,7 @@ def __getattr__(name: str) -> t.Any: import warnings warnings.warn( - "'_request_ctx_stack' is deprecated and will be remoevd in Flask 2.3.", + "'_request_ctx_stack' is deprecated and will be removed in Flask 2.3.", DeprecationWarning, stacklevel=2, ) diff --git a/src/flask/helpers.py b/src/flask/helpers.py index 15990d0e82..3833cb8a0a 100644 --- a/src/flask/helpers.py +++ b/src/flask/helpers.py @@ -149,7 +149,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 @@ -287,7 +287,7 @@ def redirect( return _wz_redirect(location, code=code, Response=Response) -def abort( # type: ignore[misc] +def abort( code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any ) -> "te.NoReturn": """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given @@ -617,7 +617,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) diff --git a/src/flask/scaffold.py b/src/flask/scaffold.py index 1530a11ec8..7277b33ad3 100644 --- a/src/flask/scaffold.py +++ b/src/flask/scaffold.py @@ -93,7 +93,7 @@ def __init__( 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, + template_folder: t.Optional[t.Union[str, os.PathLike]] = None, root_path: t.Optional[str] = None, ): #: The name of the package or module that this object belongs @@ -561,6 +561,11 @@ def load_user(): a non-``None`` value, the value is handled as if it was the return value from the view, and further request handling is stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. """ self.before_request_funcs.setdefault(None, []).append(f) return f @@ -577,6 +582,11 @@ def after_request(self, f: T_after_request) -> T_after_request: ``after_request`` functions will not be called. Therefore, this should not be used for actions that must execute, such as to close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. """ self.after_request_funcs.setdefault(None, []).append(f) return f @@ -606,6 +616,11 @@ def teardown_request(self, f: T_teardown) -> T_teardown: ``try``/``except`` block and log any errors. The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. """ self.teardown_request_funcs.setdefault(None, []).append(f) return f @@ -615,7 +630,15 @@ def context_processor( self, f: T_template_context_processor, ) -> T_template_context_processor: - """Registers a template context processor function.""" + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ self.template_context_processors[None].append(f) return f @@ -635,6 +658,11 @@ def url_value_preprocessor( The function is passed the endpoint name and values dict. The return value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. """ self.url_value_preprocessors[None].append(f) return f @@ -644,6 +672,11 @@ def url_defaults(self, f: T_url_defaults) -> T_url_defaults: """Callback function for URL defaults for all view functions of the application. It's called with the endpoint and values and should update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. """ self.url_default_functions[None].append(f) return f @@ -667,6 +700,11 @@ def page_not_found(error): def special_exception_handler(error): return 'Database connection failed', 500 + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + .. versionadded:: 0.7 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error diff --git a/src/flask/testing.py b/src/flask/testing.py index ec9ebb9def..3b21b093fb 100644 --- a/src/flask/testing.py +++ b/src/flask/testing.py @@ -225,7 +225,7 @@ def open( buffered=buffered, follow_redirects=follow_redirects, ) - response.json_module = self.application.json # type: ignore[misc] + response.json_module = self.application.json # type: ignore[assignment] # Re-push contexts that were preserved during the request. while self._new_contexts: diff --git a/src/flask/views.py b/src/flask/views.py index a82f191238..f86172b43c 100644 --- a/src/flask/views.py +++ b/src/flask/views.py @@ -92,8 +92,8 @@ def as_view( :attr:`init_every_request` to ``False``, the same instance will be used for every request. - The arguments passed to this method are forwarded to the view - class ``__init__`` method. + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. .. versionchanged:: 2.2 Added the ``init_every_request`` class attribute. diff --git a/src/flask/wrappers.py b/src/flask/wrappers.py index 4b855bfccc..e36a72cb47 100644 --- a/src/flask/wrappers.py +++ b/src/flask/wrappers.py @@ -25,7 +25,7 @@ class Request(RequestBase): specific ones. """ - json_module = json + json_module: t.Any = json #: The internal URL rule that matched the request. This can be #: useful to inspect which methods are allowed for the URL from diff --git a/tests/test_appctx.py b/tests/test_appctx.py index f5ca0bde42..aa3a8b4e5c 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -120,14 +120,14 @@ def cleanup(exception): @app.route("/") def index(): - raise Exception("dummy") + raise ValueError("dummy") - with pytest.raises(Exception, match="dummy"): + with pytest.raises(ValueError, match="dummy"): with app.app_context(): client.get("/") assert len(cleanup_stuff) == 1 - assert isinstance(cleanup_stuff[0], Exception) + assert isinstance(cleanup_stuff[0], ValueError) assert str(cleanup_stuff[0]) == "dummy" diff --git a/tests/test_apps/blueprintapp/__init__.py b/tests/test_apps/blueprintapp/__init__.py index 4b05798531..ad594cf1da 100644 --- a/tests/test_apps/blueprintapp/__init__.py +++ b/tests/test_apps/blueprintapp/__init__.py @@ -2,8 +2,8 @@ app = Flask(__name__) app.config["DEBUG"] = True -from blueprintapp.apps.admin import admin -from blueprintapp.apps.frontend import frontend +from blueprintapp.apps.admin import admin # noqa: E402 +from blueprintapp.apps.frontend import frontend # noqa: E402 app.register_blueprint(admin) app.register_blueprint(frontend) diff --git a/tests/test_basic.py b/tests/test_basic.py index d547012a5a..9aca667938 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1472,11 +1472,11 @@ def test_static_route_with_host_matching(): rv = flask.url_for("static", filename="index.html", _external=True) assert rv == "http://example.com/static/index.html" # Providing static_host without host_matching=True should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, static_host="example.com") # Providing host_matching=True with static_folder # but without static_host should error. - with pytest.raises(Exception): + with pytest.raises(AssertionError): flask.Flask(__name__, host_matching=True) # Providing host_matching=True without static_host # but with static_folder=None should not error. diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index abfacb98bf..6c38b66186 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -227,7 +227,6 @@ def index(): def test_session_dynamic_cookie_name(): - # This session interface will use a cookie with a different name if the # requested url ends with the string "dynamic_cookie" class PathAwareSessionInterface(SecureCookieSessionInterface): diff --git a/tests/typing/typing_app_decorators.py b/tests/typing/typing_app_decorators.py index 3df3e716fb..6b2188aa60 100644 --- a/tests/typing/typing_app_decorators.py +++ b/tests/typing/typing_app_decorators.py @@ -10,12 +10,12 @@ @app.after_request def after_sync(response: Response) -> Response: - ... + return Response() @app.after_request async def after_async(response: Response) -> Response: - ... + return Response() @app.before_request diff --git a/tox.ini b/tox.ini index ee4d40f689..08c6dca2f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist = - py3{11,10,9,8,7},pypy3{8,7} - py310-min + py3{12,11,10,9,8,7} + pypy3{9,8,7} + py311-min py37-dev style typing @@ -9,12 +10,17 @@ envlist = skip_missing_interpreters = true [testenv] +package = wheel +wheel_build_env = .pkg envtmpdir = {toxworkdir}/tmp/{envname} deps = -r requirements/tests.txt min: -r requirements/tests-pallets-min.txt - dev: -r requirements/tests-pallets-dev.txt - + dev: https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz + dev: https://github.com/pallets/click/archive/refs/heads/main.tar.gz # examples/tutorial[test] # examples/javascript[test] # commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests examples} @@ -23,12 +29,16 @@ commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests} [testenv:style] deps = pre-commit skip_install = true -commands = pre-commit run --all-files --show-diff-on-failure +commands = pre-commit run --all-files [testenv:typing] +package = wheel +wheel_build_env = .pkg deps = -r requirements/typing.txt commands = mypy [testenv:docs] +package = wheel +wheel_build_env = .pkg deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html