Merge branch 'main' into docs-javascript
This commit is contained in:
commit
a9b370d9d7
49 changed files with 1360 additions and 924 deletions
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
- {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev}
|
- {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev}
|
||||||
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v3
|
- uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,7 +11,6 @@ dist/
|
||||||
build/
|
build/
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
_mailinglist
|
|
||||||
.tox/
|
.tox/
|
||||||
.cache/
|
.cache/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ ci:
|
||||||
autoupdate_schedule: monthly
|
autoupdate_schedule: monthly
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.32.0
|
rev: v2.32.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: ["--py36-plus"]
|
args: ["--py36-plus"]
|
||||||
|
|
|
||||||
41
CHANGES.rst
41
CHANGES.rst
|
|
@ -1,5 +1,43 @@
|
||||||
.. currentmodule:: flask
|
.. currentmodule:: flask
|
||||||
|
|
||||||
|
Version 2.2.0
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
|
||||||
|
- Add new customization points to the ``Flask`` app object for many
|
||||||
|
previously global behaviors.
|
||||||
|
|
||||||
|
- ``flask.url_for`` will call ``app.url_for``. :issue:`4568`
|
||||||
|
- ``flask.abort`` will call ``app.aborter``.
|
||||||
|
``Flask.aborter_class`` and ``Flask.make_aborter`` can be used
|
||||||
|
to customize this aborter. :issue:`4567`
|
||||||
|
- ``flask.redirect`` will call ``app.redirect``. :issue:`4569`
|
||||||
|
|
||||||
|
- Refactor ``register_error_handler`` to consolidate error checking.
|
||||||
|
Rewrite some error messages to be more consistent. :issue:`4559`
|
||||||
|
- Use Blueprint decorators and functions intended for setup after
|
||||||
|
registering the blueprint will show a warning. In the next version,
|
||||||
|
this will become an error just like the application setup methods.
|
||||||
|
:issue:`4571`
|
||||||
|
- ``before_first_request`` is deprecated. Run setup code when creating
|
||||||
|
the application instead. :issue:`4605`
|
||||||
|
- Added the ``View.init_every_request`` class attribute. If a view
|
||||||
|
subclass sets this to ``False``, the view will not create a new
|
||||||
|
instance on every request. :issue:`2520`.
|
||||||
|
|
||||||
|
Version 2.1.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
|
||||||
|
- Inline some optional imports that are only used for certain CLI
|
||||||
|
commands. :pr:`4606`
|
||||||
|
- Relax type annotation for ``after_request`` functions. :issue:`4600`
|
||||||
|
- ``instance_path`` for namespace packages uses the path closest to
|
||||||
|
the imported submodule. :issue:`4600`
|
||||||
|
|
||||||
|
|
||||||
Version 2.1.2
|
Version 2.1.2
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
@ -998,8 +1036,7 @@ Released 2011-09-29, codename Rakija
|
||||||
earlier feedback when users forget to import view code ahead of
|
earlier feedback when users forget to import view code ahead of
|
||||||
time.
|
time.
|
||||||
- Added the ability to register callbacks that are only triggered once
|
- Added the ability to register callbacks that are only triggered once
|
||||||
at the beginning of the first request.
|
at the beginning of the first request. (``before_first_request``)
|
||||||
(:meth:`Flask.before_first_request`)
|
|
||||||
- Malformed JSON data will now trigger a bad request HTTP exception
|
- Malformed JSON data will now trigger a bad request HTTP exception
|
||||||
instead of a value error which usually would result in a 500
|
instead of a value error which usually would result in a 500
|
||||||
internal server error if not handled. This is a backwards
|
internal server error if not handled. This is a backwards
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@ own code:
|
||||||
|
|
||||||
- The ``#questions`` 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
|
|
||||||
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`_.
|
- Ask on our `GitHub Discussions`_ for long term discussion or larger
|
||||||
|
questions.
|
||||||
|
|
||||||
.. _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
|
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
|
||||||
|
|
@ -98,21 +97,20 @@ First time setup
|
||||||
|
|
||||||
- Create a virtualenv.
|
- Create a virtualenv.
|
||||||
|
|
||||||
.. tabs::
|
|
||||||
|
|
||||||
.. group-tab:: Linux/macOS
|
- Linux/macOS
|
||||||
|
|
||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
$ python3 -m venv env
|
$ python3 -m venv env
|
||||||
$ . env/bin/activate
|
$ . env/bin/activate
|
||||||
|
|
||||||
.. group-tab:: Windows
|
- Windows
|
||||||
|
|
||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
> py -3 -m venv env
|
> py -3 -m venv env
|
||||||
> env\Scripts\activate
|
> env\Scripts\activate
|
||||||
|
|
||||||
- Upgrade pip and setuptools.
|
- Upgrade pip and setuptools.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
Becoming Big
|
|
||||||
============
|
|
||||||
|
|
||||||
Here are your options when growing your codebase or scaling your application.
|
|
||||||
|
|
||||||
Read the Source.
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Flask started in part to demonstrate how to build your own framework on top of
|
|
||||||
existing well-used tools Werkzeug (WSGI) and Jinja (templating), and as it
|
|
||||||
developed, it became useful to a wide audience. As you grow your codebase,
|
|
||||||
don't just use Flask -- understand it. Read the source. Flask's code is
|
|
||||||
written to be read; its documentation is published so you can use its internal
|
|
||||||
APIs. Flask sticks to documented APIs in upstream libraries, and documents its
|
|
||||||
internal utilities so that you can find the hook points needed for your
|
|
||||||
project.
|
|
||||||
|
|
||||||
Hook. Extend.
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The :doc:`/api` docs are full of available overrides, hook points, and
|
|
||||||
:doc:`/signals`. You can provide custom classes for things like the
|
|
||||||
request and response objects. Dig deeper on the APIs you use, and look
|
|
||||||
for the customizations which are available out of the box in a Flask
|
|
||||||
release. Look for ways in which your project can be refactored into a
|
|
||||||
collection of utilities and Flask extensions. Explore the many
|
|
||||||
:doc:`/extensions` in the community, and look for patterns to build your
|
|
||||||
own extensions if you do not find the tools you need.
|
|
||||||
|
|
||||||
Subclass.
|
|
||||||
---------
|
|
||||||
|
|
||||||
The :class:`~flask.Flask` class has many methods designed for subclassing. You
|
|
||||||
can quickly add or customize behavior by subclassing :class:`~flask.Flask` (see
|
|
||||||
the linked method docs) and using that subclass wherever you instantiate an
|
|
||||||
application class. This works well with :doc:`/patterns/appfactories`.
|
|
||||||
See :doc:`/patterns/subclassing` for an example.
|
|
||||||
|
|
||||||
Wrap with middleware.
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
The :doc:`/patterns/appdispatch` pattern shows in detail how to apply middleware. You
|
|
||||||
can introduce WSGI middleware to wrap your Flask instances and introduce fixes
|
|
||||||
and changes at the layer between your Flask application and your HTTP
|
|
||||||
server. Werkzeug includes several `middlewares
|
|
||||||
<https://werkzeug.palletsprojects.com/middleware/>`_.
|
|
||||||
|
|
||||||
Fork.
|
|
||||||
-----
|
|
||||||
|
|
||||||
If none of the above options work, fork Flask. The majority of code of Flask
|
|
||||||
is within Werkzeug and Jinja2. These libraries do the majority of the work.
|
|
||||||
Flask is just the paste that glues those together. For every project there is
|
|
||||||
the point where the underlying framework gets in the way (due to assumptions
|
|
||||||
the original developers had). This is natural because if this would not be the
|
|
||||||
case, the framework would be a very complex system to begin with which causes a
|
|
||||||
steep learning curve and a lot of user frustration.
|
|
||||||
|
|
||||||
This is not unique to Flask. Many people use patched and modified
|
|
||||||
versions of their framework to counter shortcomings. This idea is also
|
|
||||||
reflected in the license of Flask. You don't have to contribute any
|
|
||||||
changes back if you decide to modify the framework.
|
|
||||||
|
|
||||||
The downside of forking is of course that Flask extensions will most
|
|
||||||
likely break because the new framework has a different import name.
|
|
||||||
Furthermore integrating upstream changes can be a complex process,
|
|
||||||
depending on the number of changes. Because of that, forking should be
|
|
||||||
the very last resort.
|
|
||||||
|
|
||||||
Scale like a pro.
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
For many web applications the complexity of the code is less an issue than
|
|
||||||
the scaling for the number of users or data entries expected. Flask by
|
|
||||||
itself is only limited in terms of scaling by your application code, the
|
|
||||||
data store you want to use and the Python implementation and webserver you
|
|
||||||
are running on.
|
|
||||||
|
|
||||||
Scaling well means for example that if you double the amount of servers
|
|
||||||
you get about twice the performance. Scaling bad means that if you add a
|
|
||||||
new server the application won't perform any better or would not even
|
|
||||||
support a second server.
|
|
||||||
|
|
||||||
There is only one limiting factor regarding scaling in Flask which are
|
|
||||||
the context local proxies. They depend on context which in Flask is
|
|
||||||
defined as being either a thread, process or greenlet. If your server
|
|
||||||
uses some kind of concurrency that is not based on threads or greenlets,
|
|
||||||
Flask will no longer be able to support these global proxies. However the
|
|
||||||
majority of servers are using either threads, greenlets or separate
|
|
||||||
processes to achieve concurrency which are all methods well supported by
|
|
||||||
the underlying Werkzeug library.
|
|
||||||
|
|
||||||
Discuss with the community.
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
The Flask developers keep the framework accessible to users with codebases big
|
|
||||||
and small. If you find an obstacle in your way, caused by Flask, don't hesitate
|
|
||||||
to contact the developers on the mailing list or Discord server. The best way for
|
|
||||||
the Flask and Flask extension developers to improve the tools for larger
|
|
||||||
applications is getting feedback from users.
|
|
||||||
|
|
@ -299,9 +299,7 @@ used for public variables, such as ``FLASK_APP``, while :file:`.env` should not
|
||||||
be committed to your repository so that it can set private variables.
|
be committed to your repository so that it can set private variables.
|
||||||
|
|
||||||
Directories are scanned upwards from the directory you call ``flask``
|
Directories are scanned upwards from the directory you call ``flask``
|
||||||
from to locate the files. The current working directory will be set to the
|
from to locate the files.
|
||||||
location of the file, with the assumption that that is the top level project
|
|
||||||
directory.
|
|
||||||
|
|
||||||
The files are only loaded by the ``flask`` command or calling
|
The files are only loaded by the ``flask`` command or calling
|
||||||
:meth:`~Flask.run`. If you would like to load these files when running in
|
:meth:`~Flask.run`. If you would like to load these files when running in
|
||||||
|
|
|
||||||
|
|
@ -167,9 +167,6 @@ large applications harder to maintain. However Flask is just not designed
|
||||||
for large applications or asynchronous servers. Flask wants to make it
|
for large applications or asynchronous servers. Flask wants to make it
|
||||||
quick and easy to write a traditional web application.
|
quick and easy to write a traditional web application.
|
||||||
|
|
||||||
Also see the :doc:`/becomingbig` section of the documentation for some
|
|
||||||
inspiration for larger applications based on Flask.
|
|
||||||
|
|
||||||
|
|
||||||
Async/await and ASGI support
|
Async/await and ASGI support
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
|
||||||
|
|
@ -271,16 +271,16 @@ Learn from Others
|
||||||
|
|
||||||
This documentation only touches the bare minimum for extension development.
|
This documentation only touches the bare minimum for extension development.
|
||||||
If you want to learn more, it's a very good idea to check out existing extensions
|
If you want to learn more, it's a very good idea to check out existing extensions
|
||||||
on the `PyPI`_. If you feel lost there is still the `mailinglist`_ and the
|
on `PyPI`_. If you feel lost there is `Discord Chat`_ or
|
||||||
`Discord server`_ to get some ideas for nice looking APIs. Especially if you do
|
`GitHub Discussions`_ to get some ideas for nice looking APIs. Especially if you do
|
||||||
something nobody before you did, it might be a very good idea to get some more
|
something nobody before you did, it might be a very good idea to get some more
|
||||||
input. This not only generates useful feedback on what people might want from
|
input. This not only generates useful feedback on what people might want from
|
||||||
an extension, but also avoids having multiple developers working in isolation
|
an extension, but also avoids having multiple developers working in isolation
|
||||||
on pretty much the same problem.
|
on pretty much the same problem.
|
||||||
|
|
||||||
Remember: good API design is hard, so introduce your project on the
|
Remember: good API design is hard, so introduce your project on
|
||||||
mailing list, and let other developers give you a helping hand with
|
`Discord Chat`_ or `GitHub Discussions`_, and let other developers give
|
||||||
designing the API.
|
you a helping hand with designing the API.
|
||||||
|
|
||||||
The best Flask extensions are extensions that share common idioms for the
|
The best Flask extensions are extensions that share common idioms for the
|
||||||
API. And this can only work if collaboration happens early.
|
API. And this can only work if collaboration happens early.
|
||||||
|
|
@ -327,6 +327,6 @@ ecosystem remain consistent and compatible.
|
||||||
indicate 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
|
.. _Discord Chat: https://discord.gg/pallets
|
||||||
.. _Discord server: https://discord.gg/pallets
|
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
|
||||||
.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/
|
.. _Official Pallets Themes: https://pypi.org/project/Pallets-Sphinx-Themes/
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ SQLAlchemy or another database tool, introduce non-relational data persistence
|
||||||
as appropriate, and take advantage of framework-agnostic tools built for WSGI,
|
as appropriate, and take advantage of framework-agnostic tools built for WSGI,
|
||||||
the Python web interface.
|
the Python web interface.
|
||||||
|
|
||||||
Flask includes many hooks to customize its behavior. Should you need more
|
Flask includes many hooks to customize its behavior. Should you need
|
||||||
customization, the Flask class is built for subclassing. If you are interested
|
more customization, the Flask class is built for subclassing. If you are
|
||||||
in that, check out the :doc:`becomingbig` chapter. If you are curious about
|
curious about the Flask design principles, head over to the section
|
||||||
the Flask design principles, head over to the section about :doc:`design`.
|
about :doc:`design`.
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ instructions for web development with Flask.
|
||||||
shell
|
shell
|
||||||
patterns/index
|
patterns/index
|
||||||
deploying/index
|
deploying/index
|
||||||
becomingbig
|
|
||||||
async-await
|
async-await
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,15 @@ Celery without any reconfiguration with Flask, it becomes a bit nicer by
|
||||||
subclassing tasks and adding support for Flask's application contexts and
|
subclassing tasks and adding support for Flask's application contexts and
|
||||||
hooking it up with the Flask configuration.
|
hooking it up with the Flask configuration.
|
||||||
|
|
||||||
This is all that is necessary to properly integrate Celery with Flask::
|
This is all that is necessary to integrate Celery with Flask:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
|
||||||
def make_celery(app):
|
def make_celery(app):
|
||||||
celery = Celery(
|
celery = Celery(app.import_name)
|
||||||
app.import_name,
|
celery.conf.update(app.config["CELERY_CONFIG"])
|
||||||
backend=app.config['CELERY_RESULT_BACKEND'],
|
|
||||||
broker=app.config['CELERY_BROKER_URL']
|
|
||||||
)
|
|
||||||
celery.conf.update(app.config)
|
|
||||||
|
|
||||||
class ContextTask(celery.Task):
|
class ContextTask(celery.Task):
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
|
|
@ -59,6 +57,12 @@ from the application config, updates the rest of the Celery config from
|
||||||
the Flask config and then creates a subclass of the task that wraps the
|
the Flask config and then creates a subclass of the task that wraps the
|
||||||
task execution in an application context.
|
task execution in an application context.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
Celery 5.x deprecated uppercase configuration keys, and 6.x will
|
||||||
|
remove them. See their official `migration guide`_.
|
||||||
|
|
||||||
|
.. _migration guide: https://docs.celeryproject.org/en/stable/userguide/configuration.html#conf-old-settings-map.
|
||||||
|
|
||||||
An example task
|
An example task
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
@ -69,10 +73,10 @@ application using the factory from above, and then use it to define the task. ::
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
flask_app = Flask(__name__)
|
flask_app = Flask(__name__)
|
||||||
flask_app.config.update(
|
flask_app.config.update(CELERY_CONFIG={
|
||||||
CELERY_BROKER_URL='redis://localhost:6379',
|
'broker_url': 'redis://localhost:6379',
|
||||||
CELERY_RESULT_BACKEND='redis://localhost:6379'
|
'result_backend': 'redis://localhost:6379',
|
||||||
)
|
})
|
||||||
celery = make_celery(flask_app)
|
celery = make_celery(flask_app)
|
||||||
|
|
||||||
@celery.task()
|
@celery.task()
|
||||||
|
|
|
||||||
|
|
@ -173,10 +173,6 @@ You should then end up with something like that::
|
||||||
ensuring the module is imported and we are doing that at the bottom of
|
ensuring the module is imported and we are doing that at the bottom of
|
||||||
the file.
|
the file.
|
||||||
|
|
||||||
There are still some problems with that approach but if you want to use
|
|
||||||
decorators there is no way around that. Check out the
|
|
||||||
:doc:`/becomingbig` section for some inspiration how to deal with that.
|
|
||||||
|
|
||||||
|
|
||||||
Working with Blueprints
|
Working with Blueprints
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ The ``client`` has methods that match the common HTTP request methods,
|
||||||
such as ``client.get()`` and ``client.post()``. They take many arguments
|
such as ``client.get()`` and ``client.post()``. They take many arguments
|
||||||
for building the request; you can find the full documentation in
|
for building the request; you can find the full documentation in
|
||||||
:class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``,
|
:class:`~werkzeug.test.EnvironBuilder`. Typically you'll use ``path``,
|
||||||
``query``, ``headers``, and ``data`` or ``json``.
|
``query_string``, ``headers``, and ``data`` or ``json``.
|
||||||
|
|
||||||
To make a request, call the method the request should use with the path
|
To make a request, call the method the request should use with the path
|
||||||
to the route to test. A :class:`~werkzeug.test.TestResponse` is returned
|
to the route to test. A :class:`~werkzeug.test.TestResponse` is returned
|
||||||
|
|
@ -108,9 +108,9 @@ provides ``response.text``, or use ``response.get_data(as_text=True)``.
|
||||||
assert b"<h2>Hello, World!</h2>" in response.data
|
assert b"<h2>Hello, World!</h2>" in response.data
|
||||||
|
|
||||||
|
|
||||||
Pass a dict ``query={"key": "value", ...}`` to set arguments in the
|
Pass a dict ``query_string={"key": "value", ...}`` to set arguments in
|
||||||
query string (after the ``?`` in the URL). Pass a dict ``headers={}``
|
the query string (after the ``?`` in the URL). Pass a dict
|
||||||
to set request headers.
|
``headers={}`` to set request headers.
|
||||||
|
|
||||||
To send a request body in a POST or PUT request, pass a value to
|
To send a request body in a POST or PUT request, pass a value to
|
||||||
``data``. If raw bytes are passed, that exact body is used. Usually,
|
``data``. If raw bytes are passed, that exact body is used. Usually,
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,12 @@ This tells Python to copy everything in the ``static`` and ``templates``
|
||||||
directories, and the ``schema.sql`` file, but to exclude all bytecode
|
directories, and the ``schema.sql`` file, but to exclude all bytecode
|
||||||
files.
|
files.
|
||||||
|
|
||||||
See the `official packaging guide`_ for another explanation of the files
|
See the official `Packaging tutorial <packaging tutorial_>`_ and
|
||||||
|
`detailed guide <packaging guide_>`_ for more explanation of the files
|
||||||
and options used.
|
and options used.
|
||||||
|
|
||||||
.. _official packaging guide: https://packaging.python.org/tutorials/packaging-projects/
|
.. _packaging tutorial: https://packaging.python.org/tutorials/packaging-projects/
|
||||||
|
.. _packaging guide: https://packaging.python.org/guides/distributing-packages-using-setuptools/
|
||||||
|
|
||||||
|
|
||||||
Install the Project
|
Install the Project
|
||||||
|
|
|
||||||
444
docs/views.rst
444
docs/views.rst
|
|
@ -1,235 +1,319 @@
|
||||||
Pluggable Views
|
Class-based Views
|
||||||
===============
|
=================
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
.. currentmodule:: flask.views
|
||||||
|
|
||||||
Flask 0.7 introduces pluggable views inspired by the generic views from
|
This page introduces using the :class:`View` and :class:`MethodView`
|
||||||
Django which are based on classes instead of functions. The main
|
classes to write class-based views.
|
||||||
intention is that you can replace parts of the implementations and this
|
|
||||||
way have customizable pluggable views.
|
|
||||||
|
|
||||||
Basic Principle
|
A class-based view is a class that acts as a view function. Because it
|
||||||
---------------
|
is a class, different instances of the class can be created with
|
||||||
|
different arguments, to change the behavior of the view. This is also
|
||||||
|
known as generic, reusable, or pluggable views.
|
||||||
|
|
||||||
Consider you have a function that loads a list of objects from the
|
An example of where this is useful is defining a class that creates an
|
||||||
database and renders into a template::
|
API based on the database model it is initialized with.
|
||||||
|
|
||||||
@app.route('/users/')
|
For more complex API behavior and customization, look into the various
|
||||||
def show_users(page):
|
API extensions for Flask.
|
||||||
|
|
||||||
|
|
||||||
|
Basic Reusable View
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Let's walk through an example converting a view function to a view
|
||||||
|
class. We start with a view function that queries a list of users then
|
||||||
|
renders a template to show the list.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/users/")
|
||||||
|
def user_list():
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
return render_template('users.html', users=users)
|
return render_template("users.html", users=users)
|
||||||
|
|
||||||
This is simple and flexible, but if you want to provide this view in a
|
This works for the user model, but let's say you also had more models
|
||||||
generic fashion that can be adapted to other models and templates as well
|
that needed list pages. You'd need to write another view function for
|
||||||
you might want more flexibility. This is where pluggable class-based
|
each model, even though the only thing that would change is the model
|
||||||
views come into place. As the first step to convert this into a class
|
and template name.
|
||||||
based view you would do this::
|
|
||||||
|
|
||||||
|
Instead, you can write a :class:`View` subclass that will query a model
|
||||||
|
and render a template. As the first step, we'll convert the view to a
|
||||||
|
class without any customization.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from flask.views import View
|
from flask.views import View
|
||||||
|
|
||||||
class ShowUsers(View):
|
class UserList(View):
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
return render_template('users.html', objects=users)
|
return render_template("users.html", objects=users)
|
||||||
|
|
||||||
app.add_url_rule('/users/', view_func=ShowUsers.as_view('show_users'))
|
app.add_url_rule("/users/", view_func=UserList.as_view("user_list"))
|
||||||
|
|
||||||
As you can see what you have to do is to create a subclass of
|
The :meth:`View.dispatch_request` method is the equivalent of the view
|
||||||
:class:`flask.views.View` and implement
|
function. Calling :meth:`View.as_view` method will create a view
|
||||||
:meth:`~flask.views.View.dispatch_request`. Then we have to convert that
|
function that can be registered on the app with its
|
||||||
class into an actual view function by using the
|
:meth:`~flask.Flask.add_url_rule` method. The first argument to
|
||||||
:meth:`~flask.views.View.as_view` class method. The string you pass to
|
``as_view`` is the name to use to refer to the view with
|
||||||
that function is the name of the endpoint that view will then have. But
|
:func:`~flask.url_for`.
|
||||||
this by itself is not helpful, so let's refactor the code a bit::
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
from flask.views import View
|
You can't decorate the class with ``@app.route()`` the way you'd
|
||||||
|
do with a basic view function.
|
||||||
|
|
||||||
|
Next, we need to be able to register the same view class for different
|
||||||
|
models and templates, to make it more useful than the original function.
|
||||||
|
The class will take two arguments, the model and template, and store
|
||||||
|
them on ``self``. Then ``dispatch_request`` can reference these instead
|
||||||
|
of hard-coded values.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
class ListView(View):
|
class ListView(View):
|
||||||
|
def __init__(self, model, template):
|
||||||
def get_template_name(self):
|
self.model = model
|
||||||
raise NotImplementedError()
|
self.template = template
|
||||||
|
|
||||||
def render_template(self, context):
|
|
||||||
return render_template(self.get_template_name(), **context)
|
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
context = {'objects': self.get_objects()}
|
items = self.model.query.all()
|
||||||
return self.render_template(context)
|
return render_template(self.template, items=items)
|
||||||
|
|
||||||
class UserView(ListView):
|
Remember, we create the view function with ``View.as_view()`` instead of
|
||||||
|
creating the class directly. Any extra arguments passed to ``as_view``
|
||||||
|
are then passed when creating the class. Now we can register the same
|
||||||
|
view to handle multiple models.
|
||||||
|
|
||||||
def get_template_name(self):
|
.. code-block:: python
|
||||||
return 'users.html'
|
|
||||||
|
|
||||||
def get_objects(self):
|
app.add_url_rule(
|
||||||
return User.query.all()
|
"/users/",
|
||||||
|
view_func=ListView.as_view("user_list", User, "users.html"),
|
||||||
|
)
|
||||||
|
app.add_url_rule(
|
||||||
|
"/stories/",
|
||||||
|
view_func=ListView.as_view("story_list", Story, "stories.html"),
|
||||||
|
)
|
||||||
|
|
||||||
This of course is not that helpful for such a small example, but it's good
|
|
||||||
enough to explain the basic principle. When you have a class-based view
|
|
||||||
the question comes up what ``self`` points to. The way this works is that
|
|
||||||
whenever the request is dispatched a new instance of the class is created
|
|
||||||
and the :meth:`~flask.views.View.dispatch_request` method is called with
|
|
||||||
the parameters from the URL rule. The class itself is instantiated with
|
|
||||||
the parameters passed to the :meth:`~flask.views.View.as_view` function.
|
|
||||||
For instance you can write a class like this::
|
|
||||||
|
|
||||||
class RenderTemplateView(View):
|
URL Variables
|
||||||
def __init__(self, template_name):
|
-------------
|
||||||
self.template_name = template_name
|
|
||||||
|
Any variables captured by the URL are passed as keyword arguments to the
|
||||||
|
``dispatch_request`` method, as they would be for a regular view
|
||||||
|
function.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class DetailView(View):
|
||||||
|
def __init__(self, model):
|
||||||
|
self.model = model
|
||||||
|
self.template = f"{model.__name__.lower()}/detail.html"
|
||||||
|
|
||||||
|
def dispatch_request(self, id)
|
||||||
|
item = self.model.query.get_or_404(id)
|
||||||
|
return render_template(self.template, item=item)
|
||||||
|
|
||||||
|
app.add_url_rule("/users/<int:id>", view_func=DetailView.as_view("user_detail"))
|
||||||
|
|
||||||
|
|
||||||
|
View Lifetime and ``self``
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
By default, a new instance of the view class is created every time a
|
||||||
|
request is handled. This means that it is safe to write other data to
|
||||||
|
``self`` during the request, since the next request will not see it,
|
||||||
|
unlike other forms of global state.
|
||||||
|
|
||||||
|
However, if your view class needs to do a lot of complex initialization,
|
||||||
|
doing it for every request is unnecessary and can be inefficient. To
|
||||||
|
avoid this, set :attr:`View.init_every_request` to ``False``, which will
|
||||||
|
only create one instance of the class and use it for every request. In
|
||||||
|
this case, writing to ``self`` is not safe. If you need to store data
|
||||||
|
during the request, use :data:`~flask.g` instead.
|
||||||
|
|
||||||
|
In the ``ListView`` example, nothing writes to ``self`` during the
|
||||||
|
request, so it is more efficient to create a single instance.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class ListView(View):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
|
def __init__(self, model, template):
|
||||||
|
self.model = model
|
||||||
|
self.template = template
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
return render_template(self.template_name)
|
items = self.model.query.all()
|
||||||
|
return render_template(self.template, items=items)
|
||||||
|
|
||||||
And then you can register it like this::
|
Different instances will still be created each for each ``as_view``
|
||||||
|
call, but not for each request to those views.
|
||||||
|
|
||||||
|
|
||||||
|
View Decorators
|
||||||
|
---------------
|
||||||
|
|
||||||
|
The view class itself is not the view function. View decorators need to
|
||||||
|
be applied to the view function returned by ``as_view``, not the class
|
||||||
|
itself. Set :attr:`View.decorators` to a list of decorators to apply.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class UserList(View):
|
||||||
|
decorators = [cache(minutes=2), login_required]
|
||||||
|
|
||||||
|
app.add_url_rule('/users/', view_func=UserList.as_view())
|
||||||
|
|
||||||
|
If you didn't set ``decorators``, you could apply them manually instead.
|
||||||
|
This is equivalent to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
view = UserList.as_view("users_list")
|
||||||
|
view = cache(minutes=2)(view)
|
||||||
|
view = login_required(view)
|
||||||
|
app.add_url_rule('/users/', view_func=view)
|
||||||
|
|
||||||
|
Keep in mind that order matters. If you're used to ``@decorator`` style,
|
||||||
|
this is equivalent to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.route("/users/")
|
||||||
|
@login_required
|
||||||
|
@cache(minutes=2)
|
||||||
|
def user_list():
|
||||||
|
...
|
||||||
|
|
||||||
app.add_url_rule('/about', view_func=RenderTemplateView.as_view(
|
|
||||||
'about_page', template_name='about.html'))
|
|
||||||
|
|
||||||
Method Hints
|
Method Hints
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Pluggable views are attached to the application like a regular function by
|
A common pattern is to register a view with ``methods=["GET", "POST"]``,
|
||||||
either using :func:`~flask.Flask.route` or better
|
then check ``request.method == "POST"`` to decide what to do. Setting
|
||||||
:meth:`~flask.Flask.add_url_rule`. That however also means that you would
|
:attr:`View.methods` is equivalent to passing the list of methods to
|
||||||
have to provide the names of the HTTP methods the view supports when you
|
``add_url_rule`` or ``route``.
|
||||||
attach this. In order to move that information to the class you can
|
|
||||||
provide a :attr:`~flask.views.View.methods` attribute that has this
|
.. code-block:: python
|
||||||
information::
|
|
||||||
|
|
||||||
class MyView(View):
|
class MyView(View):
|
||||||
methods = ['GET', 'POST']
|
methods = ["GET", "POST"]
|
||||||
|
|
||||||
def dispatch_request(self):
|
def dispatch_request(self):
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
...
|
...
|
||||||
...
|
...
|
||||||
|
|
||||||
app.add_url_rule('/myview', view_func=MyView.as_view('myview'))
|
app.add_url_rule('/my-view', view_func=MyView.as_view('my-view'))
|
||||||
|
|
||||||
Method Based Dispatching
|
This is equivalent to the following, except further subclasses can
|
||||||
------------------------
|
inherit or change the methods.
|
||||||
|
|
||||||
For RESTful APIs it's especially helpful to execute a different function
|
.. code-block:: python
|
||||||
for each HTTP method. With the :class:`flask.views.MethodView` you can
|
|
||||||
easily do that. Each HTTP method maps to a method of the class with the
|
app.add_url_rule(
|
||||||
same name (just in lowercase)::
|
"/my-view",
|
||||||
|
view_func=MyView.as_view("my-view"),
|
||||||
|
methods=["GET", "POST"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Method Dispatching and APIs
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
For APIs it can be helpful to use a different function for each HTTP
|
||||||
|
method. :class:`MethodView` extends the basic :class:`View` to dispatch
|
||||||
|
to different methods of the class based on the request method. Each HTTP
|
||||||
|
method maps to a method of the class with the same (lowercase) name.
|
||||||
|
|
||||||
|
:class:`MethodView` automatically sets :attr:`View.methods` based on the
|
||||||
|
methods defined by the class. It even knows how to handle subclasses
|
||||||
|
that override or define other methods.
|
||||||
|
|
||||||
|
We can make a generic ``ItemAPI`` class that provides get (detail),
|
||||||
|
patch (edit), and delete methods for a given model. A ``GroupAPI`` can
|
||||||
|
provide get (list) and post (create) methods.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from flask.views import MethodView
|
from flask.views import MethodView
|
||||||
|
|
||||||
class UserAPI(MethodView):
|
class ItemAPI(MethodView):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
|
def __init__(self, model):
|
||||||
|
self.model
|
||||||
|
self.validator = generate_validator(model)
|
||||||
|
|
||||||
|
def _get_item(self, id):
|
||||||
|
return self.model.query.get_or_404(id)
|
||||||
|
|
||||||
|
def get(self, id):
|
||||||
|
user = self._get_item(id)
|
||||||
|
return jsonify(item.to_json())
|
||||||
|
|
||||||
|
def patch(self, id):
|
||||||
|
item = self._get_item(id)
|
||||||
|
errors = self.validator.validate(item, request.json)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
return jsonify(errors), 400
|
||||||
|
|
||||||
|
item.update_from_json(request.json)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(item.to_json())
|
||||||
|
|
||||||
|
def delete(self, id):
|
||||||
|
item = self._get_item(id)
|
||||||
|
db.session.delete(item)
|
||||||
|
db.session.commit()
|
||||||
|
return "", 204
|
||||||
|
|
||||||
|
class GroupAPI(MethodView):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
|
def __init__(self, model):
|
||||||
|
self.model = model
|
||||||
|
self.validator = generate_validator(model, create=True)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
users = User.query.all()
|
items = self.model.query.all()
|
||||||
...
|
return jsonify([item.to_json() for item in items])
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
user = User.from_form_data(request.form)
|
errors = self.validator.validate(request.json)
|
||||||
...
|
|
||||||
|
|
||||||
app.add_url_rule('/users/', view_func=UserAPI.as_view('users'))
|
if errors:
|
||||||
|
return jsonify(errors), 400
|
||||||
|
|
||||||
That way you also don't have to provide the
|
db.session.add(self.model.from_json(request.json))
|
||||||
:attr:`~flask.views.View.methods` attribute. It's automatically set based
|
db.session.commit()
|
||||||
on the methods defined in the class.
|
return jsonify(item.to_json())
|
||||||
|
|
||||||
Decorating Views
|
def register_api(app, model, url):
|
||||||
----------------
|
app.add_url_rule(f"/{name}/<int:id>", view_func=ItemAPI(f"{name}-item", model))
|
||||||
|
app.add_url_rule(f"/{name}/", view_func=GroupAPI(f"{name}-group", model))
|
||||||
|
|
||||||
Since the view class itself is not the view function that is added to the
|
register_api(app, User, "users")
|
||||||
routing system it does not make much sense to decorate the class itself.
|
register_api(app, Story, "stories")
|
||||||
Instead you either have to decorate the return value of
|
|
||||||
:meth:`~flask.views.View.as_view` by hand::
|
|
||||||
|
|
||||||
def user_required(f):
|
This produces the following views, a standard REST API!
|
||||||
"""Checks whether user is logged in or raises error 401."""
|
|
||||||
def decorator(*args, **kwargs):
|
|
||||||
if not g.user:
|
|
||||||
abort(401)
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
view = user_required(UserAPI.as_view('users'))
|
================= ========== ===================
|
||||||
app.add_url_rule('/users/', view_func=view)
|
URL Method Description
|
||||||
|
----------------- ---------- -------------------
|
||||||
Starting with Flask 0.8 there is also an alternative way where you can
|
``/users/`` ``GET`` List all users
|
||||||
specify a list of decorators to apply in the class declaration::
|
``/users/`` ``POST`` Create a new user
|
||||||
|
``/users/<id>`` ``GET`` Show a single user
|
||||||
class UserAPI(MethodView):
|
``/users/<id>`` ``PATCH`` Update a user
|
||||||
decorators = [user_required]
|
``/users/<id>`` ``DELETE`` Delete a user
|
||||||
|
``/stories/`` ``GET`` List all stories
|
||||||
Due to the implicit self from the caller's perspective you cannot use
|
``/stories/`` ``POST`` Create a new story
|
||||||
regular view decorators on the individual methods of the view however,
|
``/stories/<id>`` ``GET`` Show a single story
|
||||||
keep this in mind.
|
``/stories/<id>`` ``PATCH`` Update a story
|
||||||
|
``/stories/<id>`` ``DELETE`` Delete a story
|
||||||
Method Views for APIs
|
================= ========== ===================
|
||||||
---------------------
|
|
||||||
|
|
||||||
Web APIs are often working very closely with HTTP verbs so it makes a lot
|
|
||||||
of sense to implement such an API based on the
|
|
||||||
:class:`~flask.views.MethodView`. That said, you will notice that the API
|
|
||||||
will require different URL rules that go to the same method view most of
|
|
||||||
the time. For instance consider that you are exposing a user object on
|
|
||||||
the web:
|
|
||||||
|
|
||||||
=============== =============== ======================================
|
|
||||||
URL Method Description
|
|
||||||
--------------- --------------- --------------------------------------
|
|
||||||
``/users/`` ``GET`` Gives a list of all users
|
|
||||||
``/users/`` ``POST`` Creates a new user
|
|
||||||
``/users/<id>`` ``GET`` Shows a single user
|
|
||||||
``/users/<id>`` ``PUT`` Updates a single user
|
|
||||||
``/users/<id>`` ``DELETE`` Deletes a single user
|
|
||||||
=============== =============== ======================================
|
|
||||||
|
|
||||||
So how would you go about doing that with the
|
|
||||||
:class:`~flask.views.MethodView`? The trick is to take advantage of the
|
|
||||||
fact that you can provide multiple rules to the same view.
|
|
||||||
|
|
||||||
Let's assume for the moment the view would look like this::
|
|
||||||
|
|
||||||
class UserAPI(MethodView):
|
|
||||||
|
|
||||||
def get(self, user_id):
|
|
||||||
if user_id is None:
|
|
||||||
# return a list of users
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# expose a single user
|
|
||||||
pass
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
# create a new user
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete(self, user_id):
|
|
||||||
# delete a single user
|
|
||||||
pass
|
|
||||||
|
|
||||||
def put(self, user_id):
|
|
||||||
# update a single user
|
|
||||||
pass
|
|
||||||
|
|
||||||
So how do we hook this up with the routing system? By adding two rules
|
|
||||||
and explicitly mentioning the methods for each::
|
|
||||||
|
|
||||||
user_view = UserAPI.as_view('user_api')
|
|
||||||
app.add_url_rule('/users/', defaults={'user_id': None},
|
|
||||||
view_func=user_view, methods=['GET',])
|
|
||||||
app.add_url_rule('/users/', view_func=user_view, methods=['POST',])
|
|
||||||
app.add_url_rule('/users/<int:user_id>', view_func=user_view,
|
|
||||||
methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
|
|
||||||
If you have a lot of APIs that look similar you can refactor that
|
|
||||||
registration code::
|
|
||||||
|
|
||||||
def register_api(view, endpoint, url, pk='id', pk_type='int'):
|
|
||||||
view_func = view.as_view(endpoint)
|
|
||||||
app.add_url_rule(url, defaults={pk: None},
|
|
||||||
view_func=view_func, methods=['GET',])
|
|
||||||
app.add_url_rule(url, view_func=view_func, methods=['POST',])
|
|
||||||
app.add_url_rule(f'{url}<{pk_type}:{pk}>', view_func=view_func,
|
|
||||||
methods=['GET', 'PUT', 'DELETE'])
|
|
||||||
|
|
||||||
register_api(UserAPI, 'user_api', '/users/', pk='user_id')
|
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,19 @@
|
||||||
-r typing.txt
|
-r typing.txt
|
||||||
cfgv==3.3.1
|
cfgv==3.3.1
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
click==8.1.2
|
click==8.1.3
|
||||||
# via
|
# via
|
||||||
# pip-compile-multi
|
# pip-compile-multi
|
||||||
# pip-tools
|
# pip-tools
|
||||||
distlib==0.3.4
|
distlib==0.3.4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
filelock==3.6.0
|
filelock==3.7.1
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
greenlet==1.1.2 ; python_version < "3.11"
|
greenlet==1.1.2 ; python_version < "3.11"
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
identify==2.5.0
|
identify==2.5.1
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
nodeenv==1.6.0
|
nodeenv==1.6.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
|
@ -30,11 +30,11 @@ pep517==0.12.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
pip-compile-multi==2.4.5
|
pip-compile-multi==2.4.5
|
||||||
# via -r requirements/dev.in
|
# via -r requirements/dev.in
|
||||||
pip-tools==6.6.0
|
pip-tools==6.6.2
|
||||||
# via pip-compile-multi
|
# via pip-compile-multi
|
||||||
platformdirs==2.5.2
|
platformdirs==2.5.2
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
pre-commit==2.18.1
|
pre-commit==2.19.0
|
||||||
# via -r requirements/dev.in
|
# via -r requirements/dev.in
|
||||||
pyyaml==6.0
|
pyyaml==6.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ alabaster==0.7.12
|
||||||
# via sphinx
|
# via sphinx
|
||||||
babel==2.10.1
|
babel==2.10.1
|
||||||
# via sphinx
|
# via sphinx
|
||||||
certifi==2021.10.8
|
certifi==2022.5.18.1
|
||||||
# via requests
|
# via requests
|
||||||
charset-normalizer==2.0.12
|
charset-normalizer==2.0.12
|
||||||
# via requests
|
# via requests
|
||||||
|
|
@ -21,7 +21,7 @@ idna==3.3
|
||||||
# via requests
|
# via requests
|
||||||
imagesize==1.3.0
|
imagesize==1.3.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
jinja2==3.1.1
|
jinja2==3.1.2
|
||||||
# via sphinx
|
# via sphinx
|
||||||
markupsafe==2.1.1
|
markupsafe==2.1.1
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
|
@ -35,7 +35,7 @@ pygments==2.12.0
|
||||||
# via
|
# via
|
||||||
# sphinx
|
# sphinx
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
pyparsing==3.0.8
|
pyparsing==3.0.9
|
||||||
# via packaging
|
# via packaging
|
||||||
pytz==2022.1
|
pytz==2022.1
|
||||||
# via babel
|
# via babel
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
#
|
#
|
||||||
# pip-compile-multi
|
# pip-compile-multi
|
||||||
#
|
#
|
||||||
asgiref==3.5.0
|
asgiref==3.5.2
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
attrs==21.4.0
|
attrs==21.4.0
|
||||||
# via pytest
|
# via pytest
|
||||||
|
|
@ -21,7 +21,7 @@ pluggy==1.0.0
|
||||||
# via pytest
|
# via pytest
|
||||||
py==1.11.0
|
py==1.11.0
|
||||||
# via pytest
|
# via pytest
|
||||||
pyparsing==3.0.8
|
pyparsing==3.0.9
|
||||||
# via packaging
|
# via packaging
|
||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
#
|
#
|
||||||
cffi==1.15.0
|
cffi==1.15.0
|
||||||
# via cryptography
|
# via cryptography
|
||||||
cryptography==37.0.1
|
cryptography==37.0.2
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
mypy==0.950
|
mypy==0.960
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
mypy-extensions==0.4.3
|
mypy-extensions==0.4.3
|
||||||
# via mypy
|
# via mypy
|
||||||
|
|
@ -17,11 +17,11 @@ pycparser==2.21
|
||||||
# via cffi
|
# via cffi
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
# via mypy
|
# via mypy
|
||||||
types-contextvars==2.4.5
|
types-contextvars==2.4.6
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
types-dataclasses==0.6.5
|
types-dataclasses==0.6.5
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
types-setuptools==57.4.14
|
types-setuptools==57.4.17
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
typing-extensions==4.2.0
|
typing-extensions==4.2.0
|
||||||
# via mypy
|
# via mypy
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ per-file-ignores =
|
||||||
src/flask/__init__.py: F401
|
src/flask/__init__.py: F401
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
files = src/flask
|
files = src/flask, tests/typing
|
||||||
python_version = 3.7
|
python_version = 3.7
|
||||||
show_error_codes = True
|
show_error_codes = True
|
||||||
allow_redefinition = True
|
allow_redefinition = True
|
||||||
|
|
@ -116,3 +116,6 @@ ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-cryptography.*]
|
[mypy-cryptography.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-importlib_metadata]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
from markupsafe import escape
|
from markupsafe import escape
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from werkzeug.exceptions import abort as abort
|
|
||||||
from werkzeug.utils import redirect as redirect
|
|
||||||
|
|
||||||
from . import json as json
|
from . import json as json
|
||||||
from .app import Flask as Flask
|
from .app import Flask as Flask
|
||||||
|
|
@ -19,10 +17,12 @@ from .globals import current_app as current_app
|
||||||
from .globals import g as g
|
from .globals import g as g
|
||||||
from .globals import request as request
|
from .globals import request as request
|
||||||
from .globals import session as session
|
from .globals import session as session
|
||||||
|
from .helpers import abort as abort
|
||||||
from .helpers import flash as flash
|
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 redirect as redirect
|
||||||
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
|
||||||
|
|
@ -42,4 +42,4 @@ from .signals import template_rendered as template_rendered
|
||||||
from .templating import render_template as render_template
|
from .templating import render_template as render_template
|
||||||
from .templating import render_template_string as render_template_string
|
from .templating import render_template_string as render_template_string
|
||||||
|
|
||||||
__version__ = "2.1.2"
|
__version__ = "2.2.0.dev0"
|
||||||
|
|
|
||||||
325
src/flask/app.py
325
src/flask/app.py
|
|
@ -12,6 +12,7 @@ from types import TracebackType
|
||||||
|
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.datastructures import ImmutableDict
|
from werkzeug.datastructures import ImmutableDict
|
||||||
|
from werkzeug.exceptions import Aborter
|
||||||
from werkzeug.exceptions import BadRequest
|
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
|
||||||
|
|
@ -22,15 +23,19 @@ from werkzeug.routing import MapAdapter
|
||||||
from werkzeug.routing import RequestRedirect
|
from werkzeug.routing import RequestRedirect
|
||||||
from werkzeug.routing import RoutingException
|
from werkzeug.routing import RoutingException
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
from werkzeug.urls import url_quote
|
||||||
|
from werkzeug.utils import redirect as _wz_redirect
|
||||||
from werkzeug.wrappers import Response as BaseResponse
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
|
|
||||||
from . import cli
|
from . import cli
|
||||||
from . import json
|
from . import json
|
||||||
|
from . import typing as ft
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .config import ConfigAttribute
|
from .config import ConfigAttribute
|
||||||
from .ctx import _AppCtxGlobals
|
from .ctx import _AppCtxGlobals
|
||||||
from .ctx import AppContext
|
from .ctx import AppContext
|
||||||
from .ctx import RequestContext
|
from .ctx import RequestContext
|
||||||
|
from .globals import _app_ctx_stack
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
from .globals import g
|
from .globals import g
|
||||||
from .globals import request
|
from .globals import request
|
||||||
|
|
@ -41,7 +46,6 @@ from .helpers import get_env
|
||||||
from .helpers import get_flashed_messages
|
from .helpers import get_flashed_messages
|
||||||
from .helpers import get_load_dotenv
|
from .helpers import get_load_dotenv
|
||||||
from .helpers import locked_cached_property
|
from .helpers import locked_cached_property
|
||||||
from .helpers import url_for
|
|
||||||
from .json import jsonify
|
from .json import jsonify
|
||||||
from .logging import create_logger
|
from .logging import create_logger
|
||||||
from .scaffold import _endpoint_from_view_func
|
from .scaffold import _endpoint_from_view_func
|
||||||
|
|
@ -58,21 +62,14 @@ 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 BeforeFirstRequestCallable
|
|
||||||
from .typing import ResponseReturnValue
|
|
||||||
from .typing import TeardownCallable
|
|
||||||
from .typing import TemplateFilterCallable
|
|
||||||
from .typing import TemplateGlobalCallable
|
|
||||||
from .typing import TemplateTestCallable
|
|
||||||
from .wrappers import Request
|
from .wrappers import Request
|
||||||
from .wrappers import Response
|
from .wrappers import Response
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
import typing_extensions as te
|
import typing_extensions as te
|
||||||
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
|
||||||
|
|
@ -200,6 +197,16 @@ class Flask(Scaffold):
|
||||||
#: :class:`~flask.Response` for more information.
|
#: :class:`~flask.Response` for more information.
|
||||||
response_class = Response
|
response_class = Response
|
||||||
|
|
||||||
|
#: The class of the object assigned to :attr:`aborter`, created by
|
||||||
|
#: :meth:`create_aborter`. That object is called by
|
||||||
|
#: :func:`flask.abort` to raise HTTP errors, and can be
|
||||||
|
#: called directly as well.
|
||||||
|
#:
|
||||||
|
#: Defaults to :class:`werkzeug.exceptions.Aborter`.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.2
|
||||||
|
aborter_class = Aborter
|
||||||
|
|
||||||
#: The class that is used for the Jinja environment.
|
#: The class that is used for the Jinja environment.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.11
|
#: .. versionadded:: 0.11
|
||||||
|
|
@ -420,23 +427,36 @@ class Flask(Scaffold):
|
||||||
#: to load a config from files.
|
#: to load a config from files.
|
||||||
self.config = self.make_config(instance_relative_config)
|
self.config = self.make_config(instance_relative_config)
|
||||||
|
|
||||||
#: A list of functions that are called when :meth:`url_for` raises a
|
#: An instance of :attr:`aborter_class` created by
|
||||||
#: :exc:`~werkzeug.routing.BuildError`. Each function registered here
|
#: :meth:`make_aborter`. This is called by :func:`flask.abort`
|
||||||
#: is called with `error`, `endpoint` and `values`. If a function
|
#: to raise HTTP errors, and can be called directly as well.
|
||||||
#: returns ``None`` or raises a :exc:`BuildError` the next function is
|
#:
|
||||||
#: tried.
|
#: .. versionadded:: 2.2
|
||||||
|
#: Moved from ``flask.abort``, which calls this object.
|
||||||
|
self.aborter = self.make_aborter()
|
||||||
|
|
||||||
|
#: A list of functions that are called by
|
||||||
|
#: :meth:`handle_url_build_error` when :meth:`.url_for` raises a
|
||||||
|
#: :exc:`~werkzeug.routing.BuildError`. Each function is called
|
||||||
|
#: with ``error``, ``endpoint`` and ``values``. If a function
|
||||||
|
#: returns ``None`` or raises a ``BuildError``, it is skipped.
|
||||||
|
#: Otherwise, its return value is returned by ``url_for``.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.9
|
#: .. versionadded:: 0.9
|
||||||
self.url_build_error_handlers: t.List[
|
self.url_build_error_handlers: t.List[
|
||||||
t.Callable[[Exception, str, dict], str]
|
t.Callable[[Exception, str, t.Dict[str, t.Any]], str]
|
||||||
] = []
|
] = []
|
||||||
|
|
||||||
#: A list of functions that will be called at the beginning of the
|
#: A list of functions that will be called at the beginning of the
|
||||||
#: first request to this instance. To register a function, use the
|
#: first request to this instance. To register a function, use the
|
||||||
#: :meth:`before_first_request` decorator.
|
#: :meth:`before_first_request` decorator.
|
||||||
#:
|
#:
|
||||||
|
#: .. deprecated:: 2.2
|
||||||
|
#: Will be removed in Flask 2.3. Run setup code when
|
||||||
|
#: creating the application instead.
|
||||||
|
#:
|
||||||
#: .. versionadded:: 0.8
|
#: .. versionadded:: 0.8
|
||||||
self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = []
|
self.before_first_request_funcs: t.List[ft.BeforeFirstRequestCallable] = []
|
||||||
|
|
||||||
#: A list of functions that are called when the application context
|
#: A list of functions that are called when the application context
|
||||||
#: is destroyed. Since the application context is also torn down
|
#: is destroyed. Since the application context is also torn down
|
||||||
|
|
@ -444,7 +464,7 @@ class Flask(Scaffold):
|
||||||
#: from databases.
|
#: from databases.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.9
|
#: .. versionadded:: 0.9
|
||||||
self.teardown_appcontext_funcs: t.List[TeardownCallable] = []
|
self.teardown_appcontext_funcs: t.List[ft.TeardownCallable] = []
|
||||||
|
|
||||||
#: A list of shell context processor functions that should be run
|
#: A list of shell context processor functions that should be run
|
||||||
#: when a shell context is created.
|
#: when a shell context is created.
|
||||||
|
|
@ -519,8 +539,17 @@ class Flask(Scaffold):
|
||||||
# the app's commands to another CLI tool.
|
# the app's commands to another CLI tool.
|
||||||
self.cli.name = self.name
|
self.cli.name = self.name
|
||||||
|
|
||||||
def _is_setup_finished(self) -> bool:
|
def _check_setup_finished(self, f_name: str) -> None:
|
||||||
return self.debug and self._got_first_request
|
if self._got_first_request:
|
||||||
|
raise AssertionError(
|
||||||
|
f"The setup method '{f_name}' can no longer be called"
|
||||||
|
" on the application. It has already handled its first"
|
||||||
|
" request, any changes will not be applied"
|
||||||
|
" consistently.\n"
|
||||||
|
"Make sure all imports, decorators, functions, etc."
|
||||||
|
" needed to set up the application are done before"
|
||||||
|
" running it."
|
||||||
|
)
|
||||||
|
|
||||||
@locked_cached_property
|
@locked_cached_property
|
||||||
def name(self) -> str: # type: ignore
|
def name(self) -> str: # type: ignore
|
||||||
|
|
@ -627,6 +656,18 @@ class Flask(Scaffold):
|
||||||
defaults["DEBUG"] = get_debug_flag()
|
defaults["DEBUG"] = get_debug_flag()
|
||||||
return self.config_class(root_path, defaults)
|
return self.config_class(root_path, defaults)
|
||||||
|
|
||||||
|
def make_aborter(self) -> Aborter:
|
||||||
|
"""Create the object to assign to :attr:`aborter`. That object
|
||||||
|
is called by :func:`flask.abort` to raise HTTP errors, and can
|
||||||
|
be called directly as well.
|
||||||
|
|
||||||
|
By default, this creates an instance of :attr:`aborter_class`,
|
||||||
|
which defaults to :class:`werkzeug.exceptions.Aborter`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
"""
|
||||||
|
return self.aborter_class()
|
||||||
|
|
||||||
def auto_find_instance_path(self) -> str:
|
def auto_find_instance_path(self) -> str:
|
||||||
"""Tries to locate the instance path if it was not provided to the
|
"""Tries to locate the instance path if it was not provided to the
|
||||||
constructor of the application class. It will basically calculate
|
constructor of the application class. It will basically calculate
|
||||||
|
|
@ -693,7 +734,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
rv = self.jinja_environment(self, **options)
|
rv = self.jinja_environment(self, **options)
|
||||||
rv.globals.update(
|
rv.globals.update(
|
||||||
url_for=url_for,
|
url_for=self.url_for,
|
||||||
get_flashed_messages=get_flashed_messages,
|
get_flashed_messages=get_flashed_messages,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
# request, session and g are normally added with the
|
# request, session and g are normally added with the
|
||||||
|
|
@ -1039,7 +1080,7 @@ class Flask(Scaffold):
|
||||||
self,
|
self,
|
||||||
rule: str,
|
rule: str,
|
||||||
endpoint: t.Optional[str] = None,
|
endpoint: t.Optional[str] = None,
|
||||||
view_func: t.Optional[t.Callable] = None,
|
view_func: t.Optional[ft.ViewCallable] = None,
|
||||||
provide_automatic_options: t.Optional[bool] = None,
|
provide_automatic_options: t.Optional[bool] = None,
|
||||||
**options: t.Any,
|
**options: t.Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -1096,7 +1137,7 @@ class Flask(Scaffold):
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def template_filter(
|
def template_filter(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
|
) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]:
|
||||||
"""A decorator that is used to register custom template filter.
|
"""A decorator that is used to register custom template filter.
|
||||||
You can specify a name for the filter, otherwise the function
|
You can specify a name for the filter, otherwise the function
|
||||||
name will be used. Example::
|
name will be used. Example::
|
||||||
|
|
@ -1109,7 +1150,7 @@ class Flask(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable:
|
def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable:
|
||||||
self.add_template_filter(f, name=name)
|
self.add_template_filter(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -1117,7 +1158,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def add_template_filter(
|
def add_template_filter(
|
||||||
self, f: TemplateFilterCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template filter. Works exactly like the
|
"""Register a custom template filter. Works exactly like the
|
||||||
:meth:`template_filter` decorator.
|
:meth:`template_filter` decorator.
|
||||||
|
|
@ -1130,7 +1171,7 @@ class Flask(Scaffold):
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def template_test(
|
def template_test(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
|
) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]:
|
||||||
"""A decorator that is used to register custom template test.
|
"""A decorator that is used to register custom template test.
|
||||||
You can specify a name for the test, otherwise the function
|
You can specify a name for the test, otherwise the function
|
||||||
name will be used. Example::
|
name will be used. Example::
|
||||||
|
|
@ -1150,7 +1191,7 @@ class Flask(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateTestCallable) -> TemplateTestCallable:
|
def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable:
|
||||||
self.add_template_test(f, name=name)
|
self.add_template_test(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -1158,7 +1199,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def add_template_test(
|
def add_template_test(
|
||||||
self, f: TemplateTestCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template test. Works exactly like the
|
"""Register a custom template test. Works exactly like the
|
||||||
:meth:`template_test` decorator.
|
:meth:`template_test` decorator.
|
||||||
|
|
@ -1173,7 +1214,7 @@ class Flask(Scaffold):
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def template_global(
|
def template_global(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
|
) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]:
|
||||||
"""A decorator that is used to register a custom template global function.
|
"""A decorator that is used to register a custom template global function.
|
||||||
You can specify a name for the global function, otherwise the function
|
You can specify a name for the global function, otherwise the function
|
||||||
name will be used. Example::
|
name will be used. Example::
|
||||||
|
|
@ -1188,7 +1229,7 @@ class Flask(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable:
|
def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable:
|
||||||
self.add_template_global(f, name=name)
|
self.add_template_global(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -1196,7 +1237,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def add_template_global(
|
def add_template_global(
|
||||||
self, f: TemplateGlobalCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template global function. Works exactly like the
|
"""Register a custom template global function. Works exactly like the
|
||||||
:meth:`template_global` decorator.
|
:meth:`template_global` decorator.
|
||||||
|
|
@ -1210,21 +1251,34 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def before_first_request(
|
def before_first_request(
|
||||||
self, f: BeforeFirstRequestCallable
|
self, f: ft.BeforeFirstRequestCallable
|
||||||
) -> BeforeFirstRequestCallable:
|
) -> ft.BeforeFirstRequestCallable:
|
||||||
"""Registers a function to be run before the first request to this
|
"""Registers a function to be run before the first request to this
|
||||||
instance of the application.
|
instance of the application.
|
||||||
|
|
||||||
The function will be called without any arguments and its return
|
The function will be called without any arguments and its return
|
||||||
value is ignored.
|
value is ignored.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Run setup code when creating
|
||||||
|
the application instead.
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'before_first_request' is deprecated and will be removed"
|
||||||
|
" in Flask 2.3. Run setup code while creating the"
|
||||||
|
" application instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
self.before_first_request_funcs.append(f)
|
self.before_first_request_funcs.append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def teardown_appcontext(self, f: TeardownCallable) -> TeardownCallable:
|
def teardown_appcontext(self, f: ft.TeardownCallable) -> ft.TeardownCallable:
|
||||||
"""Registers a function to be called when the application context
|
"""Registers a function to be called when the application context
|
||||||
ends. These functions are typically also called when the request
|
ends. These functions are typically also called when the request
|
||||||
context is popped.
|
context is popped.
|
||||||
|
|
@ -1265,7 +1319,7 @@ 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[ft.ErrorHandlerCallable]:
|
||||||
"""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
|
||||||
|
|
@ -1290,7 +1344,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
def handle_http_exception(
|
def handle_http_exception(
|
||||||
self, e: HTTPException
|
self, e: HTTPException
|
||||||
) -> t.Union[HTTPException, ResponseReturnValue]:
|
) -> t.Union[HTTPException, ft.ResponseReturnValue]:
|
||||||
"""Handles an HTTP exception. By default this will invoke the
|
"""Handles an HTTP exception. By default this will invoke the
|
||||||
registered error handlers and fall back to returning the
|
registered error handlers and fall back to returning the
|
||||||
exception as response.
|
exception as response.
|
||||||
|
|
@ -1360,7 +1414,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
def handle_user_exception(
|
def handle_user_exception(
|
||||||
self, e: Exception
|
self, e: Exception
|
||||||
) -> t.Union[HTTPException, ResponseReturnValue]:
|
) -> t.Union[HTTPException, ft.ResponseReturnValue]:
|
||||||
"""This method is called whenever an exception occurs that
|
"""This method is called whenever an exception occurs that
|
||||||
should be handled. A special case is :class:`~werkzeug
|
should be handled. A special case is :class:`~werkzeug
|
||||||
.exceptions.HTTPException` which is forwarded to the
|
.exceptions.HTTPException` which is forwarded to the
|
||||||
|
|
@ -1430,7 +1484,7 @@ class Flask(Scaffold):
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
self.log_exception(exc_info)
|
self.log_exception(exc_info)
|
||||||
server_error: t.Union[InternalServerError, ResponseReturnValue]
|
server_error: t.Union[InternalServerError, ft.ResponseReturnValue]
|
||||||
server_error = InternalServerError(original_exception=e)
|
server_error = InternalServerError(original_exception=e)
|
||||||
handler = self._find_error_handler(server_error)
|
handler = self._find_error_handler(server_error)
|
||||||
|
|
||||||
|
|
@ -1484,7 +1538,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
raise FormDataRoutingRedirect(request)
|
raise FormDataRoutingRedirect(request)
|
||||||
|
|
||||||
def dispatch_request(self) -> ResponseReturnValue:
|
def dispatch_request(self) -> ft.ResponseReturnValue:
|
||||||
"""Does the request dispatching. Matches the URL and returns the
|
"""Does the request dispatching. Matches the URL and returns the
|
||||||
return value of the view or error handler. This does not have to
|
return value of the view or error handler. This does not have to
|
||||||
be a response object. In order to convert the return value to a
|
be a response object. In order to convert the return value to a
|
||||||
|
|
@ -1515,7 +1569,17 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
.. versionadded:: 0.7
|
||||||
"""
|
"""
|
||||||
self.try_trigger_before_first_request_functions()
|
# Run before_first_request functions if this is the thread's first request.
|
||||||
|
# Inlined to avoid a method call on subsequent requests.
|
||||||
|
# This is deprecated, will be removed in Flask 2.3.
|
||||||
|
if not self._got_first_request:
|
||||||
|
with self._before_request_lock:
|
||||||
|
if not self._got_first_request:
|
||||||
|
for func in self.before_first_request_funcs:
|
||||||
|
self.ensure_sync(func)()
|
||||||
|
|
||||||
|
self._got_first_request = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request_started.send(self)
|
request_started.send(self)
|
||||||
rv = self.preprocess_request()
|
rv = self.preprocess_request()
|
||||||
|
|
@ -1527,7 +1591,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
def finalize_request(
|
def finalize_request(
|
||||||
self,
|
self,
|
||||||
rv: t.Union[ResponseReturnValue, HTTPException],
|
rv: t.Union[ft.ResponseReturnValue, HTTPException],
|
||||||
from_error_handler: bool = False,
|
from_error_handler: bool = False,
|
||||||
) -> Response:
|
) -> Response:
|
||||||
"""Given the return value from a view function this finalizes
|
"""Given the return value from a view function this finalizes
|
||||||
|
|
@ -1554,22 +1618,6 @@ class Flask(Scaffold):
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def try_trigger_before_first_request_functions(self) -> None:
|
|
||||||
"""Called before each request and will ensure that it triggers
|
|
||||||
the :attr:`before_first_request_funcs` and only exactly once per
|
|
||||||
application instance (which means process usually).
|
|
||||||
|
|
||||||
:internal:
|
|
||||||
"""
|
|
||||||
if self._got_first_request:
|
|
||||||
return
|
|
||||||
with self._before_request_lock:
|
|
||||||
if self._got_first_request:
|
|
||||||
return
|
|
||||||
for func in self.before_first_request_funcs:
|
|
||||||
self.ensure_sync(func)()
|
|
||||||
self._got_first_request = True
|
|
||||||
|
|
||||||
def make_default_options_response(self) -> Response:
|
def make_default_options_response(self) -> Response:
|
||||||
"""This method is called to create the default ``OPTIONS`` response.
|
"""This method is called to create the default ``OPTIONS`` response.
|
||||||
This can be changed through subclassing to change the default
|
This can be changed through subclassing to change the default
|
||||||
|
|
@ -1630,7 +1678,145 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
return asgiref_async_to_sync(func)
|
return asgiref_async_to_sync(func)
|
||||||
|
|
||||||
def make_response(self, rv: ResponseReturnValue) -> Response:
|
def url_for(
|
||||||
|
self,
|
||||||
|
endpoint: str,
|
||||||
|
*,
|
||||||
|
_anchor: t.Optional[str] = None,
|
||||||
|
_method: t.Optional[str] = None,
|
||||||
|
_scheme: t.Optional[str] = None,
|
||||||
|
_external: t.Optional[bool] = None,
|
||||||
|
**values: t.Any,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a URL to the given endpoint with the given values.
|
||||||
|
|
||||||
|
This is called by :func:`flask.url_for`, and can be called
|
||||||
|
directly as well.
|
||||||
|
|
||||||
|
An *endpoint* is the name of a URL rule, usually added with
|
||||||
|
:meth:`@app.route() <route>`, and usually the same name as the
|
||||||
|
view function. A route defined in a :class:`~flask.Blueprint`
|
||||||
|
will prepend the blueprint's name separated by a ``.`` to the
|
||||||
|
endpoint.
|
||||||
|
|
||||||
|
In some cases, such as email messages, you want URLs to include
|
||||||
|
the scheme and domain, like ``https://example.com/hello``. When
|
||||||
|
not in an active request, URLs will be external by default, but
|
||||||
|
this requires setting :data:`SERVER_NAME` so Flask knows what
|
||||||
|
domain to use. :data:`APPLICATION_ROOT` and
|
||||||
|
:data:`PREFERRED_URL_SCHEME` should also be configured as
|
||||||
|
needed. This config is only used when not in an active request.
|
||||||
|
|
||||||
|
Functions can be decorated with :meth:`url_defaults` to modify
|
||||||
|
keyword arguments before the URL is built.
|
||||||
|
|
||||||
|
If building fails for some reason, such as an unknown endpoint
|
||||||
|
or incorrect values, the app's :meth:`handle_url_build_error`
|
||||||
|
method is called. If that returns a string, that is returned,
|
||||||
|
otherwise a :exc:`~werkzeug.routing.BuildError` is raised.
|
||||||
|
|
||||||
|
:param endpoint: The endpoint name associated with the URL to
|
||||||
|
generate. If this starts with a ``.``, the current blueprint
|
||||||
|
name (if any) will be used.
|
||||||
|
:param _anchor: If given, append this as ``#anchor`` to the URL.
|
||||||
|
:param _method: If given, generate the URL associated with this
|
||||||
|
method for the endpoint.
|
||||||
|
:param _scheme: If given, the URL will have this scheme if it
|
||||||
|
is external.
|
||||||
|
:param _external: If given, prefer the URL to be internal
|
||||||
|
(False) or require it to be external (True). External URLs
|
||||||
|
include the scheme and domain. When not in an active
|
||||||
|
request, URLs are external by default.
|
||||||
|
:param values: Values to use for the variable parts of the URL
|
||||||
|
rule. Unknown keys are appended as query string arguments,
|
||||||
|
like ``?a=b&c=d``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
Moved from ``flask.url_for``, which calls this method.
|
||||||
|
"""
|
||||||
|
req_ctx = _request_ctx_stack.top
|
||||||
|
|
||||||
|
if req_ctx is not None:
|
||||||
|
url_adapter = req_ctx.url_adapter
|
||||||
|
blueprint_name = req_ctx.request.blueprint
|
||||||
|
|
||||||
|
# If the endpoint starts with "." and the request matches a
|
||||||
|
# blueprint, the endpoint is relative to the blueprint.
|
||||||
|
if endpoint[:1] == ".":
|
||||||
|
if blueprint_name is not None:
|
||||||
|
endpoint = f"{blueprint_name}{endpoint}"
|
||||||
|
else:
|
||||||
|
endpoint = endpoint[1:]
|
||||||
|
|
||||||
|
# When in a request, generate a URL without scheme and
|
||||||
|
# domain by default, unless a scheme is given.
|
||||||
|
if _external is None:
|
||||||
|
_external = _scheme is not None
|
||||||
|
else:
|
||||||
|
app_ctx = _app_ctx_stack.top
|
||||||
|
|
||||||
|
# If called by helpers.url_for, an app context is active,
|
||||||
|
# use its url_adapter. Otherwise, app.url_for was called
|
||||||
|
# directly, build an adapter.
|
||||||
|
if app_ctx is not None:
|
||||||
|
url_adapter = app_ctx.url_adapter
|
||||||
|
else:
|
||||||
|
url_adapter = self.create_url_adapter(None)
|
||||||
|
|
||||||
|
if url_adapter is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Unable to build URLs outside an active request"
|
||||||
|
" without 'SERVER_NAME' configured. Also configure"
|
||||||
|
" 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as"
|
||||||
|
" needed."
|
||||||
|
)
|
||||||
|
|
||||||
|
# When outside a request, generate a URL with scheme and
|
||||||
|
# domain by default.
|
||||||
|
if _external is None:
|
||||||
|
_external = True
|
||||||
|
|
||||||
|
# It is an error to set _scheme when _external=False, in order
|
||||||
|
# to avoid accidental insecure URLs.
|
||||||
|
if _scheme is not None and not _external:
|
||||||
|
raise ValueError("When specifying '_scheme', '_external' must be True.")
|
||||||
|
|
||||||
|
self.inject_url_defaults(endpoint, values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
rv = url_adapter.build(
|
||||||
|
endpoint,
|
||||||
|
values,
|
||||||
|
method=_method,
|
||||||
|
url_scheme=_scheme,
|
||||||
|
force_external=_external,
|
||||||
|
)
|
||||||
|
except BuildError as error:
|
||||||
|
values.update(
|
||||||
|
_anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external
|
||||||
|
)
|
||||||
|
return self.handle_url_build_error(error, endpoint, values)
|
||||||
|
|
||||||
|
if _anchor is not None:
|
||||||
|
rv = f"{rv}#{url_quote(_anchor)}"
|
||||||
|
|
||||||
|
return rv
|
||||||
|
|
||||||
|
def redirect(self, location: str, code: int = 302) -> BaseResponse:
|
||||||
|
"""Create a redirect response object.
|
||||||
|
|
||||||
|
This is called by :func:`flask.redirect`, and can be called
|
||||||
|
directly as well.
|
||||||
|
|
||||||
|
:param location: The URL to redirect to.
|
||||||
|
:param code: The status code for the redirect.
|
||||||
|
|
||||||
|
.. versionadded:: 2.2
|
||||||
|
Moved from ``flask.redirect``, which calls this method.
|
||||||
|
"""
|
||||||
|
return _wz_redirect(location, code=code, Response=self.response_class)
|
||||||
|
|
||||||
|
def make_response(self, rv: ft.ResponseReturnValue) -> Response:
|
||||||
"""Convert the return value from a view function to an instance of
|
"""Convert the return value from a view function to an instance of
|
||||||
:attr:`response_class`.
|
:attr:`response_class`.
|
||||||
|
|
||||||
|
|
@ -1687,7 +1873,7 @@ class Flask(Scaffold):
|
||||||
if isinstance(rv[1], (Headers, dict, tuple, list)):
|
if isinstance(rv[1], (Headers, dict, tuple, list)):
|
||||||
rv, headers = rv
|
rv, headers = rv
|
||||||
else:
|
else:
|
||||||
rv, status = rv # type: ignore[misc]
|
rv, status = rv # type: ignore[assignment,misc]
|
||||||
# other sized tuples are not allowed
|
# other sized tuples are not allowed
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
|
|
@ -1722,7 +1908,9 @@ class Flask(Scaffold):
|
||||||
# evaluate a WSGI callable, or coerce a different response
|
# evaluate a WSGI callable, or coerce a different response
|
||||||
# class to the correct type
|
# class to the correct type
|
||||||
try:
|
try:
|
||||||
rv = self.response_class.force_type(rv, request.environ) # type: ignore # noqa: B950
|
rv = self.response_class.force_type(
|
||||||
|
rv, request.environ # type: ignore[arg-type]
|
||||||
|
)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"{e}\nThe view function did not return a valid"
|
f"{e}\nThe view function did not return a valid"
|
||||||
|
|
@ -1816,10 +2004,21 @@ class Flask(Scaffold):
|
||||||
func(endpoint, values)
|
func(endpoint, values)
|
||||||
|
|
||||||
def handle_url_build_error(
|
def handle_url_build_error(
|
||||||
self, error: Exception, endpoint: str, values: dict
|
self, error: BuildError, endpoint: str, values: t.Dict[str, t.Any]
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Handle :class:`~werkzeug.routing.BuildError` on
|
"""Called by :meth:`.url_for` if a
|
||||||
:meth:`url_for`.
|
:exc:`~werkzeug.routing.BuildError` was raised. If this returns
|
||||||
|
a value, it will be returned by ``url_for``, otherwise the error
|
||||||
|
will be re-raised.
|
||||||
|
|
||||||
|
Each function in :attr:`url_build_error_handlers` is called with
|
||||||
|
``error``, ``endpoint`` and ``values``. If a function returns
|
||||||
|
``None`` or raises a ``BuildError``, it is skipped. Otherwise,
|
||||||
|
its return value is returned by ``url_for``.
|
||||||
|
|
||||||
|
:param error: The active ``BuildError`` being handled.
|
||||||
|
:param endpoint: The endpoint being built.
|
||||||
|
:param values: The keyword arguments passed to ``url_for``.
|
||||||
"""
|
"""
|
||||||
for handler in self.url_build_error_handlers:
|
for handler in self.url_build_error_handlers:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1838,7 +2037,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
def preprocess_request(self) -> t.Optional[ResponseReturnValue]:
|
def preprocess_request(self) -> t.Optional[ft.ResponseReturnValue]:
|
||||||
"""Called before the request is dispatched. Calls
|
"""Called before the request is dispatched. Calls
|
||||||
:attr:`url_value_preprocessors` registered with the app and the
|
:attr:`url_value_preprocessors` registered with the app and the
|
||||||
current blueprint (if any). Then calls :attr:`before_request_funcs`
|
current blueprint (if any). Then calls :attr:`before_request_funcs`
|
||||||
|
|
|
||||||
|
|
@ -3,23 +3,14 @@ import typing as t
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
from .scaffold import _endpoint_from_view_func
|
from .scaffold import _endpoint_from_view_func
|
||||||
from .scaffold import _sentinel
|
from .scaffold import _sentinel
|
||||||
from .scaffold import Scaffold
|
from .scaffold import Scaffold
|
||||||
from .typing import AfterRequestCallable
|
from .scaffold import setupmethod
|
||||||
from .typing import BeforeFirstRequestCallable
|
|
||||||
from .typing import BeforeRequestCallable
|
|
||||||
from .typing import TeardownCallable
|
|
||||||
from .typing import TemplateContextProcessorCallable
|
|
||||||
from .typing import TemplateFilterCallable
|
|
||||||
from .typing import TemplateGlobalCallable
|
|
||||||
from .typing import TemplateTestCallable
|
|
||||||
from .typing import URLDefaultCallable
|
|
||||||
from .typing import URLValuePreprocessorCallable
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .typing import ErrorHandlerCallable
|
|
||||||
|
|
||||||
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
||||||
|
|
||||||
|
|
@ -162,7 +153,6 @@ class Blueprint(Scaffold):
|
||||||
.. versionadded:: 0.7
|
.. versionadded:: 0.7
|
||||||
"""
|
"""
|
||||||
|
|
||||||
warn_on_modifications = False
|
|
||||||
_got_registered_once = False
|
_got_registered_once = False
|
||||||
|
|
||||||
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
|
#: Blueprint local JSON encoder class to use. Set to ``None`` to use
|
||||||
|
|
@ -208,27 +198,33 @@ class Blueprint(Scaffold):
|
||||||
self.cli_group = cli_group
|
self.cli_group = cli_group
|
||||||
self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
|
self._blueprints: t.List[t.Tuple["Blueprint", dict]] = []
|
||||||
|
|
||||||
def _is_setup_finished(self) -> bool:
|
def _check_setup_finished(self, f_name: str) -> None:
|
||||||
return self.warn_on_modifications and self._got_registered_once
|
if self._got_registered_once:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
f"The setup method '{f_name}' can no longer be called on"
|
||||||
|
f" the blueprint '{self.name}'. It has already been"
|
||||||
|
" registered at least once, any changes will not be"
|
||||||
|
" applied consistently.\n"
|
||||||
|
"Make sure all imports, decorators, functions, etc."
|
||||||
|
" needed to set up the blueprint are done before"
|
||||||
|
" registering it.\n"
|
||||||
|
"This warning will become an exception in Flask 2.3.",
|
||||||
|
UserWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def record(self, func: t.Callable) -> None:
|
def record(self, func: t.Callable) -> None:
|
||||||
"""Registers a function that is called when the blueprint is
|
"""Registers a function that is called when the blueprint is
|
||||||
registered on the application. This function is called with the
|
registered on the application. This function is called with the
|
||||||
state as argument as returned by the :meth:`make_setup_state`
|
state as argument as returned by the :meth:`make_setup_state`
|
||||||
method.
|
method.
|
||||||
"""
|
"""
|
||||||
if self._got_registered_once and self.warn_on_modifications:
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
warn(
|
|
||||||
Warning(
|
|
||||||
"The blueprint was already registered once but is"
|
|
||||||
" getting modified now. These changes will not show"
|
|
||||||
" up."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.deferred_functions.append(func)
|
self.deferred_functions.append(func)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def record_once(self, func: t.Callable) -> None:
|
def record_once(self, func: t.Callable) -> None:
|
||||||
"""Works like :meth:`record` but wraps the function in another
|
"""Works like :meth:`record` but wraps the function in another
|
||||||
function that will ensure the function is only called once. If the
|
function that will ensure the function is only called once. If the
|
||||||
|
|
@ -251,6 +247,7 @@ class Blueprint(Scaffold):
|
||||||
"""
|
"""
|
||||||
return BlueprintSetupState(self, app, options, first_registration)
|
return BlueprintSetupState(self, app, options, first_registration)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
|
def register_blueprint(self, blueprint: "Blueprint", **options: t.Any) -> None:
|
||||||
"""Register a :class:`~flask.Blueprint` on this blueprint. Keyword
|
"""Register a :class:`~flask.Blueprint` on this blueprint. Keyword
|
||||||
arguments passed to this method will override the defaults set
|
arguments passed to this method will override the defaults set
|
||||||
|
|
@ -390,11 +387,12 @@ class Blueprint(Scaffold):
|
||||||
bp_options["name_prefix"] = name
|
bp_options["name_prefix"] = name
|
||||||
blueprint.register(app, bp_options)
|
blueprint.register(app, bp_options)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def add_url_rule(
|
def add_url_rule(
|
||||||
self,
|
self,
|
||||||
rule: str,
|
rule: str,
|
||||||
endpoint: t.Optional[str] = None,
|
endpoint: t.Optional[str] = None,
|
||||||
view_func: t.Optional[t.Callable] = None,
|
view_func: t.Optional[ft.ViewCallable] = None,
|
||||||
provide_automatic_options: t.Optional[bool] = None,
|
provide_automatic_options: t.Optional[bool] = None,
|
||||||
**options: t.Any,
|
**options: t.Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -417,9 +415,10 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def app_template_filter(
|
def app_template_filter(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
|
) -> t.Callable[[ft.TemplateFilterCallable], ft.TemplateFilterCallable]:
|
||||||
"""Register a custom template filter, available application wide. Like
|
"""Register a custom template filter, available application wide. Like
|
||||||
:meth:`Flask.template_filter` but for a blueprint.
|
:meth:`Flask.template_filter` but for a blueprint.
|
||||||
|
|
||||||
|
|
@ -427,14 +426,15 @@ class Blueprint(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateFilterCallable) -> TemplateFilterCallable:
|
def decorator(f: ft.TemplateFilterCallable) -> ft.TemplateFilterCallable:
|
||||||
self.add_app_template_filter(f, name=name)
|
self.add_app_template_filter(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def add_app_template_filter(
|
def add_app_template_filter(
|
||||||
self, f: TemplateFilterCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateFilterCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template filter, available application wide. Like
|
"""Register a custom template filter, available application wide. Like
|
||||||
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly
|
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly
|
||||||
|
|
@ -449,9 +449,10 @@ class Blueprint(Scaffold):
|
||||||
|
|
||||||
self.record_once(register_template)
|
self.record_once(register_template)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def app_template_test(
|
def app_template_test(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
|
) -> t.Callable[[ft.TemplateTestCallable], ft.TemplateTestCallable]:
|
||||||
"""Register a custom template test, available application wide. Like
|
"""Register a custom template test, available application wide. Like
|
||||||
:meth:`Flask.template_test` but for a blueprint.
|
:meth:`Flask.template_test` but for a blueprint.
|
||||||
|
|
||||||
|
|
@ -461,14 +462,15 @@ class Blueprint(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateTestCallable) -> TemplateTestCallable:
|
def decorator(f: ft.TemplateTestCallable) -> ft.TemplateTestCallable:
|
||||||
self.add_app_template_test(f, name=name)
|
self.add_app_template_test(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def add_app_template_test(
|
def add_app_template_test(
|
||||||
self, f: TemplateTestCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateTestCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template test, available application wide. Like
|
"""Register a custom template test, available application wide. Like
|
||||||
:meth:`Flask.add_template_test` but for a blueprint. Works exactly
|
:meth:`Flask.add_template_test` but for a blueprint. Works exactly
|
||||||
|
|
@ -485,9 +487,10 @@ class Blueprint(Scaffold):
|
||||||
|
|
||||||
self.record_once(register_template)
|
self.record_once(register_template)
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def app_template_global(
|
def app_template_global(
|
||||||
self, name: t.Optional[str] = None
|
self, name: t.Optional[str] = None
|
||||||
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
|
) -> t.Callable[[ft.TemplateGlobalCallable], ft.TemplateGlobalCallable]:
|
||||||
"""Register a custom template global, available application wide. Like
|
"""Register a custom template global, available application wide. Like
|
||||||
:meth:`Flask.template_global` but for a blueprint.
|
:meth:`Flask.template_global` but for a blueprint.
|
||||||
|
|
||||||
|
|
@ -497,14 +500,15 @@ class Blueprint(Scaffold):
|
||||||
function name will be used.
|
function name will be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable:
|
def decorator(f: ft.TemplateGlobalCallable) -> ft.TemplateGlobalCallable:
|
||||||
self.add_app_template_global(f, name=name)
|
self.add_app_template_global(f, name=name)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def add_app_template_global(
|
def add_app_template_global(
|
||||||
self, f: TemplateGlobalCallable, name: t.Optional[str] = None
|
self, f: ft.TemplateGlobalCallable, name: t.Optional[str] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a custom template global, available application wide. Like
|
"""Register a custom template global, available application wide. Like
|
||||||
:meth:`Flask.add_template_global` but for a blueprint. Works exactly
|
:meth:`Flask.add_template_global` but for a blueprint. Works exactly
|
||||||
|
|
@ -521,7 +525,10 @@ class Blueprint(Scaffold):
|
||||||
|
|
||||||
self.record_once(register_template)
|
self.record_once(register_template)
|
||||||
|
|
||||||
def before_app_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
|
@setupmethod
|
||||||
|
def before_app_request(
|
||||||
|
self, f: ft.BeforeRequestCallable
|
||||||
|
) -> ft.BeforeRequestCallable:
|
||||||
"""Like :meth:`Flask.before_request`. Such a function is executed
|
"""Like :meth:`Flask.before_request`. Such a function is executed
|
||||||
before each request, even if outside of a blueprint.
|
before each request, even if outside of a blueprint.
|
||||||
"""
|
"""
|
||||||
|
|
@ -530,16 +537,30 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def before_app_first_request(
|
def before_app_first_request(
|
||||||
self, f: BeforeFirstRequestCallable
|
self, f: ft.BeforeFirstRequestCallable
|
||||||
) -> BeforeFirstRequestCallable:
|
) -> ft.BeforeFirstRequestCallable:
|
||||||
"""Like :meth:`Flask.before_first_request`. Such a function is
|
"""Like :meth:`Flask.before_first_request`. Such a function is
|
||||||
executed before the first request to the application.
|
executed before the first request to the application.
|
||||||
|
|
||||||
|
.. deprecated:: 2.2
|
||||||
|
Will be removed in Flask 2.3. Run setup code when creating
|
||||||
|
the application instead.
|
||||||
"""
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"'before_app_first_request' is deprecated and will be"
|
||||||
|
" removed in Flask 2.3. Use 'record_once' instead to run"
|
||||||
|
" setup code when registering the blueprint.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
|
def after_app_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
|
||||||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
||||||
is executed after each request, even if outside of the blueprint.
|
is executed after each request, even if outside of the blueprint.
|
||||||
"""
|
"""
|
||||||
|
|
@ -548,7 +569,8 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def teardown_app_request(self, f: TeardownCallable) -> TeardownCallable:
|
@setupmethod
|
||||||
|
def teardown_app_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable:
|
||||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
||||||
function is executed when tearing down each request, even if outside of
|
function is executed when tearing down each request, even if outside of
|
||||||
the blueprint.
|
the blueprint.
|
||||||
|
|
@ -558,9 +580,10 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def app_context_processor(
|
def app_context_processor(
|
||||||
self, f: TemplateContextProcessorCallable
|
self, f: ft.TemplateContextProcessorCallable
|
||||||
) -> TemplateContextProcessorCallable:
|
) -> ft.TemplateContextProcessorCallable:
|
||||||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
||||||
function is executed each request, even if outside of the blueprint.
|
function is executed each request, even if outside of the blueprint.
|
||||||
"""
|
"""
|
||||||
|
|
@ -569,27 +592,32 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def app_errorhandler(self, code: t.Union[t.Type[Exception], int]) -> t.Callable:
|
@setupmethod
|
||||||
|
def app_errorhandler(
|
||||||
|
self, code: t.Union[t.Type[Exception], int]
|
||||||
|
) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]:
|
||||||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
||||||
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: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator:
|
||||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def app_url_value_preprocessor(
|
def app_url_value_preprocessor(
|
||||||
self, f: URLValuePreprocessorCallable
|
self, f: ft.URLValuePreprocessorCallable
|
||||||
) -> URLValuePreprocessorCallable:
|
) -> ft.URLValuePreprocessorCallable:
|
||||||
"""Same as :meth:`url_value_preprocessor` but application wide."""
|
"""Same as :meth:`url_value_preprocessor` but application wide."""
|
||||||
self.record_once(
|
self.record_once(
|
||||||
lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
|
lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f)
|
||||||
)
|
)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def app_url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable:
|
@setupmethod
|
||||||
|
def app_url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable:
|
||||||
"""Same as :meth:`url_defaults` but application wide."""
|
"""Same as :meth:`url_defaults` but application wide."""
|
||||||
self.record_once(
|
self.record_once(
|
||||||
lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
|
lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,6 @@ from .helpers import get_debug_flag
|
||||||
from .helpers import get_env
|
from .helpers import get_env
|
||||||
from .helpers import get_load_dotenv
|
from .helpers import get_load_dotenv
|
||||||
|
|
||||||
try:
|
|
||||||
import dotenv
|
|
||||||
except ImportError:
|
|
||||||
dotenv = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ssl
|
|
||||||
except ImportError:
|
|
||||||
ssl = None # type: ignore
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
from importlib import metadata
|
|
||||||
else:
|
|
||||||
# Use a backport on Python < 3.10.
|
|
||||||
#
|
|
||||||
# We technically have importlib.metadata on 3.8+,
|
|
||||||
# but the API changed in 3.10, so use the backport
|
|
||||||
# for consistency.
|
|
||||||
import importlib_metadata as metadata # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class NoAppException(click.UsageError):
|
class NoAppException(click.UsageError):
|
||||||
"""Raised if an application cannot be found or loaded."""
|
"""Raised if an application cannot be found or loaded."""
|
||||||
|
|
@ -513,6 +493,14 @@ class FlaskGroup(AppGroup):
|
||||||
if self._loaded_plugin_commands:
|
if self._loaded_plugin_commands:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
from importlib import metadata
|
||||||
|
else:
|
||||||
|
# Use a backport on Python < 3.10. We technically have
|
||||||
|
# importlib.metadata on 3.8+, but the API changed in 3.10,
|
||||||
|
# so use the backport for consistency.
|
||||||
|
import importlib_metadata as metadata
|
||||||
|
|
||||||
for ep in metadata.entry_points(group="flask.commands"):
|
for ep in metadata.entry_points(group="flask.commands"):
|
||||||
self.add_command(ep.load(), ep.name)
|
self.add_command(ep.load(), ep.name)
|
||||||
|
|
||||||
|
|
@ -608,7 +596,9 @@ def load_dotenv(path=None):
|
||||||
|
|
||||||
.. versionadded:: 1.0
|
.. versionadded:: 1.0
|
||||||
"""
|
"""
|
||||||
if dotenv is None:
|
try:
|
||||||
|
import dotenv
|
||||||
|
except ImportError:
|
||||||
if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
|
if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"):
|
||||||
click.secho(
|
click.secho(
|
||||||
" * Tip: There are .env or .flaskenv files present."
|
" * Tip: There are .env or .flaskenv files present."
|
||||||
|
|
@ -684,12 +674,14 @@ class CertParamType(click.ParamType):
|
||||||
self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
|
self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True)
|
||||||
|
|
||||||
def convert(self, value, param, ctx):
|
def convert(self, value, param, ctx):
|
||||||
if ssl is None:
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
raise click.BadParameter(
|
raise click.BadParameter(
|
||||||
'Using "--cert" requires Python to be compiled with SSL support.',
|
'Using "--cert" requires Python to be compiled with SSL support.',
|
||||||
ctx,
|
ctx,
|
||||||
param,
|
param,
|
||||||
)
|
) from None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.path_type(value, param, ctx)
|
return self.path_type(value, param, ctx)
|
||||||
|
|
@ -722,7 +714,13 @@ def _validate_key(ctx, param, value):
|
||||||
"""
|
"""
|
||||||
cert = ctx.params.get("cert")
|
cert = ctx.params.get("cert")
|
||||||
is_adhoc = cert == "adhoc"
|
is_adhoc = cert == "adhoc"
|
||||||
is_context = ssl and isinstance(cert, ssl.SSLContext)
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
|
is_context = False
|
||||||
|
else:
|
||||||
|
is_context = isinstance(cert, ssl.SSLContext)
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
if is_adhoc:
|
if is_adhoc:
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ from types import TracebackType
|
||||||
|
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
from .globals import _app_ctx_stack
|
from .globals import _app_ctx_stack
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
from .signals import appcontext_popped
|
from .signals import appcontext_popped
|
||||||
from .signals import appcontext_pushed
|
from .signals import appcontext_pushed
|
||||||
from .typing import AfterRequestCallable
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .sessions import SessionMixin
|
from .sessions import SessionMixin
|
||||||
from .wrappers import Request
|
from .wrappers import Request
|
||||||
|
|
@ -109,7 +109,7 @@ class _AppCtxGlobals:
|
||||||
return object.__repr__(self)
|
return object.__repr__(self)
|
||||||
|
|
||||||
|
|
||||||
def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable:
|
def after_this_request(f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
|
||||||
"""Executes a function after this request. This is useful to modify
|
"""Executes a function after this request. This is useful to modify
|
||||||
response objects. The function is passed the response object and has
|
response objects. The function is passed the response object and has
|
||||||
to return the same or a new one.
|
to return the same or a new one.
|
||||||
|
|
@ -341,7 +341,7 @@ class RequestContext:
|
||||||
# Functions that should be executed after the request on the response
|
# Functions that should be executed after the request on the response
|
||||||
# object. These will be called before the regular "after_request"
|
# object. These will be called before the regular "after_request"
|
||||||
# functions.
|
# functions.
|
||||||
self._after_request_functions: t.List[AfterRequestCallable] = []
|
self._after_request_functions: t.List[ft.AfterRequestCallable] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def g(self) -> _AppCtxGlobals:
|
def g(self) -> _AppCtxGlobals:
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from functools import partial
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
from werkzeug.local import LocalStack
|
from werkzeug.local import LocalStack
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .ctx import _AppCtxGlobals
|
from .ctx import _AppCtxGlobals
|
||||||
from .sessions import SessionMixin
|
from .sessions import SessionMixin
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@ from functools import update_wrapper
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
import werkzeug.utils
|
import werkzeug.utils
|
||||||
from werkzeug.routing import BuildError
|
from werkzeug.exceptions import abort as _wz_abort
|
||||||
from werkzeug.urls import url_quote
|
from werkzeug.utils import redirect as _wz_redirect
|
||||||
|
|
||||||
from .globals import _app_ctx_stack
|
|
||||||
from .globals import _request_ctx_stack
|
from .globals import _request_ctx_stack
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .globals import request
|
from .globals import request
|
||||||
from .globals import session
|
from .globals import session
|
||||||
from .signals import message_flashed
|
from .signals import message_flashed
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
|
from werkzeug.wrappers import Response as BaseResponse
|
||||||
from .wrappers import Response
|
from .wrappers import Response
|
||||||
|
import typing_extensions as te
|
||||||
|
|
||||||
|
|
||||||
def get_env() -> str:
|
def get_env() -> str:
|
||||||
|
|
@ -189,155 +190,107 @@ def make_response(*args: t.Any) -> "Response":
|
||||||
return current_app.make_response(args) # type: ignore
|
return current_app.make_response(args) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def url_for(endpoint: str, **values: t.Any) -> str:
|
def url_for(
|
||||||
"""Generates a URL to the given endpoint with the method provided.
|
endpoint: str,
|
||||||
|
*,
|
||||||
|
_anchor: t.Optional[str] = None,
|
||||||
|
_method: t.Optional[str] = None,
|
||||||
|
_scheme: t.Optional[str] = None,
|
||||||
|
_external: t.Optional[bool] = None,
|
||||||
|
**values: t.Any,
|
||||||
|
) -> str:
|
||||||
|
"""Generate a URL to the given endpoint with the given values.
|
||||||
|
|
||||||
Variable arguments that are unknown to the target endpoint are appended
|
This requires an active request or application context, and calls
|
||||||
to the generated URL as query arguments. If the value of a query argument
|
:meth:`current_app.url_for() <flask.Flask.url_for>`. See that method
|
||||||
is ``None``, the whole pair is skipped. In case blueprints are active
|
for full documentation.
|
||||||
you can shortcut references to the same blueprint by prefixing the
|
|
||||||
local endpoint with a dot (``.``).
|
|
||||||
|
|
||||||
This will reference the index function local to the current blueprint::
|
:param endpoint: The endpoint name associated with the URL to
|
||||||
|
generate. If this starts with a ``.``, the current blueprint
|
||||||
|
name (if any) will be used.
|
||||||
|
:param _anchor: If given, append this as ``#anchor`` to the URL.
|
||||||
|
:param _method: If given, generate the URL associated with this
|
||||||
|
method for the endpoint.
|
||||||
|
:param _scheme: If given, the URL will have this scheme if it is
|
||||||
|
external.
|
||||||
|
:param _external: If given, prefer the URL to be internal (False) or
|
||||||
|
require it to be external (True). External URLs include the
|
||||||
|
scheme and domain. When not in an active request, URLs are
|
||||||
|
external by default.
|
||||||
|
:param values: Values to use for the variable parts of the URL rule.
|
||||||
|
Unknown keys are appended as query string arguments, like
|
||||||
|
``?a=b&c=d``.
|
||||||
|
|
||||||
url_for('.index')
|
.. versionchanged:: 2.2
|
||||||
|
Calls ``current_app.url_for``, allowing an app to override the
|
||||||
|
behavior.
|
||||||
|
|
||||||
See :ref:`url-building`.
|
.. versionchanged:: 0.10
|
||||||
|
The ``_scheme`` parameter was added.
|
||||||
|
|
||||||
Configuration values ``APPLICATION_ROOT`` and ``SERVER_NAME`` are only used when
|
.. versionchanged:: 0.9
|
||||||
generating URLs outside of a request context.
|
The ``_anchor`` and ``_method`` parameters were added.
|
||||||
|
|
||||||
To integrate applications, :class:`Flask` has a hook to intercept URL build
|
.. versionchanged:: 0.9
|
||||||
errors through :attr:`Flask.url_build_error_handlers`. The `url_for`
|
Calls ``app.handle_url_build_error`` on build errors.
|
||||||
function results in a :exc:`~werkzeug.routing.BuildError` when the current
|
|
||||||
app does not have a URL for the given endpoint and values. When it does, the
|
|
||||||
:data:`~flask.current_app` calls its :attr:`~Flask.url_build_error_handlers` if
|
|
||||||
it is not ``None``, which can return a string to use as the result of
|
|
||||||
`url_for` (instead of `url_for`'s default to raise the
|
|
||||||
:exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception.
|
|
||||||
An example::
|
|
||||||
|
|
||||||
def external_url_handler(error, endpoint, values):
|
|
||||||
"Looks up an external URL when `url_for` cannot build a URL."
|
|
||||||
# This is an example of hooking the build_error_handler.
|
|
||||||
# Here, lookup_url is some utility function you've built
|
|
||||||
# which looks up the endpoint in some external URL registry.
|
|
||||||
url = lookup_url(endpoint, **values)
|
|
||||||
if url is None:
|
|
||||||
# External lookup did not have a URL.
|
|
||||||
# Re-raise the BuildError, in context of original traceback.
|
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
|
||||||
if exc_value is error:
|
|
||||||
raise exc_type(exc_value).with_traceback(tb)
|
|
||||||
else:
|
|
||||||
raise error
|
|
||||||
# url_for will use this result, instead of raising BuildError.
|
|
||||||
return url
|
|
||||||
|
|
||||||
app.url_build_error_handlers.append(external_url_handler)
|
|
||||||
|
|
||||||
Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and
|
|
||||||
`endpoint` and `values` are the arguments passed into `url_for`. Note
|
|
||||||
that this is for building URLs outside the current application, and not for
|
|
||||||
handling 404 NotFound errors.
|
|
||||||
|
|
||||||
.. versionadded:: 0.10
|
|
||||||
The `_scheme` parameter was added.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
The `_anchor` and `_method` parameters were added.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
Calls :meth:`Flask.handle_build_error` on
|
|
||||||
:exc:`~werkzeug.routing.BuildError`.
|
|
||||||
|
|
||||||
:param endpoint: the endpoint of the URL (name of the function)
|
|
||||||
:param values: the variable arguments of the URL rule
|
|
||||||
:param _external: if set to ``True``, an absolute URL is generated. Server
|
|
||||||
address can be changed via ``SERVER_NAME`` configuration variable which
|
|
||||||
falls back to the `Host` header, then to the IP and port of the request.
|
|
||||||
:param _scheme: a string specifying the desired URL scheme. The `_external`
|
|
||||||
parameter must be set to ``True`` or a :exc:`ValueError` is raised. The default
|
|
||||||
behavior uses the same scheme as the current request, or
|
|
||||||
:data:`PREFERRED_URL_SCHEME` if no request context is available.
|
|
||||||
This also can be set to an empty string to build protocol-relative
|
|
||||||
URLs.
|
|
||||||
:param _anchor: if provided this is added as anchor to the URL.
|
|
||||||
:param _method: if provided this explicitly specifies an HTTP method.
|
|
||||||
"""
|
"""
|
||||||
appctx = _app_ctx_stack.top
|
return current_app.url_for(
|
||||||
reqctx = _request_ctx_stack.top
|
endpoint,
|
||||||
|
_anchor=_anchor,
|
||||||
|
_method=_method,
|
||||||
|
_scheme=_scheme,
|
||||||
|
_external=_external,
|
||||||
|
**values,
|
||||||
|
)
|
||||||
|
|
||||||
if appctx is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
"Attempted to generate a URL without the application context being"
|
|
||||||
" pushed. This has to be executed when application context is"
|
|
||||||
" available."
|
|
||||||
)
|
|
||||||
|
|
||||||
# If request specific information is available we have some extra
|
def redirect(
|
||||||
# features that support "relative" URLs.
|
location: str, code: int = 302, Response: t.Optional[t.Type["BaseResponse"]] = None
|
||||||
if reqctx is not None:
|
) -> "BaseResponse":
|
||||||
url_adapter = reqctx.url_adapter
|
"""Create a redirect response object.
|
||||||
blueprint_name = request.blueprint
|
|
||||||
|
|
||||||
if endpoint[:1] == ".":
|
If :data:`~flask.current_app` is available, it will use its
|
||||||
if blueprint_name is not None:
|
:meth:`~flask.Flask.redirect` method, otherwise it will use
|
||||||
endpoint = f"{blueprint_name}{endpoint}"
|
:func:`werkzeug.utils.redirect`.
|
||||||
else:
|
|
||||||
endpoint = endpoint[1:]
|
|
||||||
|
|
||||||
external = values.pop("_external", False)
|
:param location: The URL to redirect to.
|
||||||
|
:param code: The status code for the redirect.
|
||||||
|
:param Response: The response class to use. Not used when
|
||||||
|
``current_app`` is active, which uses ``app.response_class``.
|
||||||
|
|
||||||
# Otherwise go with the url adapter from the appctx and make
|
.. versionadded:: 2.2
|
||||||
# the URLs external by default.
|
Calls ``current_app.redirect`` if available instead of always
|
||||||
else:
|
using Werkzeug's default ``redirect``.
|
||||||
url_adapter = appctx.url_adapter
|
"""
|
||||||
|
if current_app:
|
||||||
|
return current_app.redirect(location, code=code)
|
||||||
|
|
||||||
if url_adapter is None:
|
return _wz_redirect(location, code=code, Response=Response)
|
||||||
raise RuntimeError(
|
|
||||||
"Application was not able to create a URL adapter for request"
|
|
||||||
" independent URL generation. You might be able to fix this by"
|
|
||||||
" setting the SERVER_NAME config variable."
|
|
||||||
)
|
|
||||||
|
|
||||||
external = values.pop("_external", True)
|
|
||||||
|
|
||||||
anchor = values.pop("_anchor", None)
|
def abort( # type: ignore[misc]
|
||||||
method = values.pop("_method", None)
|
code: t.Union[int, "BaseResponse"], *args: t.Any, **kwargs: t.Any
|
||||||
scheme = values.pop("_scheme", None)
|
) -> "te.NoReturn":
|
||||||
appctx.app.inject_url_defaults(endpoint, values)
|
"""Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given
|
||||||
|
status code.
|
||||||
|
|
||||||
# This is not the best way to deal with this but currently the
|
If :data:`~flask.current_app` is available, it will call its
|
||||||
# underlying Werkzeug router does not support overriding the scheme on
|
:attr:`~flask.Flask.aborter` object, otherwise it will use
|
||||||
# a per build call basis.
|
:func:`werkzeug.exceptions.abort`.
|
||||||
old_scheme = None
|
|
||||||
if scheme is not None:
|
|
||||||
if not external:
|
|
||||||
raise ValueError("When specifying _scheme, _external must be True")
|
|
||||||
old_scheme = url_adapter.url_scheme
|
|
||||||
url_adapter.url_scheme = scheme
|
|
||||||
|
|
||||||
try:
|
:param code: The status code for the exception, which must be
|
||||||
try:
|
registered in ``app.aborter``.
|
||||||
rv = url_adapter.build(
|
:param args: Passed to the exception.
|
||||||
endpoint, values, method=method, force_external=external
|
:param kwargs: Passed to the exception.
|
||||||
)
|
|
||||||
finally:
|
|
||||||
if old_scheme is not None:
|
|
||||||
url_adapter.url_scheme = old_scheme
|
|
||||||
except BuildError as error:
|
|
||||||
# We need to inject the values again so that the app callback can
|
|
||||||
# deal with that sort of stuff.
|
|
||||||
values["_external"] = external
|
|
||||||
values["_anchor"] = anchor
|
|
||||||
values["_method"] = method
|
|
||||||
values["_scheme"] = scheme
|
|
||||||
return appctx.app.handle_url_build_error(error, endpoint, values)
|
|
||||||
|
|
||||||
if anchor is not None:
|
.. versionadded:: 2.2
|
||||||
rv += f"#{url_quote(anchor)}"
|
Calls ``current_app.aborter`` if available instead of always
|
||||||
return rv
|
using Werkzeug's default ``abort``.
|
||||||
|
"""
|
||||||
|
if current_app:
|
||||||
|
current_app.aborter(code, *args, **kwargs)
|
||||||
|
|
||||||
|
_wz_abort(code, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_template_attribute(template_name: str, attribute: str) -> t.Any:
|
def get_template_attribute(template_name: str, attribute: str) -> t.Any:
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from werkzeug.http import http_date
|
||||||
from ..globals import current_app
|
from ..globals import current_app
|
||||||
from ..globals import request
|
from ..globals import request
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from ..app import Flask
|
from ..app import Flask
|
||||||
from ..wrappers import Response
|
from ..wrappers import Response
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ from werkzeug.local import LocalProxy
|
||||||
|
|
||||||
from .globals import request
|
from .globals import request
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
@ -12,23 +13,16 @@ from jinja2 import FileSystemLoader
|
||||||
from werkzeug.exceptions import default_exceptions
|
from werkzeug.exceptions import default_exceptions
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
from .cli import AppGroup
|
from .cli import AppGroup
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .helpers import get_root_path
|
from .helpers import get_root_path
|
||||||
from .helpers import locked_cached_property
|
from .helpers import locked_cached_property
|
||||||
from .helpers import send_from_directory
|
from .helpers import send_from_directory
|
||||||
from .templating import _default_template_ctx_processor
|
from .templating import _default_template_ctx_processor
|
||||||
from .typing import AfterRequestCallable
|
|
||||||
from .typing import AppOrBlueprintKey
|
|
||||||
from .typing import BeforeRequestCallable
|
|
||||||
from .typing import TeardownCallable
|
|
||||||
from .typing import TemplateContextProcessorCallable
|
|
||||||
from .typing import URLDefaultCallable
|
|
||||||
from .typing import URLValuePreprocessorCallable
|
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
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()
|
||||||
|
|
@ -37,22 +31,10 @@ F = t.TypeVar("F", bound=t.Callable[..., t.Any])
|
||||||
|
|
||||||
|
|
||||||
def setupmethod(f: F) -> F:
|
def setupmethod(f: F) -> F:
|
||||||
"""Wraps a method so that it performs a check in debug mode if the
|
f_name = f.__name__
|
||||||
first request was already handled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
def wrapper_func(self, *args: t.Any, **kwargs: t.Any) -> t.Any:
|
||||||
if self._is_setup_finished():
|
self._check_setup_finished(f_name)
|
||||||
raise AssertionError(
|
|
||||||
"A setup function was called after the first request "
|
|
||||||
"was handled. This usually indicates a bug in the"
|
|
||||||
" application where a module was not imported and"
|
|
||||||
" decorators or other functionality was called too"
|
|
||||||
" late.\nTo fix this make sure to import all your view"
|
|
||||||
" modules, database models, and everything related at a"
|
|
||||||
" central place before the application starts serving"
|
|
||||||
" requests."
|
|
||||||
)
|
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
|
|
||||||
return t.cast(F, update_wrapper(wrapper_func, f))
|
return t.cast(F, update_wrapper(wrapper_func, f))
|
||||||
|
|
@ -130,7 +112,7 @@ class Scaffold:
|
||||||
self.view_functions: t.Dict[str, t.Callable] = {}
|
self.view_functions: t.Dict[str, t.Callable] = {}
|
||||||
|
|
||||||
#: A data structure of registered error handlers, in the format
|
#: A data structure of registered error handlers, in the format
|
||||||
#: ``{scope: {code: {class: handler}}}```. The ``scope`` key is
|
#: ``{scope: {code: {class: handler}}}``. The ``scope`` key is
|
||||||
#: the name of a blueprint the handlers are active for, or
|
#: the name of a blueprint the handlers are active for, or
|
||||||
#: ``None`` for all requests. The ``code`` key is the HTTP
|
#: ``None`` for all requests. The ``code`` key is the HTTP
|
||||||
#: status code for ``HTTPException``, or ``None`` for
|
#: status code for ``HTTPException``, or ``None`` for
|
||||||
|
|
@ -143,8 +125,8 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: 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,
|
ft.AppOrBlueprintKey,
|
||||||
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], "ErrorHandlerCallable"]],
|
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ft.ErrorHandlerCallable]],
|
||||||
] = 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
|
||||||
|
|
@ -158,7 +140,7 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.before_request_funcs: t.Dict[
|
self.before_request_funcs: t.Dict[
|
||||||
AppOrBlueprintKey, t.List[BeforeRequestCallable]
|
ft.AppOrBlueprintKey, t.List[ft.BeforeRequestCallable]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
|
|
||||||
#: A data structure of functions to call at the end of each
|
#: A data structure of functions to call at the end of each
|
||||||
|
|
@ -172,7 +154,7 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.after_request_funcs: t.Dict[
|
self.after_request_funcs: t.Dict[
|
||||||
AppOrBlueprintKey, t.List[AfterRequestCallable]
|
ft.AppOrBlueprintKey, t.List[ft.AfterRequestCallable]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
|
|
||||||
#: A data structure of functions to call at the end of each
|
#: A data structure of functions to call at the end of each
|
||||||
|
|
@ -187,7 +169,7 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.teardown_request_funcs: t.Dict[
|
self.teardown_request_funcs: t.Dict[
|
||||||
AppOrBlueprintKey, t.List[TeardownCallable]
|
ft.AppOrBlueprintKey, t.List[ft.TeardownCallable]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
|
|
||||||
#: A data structure of functions to call to pass extra context
|
#: A data structure of functions to call to pass extra context
|
||||||
|
|
@ -202,7 +184,7 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.template_context_processors: t.Dict[
|
self.template_context_processors: t.Dict[
|
||||||
AppOrBlueprintKey, t.List[TemplateContextProcessorCallable]
|
ft.AppOrBlueprintKey, t.List[ft.TemplateContextProcessorCallable]
|
||||||
] = defaultdict(list, {None: [_default_template_ctx_processor]})
|
] = defaultdict(list, {None: [_default_template_ctx_processor]})
|
||||||
|
|
||||||
#: A data structure of functions to call to modify the keyword
|
#: A data structure of functions to call to modify the keyword
|
||||||
|
|
@ -217,8 +199,8 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.url_value_preprocessors: t.Dict[
|
self.url_value_preprocessors: t.Dict[
|
||||||
AppOrBlueprintKey,
|
ft.AppOrBlueprintKey,
|
||||||
t.List[URLValuePreprocessorCallable],
|
t.List[ft.URLValuePreprocessorCallable],
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
|
|
||||||
#: A data structure of functions to call to modify the keyword
|
#: A data structure of functions to call to modify the keyword
|
||||||
|
|
@ -233,13 +215,13 @@ class Scaffold:
|
||||||
#: This data structure is internal. It should not be modified
|
#: This data structure is internal. It should not be modified
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.url_default_functions: t.Dict[
|
self.url_default_functions: t.Dict[
|
||||||
AppOrBlueprintKey, t.List[URLDefaultCallable]
|
ft.AppOrBlueprintKey, t.List[ft.URLDefaultCallable]
|
||||||
] = defaultdict(list)
|
] = defaultdict(list)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<{type(self).__name__} {self.name!r}>"
|
return f"<{type(self).__name__} {self.name!r}>"
|
||||||
|
|
||||||
def _is_setup_finished(self) -> bool:
|
def _check_setup_finished(self, f_name: str) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -370,48 +352,66 @@ class Scaffold:
|
||||||
method: str,
|
method: str,
|
||||||
rule: str,
|
rule: str,
|
||||||
options: dict,
|
options: dict,
|
||||||
) -> t.Callable[[F], F]:
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
if "methods" in options:
|
if "methods" in options:
|
||||||
raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
|
raise TypeError("Use the 'route' decorator to use the 'methods' argument.")
|
||||||
|
|
||||||
return self.route(rule, methods=[method], **options)
|
return self.route(rule, methods=[method], **options)
|
||||||
|
|
||||||
def get(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def get(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Shortcut for :meth:`route` with ``methods=["GET"]``.
|
"""Shortcut for :meth:`route` with ``methods=["GET"]``.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return self._method_route("GET", rule, options)
|
return self._method_route("GET", rule, options)
|
||||||
|
|
||||||
def post(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def post(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Shortcut for :meth:`route` with ``methods=["POST"]``.
|
"""Shortcut for :meth:`route` with ``methods=["POST"]``.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return self._method_route("POST", rule, options)
|
return self._method_route("POST", rule, options)
|
||||||
|
|
||||||
def put(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def put(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Shortcut for :meth:`route` with ``methods=["PUT"]``.
|
"""Shortcut for :meth:`route` with ``methods=["PUT"]``.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return self._method_route("PUT", rule, options)
|
return self._method_route("PUT", rule, options)
|
||||||
|
|
||||||
def delete(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def delete(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Shortcut for :meth:`route` with ``methods=["DELETE"]``.
|
"""Shortcut for :meth:`route` with ``methods=["DELETE"]``.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return self._method_route("DELETE", rule, options)
|
return self._method_route("DELETE", rule, options)
|
||||||
|
|
||||||
def patch(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def patch(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Shortcut for :meth:`route` with ``methods=["PATCH"]``.
|
"""Shortcut for :meth:`route` with ``methods=["PATCH"]``.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
return self._method_route("PATCH", rule, options)
|
return self._method_route("PATCH", rule, options)
|
||||||
|
|
||||||
def route(self, rule: str, **options: t.Any) -> t.Callable[[F], F]:
|
@setupmethod
|
||||||
|
def route(
|
||||||
|
self, rule: str, **options: t.Any
|
||||||
|
) -> t.Callable[[ft.RouteDecorator], ft.RouteDecorator]:
|
||||||
"""Decorate a view function to register it with the given URL
|
"""Decorate a view function to register it with the given URL
|
||||||
rule and options. Calls :meth:`add_url_rule`, which has more
|
rule and options. Calls :meth:`add_url_rule`, which has more
|
||||||
details about the implementation.
|
details about the implementation.
|
||||||
|
|
@ -435,7 +435,7 @@ class Scaffold:
|
||||||
:class:`~werkzeug.routing.Rule` object.
|
:class:`~werkzeug.routing.Rule` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: F) -> F:
|
def decorator(f: ft.RouteDecorator) -> ft.RouteDecorator:
|
||||||
endpoint = options.pop("endpoint", None)
|
endpoint = options.pop("endpoint", None)
|
||||||
self.add_url_rule(rule, endpoint, f, **options)
|
self.add_url_rule(rule, endpoint, f, **options)
|
||||||
return f
|
return f
|
||||||
|
|
@ -447,7 +447,7 @@ class Scaffold:
|
||||||
self,
|
self,
|
||||||
rule: str,
|
rule: str,
|
||||||
endpoint: t.Optional[str] = None,
|
endpoint: t.Optional[str] = None,
|
||||||
view_func: t.Optional[t.Callable] = None,
|
view_func: t.Optional[ft.ViewCallable] = None,
|
||||||
provide_automatic_options: t.Optional[bool] = None,
|
provide_automatic_options: t.Optional[bool] = None,
|
||||||
**options: t.Any,
|
**options: t.Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -510,6 +510,7 @@ class Scaffold:
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@setupmethod
|
||||||
def endpoint(self, endpoint: str) -> t.Callable:
|
def endpoint(self, endpoint: str) -> t.Callable:
|
||||||
"""Decorate a view function to register it for the given
|
"""Decorate a view function to register it for the given
|
||||||
endpoint. Used if a rule is added without a ``view_func`` with
|
endpoint. Used if a rule is added without a ``view_func`` with
|
||||||
|
|
@ -534,7 +535,7 @@ class Scaffold:
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def before_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
|
def before_request(self, f: ft.BeforeRequestCallable) -> ft.BeforeRequestCallable:
|
||||||
"""Register a function to run before each request.
|
"""Register a function to run before each request.
|
||||||
|
|
||||||
For example, this can be used to open a database connection, or
|
For example, this can be used to open a database connection, or
|
||||||
|
|
@ -556,7 +557,7 @@ class Scaffold:
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def after_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
|
def after_request(self, f: ft.AfterRequestCallable) -> ft.AfterRequestCallable:
|
||||||
"""Register a function to run after each request to this object.
|
"""Register a function to run after each request to this object.
|
||||||
|
|
||||||
The function is called with the response object, and must return
|
The function is called with the response object, and must return
|
||||||
|
|
@ -572,7 +573,7 @@ class Scaffold:
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def teardown_request(self, f: TeardownCallable) -> TeardownCallable:
|
def teardown_request(self, f: ft.TeardownCallable) -> ft.TeardownCallable:
|
||||||
"""Register a function to be run at the end of each request,
|
"""Register a function to be run at the end of each request,
|
||||||
regardless of whether there was an exception or not. These functions
|
regardless of whether there was an exception or not. These functions
|
||||||
are executed when the request context is popped, even if not an
|
are executed when the request context is popped, even if not an
|
||||||
|
|
@ -612,16 +613,16 @@ class Scaffold:
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def context_processor(
|
def context_processor(
|
||||||
self, f: TemplateContextProcessorCallable
|
self, f: ft.TemplateContextProcessorCallable
|
||||||
) -> TemplateContextProcessorCallable:
|
) -> ft.TemplateContextProcessorCallable:
|
||||||
"""Registers a template context processor function."""
|
"""Registers a template context processor function."""
|
||||||
self.template_context_processors[None].append(f)
|
self.template_context_processors[None].append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def url_value_preprocessor(
|
def url_value_preprocessor(
|
||||||
self, f: URLValuePreprocessorCallable
|
self, f: ft.URLValuePreprocessorCallable
|
||||||
) -> URLValuePreprocessorCallable:
|
) -> ft.URLValuePreprocessorCallable:
|
||||||
"""Register a URL value preprocessor function for all view
|
"""Register a URL value preprocessor function for all view
|
||||||
functions in the application. These functions will be called before the
|
functions in the application. These functions will be called before the
|
||||||
:meth:`before_request` functions.
|
:meth:`before_request` functions.
|
||||||
|
|
@ -638,7 +639,7 @@ class Scaffold:
|
||||||
return f
|
return f
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def url_defaults(self, f: URLDefaultCallable) -> URLDefaultCallable:
|
def url_defaults(self, f: ft.URLDefaultCallable) -> ft.URLDefaultCallable:
|
||||||
"""Callback function for URL defaults for all view functions of the
|
"""Callback function for URL defaults for all view functions of the
|
||||||
application. It's called with the endpoint and values and should
|
application. It's called with the endpoint and values and should
|
||||||
update the values passed in place.
|
update the values passed in place.
|
||||||
|
|
@ -649,7 +650,7 @@ 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[Exception], int]
|
||||||
) -> t.Callable[["ErrorHandlerCallable"], "ErrorHandlerCallable"]:
|
) -> t.Callable[[ft.ErrorHandlerDecorator], ft.ErrorHandlerDecorator]:
|
||||||
"""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
|
||||||
|
|
@ -679,7 +680,7 @@ class Scaffold:
|
||||||
an arbitrary exception
|
an arbitrary exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: "ErrorHandlerCallable") -> "ErrorHandlerCallable":
|
def decorator(f: ft.ErrorHandlerDecorator) -> ft.ErrorHandlerDecorator:
|
||||||
self.register_error_handler(code_or_exception, f)
|
self.register_error_handler(code_or_exception, f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -689,7 +690,7 @@ class Scaffold:
|
||||||
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[Exception], int],
|
||||||
f: "ErrorHandlerCallable",
|
f: ft.ErrorHandlerCallable,
|
||||||
) -> 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
|
||||||
|
|
@ -697,22 +698,7 @@ class Scaffold:
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
.. versionadded:: 0.7
|
||||||
"""
|
"""
|
||||||
if isinstance(code_or_exception, HTTPException): # old broken behavior
|
exc_class, code = self._get_exc_class_and_code(code_or_exception)
|
||||||
raise ValueError(
|
|
||||||
"Tried to register a handler for an exception instance"
|
|
||||||
f" {code_or_exception!r}. Handlers can only be"
|
|
||||||
" registered for exception classes or HTTP error codes."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
exc_class, code = self._get_exc_class_and_code(code_or_exception)
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError(
|
|
||||||
f"'{code_or_exception}' is not a recognized HTTP error"
|
|
||||||
" code. Use a subclass of HTTPException with that code"
|
|
||||||
" instead."
|
|
||||||
) from None
|
|
||||||
|
|
||||||
self.error_handler_spec[None][code][exc_class] = f
|
self.error_handler_spec[None][code][exc_class] = f
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -727,14 +713,32 @@ class Scaffold:
|
||||||
code as an integer.
|
code as an integer.
|
||||||
"""
|
"""
|
||||||
exc_class: t.Type[Exception]
|
exc_class: t.Type[Exception]
|
||||||
|
|
||||||
if isinstance(exc_class_or_code, int):
|
if isinstance(exc_class_or_code, int):
|
||||||
exc_class = default_exceptions[exc_class_or_code]
|
try:
|
||||||
|
exc_class = default_exceptions[exc_class_or_code]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(
|
||||||
|
f"'{exc_class_or_code}' is not a recognized HTTP"
|
||||||
|
" error code. Use a subclass of HTTPException with"
|
||||||
|
" that code instead."
|
||||||
|
) from None
|
||||||
else:
|
else:
|
||||||
exc_class = exc_class_or_code
|
exc_class = exc_class_or_code
|
||||||
|
|
||||||
assert issubclass(
|
if isinstance(exc_class, Exception):
|
||||||
exc_class, Exception
|
raise TypeError(
|
||||||
), "Custom exceptions must be subclasses of Exception."
|
f"{exc_class!r} is an instance, not a class. Handlers"
|
||||||
|
" can only be registered for Exception classes or HTTP"
|
||||||
|
" error codes."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not issubclass(exc_class, Exception):
|
||||||
|
raise ValueError(
|
||||||
|
f"'{exc_class.__name__}' is not a subclass of Exception."
|
||||||
|
" Handlers can only be registered for Exception classes"
|
||||||
|
" or HTTP error codes."
|
||||||
|
)
|
||||||
|
|
||||||
if issubclass(exc_class, HTTPException):
|
if issubclass(exc_class, HTTPException):
|
||||||
return exc_class, exc_class.code
|
return exc_class, exc_class.code
|
||||||
|
|
@ -775,30 +779,55 @@ def _matching_loader_thinks_module_is_package(loader, mod_name):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _find_package_path(root_mod_name):
|
def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool:
|
||||||
"""Find the path that contains the package or module."""
|
# Path.is_relative_to doesn't exist until Python 3.9
|
||||||
try:
|
try:
|
||||||
spec = importlib.util.find_spec(root_mod_name)
|
path.relative_to(base)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
if spec is None:
|
|
||||||
|
def _find_package_path(import_name):
|
||||||
|
"""Find the path that contains the package or module."""
|
||||||
|
root_mod_name, _, _ = import_name.partition(".")
|
||||||
|
|
||||||
|
try:
|
||||||
|
root_spec = importlib.util.find_spec(root_mod_name)
|
||||||
|
|
||||||
|
if root_spec is None:
|
||||||
raise ValueError("not found")
|
raise ValueError("not found")
|
||||||
# ImportError: the machinery told us it does not exist
|
# ImportError: the machinery told us it does not exist
|
||||||
# ValueError:
|
# ValueError:
|
||||||
# - the module name was invalid
|
# - the module name was invalid
|
||||||
# - the module name is __main__
|
# - the module name is __main__
|
||||||
# - *we* raised `ValueError` due to `spec` being `None`
|
# - *we* raised `ValueError` due to `root_spec` being `None`
|
||||||
except (ImportError, ValueError):
|
except (ImportError, ValueError):
|
||||||
pass # handled below
|
pass # handled below
|
||||||
else:
|
else:
|
||||||
# namespace package
|
# namespace package
|
||||||
if spec.origin in {"namespace", None}:
|
if root_spec.origin in {"namespace", None}:
|
||||||
return os.path.dirname(next(iter(spec.submodule_search_locations)))
|
package_spec = importlib.util.find_spec(import_name)
|
||||||
|
if package_spec is not None and package_spec.submodule_search_locations:
|
||||||
|
# Pick the path in the namespace that contains the submodule.
|
||||||
|
package_path = pathlib.Path(
|
||||||
|
os.path.commonpath(package_spec.submodule_search_locations)
|
||||||
|
)
|
||||||
|
search_locations = (
|
||||||
|
location
|
||||||
|
for location in root_spec.submodule_search_locations
|
||||||
|
if _path_is_relative_to(package_path, location)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Pick the first path.
|
||||||
|
search_locations = iter(root_spec.submodule_search_locations)
|
||||||
|
return os.path.dirname(next(search_locations))
|
||||||
# a package (with __init__.py)
|
# a package (with __init__.py)
|
||||||
elif spec.submodule_search_locations:
|
elif root_spec.submodule_search_locations:
|
||||||
return os.path.dirname(os.path.dirname(spec.origin))
|
return os.path.dirname(os.path.dirname(root_spec.origin))
|
||||||
# just a normal module
|
# just a normal module
|
||||||
else:
|
else:
|
||||||
return os.path.dirname(spec.origin)
|
return os.path.dirname(root_spec.origin)
|
||||||
|
|
||||||
# we were unable to find the `package_path` using PEP 451 loaders
|
# we were unable to find the `package_path` using PEP 451 loaders
|
||||||
loader = pkgutil.get_loader(root_mod_name)
|
loader = pkgutil.get_loader(root_mod_name)
|
||||||
|
|
@ -840,12 +869,11 @@ def find_package(import_name: str):
|
||||||
for import. If the package is not installed, it's assumed that the
|
for import. If the package is not installed, it's assumed that the
|
||||||
package was imported from the current working directory.
|
package was imported from the current working directory.
|
||||||
"""
|
"""
|
||||||
root_mod_name, _, _ = import_name.partition(".")
|
package_path = _find_package_path(import_name)
|
||||||
package_path = _find_package_path(root_mod_name)
|
|
||||||
py_prefix = os.path.abspath(sys.prefix)
|
py_prefix = os.path.abspath(sys.prefix)
|
||||||
|
|
||||||
# installed to the system
|
# installed to the system
|
||||||
if package_path.startswith(py_prefix):
|
if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix):
|
||||||
return py_prefix, package_path
|
return py_prefix, package_path
|
||||||
|
|
||||||
site_parent, site_folder = os.path.split(package_path)
|
site_parent, site_folder = os.path.split(package_path)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from werkzeug.datastructures import CallbackDict
|
||||||
from .helpers import is_ip
|
from .helpers import is_ip
|
||||||
from .json.tag import TaggedJSONSerializer
|
from .json.tag import TaggedJSONSerializer
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
import typing_extensions as te
|
import typing_extensions as te
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .wrappers import Request, Response
|
from .wrappers import Request, Response
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from .globals import _request_ctx_stack
|
||||||
from .signals import before_render_template
|
from .signals import before_render_template
|
||||||
from .signals import template_rendered
|
from .signals import template_rendered
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .scaffold import Scaffold
|
from .scaffold import Scaffold
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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: # pragma: no cover
|
||||||
from werkzeug.test import TestResponse
|
from werkzeug.test import TestResponse
|
||||||
|
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,40 @@
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
if t.TYPE_CHECKING:
|
|
||||||
from _typeshed.wsgi import WSGIApplication # noqa: F401
|
from _typeshed.wsgi import WSGIApplication # noqa: F401
|
||||||
from werkzeug.datastructures import Headers # noqa: F401
|
from werkzeug.datastructures import Headers # noqa: F401
|
||||||
from werkzeug.wrappers.response import Response # noqa: F401
|
from werkzeug.wrappers import Response # noqa: F401
|
||||||
|
|
||||||
# The possible types that are directly convertible or are a Response object.
|
# The possible types that are directly convertible or are a Response object.
|
||||||
ResponseValue = t.Union[
|
ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]]
|
||||||
"Response",
|
|
||||||
str,
|
|
||||||
bytes,
|
|
||||||
t.Dict[str, t.Any], # any jsonify-able dict
|
|
||||||
t.Iterator[str],
|
|
||||||
t.Iterator[bytes],
|
|
||||||
]
|
|
||||||
StatusCode = int
|
|
||||||
|
|
||||||
# the possible types for an individual HTTP header
|
# the possible types for an individual HTTP header
|
||||||
HeaderName = str
|
# This should be a Union, but mypy doesn't pass unless it's a TypeVar.
|
||||||
HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]]
|
HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]]
|
||||||
|
|
||||||
# the possible types for HTTP headers
|
# the possible types for HTTP headers
|
||||||
HeadersValue = t.Union[
|
HeadersValue = t.Union[
|
||||||
"Headers", t.Dict[HeaderName, HeaderValue], t.List[t.Tuple[HeaderName, HeaderValue]]
|
"Headers",
|
||||||
|
t.Mapping[str, HeaderValue],
|
||||||
|
t.Sequence[t.Tuple[str, HeaderValue]],
|
||||||
]
|
]
|
||||||
|
|
||||||
# The possible types returned by a route function.
|
# The possible types returned by a route function.
|
||||||
ResponseReturnValue = t.Union[
|
ResponseReturnValue = t.Union[
|
||||||
ResponseValue,
|
ResponseValue,
|
||||||
t.Tuple[ResponseValue, HeadersValue],
|
t.Tuple[ResponseValue, HeadersValue],
|
||||||
t.Tuple[ResponseValue, StatusCode],
|
t.Tuple[ResponseValue, int],
|
||||||
t.Tuple[ResponseValue, StatusCode, HeadersValue],
|
t.Tuple[ResponseValue, int, HeadersValue],
|
||||||
"WSGIApplication",
|
"WSGIApplication",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Allow any subclass of werkzeug.Response, such as the one from Flask,
|
||||||
|
# as a callback argument. Using werkzeug.Response directly makes a
|
||||||
|
# callback annotated with flask.Response fail type checking.
|
||||||
|
ResponseClass = t.TypeVar("ResponseClass", bound="Response")
|
||||||
|
|
||||||
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[[ResponseClass], ResponseClass]
|
||||||
BeforeFirstRequestCallable = t.Callable[[], None]
|
BeforeFirstRequestCallable = t.Callable[[], None]
|
||||||
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
|
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
|
||||||
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
|
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
|
||||||
|
|
@ -46,6 +44,7 @@ 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]
|
||||||
|
|
||||||
# This should take Exception, but that either breaks typing the argument
|
# This should take Exception, but that either breaks typing the argument
|
||||||
# with a specific exception, or decorating multiple times with different
|
# with a specific exception, or decorating multiple times with different
|
||||||
# exceptions (and using a union type on the argument).
|
# exceptions (and using a union type on the argument).
|
||||||
|
|
@ -53,3 +52,7 @@ URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], N
|
||||||
# https://github.com/pallets/flask/issues/4295
|
# https://github.com/pallets/flask/issues/4295
|
||||||
# https://github.com/pallets/flask/issues/4297
|
# https://github.com/pallets/flask/issues/4297
|
||||||
ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue]
|
ErrorHandlerCallable = t.Callable[[t.Any], ResponseReturnValue]
|
||||||
|
ErrorHandlerDecorator = t.TypeVar("ErrorHandlerDecorator", bound=ErrorHandlerCallable)
|
||||||
|
|
||||||
|
ViewCallable = t.Callable[..., ResponseReturnValue]
|
||||||
|
RouteDecorator = t.TypeVar("RouteDecorator", bound=ViewCallable)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
|
from . import typing as ft
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .globals import request
|
from .globals import request
|
||||||
from .typing import ResponseReturnValue
|
|
||||||
|
|
||||||
|
|
||||||
http_method_funcs = frozenset(
|
http_method_funcs = frozenset(
|
||||||
|
|
@ -11,77 +11,106 @@ http_method_funcs = frozenset(
|
||||||
|
|
||||||
|
|
||||||
class View:
|
class View:
|
||||||
"""Alternative way to use view functions. A subclass has to implement
|
"""Subclass this class and override :meth:`dispatch_request` to
|
||||||
:meth:`dispatch_request` which is called with the view arguments from
|
create a generic class-based view. Call :meth:`as_view` to create a
|
||||||
the URL routing system. If :attr:`methods` is provided the methods
|
view function that creates an instance of the class with the given
|
||||||
do not have to be passed to the :meth:`~flask.Flask.add_url_rule`
|
arguments and calls its ``dispatch_request`` method with any URL
|
||||||
method explicitly::
|
variables.
|
||||||
|
|
||||||
class MyView(View):
|
See :doc:`views` for a detailed guide.
|
||||||
methods = ['GET']
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class Hello(View):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
def dispatch_request(self, name):
|
def dispatch_request(self, name):
|
||||||
return f"Hello {name}!"
|
return f"Hello, {name}!"
|
||||||
|
|
||||||
app.add_url_rule('/hello/<name>', view_func=MyView.as_view('myview'))
|
app.add_url_rule(
|
||||||
|
"/hello/<name>", view_func=Hello.as_view("hello")
|
||||||
|
)
|
||||||
|
|
||||||
When you want to decorate a pluggable view you will have to either do that
|
Set :attr:`methods` on the class to change what methods the view
|
||||||
when the view function is created (by wrapping the return value of
|
accepts.
|
||||||
:meth:`as_view`) or you can use the :attr:`decorators` attribute::
|
|
||||||
|
|
||||||
class SecretView(View):
|
Set :attr:`decorators` on the class to apply a list of decorators to
|
||||||
methods = ['GET']
|
the generated view function. Decorators applied to the class itself
|
||||||
decorators = [superuser_required]
|
will not be applied to the generated view function!
|
||||||
|
|
||||||
def dispatch_request(self):
|
Set :attr:`init_every_request` to ``False`` for efficiency, unless
|
||||||
...
|
you need to store request-global data on ``self``.
|
||||||
|
|
||||||
The decorators stored in the decorators list are applied one after another
|
|
||||||
when the view function is created. Note that you can *not* use the class
|
|
||||||
based decorators since those would decorate the view class and not the
|
|
||||||
generated view function!
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: A list of methods this view can handle.
|
#: The methods this view is registered for. Uses the same default
|
||||||
methods: t.Optional[t.List[str]] = None
|
#: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and
|
||||||
|
#: ``add_url_rule`` by default.
|
||||||
|
methods: t.ClassVar[t.Optional[t.Collection[str]]] = None
|
||||||
|
|
||||||
#: Setting this disables or force-enables the automatic options handling.
|
#: Control whether the ``OPTIONS`` method is handled automatically.
|
||||||
provide_automatic_options: t.Optional[bool] = None
|
#: Uses the same default (``True``) as ``route`` and
|
||||||
|
#: ``add_url_rule`` by default.
|
||||||
|
provide_automatic_options: t.ClassVar[t.Optional[bool]] = None
|
||||||
|
|
||||||
#: The canonical way to decorate class-based views is to decorate the
|
#: A list of decorators to apply, in order, to the generated view
|
||||||
#: return value of as_view(). However since this moves parts of the
|
#: function. Remember that ``@decorator`` syntax is applied bottom
|
||||||
#: logic from the class declaration to the place where it's hooked
|
#: to top, so the first decorator in the list would be the bottom
|
||||||
#: into the routing system.
|
#: decorator.
|
||||||
#:
|
|
||||||
#: You can place one or more decorators in this list and whenever the
|
|
||||||
#: view function is created the result is automatically decorated.
|
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.8
|
#: .. versionadded:: 0.8
|
||||||
decorators: t.List[t.Callable] = []
|
decorators: t.ClassVar[t.List[t.Callable]] = []
|
||||||
|
|
||||||
def dispatch_request(self) -> ResponseReturnValue:
|
#: Create a new instance of this view class for every request by
|
||||||
"""Subclasses have to override this method to implement the
|
#: default. If a view subclass sets this to ``False``, the same
|
||||||
actual view function code. This method is called with all
|
#: instance is used for every request.
|
||||||
the arguments from the URL rule.
|
#:
|
||||||
|
#: A single instance is more efficient, especially if complex setup
|
||||||
|
#: is done during init. However, storing data on ``self`` is no
|
||||||
|
#: longer safe across requests, and :data:`~flask.g` should be used
|
||||||
|
#: instead.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.2
|
||||||
|
init_every_request: t.ClassVar[bool] = True
|
||||||
|
|
||||||
|
def dispatch_request(self) -> ft.ResponseReturnValue:
|
||||||
|
"""The actual view function behavior. Subclasses must override
|
||||||
|
this and return a valid response. Any variables from the URL
|
||||||
|
rule are passed as keyword arguments.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(
|
def as_view(
|
||||||
cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
|
cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
|
||||||
) -> t.Callable:
|
) -> ft.ViewCallable:
|
||||||
"""Converts the class into an actual view function that can be used
|
"""Convert the class into a view function that can be registered
|
||||||
with the routing system. Internally this generates a function on the
|
for a route.
|
||||||
fly which will instantiate the :class:`View` on each request and call
|
|
||||||
the :meth:`dispatch_request` method on it.
|
|
||||||
|
|
||||||
The arguments passed to :meth:`as_view` are forwarded to the
|
By default, the generated view will create a new instance of the
|
||||||
constructor of the class.
|
view class for every request and call its
|
||||||
|
:meth:`dispatch_request` method. If the view class sets
|
||||||
|
:attr:`init_every_request` to ``False``, the same instance will
|
||||||
|
be used for every request.
|
||||||
|
|
||||||
|
The arguments passed to this method are forwarded to the view
|
||||||
|
class ``__init__`` method.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.2
|
||||||
|
Added the ``init_every_request`` class attribute.
|
||||||
"""
|
"""
|
||||||
|
if cls.init_every_request:
|
||||||
|
|
||||||
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
|
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
self = view.view_class(*class_args, **class_kwargs) # type: ignore
|
self = view.view_class( # type: ignore[attr-defined]
|
||||||
return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
|
*class_args, **class_kwargs
|
||||||
|
)
|
||||||
|
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self = cls(*class_args, **class_kwargs)
|
||||||
|
|
||||||
|
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
|
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
|
||||||
|
|
||||||
if cls.decorators:
|
if cls.decorators:
|
||||||
view.__name__ = name
|
view.__name__ = name
|
||||||
|
|
@ -103,50 +132,51 @@ class View:
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
class MethodViewType(type):
|
class MethodView(View):
|
||||||
"""Metaclass for :class:`MethodView` that determines what methods the view
|
"""Dispatches request methods to the corresponding instance methods.
|
||||||
defines.
|
For example, if you implement a ``get`` method, it will be used to
|
||||||
|
handle ``GET`` requests.
|
||||||
|
|
||||||
|
This can be useful for defining a REST API.
|
||||||
|
|
||||||
|
:attr:`methods` is automatically set based on the methods defined on
|
||||||
|
the class.
|
||||||
|
|
||||||
|
See :doc:`views` for a detailed guide.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class CounterAPI(MethodView):
|
||||||
|
def get(self):
|
||||||
|
return str(session.get("counter", 0))
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
session["counter"] = session.get("counter", 0) + 1
|
||||||
|
return redirect(url_for("counter"))
|
||||||
|
|
||||||
|
app.add_url_rule(
|
||||||
|
"/counter", view_func=CounterAPI.as_view("counter")
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(cls, name, bases, d):
|
def __init_subclass__(cls, **kwargs: t.Any) -> None:
|
||||||
super().__init__(name, bases, d)
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
if "methods" not in d:
|
if "methods" not in cls.__dict__:
|
||||||
methods = set()
|
methods = set()
|
||||||
|
|
||||||
for base in bases:
|
for base in cls.__bases__:
|
||||||
if getattr(base, "methods", None):
|
if getattr(base, "methods", None):
|
||||||
methods.update(base.methods)
|
methods.update(base.methods) # type: ignore[attr-defined]
|
||||||
|
|
||||||
for key in http_method_funcs:
|
for key in http_method_funcs:
|
||||||
if hasattr(cls, key):
|
if hasattr(cls, key):
|
||||||
methods.add(key.upper())
|
methods.add(key.upper())
|
||||||
|
|
||||||
# If we have no method at all in there we don't want to add a
|
|
||||||
# method list. This is for instance the case for the base class
|
|
||||||
# or another subclass of a base method view that does not introduce
|
|
||||||
# new methods.
|
|
||||||
if methods:
|
if methods:
|
||||||
cls.methods = methods
|
cls.methods = methods
|
||||||
|
|
||||||
|
def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
|
||||||
class MethodView(View, metaclass=MethodViewType):
|
|
||||||
"""A class-based view that dispatches request methods to the corresponding
|
|
||||||
class methods. For example, if you implement a ``get`` method, it will be
|
|
||||||
used to handle ``GET`` requests. ::
|
|
||||||
|
|
||||||
class CounterAPI(MethodView):
|
|
||||||
def get(self):
|
|
||||||
return session.get('counter', 0)
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
session['counter'] = session.get('counter', 0) + 1
|
|
||||||
return 'OK'
|
|
||||||
|
|
||||||
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
|
|
||||||
"""
|
|
||||||
|
|
||||||
def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
|
|
||||||
meth = getattr(self, request.method.lower(), None)
|
meth = getattr(self, request.method.lower(), None)
|
||||||
|
|
||||||
# If the request method is HEAD and we don't have a handler for it
|
# If the request method is HEAD and we don't have a handler for it
|
||||||
|
|
@ -155,4 +185,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 current_app.ensure_sync(meth)(*args, **kwargs)
|
return current_app.ensure_sync(meth)(**kwargs)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from . import json
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .helpers import _split_blueprint_path
|
from .helpers import _split_blueprint_path
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING: # pragma: no cover
|
||||||
from werkzeug.routing import Rule
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,10 +107,12 @@ def test_async_before_after_request():
|
||||||
def index():
|
def index():
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@app.before_first_request
|
with pytest.deprecated_call():
|
||||||
async def before_first():
|
|
||||||
nonlocal app_first_called
|
@app.before_first_request
|
||||||
app_first_called = True
|
async def before_first():
|
||||||
|
nonlocal app_first_called
|
||||||
|
app_first_called = True
|
||||||
|
|
||||||
@app.before_request
|
@app.before_request
|
||||||
async def before():
|
async def before():
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,11 @@ def test_session_using_session_settings(app, client):
|
||||||
flask.session["testing"] = 42
|
flask.session["testing"] = 42
|
||||||
return "Hello World"
|
return "Hello World"
|
||||||
|
|
||||||
|
@app.route("/clear")
|
||||||
|
def clear():
|
||||||
|
flask.session.pop("testing", None)
|
||||||
|
return "Goodbye World"
|
||||||
|
|
||||||
rv = client.get("/", "http://www.example.com:8080/test/")
|
rv = client.get("/", "http://www.example.com:8080/test/")
|
||||||
cookie = rv.headers["set-cookie"].lower()
|
cookie = rv.headers["set-cookie"].lower()
|
||||||
assert "domain=.example.com" in cookie
|
assert "domain=.example.com" in cookie
|
||||||
|
|
@ -337,11 +342,6 @@ def test_session_using_session_settings(app, client):
|
||||||
assert "httponly" not in cookie
|
assert "httponly" not in cookie
|
||||||
assert "samesite" in cookie
|
assert "samesite" in cookie
|
||||||
|
|
||||||
@app.route("/clear")
|
|
||||||
def clear():
|
|
||||||
flask.session.pop("testing", None)
|
|
||||||
return "Goodbye World"
|
|
||||||
|
|
||||||
rv = client.get("/clear", "http://www.example.com:8080/test/")
|
rv = client.get("/clear", "http://www.example.com:8080/test/")
|
||||||
cookie = rv.headers["set-cookie"].lower()
|
cookie = rv.headers["set-cookie"].lower()
|
||||||
assert "session=;" in cookie
|
assert "session=;" in cookie
|
||||||
|
|
@ -899,13 +899,6 @@ def test_error_handling(app, client):
|
||||||
assert b"forbidden" == rv.data
|
assert b"forbidden" == rv.data
|
||||||
|
|
||||||
|
|
||||||
def test_error_handler_unknown_code(app):
|
|
||||||
with pytest.raises(KeyError) as exc_info:
|
|
||||||
app.register_error_handler(999, lambda e: ("999", 999))
|
|
||||||
|
|
||||||
assert "Use a subclass" in exc_info.value.args[0]
|
|
||||||
|
|
||||||
|
|
||||||
def test_error_handling_processing(app, client):
|
def test_error_handling_processing(app, client):
|
||||||
app.testing = False
|
app.testing = False
|
||||||
|
|
||||||
|
|
@ -1038,7 +1031,14 @@ def test_errorhandler_precedence(app, client):
|
||||||
assert rv.data == b"E2"
|
assert rv.data == b"E2"
|
||||||
|
|
||||||
|
|
||||||
def test_trapping_of_bad_request_key_errors(app, client):
|
@pytest.mark.parametrize(
|
||||||
|
("debug", "trap", "expect_key", "expect_abort"),
|
||||||
|
[(False, None, True, True), (True, None, False, True), (False, True, False, False)],
|
||||||
|
)
|
||||||
|
def test_trap_bad_request_key_error(app, client, debug, trap, expect_key, expect_abort):
|
||||||
|
app.config["DEBUG"] = debug
|
||||||
|
app.config["TRAP_BAD_REQUEST_ERRORS"] = trap
|
||||||
|
|
||||||
@app.route("/key")
|
@app.route("/key")
|
||||||
def fail():
|
def fail():
|
||||||
flask.request.form["missing_key"]
|
flask.request.form["missing_key"]
|
||||||
|
|
@ -1047,26 +1047,23 @@ def test_trapping_of_bad_request_key_errors(app, client):
|
||||||
def allow_abort():
|
def allow_abort():
|
||||||
flask.abort(400)
|
flask.abort(400)
|
||||||
|
|
||||||
rv = client.get("/key")
|
if expect_key:
|
||||||
assert rv.status_code == 400
|
rv = client.get("/key")
|
||||||
assert b"missing_key" not in rv.data
|
assert rv.status_code == 400
|
||||||
rv = client.get("/abort")
|
assert b"missing_key" not in rv.data
|
||||||
assert rv.status_code == 400
|
else:
|
||||||
|
with pytest.raises(KeyError) as exc_info:
|
||||||
|
client.get("/key")
|
||||||
|
|
||||||
app.debug = True
|
assert exc_info.errisinstance(BadRequest)
|
||||||
with pytest.raises(KeyError) as e:
|
assert "missing_key" in exc_info.value.get_description()
|
||||||
client.get("/key")
|
|
||||||
assert e.errisinstance(BadRequest)
|
|
||||||
assert "missing_key" in e.value.get_description()
|
|
||||||
rv = client.get("/abort")
|
|
||||||
assert rv.status_code == 400
|
|
||||||
|
|
||||||
app.debug = False
|
if expect_abort:
|
||||||
app.config["TRAP_BAD_REQUEST_ERRORS"] = True
|
rv = client.get("/abort")
|
||||||
with pytest.raises(KeyError):
|
assert rv.status_code == 400
|
||||||
client.get("/key")
|
else:
|
||||||
with pytest.raises(BadRequest):
|
with pytest.raises(BadRequest):
|
||||||
client.get("/abort")
|
client.get("/abort")
|
||||||
|
|
||||||
|
|
||||||
def test_trapping_of_all_http_exceptions(app, client):
|
def test_trapping_of_all_http_exceptions(app, client):
|
||||||
|
|
@ -1668,7 +1665,7 @@ def test_nonascii_pathinfo(app, client):
|
||||||
assert rv.data == b"Hello World!"
|
assert rv.data == b"Hello World!"
|
||||||
|
|
||||||
|
|
||||||
def test_debug_mode_complains_after_first_request(app, client):
|
def test_no_setup_after_first_request(app, client):
|
||||||
app.debug = True
|
app.debug = True
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
@ -1678,27 +1675,20 @@ def test_debug_mode_complains_after_first_request(app, client):
|
||||||
assert not app.got_first_request
|
assert not app.got_first_request
|
||||||
assert client.get("/").data == b"Awesome"
|
assert client.get("/").data == b"Awesome"
|
||||||
|
|
||||||
with pytest.raises(AssertionError) as e:
|
with pytest.raises(AssertionError) as exc_info:
|
||||||
app.add_url_rule("/foo", endpoint="late")
|
app.add_url_rule("/foo", endpoint="late")
|
||||||
|
|
||||||
assert "A setup function was called" in str(e.value)
|
assert "setup method 'add_url_rule'" in str(exc_info.value)
|
||||||
|
|
||||||
app.debug = False
|
|
||||||
|
|
||||||
@app.route("/foo")
|
|
||||||
def working():
|
|
||||||
return "Meh"
|
|
||||||
|
|
||||||
assert client.get("/foo").data == b"Meh"
|
|
||||||
assert app.got_first_request
|
|
||||||
|
|
||||||
|
|
||||||
def test_before_first_request_functions(app, client):
|
def test_before_first_request_functions(app, client):
|
||||||
got = []
|
got = []
|
||||||
|
|
||||||
@app.before_first_request
|
with pytest.deprecated_call():
|
||||||
def foo():
|
|
||||||
got.append(42)
|
@app.before_first_request
|
||||||
|
def foo():
|
||||||
|
got.append(42)
|
||||||
|
|
||||||
client.get("/")
|
client.get("/")
|
||||||
assert got == [42]
|
assert got == [42]
|
||||||
|
|
@ -1710,10 +1700,12 @@ def test_before_first_request_functions(app, client):
|
||||||
def test_before_first_request_functions_concurrent(app, client):
|
def test_before_first_request_functions_concurrent(app, client):
|
||||||
got = []
|
got = []
|
||||||
|
|
||||||
@app.before_first_request
|
with pytest.deprecated_call():
|
||||||
def foo():
|
|
||||||
time.sleep(0.2)
|
@app.before_first_request
|
||||||
got.append(42)
|
def foo():
|
||||||
|
time.sleep(0.2)
|
||||||
|
got.append(42)
|
||||||
|
|
||||||
def get_and_assert():
|
def get_and_assert():
|
||||||
client.get("/")
|
client.get("/")
|
||||||
|
|
@ -1727,28 +1719,23 @@ def test_before_first_request_functions_concurrent(app, client):
|
||||||
|
|
||||||
|
|
||||||
def test_routing_redirect_debugging(monkeypatch, app, client):
|
def test_routing_redirect_debugging(monkeypatch, app, client):
|
||||||
@app.route("/foo/", methods=["GET", "POST"])
|
app.config["DEBUG"] = True
|
||||||
def foo():
|
|
||||||
return "success"
|
|
||||||
|
|
||||||
app.debug = False
|
@app.route("/user/", methods=["GET", "POST"])
|
||||||
rv = client.post("/foo", data={}, follow_redirects=True)
|
def user():
|
||||||
|
return flask.request.form["status"]
|
||||||
|
|
||||||
|
# default redirect code preserves form data
|
||||||
|
rv = client.post("/user", data={"status": "success"}, follow_redirects=True)
|
||||||
assert rv.data == b"success"
|
assert rv.data == b"success"
|
||||||
|
|
||||||
app.debug = True
|
# 301 and 302 raise error
|
||||||
|
|
||||||
with client:
|
|
||||||
rv = client.post("/foo", data={}, follow_redirects=True)
|
|
||||||
assert rv.data == b"success"
|
|
||||||
rv = client.get("/foo", data={}, follow_redirects=True)
|
|
||||||
assert rv.data == b"success"
|
|
||||||
|
|
||||||
monkeypatch.setattr(RequestRedirect, "code", 301)
|
monkeypatch.setattr(RequestRedirect, "code", 301)
|
||||||
|
|
||||||
with client, pytest.raises(AssertionError) as e:
|
with client, pytest.raises(AssertionError) as exc_info:
|
||||||
client.post("/foo", data={})
|
client.post("/user", data={"status": "error"}, follow_redirects=True)
|
||||||
|
|
||||||
assert "canonical URL 'http://localhost/foo/'" in str(e.value)
|
assert "canonical URL 'http://localhost/user/'" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
def test_route_decorator_custom_endpoint(app, client):
|
def test_route_decorator_custom_endpoint(app, client):
|
||||||
|
|
|
||||||
|
|
@ -722,9 +722,11 @@ def test_app_request_processing(app, client):
|
||||||
bp = flask.Blueprint("bp", __name__)
|
bp = flask.Blueprint("bp", __name__)
|
||||||
evts = []
|
evts = []
|
||||||
|
|
||||||
@bp.before_app_first_request
|
with pytest.deprecated_call():
|
||||||
def before_first_request():
|
|
||||||
evts.append("first")
|
@bp.before_app_first_request
|
||||||
|
def before_first_request():
|
||||||
|
evts.append("first")
|
||||||
|
|
||||||
@bp.before_app_request
|
@bp.before_app_request
|
||||||
def before_app():
|
def before_app():
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ 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 DispatchingApp
|
||||||
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
|
||||||
from flask.cli import get_version
|
from flask.cli import get_version
|
||||||
|
|
@ -492,7 +491,18 @@ class TestRoutes:
|
||||||
assert "No routes were registered." in result.output
|
assert "No routes were registered." in result.output
|
||||||
|
|
||||||
|
|
||||||
need_dotenv = pytest.mark.skipif(dotenv is None, reason="dotenv is not installed")
|
def dotenv_not_available():
|
||||||
|
try:
|
||||||
|
import dotenv # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
need_dotenv = pytest.mark.skipif(
|
||||||
|
dotenv_not_available(), reason="dotenv is not installed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@need_dotenv
|
@need_dotenv
|
||||||
|
|
@ -530,7 +540,7 @@ def test_dotenv_path(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
def test_dotenv_optional(monkeypatch):
|
def test_dotenv_optional(monkeypatch):
|
||||||
monkeypatch.setattr("flask.cli.dotenv", None)
|
monkeypatch.setitem(sys.modules, "dotenv", None)
|
||||||
monkeypatch.chdir(test_path)
|
monkeypatch.chdir(test_path)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
assert "FOO" not in os.environ
|
assert "FOO" not in os.environ
|
||||||
|
|
@ -602,7 +612,8 @@ def test_run_cert_import(monkeypatch):
|
||||||
|
|
||||||
|
|
||||||
def test_run_cert_no_ssl(monkeypatch):
|
def test_run_cert_no_ssl(monkeypatch):
|
||||||
monkeypatch.setattr("flask.cli.ssl", None)
|
monkeypatch.setitem(sys.modules, "ssl", None)
|
||||||
|
|
||||||
with pytest.raises(click.BadParameter):
|
with pytest.raises(click.BadParameter):
|
||||||
run_command.make_context("run", ["--cert", "not_here"])
|
run_command.make_context("run", ["--cert", "not_here"])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,10 @@ def test_config_from_mapping():
|
||||||
app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo")
|
app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo")
|
||||||
common_object_test(app)
|
common_object_test(app)
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
app.config.from_mapping(SECRET_KEY="config", TEST_KEY="foo", skip_key="skip")
|
||||||
|
common_object_test(app)
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
app.config.from_mapping({}, {})
|
app.config.from_mapping({}, {})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import werkzeug.exceptions
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask.helpers import get_debug_flag
|
from flask.helpers import get_debug_flag
|
||||||
|
|
@ -118,11 +119,15 @@ class TestUrlFor:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_url_for_with_scheme_not_external(self, app, req_ctx):
|
def test_url_for_with_scheme_not_external(self, app, req_ctx):
|
||||||
@app.route("/")
|
app.add_url_rule("/", endpoint="index")
|
||||||
def index():
|
|
||||||
return "42"
|
|
||||||
|
|
||||||
pytest.raises(ValueError, flask.url_for, "index", _scheme="https")
|
# Implicit external with scheme.
|
||||||
|
url = flask.url_for("index", _scheme="https")
|
||||||
|
assert url == "https://localhost/"
|
||||||
|
|
||||||
|
# Error when external=False with scheme
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
flask.url_for("index", _scheme="https", _external=False)
|
||||||
|
|
||||||
def test_url_for_with_alternating_schemes(self, app, req_ctx):
|
def test_url_for_with_alternating_schemes(self, app, req_ctx):
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
|
|
@ -158,6 +163,51 @@ class TestUrlFor:
|
||||||
assert flask.url_for("myview", _method="POST") == "/myview/create"
|
assert flask.url_for("myview", _method="POST") == "/myview/create"
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_no_app():
|
||||||
|
response = flask.redirect("https://localhost", 307)
|
||||||
|
assert response.location == "https://localhost"
|
||||||
|
assert response.status_code == 307
|
||||||
|
|
||||||
|
|
||||||
|
def test_redirect_with_app(app):
|
||||||
|
def redirect(location, code=302):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
app.redirect = redirect
|
||||||
|
|
||||||
|
with app.app_context(), pytest.raises(ValueError):
|
||||||
|
flask.redirect("other")
|
||||||
|
|
||||||
|
|
||||||
|
def test_abort_no_app():
|
||||||
|
with pytest.raises(werkzeug.exceptions.Unauthorized):
|
||||||
|
flask.abort(401)
|
||||||
|
|
||||||
|
with pytest.raises(LookupError):
|
||||||
|
flask.abort(900)
|
||||||
|
|
||||||
|
|
||||||
|
def test_app_aborter_class():
|
||||||
|
class MyAborter(werkzeug.exceptions.Aborter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MyFlask(flask.Flask):
|
||||||
|
aborter_class = MyAborter
|
||||||
|
|
||||||
|
app = MyFlask(__name__)
|
||||||
|
assert isinstance(app.aborter, MyAborter)
|
||||||
|
|
||||||
|
|
||||||
|
def test_abort_with_app(app):
|
||||||
|
class My900Error(werkzeug.exceptions.HTTPException):
|
||||||
|
code = 900
|
||||||
|
|
||||||
|
app.aborter.mapping[900] = My900Error
|
||||||
|
|
||||||
|
with app.app_context(), pytest.raises(My900Error):
|
||||||
|
flask.abort(900)
|
||||||
|
|
||||||
|
|
||||||
class TestNoImports:
|
class TestNoImports:
|
||||||
"""Test Flasks are created without import.
|
"""Test Flasks are created without import.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -15,19 +14,6 @@ def test_explicit_instance_paths(modules_tmpdir):
|
||||||
assert app.instance_path == str(modules_tmpdir)
|
assert app.instance_path == str(modules_tmpdir)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="weird interaction with tox")
|
|
||||||
def test_main_module_paths(modules_tmpdir, purge_module):
|
|
||||||
app = modules_tmpdir.join("main_app.py")
|
|
||||||
app.write('import flask\n\napp = flask.Flask("__main__")')
|
|
||||||
purge_module("main_app")
|
|
||||||
|
|
||||||
from main_app import app
|
|
||||||
|
|
||||||
here = os.path.abspath(os.getcwd())
|
|
||||||
assert app.instance_path == os.path.join(here, "instance")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="weird interaction with tox")
|
|
||||||
def test_uninstalled_module_paths(modules_tmpdir, purge_module):
|
def test_uninstalled_module_paths(modules_tmpdir, purge_module):
|
||||||
app = modules_tmpdir.join("config_module_app.py").write(
|
app = modules_tmpdir.join("config_module_app.py").write(
|
||||||
"import os\n"
|
"import os\n"
|
||||||
|
|
@ -42,7 +28,6 @@ def test_uninstalled_module_paths(modules_tmpdir, purge_module):
|
||||||
assert app.instance_path == str(modules_tmpdir.join("instance"))
|
assert app.instance_path == str(modules_tmpdir.join("instance"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="weird interaction with tox")
|
|
||||||
def test_uninstalled_package_paths(modules_tmpdir, purge_module):
|
def test_uninstalled_package_paths(modules_tmpdir, purge_module):
|
||||||
app = modules_tmpdir.mkdir("config_package_app")
|
app = modules_tmpdir.mkdir("config_package_app")
|
||||||
init = app.join("__init__.py")
|
init = app.join("__init__.py")
|
||||||
|
|
@ -59,6 +44,25 @@ def test_uninstalled_package_paths(modules_tmpdir, purge_module):
|
||||||
assert app.instance_path == str(modules_tmpdir.join("instance"))
|
assert app.instance_path == str(modules_tmpdir.join("instance"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_uninstalled_namespace_paths(tmpdir, monkeypatch, purge_module):
|
||||||
|
def create_namespace(package):
|
||||||
|
project = tmpdir.join(f"project-{package}")
|
||||||
|
monkeypatch.syspath_prepend(str(project))
|
||||||
|
project.join("namespace").join(package).join("__init__.py").write(
|
||||||
|
"import flask\napp = flask.Flask(__name__)\n", ensure=True
|
||||||
|
)
|
||||||
|
return project
|
||||||
|
|
||||||
|
_ = create_namespace("package1")
|
||||||
|
project2 = create_namespace("package2")
|
||||||
|
purge_module("namespace.package2")
|
||||||
|
purge_module("namespace")
|
||||||
|
|
||||||
|
from namespace.package2 import app
|
||||||
|
|
||||||
|
assert app.instance_path == str(project2.join("instance"))
|
||||||
|
|
||||||
|
|
||||||
def test_installed_module_paths(
|
def test_installed_module_paths(
|
||||||
modules_tmpdir, modules_tmpdir_prefix, purge_module, site_packages, limit_loader
|
modules_tmpdir, modules_tmpdir_prefix, purge_module, site_packages, limit_loader
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -11,29 +11,35 @@ def test_error_handler_no_match(app, client):
|
||||||
class CustomException(Exception):
|
class CustomException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class UnacceptableCustomException(BaseException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@app.errorhandler(CustomException)
|
@app.errorhandler(CustomException)
|
||||||
def custom_exception_handler(e):
|
def custom_exception_handler(e):
|
||||||
assert isinstance(e, CustomException)
|
assert isinstance(e, CustomException)
|
||||||
return "custom"
|
return "custom"
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(TypeError) as exc_info:
|
||||||
AssertionError, match="Custom exceptions must be subclasses of Exception."
|
app.register_error_handler(CustomException(), None)
|
||||||
):
|
|
||||||
app.register_error_handler(UnacceptableCustomException, None)
|
assert "CustomException() is an instance, not a class." in str(exc_info.value)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
app.register_error_handler(list, None)
|
||||||
|
|
||||||
|
assert "'list' is not a subclass of Exception." in str(exc_info.value)
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def handle_500(e):
|
def handle_500(e):
|
||||||
assert isinstance(e, InternalServerError)
|
assert isinstance(e, InternalServerError)
|
||||||
original = getattr(e, "original_exception", None)
|
|
||||||
|
|
||||||
if original is not None:
|
if e.original_exception is not None:
|
||||||
return f"wrapped {type(original).__name__}"
|
return f"wrapped {type(e.original_exception).__name__}"
|
||||||
|
|
||||||
return "direct"
|
return "direct"
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
app.register_error_handler(999, None)
|
||||||
|
|
||||||
|
assert "Use a subclass of HTTPException" in str(exc_info.value)
|
||||||
|
|
||||||
@app.route("/custom")
|
@app.route("/custom")
|
||||||
def custom_test():
|
def custom_test():
|
||||||
raise CustomException()
|
raise CustomException()
|
||||||
|
|
|
||||||
|
|
@ -240,3 +240,21 @@ def test_remove_method_from_parent(app, client):
|
||||||
assert client.get("/").data == b"GET"
|
assert client.get("/").data == b"GET"
|
||||||
assert client.post("/").status_code == 405
|
assert client.post("/").status_code == 405
|
||||||
assert sorted(View.methods) == ["GET"]
|
assert sorted(View.methods) == ["GET"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_once(app, client):
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
class CountInit(flask.views.View):
|
||||||
|
init_every_request = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
nonlocal n
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
def dispatch_request(self):
|
||||||
|
return str(n)
|
||||||
|
|
||||||
|
app.add_url_rule("/", view_func=CountInit.as_view("index"))
|
||||||
|
assert client.get("/").data == b"1"
|
||||||
|
assert client.get("/").data == b"1"
|
||||||
|
|
|
||||||
33
tests/typing/typing_error_handler.py
Normal file
33
tests/typing/typing_error_handler.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(400)
|
||||||
|
@app.errorhandler(HTTPStatus.BAD_REQUEST)
|
||||||
|
@app.errorhandler(BadRequest)
|
||||||
|
def handle_400(e: BadRequest) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(ValueError)
|
||||||
|
def handle_custom(e: ValueError) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(ValueError)
|
||||||
|
def handle_accept_base(e: Exception) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(BadRequest)
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def handle_multiple(e: BadRequest | NotFound) -> str:
|
||||||
|
return ""
|
||||||
62
tests/typing/typing_route.py
Normal file
62
tests/typing/typing_route.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask import jsonify
|
||||||
|
from flask.templating import render_template
|
||||||
|
from flask.views import View
|
||||||
|
from flask.wrappers import Response
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/str")
|
||||||
|
def hello_str() -> str:
|
||||||
|
return "<p>Hello, World!</p>"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/bytes")
|
||||||
|
def hello_bytes() -> bytes:
|
||||||
|
return b"<p>Hello, World!</p>"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/json")
|
||||||
|
def hello_json() -> Response:
|
||||||
|
return jsonify({"response": "Hello, World!"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/status")
|
||||||
|
@app.route("/status/<int:code>")
|
||||||
|
def tuple_status(code: int = 200) -> tuple[str, int]:
|
||||||
|
return "hello", code
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/status-enum")
|
||||||
|
def tuple_status_enum() -> tuple[str, int]:
|
||||||
|
return "hello", HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/headers")
|
||||||
|
def tuple_headers() -> tuple[str, dict[str, str]]:
|
||||||
|
return "Hello, World!", {"Content-Type": "text/plain"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/template")
|
||||||
|
@app.route("/template/<name>")
|
||||||
|
def return_template(name: str | None = None) -> str:
|
||||||
|
return render_template("index.html", name=name)
|
||||||
|
|
||||||
|
|
||||||
|
class RenderTemplateView(View):
|
||||||
|
def __init__(self: RenderTemplateView, template_name: str) -> None:
|
||||||
|
self.template_name = template_name
|
||||||
|
|
||||||
|
def dispatch_request(self: RenderTemplateView) -> str:
|
||||||
|
return render_template(self.template_name)
|
||||||
|
|
||||||
|
|
||||||
|
app.add_url_rule(
|
||||||
|
"/about",
|
||||||
|
view_func=RenderTemplateView.as_view("about_page", template_name="about.html"),
|
||||||
|
)
|
||||||
1
tox.ini
1
tox.ini
|
|
@ -9,6 +9,7 @@ envlist =
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
envtmpdir = {toxworkdir}/tmp/{envname}
|
||||||
deps =
|
deps =
|
||||||
-r requirements/tests.txt
|
-r requirements/tests.txt
|
||||||
min: -r requirements/tests-pallets-min.txt
|
min: -r requirements/tests-pallets-min.txt
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue