Merge branch 'pallets:main' into main

This commit is contained in:
Peace 2022-01-12 12:35:24 +02:00 committed by GitHub
commit 0d0817bc30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1086 additions and 715 deletions

View file

@ -1,8 +1,19 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: pip - package-ecosystem: "pip"
directory: "/requirements"
target-branch: "2.0.x"
versioning-strategy: "lockfile-only"
schedule:
interval: "monthly"
day: "monday"
time: "16:00"
timezone: "UTC"
open-pull-requests-limit: 99
- package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: "monthly"
time: "08:00" day: "monday"
open-pull-requests-limit: 99 time: "16:00"
timezone: "UTC"

View file

@ -8,8 +8,8 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2 - uses: dessant/lock-threads@v3
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-lock-inactive-days: 14 issue-inactive-days: 14
pr-lock-inactive-days: 14 pr-inactive-days: 14

View file

@ -24,14 +24,17 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39} - {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
- {name: Windows, python: '3.9', os: windows-latest, tox: py39} - {name: Windows, python: '3.10', os: windows-latest, tox: py310}
- {name: Mac, python: '3.9', os: macos-latest, tox: py39} - {name: Mac, python: '3.10', os: macos-latest, tox: py310}
- {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311}
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} - {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3} - {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min}
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing} - {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev}
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2

View file

@ -1,31 +1,32 @@
ci: ci:
autoupdate_branch: "2.0.x"
autoupdate_schedule: monthly autoupdate_schedule: monthly
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.15.0 rev: v2.29.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py36-plus"] args: ["--py36-plus"]
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v2.5.0 rev: v2.6.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
name: Reorder Python imports (src, tests) name: Reorder Python imports (src, tests)
files: "^(?!examples/)" files: "^(?!examples/)"
args: ["--application-directories", "src"] args: ["--application-directories", "src"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.5b1 rev: 21.12b0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 3.9.2 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
- flake8-bugbear - flake8-bugbear
- flake8-implicit-str-concat - flake8-implicit-str-concat
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0 rev: v4.0.1
hooks: hooks:
- id: fix-byte-order-marker - id: fix-byte-order-marker
- id: trailing-whitespace - id: trailing-whitespace

View file

@ -1,4 +1,8 @@
version: 2 version: 2
build:
os: ubuntu-20.04
tools:
python: "3.10"
python: python:
install: install:
- requirements: requirements/docs.txt - requirements: requirements/docs.txt

View file

@ -5,19 +5,79 @@ Version 2.1.0
Unreleased Unreleased
- Update Click dependency to >= 8.0. - Drop support for Python 3.6. :pr:`4335`
- Update Click dependency to >= 8.0. :pr:`4008`
- Remove previously deprecated code. :pr:`4337`
- The CLI does not pass ``script_info`` to app factory functions.
- ``config.from_json`` is replaced by
``config.from_file(name, load=json.load)``.
- ``json`` functions no longer take an ``encoding`` parameter.
- ``safe_join`` is removed, use ``werkzeug.utils.safe_join``
instead.
- ``total_seconds`` is removed, use ``timedelta.total_seconds``
instead.
- The same blueprint cannot be registered with the same name. Use
``name=`` when registering to specify a unique name.
- Some parameters in ``send_file`` and ``send_from_directory`` were
renamed in 2.0. The deprecation period for the old names is extended
to 2.2. Be sure to test with deprecation warnings visible.
- ``attachment_filename`` is renamed to ``download_name``.
- ``cache_timeout`` is renamed to ``max_age``.
- ``add_etags`` is renamed to ``etag``.
- ``filename`` is renamed to ``path``.
- The ``RequestContext.g`` property is deprecated. Use ``g`` directly
or ``AppContext.g`` instead. :issue:`3898`
- ``copy_current_request_context`` can decorate async functions.
:pr:`4303`
Version 2.0.3
-------------
Unreleased
- The test client's ``as_tuple`` parameter is deprecated and will be
removed in Werkzeug 2.1. It is now also deprecated in Flask, to be
removed in Flask 2.1, while remaining compatible with both in
2.0.x. Use ``response.request.environ`` instead. :pr:`4341`
- Fix type annotation for ``errorhandler`` decorator. :issue:`4295`
- Revert a change to the CLI that caused it to hide ``ImportError``
tracebacks when importing the application. :issue:`4307`
- ``app.json_encoder`` and ``json_decoder`` are only passed to
``dumps`` and ``loads`` if they have custom behavior. This improves
performance, mainly on PyPy. :issue:`4349`
- Clearer error message when ``after_this_request`` is used outside a
request context. :issue:`4333`
Version 2.0.2 Version 2.0.2
------------- -------------
Unreleased Released 2021-10-04
- Fix type annotation for ``teardown_request``. :issue:`4093` - Fix type annotation for ``teardown_*`` methods. :issue:`4093`
- Fix type annotation for ``before_request`` and ``before_app_request`` - Fix type annotation for ``before_request`` and ``before_app_request``
decorators. :issue:`4104` decorators. :issue:`4104`
- Fixed the issue where typing requires template global - Fixed the issue where typing requires template global
decorators to accept functions with no arguments. :issue:`4098` decorators to accept functions with no arguments. :issue:`4098`
- Support View and MethodView instances with async handlers. :issue:`4112`
- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
- Fix registering a blueprint twice with differing names. :issue:`4124`
- Fix the type of ``static_folder`` to accept ``pathlib.Path``.
:issue:`4150`
- ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``.
:issue:`4157`
- Correctly handle raising deferred errors in CLI lazy loading.
:issue:`4096`
- The CLI loader handles ``**kwargs`` in a ``create_app`` function.
:issue:`4170`
- Fix the order of ``before_request`` and other callbacks that trigger
before the view returns. They are called from the app down to the
closest nested blueprint. :issue:`4229`
Version 2.0.1 Version 2.0.1

View file

@ -12,14 +12,16 @@ to address bugs and feature requests in Flask itself. Use one of the
following resources for questions about using Flask or issues with your following resources for questions about using Flask or issues with your
own code: own code:
- The ``#get-help`` channel on our Discord chat: - The ``#questions`` channel on our Discord chat:
https://discord.gg/pallets https://discord.gg/pallets
- The mailing list flask@python.org for long term discussion or larger - The mailing list flask@python.org for long term discussion or larger
issues. issues.
- Ask on `Stack Overflow`_. Search with Google first using: - Ask on `Stack Overflow`_. Search with Google first using:
``site:stackoverflow.com flask {search term, exception message, etc.}`` ``site:stackoverflow.com flask {search term, exception message, etc.}``
- Ask on our `GitHub Discussions`_.
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent .. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
Reporting issues Reporting issues
@ -92,7 +94,7 @@ First time setup
.. code-block:: text .. code-block:: text
git remote add fork https://github.com/{username}/flask $ git remote add fork https://github.com/{username}/flask
- Create a virtualenv. - Create a virtualenv.

View file

@ -26,7 +26,7 @@ Install and update using `pip`_:
$ pip install -U Flask $ pip install -U Flask
.. _pip: https://pip.pypa.io/en/stable/quickstart/ .. _pip: https://pip.pypa.io/en/stable/getting-started/
A Simple Example A Simple Example

View file

@ -10,7 +10,7 @@ following conditions are met:
1. Redistributions of source code must retain the above copyright 1. Redistributions of source code must retain the above copyright
notice and this list of conditions. notice and this list of conditions.
3. Neither the name of the copyright holder nor the names of its 2. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.

View file

@ -218,8 +218,6 @@ Useful Functions and Classes
.. autofunction:: send_from_directory .. autofunction:: send_from_directory
.. autofunction:: safe_join
.. autofunction:: escape .. autofunction:: escape
.. autoclass:: Markup .. autoclass:: Markup
@ -256,7 +254,7 @@ the filter to render data inside ``<script>`` tags.
.. sourcecode:: html+jinja .. sourcecode:: html+jinja
<script type=text/javascript> <script>
const names = {{ names|tosjon }}; const names = {{ names|tosjon }};
renderChart(names, {{ axis_data|tojson }}); renderChart(names, {{ axis_data|tojson }});
</script> </script>

View file

@ -8,7 +8,7 @@ a request, CLI command, or other activity. Rather than passing the
application around to each function, the :data:`current_app` and application around to each function, the :data:`current_app` and
:data:`g` proxies are accessed instead. :data:`g` proxies are accessed instead.
This is similar to the :doc:`/reqcontext`, which keeps track of This is similar to :doc:`/reqcontext`, which keeps track of
request-level data during a request. A corresponding application context request-level data during a request. A corresponding application context
is pushed when a request context is pushed. is pushed when a request context is pushed.

View file

@ -17,12 +17,24 @@ defined with ``async def`` and use ``await``.
data = await async_db_query(...) data = await async_db_query(...)
return jsonify(data) return jsonify(data)
Pluggable class-based views also support handlers that are implemented as
coroutines. This applies to the :meth:`~flask.views.View.dispatch_request`
method in views that inherit from the :class:`flask.views.View` class, as
well as all the HTTP method handlers in views that inherit from the
:class:`flask.views.MethodView` class.
.. admonition:: Using ``async`` on Windows on Python 3.8 .. admonition:: Using ``async`` on Windows on Python 3.8
Python 3.8 has a bug related to asyncio on Windows. If you encounter Python 3.8 has a bug related to asyncio on Windows. If you encounter
something like ``ValueError: set_wakeup_fd only works in main thread``, something like ``ValueError: set_wakeup_fd only works in main thread``,
please upgrade to Python 3.9. please upgrade to Python 3.9.
.. admonition:: Using ``async`` with greenlet
When using gevent or eventlet to serve an application or patch the
runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
required.
Performance Performance
----------- -----------

View file

@ -103,6 +103,11 @@ replaces the :meth:`Flask.run` method in most cases. ::
is provided for convenience, but is not designed to be particularly secure, is provided for convenience, but is not designed to be particularly secure,
stable, or efficient. See :doc:`/deploying/index` for how to run in production. stable, or efficient. See :doc:`/deploying/index` for how to run in production.
If another program is already using port 5000, you'll see
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
server tries to start. See :ref:`address-already-in-use` for how to
handle that.
Open a Shell Open a Shell
------------ ------------
@ -112,10 +117,9 @@ shell with the :func:`shell <cli.shell_command>` command. An application
context will be active, and the app instance will be imported. :: context will be active, and the app instance will be imported. ::
$ flask shell $ flask shell
Python 3.6.2 (default, Jul 20 2017, 03:52:27) Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux
[GCC 7.1.1 20170630] on linux App: example [production]
App: example Instance: /home/david/Projects/pallets/flask/instance
Instance: /home/user/Projects/hello/instance
>>> >>>
Use :meth:`~Flask.shell_context_processor` to add other automatic imports. Use :meth:`~Flask.shell_context_processor` to add other automatic imports.

View file

@ -38,7 +38,7 @@ method::
app.config.update( app.config.update(
TESTING=True, TESTING=True,
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/' SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
) )
@ -180,8 +180,8 @@ The following configuration values are used internally by Flask:
application. It should be a long random ``bytes`` or ``str``. For application. It should be a long random ``bytes`` or ``str``. For
example, copy the output of this to your config:: example, copy the output of this to your config::
$ python -c 'import os; print(os.urandom(16))' $ python -c 'import secrets; print(secrets.token_hex())'
b'_5#y2L"F4Q8z\n\xec]/' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
**Do not reveal the secret key when posting questions or committing code.** **Do not reveal the secret key when posting questions or committing code.**
@ -468,7 +468,7 @@ sure to use uppercase letters for your config keys.
Here is an example of a configuration file:: Here is an example of a configuration file::
# Example configuration # Example configuration
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
Make sure to load the configuration very early on, so that extensions have Make sure to load the configuration very early on, so that extensions have
the ability to access the configuration when starting up. There are other the ability to access the configuration when starting up. There are other

View file

@ -69,7 +69,7 @@ enables the debugger and reloader.
``FLASK_ENV`` can only be set as an environment variable. When running ``FLASK_ENV`` can only be set as an environment variable. When running
from Python code, passing ``debug=True`` enables debug mode, which is from Python code, passing ``debug=True`` enables debug mode, which is
mostly equivalent. Debug mode can be controled separately from mostly equivalent. Debug mode can be controlled separately from
``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well. ``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well.
.. code-block:: python .. code-block:: python

View file

@ -5,7 +5,7 @@ ASGI
If you'd like to use an ASGI server you will need to utilise WSGI to If you'd like to use an ASGI server you will need to utilise WSGI to
ASGI middleware. The asgiref ASGI middleware. The asgiref
[WsgiToAsgi](https://github.com/django/asgiref#wsgi-to-asgi-adapter) `WsgiToAsgi <https://github.com/django/asgiref#wsgi-to-asgi-adapter>`_
adapter is recommended as it integrates with the event loop used for adapter is recommended as it integrates with the event loop used for
Flask's :ref:`async_await` support. You can use the adapter by Flask's :ref:`async_await` support. You can use the adapter by
wrapping the Flask app, wrapping the Flask app,
@ -21,7 +21,7 @@ wrapping the Flask app,
asgi_app = WsgiToAsgi(app) asgi_app = WsgiToAsgi(app)
and then serving the ``asgi_app`` with the asgi server, e.g. using and then serving the ``asgi_app`` with the ASGI server, e.g. using
`Hypercorn <https://gitlab.com/pgjones/hypercorn>`_, `Hypercorn <https://gitlab.com/pgjones/hypercorn>`_,
.. sourcecode:: text .. sourcecode:: text

View file

@ -234,5 +234,5 @@ python path. Common problems are:
.. _nginx: https://nginx.org/ .. _nginx: https://nginx.org/
.. _lighttpd: https://www.lighttpd.net/ .. _lighttpd: https://www.lighttpd.net/
.. _cherokee: http://cherokee-project.com/ .. _cherokee: https://cherokee-project.com/
.. _flup: https://pypi.org/project/flup/ .. _flup: https://pypi.org/project/flup/

View file

@ -37,7 +37,7 @@ If your application is accessible at root level, you can use a
single ``/`` instead of ``/yourapplication``. ``myapp`` refers to the name of single ``/`` instead of ``/yourapplication``. ``myapp`` refers to the name of
the file of your flask application (without extension) or the module which the file of your flask application (without extension) or the module which
provides ``app``. ``app`` is the callable inside of your application (usually provides ``app``. ``app`` is the callable inside of your application (usually
the line reads ``app = Flask(__name__)``. the line reads ``app = Flask(__name__)``).
If you want to deploy your flask application inside of a virtual environment, If you want to deploy your flask application inside of a virtual environment,
you need to also add ``--virtualenv /path/to/virtual/environment``. You might you need to also add ``--virtualenv /path/to/virtual/environment``. You might
@ -67,5 +67,5 @@ to have it in the URL root its a bit simpler::
.. _nginx: https://nginx.org/ .. _nginx: https://nginx.org/
.. _lighttpd: https://www.lighttpd.net/ .. _lighttpd: https://www.lighttpd.net/
.. _cherokee: http://cherokee-project.com/ .. _cherokee: https://cherokee-project.com/
.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/ .. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/

View file

@ -1,67 +1,171 @@
Standalone WSGI Containers Standalone WSGI Servers
========================== =======================
Most WSGI servers also provide HTTP servers, so they can run a WSGI
application and make it available externally.
It may still be a good idea to run the server behind a dedicated HTTP
server such as Apache or Nginx. See :ref:`deploying-proxy-setups` if you
run into issues with that.
There are popular servers written in Python that contain WSGI applications and
serve HTTP. These servers stand alone when they run; you can proxy to them
from your web server. Note the section on :ref:`deploying-proxy-setups` if you
run into issues.
Gunicorn Gunicorn
-------- --------
`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork `Gunicorn`_ is a WSGI and HTTP server for UNIX. To run a Flask
worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ application, tell Gunicorn how to import your Flask app object.
and `greenlet`_. Running a Flask application on this server is quite simple::
$ gunicorn myproject:app .. code-block:: text
`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. $ gunicorn -w 4 -b 0.0.0.0:5000 your_project:app
For example, to run a Flask application with 4 worker processes (``-w
4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``)::
$ gunicorn -w 4 -b 127.0.0.1:4000 myproject:app The ``-w 4`` option uses 4 workers to handle 4 requests at once. The
``-b 0.0.0.0:5000`` serves the application on all interfaces on port
5000.
The ``gunicorn`` command expects the names of your application module or Gunicorn provides many options for configuring the server, either
package and the application instance within the module. If you use the through a configuration file or with command line options. Use
application factory pattern, you can pass a call to that:: ``gunicorn --help`` or see the docs for more information.
The command expects the name of your module or package to import and
the application instance within the module. If you use the application
factory pattern, you can pass a call to that.
.. code-block:: text
$ gunicorn -w 4 -b 0.0.0.0:5000 "myproject:create_app()"
Async with Gevent or Eventlet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The default sync worker is appropriate for many use cases. If you need
asynchronous support, Gunicorn provides workers using either `gevent`_
or `eventlet`_. This is not the same as Python's ``async/await``, or the
ASGI server spec.
When using either gevent or eventlet, greenlet>=1.0 is required,
otherwise context locals such as ``request`` will not work as expected.
When using PyPy, PyPy>=7.3.7 is required.
To use gevent:
.. code-block:: text
$ gunicorn -k gevent -b 0.0.0.0:5000 your_project:app
To use eventlet:
.. code-block:: text
$ gunicorn -k eventlet -b 0.0.0.0:5000 your_project:app
$ gunicorn "myproject:create_app()"
.. _Gunicorn: https://gunicorn.org/ .. _Gunicorn: https://gunicorn.org/
.. _gevent: http://www.gevent.org/
.. _eventlet: https://eventlet.net/ .. _eventlet: https://eventlet.net/
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
uWSGI uWSGI
-------- -----
`uWSGI`_ is a fast application server written in C. It is very configurable `uWSGI`_ is a fast application server written in C. It is very
which makes it more complicated to setup than gunicorn. configurable, which makes it more complicated to setup than Gunicorn.
It also provides many other utilities for writing robust web
applications. To run a Flask application, tell Gunicorn how to import
your Flask app object.
Running `uWSGI HTTP Router`_:: .. code-block:: text
$ uwsgi --http 127.0.0.1:5000 --module myproject:app $ uwsgi --master -p 4 --http 0.0.0.0:5000 -w your_project:app
For a more optimized setup, see :doc:`configuring uWSGI and NGINX <uwsgi>`. The ``-p 4`` option uses 4 workers to handle 4 requests at once. The
``--http 0.0.0.0:5000`` serves the application on all interfaces on port
5000.
uWSGI has optimized integration with Nginx and Apache instead of using
a standard HTTP proxy. See :doc:`configuring uWSGI and Nginx <uwsgi>`.
Async with Gevent
~~~~~~~~~~~~~~~~~
The default sync worker is appropriate for many use cases. If you need
asynchronous support, uWSGI provides workers using `gevent`_. It also
supports other async modes, see the docs for more information. This is
not the same as Python's ``async/await``, or the ASGI server spec.
When using gevent, greenlet>=1.0 is required, otherwise context locals
such as ``request`` will not work as expected. When using PyPy,
PyPy>=7.3.7 is required.
.. code-block:: text
$ uwsgi --master --gevent 100 --http 0.0.0.0:5000 -w your_project:app
.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/ .. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/
.. _uWSGI HTTP Router: https://uwsgi-docs.readthedocs.io/en/latest/HTTP.html#the-uwsgi-http-https-router
Gevent Gevent
------- ------
`Gevent`_ is a coroutine-based Python networking library that uses Prefer using `Gunicorn`_ with Gevent workers rather than using Gevent
`greenlet`_ to provide a high-level synchronous API on top of `libev`_ directly. Gunicorn provides a much more configurable and
event loop:: production-tested server. See the section on Gunicorn above.
`Gevent`_ allows writing asynchronous, coroutine-based code that looks
like standard synchronous Python. It uses `greenlet`_ to enable task
switching without writing ``async/await`` or using ``asyncio``.
It provides a WSGI server that can handle many connections at once
instead of one per worker process.
`Eventlet`_, described below, is another library that does the same
thing. Certain dependencies you have, or other consideration, may affect
which of the two you choose to use
To use gevent to serve your application, import its ``WSGIServer`` and
use it to run your ``app``.
.. code-block:: python
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from yourapplication import app from your_project import app
http_server = WSGIServer(('', 5000), app) http_server = WSGIServer(("", 5000), app)
http_server.serve_forever() http_server.serve_forever()
.. _Gevent: http://www.gevent.org/
.. _greenlet: https://greenlet.readthedocs.io/en/latest/ Eventlet
.. _libev: http://software.schmorp.de/pkg/libev.html --------
Prefer using `Gunicorn`_ with Eventlet workers rather than using
Eventlet directly. Gunicorn provides a much more configurable and
production-tested server. See the section on Gunicorn above.
`Eventlet`_ allows writing asynchronous, coroutine-based code that looks
like standard synchronous Python. It uses `greenlet`_ to enable task
switching without writing ``async/await`` or using ``asyncio``.
It provides a WSGI server that can handle many connections at once
instead of one per worker process.
`Gevent`_, described above, is another library that does the same
thing. Certain dependencies you have, or other consideration, may affect
which of the two you choose to use
To use eventlet to serve your application, import its ``wsgi.server``
and use it to run your ``app``.
.. code-block:: python
import eventlet
from eventlet import wsgi
from your_project import app
wsgi.server(eventlet.listen(("", 5000), app)
Twisted Web Twisted Web
----------- -----------
@ -69,7 +173,9 @@ Twisted Web
`Twisted Web`_ is the web server shipped with `Twisted`_, a mature, `Twisted Web`_ is the web server shipped with `Twisted`_, a mature,
non-blocking event-driven networking library. Twisted Web comes with a non-blocking event-driven networking library. Twisted Web comes with a
standard WSGI container which can be controlled from the command line using standard WSGI container which can be controlled from the command line using
the ``twistd`` utility:: the ``twistd`` utility:
.. code-block:: text
$ twistd web --wsgi myproject.app $ twistd web --wsgi myproject.app
@ -79,13 +185,16 @@ This example will run a Flask application called ``app`` from a module named
Twisted Web supports many flags and options, and the ``twistd`` utility does Twisted Web supports many flags and options, and the ``twistd`` utility does
as well; see ``twistd -h`` and ``twistd web -h`` for more information. For as well; see ``twistd -h`` and ``twistd web -h`` for more information. For
example, to run a Twisted Web server in the foreground, on port 8080, with an example, to run a Twisted Web server in the foreground, on port 8080, with an
application from ``myproject``:: application from ``myproject``:
.. code-block:: text
$ twistd -n web --port tcp:8080 --wsgi myproject.app $ twistd -n web --port tcp:8080 --wsgi myproject.app
.. _Twisted: https://twistedmatrix.com/trac/ .. _Twisted: https://twistedmatrix.com/trac/
.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb .. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb
.. _deploying-proxy-setups: .. _deploying-proxy-setups:
Proxy Setups Proxy Setups

View file

@ -322,9 +322,9 @@ ecosystem remain consistent and compatible.
`Official Pallets Themes`_. A link to the documentation or project `Official Pallets Themes`_. A link to the documentation or project
website must be in the PyPI metadata or the readme. website must be in the PyPI metadata or the readme.
7. For maximum compatibility, the extension should support the same 7. For maximum compatibility, the extension should support the same
versions of Python that Flask supports. 3.6+ is recommended as of versions of Python that Flask supports. 3.7+ is recommended as of
2020. Use ``python_requires=">= 3.6"`` in ``setup.py`` to indicate December 2021. Use ``python_requires=">= 3.7"`` in ``setup.py`` to
supported versions. indicate supported versions.
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask .. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
.. _mailinglist: https://mail.python.org/mailman/listinfo/flask .. _mailinglist: https://mail.python.org/mailman/listinfo/flask

View file

@ -6,7 +6,7 @@ Python Version
-------------- --------------
We recommend using the latest version of Python. Flask supports Python We recommend using the latest version of Python. Flask supports Python
3.6 and newer. 3.7 and newer.
Dependencies Dependencies
@ -49,6 +49,18 @@ use them if you install them.
.. _watchdog: https://pythonhosted.org/watchdog/ .. _watchdog: https://pythonhosted.org/watchdog/
greenlet
~~~~~~~~
You may choose to use gevent or eventlet with your application. In this
case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
required.
These are not minimum supported versions, they only indicate the first
versions that added necessary features. You should use the latest
versions of each.
Virtual environments Virtual environments
-------------------- --------------------

View file

@ -64,7 +64,7 @@ An example task
Let's write a task that adds two numbers together and returns the result. We 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`` configure Celery's broker and backend to use Redis, create a ``celery``
application using the factor from above, and then use it to define the task. :: application using the factory from above, and then use it to define the task. ::
from flask import Flask from flask import Flask

View file

@ -175,9 +175,8 @@ are JavaScript libraries like jQuery_ that have form plugins to ease the
construction of progress bar. construction of progress bar.
Because the common pattern for file uploads exists almost unchanged in all Because the common pattern for file uploads exists almost unchanged in all
applications dealing with uploads, there is also a Flask extension called applications dealing with uploads, there are also some Flask extensions that
`Flask-Uploads`_ that implements a full fledged upload mechanism that implement a full fledged upload mechanism that allows controlling which
allows controlling which file extensions are allowed to be uploaded. file extensions are allowed to be uploaded.
.. _jQuery: https://jquery.com/ .. _jQuery: https://jquery.com/
.. _Flask-Uploads: https://flask-uploads.readthedocs.io/en/latest/

View file

@ -23,8 +23,7 @@ to add a script statement to the bottom of your ``<body>`` to load jQuery:
.. sourcecode:: html .. sourcecode:: html
<script type=text/javascript src="{{ <script src="{{ url_for('static', filename='jquery.js') }}"></script>
url_for('static', filename='jquery.js') }}"></script>
Another method is using Google's `AJAX Libraries API Another method is using Google's `AJAX Libraries API
<https://developers.google.com/speed/libraries/>`_ to load jQuery: <https://developers.google.com/speed/libraries/>`_ to load jQuery:
@ -59,7 +58,7 @@ like this:
.. sourcecode:: html+jinja .. sourcecode:: html+jinja
<script type=text/javascript> <script>
$SCRIPT_ROOT = {{ request.script_root|tojson }}; $SCRIPT_ROOT = {{ request.script_root|tojson }};
</script> </script>
@ -109,7 +108,7 @@ usually a better idea to have that in a separate script file:
.. sourcecode:: html .. sourcecode:: html
<script type=text/javascript> <script>
$(function() { $(function() {
$('a#calculate').bind('click', function() { $('a#calculate').bind('click', function() {
$.getJSON($SCRIPT_ROOT + '/_add_numbers', { $.getJSON($SCRIPT_ROOT + '/_add_numbers', {

View file

@ -113,16 +113,16 @@ raw cursor and connection objects.
Here is how you can use it:: Here is how you can use it::
for user in query_db('select * from users'): for user in query_db('select * from users'):
print user['username'], 'has the id', user['user_id'] print(user['username'], 'has the id', user['user_id'])
Or if you just want a single result:: Or if you just want a single result::
user = query_db('select * from users where username = ?', user = query_db('select * from users where username = ?',
[the_username], one=True) [the_username], one=True)
if user is None: if user is None:
print 'No such user' print('No such user')
else: else:
print the_username, 'has the id', user['user_id'] print(the_username, 'has the id', user['user_id'])
To pass variable parts to the SQL statement, use a question mark in the To pass variable parts to the SQL statement, use a question mark in the
statement and pass in the arguments as a list. Never directly add them to statement and pass in the arguments as a list. Never directly add them to

View file

@ -19,7 +19,7 @@ forms.
fun. You can get it from `PyPI fun. You can get it from `PyPI
<https://pypi.org/project/Flask-WTF/>`_. <https://pypi.org/project/Flask-WTF/>`_.
.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ .. _Flask-WTF: https://flask-wtf.readthedocs.io/
The Forms The Forms
--------- ---------

View file

@ -83,6 +83,11 @@ deployment options see :doc:`deploying/index`.
Now head over to http://127.0.0.1:5000/, and you should see your hello Now head over to http://127.0.0.1:5000/, and you should see your hello
world greeting. world greeting.
If another program is already using port 5000, you'll see
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
server tries to start. See :ref:`address-already-in-use` for how to
handle that.
.. _public-server: .. _public-server:
.. admonition:: Externally Visible Server .. admonition:: Externally Visible Server
@ -444,9 +449,9 @@ Here is an example template:
<h1>Hello, World!</h1> <h1>Hello, World!</h1>
{% endif %} {% endif %}
Inside templates you also have access to the :class:`~flask.request`, Inside templates you also have access to the :data:`~flask.Flask.config`,
:class:`~flask.session` and :class:`~flask.g` [#]_ objects :class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
as well as the :func:`~flask.get_flashed_messages` function. as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions.
Templates are especially useful if inheritance is used. If you want to Templates are especially useful if inheritance is used. If you want to
know how that works, see :doc:`patterns/templateinheritance`. Basically know how that works, see :doc:`patterns/templateinheritance`. Basically
@ -468,7 +473,7 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:
>>> Markup.escape('<blink>hacker</blink>') >>> Markup.escape('<blink>hacker</blink>')
Markup('&lt;blink&gt;hacker&lt;/blink&gt;') Markup('&lt;blink&gt;hacker&lt;/blink&gt;')
>>> Markup('<em>Marked up</em> &raquo; HTML').striptags() >>> Markup('<em>Marked up</em> &raquo; HTML').striptags()
'Marked up \xbb HTML' 'Marked up » HTML'
.. versionchanged:: 0.5 .. versionchanged:: 0.5
@ -626,7 +631,7 @@ Werkzeug provides for you::
def upload_file(): def upload_file():
if request.method == 'POST': if request.method == 'POST':
file = request.files['the_file'] file = request.files['the_file']
file.save(f"/var/www/uploads/{secure_filename(f.filename)}") file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
... ...
For some better examples, see :doc:`patterns/fileuploads`. For some better examples, see :doc:`patterns/fileuploads`.
@ -847,8 +852,8 @@ sessions work::
generator. Use the following command to quickly generate a value for generator. Use the following command to quickly generate a value for
:attr:`Flask.secret_key` (or :data:`SECRET_KEY`):: :attr:`Flask.secret_key` (or :data:`SECRET_KEY`)::
$ python -c 'import os; print(os.urandom(16))' $ python -c 'import secrets; print(secrets.token_hex())'
b'_5#y2L"F4Q8z\n\xec]/' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
A note on cookie-based sessions: Flask will take the values you put into the A note on cookie-based sessions: Flask will take the values you put into the
session object and serialize them into a cookie. If you are finding some session object and serialize them into a cookie. If you are finding some

View file

@ -8,7 +8,7 @@ request. Rather than passing the request object to each function that
runs during a request, the :data:`request` and :data:`session` proxies runs during a request, the :data:`request` and :data:`session` proxies
are accessed instead. are accessed instead.
This is similar to the :doc:`/appcontext`, which keeps track of the This is similar to :doc:`/appcontext`, which keeps track of the
application-level data independent of a request. A corresponding application-level data independent of a request. A corresponding
application context is pushed when a request context is pushed. application context is pushed when a request context is pushed.
@ -33,8 +33,8 @@ Lifetime of the Context
----------------------- -----------------------
When a Flask application begins handling a request, it pushes a request When a Flask application begins handling a request, it pushes a request
context, which also pushes an :doc:`/appcontext`. When the request ends context, which also pushes an :doc:`app context </appcontext>`. When the
it pops the request context then the application context. request ends it pops the request context then the application context.
The context is unique to each thread (or other worker type). The context is unique to each thread (or other worker type).
:data:`request` cannot be passed to another thread, the other thread :data:`request` cannot be passed to another thread, the other thread

View file

@ -64,6 +64,54 @@ and using the CLI.
above. above.
.. _address-already-in-use:
Address already in use
~~~~~~~~~~~~~~~~~~~~~~
If another program is already using port 5000, you'll see an ``OSError``
when the server tries to start. It may have one of the following
messages:
- ``OSError: [Errno 98] Address already in use``
- ``OSError: [WinError 10013] An attempt was made to access a socket
in a way forbidden by its access permissions``
Either identify and stop the other program, or use
``flask run --port 5001`` to pick a different port.
You can use ``netstat`` or ``lsof`` to identify what process id is using
a port, then use other operating system tools stop that process. The
following example shows that process id 6847 is using port 5000.
.. tabs::
.. tab:: ``netstat`` (Linux)
.. code-block:: text
$ netstat -nlp | grep 5000
tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python
.. tab:: ``lsof`` (macOS / Linux)
.. code-block:: text
$ lsof -P -i :5000
Python 6847 IPv4 TCP localhost:5000 (LISTEN)
.. tab:: ``netstat`` (Windows)
.. code-block:: text
> netstat -ano | findstr 5000
TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847
macOS Monterey and later automatically starts a service that uses port
5000. To disable the service, go to System Preferences, Sharing, and
disable "AirPlay Receiver".
Lazy or Eager Loading Lazy or Eager Loading
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~

View file

@ -21,8 +21,7 @@ that these functions are not only there for interactive shell usage, but
also for unit testing and other situations that require a faked request also for unit testing and other situations that require a faked request
context. context.
Generally it's recommended that you read the :doc:`reqcontext` Generally it's recommended that you read :doc:`reqcontext` first.
chapter of the documentation first.
Command Line Interface Command Line Interface
---------------------- ----------------------

View file

@ -177,7 +177,7 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new
@template_rendered.connect_via(app) @template_rendered.connect_via(app)
def when_template_rendered(sender, template, context, **extra): def when_template_rendered(sender, template, context, **extra):
print f'Template {template.name} is rendered with {context}' print(f'Template {template.name} is rendered with {context}')
Core Signals Core Signals
------------ ------------

View file

@ -37,7 +37,7 @@ by default:
.. data:: config .. data:: config
:noindex: :noindex:
The current configuration object (:data:`flask.config`) The current configuration object (:data:`flask.Flask.config`)
.. versionadded:: 0.6 .. versionadded:: 0.6

View file

@ -48,20 +48,21 @@ the application for testing and initializes a new database::
import pytest import pytest
from flaskr import create_app from flaskr import create_app
from flaskr.db import init_db
@pytest.fixture @pytest.fixture
def client(): def client():
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() db_fd, db_path = tempfile.mkstemp()
flaskr.app.config['TESTING'] = True app = create_app({'TESTING': True, 'DATABASE': db_path})
with flaskr.app.test_client() as client: with app.test_client() as client:
with flaskr.app.app_context(): with app.app_context():
flaskr.init_db() init_db()
yield client yield client
os.close(db_fd) os.close(db_fd)
os.unlink(flaskr.app.config['DATABASE']) os.unlink(db_path)
This client fixture will be called by each individual test. It gives us a This client fixture will be called by each individual test. It gives us a
simple interface to the application, where we can trigger test requests to the simple interface to the application, where we can trigger test requests to the
@ -224,13 +225,13 @@ temporarily. With this you can access the :class:`~flask.request`,
:class:`~flask.g` and :class:`~flask.session` objects like in view :class:`~flask.g` and :class:`~flask.session` objects like in view
functions. Here is a full example that demonstrates this approach:: functions. Here is a full example that demonstrates this approach::
import flask from flask import Flask, request
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_request_context('/?name=Peter'): with app.test_request_context('/?name=Peter'):
assert flask.request.path == '/' assert request.path == '/'
assert flask.request.args['name'] == 'Peter' assert request.args['name'] == 'Peter'
All the other objects that are context bound can be used in the same All the other objects that are context bound can be used in the same
way. way.
@ -247,7 +248,7 @@ the test request context leaves the ``with`` block. If you do want the
:meth:`~flask.Flask.before_request` functions to be called as well, you :meth:`~flask.Flask.before_request` functions to be called as well, you
need to call :meth:`~flask.Flask.preprocess_request` yourself:: need to call :meth:`~flask.Flask.preprocess_request` yourself::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_request_context('/?name=Peter'): with app.test_request_context('/?name=Peter'):
app.preprocess_request() app.preprocess_request()
@ -260,7 +261,7 @@ If you want to call the :meth:`~flask.Flask.after_request` functions you
need to call into :meth:`~flask.Flask.process_response` which however need to call into :meth:`~flask.Flask.process_response` which however
requires that you pass it a response object:: requires that you pass it a response object::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_request_context('/?name=Peter'): with app.test_request_context('/?name=Peter'):
resp = Response('...') resp = Response('...')
@ -329,7 +330,7 @@ context around for a little longer so that additional introspection can
happen. With Flask 0.4 this is possible by using the happen. With Flask 0.4 this is possible by using the
:meth:`~flask.Flask.test_client` with a ``with`` block:: :meth:`~flask.Flask.test_client` with a ``with`` block::
app = flask.Flask(__name__) app = Flask(__name__)
with app.test_client() as c: with app.test_client() as c:
rv = c.get('/?tequila=42') rv = c.get('/?tequila=42')
@ -353,7 +354,7 @@ keep the context around and access :data:`flask.session`::
with app.test_client() as c: with app.test_client() as c:
rv = c.get('/') rv = c.get('/')
assert flask.session['foo'] == 42 assert session['foo'] == 42
This however does not make it possible to also modify the session or to This however does not make it possible to also modify the session or to
access the session before a request was fired. Starting with Flask 0.8 we access the session before a request was fired. Starting with Flask 0.8 we

View file

@ -88,9 +88,9 @@ You can use the following command to output a random secret key:
.. code-block:: none .. code-block:: none
$ python -c 'import os; print(os.urandom(16))' $ python -c 'import secrets; print(secrets.token_hex())'
b'_5#y2L"F4Q8z\n\xec]/' '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
Create the ``config.py`` file in the instance folder, which the factory Create the ``config.py`` file in the instance folder, which the factory
will read from if it exists. Copy the generated value into it. will read from if it exists. Copy the generated value into it.
@ -98,7 +98,7 @@ will read from if it exists. Copy the generated value into it.
.. code-block:: python .. code-block:: python
:caption: ``venv/var/flaskr-instance/config.py`` :caption: ``venv/var/flaskr-instance/config.py``
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
You can also set any other necessary configuration here, although You can also set any other necessary configuration here, although
``SECRET_KEY`` is the only one needed for Flaskr. ``SECRET_KEY`` is the only one needed for Flaskr.

View file

@ -177,4 +177,9 @@ Visit http://127.0.0.1:5000/hello in a browser and you should see the
"Hello, World!" message. Congratulations, you're now running your Flask "Hello, World!" message. Congratulations, you're now running your Flask
web application! web application!
If another program is already using port 5000, you'll see
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
server tries to start. See :ref:`address-already-in-use` for how to
handle that.
Continue to :doc:`database`. Continue to :doc:`database`.

View file

@ -57,29 +57,29 @@ By the end, your project layout will look like this:
/home/user/Projects/flask-tutorial /home/user/Projects/flask-tutorial
├── flaskr/ ├── flaskr/
   ├── __init__.py ├── __init__.py
   ├── db.py ├── db.py
   ├── schema.sql ├── schema.sql
   ├── auth.py ├── auth.py
   ├── blog.py ├── blog.py
   ├── templates/ ├── templates/
   │ ├── base.html │ ├── base.html
   │ ├── auth/ │ ├── auth/
   │ │   ├── login.html │ │ ├── login.html
   │ │   └── register.html │ │ └── register.html
   │ └── blog/ │ └── blog/
   │ ├── create.html │ ├── create.html
   │ ├── index.html │ ├── index.html
   │ └── update.html │ └── update.html
   └── static/ └── static/
      └── style.css └── style.css
├── tests/ ├── tests/
   ├── conftest.py ├── conftest.py
  ├── data.sql ├── data.sql
   ├── test_factory.py ├── test_factory.py
   ├── test_db.py ├── test_db.py
  ├── test_auth.py ├── test_auth.py
  └── test_blog.py └── test_blog.py
├── venv/ ├── venv/
├── setup.py ├── setup.py
└── MANIFEST.in └── MANIFEST.in

View file

@ -270,7 +270,7 @@ messages.
with app.app_context(): with app.app_context():
assert get_db().execute( assert get_db().execute(
"select * from user where username = 'a'", "SELECT * FROM user WHERE username = 'a'",
).fetchone() is not None ).fetchone() is not None

View file

@ -91,18 +91,18 @@ write templates to generate the HTML form.
error = 'Username is required.' error = 'Username is required.'
elif not password: elif not password:
error = 'Password is required.' error = 'Password is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = f"User {username} is already registered."
if error is None: if error is None:
try:
db.execute( db.execute(
'INSERT INTO user (username, password) VALUES (?, ?)', "INSERT INTO user (username, password) VALUES (?, ?)",
(username, generate_password_hash(password)) (username, generate_password_hash(password)),
) )
db.commit() db.commit()
return redirect(url_for('auth.login')) except db.IntegrityError:
error = f"User {username} is already registered."
else:
return redirect(url_for("auth.login"))
flash(error) flash(error)
@ -125,26 +125,25 @@ Here's what the ``register`` view function is doing:
#. Validate that ``username`` and ``password`` are not empty. #. Validate that ``username`` and ``password`` are not empty.
#. Validate that ``username`` is not already registered by querying the
database and checking if a result is returned.
:meth:`db.execute <sqlite3.Connection.execute>` takes a SQL query
with ``?`` placeholders for any user input, and a tuple of values
to replace the placeholders with. The database library will take
care of escaping the values so you are not vulnerable to a
*SQL injection attack*.
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
If the query returned no results, it returns ``None``. Later,
:meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of
all results.
#. If validation succeeds, insert the new user data into the database. #. If validation succeeds, insert the new user data into the database.
For security, passwords should never be stored in the database
- :meth:`db.execute <sqlite3.Connection.execute>` takes a SQL
query with ``?`` placeholders for any user input, and a tuple of
values to replace the placeholders with. The database library
will take care of escaping the values so you are not vulnerable
to a *SQL injection attack*.
- For security, passwords should never be stored in the database
directly. Instead, directly. Instead,
:func:`~werkzeug.security.generate_password_hash` is used to :func:`~werkzeug.security.generate_password_hash` is used to
securely hash the password, and that hash is stored. Since this securely hash the password, and that hash is stored. Since this
query modifies data, :meth:`db.commit() <sqlite3.Connection.commit>` query modifies data,
needs to be called afterwards to save the changes. :meth:`db.commit() <sqlite3.Connection.commit>` needs to be
called afterwards to save the changes.
- An :exc:`sqlite3.IntegrityError` will occur if the username
already exists, which should be shown to the user as another
validation error.
#. After storing the user, they are redirected to the login page. #. After storing the user, they are redirected to the login page.
:func:`url_for` generates the URL for the login view based on its :func:`url_for` generates the URL for the login view based on its
@ -200,6 +199,11 @@ There are a few differences from the ``register`` view:
#. The user is queried first and stored in a variable for later use. #. The user is queried first and stored in a variable for later use.
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
If the query returned no results, it returns ``None``. Later,
:meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list
of all results.
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted #. :func:`~werkzeug.security.check_password_hash` hashes the submitted
password in the same way as the stored hash and securely compares password in the same way as the stored hash and securely compares
them. If they match, the password is valid. them. If they match, the password is valid.

View file

@ -113,8 +113,8 @@ Method Based Dispatching
For RESTful APIs it's especially helpful to execute a different function For RESTful APIs it's especially helpful to execute a different function
for each HTTP method. With the :class:`flask.views.MethodView` you can for each HTTP method. With the :class:`flask.views.MethodView` you can
easily do that. Each HTTP method maps to a function with the same name easily do that. Each HTTP method maps to a method of the class with the
(just in lowercase):: same name (just in lowercase)::
from flask.views import MethodView from flask.views import MethodView

View file

@ -60,20 +60,20 @@ def register():
error = "Username is required." error = "Username is required."
elif not password: elif not password:
error = "Password is required." error = "Password is required."
elif (
db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone()
is not None
):
error = f"User {username} is already registered."
if error is None: if error is None:
# the name is available, store it in the database and go to try:
# the login page
db.execute( db.execute(
"INSERT INTO user (username, password) VALUES (?, ?)", "INSERT INTO user (username, password) VALUES (?, ?)",
(username, generate_password_hash(password)), (username, generate_password_hash(password)),
) )
db.commit() db.commit()
except db.IntegrityError:
# The username was already taken, which caused the
# commit to fail. Show a validation error.
error = f"User {username} is already registered."
else:
# Success, go to the login page.
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
flash(error) flash(error)

View file

@ -16,7 +16,7 @@ def test_register(client, app):
# test that the user was inserted into the database # test that the user was inserted into the database
with app.app_context(): with app.app_context():
assert ( assert (
get_db().execute("select * from user where username = 'a'").fetchone() get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone()
is not None is not None
) )

View file

@ -1,146 +1,163 @@
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile with python 3.10
# To update, run: # To update, run:
# #
# pip-compile requirements/dev.in # pip-compile requirements/dev.in
# #
alabaster==0.7.12 alabaster==0.7.12
# via sphinx # via sphinx
appdirs==1.4.4 asgiref==3.4.1
# via virtualenv # via -r requirements/tests.in
asgiref==3.3.4
# via -r tests.in
attrs==21.2.0 attrs==21.2.0
# via pytest # via pytest
babel==2.9.1 babel==2.9.1
# via sphinx # via sphinx
backports.entry-points-selectable==1.1.1
# via virtualenv
blinker==1.4 blinker==1.4
# via -r tests.in # via -r requirements/tests.in
certifi==2020.12.5 certifi==2021.10.8
# via requests # via requests
cfgv==3.3.0 cffi==1.15.0
# via cryptography
cfgv==3.3.1
# via pre-commit # via pre-commit
chardet==4.0.0 charset-normalizer==2.0.9
# via requests # via requests
click==8.0.1 click==8.0.3
# via pip-tools # via pip-tools
distlib==0.3.1 cryptography==36.0.1
# via -r requirements/typing.in
distlib==0.3.4
# via virtualenv # via virtualenv
docutils==0.16 docutils==0.16
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
filelock==3.0.12 filelock==3.4.0
# via # via
# tox # tox
# virtualenv # virtualenv
greenlet==1.1.0 greenlet==1.1.2 ; python_version < "3.11"
# via -r tests.in # via -r requirements/tests.in
identify==2.2.4 identify==2.4.0
# via pre-commit # via pre-commit
idna==2.10 idna==3.3
# via requests # via requests
imagesize==1.2.0 imagesize==1.3.0
# via sphinx # via sphinx
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
jinja2==3.0.1 jinja2==3.0.3
# via sphinx # via sphinx
markupsafe==2.0.1 markupsafe==2.0.1
# via jinja2 # via jinja2
mypy==0.930
# via -r requirements/typing.in
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via mypy # via mypy
mypy==0.812
# via -r typing.in
nodeenv==1.6.0 nodeenv==1.6.0
# via pre-commit # via pre-commit
packaging==20.9 packaging==21.3
# via # via
# pallets-sphinx-themes # pallets-sphinx-themes
# pytest # pytest
# sphinx # sphinx
# tox # tox
pallets-sphinx-themes==2.0.1 pallets-sphinx-themes==2.0.2
# via -r docs.in # via -r requirements/docs.in
pep517==0.10.0 pep517==0.12.0
# via pip-tools # via pip-tools
pip-tools==6.1.0 pip-tools==6.4.0
# via -r dev.in # via -r requirements/dev.in
pluggy==0.13.1 platformdirs==2.4.0
# via virtualenv
pluggy==1.0.0
# via # via
# pytest # pytest
# tox # tox
pre-commit==2.13.0 pre-commit==2.16.0
# via -r dev.in # via -r requirements/dev.in
py==1.10.0 py==1.11.0
# via # via
# pytest # pytest
# tox # tox
pygments==2.9.0 pycparser==2.21
# via cffi
pygments==2.10.0
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
pyparsing==2.4.7 pyparsing==3.0.6
# via packaging # via packaging
pytest==6.2.4 pytest==6.2.5
# via -r tests.in # via -r requirements/tests.in
python-dotenv==0.17.1 python-dotenv==0.19.2
# via -r tests.in # via -r requirements/tests.in
pytz==2021.1 pytz==2021.3
# via babel # via babel
pyyaml==5.4.1 pyyaml==6.0
# via pre-commit # via pre-commit
requests==2.25.1 requests==2.26.0
# via sphinx # via sphinx
six==1.16.0 six==1.16.0
# via # via
# tox # tox
# virtualenv # virtualenv
snowballstemmer==2.1.0 snowballstemmer==2.2.0
# via sphinx # via sphinx
sphinx-issues==1.2.0 sphinx==4.3.2
# via -r docs.in
sphinx-tabs==3.0.0
# via -r docs.in
sphinx==4.0.2
# via # via
# -r docs.in # -r requirements/docs.in
# pallets-sphinx-themes # pallets-sphinx-themes
# sphinx-issues # sphinx-issues
# sphinx-tabs # sphinx-tabs
# sphinxcontrib-log-cabinet # sphinxcontrib-log-cabinet
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2 sphinxcontrib-applehelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-devhelp==1.0.2 sphinxcontrib-devhelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-htmlhelp==2.0.0
# via sphinx # via sphinx
sphinxcontrib-jsmath==1.0.1 sphinxcontrib-jsmath==1.0.1
# via sphinx # via sphinx
sphinxcontrib-log-cabinet==1.0.1 sphinxcontrib-log-cabinet==1.0.1
# via -r docs.in # via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3 sphinxcontrib-qthelp==1.0.3
# via sphinx # via sphinx
sphinxcontrib-serializinghtml==1.1.4 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
toml==0.10.2 toml==0.10.2
# via # via
# pep517
# pre-commit # pre-commit
# pytest # pytest
# tox # tox
tox==3.23.1 tomli==2.0.0
# via -r dev.in # via
typed-ast==1.4.3 # mypy
# pep517
tox==3.24.4
# via -r requirements/dev.in
types-contextvars==2.4.0
# via -r requirements/typing.in
types-dataclasses==0.6.1
# via -r requirements/typing.in
types-setuptools==57.4.4
# via -r requirements/typing.in
typing-extensions==4.0.1
# via mypy # via mypy
typing-extensions==3.10.0.0 urllib3==1.26.7
# via mypy
urllib3==1.26.5
# via requests # via requests
virtualenv==20.4.6 virtualenv==20.10.0
# via # via
# pre-commit # pre-commit
# tox # tox
wheel==0.37.1
# via pip-tools
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip # pip

View file

@ -1,5 +1,5 @@
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile with python 3.10
# To update, run: # To update, run:
# #
# pip-compile requirements/docs.in # pip-compile requirements/docs.in
@ -8,66 +8,66 @@ alabaster==0.7.12
# via sphinx # via sphinx
babel==2.9.1 babel==2.9.1
# via sphinx # via sphinx
certifi==2020.12.5 certifi==2021.10.8
# via requests # via requests
chardet==4.0.0 charset-normalizer==2.0.9
# via requests # via requests
docutils==0.16 docutils==0.16
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
idna==2.10 idna==3.3
# via requests # via requests
imagesize==1.2.0 imagesize==1.3.0
# via sphinx # via sphinx
jinja2==3.0.1 jinja2==3.0.3
# via sphinx # via sphinx
markupsafe==2.0.1 markupsafe==2.0.1
# via jinja2 # via jinja2
packaging==20.9 packaging==21.3
# via # via
# pallets-sphinx-themes # pallets-sphinx-themes
# sphinx # sphinx
pallets-sphinx-themes==2.0.1 pallets-sphinx-themes==2.0.2
# via -r docs.in # via -r requirements/docs.in
pygments==2.9.0 pygments==2.10.0
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
pyparsing==2.4.7 pyparsing==3.0.6
# via packaging # via packaging
pytz==2021.1 pytz==2021.3
# via babel # via babel
requests==2.25.1 requests==2.26.0
# via sphinx # via sphinx
snowballstemmer==2.1.0 snowballstemmer==2.2.0
# via sphinx # via sphinx
sphinx-issues==1.2.0 sphinx==4.3.2
# via -r docs.in
sphinx-tabs==3.0.0
# via -r docs.in
sphinx==4.0.2
# via # via
# -r docs.in # -r requirements/docs.in
# pallets-sphinx-themes # pallets-sphinx-themes
# sphinx-issues # sphinx-issues
# sphinx-tabs # sphinx-tabs
# sphinxcontrib-log-cabinet # sphinxcontrib-log-cabinet
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2 sphinxcontrib-applehelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-devhelp==1.0.2 sphinxcontrib-devhelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-htmlhelp==2.0.0
# via sphinx # via sphinx
sphinxcontrib-jsmath==1.0.1 sphinxcontrib-jsmath==1.0.1
# via sphinx # via sphinx
sphinxcontrib-log-cabinet==1.0.1 sphinxcontrib-log-cabinet==1.0.1
# via -r docs.in # via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3 sphinxcontrib-qthelp==1.0.3
# via sphinx # via sphinx
sphinxcontrib-serializinghtml==1.1.4 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
urllib3==1.26.5 urllib3==1.26.7
# via requests # via requests
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -0,0 +1,5 @@
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

View file

@ -0,0 +1,18 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile requirements/tests-pallets-dev.in
#
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 @ https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
# via -r requirements/tests-pallets-dev.in

View file

@ -0,0 +1,5 @@
Werkzeug==2.0.0
Jinja2==3.0.0
MarkupSafe==2.0.0
itsdangerous==2.0.0
click==8.0.0

View file

@ -0,0 +1,18 @@
#
# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile requirements/tests-pallets-min.in
#
click==8.0.0
# via -r requirements/tests-pallets-min.in
itsdangerous==2.0.0
# via -r requirements/tests-pallets-min.in
jinja2==3.0.0
# via -r requirements/tests-pallets-min.in
markupsafe==2.0.0
# via
# -r requirements/tests-pallets-min.in
# jinja2
werkzeug==2.0.0
# via -r requirements/tests-pallets-min.in

View file

@ -1,5 +1,5 @@
pytest pytest
asgiref asgiref
blinker blinker
greenlet greenlet ; python_version < "3.11"
python-dotenv python-dotenv

View file

@ -1,30 +1,30 @@
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile with python 3.10
# To update, run: # To update, run:
# #
# pip-compile requirements/tests.in # pip-compile requirements/tests.in
# #
asgiref==3.3.4 asgiref==3.4.1
# via -r requirements/tests.in # via -r requirements/tests.in
attrs==21.2.0 attrs==21.2.0
# via pytest # via pytest
blinker==1.4 blinker==1.4
# via -r requirements/tests.in # via -r requirements/tests.in
greenlet==1.1.0 greenlet==1.1.2 ; python_version < "3.11"
# via -r requirements/tests.in # via -r requirements/tests.in
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
packaging==20.9 packaging==21.3
# via pytest # via pytest
pluggy==0.13.1 pluggy==1.0.0
# via pytest # via pytest
py==1.10.0 py==1.11.0
# via pytest # via pytest
pyparsing==2.4.7 pyparsing==3.0.6
# via packaging # via packaging
pytest==6.2.4 pytest==6.2.5
# via -r requirements/tests.in # via -r requirements/tests.in
python-dotenv==0.17.1 python-dotenv==0.19.2
# via -r requirements/tests.in # via -r requirements/tests.in
toml==0.10.2 toml==0.10.2
# via pytest # via pytest

View file

@ -1 +1,5 @@
mypy mypy
types-contextvars
types-dataclasses
types-setuptools
cryptography

View file

@ -1,14 +1,26 @@
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile with python 3.10
# To update, run: # To update, run:
# #
# pip-compile requirements/typing.in # pip-compile requirements/typing.in
# #
cffi==1.15.0
# via cryptography
cryptography==36.0.1
# via -r requirements/typing.in
mypy==0.930
# via -r requirements/typing.in
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via mypy # via mypy
mypy==0.812 pycparser==2.21
# via cffi
tomli==2.0.0
# via mypy
types-contextvars==2.4.0
# via -r requirements/typing.in # via -r requirements/typing.in
typed-ast==1.4.3 types-dataclasses==0.6.1
# via mypy # via -r requirements/typing.in
typing-extensions==3.10.0.0 types-setuptools==57.4.4
# via -r requirements/typing.in
typing-extensions==4.0.1
# via mypy # via mypy

View file

@ -35,7 +35,7 @@ classifiers =
packages = find: packages = find:
package_dir = = src package_dir = = src
include_package_data = true include_package_data = true
python_requires = >= 3.6 python_requires = >= 3.7
# Dependencies are in setup.py for GitHub's dependency graph. # Dependencies are in setup.py for GitHub's dependency graph.
[options.packages.find] [options.packages.find]
@ -88,7 +88,7 @@ per-file-ignores =
[mypy] [mypy]
files = src/flask files = src/flask
python_version = 3.6 python_version = 3.7
allow_redefinition = True allow_redefinition = True
disallow_subclassing_any = True disallow_subclassing_any = True
# disallow_untyped_calls = True # disallow_untyped_calls = True
@ -112,3 +112,6 @@ ignore_missing_imports = True
[mypy-dotenv.*] [mypy-dotenv.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-cryptography.*]
ignore_missing_imports = True

View file

@ -23,7 +23,6 @@ from .helpers import flash as flash
from .helpers import get_flashed_messages as get_flashed_messages from .helpers import get_flashed_messages as get_flashed_messages
from .helpers import get_template_attribute as get_template_attribute from .helpers import get_template_attribute as get_template_attribute
from .helpers import make_response as make_response from .helpers import make_response as make_response
from .helpers import safe_join as safe_join
from .helpers import send_file as send_file from .helpers import send_file as send_file
from .helpers import send_from_directory as send_from_directory from .helpers import send_from_directory as send_from_directory
from .helpers import stream_with_context as stream_with_context from .helpers import stream_with_context as stream_with_context

View file

@ -16,7 +16,6 @@ from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import BadRequestKeyError from werkzeug.exceptions import BadRequestKeyError
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
from werkzeug.local import ContextVar
from werkzeug.routing import BuildError from werkzeug.routing import BuildError
from werkzeug.routing import Map from werkzeug.routing import Map
from werkzeug.routing import MapAdapter from werkzeug.routing import MapAdapter
@ -58,18 +57,12 @@ from .signals import request_started
from .signals import request_tearing_down from .signals import request_tearing_down
from .templating import DispatchingJinjaLoader from .templating import DispatchingJinjaLoader
from .templating import Environment from .templating import Environment
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import ResponseReturnValue from .typing import ResponseReturnValue
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable from .typing import TemplateFilterCallable
from .typing import TemplateGlobalCallable from .typing import TemplateGlobalCallable
from .typing import TemplateTestCallable from .typing import TemplateTestCallable
from .typing import URLDefaultCallable
from .typing import URLValuePreprocessorCallable
from .wrappers import Request from .wrappers import Request
from .wrappers import Response from .wrappers import Response
@ -78,6 +71,7 @@ if t.TYPE_CHECKING:
from .blueprints import Blueprint from .blueprints import Blueprint
from .testing import FlaskClient from .testing import FlaskClient
from .testing import FlaskCliRunner from .testing import FlaskCliRunner
from .typing import ErrorHandlerCallable
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
iscoroutinefunction = inspect.iscoroutinefunction iscoroutinefunction = inspect.iscoroutinefunction
@ -366,7 +360,8 @@ class Flask(Scaffold):
#: .. versionadded:: 1.1.0 #: .. versionadded:: 1.1.0
url_map_class = Map url_map_class = Map
#: the test client that is used with when `test_client` is used. #: The :meth:`test_client` method creates an instance of this test
#: client class. Defaults to :class:`~flask.testing.FlaskClient`.
#: #:
#: .. versionadded:: 0.7 #: .. versionadded:: 0.7
test_client_class: t.Optional[t.Type["FlaskClient"]] = None test_client_class: t.Optional[t.Type["FlaskClient"]] = None
@ -389,7 +384,7 @@ class Flask(Scaffold):
self, self,
import_name: str, import_name: str,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
static_folder: t.Optional[str] = "static", static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
static_host: t.Optional[str] = None, static_host: t.Optional[str] = None,
host_matching: bool = False, host_matching: bool = False,
subdomain_matching: bool = False, subdomain_matching: bool = False,
@ -744,20 +739,21 @@ class Flask(Scaffold):
:param context: the context as a dictionary that is updated in place :param context: the context as a dictionary that is updated in place
to add extra variables. to add extra variables.
""" """
funcs: t.Iterable[ names: t.Iterable[t.Optional[str]] = (None,)
TemplateContextProcessorCallable
] = self.template_context_processors[None] # A template may be rendered outside a request context.
reqctx = _request_ctx_stack.top if request:
if reqctx is not None: names = chain(names, reversed(request.blueprints))
for bp in request.blueprints:
if bp in self.template_context_processors: # The values passed to render_template take precedence. Keep a
funcs = chain(funcs, self.template_context_processors[bp]) # copy to re-apply after all context functions.
orig_ctx = context.copy() orig_ctx = context.copy()
for func in funcs:
for name in names:
if name in self.template_context_processors:
for func in self.template_context_processors[name]:
context.update(func()) context.update(func())
# make sure the original values win. This makes it possible to
# easier add new variables in context processors without breaking
# existing views.
context.update(orig_ctx) context.update(orig_ctx)
def make_shell_context(self) -> dict: def make_shell_context(self) -> dict:
@ -1268,16 +1264,19 @@ class Flask(Scaffold):
self.shell_context_processors.append(f) self.shell_context_processors.append(f)
return f return f
def _find_error_handler(self, e: Exception) -> t.Optional[ErrorHandlerCallable]: def _find_error_handler(
self, e: Exception
) -> t.Optional["ErrorHandlerCallable[Exception]"]:
"""Return a registered error handler for an exception in this order: """Return a registered error handler for an exception in this order:
blueprint handler for a specific code, app handler for a specific code, blueprint handler for a specific code, app handler for a specific code,
blueprint handler for an exception class, app handler for an exception blueprint handler for an exception class, app handler for an exception
class, or ``None`` if a suitable handler is not found. class, or ``None`` if a suitable handler is not found.
""" """
exc_class, code = self._get_exc_class_and_code(type(e)) exc_class, code = self._get_exc_class_and_code(type(e))
names = (*request.blueprints, None)
for c in [code, None]: for c in (code, None) if code is not None else (None,):
for name in chain(request.blueprints, [None]): for name in names:
handler_map = self.error_handler_spec[name][c] handler_map = self.error_handler_spec[name][c]
if not handler_map: if not handler_map:
@ -1304,7 +1303,7 @@ class Flask(Scaffold):
.. versionchanged:: 1.0 .. versionchanged:: 1.0
Exceptions are looked up by code *and* by MRO, so Exceptions are looked up by code *and* by MRO, so
``HTTPExcpetion`` subclasses can be handled with a catch-all ``HTTPException`` subclasses can be handled with a catch-all
handler for the base ``HTTPException``. handler for the base ``HTTPException``.
.. versionadded:: 0.3 .. versionadded:: 0.3
@ -1619,14 +1618,7 @@ class Flask(Scaffold):
except ImportError: except ImportError:
raise RuntimeError( raise RuntimeError(
"Install Flask with the 'async' extra in order to use async views." "Install Flask with the 'async' extra in order to use async views."
) ) from None
# Check that Werkzeug isn't using its fallback ContextVar class.
if ContextVar.__module__ == "werkzeug.local":
raise RuntimeError(
"Async cannot be used with this combination of Python "
"and Greenlet versions."
)
return asgiref_async_to_sync(func) return asgiref_async_to_sync(func)
@ -1725,7 +1717,7 @@ class Flask(Scaffold):
" response. The return type must be a string," " response. The return type must be a string,"
" dict, tuple, Response instance, or WSGI" " dict, tuple, Response instance, or WSGI"
f" callable, but it was a {type(rv).__name__}." f" callable, but it was a {type(rv).__name__}."
).with_traceback(sys.exc_info()[2]) ).with_traceback(sys.exc_info()[2]) from None
else: else:
raise TypeError( raise TypeError(
"The view function did not return a valid" "The view function did not return a valid"
@ -1797,16 +1789,18 @@ class Flask(Scaffold):
.. versionadded:: 0.7 .. versionadded:: 0.7
""" """
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None] names: t.Iterable[t.Optional[str]] = (None,)
# url_for may be called outside a request context, parse the
# passed endpoint instead of using request.blueprints.
if "." in endpoint: if "." in endpoint:
# This is called by url_for, which can be called outside a names = chain(
# request, can't use request.blueprints. names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
bps = _split_blueprint_path(endpoint.rpartition(".")[0]) )
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
funcs = chain(funcs, bp_funcs)
for func in funcs: for name in names:
if name in self.url_default_functions:
for func in self.url_default_functions[name]:
func(endpoint, values) func(endpoint, values)
def handle_url_build_error( def handle_url_build_error(
@ -1842,22 +1836,18 @@ class Flask(Scaffold):
value is handled as if it was the return value from the view, and value is handled as if it was the return value from the view, and
further request handling is stopped. further request handling is stopped.
""" """
names = (None, *reversed(request.blueprints))
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[ for name in names:
None if name in self.url_value_preprocessors:
] for url_func in self.url_value_preprocessors[name]:
for bp in request.blueprints: url_func(request.endpoint, request.view_args)
if bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp]) for name in names:
for func in funcs: if name in self.before_request_funcs:
func(request.endpoint, request.view_args) for before_func in self.before_request_funcs[name]:
rv = self.ensure_sync(before_func)()
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None]
for bp in request.blueprints:
if bp in self.before_request_funcs:
funcs = chain(funcs, self.before_request_funcs[bp])
for func in funcs:
rv = self.ensure_sync(func)()
if rv is not None: if rv is not None:
return rv return rv
@ -1877,16 +1867,18 @@ class Flask(Scaffold):
instance of :attr:`response_class`. instance of :attr:`response_class`.
""" """
ctx = _request_ctx_stack.top ctx = _request_ctx_stack.top
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
for bp in request.blueprints: for func in ctx._after_request_functions:
if bp in self.after_request_funcs: response = self.ensure_sync(func)(response)
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs: for name in chain(request.blueprints, (None,)):
funcs = chain(funcs, reversed(self.after_request_funcs[None])) if name in self.after_request_funcs:
for handler in funcs: for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(handler)(response) response = self.ensure_sync(func)(response)
if not self.session_interface.is_null_session(ctx.session): if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response) self.session_interface.save_session(self, ctx.session, response)
return response return response
def do_teardown_request( def do_teardown_request(
@ -1914,14 +1906,12 @@ class Flask(Scaffold):
""" """
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
funcs: t.Iterable[TeardownCallable] = reversed(
self.teardown_request_funcs[None] for name in chain(request.blueprints, (None,)):
) if name in self.teardown_request_funcs:
for bp in request.blueprints: for func in reversed(self.teardown_request_funcs[name]):
if bp in self.teardown_request_funcs:
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
for func in funcs:
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
request_tearing_down.send(self, exc=exc) request_tearing_down.send(self, exc=exc)
def do_teardown_appcontext( def do_teardown_appcontext(
@ -1943,8 +1933,10 @@ class Flask(Scaffold):
""" """
if exc is _sentinel: if exc is _sentinel:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
for func in reversed(self.teardown_appcontext_funcs): for func in reversed(self.teardown_appcontext_funcs):
self.ensure_sync(func)(exc) self.ensure_sync(func)(exc)
appcontext_tearing_down.send(self, exc=exc) appcontext_tearing_down.send(self, exc=exc)
def app_context(self) -> AppContext: def app_context(self) -> AppContext:

View file

@ -1,3 +1,4 @@
import os
import typing as t import typing as t
from collections import defaultdict from collections import defaultdict
from functools import update_wrapper from functools import update_wrapper
@ -8,7 +9,6 @@ from .scaffold import Scaffold
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable from .typing import TemplateFilterCallable
@ -19,6 +19,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from .app import Flask from .app import Flask
from .typing import ErrorHandlerCallable
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable] DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
@ -175,7 +176,7 @@ class Blueprint(Scaffold):
self, self,
name: str, name: str,
import_name: str, import_name: str,
static_folder: t.Optional[str] = None, static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
template_folder: t.Optional[str] = None, template_folder: t.Optional[str] = None,
url_prefix: t.Optional[str] = None, url_prefix: t.Optional[str] = None,
@ -293,34 +294,26 @@ class Blueprint(Scaffold):
Registering the same blueprint with the same name multiple Registering the same blueprint with the same name multiple
times is deprecated and will become an error in Flask 2.1. times is deprecated and will become an error in Flask 2.1.
""" """
first_registration = not any(bp is self for bp in app.blueprints.values())
name_prefix = options.get("name_prefix", "") name_prefix = options.get("name_prefix", "")
self_name = options.get("name", self.name) self_name = options.get("name", self.name)
name = f"{name_prefix}.{self_name}".lstrip(".") name = f"{name_prefix}.{self_name}".lstrip(".")
if name in app.blueprints: if name in app.blueprints:
bp_desc = "this" if app.blueprints[name] is self else "a different"
existing_at = f" '{name}'" if self_name != name else "" existing_at = f" '{name}'" if self_name != name else ""
if app.blueprints[name] is not self:
raise ValueError( raise ValueError(
f"The name '{self_name}' is already registered for" f"The name '{self_name}' is already registered for"
f" a different blueprint{existing_at}. Use 'name='" f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
" to provide a unique name." f" provide a unique name."
) )
else:
import warnings
warnings.warn( first_bp_registration = not any(bp is self for bp in app.blueprints.values())
f"The name '{self_name}' is already registered for" first_name_registration = name not in app.blueprints
f" this blueprint{existing_at}. Use 'name=' to"
" provide a unique name. This will become an error"
" in Flask 2.1.",
stacklevel=4,
)
app.blueprints[name] = self app.blueprints[name] = self
self._got_registered_once = True self._got_registered_once = True
state = self.make_setup_state(app, options, first_registration) state = self.make_setup_state(app, options, first_bp_registration)
if self.has_static_folder: if self.has_static_folder:
state.add_url_rule( state.add_url_rule(
@ -330,7 +323,7 @@ class Blueprint(Scaffold):
) )
# Merge blueprint data into parent. # Merge blueprint data into parent.
if first_registration: if first_bp_registration or first_name_registration:
def extend(bp_dict, parent_dict): def extend(bp_dict, parent_dict):
for key, values in bp_dict.items(): for key, values in bp_dict.items():
@ -581,7 +574,9 @@ class Blueprint(Scaffold):
handler is used for all requests, even if outside of the blueprint. handler is used for all requests, even if outside of the blueprint.
""" """
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable: def decorator(
f: "ErrorHandlerCallable[Exception]",
) -> "ErrorHandlerCallable[Exception]":
self.record_once(lambda s: s.app.errorhandler(code)(f)) self.record_once(lambda s: s.app.errorhandler(code)(f))
return f return f

View file

@ -5,7 +5,6 @@ import platform
import re import re
import sys import sys
import traceback import traceback
import warnings
from functools import update_wrapper from functools import update_wrapper
from operator import attrgetter from operator import attrgetter
from threading import Lock from threading import Lock
@ -34,7 +33,7 @@ class NoAppException(click.UsageError):
"""Raised if an application cannot be found or loaded.""" """Raised if an application cannot be found or loaded."""
def find_best_app(script_info, module): def find_best_app(module):
"""Given a module instance this tries to find the best possible """Given a module instance this tries to find the best possible
application in the module or raises an exception. application in the module or raises an exception.
""" """
@ -65,19 +64,20 @@ def find_best_app(script_info, module):
if inspect.isfunction(app_factory): if inspect.isfunction(app_factory):
try: try:
app = call_factory(script_info, app_factory) app = app_factory()
if isinstance(app, Flask): if isinstance(app, Flask):
return app return app
except TypeError: except TypeError as e:
if not _called_with_wrong_args(app_factory): if not _called_with_wrong_args(app_factory):
raise raise
raise NoAppException( raise NoAppException(
f"Detected factory {attr_name!r} in module {module.__name__!r}," f"Detected factory {attr_name!r} in module {module.__name__!r},"
" but could not call it without arguments. Use" " but could not call it without arguments. Use"
f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\"" f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
" to specify arguments." " to specify arguments."
) ) from e
raise NoAppException( raise NoAppException(
"Failed to find Flask application or factory in module" "Failed to find Flask application or factory in module"
@ -86,39 +86,6 @@ def find_best_app(script_info, module):
) )
def call_factory(script_info, app_factory, args=None, kwargs=None):
"""Takes an app factory, a ``script_info` object and optionally a tuple
of arguments. Checks for the existence of a script_info argument and calls
the app_factory depending on that and the arguments provided.
"""
sig = inspect.signature(app_factory)
args = [] if args is None else args
kwargs = {} if kwargs is None else kwargs
if "script_info" in sig.parameters:
warnings.warn(
"The 'script_info' argument is deprecated and will not be"
" passed to the app factory function in Flask 2.1.",
DeprecationWarning,
)
kwargs["script_info"] = script_info
if (
not args
and len(sig.parameters) == 1
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty
):
warnings.warn(
"Script info is deprecated and will not be passed as the"
" single argument to the app factory function in Flask"
" 2.1.",
DeprecationWarning,
)
args.append(script_info)
return app_factory(*args, **kwargs)
def _called_with_wrong_args(f): def _called_with_wrong_args(f):
"""Check whether calling a function raised a ``TypeError`` because """Check whether calling a function raised a ``TypeError`` because
the call failed or because something in the factory raised the the call failed or because something in the factory raised the
@ -145,7 +112,7 @@ def _called_with_wrong_args(f):
del tb del tb
def find_app_by_string(script_info, module, app_name): def find_app_by_string(module, app_name):
"""Check if the given string is a variable name or a function. Call """Check if the given string is a variable name or a function. Call
a function to get the app instance, or return the variable directly. a function to get the app instance, or return the variable directly.
""" """
@ -158,11 +125,12 @@ def find_app_by_string(script_info, module, app_name):
except SyntaxError: except SyntaxError:
raise NoAppException( raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call." f"Failed to parse {app_name!r} as an attribute name or function call."
) ) from None
if isinstance(expr, ast.Name): if isinstance(expr, ast.Name):
name = expr.id name = expr.id
args = kwargs = None args = []
kwargs = {}
elif isinstance(expr, ast.Call): elif isinstance(expr, ast.Call):
# Ensure the function name is an attribute name only. # Ensure the function name is an attribute name only.
if not isinstance(expr.func, ast.Name): if not isinstance(expr.func, ast.Name):
@ -181,7 +149,7 @@ def find_app_by_string(script_info, module, app_name):
# message with the full expression instead. # message with the full expression instead.
raise NoAppException( raise NoAppException(
f"Failed to parse arguments as literal values: {app_name!r}." f"Failed to parse arguments as literal values: {app_name!r}."
) ) from None
else: else:
raise NoAppException( raise NoAppException(
f"Failed to parse {app_name!r} as an attribute name or function call." f"Failed to parse {app_name!r} as an attribute name or function call."
@ -189,17 +157,17 @@ def find_app_by_string(script_info, module, app_name):
try: try:
attr = getattr(module, name) attr = getattr(module, name)
except AttributeError: except AttributeError as e:
raise NoAppException( raise NoAppException(
f"Failed to find attribute {name!r} in {module.__name__!r}." f"Failed to find attribute {name!r} in {module.__name__!r}."
) ) from e
# If the attribute is a function, call it with any args and kwargs # If the attribute is a function, call it with any args and kwargs
# to get the real application. # to get the real application.
if inspect.isfunction(attr): if inspect.isfunction(attr):
try: try:
app = call_factory(script_info, attr, args, kwargs) app = attr(*args, **kwargs)
except TypeError: except TypeError as e:
if not _called_with_wrong_args(attr): if not _called_with_wrong_args(attr):
raise raise
@ -207,7 +175,7 @@ def find_app_by_string(script_info, module, app_name):
f"The factory {app_name!r} in module" f"The factory {app_name!r} in module"
f" {module.__name__!r} could not be called with the" f" {module.__name__!r} could not be called with the"
" specified arguments." " specified arguments."
) ) from e
else: else:
app = attr app = attr
@ -249,7 +217,7 @@ def prepare_import(path):
return ".".join(module_name[::-1]) return ".".join(module_name[::-1])
def locate_app(script_info, module_name, app_name, raise_if_not_found=True): def locate_app(module_name, app_name, raise_if_not_found=True):
__traceback_hide__ = True # noqa: F841 __traceback_hide__ = True # noqa: F841
try: try:
@ -261,18 +229,18 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
raise NoAppException( raise NoAppException(
f"While importing {module_name!r}, an ImportError was" f"While importing {module_name!r}, an ImportError was"
f" raised:\n\n{traceback.format_exc()}" f" raised:\n\n{traceback.format_exc()}"
) ) from None
elif raise_if_not_found: elif raise_if_not_found:
raise NoAppException(f"Could not import {module_name!r}.") raise NoAppException(f"Could not import {module_name!r}.") from None
else: else:
return return
module = sys.modules[module_name] module = sys.modules[module_name]
if app_name is None: if app_name is None:
return find_best_app(script_info, module) return find_best_app(module)
else: else:
return find_app_by_string(script_info, module, app_name) return find_app_by_string(module, app_name)
def get_version(ctx, param, value): def get_version(ctx, param, value):
@ -312,7 +280,7 @@ class DispatchingApp:
self.loader = loader self.loader = loader
self._app = None self._app = None
self._lock = Lock() self._lock = Lock()
self._bg_loading_exc_info = None self._bg_loading_exc = None
if use_eager_loading is None: if use_eager_loading is None:
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
@ -328,23 +296,24 @@ class DispatchingApp:
with self._lock: with self._lock:
try: try:
self._load_unlocked() self._load_unlocked()
except Exception: except Exception as e:
self._bg_loading_exc_info = sys.exc_info() self._bg_loading_exc = e
t = Thread(target=_load_app, args=()) t = Thread(target=_load_app, args=())
t.start() t.start()
def _flush_bg_loading_exception(self): def _flush_bg_loading_exception(self):
__traceback_hide__ = True # noqa: F841 __traceback_hide__ = True # noqa: F841
exc_info = self._bg_loading_exc_info exc = self._bg_loading_exc
if exc_info is not None:
self._bg_loading_exc_info = None if exc is not None:
raise exc_info self._bg_loading_exc = None
raise exc
def _load_unlocked(self): def _load_unlocked(self):
__traceback_hide__ = True # noqa: F841 __traceback_hide__ = True # noqa: F841
self._app = rv = self.loader() self._app = rv = self.loader()
self._bg_loading_exc_info = None self._bg_loading_exc = None
return rv return rv
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
@ -392,18 +361,18 @@ class ScriptInfo:
return self._loaded_app return self._loaded_app
if self.create_app is not None: if self.create_app is not None:
app = call_factory(self, self.create_app) app = self.create_app()
else: else:
if self.app_import_path: if self.app_import_path:
path, name = ( path, name = (
re.split(r":(?![\\/])", self.app_import_path, 1) + [None] re.split(r":(?![\\/])", self.app_import_path, 1) + [None]
)[:2] )[:2]
import_name = prepare_import(path) import_name = prepare_import(path)
app = locate_app(self, import_name, name) app = locate_app(import_name, name)
else: else:
for path in ("wsgi.py", "app.py"): for path in ("wsgi.py", "app.py"):
import_name = prepare_import(path) import_name = prepare_import(path)
app = locate_app(self, import_name, None, raise_if_not_found=False) app = locate_app(import_name, None, raise_if_not_found=False)
if app: if app:
break break
@ -721,7 +690,7 @@ class CertParamType(click.ParamType):
"Using ad-hoc certificates requires the cryptography library.", "Using ad-hoc certificates requires the cryptography library.",
ctx, ctx,
param, param,
) ) from None
return value return value
@ -979,15 +948,7 @@ debug mode.
def main() -> None: def main() -> None:
if int(click.__version__[0]) < 8: cli.main()
warnings.warn(
"Using the `flask` cli with Click 7 is deprecated and"
" will not be supported starting with Flask 2.1."
" Please upgrade to Click 8 as soon as possible.",
DeprecationWarning,
)
# TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed
cli.main(args=sys.argv[1:])
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -83,7 +83,7 @@ class Config(dict):
:param variable_name: name of the environment variable :param variable_name: name of the environment variable
:param silent: set to ``True`` if you want silent failure for missing :param silent: set to ``True`` if you want silent failure for missing
files. files.
:return: bool. ``True`` if able to load config, ``False`` otherwise. :return: ``True`` if the file was loaded successfully.
""" """
rv = os.environ.get(variable_name) rv = os.environ.get(variable_name)
if not rv: if not rv:
@ -107,6 +107,7 @@ class Config(dict):
root path. root path.
:param silent: set to ``True`` if you want silent failure for missing :param silent: set to ``True`` if you want silent failure for missing
files. files.
:return: ``True`` if the file was loaded successfully.
.. versionadded:: 0.7 .. versionadded:: 0.7
`silent` parameter. `silent` parameter.
@ -175,6 +176,9 @@ class Config(dict):
.. code-block:: python .. code-block:: python
import json
app.config.from_file("config.json", load=json.load)
import toml import toml
app.config.from_file("config.toml", load=toml.load) app.config.from_file("config.toml", load=toml.load)
@ -185,6 +189,7 @@ class Config(dict):
:type load: ``Callable[[Reader], Mapping]`` where ``Reader`` :type load: ``Callable[[Reader], Mapping]`` where ``Reader``
implements a ``read`` method. implements a ``read`` method.
:param silent: Ignore the file if it doesn't exist. :param silent: Ignore the file if it doesn't exist.
:return: ``True`` if the file was loaded successfully.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
@ -202,36 +207,12 @@ class Config(dict):
return self.from_mapping(obj) return self.from_mapping(obj)
def from_json(self, filename: str, silent: bool = False) -> bool:
"""Update the values in the config from a JSON file. The loaded
data is passed to the :meth:`from_mapping` method.
:param filename: The path to the JSON file. This can be an
absolute path or relative to the config root path.
:param silent: Ignore the file if it doesn't exist.
.. deprecated:: 2.0.0
Will be removed in Flask 2.1. Use :meth:`from_file` instead.
This was removed early in 2.0.0, was added back in 2.0.1.
.. versionadded:: 0.11
"""
import warnings
from . import json
warnings.warn(
"'from_json' is deprecated and will be removed in Flask"
" 2.1. Use 'from_file(path, json.load)' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.from_file(filename, json.load, silent=silent)
def from_mapping( def from_mapping(
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
) -> bool: ) -> bool:
"""Updates the config like :meth:`update` ignoring items with non-upper """Updates the config like :meth:`update` ignoring items with non-upper
keys. keys.
:return: Always returns ``True``.
.. versionadded:: 0.11 .. versionadded:: 0.11
""" """

View file

@ -130,7 +130,15 @@ def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable:
.. versionadded:: 0.9 .. versionadded:: 0.9
""" """
_request_ctx_stack.top._after_request_functions.append(f) top = _request_ctx_stack.top
if top is None:
raise RuntimeError(
"This decorator can only be used when a request context is"
" active, such as within a view function."
)
top._after_request_functions.append(f)
return f return f
@ -159,17 +167,18 @@ def copy_current_request_context(f: t.Callable) -> t.Callable:
.. versionadded:: 0.10 .. versionadded:: 0.10
""" """
top = _request_ctx_stack.top top = _request_ctx_stack.top
if top is None: if top is None:
raise RuntimeError( raise RuntimeError(
"This decorator can only be used at local scopes " "This decorator can only be used when a request context is"
"when a request context is on the stack. For instance within " " active, such as within a view function."
"view functions."
) )
reqctx = top.copy() reqctx = top.copy()
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
with reqctx: with reqctx:
return f(*args, **kwargs) return reqctx.app.ensure_sync(f)(*args, **kwargs)
return update_wrapper(wrapper, f) return update_wrapper(wrapper, f)
@ -332,11 +341,29 @@ class RequestContext:
self._after_request_functions: t.List[AfterRequestCallable] = [] self._after_request_functions: t.List[AfterRequestCallable] = []
@property @property
def g(self) -> AppContext: def g(self) -> _AppCtxGlobals:
import warnings
warnings.warn(
"Accessing 'g' on the request context is deprecated and"
" will be removed in Flask 2.2. Access `g` directly or from"
"the application context instead.",
DeprecationWarning,
stacklevel=2,
)
return _app_ctx_stack.top.g return _app_ctx_stack.top.g
@g.setter @g.setter
def g(self, value: AppContext) -> None: def g(self, value: _AppCtxGlobals) -> None:
import warnings
warnings.warn(
"Setting 'g' on the request context is deprecated and"
" will be removed in Flask 2.2. Set it on the application"
" context instead.",
DeprecationWarning,
stacklevel=2,
)
_app_ctx_stack.top.g = value _app_ctx_stack.top.g = value
def copy(self) -> "RequestContext": def copy(self) -> "RequestContext":

View file

@ -83,10 +83,11 @@ def attach_enctype_error_multidict(request):
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return oldcls.__getitem__(self, key) return oldcls.__getitem__(self, key)
except KeyError: except KeyError as e:
if key not in request.form: if key not in request.form:
raise raise
raise DebugFilesKeyError(request, key)
raise DebugFilesKeyError(request, key) from e
newcls.__name__ = oldcls.__name__ newcls.__name__ = oldcls.__name__
newcls.__module__ = oldcls.__module__ newcls.__module__ = oldcls.__module__

View file

@ -5,13 +5,11 @@ import sys
import typing as t import typing as t
import warnings import warnings
from datetime import datetime from datetime import datetime
from datetime import timedelta
from functools import lru_cache from functools import lru_cache
from functools import update_wrapper from functools import update_wrapper
from threading import RLock from threading import RLock
import werkzeug.utils import werkzeug.utils
from werkzeug.exceptions import NotFound
from werkzeug.routing import BuildError from werkzeug.routing import BuildError
from werkzeug.urls import url_quote from werkzeug.urls import url_quote
@ -454,7 +452,7 @@ def _prepare_send_file_kwargs(
warnings.warn( warnings.warn(
"The 'attachment_filename' parameter has been renamed to" "The 'attachment_filename' parameter has been renamed to"
" 'download_name'. The old name will be removed in Flask" " 'download_name'. The old name will be removed in Flask"
" 2.1.", " 2.2.",
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=3,
) )
@ -463,7 +461,7 @@ def _prepare_send_file_kwargs(
if cache_timeout is not None: if cache_timeout is not None:
warnings.warn( warnings.warn(
"The 'cache_timeout' parameter has been renamed to" "The 'cache_timeout' parameter has been renamed to"
" 'max_age'. The old name will be removed in Flask 2.1.", " 'max_age'. The old name will be removed in Flask 2.2.",
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=3,
) )
@ -472,7 +470,7 @@ def _prepare_send_file_kwargs(
if add_etags is not None: if add_etags is not None:
warnings.warn( warnings.warn(
"The 'add_etags' parameter has been renamed to 'etag'. The" "The 'add_etags' parameter has been renamed to 'etag'. The"
" old name will be removed in Flask 2.1.", " old name will be removed in Flask 2.2.",
DeprecationWarning, DeprecationWarning,
stacklevel=3, stacklevel=3,
) )
@ -627,29 +625,6 @@ def send_file(
) )
def safe_join(directory: str, *pathnames: str) -> str:
"""Safely join zero or more untrusted path components to a base
directory to avoid escaping the base directory.
:param directory: The trusted base directory.
:param pathnames: The untrusted path components relative to the
base directory.
:return: A safe path, otherwise ``None``.
"""
warnings.warn(
"'flask.helpers.safe_join' is deprecated and will be removed in"
" Flask 2.1. Use 'werkzeug.utils.safe_join' instead.",
DeprecationWarning,
stacklevel=2,
)
path = werkzeug.utils.safe_join(directory, *pathnames)
if path is None:
raise NotFound()
return path
def send_from_directory( def send_from_directory(
directory: t.Union[os.PathLike, str], directory: t.Union[os.PathLike, str],
path: t.Union[os.PathLike, str], path: t.Union[os.PathLike, str],
@ -691,7 +666,7 @@ def send_from_directory(
if filename is not None: if filename is not None:
warnings.warn( warnings.warn(
"The 'filename' parameter has been renamed to 'path'. The" "The 'filename' parameter has been renamed to 'path'. The"
" old name will be removed in Flask 2.1.", " old name will be removed in Flask 2.2.",
DeprecationWarning, DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -714,7 +689,7 @@ def get_root_path(import_name: str) -> str:
# Module already imported and has a file attribute. Use that first. # Module already imported and has a file attribute. Use that first.
mod = sys.modules.get(import_name) mod = sys.modules.get(import_name)
if mod is not None and hasattr(mod, "__file__"): if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
return os.path.dirname(os.path.abspath(mod.__file__)) return os.path.dirname(os.path.abspath(mod.__file__))
# Next attempt: check the loader. # Next attempt: check the loader.
@ -785,27 +760,6 @@ class locked_cached_property(werkzeug.utils.cached_property):
super().__delete__(obj) super().__delete__(obj)
def total_seconds(td: timedelta) -> int:
"""Returns the total seconds from a timedelta object.
:param timedelta td: the timedelta to be converted in seconds
:returns: number of seconds
:rtype: int
.. deprecated:: 2.0
Will be removed in Flask 2.1. Use
:meth:`timedelta.total_seconds` instead.
"""
warnings.warn(
"'total_seconds' is deprecated and will be removed in Flask"
" 2.1. Use 'timedelta.total_seconds' instead.",
DeprecationWarning,
stacklevel=2,
)
return td.days * 60 * 60 * 24 + td.seconds
def is_ip(value: str) -> bool: def is_ip(value: str) -> bool:
"""Determine if the given string is an IP address. """Determine if the given string is an IP address.

View file

@ -1,8 +1,8 @@
import io import dataclasses
import decimal
import json as _json import json as _json
import typing as t import typing as t
import uuid import uuid
import warnings
from datetime import date from datetime import date
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
@ -15,12 +15,6 @@ if t.TYPE_CHECKING:
from ..app import Flask from ..app import Flask
from ..wrappers import Response from ..wrappers import Response
try:
import dataclasses
except ImportError:
# Python < 3.7
dataclasses = None # type: ignore
class JSONEncoder(_json.JSONEncoder): class JSONEncoder(_json.JSONEncoder):
"""The default JSON encoder. Handles extra types compared to the """The default JSON encoder. Handles extra types compared to the
@ -47,7 +41,7 @@ class JSONEncoder(_json.JSONEncoder):
""" """
if isinstance(o, date): if isinstance(o, date):
return http_date(o) return http_date(o)
if isinstance(o, uuid.UUID): if isinstance(o, (decimal.Decimal, uuid.UUID)):
return str(o) return str(o)
if dataclasses and dataclasses.is_dataclass(o): if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o) return dataclasses.asdict(o)
@ -80,6 +74,11 @@ def _dump_arg_defaults(
if bp is not None and bp.json_encoder is not None: if bp is not None and bp.json_encoder is not None:
cls = bp.json_encoder cls = bp.json_encoder
# Only set a custom encoder if it has custom behavior. This is
# faster on PyPy.
if cls is not _json.JSONEncoder:
kwargs.setdefault("cls", cls)
kwargs.setdefault("cls", cls) kwargs.setdefault("cls", cls)
kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"]) kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"]) kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
@ -101,9 +100,10 @@ def _load_arg_defaults(
if bp is not None and bp.json_decoder is not None: if bp is not None and bp.json_decoder is not None:
cls = bp.json_decoder cls = bp.json_decoder
# Only set a custom decoder if it has custom behavior. This is
# faster on PyPy.
if cls not in {JSONDecoder, _json.JSONDecoder}:
kwargs.setdefault("cls", cls) kwargs.setdefault("cls", cls)
else:
kwargs.setdefault("cls", JSONDecoder)
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str: def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
@ -117,6 +117,9 @@ def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
or defaults. or defaults.
:param kwargs: Extra arguments passed to :func:`json.dumps`. :param kwargs: Extra arguments passed to :func:`json.dumps`.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. ``encoding`` is deprecated and will be removed in Flask 2.1.
@ -125,20 +128,7 @@ def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
context for configuration. context for configuration.
""" """
_dump_arg_defaults(kwargs, app=app) _dump_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None) return _json.dumps(obj, **kwargs)
rv = _json.dumps(obj, **kwargs)
if encoding is not None:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(rv, str):
return rv.encode(encoding) # type: ignore
return rv
def dump( def dump(
@ -160,23 +150,6 @@ def dump(
deprecated and will be removed in Flask 2.1. deprecated and will be removed in Flask 2.1.
""" """
_dump_arg_defaults(kwargs, app=app) _dump_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
show_warning = encoding is not None
try:
fp.write("")
except TypeError:
show_warning = True
fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore
if show_warning:
warnings.warn(
"Writing to a binary file, and the 'encoding' argument, is"
" deprecated and will be removed in Flask 2.1.",
DeprecationWarning,
stacklevel=2,
)
_json.dump(obj, fp, **kwargs) _json.dump(obj, fp, **kwargs)
@ -200,19 +173,6 @@ def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
context for configuration. context for configuration.
""" """
_load_arg_defaults(kwargs, app=app) _load_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
if encoding is not None:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1."
" The data must be a string or UTF-8 bytes.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(s, bytes):
s = s.decode(encoding)
return _json.loads(s, **kwargs) return _json.loads(s, **kwargs)
@ -232,20 +192,6 @@ def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.A
file must be text mode, or binary mode with UTF-8 bytes. file must be text mode, or binary mode with UTF-8 bytes.
""" """
_load_arg_defaults(kwargs, app=app) _load_arg_defaults(kwargs, app=app)
encoding = kwargs.pop("encoding", None)
if encoding is not None:
warnings.warn(
"'encoding' is deprecated and will be removed in Flask 2.1."
" The file must be text mode, or binary mode with UTF-8"
" bytes.",
DeprecationWarning,
stacklevel=2,
)
if isinstance(fp.read(0), bytes):
fp = io.TextIOWrapper(fp, encoding) # type: ignore
return _json.load(fp, **kwargs) return _json.load(fp, **kwargs)
@ -324,6 +270,9 @@ def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``, debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
the output will be formatted to be easier to read. the output will be formatted to be easier to read.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 0.11 .. versionchanged:: 0.11
Added support for serializing top-level arrays. This introduces Added support for serializing top-level arrays. This introduces
a security risk in ancient browsers. See :ref:`security-json`. a security risk in ancient browsers. See :ref:`security-json`.

View file

@ -21,7 +21,7 @@ from .templating import _default_template_ctx_processor
from .typing import AfterRequestCallable from .typing import AfterRequestCallable
from .typing import AppOrBlueprintKey from .typing import AppOrBlueprintKey
from .typing import BeforeRequestCallable from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable from .typing import GenericException
from .typing import TeardownCallable from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable from .typing import TemplateContextProcessorCallable
from .typing import URLDefaultCallable from .typing import URLDefaultCallable
@ -29,6 +29,7 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from .wrappers import Response from .wrappers import Response
from .typing import ErrorHandlerCallable
# a singleton sentinel value for parameter defaults # a singleton sentinel value for parameter defaults
_sentinel = object() _sentinel = object()
@ -91,7 +92,7 @@ class Scaffold:
def __init__( def __init__(
self, self,
import_name: str, import_name: str,
static_folder: t.Optional[str] = None, static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
template_folder: t.Optional[str] = None, template_folder: t.Optional[str] = None,
root_path: t.Optional[str] = None, root_path: t.Optional[str] = None,
@ -100,7 +101,7 @@ class Scaffold:
#: to. Do not change this once it is set by the constructor. #: to. Do not change this once it is set by the constructor.
self.import_name = import_name self.import_name = import_name
self.static_folder = static_folder self.static_folder = static_folder # type: ignore
self.static_url_path = static_url_path self.static_url_path = static_url_path
#: The path to the templates folder, relative to #: The path to the templates folder, relative to
@ -144,7 +145,10 @@ class Scaffold:
#: directly and its format may change at any time. #: directly and its format may change at any time.
self.error_handler_spec: t.Dict[ self.error_handler_spec: t.Dict[
AppOrBlueprintKey, AppOrBlueprintKey,
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ErrorHandlerCallable]], t.Dict[
t.Optional[int],
t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
],
] = defaultdict(lambda: defaultdict(dict)) ] = defaultdict(lambda: defaultdict(dict))
#: A data structure of functions to call at the beginning of #: A data structure of functions to call at the beginning of
@ -253,7 +257,7 @@ class Scaffold:
return None return None
@static_folder.setter @static_folder.setter
def static_folder(self, value: t.Optional[str]) -> None: def static_folder(self, value: t.Optional[t.Union[str, os.PathLike]]) -> None:
if value is not None: if value is not None:
value = os.fspath(value).rstrip(r"\/") value = os.fspath(value).rstrip(r"\/")
@ -585,10 +589,10 @@ class Scaffold:
stack of active contexts. This becomes relevant if you are using stack of active contexts. This becomes relevant if you are using
such constructs in tests. such constructs in tests.
Teardown functions must avoid raising exceptions, since they . If they Teardown functions must avoid raising exceptions. If
execute code that might fail they they execute code that might fail they
will have to surround the execution of these code by try/except will have to surround the execution of that code with try/except
statements and log occurring errors. statements and log any errors.
When a teardown function was called because of an exception it will When a teardown function was called because of an exception it will
be passed an error object. be passed an error object.
@ -643,8 +647,11 @@ class Scaffold:
@setupmethod @setupmethod
def errorhandler( def errorhandler(
self, code_or_exception: t.Union[t.Type[Exception], int] self, code_or_exception: t.Union[t.Type[GenericException], int]
) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]: ) -> t.Callable[
["ErrorHandlerCallable[GenericException]"],
"ErrorHandlerCallable[GenericException]",
]:
"""Register a function to handle errors by code or exception class. """Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an A decorator that is used to register a function given an
@ -674,7 +681,9 @@ class Scaffold:
an arbitrary exception an arbitrary exception
""" """
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable: def decorator(
f: "ErrorHandlerCallable[GenericException]",
) -> "ErrorHandlerCallable[GenericException]":
self.register_error_handler(code_or_exception, f) self.register_error_handler(code_or_exception, f)
return f return f
@ -683,8 +692,8 @@ class Scaffold:
@setupmethod @setupmethod
def register_error_handler( def register_error_handler(
self, self,
code_or_exception: t.Union[t.Type[Exception], int], code_or_exception: t.Union[t.Type[GenericException], int],
f: ErrorHandlerCallable, f: "ErrorHandlerCallable[GenericException]",
) -> None: ) -> None:
"""Alternative error attach function to the :meth:`errorhandler` """Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator decorator that is more straightforward to use for non decorator
@ -706,9 +715,11 @@ class Scaffold:
f"'{code_or_exception}' is not a recognized HTTP error" f"'{code_or_exception}' is not a recognized HTTP error"
" code. Use a subclass of HTTPException with that code" " code. Use a subclass of HTTPException with that code"
" instead." " instead."
) ) from None
self.error_handler_spec[None][code][exc_class] = f self.error_handler_spec[None][code][exc_class] = t.cast(
"ErrorHandlerCallable[Exception]", f
)
@staticmethod @staticmethod
def _get_exc_class_and_code( def _get_exc_class_and_code(

View file

@ -131,6 +131,13 @@ class SessionInterface:
app = Flask(__name__) app = Flask(__name__)
app.session_interface = MySessionInterface() app.session_interface = MySessionInterface()
Multiple requests with the same session may be sent and handled
concurrently. When implementing a new session interface, consider
whether reads or writes to the backing store must be synchronized.
There is no guarantee on the order in which the session for each
request is opened or saved, it will occur in the order that requests
begin and end processing.
.. versionadded:: 0.8 .. versionadded:: 0.8
""" """
@ -292,20 +299,25 @@ class SessionInterface:
def open_session( def open_session(
self, app: "Flask", request: "Request" self, app: "Flask", request: "Request"
) -> t.Optional[SessionMixin]: ) -> t.Optional[SessionMixin]:
"""This method has to be implemented and must either return ``None`` """This is called at the beginning of each request, after
in case the loading failed because of a configuration error or an pushing the request context, before matching the URL.
instance of a session object which implements a dictionary like
interface + the methods and attributes on :class:`SessionMixin`. This must return an object which implements a dictionary-like
interface as well as the :class:`SessionMixin` interface.
This will return ``None`` to indicate that loading failed in
some way that is not immediately an error. The request
context will fall back to using :meth:`make_null_session`
in this case.
""" """
raise NotImplementedError() raise NotImplementedError()
def save_session( def save_session(
self, app: "Flask", session: SessionMixin, response: "Response" self, app: "Flask", session: SessionMixin, response: "Response"
) -> None: ) -> None:
"""This is called for actual sessions returned by :meth:`open_session` """This is called at the end of each request, after generating
at the end of the request. This is still called during a request a response, before removing the request context. It is skipped
context so if you absolutely need access to the request you can do if :meth:`is_null_session` returns ``True``.
that.
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -29,7 +29,7 @@ except ImportError:
raise RuntimeError( raise RuntimeError(
"Signalling support is unavailable because the blinker" "Signalling support is unavailable because the blinker"
" library is not installed." " library is not installed."
) ) from None
connect = connect_via = connected_to = temporarily_connected_to = _fail connect = connect_via = connected_to = temporarily_connected_to = _fail
disconnect = _fail disconnect = _fail

View file

@ -9,14 +9,15 @@ from werkzeug.test import Client
from werkzeug.urls import url_parse from werkzeug.urls import url_parse
from werkzeug.wrappers import Request as BaseRequest from werkzeug.wrappers import Request as BaseRequest
from . import _request_ctx_stack
from .cli import ScriptInfo from .cli import ScriptInfo
from .globals import _request_ctx_stack
from .json import dumps as json_dumps from .json import dumps as json_dumps
from .sessions import SessionMixin from .sessions import SessionMixin
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
from werkzeug.test import TestResponse
from .app import Flask from .app import Flask
from .wrappers import Response
class EnvironBuilder(werkzeug.test.EnvironBuilder): class EnvironBuilder(werkzeug.test.EnvironBuilder):
@ -171,14 +172,15 @@ class FlaskClient(Client):
headers = resp.get_wsgi_headers(c.request.environ) headers = resp.get_wsgi_headers(c.request.environ)
self.cookie_jar.extract_wsgi(c.request.environ, headers) self.cookie_jar.extract_wsgi(c.request.environ, headers)
def open( # type: ignore def open(
self, self,
*args: t.Any, *args: t.Any,
as_tuple: bool = False,
buffered: bool = False, buffered: bool = False,
follow_redirects: bool = False, follow_redirects: bool = False,
**kwargs: t.Any, **kwargs: t.Any,
) -> "Response": ) -> "TestResponse":
as_tuple = kwargs.pop("as_tuple", None)
# Same logic as super.open, but apply environ_base and preserve_context. # Same logic as super.open, but apply environ_base and preserve_context.
request = None request = None
@ -213,12 +215,28 @@ class FlaskClient(Client):
finally: finally:
builder.close() builder.close()
return super().open( # type: ignore if as_tuple is not None:
import warnings
warnings.warn(
"'as_tuple' is deprecated and will be removed in"
" Werkzeug 2.1 and Flask 2.1. Use"
" 'response.request.environ' instead.",
DeprecationWarning,
stacklevel=3,
)
return super().open(
request, request,
as_tuple=as_tuple, as_tuple=as_tuple,
buffered=buffered, buffered=buffered,
follow_redirects=follow_redirects, follow_redirects=follow_redirects,
) )
else:
return super().open(
request,
buffered=buffered,
follow_redirects=follow_redirects,
)
def __enter__(self) -> "FlaskClient": def __enter__(self) -> "FlaskClient":
if self.preserve_context: if self.preserve_context:
@ -272,7 +290,7 @@ class FlaskCliRunner(CliRunner):
:return: a :class:`~click.testing.Result` object. :return: a :class:`~click.testing.Result` object.
""" """
if cli is None: if cli is None:
cli = self.app.cli cli = self.app.cli # type: ignore
if "obj" not in kwargs: if "obj" not in kwargs:
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)

View file

@ -33,11 +33,12 @@ ResponseReturnValue = t.Union[
"WSGIApplication", "WSGIApplication",
] ]
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Callable[["Response"], "Response"] AfterRequestCallable = t.Callable[["Response"], "Response"]
BeforeFirstRequestCallable = t.Callable[[], None] BeforeFirstRequestCallable = t.Callable[[], None]
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]] BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
TeardownCallable = t.Callable[[t.Optional[BaseException]], None] TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
TemplateFilterCallable = t.Callable[..., t.Any] TemplateFilterCallable = t.Callable[..., t.Any]
@ -45,3 +46,4 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool] TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None] URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None] URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
ErrorHandlerCallable = t.Callable[[GenericException], ResponseReturnValue]

View file

@ -1,5 +1,6 @@
import typing as t import typing as t
from .globals import current_app
from .globals import request from .globals import request
from .typing import ResponseReturnValue from .typing import ResponseReturnValue
@ -80,7 +81,7 @@ class View:
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue: def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
self = view.view_class(*class_args, **class_kwargs) # type: ignore self = view.view_class(*class_args, **class_kwargs) # type: ignore
return self.dispatch_request(*args, **kwargs) return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
if cls.decorators: if cls.decorators:
view.__name__ = name view.__name__ = name
@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType):
meth = getattr(self, "get", None) meth = getattr(self, "get", None)
assert meth is not None, f"Unimplemented method {request.method!r}" assert meth is not None, f"Unimplemented method {request.method!r}"
return meth(*args, **kwargs) return current_app.ensure_sync(meth)(*args, **kwargs)

View file

@ -1,11 +1,12 @@
import asyncio import asyncio
import sys
import pytest import pytest
from flask import Blueprint from flask import Blueprint
from flask import Flask from flask import Flask
from flask import request from flask import request
from flask.views import MethodView
from flask.views import View
pytest.importorskip("asgiref") pytest.importorskip("asgiref")
@ -18,6 +19,24 @@ class BlueprintError(Exception):
pass pass
class AsyncView(View):
methods = ["GET", "POST"]
async def dispatch_request(self):
await asyncio.sleep(0)
return request.method
class AsyncMethodView(MethodView):
async def get(self):
await asyncio.sleep(0)
return "GET"
async def post(self):
await asyncio.sleep(0)
return "POST"
@pytest.fixture(name="async_app") @pytest.fixture(name="async_app")
def _async_app(): def _async_app():
app = Flask(__name__) app = Flask(__name__)
@ -53,11 +72,13 @@ def _async_app():
app.register_blueprint(blueprint, url_prefix="/bp") app.register_blueprint(blueprint, url_prefix="/bp")
app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
return app return app
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7") @pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
@pytest.mark.parametrize("path", ["/", "/home", "/bp/"])
def test_async_route(path, async_app): def test_async_route(path, async_app):
test_client = async_app.test_client() test_client = async_app.test_client()
response = test_client.get(path) response = test_client.get(path)
@ -66,7 +87,6 @@ def test_async_route(path, async_app):
assert b"POST" in response.get_data() assert b"POST" in response.get_data()
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
@pytest.mark.parametrize("path", ["/error", "/bp/error"]) @pytest.mark.parametrize("path", ["/error", "/bp/error"])
def test_async_error_handler(path, async_app): def test_async_error_handler(path, async_app):
test_client = async_app.test_client() test_client = async_app.test_client()
@ -74,7 +94,6 @@ def test_async_error_handler(path, async_app):
assert response.status_code == 412 assert response.status_code == 412
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
def test_async_before_after_request(): def test_async_before_after_request():
app_first_called = False app_first_called = False
app_before_called = False app_before_called = False
@ -131,10 +150,3 @@ def test_async_before_after_request():
test_client.get("/bp/") test_client.get("/bp/")
assert bp_before_called assert bp_before_called
assert bp_after_called assert bp_after_called
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7")
def test_async_runtime_error():
app = Flask(__name__)
with pytest.raises(RuntimeError):
app.async_to_sync(None)

View file

@ -1,6 +1,5 @@
import gc import gc
import re import re
import sys
import time import time
import uuid import uuid
import weakref import weakref
@ -1323,7 +1322,6 @@ def test_jsonify_mimetype(app, req_ctx):
assert rv.mimetype == "application/vnd.api+json" assert rv.mimetype == "application/vnd.api+json"
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
def test_json_dump_dataclass(app, req_ctx): def test_json_dump_dataclass(app, req_ctx):
from dataclasses import make_dataclass from dataclasses import make_dataclass

View file

@ -837,6 +837,86 @@ def test_nested_blueprint(app, client):
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no" assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
def test_nested_callback_order(app, client):
parent = flask.Blueprint("parent", __name__)
child = flask.Blueprint("child", __name__)
@app.before_request
def app_before1():
flask.g.setdefault("seen", []).append("app_1")
@app.teardown_request
def app_teardown1(e=None):
assert flask.g.seen.pop() == "app_1"
@app.before_request
def app_before2():
flask.g.setdefault("seen", []).append("app_2")
@app.teardown_request
def app_teardown2(e=None):
assert flask.g.seen.pop() == "app_2"
@app.context_processor
def app_ctx():
return dict(key="app")
@parent.before_request
def parent_before1():
flask.g.setdefault("seen", []).append("parent_1")
@parent.teardown_request
def parent_teardown1(e=None):
assert flask.g.seen.pop() == "parent_1"
@parent.before_request
def parent_before2():
flask.g.setdefault("seen", []).append("parent_2")
@parent.teardown_request
def parent_teardown2(e=None):
assert flask.g.seen.pop() == "parent_2"
@parent.context_processor
def parent_ctx():
return dict(key="parent")
@child.before_request
def child_before1():
flask.g.setdefault("seen", []).append("child_1")
@child.teardown_request
def child_teardown1(e=None):
assert flask.g.seen.pop() == "child_1"
@child.before_request
def child_before2():
flask.g.setdefault("seen", []).append("child_2")
@child.teardown_request
def child_teardown2(e=None):
assert flask.g.seen.pop() == "child_2"
@child.context_processor
def child_ctx():
return dict(key="child")
@child.route("/a")
def a():
return ", ".join(flask.g.seen)
@child.route("/b")
def b():
return flask.render_template_string("{{ key }}")
parent.register_blueprint(child)
app.register_blueprint(parent)
assert (
client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
)
assert client.get("/b").data == b"child"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"parent_init, child_init, parent_registration, child_registration", "parent_init, child_init, parent_registration, child_registration",
[ [
@ -874,8 +954,8 @@ def test_unique_blueprint_names(app, client) -> None:
app.register_blueprint(bp) app.register_blueprint(bp)
with pytest.warns(UserWarning): with pytest.raises(ValueError):
app.register_blueprint(bp) # same bp, same name, warning app.register_blueprint(bp) # same bp, same name, error
app.register_blueprint(bp, name="again") # same bp, different name, ok app.register_blueprint(bp, name="again") # same bp, different name, ok
@ -899,6 +979,14 @@ def test_blueprint_renaming(app, client) -> None:
def index(): def index():
return flask.request.endpoint return flask.request.endpoint
@bp.get("/error")
def error():
flask.abort(403)
@bp.errorhandler(403)
def forbidden(_: Exception):
return "Error", 403
@bp2.get("/") @bp2.get("/")
def index2(): def index2():
return flask.request.endpoint return flask.request.endpoint
@ -911,3 +999,5 @@ def test_blueprint_renaming(app, client) -> None:
assert client.get("/b/").data == b"alt.index" assert client.get("/b/").data == b"alt.index"
assert client.get("/a/a/").data == b"bp.sub.index2" assert client.get("/a/a/").data == b"bp.sub.index2"
assert client.get("/b/a/").data == b"alt.sub.index2" assert client.get("/b/a/").data == b"alt.sub.index2"
assert client.get("/a/error").data == b"Error"
assert client.get("/b/error").data == b"Error"

View file

@ -1,6 +1,7 @@
# This file was part of Flask-CLI and was modified under the terms of # This file was part of Flask-CLI and was modified under the terms of
# its Revised BSD License. Copyright © 2015 CERN. # its Revised BSD License. Copyright © 2015 CERN.
import os import os
import platform
import ssl import ssl
import sys import sys
import types import types
@ -17,6 +18,7 @@ from flask import Blueprint
from flask import current_app from flask import current_app
from flask import Flask from flask import Flask
from flask.cli import AppGroup from flask.cli import AppGroup
from flask.cli import DispatchingApp
from flask.cli import dotenv from flask.cli import dotenv
from flask.cli import find_best_app from flask.cli import find_best_app
from flask.cli import FlaskGroup from flask.cli import FlaskGroup
@ -47,51 +49,36 @@ def test_cli_name(test_apps):
def test_find_best_app(test_apps): def test_find_best_app(test_apps):
script_info = ScriptInfo()
class Module: class Module:
app = Flask("appname") app = Flask("appname")
assert find_best_app(script_info, Module) == Module.app assert find_best_app(Module) == Module.app
class Module: class Module:
application = Flask("appname") application = Flask("appname")
assert find_best_app(script_info, Module) == Module.application assert find_best_app(Module) == Module.application
class Module: class Module:
myapp = Flask("appname") myapp = Flask("appname")
assert find_best_app(script_info, Module) == Module.myapp assert find_best_app(Module) == Module.myapp
class Module: class Module:
@staticmethod @staticmethod
def create_app(): def create_app():
return Flask("appname") return Flask("appname")
app = find_best_app(script_info, Module) app = find_best_app(Module)
assert isinstance(app, Flask) assert isinstance(app, Flask)
assert app.name == "appname" assert app.name == "appname"
class Module: class Module:
@staticmethod @staticmethod
def create_app(foo): def create_app(**kwargs):
return Flask("appname") return Flask("appname")
with pytest.deprecated_call(match="Script info"): app = find_best_app(Module)
app = find_best_app(script_info, Module)
assert isinstance(app, Flask)
assert app.name == "appname"
class Module:
@staticmethod
def create_app(foo=None, script_info=None):
return Flask("appname")
with pytest.deprecated_call(match="script_info"):
app = find_best_app(script_info, Module)
assert isinstance(app, Flask) assert isinstance(app, Flask)
assert app.name == "appname" assert app.name == "appname"
@ -100,7 +87,7 @@ def test_find_best_app(test_apps):
def make_app(): def make_app():
return Flask("appname") return Flask("appname")
app = find_best_app(script_info, Module) app = find_best_app(Module)
assert isinstance(app, Flask) assert isinstance(app, Flask)
assert app.name == "appname" assert app.name == "appname"
@ -111,7 +98,7 @@ def test_find_best_app(test_apps):
def create_app(): def create_app():
return Flask("appname2") return Flask("appname2")
assert find_best_app(script_info, Module) == Module.myapp assert find_best_app(Module) == Module.myapp
class Module: class Module:
myapp = Flask("appname1") myapp = Flask("appname1")
@ -120,32 +107,32 @@ def test_find_best_app(test_apps):
def create_app(): def create_app():
return Flask("appname2") return Flask("appname2")
assert find_best_app(script_info, Module) == Module.myapp assert find_best_app(Module) == Module.myapp
class Module: class Module:
pass pass
pytest.raises(NoAppException, find_best_app, script_info, Module) pytest.raises(NoAppException, find_best_app, Module)
class Module: class Module:
myapp1 = Flask("appname1") myapp1 = Flask("appname1")
myapp2 = Flask("appname2") myapp2 = Flask("appname2")
pytest.raises(NoAppException, find_best_app, script_info, Module) pytest.raises(NoAppException, find_best_app, Module)
class Module: class Module:
@staticmethod @staticmethod
def create_app(foo, bar): def create_app(foo, bar):
return Flask("appname2") return Flask("appname2")
pytest.raises(NoAppException, find_best_app, script_info, Module) pytest.raises(NoAppException, find_best_app, Module)
class Module: class Module:
@staticmethod @staticmethod
def create_app(): def create_app():
raise TypeError("bad bad factory!") raise TypeError("bad bad factory!")
pytest.raises(TypeError, find_best_app, script_info, Module) pytest.raises(TypeError, find_best_app, Module)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -209,8 +196,7 @@ def test_prepare_import(request, value, path, result):
), ),
) )
def test_locate_app(test_apps, iname, aname, result): def test_locate_app(test_apps, iname, aname, result):
info = ScriptInfo() assert locate_app(iname, aname).name == result
assert locate_app(info, iname, aname).name == result
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -232,20 +218,17 @@ def test_locate_app(test_apps, iname, aname, result):
), ),
) )
def test_locate_app_raises(test_apps, iname, aname): def test_locate_app_raises(test_apps, iname, aname):
info = ScriptInfo()
with pytest.raises(NoAppException): with pytest.raises(NoAppException):
locate_app(info, iname, aname) locate_app(iname, aname)
def test_locate_app_suppress_raise(test_apps): def test_locate_app_suppress_raise(test_apps):
info = ScriptInfo() app = locate_app("notanapp.py", None, raise_if_not_found=False)
app = locate_app(info, "notanapp.py", None, raise_if_not_found=False)
assert app is None assert app is None
# only direct import error is suppressed # only direct import error is suppressed
with pytest.raises(NoAppException): with pytest.raises(NoAppException):
locate_app(info, "cliapp.importerrorapp", None, raise_if_not_found=False) locate_app("cliapp.importerrorapp", None, raise_if_not_found=False)
def test_get_version(test_apps, capsys): def test_get_version(test_apps, capsys):
@ -310,6 +293,26 @@ def test_scriptinfo(test_apps, monkeypatch):
assert app.name == "testapp" assert app.name == "testapp"
@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="flaky on pypy")
def test_lazy_load_error(monkeypatch):
"""When using lazy loading, the correct exception should be
re-raised.
"""
class BadExc(Exception):
pass
def bad_load():
raise BadExc
lazy = DispatchingApp(bad_load, use_eager_loading=False)
with pytest.raises(BadExc):
# reduce flakiness by waiting for the internal loading lock
with lazy._lock:
lazy._flush_bg_loading_exception()
def test_with_appcontext(runner): def test_with_appcontext(runner):
@click.command() @click.command()
@with_appcontext @with_appcontext

View file

@ -1,4 +1,5 @@
import datetime import datetime
import decimal
import io import io
import uuid import uuid
@ -187,6 +188,11 @@ def test_jsonify_uuid_types(app, client):
assert rv_uuid == test_uuid assert rv_uuid == test_uuid
def test_json_decimal():
rv = flask.json.dumps(decimal.Decimal("0.003"))
assert rv == '"0.003"'
def test_json_attr(app, client): def test_json_attr(app, client):
@app.route("/add", methods=["POST"]) @app.route("/add", methods=["POST"])
def add(): def add():

View file

@ -1,7 +1,8 @@
[tox] [tox]
envlist = envlist =
py{39,38,37,36,py3} py3{11,10,9,8,7},pypy3{8,7}
py39-click7 py310-min
py37-dev
style style
typing typing
docs docs
@ -10,8 +11,8 @@ skip_missing_interpreters = true
[testenv] [testenv]
deps = deps =
-r requirements/tests.txt -r requirements/tests.txt
min: -r requirements/tests-pallets-min.txt
click7: click<8 dev: -r requirements/tests-pallets-dev.txt
examples/tutorial[test] examples/tutorial[test]
examples/javascript[test] examples/javascript[test]