Merge branch 'main' into class-route
This commit is contained in:
commit
e5fb65b171
50 changed files with 902 additions and 534 deletions
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: "08:00"
|
||||
open-pull-requests-limit: 99
|
||||
4
.github/workflows/tests.yaml
vendored
4
.github/workflows/tests.yaml
vendored
|
|
@ -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
2
.gitignore
vendored
|
|
@ -4,6 +4,8 @@
|
|||
*.pyc
|
||||
*.pyo
|
||||
env/
|
||||
venv/
|
||||
.venv/
|
||||
env*
|
||||
dist/
|
||||
build/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
85
CHANGES.rst
85
CHANGES.rst
|
|
@ -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
|
||||
-------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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`_.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
10
docs/cli.rst
10
docs/cli.rst
|
|
@ -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``).
|
||||
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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/>`_
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ../..
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
-r docs.in
|
||||
-r tests.in
|
||||
-r typing.in
|
||||
pip-tools
|
||||
pre-commit
|
||||
tox
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Pallets-Sphinx-Themes >= 2.0.0rc1
|
||||
Pallets-Sphinx-Themes
|
||||
Sphinx
|
||||
sphinx-issues
|
||||
sphinxcontrib-log-cabinet
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -112,3 +112,6 @@ ignore_missing_imports = True
|
|||
|
||||
[mypy-dotenv.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-cryptography.*]
|
||||
ignore_missing_imports = True
|
||||
|
|
|
|||
10
setup.py
10
setup.py
|
|
@ -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"],
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
128
src/flask/app.py
128
src/flask/app.py
|
|
@ -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("."))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
...
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
13
tox.ini
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue