Merge branch 'main' into class-route

This commit is contained in:
Grey Li 2021-08-06 09:47:32 +08:00 committed by GitHub
commit e5fb65b171
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 902 additions and 534 deletions

8
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/"
schedule:
interval: monthly
time: "08:00"
open-pull-requests-limit: 99

View file

@ -2,7 +2,7 @@ name: Tests
on:
push:
branches:
- master
- main
- '*.x'
paths-ignore:
- 'docs/**'
@ -10,7 +10,7 @@ on:
- '*.rst'
pull_request:
branches:
- master
- main
- '*.x'
paths-ignore:
- 'docs/**'

2
.gitignore vendored
View file

@ -4,6 +4,8 @@
*.pyc
*.pyo
env/
venv/
.venv/
env*
dist/
build/

View file

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

View file

@ -1,9 +1,70 @@
.. currentmodule:: flask
Version 2.1.0
-------------
Unreleased
- Update Click dependency to >= 8.0.
Version 2.0.2
-------------
Unreleased
- Fix type annotation for ``teardown_*`` methods. :issue:`4093`
- Fix type annotation for ``before_request`` and ``before_app_request``
decorators. :issue:`4104`
- Fixed the issue where typing requires template global
decorators to accept functions with no arguments. :issue:`4098`
- Support View and MethodView instances with async handlers. :issue:`4112`
- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
- Fix registering a blueprint twice with differing names. :issue:`4124`
Version 2.0.1
-------------
Released 2021-05-21
- Re-add the ``filename`` parameter in ``send_from_directory``. The
``filename`` parameter has been renamed to ``path``, the old name
is deprecated. :pr:`4019`
- Mark top-level names as exported so type checking understands
imports in user projects. :issue:`4024`
- Fix type annotation for ``g`` and inform mypy that it is a namespace
object that has arbitrary attributes. :issue:`4020`
- Fix some types that weren't available in Python 3.6.0. :issue:`4040`
- Improve typing for ``send_file``, ``send_from_directory``, and
``get_send_file_max_age``. :issue:`4044`, :pr:`4026`
- Show an error when a blueprint name contains a dot. The ``.`` has
special meaning, it is used to separate (nested) blueprint names and
the endpoint name. :issue:`4041`
- Combine URL prefixes when nesting blueprints that were created with
a ``url_prefix`` value. :issue:`4037`
- Roll back a change to the order that URL matching was done. The
URL is again matched after the session is loaded, so the session is
available in custom URL converters. :issue:`4053`
- Re-add deprecated ``Config.from_json``, which was accidentally
removed early. :issue:`4078`
- Improve typing for some functions using ``Callable`` in their type
signatures, focusing on decorator factories. :issue:`4060`
- Nested blueprints are registered with their dotted name. This allows
different blueprints with the same name to be nested at different
locations. :issue:`4069`
- ``register_blueprint`` takes a ``name`` option to change the
(pre-dotted) name the blueprint is registered with. This allows the
same blueprint to be registered multiple times with unique names for
``url_for``. Registering the same blueprint with the same name
multiple times is deprecated. :issue:`1091`
- Improve typing for ``stream_with_context``. :issue:`4052`
Version 2.0.0
-------------
Unreleased
Released 2021-05-11
- Drop support for Python 2 and 3.5.
- Bump minimum versions of other Pallets projects: Werkzeug >= 2,
@ -76,6 +137,8 @@ Unreleased
- Support async views, error handlers, before and after request, and
teardown functions. :pr:`3412`
- Support nesting blueprints. :issue:`593, 1548`, :pr:`3923`
- Set the default encoding to "UTF-8" when loading ``.env`` and
``.flaskenv`` files to allow to use non-ASCII characters. :issue:`3931`
- ``flask shell`` sets up tab and history completion like the default
``python`` shell if ``readline`` is installed. :issue:`3941`
- ``helpers.total_seconds()`` is deprecated. Use
@ -84,6 +147,26 @@ Unreleased
- Support using the ``route`` decorator on view classes (i.e.
``View`` and ``MethodView`` subclasses). :issue:`3404`
Version 1.1.4
-------------
Released 2021-05-13
- Update ``static_folder`` to use ``_compat.fspath`` instead of
``os.fspath`` to continue supporting Python < 3.6 :issue:`4050`
Version 1.1.3
-------------
Released 2021-05-13
- Set maximum versions of Werkzeug, Jinja, Click, and ItsDangerous.
:issue:`4043`
- Re-add support for passing a ``pathlib.Path`` for ``static_folder``.
:pr:`3579`
Version 1.1.2
-------------

View file

@ -12,14 +12,16 @@ to address bugs and feature requests in Flask itself. Use one of the
following resources for questions about using Flask or issues with your
own code:
- The ``#get-help`` channel on our Discord chat:
- The ``#questions`` channel on our Discord chat:
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:
``site:stackoverflow.com flask {search term, exception message, etc.}``
- Ask on our `GitHub Discussions`_.
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
Reporting issues
@ -92,7 +94,7 @@ First time setup
.. code-block:: text
git remote add fork https://github.com/{username}/flask
$ git remote add fork https://github.com/{username}/flask
- Create a virtualenv.
@ -112,6 +114,12 @@ First time setup
> py -3 -m venv env
> env\Scripts\activate
- Upgrade pip and setuptools.
.. code-block:: text
$ python -m pip install --upgrade pip setuptools
- Install the development dependencies, then install Flask in editable
mode.
@ -129,7 +137,7 @@ First time setup
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
.. _GitHub account: https://github.com/join
.. _Fork: https://github.com/pallets/jinja/fork
.. _Fork: https://github.com/pallets/flask/fork
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork
@ -143,15 +151,15 @@ Start coding
.. code-block:: text
$ git fetch origin
$ git checkout -b your-branch-name origin/1.1.x
$ git checkout -b your-branch-name origin/2.0.x
If you're submitting a feature addition or change, branch off of the
"master" branch.
"main" branch.
.. code-block:: text
$ git fetch origin
$ git checkout -b your-branch-name origin/master
$ git checkout -b your-branch-name origin/main
- Using your favorite editor, make your changes,
`committing as you go`_.

View file

@ -55,7 +55,7 @@ Contributing
For guidance on setting up a development environment and how to make a
contribution to Flask, see the `contributing guidelines`_.
.. _contributing guidelines: https://github.com/pallets/flask/blob/master/CONTRIBUTING.rst
.. _contributing guidelines: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst
Donate

View file

@ -7,7 +7,8 @@ Using ``async`` and ``await``
Routes, error handlers, before request, after request, and teardown
functions can all be coroutine functions if Flask is installed with the
``async`` extra (``pip install flask[async]``). This allows views to be
``async`` extra (``pip install flask[async]``). It requires Python 3.7+
where ``contextvars.ContextVar`` is available. This allows views to be
defined with ``async def`` and use ``await``.
.. code-block:: python
@ -17,6 +18,18 @@ defined with ``async def`` and use ``await``.
data = await async_db_query(...)
return jsonify(data)
Pluggable class-based views also support handlers that are implemented as
coroutines. This applies to the :meth:`~flask.views.View.dispatch_request`
method in views that inherit from the :class:`flask.views.View` class, as
well as all the HTTP method handlers in views that inherit from the
:class:`flask.views.MethodView` class.
.. admonition:: Using ``async`` on Windows on Python 3.8
Python 3.8 has a bug related to asyncio on Windows. If you encounter
something like ``ValueError: set_wakeup_fd only works in main thread``,
please upgrade to Python 3.9.
Performance
-----------
@ -51,7 +64,7 @@ example via ``asyncio.create_task``.
If you wish to use background tasks it is best to use a task queue to
trigger background work, rather than spawn tasks in a view
function. With that in mind you can spawn asyncio tasks by serving
Flask with a ASGI server and utilising the asgiref WsgiToAsgi adapter
Flask with an ASGI server and utilising the asgiref WsgiToAsgi adapter
as described in :ref:`asgi`. This works as the adapter creates an
event loop that runs continually.
@ -64,7 +77,7 @@ to the way it is implemented. If you have a mainly async codebase it
would make sense to consider `Quart`_. Quart is a reimplementation of
Flask based on the `ASGI`_ standard instead of WSGI. This allows it to
handle many concurrent requests, long running requests, and websockets
without requiring individual worker processes or threads.
without requiring multiple worker processes or threads.
It has also already been possible to run Flask with Gevent or Eventlet
to get many of the benefits of async request handling. These libraries
@ -80,12 +93,27 @@ to understanding the specific needs of your project.
Extensions
----------
Existing Flask extensions only expect views to be synchronous. If they
provide decorators to add functionality to views, those will probably
Flask extensions predating Flask's async support do not expect async views.
If they provide decorators to add functionality to views, those will probably
not work with async views because they will not await the function or be
awaitable. Other functions they provide will not be awaitable either and
will probably be blocking if called within an async view.
Extension authors can support async functions by utilising the
:meth:`flask.Flask.ensure_sync` method. For example, if the extension
provides a view function decorator add ``ensure_sync`` before calling
the decorated function,
.. code-block:: python
def extension(func):
@wraps(func)
def wrapper(*args, **kwargs):
... # Extension logic
return current_app.ensure_sync(func)(*args, **kwargs)
return wrapper
Check the changelog of the extension you want to use to see if they've
implemented async support, or make a feature request or PR to them.

View file

@ -127,8 +127,8 @@ It is possible to register a blueprint on another blueprint.
.. code-block:: python
parent = Blueprint("parent", __name__, url_prefix="/parent")
child = Blueprint("child", __name__, url_prefix="/child)
parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)

View file

@ -45,13 +45,13 @@ While ``FLASK_APP`` supports a variety of options for specifying your
application, most use cases should be simple. Here are the typical values:
(nothing)
The file :file:`wsgi.py` is imported, automatically detecting an app
(``app``). This provides an easy way to create an app from a factory with
extra arguments.
The name "app" or "wsgi" is imported (as a ".py" file, or package),
automatically detecting an app (``app`` or ``application``) or
factory (``create_app`` or ``make_app``).
``FLASK_APP=hello``
The name is imported, automatically detecting an app (``app``) or factory
(``create_app``).
The given name is imported, automatically detecting an app (``app``
or ``application``) or factory (``create_app`` or ``make_app``).
----

View file

@ -20,6 +20,7 @@ extensions = [
"sphinx_issues",
"sphinx_tabs.tabs",
]
autodoc_typehints = "description"
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"werkzeug": ("https://werkzeug.palletsprojects.com/", None),
@ -48,10 +49,10 @@ html_context = {
]
}
html_sidebars = {
"index": ["project.html", "localtoc.html", "searchbox.html"],
"**": ["localtoc.html", "relations.html", "searchbox.html"],
"index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
"**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
}
singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
html_static_path = ["_static"]
html_favicon = "_static/flask-icon.png"
html_logo = "_static/flask-icon.png"
@ -77,7 +78,7 @@ def github_link(name, rawtext, text, lineno, inliner, options=None, content=None
words = None
if packaging.version.parse(release).is_devrelease:
url = f"{base_url}master/{text}"
url = f"{base_url}main/{text}"
else:
url = f"{base_url}{release}/{text}"

View file

@ -16,6 +16,7 @@ Hosted options
- `Deploying Flask on Heroku <https://devcenter.heroku.com/articles/getting-started-with-python>`_
- `Deploying Flask on Google App Engine <https://cloud.google.com/appengine/docs/standard/python3/runtime>`_
- `Deploying Flask on Google Cloud Run <https://cloud.google.com/run/docs/quickstarts/build-and-deploy/python>`_
- `Deploying Flask on AWS Elastic Beanstalk <https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create-deploy-python-flask.html>`_
- `Deploying on Azure (IIS) <https://docs.microsoft.com/en-us/azure/app-service/containers/how-to-configure-python>`_
- `Deploying on PythonAnywhere <https://help.pythonanywhere.com/pages/Flask/>`_

View file

@ -8,6 +8,8 @@ Python Version
We recommend using the latest version of Python. Flask supports Python
3.6 and newer.
``async`` support in Flask requires Python 3.7+ for ``contextvars.ContextVar``.
Dependencies
------------

View file

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

View file

@ -142,7 +142,7 @@ Here is the code for that decorator::
def decorated_function(*args, **kwargs):
template_name = template
if template_name is None:
template_name = f"'{request.endpoint.replace('.', '/')}.html'"
template_name = f"{request.endpoint.replace('.', '/')}.html"
ctx = f(*args, **kwargs)
if ctx is None:
ctx = {}

View file

@ -50,7 +50,7 @@ to tell your terminal the application to work with by exporting the
.. code-block:: text
$ export FLASK_APP=hello.py
$ export FLASK_APP=hello
$ flask run
* Running on http://127.0.0.1:5000/
@ -58,7 +58,7 @@ to tell your terminal the application to work with by exporting the
.. code-block:: text
> set FLASK_APP=hello.py
> set FLASK_APP=hello
> flask run
* Running on http://127.0.0.1:5000/
@ -66,10 +66,16 @@ to tell your terminal the application to work with by exporting the
.. code-block:: text
> $env:FLASK_APP = "hello.py"
> $env:FLASK_APP = "hello"
> flask run
* Running on http://127.0.0.1:5000/
.. admonition:: Application Discovery Behavior
As a shortcut, if the file is named ``app.py`` or ``wsgi.py``, you
don't have to set the ``FLASK_APP`` environment variable. See
:doc:`/cli` for more details.
This launches a very simple builtin server, which is good enough for
testing but probably not what you want to use in production. For
deployment options see :doc:`deploying/index`.
@ -240,7 +246,7 @@ of the argument like ``<converter:variable_name>``. ::
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return f'User {username}'
return f'User {escape(username)}'
@app.route('/post/<int:post_id>')
def show_post(post_id):
@ -250,7 +256,7 @@ of the argument like ``<converter:variable_name>``. ::
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return f'Subpath {subpath}'
return f'Subpath {escape(subpath)}'
Converter types:
@ -438,9 +444,9 @@ Here is an example template:
<h1>Hello, World!</h1>
{% endif %}
Inside templates you also have access to the :class:`~flask.request`,
:class:`~flask.session` and :class:`~flask.g` [#]_ objects
as well as the :func:`~flask.get_flashed_messages` function.
Inside templates you also have access to the :data:`~flask.Flask.config`,
:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions.
Templates are especially useful if inheritance is used. If you want to
know how that works, see :doc:`patterns/templateinheritance`. Basically

View file

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

View file

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

View file

@ -55,7 +55,7 @@ this structure and take full advantage of Flask's flexibility.
.. image:: flaskr_edit.png
:align: center
:class: screenshot
:alt: screenshot of login page
:alt: screenshot of edit page
:gh:`The tutorial project is available as an example in the Flask
repository <examples/tutorial>`, if you want to compare your project

View file

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

View file

@ -11,7 +11,7 @@ Install
**Be sure to use the same version of the code as the version of the docs
you're reading.** You probably want the latest tagged version, but the
default Git version is the master branch. ::
default Git version is the main branch. ::
# clone the repository
$ git clone https://github.com/pallets/flask
@ -35,7 +35,7 @@ Install Flaskr::
$ pip install -e .
Or if you are using the master branch, install Flask from source before
Or if you are using the main branch, install Flask from source before
installing Flaskr::
$ pip install -e ../..

View file

@ -1,5 +1,6 @@
-r docs.in
-r tests.in
-r typing.in
pip-tools
pre-commit
tox

View file

@ -8,33 +8,35 @@ alabaster==0.7.12
# via sphinx
appdirs==1.4.4
# via virtualenv
asgiref==3.3.4
asgiref==3.4.1
# via -r requirements/tests.in
attrs==20.3.0
attrs==21.2.0
# via pytest
babel==2.9.0
babel==2.9.1
# via sphinx
blinker==1.4
# via -r requirements/tests.in
certifi==2020.12.5
# via requests
cfgv==3.2.0
cfgv==3.3.0
# via pre-commit
chardet==4.0.0
# via requests
click==7.1.2
click==8.0.1
# via pip-tools
distlib==0.3.1
# via virtualenv
docutils==0.16
# via sphinx
# via
# sphinx
# sphinx-tabs
filelock==3.0.12
# via
# tox
# virtualenv
greenlet==1.0.0
greenlet==1.1.0
# via -r requirements/tests.in
identify==2.2.3
identify==2.2.4
# via pre-commit
idna==2.10
# via requests
@ -42,10 +44,14 @@ imagesize==1.2.0
# via sphinx
iniconfig==1.1.1
# via pytest
jinja2==2.11.3
jinja2==3.0.1
# via sphinx
markupsafe==1.1.1
markupsafe==2.0.1
# via jinja2
mypy==0.910
# via -r requirements/typing.in
mypy-extensions==0.4.3
# via mypy
nodeenv==1.6.0
# via pre-commit
packaging==20.9
@ -54,31 +60,31 @@ packaging==20.9
# pytest
# sphinx
# tox
pallets-sphinx-themes==2.0.0rc1
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pep517==0.10.0
# via pip-tools
pip-tools==6.1.0
pip-tools==6.2.0
# via -r requirements/dev.in
pluggy==0.13.1
# via
# pytest
# tox
pre-commit==2.12.0
pre-commit==2.13.0
# via -r requirements/dev.in
py==1.10.0
# via
# pytest
# tox
pygments==2.8.1
pygments==2.9.0
# via
# sphinx
# sphinx-tabs
pyparsing==2.4.7
# via packaging
pytest==6.2.3
pytest==6.2.4
# via -r requirements/tests.in
python-dotenv==0.17.0
python-dotenv==0.19.0
# via -r requirements/tests.in
pytz==2021.1
# via babel
@ -86,28 +92,28 @@ pyyaml==5.4.1
# via pre-commit
requests==2.25.1
# via sphinx
six==1.15.0
six==1.16.0
# via
# tox
# virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==2.1.0
# via -r requirements/docs.in
sphinx==3.5.4
sphinx==4.1.2
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.1.0
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
@ -115,22 +121,27 @@ sphinxcontrib-log-cabinet==1.0.1
# via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.4
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
toml==0.10.2
# via
# mypy
# pep517
# pre-commit
# pytest
# tox
tox==3.23.0
tox==3.24.1
# via -r requirements/dev.in
urllib3==1.26.4
typing-extensions==3.10.0.0
# via mypy
urllib3==1.26.5
# via requests
virtualenv==20.4.3
virtualenv==20.4.6
# via
# pre-commit
# tox
wheel==0.36.2
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip

View file

@ -1,4 +1,4 @@
Pallets-Sphinx-Themes >= 2.0.0rc1
Pallets-Sphinx-Themes
Sphinx
sphinx-issues
sphinxcontrib-log-cabinet

View file

@ -6,29 +6,31 @@
#
alabaster==0.7.12
# via sphinx
babel==2.9.0
babel==2.9.1
# via sphinx
certifi==2020.12.5
# via requests
chardet==4.0.0
# via requests
docutils==0.16
# via sphinx
# via
# sphinx
# sphinx-tabs
idna==2.10
# via requests
imagesize==1.2.0
# via sphinx
jinja2==2.11.3
jinja2==3.0.1
# via sphinx
markupsafe==1.1.1
markupsafe==2.0.1
# via jinja2
packaging==20.9
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.0.0rc1
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pygments==2.8.1
pygments==2.9.0
# via
# sphinx
# sphinx-tabs
@ -40,22 +42,22 @@ requests==2.25.1
# via sphinx
snowballstemmer==2.1.0
# via sphinx
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==2.1.0
# via -r requirements/docs.in
sphinx==3.5.4
sphinx==4.1.2
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.1.0
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==1.0.3
sphinxcontrib-htmlhelp==2.0.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
@ -63,9 +65,9 @@ sphinxcontrib-log-cabinet==1.0.1
# via -r requirements/docs.in
sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.4
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
urllib3==1.26.4
urllib3==1.26.5
# via requests
# The following packages are considered to be unsafe in a requirements file:

View file

@ -4,13 +4,13 @@
#
# pip-compile requirements/tests.in
#
asgiref==3.3.4
asgiref==3.4.1
# via -r requirements/tests.in
attrs==20.3.0
attrs==21.2.0
# via pytest
blinker==1.4
# via -r requirements/tests.in
greenlet==1.0.0
greenlet==1.1.0
# via -r requirements/tests.in
iniconfig==1.1.1
# via pytest
@ -22,9 +22,9 @@ py==1.10.0
# via pytest
pyparsing==2.4.7
# via packaging
pytest==6.2.3
pytest==6.2.4
# via -r requirements/tests.in
python-dotenv==0.17.0
python-dotenv==0.19.0
# via -r requirements/tests.in
toml==0.10.2
# via pytest

View file

@ -4,11 +4,11 @@
#
# pip-compile requirements/typing.in
#
mypy==0.910
# via -r requirements/typing.in
mypy-extensions==0.4.3
# via mypy
mypy==0.812
# via -r requirements/typing.in
typed-ast==1.4.3
toml==0.10.2
# via mypy
typing-extensions==3.7.4.3
typing-extensions==3.10.0.0
# via mypy

View file

@ -112,3 +112,6 @@ ignore_missing_imports = True
[mypy-dotenv.*]
ignore_missing_imports = True
[mypy-cryptography.*]
ignore_missing_imports = True

View file

@ -4,13 +4,13 @@ from setuptools import setup
setup(
name="Flask",
install_requires=[
"Werkzeug>=2.0.0rc4",
"Jinja2>=3.0.0rc1",
"itsdangerous>=2.0.0rc2",
"click>=7.1.2",
"Werkzeug >= 2.0",
"Jinja2 >= 3.0",
"itsdangerous >= 2.0",
"click >= 8.0",
],
extras_require={
"async": ["asgiref>=3.2"],
"async": ["asgiref >= 3.2"],
"dotenv": ["python-dotenv"],
},
)

View file

@ -1,46 +1,46 @@
from markupsafe import escape
from markupsafe import Markup
from werkzeug.exceptions import abort
from werkzeug.utils import redirect
from werkzeug.exceptions import abort as abort
from werkzeug.utils import redirect as redirect
from . import json
from .app import Flask
from .app import Request
from .app import Response
from .blueprints import Blueprint
from .config import Config
from .ctx import after_this_request
from .ctx import copy_current_request_context
from .ctx import has_app_context
from .ctx import has_request_context
from .globals import _app_ctx_stack
from .globals import _request_ctx_stack
from .globals import current_app
from .globals import g
from .globals import request
from .globals import session
from .helpers import flash
from .helpers import get_flashed_messages
from .helpers import get_template_attribute
from .helpers import make_response
from .helpers import safe_join
from .helpers import send_file
from .helpers import send_from_directory
from .helpers import stream_with_context
from .helpers import url_for
from .json import jsonify
from .signals import appcontext_popped
from .signals import appcontext_pushed
from .signals import appcontext_tearing_down
from .signals import before_render_template
from .signals import got_request_exception
from .signals import message_flashed
from .signals import request_finished
from .signals import request_started
from .signals import request_tearing_down
from .signals import signals_available
from .signals import template_rendered
from .templating import render_template
from .templating import render_template_string
from . import json as json
from .app import Flask as Flask
from .app import Request as Request
from .app import Response as Response
from .blueprints import Blueprint as Blueprint
from .config import Config as Config
from .ctx import after_this_request as after_this_request
from .ctx import copy_current_request_context as copy_current_request_context
from .ctx import has_app_context as has_app_context
from .ctx import has_request_context as has_request_context
from .globals import _app_ctx_stack as _app_ctx_stack
from .globals import _request_ctx_stack as _request_ctx_stack
from .globals import current_app as current_app
from .globals import g as g
from .globals import request as request
from .globals import session as session
from .helpers import flash as flash
from .helpers import get_flashed_messages as get_flashed_messages
from .helpers import get_template_attribute as get_template_attribute
from .helpers import make_response as make_response
from .helpers import safe_join as safe_join
from .helpers import send_file as send_file
from .helpers import send_from_directory as send_from_directory
from .helpers import stream_with_context as stream_with_context
from .helpers import url_for as url_for
from .json import jsonify as jsonify
from .signals import appcontext_popped as appcontext_popped
from .signals import appcontext_pushed as appcontext_pushed
from .signals import appcontext_tearing_down as appcontext_tearing_down
from .signals import before_render_template as before_render_template
from .signals import got_request_exception as got_request_exception
from .signals import message_flashed as message_flashed
from .signals import request_finished as request_finished
from .signals import request_started as request_started
from .signals import request_tearing_down as request_tearing_down
from .signals import signals_available as signals_available
from .signals import template_rendered as template_rendered
from .templating import render_template as render_template
from .templating import render_template_string as render_template_string
__version__ = "2.0.0rc1"
__version__ = "2.1.0.dev0"

View file

@ -16,6 +16,7 @@ from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import BadRequestKeyError
from werkzeug.exceptions import HTTPException
from werkzeug.exceptions import InternalServerError
from werkzeug.local import ContextVar
from werkzeug.routing import BuildError
from werkzeug.routing import Map
from werkzeug.routing import MapAdapter
@ -35,12 +36,12 @@ from .globals import _request_ctx_stack
from .globals import g
from .globals import request
from .globals import session
from .helpers import _split_blueprint_path
from .helpers import get_debug_flag
from .helpers import get_env
from .helpers import get_flashed_messages
from .helpers import get_load_dotenv
from .helpers import locked_cached_property
from .helpers import run_async
from .helpers import url_for
from .json import jsonify
from .logging import create_logger
@ -58,8 +59,8 @@ from .signals import request_tearing_down
from .templating import DispatchingJinjaLoader
from .templating import Environment
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import ResponseReturnValue
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
@ -74,9 +75,11 @@ from .wrappers import Request
from .wrappers import Response
if t.TYPE_CHECKING:
import typing_extensions as te
from .blueprints import Blueprint
from .testing import FlaskClient
from .testing import FlaskCliRunner
from .typing import ErrorHandlerCallable
if sys.version_info >= (3, 8):
iscoroutinefunction = inspect.iscoroutinefunction
@ -439,7 +442,7 @@ class Flask(Scaffold):
#: :meth:`before_first_request` decorator.
#:
#: .. versionadded:: 0.8
self.before_first_request_funcs: t.List[BeforeRequestCallable] = []
self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = []
#: A list of functions that are called when the application context
#: is destroyed. Since the application context is also torn down
@ -706,7 +709,7 @@ class Flask(Scaffold):
session=session,
g=g,
)
rv.policies["json.dumps_function"] = json.dumps # type: ignore
rv.policies["json.dumps_function"] = json.dumps
return rv
def create_global_jinja_loader(self) -> DispatchingJinjaLoader:
@ -748,7 +751,7 @@ class Flask(Scaffold):
] = self.template_context_processors[None]
reqctx = _request_ctx_stack.top
if reqctx is not None:
for bp in self._request_blueprints():
for bp in request.blueprints:
if bp in self.template_context_processors:
funcs = chain(funcs, self.template_context_processors[bp])
orig_ctx = context.copy()
@ -1019,6 +1022,12 @@ class Flask(Scaffold):
:class:`~flask.blueprints.BlueprintSetupState`. They can be
accessed in :meth:`~flask.Blueprint.record` callbacks.
.. versionchanged:: 2.0.1
The ``name`` option can be used to change the (pre-dotted)
name the blueprint is registered with. This allows the same
blueprint to be registered multiple times with unique names
for ``url_for``.
.. versionadded:: 0.7
"""
blueprint.register(self, options)
@ -1090,17 +1099,17 @@ class Flask(Scaffold):
view_func = view_func.as_view(endpoint)
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if getattr(old_func, "_flask_sync_wrapper", False):
old_func = old_func.__wrapped__ # type: ignore
if old_func is not None and old_func != view_func:
raise AssertionError(
"View function mapping is overwriting an existing"
f" endpoint function: {endpoint}"
)
self.view_functions[endpoint] = self.ensure_sync(view_func)
self.view_functions[endpoint] = view_func
@setupmethod
def template_filter(self, name: t.Optional[str] = None) -> t.Callable:
def template_filter(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
"""A decorator that is used to register custom template filter.
You can specify a name for the filter, otherwise the function
name will be used. Example::
@ -1132,7 +1141,9 @@ class Flask(Scaffold):
self.jinja_env.filters[name or f.__name__] = f
@setupmethod
def template_test(self, name: t.Optional[str] = None) -> t.Callable:
def template_test(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
"""A decorator that is used to register custom template test.
You can specify a name for the test, otherwise the function
name will be used. Example::
@ -1173,7 +1184,9 @@ class Flask(Scaffold):
self.jinja_env.tests[name or f.__name__] = f
@setupmethod
def template_global(self, name: t.Optional[str] = None) -> t.Callable:
def template_global(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
"""A decorator that is used to register a custom template global function.
You can specify a name for the global function, otherwise the function
name will be used. Example::
@ -1209,7 +1222,9 @@ class Flask(Scaffold):
self.jinja_env.globals[name or f.__name__] = f
@setupmethod
def before_first_request(self, f: BeforeRequestCallable) -> BeforeRequestCallable:
def before_first_request(
self, f: BeforeFirstRequestCallable
) -> BeforeFirstRequestCallable:
"""Registers a function to be run before the first request to this
instance of the application.
@ -1218,7 +1233,7 @@ class Flask(Scaffold):
.. versionadded:: 0.8
"""
self.before_first_request_funcs.append(self.ensure_sync(f))
self.before_first_request_funcs.append(f)
return f
@setupmethod
@ -1251,7 +1266,7 @@ class Flask(Scaffold):
.. versionadded:: 0.9
"""
self.teardown_appcontext_funcs.append(self.ensure_sync(f))
self.teardown_appcontext_funcs.append(f)
return f
@setupmethod
@ -1263,7 +1278,9 @@ class Flask(Scaffold):
self.shell_context_processors.append(f)
return f
def _find_error_handler(self, e: Exception) -> t.Optional[ErrorHandlerCallable]:
def _find_error_handler(
self, e: Exception
) -> t.Optional["ErrorHandlerCallable[Exception]"]:
"""Return a registered error handler for an exception in this order:
blueprint handler for a specific code, app handler for a specific code,
blueprint handler for an exception class, app handler for an exception
@ -1271,8 +1288,8 @@ class Flask(Scaffold):
"""
exc_class, code = self._get_exc_class_and_code(type(e))
for c in [code, None]:
for name in chain(self._request_blueprints(), [None]):
for c in [code, None] if code is not None else [None]:
for name in chain(request.blueprints, [None]):
handler_map = self.error_handler_spec[name][c]
if not handler_map:
@ -1299,7 +1316,7 @@ class Flask(Scaffold):
.. versionchanged:: 1.0
Exceptions are looked up by code *and* by MRO, so
``HTTPExcpetion`` subclasses can be handled with a catch-all
``HTTPException`` subclasses can be handled with a catch-all
handler for the base ``HTTPException``.
.. versionadded:: 0.3
@ -1318,7 +1335,7 @@ class Flask(Scaffold):
handler = self._find_error_handler(e)
if handler is None:
return e
return handler(e)
return self.ensure_sync(handler)(e)
def trap_http_exception(self, e: Exception) -> bool:
"""Checks if an HTTP exception should be trapped or not. By default
@ -1385,7 +1402,7 @@ class Flask(Scaffold):
if handler is None:
raise
return handler(e)
return self.ensure_sync(handler)(e)
def handle_exception(self, e: Exception) -> Response:
"""Handle an exception that did not have an error handler
@ -1432,7 +1449,7 @@ class Flask(Scaffold):
handler = self._find_error_handler(server_error)
if handler is not None:
server_error = handler(server_error)
server_error = self.ensure_sync(handler)(server_error)
return self.finalize_request(server_error, from_error_handler=True)
@ -1453,7 +1470,7 @@ class Flask(Scaffold):
f"Exception on {request.path} [{request.method}]", exc_info=exc_info
)
def raise_routing_exception(self, request: Request) -> t.NoReturn:
def raise_routing_exception(self, request: Request) -> "te.NoReturn":
"""Exceptions that are recording during routing are reraised with
this method. During debug we are not reraising redirect requests
for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising
@ -1494,7 +1511,7 @@ class Flask(Scaffold):
):
return self.make_default_options_response()
# otherwise dispatch to the handler for that endpoint
return self.view_functions[rule.endpoint](**req.view_args)
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
def full_dispatch_request(self) -> Response:
"""Dispatches the request and on top of that performs request
@ -1555,7 +1572,7 @@ class Flask(Scaffold):
if self._got_first_request:
return
for func in self.before_first_request_funcs:
func()
self.ensure_sync(func)()
self._got_first_request = True
def make_default_options_response(self) -> Response:
@ -1591,10 +1608,40 @@ class Flask(Scaffold):
.. versionadded:: 2.0
"""
if iscoroutinefunction(func):
return run_async(func)
return self.async_to_sync(func)
return func
def async_to_sync(
self, func: t.Callable[..., t.Coroutine]
) -> t.Callable[..., t.Any]:
"""Return a sync function that will run the coroutine function.
.. code-block:: python
result = app.async_to_sync(func)(*args, **kwargs)
Override this method to change how the app converts async code
to be synchronously callable.
.. versionadded:: 2.0
"""
try:
from asgiref.sync import async_to_sync as asgiref_async_to_sync
except ImportError:
raise RuntimeError(
"Install Flask with the 'async' extra in order to use async views."
)
# Check that Werkzeug isn't using its fallback ContextVar class.
if ContextVar.__module__ == "werkzeug.local":
raise RuntimeError(
"Async cannot be used with this combination of Python "
"and Greenlet versions."
)
return asgiref_async_to_sync(func)
def make_response(self, rv: ResponseReturnValue) -> Response:
"""Convert the return value from a view function to an instance of
:attr:`response_class`.
@ -1763,9 +1810,14 @@ class Flask(Scaffold):
.. versionadded:: 0.7
"""
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None]
if "." in endpoint:
bp = endpoint.rsplit(".", 1)[0]
funcs = chain(funcs, self.url_default_functions[bp])
# This is called by url_for, which can be called outside a
# request, can't use request.blueprints.
bps = _split_blueprint_path(endpoint.rpartition(".")[0])
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
funcs = chain(funcs, bp_funcs)
for func in funcs:
func(endpoint, values)
@ -1806,18 +1858,18 @@ class Flask(Scaffold):
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[
None
]
for bp in self._request_blueprints():
for bp in request.blueprints:
if bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp])
for func in funcs:
func(request.endpoint, request.view_args)
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None]
for bp in self._request_blueprints():
for bp in request.blueprints:
if bp in self.before_request_funcs:
funcs = chain(funcs, self.before_request_funcs[bp])
for func in funcs:
rv = func()
rv = self.ensure_sync(func)()
if rv is not None:
return rv
@ -1838,13 +1890,13 @@ class Flask(Scaffold):
"""
ctx = _request_ctx_stack.top
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
for bp in self._request_blueprints():
for bp in request.blueprints:
if bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
response = self.ensure_sync(handler)(response)
if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response)
return response
@ -1877,11 +1929,11 @@ class Flask(Scaffold):
funcs: t.Iterable[TeardownCallable] = reversed(
self.teardown_request_funcs[None]
)
for bp in self._request_blueprints():
for bp in request.blueprints:
if bp in self.teardown_request_funcs:
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
for func in funcs:
func(exc)
self.ensure_sync(func)(exc)
request_tearing_down.send(self, exc=exc)
def do_teardown_appcontext(
@ -1904,7 +1956,7 @@ class Flask(Scaffold):
if exc is _sentinel:
exc = sys.exc_info()[1]
for func in reversed(self.teardown_appcontext_funcs):
func(exc)
self.ensure_sync(func)(exc)
appcontext_tearing_down.send(self, exc=exc)
def app_context(self) -> AppContext:
@ -2049,9 +2101,3 @@ class Flask(Scaffold):
wrapped to apply middleware.
"""
return self.wsgi_app(environ, start_response)
def _request_blueprints(self) -> t.Iterable[str]:
if _request_ctx_stack.top.request.blueprint is None:
return []
else:
return reversed(_request_ctx_stack.top.request.blueprint.split("."))

View file

@ -6,8 +6,8 @@ from .scaffold import _endpoint_from_view_func
from .scaffold import _sentinel
from .scaffold import Scaffold
from .typing import AfterRequestCallable
from .typing import BeforeFirstRequestCallable
from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import TemplateFilterCallable
@ -20,6 +20,7 @@ from .views import View
if t.TYPE_CHECKING:
from .app import Flask
from .typing import ErrorHandlerCallable
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
@ -69,6 +70,7 @@ class BlueprintSetupState:
#: blueprint.
self.url_prefix = url_prefix
self.name = self.options.get("name", blueprint.name)
self.name_prefix = self.options.get("name_prefix", "")
#: A dictionary with URL defaults that is added to each and every
@ -103,9 +105,10 @@ class BlueprintSetupState:
defaults = dict(defaults, **options.pop("defaults"))
if isinstance(view_func, type) and issubclass(view_func, View):
view_func = view_func.as_view(endpoint)
self.app.add_url_rule(
rule,
f"{self.name_prefix}{self.blueprint.name}.{endpoint}",
f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."),
view_func,
defaults=defaults,
**options,
@ -195,6 +198,10 @@ class Blueprint(Scaffold):
template_folder=template_folder,
root_path=root_path,
)
if "." in name:
raise ValueError("'name' may not contain a dot '.' character.")
self.name = name
self.url_prefix = url_prefix
self.subdomain = subdomain
@ -255,39 +262,74 @@ class Blueprint(Scaffold):
arguments passed to this method will override the defaults set
on the blueprint.
.. versionchanged:: 2.0.1
The ``name`` option can be used to change the (pre-dotted)
name the blueprint is registered with. This allows the same
blueprint to be registered multiple times with unique names
for ``url_for``.
.. versionadded:: 2.0
"""
if blueprint is self:
raise ValueError("Cannot register a blueprint on itself")
self._blueprints.append((blueprint, options))
def register(self, app: "Flask", options: dict) -> None:
"""Called by :meth:`Flask.register_blueprint` to register all
views and callbacks registered on the blueprint with the
application. Creates a :class:`.BlueprintSetupState` and calls
each :meth:`record` callbackwith it.
each :meth:`record` callback with it.
:param app: The application this blueprint is being registered
with.
:param options: Keyword arguments forwarded from
:meth:`~Flask.register_blueprint`.
:param first_registration: Whether this is the first time this
blueprint has been registered on the application.
.. versionchanged:: 2.0.1
Nested blueprints are registered with their dotted name.
This allows different blueprints with the same name to be
nested at different locations.
.. versionchanged:: 2.0.1
The ``name`` option can be used to change the (pre-dotted)
name the blueprint is registered with. This allows the same
blueprint to be registered multiple times with unique names
for ``url_for``.
.. versionchanged:: 2.0.1
Registering the same blueprint with the same name multiple
times is deprecated and will become an error in Flask 2.1.
"""
first_registration = False
name_prefix = options.get("name_prefix", "")
self_name = options.get("name", self.name)
name = f"{name_prefix}.{self_name}".lstrip(".")
if self.name in app.blueprints:
assert app.blueprints[self.name] is self, (
"A name collision occurred between blueprints"
f" {self!r} and {app.blueprints[self.name]!r}."
f" Both share the same name {self.name!r}."
f" Blueprints that are created on the fly need unique"
f" names."
)
else:
app.blueprints[self.name] = self
first_registration = True
if name in app.blueprints:
existing_at = f" '{name}'" if self_name != name else ""
if app.blueprints[name] is not self:
raise ValueError(
f"The name '{self_name}' is already registered for"
f" a different blueprint{existing_at}. Use 'name='"
" to provide a unique name."
)
else:
import warnings
warnings.warn(
f"The name '{self_name}' is already registered for"
f" this blueprint{existing_at}. Use 'name=' to"
" provide a unique name. This will become an error"
" in Flask 2.1.",
stacklevel=4,
)
first_bp_registration = not any(bp is self for bp in app.blueprints.values())
first_name_registration = name not in app.blueprints
app.blueprints[name] = self
self._got_registered_once = True
state = self.make_setup_state(app, options, first_registration)
state = self.make_setup_state(app, options, first_bp_registration)
if self.has_static_folder:
state.add_url_rule(
@ -297,25 +339,20 @@ class Blueprint(Scaffold):
)
# Merge blueprint data into parent.
if first_registration:
if first_bp_registration or first_name_registration:
def extend(bp_dict, parent_dict, ensure_sync=False):
def extend(bp_dict, parent_dict):
for key, values in bp_dict.items():
key = self.name if key is None else f"{self.name}.{key}"
if ensure_sync:
values = [app.ensure_sync(func) for func in values]
key = name if key is None else f"{name}.{key}"
parent_dict[key].extend(values)
for key, value in self.error_handler_spec.items():
key = self.name if key is None else f"{self.name}.{key}"
key = name if key is None else f"{name}.{key}"
value = defaultdict(
dict,
{
code: {
exc_class: app.ensure_sync(func)
for exc_class, func in code_values.items()
exc_class: func for exc_class, func in code_values.items()
}
for code, code_values in value.items()
},
@ -323,16 +360,13 @@ class Blueprint(Scaffold):
app.error_handler_spec[key] = value
for endpoint, func in self.view_functions.items():
app.view_functions[endpoint] = app.ensure_sync(func)
app.view_functions[endpoint] = func
extend(
self.before_request_funcs, app.before_request_funcs, ensure_sync=True
)
extend(self.after_request_funcs, app.after_request_funcs, ensure_sync=True)
extend(self.before_request_funcs, app.before_request_funcs)
extend(self.after_request_funcs, app.after_request_funcs)
extend(
self.teardown_request_funcs,
app.teardown_request_funcs,
ensure_sync=True,
)
extend(self.url_default_functions, app.url_default_functions)
extend(self.url_value_preprocessors, app.url_value_preprocessors)
@ -347,21 +381,29 @@ class Blueprint(Scaffold):
if cli_resolved_group is None:
app.cli.commands.update(self.cli.commands)
elif cli_resolved_group is _sentinel:
self.cli.name = self.name
self.cli.name = name
app.cli.add_command(self.cli)
else:
self.cli.name = cli_resolved_group
app.cli.add_command(self.cli)
for blueprint, bp_options in self._blueprints:
url_prefix = options.get("url_prefix", "")
if "url_prefix" in bp_options:
url_prefix = (
url_prefix.rstrip("/") + "/" + bp_options["url_prefix"].lstrip("/")
)
bp_options = bp_options.copy()
bp_url_prefix = bp_options.get("url_prefix")
bp_options["url_prefix"] = url_prefix
bp_options["name_prefix"] = options.get("name_prefix", "") + self.name + "."
if bp_url_prefix is None:
bp_url_prefix = blueprint.url_prefix
if state.url_prefix is not None and bp_url_prefix is not None:
bp_options["url_prefix"] = (
state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/")
)
elif bp_url_prefix is not None:
bp_options["url_prefix"] = bp_url_prefix
elif state.url_prefix is not None:
bp_options["url_prefix"] = state.url_prefix
bp_options["name_prefix"] = name
blueprint.register(app, bp_options)
def add_url_rule(
@ -369,20 +411,31 @@ class Blueprint(Scaffold):
rule: str,
endpoint: t.Optional[str] = None,
view_func: t.Optional[t.Callable] = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any,
) -> None:
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
the :func:`url_for` function is prefixed with the name of the blueprint.
"""
if endpoint:
assert "." not in endpoint, "Blueprint endpoints should not contain dots"
if view_func and hasattr(view_func, "__name__"):
assert (
"." not in view_func.__name__
), "Blueprint view function name should not contain dots"
self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options))
if endpoint and "." in endpoint:
raise ValueError("'endpoint' may not contain a dot '.' character.")
def app_template_filter(self, name: t.Optional[str] = None) -> t.Callable:
if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__:
raise ValueError("'view_func' name may not contain a dot '.' character.")
self.record(
lambda s: s.add_url_rule(
rule,
endpoint,
view_func,
provide_automatic_options=provide_automatic_options,
**options,
)
)
def app_template_filter(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateFilterCallable], TemplateFilterCallable]:
"""Register a custom template filter, available application wide. Like
:meth:`Flask.template_filter` but for a blueprint.
@ -412,7 +465,9 @@ class Blueprint(Scaffold):
self.record_once(register_template)
def app_template_test(self, name: t.Optional[str] = None) -> t.Callable:
def app_template_test(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateTestCallable], TemplateTestCallable]:
"""Register a custom template test, available application wide. Like
:meth:`Flask.template_test` but for a blueprint.
@ -446,7 +501,9 @@ class Blueprint(Scaffold):
self.record_once(register_template)
def app_template_global(self, name: t.Optional[str] = None) -> t.Callable:
def app_template_global(
self, name: t.Optional[str] = None
) -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
"""Register a custom template global, available application wide. Like
:meth:`Flask.template_global` but for a blueprint.
@ -485,21 +542,17 @@ class Blueprint(Scaffold):
before each request, even if outside of a blueprint.
"""
self.record_once(
lambda s: s.app.before_request_funcs.setdefault(None, []).append(
s.app.ensure_sync(f)
)
lambda s: s.app.before_request_funcs.setdefault(None, []).append(f)
)
return f
def before_app_first_request(
self, f: BeforeRequestCallable
) -> BeforeRequestCallable:
self, f: BeforeFirstRequestCallable
) -> BeforeFirstRequestCallable:
"""Like :meth:`Flask.before_first_request`. Such a function is
executed before the first request to the application.
"""
self.record_once(
lambda s: s.app.before_first_request_funcs.append(s.app.ensure_sync(f))
)
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
return f
def after_app_request(self, f: AfterRequestCallable) -> AfterRequestCallable:
@ -507,9 +560,7 @@ class Blueprint(Scaffold):
is executed after each request, even if outside of the blueprint.
"""
self.record_once(
lambda s: s.app.after_request_funcs.setdefault(None, []).append(
s.app.ensure_sync(f)
)
lambda s: s.app.after_request_funcs.setdefault(None, []).append(f)
)
return f
@ -539,7 +590,9 @@ class Blueprint(Scaffold):
handler is used for all requests, even if outside of the blueprint.
"""
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
def decorator(
f: "ErrorHandlerCallable[Exception]",
) -> "ErrorHandlerCallable[Exception]":
self.record_once(lambda s: s.app.errorhandler(code)(f))
return f
@ -560,14 +613,3 @@ class Blueprint(Scaffold):
lambda s: s.app.url_default_functions.setdefault(None, []).append(f)
)
return f
def ensure_sync(self, f: t.Callable) -> t.Callable:
"""Ensure the function is synchronous.
Override if you would like custom async to sync behaviour in
this blueprint. Otherwise the app's
:meth:`~flask.Flask.ensure_sync` is used.
.. versionadded:: 2.0
"""
return f

View file

@ -202,6 +202,31 @@ class Config(dict):
return self.from_mapping(obj)
def from_json(self, filename: str, silent: bool = False) -> bool:
"""Update the values in the config from a JSON file. The loaded
data is passed to the :meth:`from_mapping` method.
:param filename: The path to the JSON file. This can be an
absolute path or relative to the config root path.
:param silent: Ignore the file if it doesn't exist.
.. deprecated:: 2.0.0
Will be removed in Flask 2.1. Use :meth:`from_file` instead.
This was removed early in 2.0.0, was added back in 2.0.1.
.. versionadded:: 0.11
"""
import warnings
from . import json
warnings.warn(
"'from_json' is deprecated and will be removed in Flask"
" 2.1. Use 'from_file(path, json.load)' instead.",
DeprecationWarning,
stacklevel=2,
)
return self.from_file(filename, json.load, silent=silent)
def from_mapping(
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
) -> bool:

View file

@ -41,6 +41,24 @@ class _AppCtxGlobals:
.. versionadded:: 0.10
"""
# Define attr methods to let mypy know this is a namespace object
# that has arbitrary attributes.
def __getattr__(self, name: str) -> t.Any:
try:
return self.__dict__[name]
except KeyError:
raise AttributeError(name) from None
def __setattr__(self, name: str, value: t.Any) -> None:
self.__dict__[name] = value
def __delattr__(self, name: str) -> None:
try:
del self.__dict__[name]
except KeyError:
raise AttributeError(name) from None
def get(self, name: str, default: t.Optional[t.Any] = None) -> t.Any:
"""Get an attribute by name, or a default value. Like
:meth:`dict.get`.
@ -78,10 +96,10 @@ class _AppCtxGlobals:
"""
return self.__dict__.setdefault(name, default)
def __contains__(self, item: t.Any) -> bool:
def __contains__(self, item: str) -> bool:
return item in self.__dict__
def __iter__(self) -> t.Iterator:
def __iter__(self) -> t.Iterator[str]:
return iter(self.__dict__)
def __repr__(self) -> str:
@ -377,9 +395,6 @@ class RequestContext:
_request_ctx_stack.push(self)
if self.url_adapter is not None:
self.match_request()
# Open the session at the moment that the request context is available.
# This allows a custom open_session method to use the request context.
# Only open a new session if this is the first time the request was
@ -391,6 +406,11 @@ class RequestContext:
if self.session is None:
self.session = session_interface.make_null_session(self.app)
# Match the request URL after loading the session, so that the
# session is available in custom URL converters.
if self.url_adapter is not None:
self.match_request()
def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None: # type: ignore
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the

View file

@ -6,7 +6,7 @@ from werkzeug.local import LocalStack
if t.TYPE_CHECKING:
from .app import Flask
from .ctx import AppContext
from .ctx import _AppCtxGlobals
from .sessions import SessionMixin
from .wrappers import Request
@ -53,5 +53,7 @@ _request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app: "Flask" = LocalProxy(_find_app) # type: ignore
request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) # type: ignore
session: "SessionMixin" = LocalProxy(partial(_lookup_req_object, "session")) # type: ignore # noqa: B950
g: "AppContext" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore
session: "SessionMixin" = LocalProxy( # type: ignore
partial(_lookup_req_object, "session")
)
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) # type: ignore

View file

@ -4,14 +4,14 @@ import socket
import sys
import typing as t
import warnings
from datetime import datetime
from datetime import timedelta
from functools import lru_cache
from functools import update_wrapper
from functools import wraps
from threading import RLock
import werkzeug.utils
from werkzeug.exceptions import NotFound
from werkzeug.local import ContextVar
from werkzeug.routing import BuildError
from werkzeug.urls import url_quote
@ -64,8 +64,10 @@ def get_load_dotenv(default: bool = True) -> bool:
def stream_with_context(
generator_or_function: t.Union[t.Generator, t.Callable]
) -> t.Generator:
generator_or_function: t.Union[
t.Iterator[t.AnyStr], t.Callable[..., t.Iterator[t.AnyStr]]
]
) -> t.Iterator[t.AnyStr]:
"""Request contexts disappear when the response is started on the server.
This is done for efficiency reasons and to make it less likely to encounter
memory leaks with badly written WSGI middlewares. The downside is that if
@ -438,14 +440,16 @@ def get_flashed_messages(
def _prepare_send_file_kwargs(
download_name=None,
attachment_filename=None,
etag=None,
add_etags=None,
max_age=None,
cache_timeout=None,
**kwargs,
):
download_name: t.Optional[str] = None,
attachment_filename: t.Optional[str] = None,
etag: t.Optional[t.Union[bool, str]] = None,
add_etags: t.Optional[t.Union[bool]] = None,
max_age: t.Optional[
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
] = None,
cache_timeout: t.Optional[int] = None,
**kwargs: t.Any,
) -> t.Dict[str, t.Any]:
if attachment_filename is not None:
warnings.warn(
"The 'attachment_filename' parameter has been renamed to"
@ -484,23 +488,25 @@ def _prepare_send_file_kwargs(
max_age=max_age,
use_x_sendfile=current_app.use_x_sendfile,
response_class=current_app.response_class,
_root_path=current_app.root_path,
_root_path=current_app.root_path, # type: ignore
)
return kwargs
def send_file(
path_or_file,
mimetype=None,
as_attachment=False,
download_name=None,
attachment_filename=None,
conditional=True,
etag=True,
add_etags=None,
last_modified=None,
max_age=None,
cache_timeout=None,
path_or_file: t.Union[os.PathLike, str, t.BinaryIO],
mimetype: t.Optional[str] = None,
as_attachment: bool = False,
download_name: t.Optional[str] = None,
attachment_filename: t.Optional[str] = None,
conditional: bool = True,
etag: t.Union[bool, str] = True,
add_etags: t.Optional[bool] = None,
last_modified: t.Optional[t.Union[datetime, int, float]] = None,
max_age: t.Optional[
t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]]
] = None,
cache_timeout: t.Optional[int] = None,
):
"""Send the contents of a file to the client.
@ -644,7 +650,12 @@ def safe_join(directory: str, *pathnames: str) -> str:
return path
def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response":
def send_from_directory(
directory: t.Union[os.PathLike, str],
path: t.Union[os.PathLike, str],
filename: t.Optional[str] = None,
**kwargs: t.Any,
) -> "Response":
"""Send a file from within a directory using :func:`send_file`.
.. code-block:: python
@ -668,12 +679,24 @@ def send_from_directory(directory: str, path: str, **kwargs: t.Any) -> "Response
``directory``.
:param kwargs: Arguments to pass to :func:`send_file`.
.. versionchanged:: 2.0
``path`` replaces the ``filename`` parameter.
.. versionadded:: 2.0
Moved the implementation to Werkzeug. This is now a wrapper to
pass some Flask-specific arguments.
.. versionadded:: 0.5
"""
if filename is not None:
warnings.warn(
"The 'filename' parameter has been renamed to 'path'. The"
" old name will be removed in Flask 2.1.",
DeprecationWarning,
stacklevel=2,
)
path = filename
return werkzeug.utils.send_from_directory( # type: ignore
directory, path, **_prepare_send_file_kwargs(**kwargs)
)
@ -803,49 +826,11 @@ def is_ip(value: str) -> bool:
return False
def run_async(func: t.Callable[..., t.Coroutine]) -> t.Callable[..., t.Any]:
"""Return a sync function that will run the coroutine function *func*."""
try:
from asgiref.sync import async_to_sync
except ImportError:
raise RuntimeError(
"Install Flask with the 'async' extra in order to use async views."
)
@lru_cache(maxsize=None)
def _split_blueprint_path(name: str) -> t.List[str]:
out: t.List[str] = [name]
# Check that Werkzeug isn't using its fallback ContextVar class.
if ContextVar.__module__ == "werkzeug.local":
raise RuntimeError(
"Async cannot be used with this combination of Python & Greenlet versions."
)
if "." in name:
out.extend(_split_blueprint_path(name.rpartition(".")[0]))
@wraps(func)
def outer(*args: t.Any, **kwargs: t.Any) -> t.Any:
"""This function grabs the current context for the inner function.
This is similar to the copy_current_xxx_context functions in the
ctx module, except it has an async inner.
"""
ctx = None
if _request_ctx_stack.top is not None:
ctx = _request_ctx_stack.top.copy()
@wraps(func)
async def inner(*a: t.Any, **k: t.Any) -> t.Any:
"""This restores the context before awaiting the func.
This is required as the function must be awaited within the
context. Only calling ``func`` (as per the
``copy_current_xxx_context`` functions) doesn't work as the
with block will close before the coroutine is awaited.
"""
if ctx is not None:
with ctx:
return await func(*a, **k)
else:
return await func(*a, **k)
return async_to_sync(inner)(*args, **kwargs)
outer._flask_sync_wrapper = True # type: ignore
return outer
return out

View file

@ -5,7 +5,7 @@ import uuid
import warnings
from datetime import date
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps # type: ignore
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
from werkzeug.http import http_date
from ..globals import current_app

View file

@ -21,7 +21,7 @@ from .templating import _default_template_ctx_processor
from .typing import AfterRequestCallable
from .typing import AppOrBlueprintKey
from .typing import BeforeRequestCallable
from .typing import ErrorHandlerCallable
from .typing import GenericException
from .typing import TeardownCallable
from .typing import TemplateContextProcessorCallable
from .typing import URLDefaultCallable
@ -29,12 +29,15 @@ from .typing import URLValuePreprocessorCallable
if t.TYPE_CHECKING:
from .wrappers import Response
from .typing import ErrorHandlerCallable
# a singleton sentinel value for parameter defaults
_sentinel = object()
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
def setupmethod(f: t.Callable) -> t.Callable:
def setupmethod(f: F) -> F:
"""Wraps a method so that it performs a check in debug mode if the
first request was already handled.
"""
@ -53,7 +56,7 @@ def setupmethod(f: t.Callable) -> t.Callable:
)
return f(self, *args, **kwargs)
return update_wrapper(wrapper_func, f)
return t.cast(F, update_wrapper(wrapper_func, f))
class Scaffold:
@ -142,7 +145,10 @@ class Scaffold:
#: directly and its format may change at any time.
self.error_handler_spec: t.Dict[
AppOrBlueprintKey,
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ErrorHandlerCallable]],
t.Dict[
t.Optional[int],
t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
],
] = defaultdict(lambda: defaultdict(dict))
#: A data structure of functions to call at the beginning of
@ -288,7 +294,7 @@ class Scaffold:
self._static_url_path = value
def get_send_file_max_age(self, filename: str) -> t.Optional[int]:
def get_send_file_max_age(self, filename: t.Optional[str]) -> t.Optional[int]:
"""Used by :func:`send_file` to determine the ``max_age`` cache
value for a given file path if it wasn't passed.
@ -446,7 +452,7 @@ class Scaffold:
view_func: t.Optional[t.Callable] = None,
provide_automatic_options: t.Optional[bool] = None,
**options: t.Any,
) -> t.Callable:
) -> None:
"""Register a rule for routing incoming requests and building
URLs. The :meth:`route` decorator is a shortcut to call this
with the ``view_func`` argument. These are equivalent:
@ -524,7 +530,7 @@ class Scaffold:
"""
def decorator(f):
self.view_functions[endpoint] = self.ensure_sync(f)
self.view_functions[endpoint] = f
return f
return decorator
@ -548,7 +554,7 @@ class Scaffold:
return value from the view, and further request handling is
stopped.
"""
self.before_request_funcs.setdefault(None, []).append(self.ensure_sync(f))
self.before_request_funcs.setdefault(None, []).append(f)
return f
@setupmethod
@ -564,7 +570,7 @@ class Scaffold:
should not be used for actions that must execute, such as to
close resources. Use :meth:`teardown_request` for that.
"""
self.after_request_funcs.setdefault(None, []).append(self.ensure_sync(f))
self.after_request_funcs.setdefault(None, []).append(f)
return f
@setupmethod
@ -603,7 +609,7 @@ class Scaffold:
debugger can still access it. This behavior can be controlled
by the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable.
"""
self.teardown_request_funcs.setdefault(None, []).append(self.ensure_sync(f))
self.teardown_request_funcs.setdefault(None, []).append(f)
return f
@setupmethod
@ -644,8 +650,11 @@ class Scaffold:
@setupmethod
def errorhandler(
self, code_or_exception: t.Union[t.Type[Exception], int]
) -> t.Callable:
self, code_or_exception: t.Union[t.Type[GenericException], int]
) -> t.Callable[
["ErrorHandlerCallable[GenericException]"],
"ErrorHandlerCallable[GenericException]",
]:
"""Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an
@ -675,7 +684,9 @@ class Scaffold:
an arbitrary exception
"""
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
def decorator(
f: "ErrorHandlerCallable[GenericException]",
) -> "ErrorHandlerCallable[GenericException]":
self.register_error_handler(code_or_exception, f)
return f
@ -684,8 +695,8 @@ class Scaffold:
@setupmethod
def register_error_handler(
self,
code_or_exception: t.Union[t.Type[Exception], int],
f: ErrorHandlerCallable,
code_or_exception: t.Union[t.Type[GenericException], int],
f: "ErrorHandlerCallable[GenericException]",
) -> None:
"""Alternative error attach function to the :meth:`errorhandler`
decorator that is more straightforward to use for non decorator
@ -709,7 +720,9 @@ class Scaffold:
" instead."
)
self.error_handler_spec[None][code][exc_class] = self.ensure_sync(f)
self.error_handler_spec[None][code][exc_class] = t.cast(
"ErrorHandlerCallable[Exception]", f
)
@staticmethod
def _get_exc_class_and_code(
@ -737,9 +750,6 @@ class Scaffold:
else:
return exc_class, None
def ensure_sync(self, func: t.Callable) -> t.Callable:
raise NotImplementedError()
def _endpoint_from_view_func(view_func: t.Callable) -> str:
"""Internal helper that returns the default endpoint for a given

View file

@ -12,6 +12,7 @@ from .helpers import is_ip
from .json.tag import TaggedJSONSerializer
if t.TYPE_CHECKING:
import typing_extensions as te
from .app import Flask
from .wrappers import Request, Response
@ -92,7 +93,7 @@ class NullSession(SecureCookieSession):
but fail on setting.
"""
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
def _fail(self, *args: t.Any, **kwargs: t.Any) -> "te.NoReturn":
raise RuntimeError(
"The session is unavailable because no secret "
"key was set. Set the secret_key on the "

View file

@ -51,18 +51,21 @@ class DispatchingJinjaLoader(BaseLoader):
def __init__(self, app: "Flask") -> None:
self.app = app
def get_source(
def get_source( # type: ignore
self, environment: Environment, template: str
) -> t.Tuple[str, t.Optional[str], t.Callable]:
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
return self._get_source_explained(environment, template)
return self._get_source_fast(environment, template)
def _get_source_explained(
self, environment: Environment, template: str
) -> t.Tuple[str, t.Optional[str], t.Callable]:
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
attempts = []
trv = None
rv: t.Optional[t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]]
trv: t.Optional[
t.Tuple[str, t.Optional[str], t.Optional[t.Callable[[], bool]]]
] = None
for srcobj, loader in self._iter_loaders(template):
try:
@ -83,7 +86,7 @@ class DispatchingJinjaLoader(BaseLoader):
def _get_source_fast(
self, environment: Environment, template: str
) -> t.Tuple[str, t.Optional[str], t.Callable]:
) -> t.Tuple[str, t.Optional[str], t.Optional[t.Callable]]:
for _srcobj, loader in self._iter_loaders(template):
try:
return loader.get_source(environment, template)

View file

@ -2,8 +2,8 @@ import typing as t
if t.TYPE_CHECKING:
from _typeshed.wsgi import WSGIApplication # noqa: F401
from werkzeug.datastructures import Headers # noqa: F401
from wsgiref.types import WSGIApplication # noqa: F401
from .wrappers import Response # noqa: F401
from .views import View # noqa: F401
@ -34,15 +34,25 @@ ResponseReturnValue = t.Union[
"WSGIApplication",
]
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
AfterRequestCallable = t.Callable[["Response"], "Response"]
BeforeRequestCallable = t.Callable[[], None]
ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
TeardownCallable = t.Callable[[t.Optional[BaseException]], "Response"]
BeforeFirstRequestCallable = t.Callable[[], None]
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
TemplateFilterCallable = t.Callable[[t.Any], str]
TemplateGlobalCallable = t.Callable[[], t.Any]
TemplateTestCallable = t.Callable[[t.Any], bool]
TemplateFilterCallable = t.Callable[..., t.Any]
TemplateGlobalCallable = t.Callable[..., t.Any]
TemplateTestCallable = t.Callable[..., bool]
URLDefaultCallable = t.Callable[[str, dict], None]
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
ViewFuncArgument = t.Optional[t.Union[t.Callable, t.Type["View"]]]
if t.TYPE_CHECKING:
import typing_extensions as te
class ErrorHandlerCallable(te.Protocol[GenericException]):
def __call__(self, error: GenericException) -> ResponseReturnValue:
...

View file

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

View file

@ -6,8 +6,10 @@ from werkzeug.wrappers import Response as ResponseBase
from . import json
from .globals import current_app
from .helpers import _split_blueprint_path
if t.TYPE_CHECKING:
import typing_extensions as te
from werkzeug.routing import Rule
@ -58,23 +60,54 @@ class Request(RequestBase):
@property
def endpoint(self) -> t.Optional[str]:
"""The endpoint that matched the request. This in combination with
:attr:`view_args` can be used to reconstruct the same or a
modified URL. If an exception happened when matching, this will
be ``None``.
"""The endpoint that matched the request URL.
This will be ``None`` if matching failed or has not been
performed yet.
This in combination with :attr:`view_args` can be used to
reconstruct the same URL or a modified URL.
"""
if self.url_rule is not None:
return self.url_rule.endpoint
else:
return None
return None
@property
def blueprint(self) -> t.Optional[str]:
"""The name of the current blueprint"""
if self.url_rule and "." in self.url_rule.endpoint:
return self.url_rule.endpoint.rsplit(".", 1)[0]
else:
return None
"""The registered name of the current blueprint.
This will be ``None`` if the endpoint is not part of a
blueprint, or if URL matching failed or has not been performed
yet.
This does not necessarily match the name the blueprint was
created with. It may have been nested, or registered with a
different name.
"""
endpoint = self.endpoint
if endpoint is not None and "." in endpoint:
return endpoint.rpartition(".")[0]
return None
@property
def blueprints(self) -> t.List[str]:
"""The registered names of the current blueprint upwards through
parent blueprints.
This will be an empty list if there is no current blueprint, or
if URL matching failed.
.. versionadded:: 2.0.1
"""
name = self.blueprint
if name is None:
return []
return _split_blueprint_path(name)
def _load_form_data(self) -> None:
RequestBase._load_form_data(self)
@ -91,7 +124,7 @@ class Request(RequestBase):
attach_enctype_error_multidict(self)
def on_json_loading_failed(self, e: Exception) -> t.NoReturn:
def on_json_loading_failed(self, e: Exception) -> "te.NoReturn":
if current_app and current_app.debug:
raise BadRequest(f"Failed to decode JSON object: {e}")

View file

@ -6,7 +6,8 @@ import pytest
from flask import Blueprint
from flask import Flask
from flask import request
from flask.helpers import run_async
from flask.views import MethodView
from flask.views import View
pytest.importorskip("asgiref")
@ -19,6 +20,24 @@ class BlueprintError(Exception):
pass
class AsyncView(View):
methods = ["GET", "POST"]
async def dispatch_request(self):
await asyncio.sleep(0)
return request.method
class AsyncMethodView(MethodView):
async def get(self):
await asyncio.sleep(0)
return "GET"
async def post(self):
await asyncio.sleep(0)
return "POST"
@pytest.fixture(name="async_app")
def _async_app():
app = Flask(__name__)
@ -54,11 +73,14 @@ def _async_app():
app.register_blueprint(blueprint, url_prefix="/bp")
app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
return app
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
@pytest.mark.parametrize("path", ["/", "/home", "/bp/"])
@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
def test_async_route(path, async_app):
test_client = async_app.test_client()
response = test_client.get(path)
@ -136,5 +158,6 @@ def test_async_before_after_request():
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7")
def test_async_runtime_error():
app = Flask(__name__)
with pytest.raises(RuntimeError):
run_async(None)
app.async_to_sync(None)

View file

@ -1448,7 +1448,6 @@ def test_static_url_empty_path_default(app):
rv.close()
@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires Python >= 3.6")
def test_static_folder_with_pathlib_path(app):
from pathlib import Path
@ -1631,7 +1630,7 @@ def test_url_processors(app, client):
def test_inject_blueprint_url_defaults(app):
bp = flask.Blueprint("foo.bar.baz", __name__, template_folder="template")
bp = flask.Blueprint("foo", __name__, template_folder="template")
@bp.url_defaults
def bp_defaults(endpoint, values):
@ -1644,12 +1643,12 @@ def test_inject_blueprint_url_defaults(app):
app.register_blueprint(bp)
values = dict()
app.inject_url_defaults("foo.bar.baz.view", values)
app.inject_url_defaults("foo.view", values)
expected = dict(page="login")
assert values == expected
with app.test_request_context("/somepage"):
url = flask.url_for("foo.bar.baz.view")
url = flask.url_for("foo.view")
expected = "/login"
assert url == expected

View file

@ -1,5 +1,3 @@
import functools
import pytest
from jinja2 import TemplateNotFound
from werkzeug.http import parse_cache_control_header
@ -142,7 +140,7 @@ def test_blueprint_url_defaults(app, client):
return str(bar)
app.register_blueprint(bp, url_prefix="/1", url_defaults={"bar": 23})
app.register_blueprint(bp, url_prefix="/2", url_defaults={"bar": 19})
app.register_blueprint(bp, name="test2", url_prefix="/2", url_defaults={"bar": 19})
assert client.get("/1/foo").data == b"23/42"
assert client.get("/2/foo").data == b"19/42"
@ -253,28 +251,9 @@ def test_templates_list(test_apps):
assert templates == ["admin/index.html", "frontend/index.html"]
def test_dotted_names(app, client):
frontend = flask.Blueprint("myapp.frontend", __name__)
backend = flask.Blueprint("myapp.backend", __name__)
@frontend.route("/fe")
def frontend_index():
return flask.url_for("myapp.backend.backend_index")
@frontend.route("/fe2")
def frontend_page2():
return flask.url_for(".frontend_index")
@backend.route("/be")
def backend_index():
return flask.url_for("myapp.frontend.frontend_index")
app.register_blueprint(frontend)
app.register_blueprint(backend)
assert client.get("/fe").data.strip() == b"/be"
assert client.get("/fe2").data.strip() == b"/fe"
assert client.get("/be").data.strip() == b"/fe"
def test_dotted_name_not_allowed(app, client):
with pytest.raises(ValueError):
flask.Blueprint("app.ui", __name__)
def test_dotted_names_from_app(app, client):
@ -343,62 +322,19 @@ def test_route_decorator_custom_endpoint(app, client):
def test_route_decorator_custom_endpoint_with_dots(app, client):
bp = flask.Blueprint("bp", __name__)
@bp.route("/foo")
def foo():
return flask.request.endpoint
with pytest.raises(ValueError):
bp.route("/", endpoint="a.b")(lambda: "")
try:
with pytest.raises(ValueError):
bp.add_url_rule("/", endpoint="a.b")
@bp.route("/bar", endpoint="bar.bar")
def foo_bar():
return flask.request.endpoint
def view():
return ""
except AssertionError:
pass
else:
raise AssertionError("expected AssertionError not raised")
view.__name__ = "a.b"
try:
@bp.route("/bar/123", endpoint="bar.123")
def foo_bar_foo():
return flask.request.endpoint
except AssertionError:
pass
else:
raise AssertionError("expected AssertionError not raised")
def foo_foo_foo():
pass
pytest.raises(
AssertionError,
lambda: bp.add_url_rule("/bar/123", endpoint="bar.123", view_func=foo_foo_foo),
)
pytest.raises(
AssertionError, bp.route("/bar/123", endpoint="bar.123"), lambda: None
)
foo_foo_foo.__name__ = "bar.123"
pytest.raises(
AssertionError, lambda: bp.add_url_rule("/bar/123", view_func=foo_foo_foo)
)
bp.add_url_rule(
"/bar/456", endpoint="foofoofoo", view_func=functools.partial(foo_foo_foo)
)
app.register_blueprint(bp, url_prefix="/py")
assert client.get("/py/foo").data == b"bp.foo"
# The rule's didn't actually made it through
rv = client.get("/py/bar")
assert rv.status_code == 404
rv = client.get("/py/bar/123")
assert rv.status_code == 404
with pytest.raises(ValueError):
bp.add_url_rule("/", view_func=view)
def test_endpoint_decorator(app, client):
@ -899,3 +835,89 @@ def test_nested_blueprint(app, client):
assert client.get("/parent/no").data == b"Parent no"
assert client.get("/parent/child/no").data == b"Parent no"
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
@pytest.mark.parametrize(
"parent_init, child_init, parent_registration, child_registration",
[
("/parent", "/child", None, None),
("/parent", None, None, "/child"),
(None, None, "/parent", "/child"),
("/other", "/something", "/parent", "/child"),
],
)
def test_nesting_url_prefixes(
parent_init,
child_init,
parent_registration,
child_registration,
app,
client,
) -> None:
parent = flask.Blueprint("parent", __name__, url_prefix=parent_init)
child = flask.Blueprint("child", __name__, url_prefix=child_init)
@child.route("/")
def index():
return "index"
parent.register_blueprint(child, url_prefix=child_registration)
app.register_blueprint(parent, url_prefix=parent_registration)
response = client.get("/parent/child/")
assert response.status_code == 200
def test_unique_blueprint_names(app, client) -> None:
bp = flask.Blueprint("bp", __name__)
bp2 = flask.Blueprint("bp", __name__)
app.register_blueprint(bp)
with pytest.warns(UserWarning):
app.register_blueprint(bp) # same bp, same name, warning
app.register_blueprint(bp, name="again") # same bp, different name, ok
with pytest.raises(ValueError):
app.register_blueprint(bp2) # different bp, same name, error
app.register_blueprint(bp2, name="alt") # different bp, different name, ok
def test_self_registration(app, client) -> None:
bp = flask.Blueprint("bp", __name__)
with pytest.raises(ValueError):
bp.register_blueprint(bp)
def test_blueprint_renaming(app, client) -> None:
bp = flask.Blueprint("bp", __name__)
bp2 = flask.Blueprint("bp2", __name__)
@bp.get("/")
def index():
return flask.request.endpoint
@bp.get("/error")
def error():
flask.abort(403)
@bp.errorhandler(403)
def forbidden(_: Exception):
return "Error", 403
@bp2.get("/")
def index2():
return flask.request.endpoint
bp.register_blueprint(bp2, url_prefix="/a", name="sub")
app.register_blueprint(bp, url_prefix="/a")
app.register_blueprint(bp, url_prefix="/b", name="alt")
assert client.get("/a/").data == b"bp.index"
assert client.get("/b/").data == b"alt.index"
assert client.get("/a/a/").data == b"bp.sub.index2"
assert client.get("/b/a/").data == b"alt.sub.index2"
assert client.get("/a/error").data == b"Error"
assert client.get("/b/error").data == b"Error"

View file

@ -5,6 +5,7 @@ import ssl
import sys
import types
from functools import partial
from pathlib import Path
from unittest.mock import patch
import click
@ -29,8 +30,8 @@ from flask.cli import run_command
from flask.cli import ScriptInfo
from flask.cli import with_appcontext
cwd = os.getcwd()
test_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "test_apps"))
cwd = Path.cwd()
test_path = (Path(__file__) / ".." / "test_apps").resolve()
@pytest.fixture
@ -152,29 +153,25 @@ def test_find_best_app(test_apps):
(
("test", cwd, "test"),
("test.py", cwd, "test"),
("a/test", os.path.join(cwd, "a"), "test"),
("a/test", cwd / "a", "test"),
("test/__init__.py", cwd, "test"),
("test/__init__", cwd, "test"),
# nested package
(
os.path.join(test_path, "cliapp", "inner1", "__init__"),
test_path / "cliapp" / "inner1" / "__init__",
test_path,
"cliapp.inner1",
),
(
os.path.join(test_path, "cliapp", "inner1", "inner2"),
test_path / "cliapp" / "inner1" / "inner2",
test_path,
"cliapp.inner1.inner2",
),
# dotted name
("test.a.b", cwd, "test.a.b"),
(os.path.join(test_path, "cliapp.app"), test_path, "cliapp.app"),
(test_path / "cliapp.app", test_path, "cliapp.app"),
# not a Python file, will be caught during import
(
os.path.join(test_path, "cliapp", "message.txt"),
test_path,
"cliapp.message.txt",
),
(test_path / "cliapp" / "message.txt", test_path, "cliapp.message.txt"),
),
)
def test_prepare_import(request, value, path, result):
@ -193,7 +190,7 @@ def test_prepare_import(request, value, path, result):
request.addfinalizer(reset_path)
assert prepare_import(value) == result
assert sys.path[0] == path
assert sys.path[0] == str(path)
@pytest.mark.parametrize(
@ -278,9 +275,8 @@ def test_scriptinfo(test_apps, monkeypatch):
assert obj.load_app() is app
# import app with module's absolute path
cli_app_path = os.path.abspath(
os.path.join(os.path.dirname(__file__), "test_apps", "cliapp", "app.py")
)
cli_app_path = str(test_path / "cliapp" / "app.py")
obj = ScriptInfo(app_import_path=cli_app_path)
app = obj.load_app()
assert app.name == "testapp"
@ -302,19 +298,13 @@ def test_scriptinfo(test_apps, monkeypatch):
pytest.raises(NoAppException, obj.load_app)
# import app from wsgi.py in current directory
monkeypatch.chdir(
os.path.abspath(
os.path.join(os.path.dirname(__file__), "test_apps", "helloworld")
)
)
monkeypatch.chdir(test_path / "helloworld")
obj = ScriptInfo()
app = obj.load_app()
assert app.name == "hello"
# import app from app.py in current directory
monkeypatch.chdir(
os.path.abspath(os.path.join(os.path.dirname(__file__), "test_apps", "cliapp"))
)
monkeypatch.chdir(test_path / "cliapp")
obj = ScriptInfo()
app = obj.load_app()
assert app.name == "testapp"
@ -513,7 +503,7 @@ def test_load_dotenv(monkeypatch):
monkeypatch.setenv("EGGS", "3")
monkeypatch.chdir(test_path)
assert load_dotenv()
assert os.getcwd() == test_path
assert Path.cwd() == test_path
# .flaskenv doesn't overwrite .env
assert os.environ["FOO"] == "env"
# set only in .flaskenv
@ -533,9 +523,8 @@ def test_dotenv_path(monkeypatch):
for item in ("FOO", "BAR", "EGGS"):
monkeypatch._setitem.append((os.environ, item, notset))
cwd = os.getcwd()
load_dotenv(os.path.join(test_path, ".flaskenv"))
assert os.getcwd() == cwd
load_dotenv(test_path / ".flaskenv")
assert Path.cwd() == cwd
assert "FOO" in os.environ

View file

@ -1,6 +1,7 @@
from werkzeug.routing import BaseConverter
from flask import has_request_context
from flask import request
from flask import session
from flask import url_for
@ -28,12 +29,13 @@ def test_custom_converters(app, client):
def test_context_available(app, client):
class ContextConverter(BaseConverter):
def to_python(self, value):
assert has_request_context()
assert request is not None
assert session is not None
return value
app.url_map.converters["ctx"] = ContextConverter
@app.route("/<ctx:name>")
@app.get("/<ctx:name>")
def index(name):
return name

View file

@ -2,21 +2,26 @@ import flask
from flask.sessions import SessionInterface
def test_open_session_endpoint_not_none():
# Define a session interface that breaks if request.endpoint is None
def test_open_session_with_endpoint():
"""If request.endpoint (or other URL matching behavior) is needed
while loading the session, RequestContext.match_request() can be
called manually.
"""
class MySessionInterface(SessionInterface):
def save_session(self):
def save_session(self, app, session, response):
pass
def open_session(self, _, request):
def open_session(self, app, request):
flask._request_ctx_stack.top.match_request()
assert request.endpoint is not None
def index():
return "Hello World!"
# Confirm a 200 response, indicating that request.endpoint was NOT None
app = flask.Flask(__name__)
app.route("/")(index)
app.session_interface = MySessionInterface()
response = app.test_client().open("/")
@app.get("/")
def index():
return "Hello, World!"
response = app.test_client().get("/")
assert response.status_code == 200

13
tox.ini
View file

@ -11,12 +11,6 @@ skip_missing_interpreters = true
deps =
-r requirements/tests.txt
https://github.com/pallets/werkzeug/archive/master.tar.gz
https://github.com/pallets/markupsafe/archive/master.tar.gz
https://github.com/pallets/jinja/archive/master.tar.gz
https://github.com/pallets/itsdangerous/archive/master.tar.gz
!click7: https://github.com/pallets/click/archive/master.tar.gz
click7: click<8
examples/tutorial[test]
@ -30,11 +24,8 @@ commands = pre-commit run --all-files --show-diff-on-failure
[testenv:typing]
deps = -r requirements/typing.txt
commands = mypy
commands = mypy --install-types --non-interactive
[testenv:docs]
deps =
-r requirements/docs.txt
https://github.com/pallets/werkzeug/archive/master.tar.gz
deps = -r requirements/docs.txt
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html