Merge branch 'pallets:main' into main
This commit is contained in:
commit
0d0817bc30
73 changed files with 1086 additions and 715 deletions
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
|
|
@ -1,8 +1,19 @@
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/requirements"
|
||||||
|
target-branch: "2.0.x"
|
||||||
|
versioning-strategy: "lockfile-only"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
day: "monday"
|
||||||
|
time: "16:00"
|
||||||
|
timezone: "UTC"
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: monthly
|
interval: "monthly"
|
||||||
time: "08:00"
|
day: "monday"
|
||||||
open-pull-requests-limit: 99
|
time: "16:00"
|
||||||
|
timezone: "UTC"
|
||||||
|
|
|
||||||
6
.github/workflows/lock.yaml
vendored
6
.github/workflows/lock.yaml
vendored
|
|
@ -8,8 +8,8 @@ jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v3
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: 14
|
issue-inactive-days: 14
|
||||||
pr-lock-inactive-days: 14
|
pr-inactive-days: 14
|
||||||
|
|
|
||||||
15
.github/workflows/tests.yaml
vendored
15
.github/workflows/tests.yaml
vendored
|
|
@ -24,14 +24,17 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
|
- {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
|
||||||
- {name: Windows, python: '3.9', os: windows-latest, tox: py39}
|
- {name: Windows, python: '3.10', os: windows-latest, tox: py310}
|
||||||
- {name: Mac, python: '3.9', os: macos-latest, tox: py39}
|
- {name: Mac, python: '3.10', os: macos-latest, tox: py310}
|
||||||
|
- {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311}
|
||||||
|
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
||||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
||||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||||
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
|
- {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
|
||||||
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
|
- {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min}
|
||||||
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
|
- {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev}
|
||||||
|
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
ci:
|
ci:
|
||||||
|
autoupdate_branch: "2.0.x"
|
||||||
autoupdate_schedule: monthly
|
autoupdate_schedule: monthly
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.15.0
|
rev: v2.29.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: ["--py36-plus"]
|
args: ["--py36-plus"]
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v2.5.0
|
rev: v2.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
name: Reorder Python imports (src, tests)
|
name: Reorder Python imports (src, tests)
|
||||||
files: "^(?!examples/)"
|
files: "^(?!examples/)"
|
||||||
args: ["--application-directories", "src"]
|
args: ["--application-directories", "src"]
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 21.5b1
|
rev: 21.12b0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.9.2
|
rev: 4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-bugbear
|
- flake8-bugbear
|
||||||
- flake8-implicit-str-concat
|
- flake8-implicit-str-concat
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: fix-byte-order-marker
|
- id: fix-byte-order-marker
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: "3.10"
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
- requirements: requirements/docs.txt
|
- requirements: requirements/docs.txt
|
||||||
|
|
|
||||||
66
CHANGES.rst
66
CHANGES.rst
|
|
@ -5,19 +5,79 @@ Version 2.1.0
|
||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
|
|
||||||
- Update Click dependency to >= 8.0.
|
- Drop support for Python 3.6. :pr:`4335`
|
||||||
|
- Update Click dependency to >= 8.0. :pr:`4008`
|
||||||
|
- Remove previously deprecated code. :pr:`4337`
|
||||||
|
|
||||||
|
- The CLI does not pass ``script_info`` to app factory functions.
|
||||||
|
- ``config.from_json`` is replaced by
|
||||||
|
``config.from_file(name, load=json.load)``.
|
||||||
|
- ``json`` functions no longer take an ``encoding`` parameter.
|
||||||
|
- ``safe_join`` is removed, use ``werkzeug.utils.safe_join``
|
||||||
|
instead.
|
||||||
|
- ``total_seconds`` is removed, use ``timedelta.total_seconds``
|
||||||
|
instead.
|
||||||
|
- The same blueprint cannot be registered with the same name. Use
|
||||||
|
``name=`` when registering to specify a unique name.
|
||||||
|
|
||||||
|
- Some parameters in ``send_file`` and ``send_from_directory`` were
|
||||||
|
renamed in 2.0. The deprecation period for the old names is extended
|
||||||
|
to 2.2. Be sure to test with deprecation warnings visible.
|
||||||
|
|
||||||
|
- ``attachment_filename`` is renamed to ``download_name``.
|
||||||
|
- ``cache_timeout`` is renamed to ``max_age``.
|
||||||
|
- ``add_etags`` is renamed to ``etag``.
|
||||||
|
- ``filename`` is renamed to ``path``.
|
||||||
|
|
||||||
|
- The ``RequestContext.g`` property is deprecated. Use ``g`` directly
|
||||||
|
or ``AppContext.g`` instead. :issue:`3898`
|
||||||
|
- ``copy_current_request_context`` can decorate async functions.
|
||||||
|
:pr:`4303`
|
||||||
|
|
||||||
|
|
||||||
|
Version 2.0.3
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
|
||||||
|
- The test client's ``as_tuple`` parameter is deprecated and will be
|
||||||
|
removed in Werkzeug 2.1. It is now also deprecated in Flask, to be
|
||||||
|
removed in Flask 2.1, while remaining compatible with both in
|
||||||
|
2.0.x. Use ``response.request.environ`` instead. :pr:`4341`
|
||||||
|
- Fix type annotation for ``errorhandler`` decorator. :issue:`4295`
|
||||||
|
- Revert a change to the CLI that caused it to hide ``ImportError``
|
||||||
|
tracebacks when importing the application. :issue:`4307`
|
||||||
|
- ``app.json_encoder`` and ``json_decoder`` are only passed to
|
||||||
|
``dumps`` and ``loads`` if they have custom behavior. This improves
|
||||||
|
performance, mainly on PyPy. :issue:`4349`
|
||||||
|
- Clearer error message when ``after_this_request`` is used outside a
|
||||||
|
request context. :issue:`4333`
|
||||||
|
|
||||||
|
|
||||||
Version 2.0.2
|
Version 2.0.2
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Unreleased
|
Released 2021-10-04
|
||||||
|
|
||||||
- Fix type annotation for ``teardown_request``. :issue:`4093`
|
- Fix type annotation for ``teardown_*`` methods. :issue:`4093`
|
||||||
- Fix type annotation for ``before_request`` and ``before_app_request``
|
- Fix type annotation for ``before_request`` and ``before_app_request``
|
||||||
decorators. :issue:`4104`
|
decorators. :issue:`4104`
|
||||||
- Fixed the issue where typing requires template global
|
- Fixed the issue where typing requires template global
|
||||||
decorators to accept functions with no arguments. :issue:`4098`
|
decorators to accept functions with no arguments. :issue:`4098`
|
||||||
|
- Support View and MethodView instances with async handlers. :issue:`4112`
|
||||||
|
- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
|
||||||
|
- Fix registering a blueprint twice with differing names. :issue:`4124`
|
||||||
|
- Fix the type of ``static_folder`` to accept ``pathlib.Path``.
|
||||||
|
:issue:`4150`
|
||||||
|
- ``jsonify`` handles ``decimal.Decimal`` by encoding to ``str``.
|
||||||
|
:issue:`4157`
|
||||||
|
- Correctly handle raising deferred errors in CLI lazy loading.
|
||||||
|
:issue:`4096`
|
||||||
|
- The CLI loader handles ``**kwargs`` in a ``create_app`` function.
|
||||||
|
:issue:`4170`
|
||||||
|
- Fix the order of ``before_request`` and other callbacks that trigger
|
||||||
|
before the view returns. They are called from the app down to the
|
||||||
|
closest nested blueprint. :issue:`4229`
|
||||||
|
|
||||||
|
|
||||||
Version 2.0.1
|
Version 2.0.1
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,16 @@ to address bugs and feature requests in Flask itself. Use one of the
|
||||||
following resources for questions about using Flask or issues with your
|
following resources for questions about using Flask or issues with your
|
||||||
own code:
|
own code:
|
||||||
|
|
||||||
- The ``#get-help`` channel on our Discord chat:
|
- The ``#questions`` channel on our Discord chat:
|
||||||
https://discord.gg/pallets
|
https://discord.gg/pallets
|
||||||
- The mailing list flask@python.org for long term discussion or larger
|
- The mailing list flask@python.org for long term discussion or larger
|
||||||
issues.
|
issues.
|
||||||
- Ask on `Stack Overflow`_. Search with Google first using:
|
- Ask on `Stack Overflow`_. Search with Google first using:
|
||||||
``site:stackoverflow.com flask {search term, exception message, etc.}``
|
``site:stackoverflow.com flask {search term, exception message, etc.}``
|
||||||
|
- Ask on our `GitHub Discussions`_.
|
||||||
|
|
||||||
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
|
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
|
||||||
|
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
|
||||||
|
|
||||||
|
|
||||||
Reporting issues
|
Reporting issues
|
||||||
|
|
@ -92,7 +94,7 @@ First time setup
|
||||||
|
|
||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
git remote add fork https://github.com/{username}/flask
|
$ git remote add fork https://github.com/{username}/flask
|
||||||
|
|
||||||
- Create a virtualenv.
|
- Create a virtualenv.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ Install and update using `pip`_:
|
||||||
|
|
||||||
$ pip install -U Flask
|
$ pip install -U Flask
|
||||||
|
|
||||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||||
|
|
||||||
|
|
||||||
A Simple Example
|
A Simple Example
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ following conditions are met:
|
||||||
1. Redistributions of source code must retain the above copyright
|
1. Redistributions of source code must retain the above copyright
|
||||||
notice and this list of conditions.
|
notice and this list of conditions.
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its
|
2. Neither the name of the copyright holder nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,8 +218,6 @@ Useful Functions and Classes
|
||||||
|
|
||||||
.. autofunction:: send_from_directory
|
.. autofunction:: send_from_directory
|
||||||
|
|
||||||
.. autofunction:: safe_join
|
|
||||||
|
|
||||||
.. autofunction:: escape
|
.. autofunction:: escape
|
||||||
|
|
||||||
.. autoclass:: Markup
|
.. autoclass:: Markup
|
||||||
|
|
@ -256,7 +254,7 @@ the filter to render data inside ``<script>`` tags.
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
<script type=text/javascript>
|
<script>
|
||||||
const names = {{ names|tosjon }};
|
const names = {{ names|tosjon }};
|
||||||
renderChart(names, {{ axis_data|tojson }});
|
renderChart(names, {{ axis_data|tojson }});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ a request, CLI command, or other activity. Rather than passing the
|
||||||
application around to each function, the :data:`current_app` and
|
application around to each function, the :data:`current_app` and
|
||||||
:data:`g` proxies are accessed instead.
|
:data:`g` proxies are accessed instead.
|
||||||
|
|
||||||
This is similar to the :doc:`/reqcontext`, which keeps track of
|
This is similar to :doc:`/reqcontext`, which keeps track of
|
||||||
request-level data during a request. A corresponding application context
|
request-level data during a request. A corresponding application context
|
||||||
is pushed when a request context is pushed.
|
is pushed when a request context is pushed.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,24 @@ defined with ``async def`` and use ``await``.
|
||||||
data = await async_db_query(...)
|
data = await async_db_query(...)
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
Pluggable class-based views also support handlers that are implemented as
|
||||||
|
coroutines. This applies to the :meth:`~flask.views.View.dispatch_request`
|
||||||
|
method in views that inherit from the :class:`flask.views.View` class, as
|
||||||
|
well as all the HTTP method handlers in views that inherit from the
|
||||||
|
:class:`flask.views.MethodView` class.
|
||||||
|
|
||||||
.. admonition:: Using ``async`` on Windows on Python 3.8
|
.. admonition:: Using ``async`` on Windows on Python 3.8
|
||||||
|
|
||||||
Python 3.8 has a bug related to asyncio on Windows. If you encounter
|
Python 3.8 has a bug related to asyncio on Windows. If you encounter
|
||||||
something like ``ValueError: set_wakeup_fd only works in main thread``,
|
something like ``ValueError: set_wakeup_fd only works in main thread``,
|
||||||
please upgrade to Python 3.9.
|
please upgrade to Python 3.9.
|
||||||
|
|
||||||
|
.. admonition:: Using ``async`` with greenlet
|
||||||
|
|
||||||
|
When using gevent or eventlet to serve an application or patch the
|
||||||
|
runtime, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
|
||||||
|
required.
|
||||||
|
|
||||||
|
|
||||||
Performance
|
Performance
|
||||||
-----------
|
-----------
|
||||||
|
|
|
||||||
12
docs/cli.rst
12
docs/cli.rst
|
|
@ -103,6 +103,11 @@ replaces the :meth:`Flask.run` method in most cases. ::
|
||||||
is provided for convenience, but is not designed to be particularly secure,
|
is provided for convenience, but is not designed to be particularly secure,
|
||||||
stable, or efficient. See :doc:`/deploying/index` for how to run in production.
|
stable, or efficient. See :doc:`/deploying/index` for how to run in production.
|
||||||
|
|
||||||
|
If another program is already using port 5000, you'll see
|
||||||
|
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
|
||||||
|
server tries to start. See :ref:`address-already-in-use` for how to
|
||||||
|
handle that.
|
||||||
|
|
||||||
|
|
||||||
Open a Shell
|
Open a Shell
|
||||||
------------
|
------------
|
||||||
|
|
@ -112,10 +117,9 @@ shell with the :func:`shell <cli.shell_command>` command. An application
|
||||||
context will be active, and the app instance will be imported. ::
|
context will be active, and the app instance will be imported. ::
|
||||||
|
|
||||||
$ flask shell
|
$ flask shell
|
||||||
Python 3.6.2 (default, Jul 20 2017, 03:52:27)
|
Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux
|
||||||
[GCC 7.1.1 20170630] on linux
|
App: example [production]
|
||||||
App: example
|
Instance: /home/david/Projects/pallets/flask/instance
|
||||||
Instance: /home/user/Projects/hello/instance
|
|
||||||
>>>
|
>>>
|
||||||
|
|
||||||
Use :meth:`~Flask.shell_context_processor` to add other automatic imports.
|
Use :meth:`~Flask.shell_context_processor` to add other automatic imports.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ method::
|
||||||
|
|
||||||
app.config.update(
|
app.config.update(
|
||||||
TESTING=True,
|
TESTING=True,
|
||||||
SECRET_KEY=b'_5#y2L"F4Q8z\n\xec]/'
|
SECRET_KEY='192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -180,8 +180,8 @@ The following configuration values are used internally by Flask:
|
||||||
application. It should be a long random ``bytes`` or ``str``. For
|
application. It should be a long random ``bytes`` or ``str``. For
|
||||||
example, copy the output of this to your config::
|
example, copy the output of this to your config::
|
||||||
|
|
||||||
$ python -c 'import os; print(os.urandom(16))'
|
$ python -c 'import secrets; print(secrets.token_hex())'
|
||||||
b'_5#y2L"F4Q8z\n\xec]/'
|
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
|
|
||||||
**Do not reveal the secret key when posting questions or committing code.**
|
**Do not reveal the secret key when posting questions or committing code.**
|
||||||
|
|
||||||
|
|
@ -468,7 +468,7 @@ sure to use uppercase letters for your config keys.
|
||||||
Here is an example of a configuration file::
|
Here is an example of a configuration file::
|
||||||
|
|
||||||
# Example configuration
|
# Example configuration
|
||||||
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
|
SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
|
|
||||||
Make sure to load the configuration very early on, so that extensions have
|
Make sure to load the configuration very early on, so that extensions have
|
||||||
the ability to access the configuration when starting up. There are other
|
the ability to access the configuration when starting up. There are other
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ enables the debugger and reloader.
|
||||||
|
|
||||||
``FLASK_ENV`` can only be set as an environment variable. When running
|
``FLASK_ENV`` can only be set as an environment variable. When running
|
||||||
from Python code, passing ``debug=True`` enables debug mode, which is
|
from Python code, passing ``debug=True`` enables debug mode, which is
|
||||||
mostly equivalent. Debug mode can be controled separately from
|
mostly equivalent. Debug mode can be controlled separately from
|
||||||
``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well.
|
``FLASK_ENV`` with the ``FLASK_DEBUG`` environment variable as well.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ ASGI
|
||||||
|
|
||||||
If you'd like to use an ASGI server you will need to utilise WSGI to
|
If you'd like to use an ASGI server you will need to utilise WSGI to
|
||||||
ASGI middleware. The asgiref
|
ASGI middleware. The asgiref
|
||||||
[WsgiToAsgi](https://github.com/django/asgiref#wsgi-to-asgi-adapter)
|
`WsgiToAsgi <https://github.com/django/asgiref#wsgi-to-asgi-adapter>`_
|
||||||
adapter is recommended as it integrates with the event loop used for
|
adapter is recommended as it integrates with the event loop used for
|
||||||
Flask's :ref:`async_await` support. You can use the adapter by
|
Flask's :ref:`async_await` support. You can use the adapter by
|
||||||
wrapping the Flask app,
|
wrapping the Flask app,
|
||||||
|
|
@ -21,7 +21,7 @@ wrapping the Flask app,
|
||||||
|
|
||||||
asgi_app = WsgiToAsgi(app)
|
asgi_app = WsgiToAsgi(app)
|
||||||
|
|
||||||
and then serving the ``asgi_app`` with the asgi server, e.g. using
|
and then serving the ``asgi_app`` with the ASGI server, e.g. using
|
||||||
`Hypercorn <https://gitlab.com/pgjones/hypercorn>`_,
|
`Hypercorn <https://gitlab.com/pgjones/hypercorn>`_,
|
||||||
|
|
||||||
.. sourcecode:: text
|
.. sourcecode:: text
|
||||||
|
|
|
||||||
|
|
@ -234,5 +234,5 @@ python path. Common problems are:
|
||||||
|
|
||||||
.. _nginx: https://nginx.org/
|
.. _nginx: https://nginx.org/
|
||||||
.. _lighttpd: https://www.lighttpd.net/
|
.. _lighttpd: https://www.lighttpd.net/
|
||||||
.. _cherokee: http://cherokee-project.com/
|
.. _cherokee: https://cherokee-project.com/
|
||||||
.. _flup: https://pypi.org/project/flup/
|
.. _flup: https://pypi.org/project/flup/
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ If your application is accessible at root level, you can use a
|
||||||
single ``/`` instead of ``/yourapplication``. ``myapp`` refers to the name of
|
single ``/`` instead of ``/yourapplication``. ``myapp`` refers to the name of
|
||||||
the file of your flask application (without extension) or the module which
|
the file of your flask application (without extension) or the module which
|
||||||
provides ``app``. ``app`` is the callable inside of your application (usually
|
provides ``app``. ``app`` is the callable inside of your application (usually
|
||||||
the line reads ``app = Flask(__name__)``.
|
the line reads ``app = Flask(__name__)``).
|
||||||
|
|
||||||
If you want to deploy your flask application inside of a virtual environment,
|
If you want to deploy your flask application inside of a virtual environment,
|
||||||
you need to also add ``--virtualenv /path/to/virtual/environment``. You might
|
you need to also add ``--virtualenv /path/to/virtual/environment``. You might
|
||||||
|
|
@ -67,5 +67,5 @@ to have it in the URL root its a bit simpler::
|
||||||
|
|
||||||
.. _nginx: https://nginx.org/
|
.. _nginx: https://nginx.org/
|
||||||
.. _lighttpd: https://www.lighttpd.net/
|
.. _lighttpd: https://www.lighttpd.net/
|
||||||
.. _cherokee: http://cherokee-project.com/
|
.. _cherokee: https://cherokee-project.com/
|
||||||
.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/
|
.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,171 @@
|
||||||
Standalone WSGI Containers
|
Standalone WSGI Servers
|
||||||
==========================
|
=======================
|
||||||
|
|
||||||
|
Most WSGI servers also provide HTTP servers, so they can run a WSGI
|
||||||
|
application and make it available externally.
|
||||||
|
|
||||||
|
It may still be a good idea to run the server behind a dedicated HTTP
|
||||||
|
server such as Apache or Nginx. See :ref:`deploying-proxy-setups` if you
|
||||||
|
run into issues with that.
|
||||||
|
|
||||||
There are popular servers written in Python that contain WSGI applications and
|
|
||||||
serve HTTP. These servers stand alone when they run; you can proxy to them
|
|
||||||
from your web server. Note the section on :ref:`deploying-proxy-setups` if you
|
|
||||||
run into issues.
|
|
||||||
|
|
||||||
Gunicorn
|
Gunicorn
|
||||||
--------
|
--------
|
||||||
|
|
||||||
`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork
|
`Gunicorn`_ is a WSGI and HTTP server for UNIX. To run a Flask
|
||||||
worker model ported from Ruby's Unicorn project. It supports both `eventlet`_
|
application, tell Gunicorn how to import your Flask app object.
|
||||||
and `greenlet`_. Running a Flask application on this server is quite simple::
|
|
||||||
|
|
||||||
$ gunicorn myproject:app
|
.. code-block:: text
|
||||||
|
|
||||||
`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``.
|
$ gunicorn -w 4 -b 0.0.0.0:5000 your_project:app
|
||||||
For example, to run a Flask application with 4 worker processes (``-w
|
|
||||||
4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``)::
|
|
||||||
|
|
||||||
$ gunicorn -w 4 -b 127.0.0.1:4000 myproject:app
|
The ``-w 4`` option uses 4 workers to handle 4 requests at once. The
|
||||||
|
``-b 0.0.0.0:5000`` serves the application on all interfaces on port
|
||||||
|
5000.
|
||||||
|
|
||||||
The ``gunicorn`` command expects the names of your application module or
|
Gunicorn provides many options for configuring the server, either
|
||||||
package and the application instance within the module. If you use the
|
through a configuration file or with command line options. Use
|
||||||
application factory pattern, you can pass a call to that::
|
``gunicorn --help`` or see the docs for more information.
|
||||||
|
|
||||||
|
The command expects the name of your module or package to import and
|
||||||
|
the application instance within the module. If you use the application
|
||||||
|
factory pattern, you can pass a call to that.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ gunicorn -w 4 -b 0.0.0.0:5000 "myproject:create_app()"
|
||||||
|
|
||||||
|
|
||||||
|
Async with Gevent or Eventlet
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The default sync worker is appropriate for many use cases. If you need
|
||||||
|
asynchronous support, Gunicorn provides workers using either `gevent`_
|
||||||
|
or `eventlet`_. This is not the same as Python's ``async/await``, or the
|
||||||
|
ASGI server spec.
|
||||||
|
|
||||||
|
When using either gevent or eventlet, greenlet>=1.0 is required,
|
||||||
|
otherwise context locals such as ``request`` will not work as expected.
|
||||||
|
When using PyPy, PyPy>=7.3.7 is required.
|
||||||
|
|
||||||
|
To use gevent:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ gunicorn -k gevent -b 0.0.0.0:5000 your_project:app
|
||||||
|
|
||||||
|
To use eventlet:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ gunicorn -k eventlet -b 0.0.0.0:5000 your_project:app
|
||||||
|
|
||||||
$ gunicorn "myproject:create_app()"
|
|
||||||
|
|
||||||
.. _Gunicorn: https://gunicorn.org/
|
.. _Gunicorn: https://gunicorn.org/
|
||||||
|
.. _gevent: http://www.gevent.org/
|
||||||
.. _eventlet: https://eventlet.net/
|
.. _eventlet: https://eventlet.net/
|
||||||
|
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
|
||||||
uWSGI
|
uWSGI
|
||||||
--------
|
-----
|
||||||
|
|
||||||
`uWSGI`_ is a fast application server written in C. It is very configurable
|
`uWSGI`_ is a fast application server written in C. It is very
|
||||||
which makes it more complicated to setup than gunicorn.
|
configurable, which makes it more complicated to setup than Gunicorn.
|
||||||
|
It also provides many other utilities for writing robust web
|
||||||
|
applications. To run a Flask application, tell Gunicorn how to import
|
||||||
|
your Flask app object.
|
||||||
|
|
||||||
Running `uWSGI HTTP Router`_::
|
.. code-block:: text
|
||||||
|
|
||||||
$ uwsgi --http 127.0.0.1:5000 --module myproject:app
|
$ uwsgi --master -p 4 --http 0.0.0.0:5000 -w your_project:app
|
||||||
|
|
||||||
For a more optimized setup, see :doc:`configuring uWSGI and NGINX <uwsgi>`.
|
The ``-p 4`` option uses 4 workers to handle 4 requests at once. The
|
||||||
|
``--http 0.0.0.0:5000`` serves the application on all interfaces on port
|
||||||
|
5000.
|
||||||
|
|
||||||
|
uWSGI has optimized integration with Nginx and Apache instead of using
|
||||||
|
a standard HTTP proxy. See :doc:`configuring uWSGI and Nginx <uwsgi>`.
|
||||||
|
|
||||||
|
|
||||||
|
Async with Gevent
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The default sync worker is appropriate for many use cases. If you need
|
||||||
|
asynchronous support, uWSGI provides workers using `gevent`_. It also
|
||||||
|
supports other async modes, see the docs for more information. This is
|
||||||
|
not the same as Python's ``async/await``, or the ASGI server spec.
|
||||||
|
|
||||||
|
When using gevent, greenlet>=1.0 is required, otherwise context locals
|
||||||
|
such as ``request`` will not work as expected. When using PyPy,
|
||||||
|
PyPy>=7.3.7 is required.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ uwsgi --master --gevent 100 --http 0.0.0.0:5000 -w your_project:app
|
||||||
|
|
||||||
.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/
|
.. _uWSGI: https://uwsgi-docs.readthedocs.io/en/latest/
|
||||||
.. _uWSGI HTTP Router: https://uwsgi-docs.readthedocs.io/en/latest/HTTP.html#the-uwsgi-http-https-router
|
|
||||||
|
|
||||||
Gevent
|
Gevent
|
||||||
-------
|
------
|
||||||
|
|
||||||
`Gevent`_ is a coroutine-based Python networking library that uses
|
Prefer using `Gunicorn`_ with Gevent workers rather than using Gevent
|
||||||
`greenlet`_ to provide a high-level synchronous API on top of `libev`_
|
directly. Gunicorn provides a much more configurable and
|
||||||
event loop::
|
production-tested server. See the section on Gunicorn above.
|
||||||
|
|
||||||
|
`Gevent`_ allows writing asynchronous, coroutine-based code that looks
|
||||||
|
like standard synchronous Python. It uses `greenlet`_ to enable task
|
||||||
|
switching without writing ``async/await`` or using ``asyncio``.
|
||||||
|
|
||||||
|
It provides a WSGI server that can handle many connections at once
|
||||||
|
instead of one per worker process.
|
||||||
|
|
||||||
|
`Eventlet`_, described below, is another library that does the same
|
||||||
|
thing. Certain dependencies you have, or other consideration, may affect
|
||||||
|
which of the two you choose to use
|
||||||
|
|
||||||
|
To use gevent to serve your application, import its ``WSGIServer`` and
|
||||||
|
use it to run your ``app``.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
from yourapplication import app
|
from your_project import app
|
||||||
|
|
||||||
http_server = WSGIServer(('', 5000), app)
|
http_server = WSGIServer(("", 5000), app)
|
||||||
http_server.serve_forever()
|
http_server.serve_forever()
|
||||||
|
|
||||||
.. _Gevent: http://www.gevent.org/
|
|
||||||
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
Eventlet
|
||||||
.. _libev: http://software.schmorp.de/pkg/libev.html
|
--------
|
||||||
|
|
||||||
|
Prefer using `Gunicorn`_ with Eventlet workers rather than using
|
||||||
|
Eventlet directly. Gunicorn provides a much more configurable and
|
||||||
|
production-tested server. See the section on Gunicorn above.
|
||||||
|
|
||||||
|
`Eventlet`_ allows writing asynchronous, coroutine-based code that looks
|
||||||
|
like standard synchronous Python. It uses `greenlet`_ to enable task
|
||||||
|
switching without writing ``async/await`` or using ``asyncio``.
|
||||||
|
|
||||||
|
It provides a WSGI server that can handle many connections at once
|
||||||
|
instead of one per worker process.
|
||||||
|
|
||||||
|
`Gevent`_, described above, is another library that does the same
|
||||||
|
thing. Certain dependencies you have, or other consideration, may affect
|
||||||
|
which of the two you choose to use
|
||||||
|
|
||||||
|
To use eventlet to serve your application, import its ``wsgi.server``
|
||||||
|
and use it to run your ``app``.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
from eventlet import wsgi
|
||||||
|
from your_project import app
|
||||||
|
|
||||||
|
wsgi.server(eventlet.listen(("", 5000), app)
|
||||||
|
|
||||||
|
|
||||||
Twisted Web
|
Twisted Web
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -69,7 +173,9 @@ Twisted Web
|
||||||
`Twisted Web`_ is the web server shipped with `Twisted`_, a mature,
|
`Twisted Web`_ is the web server shipped with `Twisted`_, a mature,
|
||||||
non-blocking event-driven networking library. Twisted Web comes with a
|
non-blocking event-driven networking library. Twisted Web comes with a
|
||||||
standard WSGI container which can be controlled from the command line using
|
standard WSGI container which can be controlled from the command line using
|
||||||
the ``twistd`` utility::
|
the ``twistd`` utility:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
$ twistd web --wsgi myproject.app
|
$ twistd web --wsgi myproject.app
|
||||||
|
|
||||||
|
|
@ -79,13 +185,16 @@ This example will run a Flask application called ``app`` from a module named
|
||||||
Twisted Web supports many flags and options, and the ``twistd`` utility does
|
Twisted Web supports many flags and options, and the ``twistd`` utility does
|
||||||
as well; see ``twistd -h`` and ``twistd web -h`` for more information. For
|
as well; see ``twistd -h`` and ``twistd web -h`` for more information. For
|
||||||
example, to run a Twisted Web server in the foreground, on port 8080, with an
|
example, to run a Twisted Web server in the foreground, on port 8080, with an
|
||||||
application from ``myproject``::
|
application from ``myproject``:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
$ twistd -n web --port tcp:8080 --wsgi myproject.app
|
$ twistd -n web --port tcp:8080 --wsgi myproject.app
|
||||||
|
|
||||||
.. _Twisted: https://twistedmatrix.com/trac/
|
.. _Twisted: https://twistedmatrix.com/trac/
|
||||||
.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb
|
.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb
|
||||||
|
|
||||||
|
|
||||||
.. _deploying-proxy-setups:
|
.. _deploying-proxy-setups:
|
||||||
|
|
||||||
Proxy Setups
|
Proxy Setups
|
||||||
|
|
|
||||||
|
|
@ -322,9 +322,9 @@ ecosystem remain consistent and compatible.
|
||||||
`Official Pallets Themes`_. A link to the documentation or project
|
`Official Pallets Themes`_. A link to the documentation or project
|
||||||
website must be in the PyPI metadata or the readme.
|
website must be in the PyPI metadata or the readme.
|
||||||
7. For maximum compatibility, the extension should support the same
|
7. For maximum compatibility, the extension should support the same
|
||||||
versions of Python that Flask supports. 3.6+ is recommended as of
|
versions of Python that Flask supports. 3.7+ is recommended as of
|
||||||
2020. Use ``python_requires=">= 3.6"`` in ``setup.py`` to indicate
|
December 2021. Use ``python_requires=">= 3.7"`` in ``setup.py`` to
|
||||||
supported versions.
|
indicate supported versions.
|
||||||
|
|
||||||
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
|
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+Flask
|
||||||
.. _mailinglist: https://mail.python.org/mailman/listinfo/flask
|
.. _mailinglist: https://mail.python.org/mailman/listinfo/flask
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Python Version
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
We recommend using the latest version of Python. Flask supports Python
|
We recommend using the latest version of Python. Flask supports Python
|
||||||
3.6 and newer.
|
3.7 and newer.
|
||||||
|
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
|
|
@ -49,6 +49,18 @@ use them if you install them.
|
||||||
.. _watchdog: https://pythonhosted.org/watchdog/
|
.. _watchdog: https://pythonhosted.org/watchdog/
|
||||||
|
|
||||||
|
|
||||||
|
greenlet
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
You may choose to use gevent or eventlet with your application. In this
|
||||||
|
case, greenlet>=1.0 is required. When using PyPy, PyPy>=7.3.7 is
|
||||||
|
required.
|
||||||
|
|
||||||
|
These are not minimum supported versions, they only indicate the first
|
||||||
|
versions that added necessary features. You should use the latest
|
||||||
|
versions of each.
|
||||||
|
|
||||||
|
|
||||||
Virtual environments
|
Virtual environments
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ An example task
|
||||||
|
|
||||||
Let's write a task that adds two numbers together and returns the result. We
|
Let's write a task that adds two numbers together and returns the result. We
|
||||||
configure Celery's broker and backend to use Redis, create a ``celery``
|
configure Celery's broker and backend to use Redis, create a ``celery``
|
||||||
application using the factor from above, and then use it to define the task. ::
|
application using the factory from above, and then use it to define the task. ::
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,9 +175,8 @@ are JavaScript libraries like jQuery_ that have form plugins to ease the
|
||||||
construction of progress bar.
|
construction of progress bar.
|
||||||
|
|
||||||
Because the common pattern for file uploads exists almost unchanged in all
|
Because the common pattern for file uploads exists almost unchanged in all
|
||||||
applications dealing with uploads, there is also a Flask extension called
|
applications dealing with uploads, there are also some Flask extensions that
|
||||||
`Flask-Uploads`_ that implements a full fledged upload mechanism that
|
implement a full fledged upload mechanism that allows controlling which
|
||||||
allows controlling which file extensions are allowed to be uploaded.
|
file extensions are allowed to be uploaded.
|
||||||
|
|
||||||
.. _jQuery: https://jquery.com/
|
.. _jQuery: https://jquery.com/
|
||||||
.. _Flask-Uploads: https://flask-uploads.readthedocs.io/en/latest/
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ to add a script statement to the bottom of your ``<body>`` to load jQuery:
|
||||||
|
|
||||||
.. sourcecode:: html
|
.. sourcecode:: html
|
||||||
|
|
||||||
<script type=text/javascript src="{{
|
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
|
||||||
url_for('static', filename='jquery.js') }}"></script>
|
|
||||||
|
|
||||||
Another method is using Google's `AJAX Libraries API
|
Another method is using Google's `AJAX Libraries API
|
||||||
<https://developers.google.com/speed/libraries/>`_ to load jQuery:
|
<https://developers.google.com/speed/libraries/>`_ to load jQuery:
|
||||||
|
|
@ -59,7 +58,7 @@ like this:
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
<script type=text/javascript>
|
<script>
|
||||||
$SCRIPT_ROOT = {{ request.script_root|tojson }};
|
$SCRIPT_ROOT = {{ request.script_root|tojson }};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -109,7 +108,7 @@ usually a better idea to have that in a separate script file:
|
||||||
|
|
||||||
.. sourcecode:: html
|
.. sourcecode:: html
|
||||||
|
|
||||||
<script type=text/javascript>
|
<script>
|
||||||
$(function() {
|
$(function() {
|
||||||
$('a#calculate').bind('click', function() {
|
$('a#calculate').bind('click', function() {
|
||||||
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
|
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
|
||||||
|
|
|
||||||
|
|
@ -113,16 +113,16 @@ raw cursor and connection objects.
|
||||||
Here is how you can use it::
|
Here is how you can use it::
|
||||||
|
|
||||||
for user in query_db('select * from users'):
|
for user in query_db('select * from users'):
|
||||||
print user['username'], 'has the id', user['user_id']
|
print(user['username'], 'has the id', user['user_id'])
|
||||||
|
|
||||||
Or if you just want a single result::
|
Or if you just want a single result::
|
||||||
|
|
||||||
user = query_db('select * from users where username = ?',
|
user = query_db('select * from users where username = ?',
|
||||||
[the_username], one=True)
|
[the_username], one=True)
|
||||||
if user is None:
|
if user is None:
|
||||||
print 'No such user'
|
print('No such user')
|
||||||
else:
|
else:
|
||||||
print the_username, 'has the id', user['user_id']
|
print(the_username, 'has the id', user['user_id'])
|
||||||
|
|
||||||
To pass variable parts to the SQL statement, use a question mark in the
|
To pass variable parts to the SQL statement, use a question mark in the
|
||||||
statement and pass in the arguments as a list. Never directly add them to
|
statement and pass in the arguments as a list. Never directly add them to
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ forms.
|
||||||
fun. You can get it from `PyPI
|
fun. You can get it from `PyPI
|
||||||
<https://pypi.org/project/Flask-WTF/>`_.
|
<https://pypi.org/project/Flask-WTF/>`_.
|
||||||
|
|
||||||
.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/
|
.. _Flask-WTF: https://flask-wtf.readthedocs.io/
|
||||||
|
|
||||||
The Forms
|
The Forms
|
||||||
---------
|
---------
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,11 @@ deployment options see :doc:`deploying/index`.
|
||||||
Now head over to http://127.0.0.1:5000/, and you should see your hello
|
Now head over to http://127.0.0.1:5000/, and you should see your hello
|
||||||
world greeting.
|
world greeting.
|
||||||
|
|
||||||
|
If another program is already using port 5000, you'll see
|
||||||
|
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
|
||||||
|
server tries to start. See :ref:`address-already-in-use` for how to
|
||||||
|
handle that.
|
||||||
|
|
||||||
.. _public-server:
|
.. _public-server:
|
||||||
|
|
||||||
.. admonition:: Externally Visible Server
|
.. admonition:: Externally Visible Server
|
||||||
|
|
@ -444,9 +449,9 @@ Here is an example template:
|
||||||
<h1>Hello, World!</h1>
|
<h1>Hello, World!</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
Inside templates you also have access to the :class:`~flask.request`,
|
Inside templates you also have access to the :data:`~flask.Flask.config`,
|
||||||
:class:`~flask.session` and :class:`~flask.g` [#]_ objects
|
:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
|
||||||
as well as the :func:`~flask.get_flashed_messages` function.
|
as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions.
|
||||||
|
|
||||||
Templates are especially useful if inheritance is used. If you want to
|
Templates are especially useful if inheritance is used. If you want to
|
||||||
know how that works, see :doc:`patterns/templateinheritance`. Basically
|
know how that works, see :doc:`patterns/templateinheritance`. Basically
|
||||||
|
|
@ -468,7 +473,7 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:
|
||||||
>>> Markup.escape('<blink>hacker</blink>')
|
>>> Markup.escape('<blink>hacker</blink>')
|
||||||
Markup('<blink>hacker</blink>')
|
Markup('<blink>hacker</blink>')
|
||||||
>>> Markup('<em>Marked up</em> » HTML').striptags()
|
>>> Markup('<em>Marked up</em> » HTML').striptags()
|
||||||
'Marked up \xbb HTML'
|
'Marked up » HTML'
|
||||||
|
|
||||||
.. versionchanged:: 0.5
|
.. versionchanged:: 0.5
|
||||||
|
|
||||||
|
|
@ -626,7 +631,7 @@ Werkzeug provides for you::
|
||||||
def upload_file():
|
def upload_file():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
file = request.files['the_file']
|
file = request.files['the_file']
|
||||||
file.save(f"/var/www/uploads/{secure_filename(f.filename)}")
|
file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
|
||||||
...
|
...
|
||||||
|
|
||||||
For some better examples, see :doc:`patterns/fileuploads`.
|
For some better examples, see :doc:`patterns/fileuploads`.
|
||||||
|
|
@ -847,8 +852,8 @@ sessions work::
|
||||||
generator. Use the following command to quickly generate a value for
|
generator. Use the following command to quickly generate a value for
|
||||||
:attr:`Flask.secret_key` (or :data:`SECRET_KEY`)::
|
:attr:`Flask.secret_key` (or :data:`SECRET_KEY`)::
|
||||||
|
|
||||||
$ python -c 'import os; print(os.urandom(16))'
|
$ python -c 'import secrets; print(secrets.token_hex())'
|
||||||
b'_5#y2L"F4Q8z\n\xec]/'
|
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
|
|
||||||
A note on cookie-based sessions: Flask will take the values you put into the
|
A note on cookie-based sessions: Flask will take the values you put into the
|
||||||
session object and serialize them into a cookie. If you are finding some
|
session object and serialize them into a cookie. If you are finding some
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ request. Rather than passing the request object to each function that
|
||||||
runs during a request, the :data:`request` and :data:`session` proxies
|
runs during a request, the :data:`request` and :data:`session` proxies
|
||||||
are accessed instead.
|
are accessed instead.
|
||||||
|
|
||||||
This is similar to the :doc:`/appcontext`, which keeps track of the
|
This is similar to :doc:`/appcontext`, which keeps track of the
|
||||||
application-level data independent of a request. A corresponding
|
application-level data independent of a request. A corresponding
|
||||||
application context is pushed when a request context is pushed.
|
application context is pushed when a request context is pushed.
|
||||||
|
|
||||||
|
|
@ -33,8 +33,8 @@ Lifetime of the Context
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
When a Flask application begins handling a request, it pushes a request
|
When a Flask application begins handling a request, it pushes a request
|
||||||
context, which also pushes an :doc:`/appcontext`. When the request ends
|
context, which also pushes an :doc:`app context </appcontext>`. When the
|
||||||
it pops the request context then the application context.
|
request ends it pops the request context then the application context.
|
||||||
|
|
||||||
The context is unique to each thread (or other worker type).
|
The context is unique to each thread (or other worker type).
|
||||||
:data:`request` cannot be passed to another thread, the other thread
|
:data:`request` cannot be passed to another thread, the other thread
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,54 @@ and using the CLI.
|
||||||
above.
|
above.
|
||||||
|
|
||||||
|
|
||||||
|
.. _address-already-in-use:
|
||||||
|
|
||||||
|
Address already in use
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If another program is already using port 5000, you'll see an ``OSError``
|
||||||
|
when the server tries to start. It may have one of the following
|
||||||
|
messages:
|
||||||
|
|
||||||
|
- ``OSError: [Errno 98] Address already in use``
|
||||||
|
- ``OSError: [WinError 10013] An attempt was made to access a socket
|
||||||
|
in a way forbidden by its access permissions``
|
||||||
|
|
||||||
|
Either identify and stop the other program, or use
|
||||||
|
``flask run --port 5001`` to pick a different port.
|
||||||
|
|
||||||
|
You can use ``netstat`` or ``lsof`` to identify what process id is using
|
||||||
|
a port, then use other operating system tools stop that process. The
|
||||||
|
following example shows that process id 6847 is using port 5000.
|
||||||
|
|
||||||
|
.. tabs::
|
||||||
|
|
||||||
|
.. tab:: ``netstat`` (Linux)
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ netstat -nlp | grep 5000
|
||||||
|
tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN 6847/python
|
||||||
|
|
||||||
|
.. tab:: ``lsof`` (macOS / Linux)
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
$ lsof -P -i :5000
|
||||||
|
Python 6847 IPv4 TCP localhost:5000 (LISTEN)
|
||||||
|
|
||||||
|
.. tab:: ``netstat`` (Windows)
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
> netstat -ano | findstr 5000
|
||||||
|
TCP 127.0.0.1:5000 0.0.0.0:0 LISTENING 6847
|
||||||
|
|
||||||
|
macOS Monterey and later automatically starts a service that uses port
|
||||||
|
5000. To disable the service, go to System Preferences, Sharing, and
|
||||||
|
disable "AirPlay Receiver".
|
||||||
|
|
||||||
|
|
||||||
Lazy or Eager Loading
|
Lazy or Eager Loading
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ that these functions are not only there for interactive shell usage, but
|
||||||
also for unit testing and other situations that require a faked request
|
also for unit testing and other situations that require a faked request
|
||||||
context.
|
context.
|
||||||
|
|
||||||
Generally it's recommended that you read the :doc:`reqcontext`
|
Generally it's recommended that you read :doc:`reqcontext` first.
|
||||||
chapter of the documentation first.
|
|
||||||
|
|
||||||
Command Line Interface
|
Command Line Interface
|
||||||
----------------------
|
----------------------
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,7 @@ With Blinker 1.1 you can also easily subscribe to signals by using the new
|
||||||
|
|
||||||
@template_rendered.connect_via(app)
|
@template_rendered.connect_via(app)
|
||||||
def when_template_rendered(sender, template, context, **extra):
|
def when_template_rendered(sender, template, context, **extra):
|
||||||
print f'Template {template.name} is rendered with {context}'
|
print(f'Template {template.name} is rendered with {context}')
|
||||||
|
|
||||||
Core Signals
|
Core Signals
|
||||||
------------
|
------------
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ by default:
|
||||||
.. data:: config
|
.. data:: config
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
The current configuration object (:data:`flask.config`)
|
The current configuration object (:data:`flask.Flask.config`)
|
||||||
|
|
||||||
.. versionadded:: 0.6
|
.. versionadded:: 0.6
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,20 +48,21 @@ the application for testing and initializes a new database::
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from flaskr import create_app
|
from flaskr import create_app
|
||||||
|
from flaskr.db import init_db
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
|
db_fd, db_path = tempfile.mkstemp()
|
||||||
flaskr.app.config['TESTING'] = True
|
app = create_app({'TESTING': True, 'DATABASE': db_path})
|
||||||
|
|
||||||
with flaskr.app.test_client() as client:
|
with app.test_client() as client:
|
||||||
with flaskr.app.app_context():
|
with app.app_context():
|
||||||
flaskr.init_db()
|
init_db()
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
os.close(db_fd)
|
os.close(db_fd)
|
||||||
os.unlink(flaskr.app.config['DATABASE'])
|
os.unlink(db_path)
|
||||||
|
|
||||||
This client fixture will be called by each individual test. It gives us a
|
This client fixture will be called by each individual test. It gives us a
|
||||||
simple interface to the application, where we can trigger test requests to the
|
simple interface to the application, where we can trigger test requests to the
|
||||||
|
|
@ -224,13 +225,13 @@ temporarily. With this you can access the :class:`~flask.request`,
|
||||||
:class:`~flask.g` and :class:`~flask.session` objects like in view
|
:class:`~flask.g` and :class:`~flask.session` objects like in view
|
||||||
functions. Here is a full example that demonstrates this approach::
|
functions. Here is a full example that demonstrates this approach::
|
||||||
|
|
||||||
import flask
|
from flask import Flask, request
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
with app.test_request_context('/?name=Peter'):
|
with app.test_request_context('/?name=Peter'):
|
||||||
assert flask.request.path == '/'
|
assert request.path == '/'
|
||||||
assert flask.request.args['name'] == 'Peter'
|
assert request.args['name'] == 'Peter'
|
||||||
|
|
||||||
All the other objects that are context bound can be used in the same
|
All the other objects that are context bound can be used in the same
|
||||||
way.
|
way.
|
||||||
|
|
@ -247,7 +248,7 @@ the test request context leaves the ``with`` block. If you do want the
|
||||||
:meth:`~flask.Flask.before_request` functions to be called as well, you
|
:meth:`~flask.Flask.before_request` functions to be called as well, you
|
||||||
need to call :meth:`~flask.Flask.preprocess_request` yourself::
|
need to call :meth:`~flask.Flask.preprocess_request` yourself::
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
with app.test_request_context('/?name=Peter'):
|
with app.test_request_context('/?name=Peter'):
|
||||||
app.preprocess_request()
|
app.preprocess_request()
|
||||||
|
|
@ -260,7 +261,7 @@ If you want to call the :meth:`~flask.Flask.after_request` functions you
|
||||||
need to call into :meth:`~flask.Flask.process_response` which however
|
need to call into :meth:`~flask.Flask.process_response` which however
|
||||||
requires that you pass it a response object::
|
requires that you pass it a response object::
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
with app.test_request_context('/?name=Peter'):
|
with app.test_request_context('/?name=Peter'):
|
||||||
resp = Response('...')
|
resp = Response('...')
|
||||||
|
|
@ -329,7 +330,7 @@ context around for a little longer so that additional introspection can
|
||||||
happen. With Flask 0.4 this is possible by using the
|
happen. With Flask 0.4 this is possible by using the
|
||||||
:meth:`~flask.Flask.test_client` with a ``with`` block::
|
:meth:`~flask.Flask.test_client` with a ``with`` block::
|
||||||
|
|
||||||
app = flask.Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
with app.test_client() as c:
|
with app.test_client() as c:
|
||||||
rv = c.get('/?tequila=42')
|
rv = c.get('/?tequila=42')
|
||||||
|
|
@ -353,7 +354,7 @@ keep the context around and access :data:`flask.session`::
|
||||||
|
|
||||||
with app.test_client() as c:
|
with app.test_client() as c:
|
||||||
rv = c.get('/')
|
rv = c.get('/')
|
||||||
assert flask.session['foo'] == 42
|
assert session['foo'] == 42
|
||||||
|
|
||||||
This however does not make it possible to also modify the session or to
|
This however does not make it possible to also modify the session or to
|
||||||
access the session before a request was fired. Starting with Flask 0.8 we
|
access the session before a request was fired. Starting with Flask 0.8 we
|
||||||
|
|
|
||||||
|
|
@ -88,9 +88,9 @@ You can use the following command to output a random secret key:
|
||||||
|
|
||||||
.. code-block:: none
|
.. code-block:: none
|
||||||
|
|
||||||
$ python -c 'import os; print(os.urandom(16))'
|
$ python -c 'import secrets; print(secrets.token_hex())'
|
||||||
|
|
||||||
b'_5#y2L"F4Q8z\n\xec]/'
|
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
|
|
||||||
Create the ``config.py`` file in the instance folder, which the factory
|
Create the ``config.py`` file in the instance folder, which the factory
|
||||||
will read from if it exists. Copy the generated value into it.
|
will read from if it exists. Copy the generated value into it.
|
||||||
|
|
@ -98,7 +98,7 @@ will read from if it exists. Copy the generated value into it.
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:caption: ``venv/var/flaskr-instance/config.py``
|
:caption: ``venv/var/flaskr-instance/config.py``
|
||||||
|
|
||||||
SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/'
|
SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||||
|
|
||||||
You can also set any other necessary configuration here, although
|
You can also set any other necessary configuration here, although
|
||||||
``SECRET_KEY`` is the only one needed for Flaskr.
|
``SECRET_KEY`` is the only one needed for Flaskr.
|
||||||
|
|
|
||||||
|
|
@ -177,4 +177,9 @@ Visit http://127.0.0.1:5000/hello in a browser and you should see the
|
||||||
"Hello, World!" message. Congratulations, you're now running your Flask
|
"Hello, World!" message. Congratulations, you're now running your Flask
|
||||||
web application!
|
web application!
|
||||||
|
|
||||||
|
If another program is already using port 5000, you'll see
|
||||||
|
``OSError: [Errno 98]`` or ``OSError: [WinError 10013]`` when the
|
||||||
|
server tries to start. See :ref:`address-already-in-use` for how to
|
||||||
|
handle that.
|
||||||
|
|
||||||
Continue to :doc:`database`.
|
Continue to :doc:`database`.
|
||||||
|
|
|
||||||
|
|
@ -57,29 +57,29 @@ By the end, your project layout will look like this:
|
||||||
|
|
||||||
/home/user/Projects/flask-tutorial
|
/home/user/Projects/flask-tutorial
|
||||||
├── flaskr/
|
├── flaskr/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── db.py
|
│ ├── db.py
|
||||||
│ ├── schema.sql
|
│ ├── schema.sql
|
||||||
│ ├── auth.py
|
│ ├── auth.py
|
||||||
│ ├── blog.py
|
│ ├── blog.py
|
||||||
│ ├── templates/
|
│ ├── templates/
|
||||||
│ │ ├── base.html
|
│ │ ├── base.html
|
||||||
│ │ ├── auth/
|
│ │ ├── auth/
|
||||||
│ │ │ ├── login.html
|
│ │ │ ├── login.html
|
||||||
│ │ │ └── register.html
|
│ │ │ └── register.html
|
||||||
│ │ └── blog/
|
│ │ └── blog/
|
||||||
│ │ ├── create.html
|
│ │ ├── create.html
|
||||||
│ │ ├── index.html
|
│ │ ├── index.html
|
||||||
│ │ └── update.html
|
│ │ └── update.html
|
||||||
│ └── static/
|
│ └── static/
|
||||||
│ └── style.css
|
│ └── style.css
|
||||||
├── tests/
|
├── tests/
|
||||||
│ ├── conftest.py
|
│ ├── conftest.py
|
||||||
│ ├── data.sql
|
│ ├── data.sql
|
||||||
│ ├── test_factory.py
|
│ ├── test_factory.py
|
||||||
│ ├── test_db.py
|
│ ├── test_db.py
|
||||||
│ ├── test_auth.py
|
│ ├── test_auth.py
|
||||||
│ └── test_blog.py
|
│ └── test_blog.py
|
||||||
├── venv/
|
├── venv/
|
||||||
├── setup.py
|
├── setup.py
|
||||||
└── MANIFEST.in
|
└── MANIFEST.in
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ messages.
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
assert get_db().execute(
|
assert get_db().execute(
|
||||||
"select * from user where username = 'a'",
|
"SELECT * FROM user WHERE username = 'a'",
|
||||||
).fetchone() is not None
|
).fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,18 +91,18 @@ write templates to generate the HTML form.
|
||||||
error = 'Username is required.'
|
error = 'Username is required.'
|
||||||
elif not password:
|
elif not password:
|
||||||
error = 'Password is required.'
|
error = 'Password is required.'
|
||||||
elif db.execute(
|
|
||||||
'SELECT id FROM user WHERE username = ?', (username,)
|
|
||||||
).fetchone() is not None:
|
|
||||||
error = f"User {username} is already registered."
|
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
db.execute(
|
try:
|
||||||
'INSERT INTO user (username, password) VALUES (?, ?)',
|
db.execute(
|
||||||
(username, generate_password_hash(password))
|
"INSERT INTO user (username, password) VALUES (?, ?)",
|
||||||
)
|
(username, generate_password_hash(password)),
|
||||||
db.commit()
|
)
|
||||||
return redirect(url_for('auth.login'))
|
db.commit()
|
||||||
|
except db.IntegrityError:
|
||||||
|
error = f"User {username} is already registered."
|
||||||
|
else:
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
flash(error)
|
flash(error)
|
||||||
|
|
||||||
|
|
@ -125,26 +125,25 @@ Here's what the ``register`` view function is doing:
|
||||||
|
|
||||||
#. Validate that ``username`` and ``password`` are not empty.
|
#. Validate that ``username`` and ``password`` are not empty.
|
||||||
|
|
||||||
#. Validate that ``username`` is not already registered by querying the
|
|
||||||
database and checking if a result is returned.
|
|
||||||
:meth:`db.execute <sqlite3.Connection.execute>` takes a SQL query
|
|
||||||
with ``?`` placeholders for any user input, and a tuple of values
|
|
||||||
to replace the placeholders with. The database library will take
|
|
||||||
care of escaping the values so you are not vulnerable to a
|
|
||||||
*SQL injection attack*.
|
|
||||||
|
|
||||||
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
|
|
||||||
If the query returned no results, it returns ``None``. Later,
|
|
||||||
:meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of
|
|
||||||
all results.
|
|
||||||
|
|
||||||
#. If validation succeeds, insert the new user data into the database.
|
#. If validation succeeds, insert the new user data into the database.
|
||||||
For security, passwords should never be stored in the database
|
|
||||||
directly. Instead,
|
- :meth:`db.execute <sqlite3.Connection.execute>` takes a SQL
|
||||||
:func:`~werkzeug.security.generate_password_hash` is used to
|
query with ``?`` placeholders for any user input, and a tuple of
|
||||||
securely hash the password, and that hash is stored. Since this
|
values to replace the placeholders with. The database library
|
||||||
query modifies data, :meth:`db.commit() <sqlite3.Connection.commit>`
|
will take care of escaping the values so you are not vulnerable
|
||||||
needs to be called afterwards to save the changes.
|
to a *SQL injection attack*.
|
||||||
|
|
||||||
|
- For security, passwords should never be stored in the database
|
||||||
|
directly. Instead,
|
||||||
|
:func:`~werkzeug.security.generate_password_hash` is used to
|
||||||
|
securely hash the password, and that hash is stored. Since this
|
||||||
|
query modifies data,
|
||||||
|
:meth:`db.commit() <sqlite3.Connection.commit>` needs to be
|
||||||
|
called afterwards to save the changes.
|
||||||
|
|
||||||
|
- An :exc:`sqlite3.IntegrityError` will occur if the username
|
||||||
|
already exists, which should be shown to the user as another
|
||||||
|
validation error.
|
||||||
|
|
||||||
#. After storing the user, they are redirected to the login page.
|
#. After storing the user, they are redirected to the login page.
|
||||||
:func:`url_for` generates the URL for the login view based on its
|
:func:`url_for` generates the URL for the login view based on its
|
||||||
|
|
@ -200,6 +199,11 @@ There are a few differences from the ``register`` view:
|
||||||
|
|
||||||
#. The user is queried first and stored in a variable for later use.
|
#. The user is queried first and stored in a variable for later use.
|
||||||
|
|
||||||
|
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
|
||||||
|
If the query returned no results, it returns ``None``. Later,
|
||||||
|
:meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list
|
||||||
|
of all results.
|
||||||
|
|
||||||
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted
|
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted
|
||||||
password in the same way as the stored hash and securely compares
|
password in the same way as the stored hash and securely compares
|
||||||
them. If they match, the password is valid.
|
them. If they match, the password is valid.
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ Method Based Dispatching
|
||||||
|
|
||||||
For RESTful APIs it's especially helpful to execute a different function
|
For RESTful APIs it's especially helpful to execute a different function
|
||||||
for each HTTP method. With the :class:`flask.views.MethodView` you can
|
for each HTTP method. With the :class:`flask.views.MethodView` you can
|
||||||
easily do that. Each HTTP method maps to a function with the same name
|
easily do that. Each HTTP method maps to a method of the class with the
|
||||||
(just in lowercase)::
|
same name (just in lowercase)::
|
||||||
|
|
||||||
from flask.views import MethodView
|
from flask.views import MethodView
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,21 +60,21 @@ def register():
|
||||||
error = "Username is required."
|
error = "Username is required."
|
||||||
elif not password:
|
elif not password:
|
||||||
error = "Password is required."
|
error = "Password is required."
|
||||||
elif (
|
|
||||||
db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
error = f"User {username} is already registered."
|
|
||||||
|
|
||||||
if error is None:
|
if error is None:
|
||||||
# the name is available, store it in the database and go to
|
try:
|
||||||
# the login page
|
db.execute(
|
||||||
db.execute(
|
"INSERT INTO user (username, password) VALUES (?, ?)",
|
||||||
"INSERT INTO user (username, password) VALUES (?, ?)",
|
(username, generate_password_hash(password)),
|
||||||
(username, generate_password_hash(password)),
|
)
|
||||||
)
|
db.commit()
|
||||||
db.commit()
|
except db.IntegrityError:
|
||||||
return redirect(url_for("auth.login"))
|
# The username was already taken, which caused the
|
||||||
|
# commit to fail. Show a validation error.
|
||||||
|
error = f"User {username} is already registered."
|
||||||
|
else:
|
||||||
|
# Success, go to the login page.
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
flash(error)
|
flash(error)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ def test_register(client, app):
|
||||||
# test that the user was inserted into the database
|
# test that the user was inserted into the database
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
assert (
|
assert (
|
||||||
get_db().execute("select * from user where username = 'a'").fetchone()
|
get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone()
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,146 +1,163 @@
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile requirements/dev.in
|
# pip-compile requirements/dev.in
|
||||||
#
|
#
|
||||||
alabaster==0.7.12
|
alabaster==0.7.12
|
||||||
# via sphinx
|
# via sphinx
|
||||||
appdirs==1.4.4
|
asgiref==3.4.1
|
||||||
# via virtualenv
|
# via -r requirements/tests.in
|
||||||
asgiref==3.3.4
|
|
||||||
# via -r tests.in
|
|
||||||
attrs==21.2.0
|
attrs==21.2.0
|
||||||
# via pytest
|
# via pytest
|
||||||
babel==2.9.1
|
babel==2.9.1
|
||||||
# via sphinx
|
# via sphinx
|
||||||
|
backports.entry-points-selectable==1.1.1
|
||||||
|
# via virtualenv
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
# via -r tests.in
|
# via -r requirements/tests.in
|
||||||
certifi==2020.12.5
|
certifi==2021.10.8
|
||||||
# via requests
|
# via requests
|
||||||
cfgv==3.3.0
|
cffi==1.15.0
|
||||||
|
# via cryptography
|
||||||
|
cfgv==3.3.1
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
chardet==4.0.0
|
charset-normalizer==2.0.9
|
||||||
# via requests
|
# via requests
|
||||||
click==8.0.1
|
click==8.0.3
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
distlib==0.3.1
|
cryptography==36.0.1
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
distlib==0.3.4
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
docutils==0.16
|
docutils==0.16
|
||||||
# via
|
# via
|
||||||
# sphinx
|
# sphinx
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
filelock==3.0.12
|
filelock==3.4.0
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
greenlet==1.1.0
|
greenlet==1.1.2 ; python_version < "3.11"
|
||||||
# via -r tests.in
|
# via -r requirements/tests.in
|
||||||
identify==2.2.4
|
identify==2.4.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
idna==2.10
|
idna==3.3
|
||||||
# via requests
|
# via requests
|
||||||
imagesize==1.2.0
|
imagesize==1.3.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
# via pytest
|
# via pytest
|
||||||
jinja2==3.0.1
|
jinja2==3.0.3
|
||||||
# via sphinx
|
# via sphinx
|
||||||
markupsafe==2.0.1
|
markupsafe==2.0.1
|
||||||
# via jinja2
|
# via jinja2
|
||||||
|
mypy==0.930
|
||||||
|
# via -r requirements/typing.in
|
||||||
mypy-extensions==0.4.3
|
mypy-extensions==0.4.3
|
||||||
# via mypy
|
# via mypy
|
||||||
mypy==0.812
|
|
||||||
# via -r typing.in
|
|
||||||
nodeenv==1.6.0
|
nodeenv==1.6.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
packaging==20.9
|
packaging==21.3
|
||||||
# via
|
# via
|
||||||
# pallets-sphinx-themes
|
# pallets-sphinx-themes
|
||||||
# pytest
|
# pytest
|
||||||
# sphinx
|
# sphinx
|
||||||
# tox
|
# tox
|
||||||
pallets-sphinx-themes==2.0.1
|
pallets-sphinx-themes==2.0.2
|
||||||
# via -r docs.in
|
# via -r requirements/docs.in
|
||||||
pep517==0.10.0
|
pep517==0.12.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
pip-tools==6.1.0
|
pip-tools==6.4.0
|
||||||
# via -r dev.in
|
# via -r requirements/dev.in
|
||||||
pluggy==0.13.1
|
platformdirs==2.4.0
|
||||||
|
# via virtualenv
|
||||||
|
pluggy==1.0.0
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
pre-commit==2.13.0
|
pre-commit==2.16.0
|
||||||
# via -r dev.in
|
# via -r requirements/dev.in
|
||||||
py==1.10.0
|
py==1.11.0
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
pygments==2.9.0
|
pycparser==2.21
|
||||||
|
# via cffi
|
||||||
|
pygments==2.10.0
|
||||||
# via
|
# via
|
||||||
# sphinx
|
# sphinx
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
pyparsing==2.4.7
|
pyparsing==3.0.6
|
||||||
# via packaging
|
# via packaging
|
||||||
pytest==6.2.4
|
pytest==6.2.5
|
||||||
# via -r tests.in
|
# via -r requirements/tests.in
|
||||||
python-dotenv==0.17.1
|
python-dotenv==0.19.2
|
||||||
# via -r tests.in
|
# via -r requirements/tests.in
|
||||||
pytz==2021.1
|
pytz==2021.3
|
||||||
# via babel
|
# via babel
|
||||||
pyyaml==5.4.1
|
pyyaml==6.0
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
requests==2.25.1
|
requests==2.26.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
snowballstemmer==2.1.0
|
snowballstemmer==2.2.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinx-issues==1.2.0
|
sphinx==4.3.2
|
||||||
# via -r docs.in
|
|
||||||
sphinx-tabs==3.0.0
|
|
||||||
# via -r docs.in
|
|
||||||
sphinx==4.0.2
|
|
||||||
# via
|
# via
|
||||||
# -r docs.in
|
# -r requirements/docs.in
|
||||||
# pallets-sphinx-themes
|
# pallets-sphinx-themes
|
||||||
# sphinx-issues
|
# sphinx-issues
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
# sphinxcontrib-log-cabinet
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx-tabs==3.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
sphinxcontrib-applehelp==1.0.2
|
sphinxcontrib-applehelp==1.0.2
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-devhelp==1.0.2
|
sphinxcontrib-devhelp==1.0.2
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-htmlhelp==1.0.3
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-jsmath==1.0.1
|
sphinxcontrib-jsmath==1.0.1
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-log-cabinet==1.0.1
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
# via -r docs.in
|
# via -r requirements/docs.in
|
||||||
sphinxcontrib-qthelp==1.0.3
|
sphinxcontrib-qthelp==1.0.3
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-serializinghtml==1.1.4
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
# via sphinx
|
# via sphinx
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via
|
# via
|
||||||
# pep517
|
|
||||||
# pre-commit
|
# pre-commit
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
tox==3.23.1
|
tomli==2.0.0
|
||||||
# via -r dev.in
|
# via
|
||||||
typed-ast==1.4.3
|
# mypy
|
||||||
|
# pep517
|
||||||
|
tox==3.24.4
|
||||||
|
# via -r requirements/dev.in
|
||||||
|
types-contextvars==2.4.0
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
types-dataclasses==0.6.1
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
types-setuptools==57.4.4
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
typing-extensions==4.0.1
|
||||||
# via mypy
|
# via mypy
|
||||||
typing-extensions==3.10.0.0
|
urllib3==1.26.7
|
||||||
# via mypy
|
|
||||||
urllib3==1.26.5
|
|
||||||
# via requests
|
# via requests
|
||||||
virtualenv==20.4.6
|
virtualenv==20.10.0
|
||||||
# via
|
# via
|
||||||
# pre-commit
|
# pre-commit
|
||||||
# tox
|
# tox
|
||||||
|
wheel==0.37.1
|
||||||
|
# via pip-tools
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# pip
|
# pip
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile requirements/docs.in
|
# pip-compile requirements/docs.in
|
||||||
|
|
@ -8,66 +8,66 @@ alabaster==0.7.12
|
||||||
# via sphinx
|
# via sphinx
|
||||||
babel==2.9.1
|
babel==2.9.1
|
||||||
# via sphinx
|
# via sphinx
|
||||||
certifi==2020.12.5
|
certifi==2021.10.8
|
||||||
# via requests
|
# via requests
|
||||||
chardet==4.0.0
|
charset-normalizer==2.0.9
|
||||||
# via requests
|
# via requests
|
||||||
docutils==0.16
|
docutils==0.16
|
||||||
# via
|
# via
|
||||||
# sphinx
|
# sphinx
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
idna==2.10
|
idna==3.3
|
||||||
# via requests
|
# via requests
|
||||||
imagesize==1.2.0
|
imagesize==1.3.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
jinja2==3.0.1
|
jinja2==3.0.3
|
||||||
# via sphinx
|
# via sphinx
|
||||||
markupsafe==2.0.1
|
markupsafe==2.0.1
|
||||||
# via jinja2
|
# via jinja2
|
||||||
packaging==20.9
|
packaging==21.3
|
||||||
# via
|
# via
|
||||||
# pallets-sphinx-themes
|
# pallets-sphinx-themes
|
||||||
# sphinx
|
# sphinx
|
||||||
pallets-sphinx-themes==2.0.1
|
pallets-sphinx-themes==2.0.2
|
||||||
# via -r docs.in
|
# via -r requirements/docs.in
|
||||||
pygments==2.9.0
|
pygments==2.10.0
|
||||||
# via
|
# via
|
||||||
# sphinx
|
# sphinx
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
pyparsing==2.4.7
|
pyparsing==3.0.6
|
||||||
# via packaging
|
# via packaging
|
||||||
pytz==2021.1
|
pytz==2021.3
|
||||||
# via babel
|
# via babel
|
||||||
requests==2.25.1
|
requests==2.26.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
snowballstemmer==2.1.0
|
snowballstemmer==2.2.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinx-issues==1.2.0
|
sphinx==4.3.2
|
||||||
# via -r docs.in
|
|
||||||
sphinx-tabs==3.0.0
|
|
||||||
# via -r docs.in
|
|
||||||
sphinx==4.0.2
|
|
||||||
# via
|
# via
|
||||||
# -r docs.in
|
# -r requirements/docs.in
|
||||||
# pallets-sphinx-themes
|
# pallets-sphinx-themes
|
||||||
# sphinx-issues
|
# sphinx-issues
|
||||||
# sphinx-tabs
|
# sphinx-tabs
|
||||||
# sphinxcontrib-log-cabinet
|
# sphinxcontrib-log-cabinet
|
||||||
|
sphinx-issues==1.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
|
sphinx-tabs==3.2.0
|
||||||
|
# via -r requirements/docs.in
|
||||||
sphinxcontrib-applehelp==1.0.2
|
sphinxcontrib-applehelp==1.0.2
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-devhelp==1.0.2
|
sphinxcontrib-devhelp==1.0.2
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-htmlhelp==1.0.3
|
sphinxcontrib-htmlhelp==2.0.0
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-jsmath==1.0.1
|
sphinxcontrib-jsmath==1.0.1
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-log-cabinet==1.0.1
|
sphinxcontrib-log-cabinet==1.0.1
|
||||||
# via -r docs.in
|
# via -r requirements/docs.in
|
||||||
sphinxcontrib-qthelp==1.0.3
|
sphinxcontrib-qthelp==1.0.3
|
||||||
# via sphinx
|
# via sphinx
|
||||||
sphinxcontrib-serializinghtml==1.1.4
|
sphinxcontrib-serializinghtml==1.1.5
|
||||||
# via sphinx
|
# via sphinx
|
||||||
urllib3==1.26.5
|
urllib3==1.26.7
|
||||||
# via requests
|
# via requests
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
|
|
||||||
5
requirements/tests-pallets-dev.in
Normal file
5
requirements/tests-pallets-dev.in
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
|
||||||
|
https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz
|
||||||
|
https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz
|
||||||
|
https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz
|
||||||
|
https://github.com/pallets/click/archive/refs/heads/main.tar.gz
|
||||||
18
requirements/tests-pallets-dev.txt
Normal file
18
requirements/tests-pallets-dev.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/tests-pallets-dev.in
|
||||||
|
#
|
||||||
|
click @ https://github.com/pallets/click/archive/refs/heads/main.tar.gz
|
||||||
|
# via -r requirements/tests-pallets-dev.in
|
||||||
|
itsdangerous @ https://github.com/pallets/itsdangerous/archive/refs/heads/main.tar.gz
|
||||||
|
# via -r requirements/tests-pallets-dev.in
|
||||||
|
jinja2 @ https://github.com/pallets/jinja/archive/refs/heads/main.tar.gz
|
||||||
|
# via -r requirements/tests-pallets-dev.in
|
||||||
|
markupsafe @ https://github.com/pallets/markupsafe/archive/refs/heads/main.tar.gz
|
||||||
|
# via
|
||||||
|
# -r requirements/tests-pallets-dev.in
|
||||||
|
# jinja2
|
||||||
|
werkzeug @ https://github.com/pallets/werkzeug/archive/refs/heads/main.tar.gz
|
||||||
|
# via -r requirements/tests-pallets-dev.in
|
||||||
5
requirements/tests-pallets-min.in
Normal file
5
requirements/tests-pallets-min.in
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Werkzeug==2.0.0
|
||||||
|
Jinja2==3.0.0
|
||||||
|
MarkupSafe==2.0.0
|
||||||
|
itsdangerous==2.0.0
|
||||||
|
click==8.0.0
|
||||||
18
requirements/tests-pallets-min.txt
Normal file
18
requirements/tests-pallets-min.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
|
# To update, run:
|
||||||
|
#
|
||||||
|
# pip-compile requirements/tests-pallets-min.in
|
||||||
|
#
|
||||||
|
click==8.0.0
|
||||||
|
# via -r requirements/tests-pallets-min.in
|
||||||
|
itsdangerous==2.0.0
|
||||||
|
# via -r requirements/tests-pallets-min.in
|
||||||
|
jinja2==3.0.0
|
||||||
|
# via -r requirements/tests-pallets-min.in
|
||||||
|
markupsafe==2.0.0
|
||||||
|
# via
|
||||||
|
# -r requirements/tests-pallets-min.in
|
||||||
|
# jinja2
|
||||||
|
werkzeug==2.0.0
|
||||||
|
# via -r requirements/tests-pallets-min.in
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pytest
|
pytest
|
||||||
asgiref
|
asgiref
|
||||||
blinker
|
blinker
|
||||||
greenlet
|
greenlet ; python_version < "3.11"
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,30 @@
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile requirements/tests.in
|
# pip-compile requirements/tests.in
|
||||||
#
|
#
|
||||||
asgiref==3.3.4
|
asgiref==3.4.1
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
attrs==21.2.0
|
attrs==21.2.0
|
||||||
# via pytest
|
# via pytest
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
greenlet==1.1.0
|
greenlet==1.1.2 ; python_version < "3.11"
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
iniconfig==1.1.1
|
iniconfig==1.1.1
|
||||||
# via pytest
|
# via pytest
|
||||||
packaging==20.9
|
packaging==21.3
|
||||||
# via pytest
|
# via pytest
|
||||||
pluggy==0.13.1
|
pluggy==1.0.0
|
||||||
# via pytest
|
# via pytest
|
||||||
py==1.10.0
|
py==1.11.0
|
||||||
# via pytest
|
# via pytest
|
||||||
pyparsing==2.4.7
|
pyparsing==3.0.6
|
||||||
# via packaging
|
# via packaging
|
||||||
pytest==6.2.4
|
pytest==6.2.5
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
python-dotenv==0.17.1
|
python-dotenv==0.19.2
|
||||||
# via -r requirements/tests.in
|
# via -r requirements/tests.in
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
# via pytest
|
# via pytest
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
mypy
|
mypy
|
||||||
|
types-contextvars
|
||||||
|
types-dataclasses
|
||||||
|
types-setuptools
|
||||||
|
cryptography
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,26 @@
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile
|
# This file is autogenerated by pip-compile with python 3.10
|
||||||
# To update, run:
|
# To update, run:
|
||||||
#
|
#
|
||||||
# pip-compile requirements/typing.in
|
# pip-compile requirements/typing.in
|
||||||
#
|
#
|
||||||
|
cffi==1.15.0
|
||||||
|
# via cryptography
|
||||||
|
cryptography==36.0.1
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
mypy==0.930
|
||||||
|
# via -r requirements/typing.in
|
||||||
mypy-extensions==0.4.3
|
mypy-extensions==0.4.3
|
||||||
# via mypy
|
# via mypy
|
||||||
mypy==0.812
|
pycparser==2.21
|
||||||
|
# via cffi
|
||||||
|
tomli==2.0.0
|
||||||
|
# via mypy
|
||||||
|
types-contextvars==2.4.0
|
||||||
# via -r requirements/typing.in
|
# via -r requirements/typing.in
|
||||||
typed-ast==1.4.3
|
types-dataclasses==0.6.1
|
||||||
# via mypy
|
# via -r requirements/typing.in
|
||||||
typing-extensions==3.10.0.0
|
types-setuptools==57.4.4
|
||||||
|
# via -r requirements/typing.in
|
||||||
|
typing-extensions==4.0.1
|
||||||
# via mypy
|
# via mypy
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ classifiers =
|
||||||
packages = find:
|
packages = find:
|
||||||
package_dir = = src
|
package_dir = = src
|
||||||
include_package_data = true
|
include_package_data = true
|
||||||
python_requires = >= 3.6
|
python_requires = >= 3.7
|
||||||
# Dependencies are in setup.py for GitHub's dependency graph.
|
# Dependencies are in setup.py for GitHub's dependency graph.
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
|
|
@ -88,7 +88,7 @@ per-file-ignores =
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
files = src/flask
|
files = src/flask
|
||||||
python_version = 3.6
|
python_version = 3.7
|
||||||
allow_redefinition = True
|
allow_redefinition = True
|
||||||
disallow_subclassing_any = True
|
disallow_subclassing_any = True
|
||||||
# disallow_untyped_calls = True
|
# disallow_untyped_calls = True
|
||||||
|
|
@ -112,3 +112,6 @@ ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-dotenv.*]
|
[mypy-dotenv.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-cryptography.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ from .helpers import flash as flash
|
||||||
from .helpers import get_flashed_messages as get_flashed_messages
|
from .helpers import get_flashed_messages as get_flashed_messages
|
||||||
from .helpers import get_template_attribute as get_template_attribute
|
from .helpers import get_template_attribute as get_template_attribute
|
||||||
from .helpers import make_response as make_response
|
from .helpers import make_response as make_response
|
||||||
from .helpers import safe_join as safe_join
|
|
||||||
from .helpers import send_file as send_file
|
from .helpers import send_file as send_file
|
||||||
from .helpers import send_from_directory as send_from_directory
|
from .helpers import send_from_directory as send_from_directory
|
||||||
from .helpers import stream_with_context as stream_with_context
|
from .helpers import stream_with_context as stream_with_context
|
||||||
|
|
|
||||||
142
src/flask/app.py
142
src/flask/app.py
|
|
@ -16,7 +16,6 @@ from werkzeug.exceptions import BadRequest
|
||||||
from werkzeug.exceptions import BadRequestKeyError
|
from werkzeug.exceptions import BadRequestKeyError
|
||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
from werkzeug.local import ContextVar
|
|
||||||
from werkzeug.routing import BuildError
|
from werkzeug.routing import BuildError
|
||||||
from werkzeug.routing import Map
|
from werkzeug.routing import Map
|
||||||
from werkzeug.routing import MapAdapter
|
from werkzeug.routing import MapAdapter
|
||||||
|
|
@ -58,18 +57,12 @@ from .signals import request_started
|
||||||
from .signals import request_tearing_down
|
from .signals import request_tearing_down
|
||||||
from .templating import DispatchingJinjaLoader
|
from .templating import DispatchingJinjaLoader
|
||||||
from .templating import Environment
|
from .templating import Environment
|
||||||
from .typing import AfterRequestCallable
|
|
||||||
from .typing import BeforeFirstRequestCallable
|
from .typing import BeforeFirstRequestCallable
|
||||||
from .typing import BeforeRequestCallable
|
|
||||||
from .typing import ErrorHandlerCallable
|
|
||||||
from .typing import ResponseReturnValue
|
from .typing import ResponseReturnValue
|
||||||
from .typing import TeardownCallable
|
from .typing import TeardownCallable
|
||||||
from .typing import TemplateContextProcessorCallable
|
|
||||||
from .typing import TemplateFilterCallable
|
from .typing import TemplateFilterCallable
|
||||||
from .typing import TemplateGlobalCallable
|
from .typing import TemplateGlobalCallable
|
||||||
from .typing import TemplateTestCallable
|
from .typing import TemplateTestCallable
|
||||||
from .typing import URLDefaultCallable
|
|
||||||
from .typing import URLValuePreprocessorCallable
|
|
||||||
from .wrappers import Request
|
from .wrappers import Request
|
||||||
from .wrappers import Response
|
from .wrappers import Response
|
||||||
|
|
||||||
|
|
@ -78,6 +71,7 @@ if t.TYPE_CHECKING:
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
from .testing import FlaskClient
|
from .testing import FlaskClient
|
||||||
from .testing import FlaskCliRunner
|
from .testing import FlaskCliRunner
|
||||||
|
from .typing import ErrorHandlerCallable
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
iscoroutinefunction = inspect.iscoroutinefunction
|
iscoroutinefunction = inspect.iscoroutinefunction
|
||||||
|
|
@ -366,7 +360,8 @@ class Flask(Scaffold):
|
||||||
#: .. versionadded:: 1.1.0
|
#: .. versionadded:: 1.1.0
|
||||||
url_map_class = Map
|
url_map_class = Map
|
||||||
|
|
||||||
#: the test client that is used with when `test_client` is used.
|
#: The :meth:`test_client` method creates an instance of this test
|
||||||
|
#: client class. Defaults to :class:`~flask.testing.FlaskClient`.
|
||||||
#:
|
#:
|
||||||
#: .. versionadded:: 0.7
|
#: .. versionadded:: 0.7
|
||||||
test_client_class: t.Optional[t.Type["FlaskClient"]] = None
|
test_client_class: t.Optional[t.Type["FlaskClient"]] = None
|
||||||
|
|
@ -389,7 +384,7 @@ class Flask(Scaffold):
|
||||||
self,
|
self,
|
||||||
import_name: str,
|
import_name: str,
|
||||||
static_url_path: t.Optional[str] = None,
|
static_url_path: t.Optional[str] = None,
|
||||||
static_folder: t.Optional[str] = "static",
|
static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
|
||||||
static_host: t.Optional[str] = None,
|
static_host: t.Optional[str] = None,
|
||||||
host_matching: bool = False,
|
host_matching: bool = False,
|
||||||
subdomain_matching: bool = False,
|
subdomain_matching: bool = False,
|
||||||
|
|
@ -744,20 +739,21 @@ class Flask(Scaffold):
|
||||||
:param context: the context as a dictionary that is updated in place
|
:param context: the context as a dictionary that is updated in place
|
||||||
to add extra variables.
|
to add extra variables.
|
||||||
"""
|
"""
|
||||||
funcs: t.Iterable[
|
names: t.Iterable[t.Optional[str]] = (None,)
|
||||||
TemplateContextProcessorCallable
|
|
||||||
] = self.template_context_processors[None]
|
# A template may be rendered outside a request context.
|
||||||
reqctx = _request_ctx_stack.top
|
if request:
|
||||||
if reqctx is not None:
|
names = chain(names, reversed(request.blueprints))
|
||||||
for bp in request.blueprints:
|
|
||||||
if bp in self.template_context_processors:
|
# The values passed to render_template take precedence. Keep a
|
||||||
funcs = chain(funcs, self.template_context_processors[bp])
|
# copy to re-apply after all context functions.
|
||||||
orig_ctx = context.copy()
|
orig_ctx = context.copy()
|
||||||
for func in funcs:
|
|
||||||
context.update(func())
|
for name in names:
|
||||||
# make sure the original values win. This makes it possible to
|
if name in self.template_context_processors:
|
||||||
# easier add new variables in context processors without breaking
|
for func in self.template_context_processors[name]:
|
||||||
# existing views.
|
context.update(func())
|
||||||
|
|
||||||
context.update(orig_ctx)
|
context.update(orig_ctx)
|
||||||
|
|
||||||
def make_shell_context(self) -> dict:
|
def make_shell_context(self) -> dict:
|
||||||
|
|
@ -1268,16 +1264,19 @@ class Flask(Scaffold):
|
||||||
self.shell_context_processors.append(f)
|
self.shell_context_processors.append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def _find_error_handler(self, e: Exception) -> t.Optional[ErrorHandlerCallable]:
|
def _find_error_handler(
|
||||||
|
self, e: Exception
|
||||||
|
) -> t.Optional["ErrorHandlerCallable[Exception]"]:
|
||||||
"""Return a registered error handler for an exception in this order:
|
"""Return a registered error handler for an exception in this order:
|
||||||
blueprint handler for a specific code, app handler for a specific code,
|
blueprint handler for a specific code, app handler for a specific code,
|
||||||
blueprint handler for an exception class, app handler for an exception
|
blueprint handler for an exception class, app handler for an exception
|
||||||
class, or ``None`` if a suitable handler is not found.
|
class, or ``None`` if a suitable handler is not found.
|
||||||
"""
|
"""
|
||||||
exc_class, code = self._get_exc_class_and_code(type(e))
|
exc_class, code = self._get_exc_class_and_code(type(e))
|
||||||
|
names = (*request.blueprints, None)
|
||||||
|
|
||||||
for c in [code, None]:
|
for c in (code, None) if code is not None else (None,):
|
||||||
for name in chain(request.blueprints, [None]):
|
for name in names:
|
||||||
handler_map = self.error_handler_spec[name][c]
|
handler_map = self.error_handler_spec[name][c]
|
||||||
|
|
||||||
if not handler_map:
|
if not handler_map:
|
||||||
|
|
@ -1304,7 +1303,7 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
.. versionchanged:: 1.0
|
.. versionchanged:: 1.0
|
||||||
Exceptions are looked up by code *and* by MRO, so
|
Exceptions are looked up by code *and* by MRO, so
|
||||||
``HTTPExcpetion`` subclasses can be handled with a catch-all
|
``HTTPException`` subclasses can be handled with a catch-all
|
||||||
handler for the base ``HTTPException``.
|
handler for the base ``HTTPException``.
|
||||||
|
|
||||||
.. versionadded:: 0.3
|
.. versionadded:: 0.3
|
||||||
|
|
@ -1619,14 +1618,7 @@ class Flask(Scaffold):
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Install Flask with the 'async' extra in order to use async views."
|
"Install Flask with the 'async' extra in order to use async views."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
# Check that Werkzeug isn't using its fallback ContextVar class.
|
|
||||||
if ContextVar.__module__ == "werkzeug.local":
|
|
||||||
raise RuntimeError(
|
|
||||||
"Async cannot be used with this combination of Python "
|
|
||||||
"and Greenlet versions."
|
|
||||||
)
|
|
||||||
|
|
||||||
return asgiref_async_to_sync(func)
|
return asgiref_async_to_sync(func)
|
||||||
|
|
||||||
|
|
@ -1725,7 +1717,7 @@ class Flask(Scaffold):
|
||||||
" response. The return type must be a string,"
|
" response. The return type must be a string,"
|
||||||
" dict, tuple, Response instance, or WSGI"
|
" dict, tuple, Response instance, or WSGI"
|
||||||
f" callable, but it was a {type(rv).__name__}."
|
f" callable, but it was a {type(rv).__name__}."
|
||||||
).with_traceback(sys.exc_info()[2])
|
).with_traceback(sys.exc_info()[2]) from None
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"The view function did not return a valid"
|
"The view function did not return a valid"
|
||||||
|
|
@ -1797,17 +1789,19 @@ class Flask(Scaffold):
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
.. versionadded:: 0.7
|
||||||
"""
|
"""
|
||||||
funcs: t.Iterable[URLDefaultCallable] = self.url_default_functions[None]
|
names: t.Iterable[t.Optional[str]] = (None,)
|
||||||
|
|
||||||
|
# url_for may be called outside a request context, parse the
|
||||||
|
# passed endpoint instead of using request.blueprints.
|
||||||
if "." in endpoint:
|
if "." in endpoint:
|
||||||
# This is called by url_for, which can be called outside a
|
names = chain(
|
||||||
# request, can't use request.blueprints.
|
names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
|
||||||
bps = _split_blueprint_path(endpoint.rpartition(".")[0])
|
)
|
||||||
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
|
|
||||||
funcs = chain(funcs, bp_funcs)
|
|
||||||
|
|
||||||
for func in funcs:
|
for name in names:
|
||||||
func(endpoint, values)
|
if name in self.url_default_functions:
|
||||||
|
for func in self.url_default_functions[name]:
|
||||||
|
func(endpoint, values)
|
||||||
|
|
||||||
def handle_url_build_error(
|
def handle_url_build_error(
|
||||||
self, error: Exception, endpoint: str, values: dict
|
self, error: Exception, endpoint: str, values: dict
|
||||||
|
|
@ -1842,24 +1836,20 @@ class Flask(Scaffold):
|
||||||
value is handled as if it was the return value from the view, and
|
value is handled as if it was the return value from the view, and
|
||||||
further request handling is stopped.
|
further request handling is stopped.
|
||||||
"""
|
"""
|
||||||
|
names = (None, *reversed(request.blueprints))
|
||||||
|
|
||||||
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[
|
for name in names:
|
||||||
None
|
if name in self.url_value_preprocessors:
|
||||||
]
|
for url_func in self.url_value_preprocessors[name]:
|
||||||
for bp in request.blueprints:
|
url_func(request.endpoint, request.view_args)
|
||||||
if bp in self.url_value_preprocessors:
|
|
||||||
funcs = chain(funcs, self.url_value_preprocessors[bp])
|
|
||||||
for func in funcs:
|
|
||||||
func(request.endpoint, request.view_args)
|
|
||||||
|
|
||||||
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None]
|
for name in names:
|
||||||
for bp in request.blueprints:
|
if name in self.before_request_funcs:
|
||||||
if bp in self.before_request_funcs:
|
for before_func in self.before_request_funcs[name]:
|
||||||
funcs = chain(funcs, self.before_request_funcs[bp])
|
rv = self.ensure_sync(before_func)()
|
||||||
for func in funcs:
|
|
||||||
rv = self.ensure_sync(func)()
|
if rv is not None:
|
||||||
if rv is not None:
|
return rv
|
||||||
return rv
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -1877,16 +1867,18 @@ class Flask(Scaffold):
|
||||||
instance of :attr:`response_class`.
|
instance of :attr:`response_class`.
|
||||||
"""
|
"""
|
||||||
ctx = _request_ctx_stack.top
|
ctx = _request_ctx_stack.top
|
||||||
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
|
|
||||||
for bp in request.blueprints:
|
for func in ctx._after_request_functions:
|
||||||
if bp in self.after_request_funcs:
|
response = self.ensure_sync(func)(response)
|
||||||
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
|
|
||||||
if None in self.after_request_funcs:
|
for name in chain(request.blueprints, (None,)):
|
||||||
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
|
if name in self.after_request_funcs:
|
||||||
for handler in funcs:
|
for func in reversed(self.after_request_funcs[name]):
|
||||||
response = self.ensure_sync(handler)(response)
|
response = self.ensure_sync(func)(response)
|
||||||
|
|
||||||
if not self.session_interface.is_null_session(ctx.session):
|
if not self.session_interface.is_null_session(ctx.session):
|
||||||
self.session_interface.save_session(self, ctx.session, response)
|
self.session_interface.save_session(self, ctx.session, response)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def do_teardown_request(
|
def do_teardown_request(
|
||||||
|
|
@ -1914,14 +1906,12 @@ class Flask(Scaffold):
|
||||||
"""
|
"""
|
||||||
if exc is _sentinel:
|
if exc is _sentinel:
|
||||||
exc = sys.exc_info()[1]
|
exc = sys.exc_info()[1]
|
||||||
funcs: t.Iterable[TeardownCallable] = reversed(
|
|
||||||
self.teardown_request_funcs[None]
|
for name in chain(request.blueprints, (None,)):
|
||||||
)
|
if name in self.teardown_request_funcs:
|
||||||
for bp in request.blueprints:
|
for func in reversed(self.teardown_request_funcs[name]):
|
||||||
if bp in self.teardown_request_funcs:
|
self.ensure_sync(func)(exc)
|
||||||
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
|
|
||||||
for func in funcs:
|
|
||||||
self.ensure_sync(func)(exc)
|
|
||||||
request_tearing_down.send(self, exc=exc)
|
request_tearing_down.send(self, exc=exc)
|
||||||
|
|
||||||
def do_teardown_appcontext(
|
def do_teardown_appcontext(
|
||||||
|
|
@ -1943,8 +1933,10 @@ class Flask(Scaffold):
|
||||||
"""
|
"""
|
||||||
if exc is _sentinel:
|
if exc is _sentinel:
|
||||||
exc = sys.exc_info()[1]
|
exc = sys.exc_info()[1]
|
||||||
|
|
||||||
for func in reversed(self.teardown_appcontext_funcs):
|
for func in reversed(self.teardown_appcontext_funcs):
|
||||||
self.ensure_sync(func)(exc)
|
self.ensure_sync(func)(exc)
|
||||||
|
|
||||||
appcontext_tearing_down.send(self, exc=exc)
|
appcontext_tearing_down.send(self, exc=exc)
|
||||||
|
|
||||||
def app_context(self) -> AppContext:
|
def app_context(self) -> AppContext:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import typing as t
|
import typing as t
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
@ -8,7 +9,6 @@ from .scaffold import Scaffold
|
||||||
from .typing import AfterRequestCallable
|
from .typing import AfterRequestCallable
|
||||||
from .typing import BeforeFirstRequestCallable
|
from .typing import BeforeFirstRequestCallable
|
||||||
from .typing import BeforeRequestCallable
|
from .typing import BeforeRequestCallable
|
||||||
from .typing import ErrorHandlerCallable
|
|
||||||
from .typing import TeardownCallable
|
from .typing import TeardownCallable
|
||||||
from .typing import TemplateContextProcessorCallable
|
from .typing import TemplateContextProcessorCallable
|
||||||
from .typing import TemplateFilterCallable
|
from .typing import TemplateFilterCallable
|
||||||
|
|
@ -19,6 +19,7 @@ from .typing import URLValuePreprocessorCallable
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
|
from .typing import ErrorHandlerCallable
|
||||||
|
|
||||||
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
||||||
|
|
||||||
|
|
@ -175,7 +176,7 @@ class Blueprint(Scaffold):
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
import_name: str,
|
import_name: str,
|
||||||
static_folder: t.Optional[str] = None,
|
static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
|
||||||
static_url_path: t.Optional[str] = None,
|
static_url_path: t.Optional[str] = None,
|
||||||
template_folder: t.Optional[str] = None,
|
template_folder: t.Optional[str] = None,
|
||||||
url_prefix: t.Optional[str] = None,
|
url_prefix: t.Optional[str] = None,
|
||||||
|
|
@ -293,34 +294,26 @@ class Blueprint(Scaffold):
|
||||||
Registering the same blueprint with the same name multiple
|
Registering the same blueprint with the same name multiple
|
||||||
times is deprecated and will become an error in Flask 2.1.
|
times is deprecated and will become an error in Flask 2.1.
|
||||||
"""
|
"""
|
||||||
first_registration = not any(bp is self for bp in app.blueprints.values())
|
|
||||||
name_prefix = options.get("name_prefix", "")
|
name_prefix = options.get("name_prefix", "")
|
||||||
self_name = options.get("name", self.name)
|
self_name = options.get("name", self.name)
|
||||||
name = f"{name_prefix}.{self_name}".lstrip(".")
|
name = f"{name_prefix}.{self_name}".lstrip(".")
|
||||||
|
|
||||||
if name in app.blueprints:
|
if name in app.blueprints:
|
||||||
|
bp_desc = "this" if app.blueprints[name] is self else "a different"
|
||||||
existing_at = f" '{name}'" if self_name != name else ""
|
existing_at = f" '{name}'" if self_name != name else ""
|
||||||
|
|
||||||
if app.blueprints[name] is not self:
|
raise ValueError(
|
||||||
raise ValueError(
|
f"The name '{self_name}' is already registered for"
|
||||||
f"The name '{self_name}' is already registered for"
|
f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
|
||||||
f" a different blueprint{existing_at}. Use 'name='"
|
f" provide a unique name."
|
||||||
" to provide a unique name."
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
warnings.warn(
|
first_bp_registration = not any(bp is self for bp in app.blueprints.values())
|
||||||
f"The name '{self_name}' is already registered for"
|
first_name_registration = name not in app.blueprints
|
||||||
f" this blueprint{existing_at}. Use 'name=' to"
|
|
||||||
" provide a unique name. This will become an error"
|
|
||||||
" in Flask 2.1.",
|
|
||||||
stacklevel=4,
|
|
||||||
)
|
|
||||||
|
|
||||||
app.blueprints[name] = self
|
app.blueprints[name] = self
|
||||||
self._got_registered_once = True
|
self._got_registered_once = True
|
||||||
state = self.make_setup_state(app, options, first_registration)
|
state = self.make_setup_state(app, options, first_bp_registration)
|
||||||
|
|
||||||
if self.has_static_folder:
|
if self.has_static_folder:
|
||||||
state.add_url_rule(
|
state.add_url_rule(
|
||||||
|
|
@ -330,7 +323,7 @@ class Blueprint(Scaffold):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Merge blueprint data into parent.
|
# Merge blueprint data into parent.
|
||||||
if first_registration:
|
if first_bp_registration or first_name_registration:
|
||||||
|
|
||||||
def extend(bp_dict, parent_dict):
|
def extend(bp_dict, parent_dict):
|
||||||
for key, values in bp_dict.items():
|
for key, values in bp_dict.items():
|
||||||
|
|
@ -581,7 +574,9 @@ class Blueprint(Scaffold):
|
||||||
handler is used for all requests, even if outside of the blueprint.
|
handler is used for all requests, even if outside of the blueprint.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
|
def decorator(
|
||||||
|
f: "ErrorHandlerCallable[Exception]",
|
||||||
|
) -> "ErrorHandlerCallable[Exception]":
|
||||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
|
||||||
107
src/flask/cli.py
107
src/flask/cli.py
|
|
@ -5,7 +5,6 @@ import platform
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
@ -34,7 +33,7 @@ class NoAppException(click.UsageError):
|
||||||
"""Raised if an application cannot be found or loaded."""
|
"""Raised if an application cannot be found or loaded."""
|
||||||
|
|
||||||
|
|
||||||
def find_best_app(script_info, module):
|
def find_best_app(module):
|
||||||
"""Given a module instance this tries to find the best possible
|
"""Given a module instance this tries to find the best possible
|
||||||
application in the module or raises an exception.
|
application in the module or raises an exception.
|
||||||
"""
|
"""
|
||||||
|
|
@ -65,19 +64,20 @@ def find_best_app(script_info, module):
|
||||||
|
|
||||||
if inspect.isfunction(app_factory):
|
if inspect.isfunction(app_factory):
|
||||||
try:
|
try:
|
||||||
app = call_factory(script_info, app_factory)
|
app = app_factory()
|
||||||
|
|
||||||
if isinstance(app, Flask):
|
if isinstance(app, Flask):
|
||||||
return app
|
return app
|
||||||
except TypeError:
|
except TypeError as e:
|
||||||
if not _called_with_wrong_args(app_factory):
|
if not _called_with_wrong_args(app_factory):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"Detected factory {attr_name!r} in module {module.__name__!r},"
|
f"Detected factory {attr_name!r} in module {module.__name__!r},"
|
||||||
" but could not call it without arguments. Use"
|
" but could not call it without arguments. Use"
|
||||||
f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
|
f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
|
||||||
" to specify arguments."
|
" to specify arguments."
|
||||||
)
|
) from e
|
||||||
|
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
"Failed to find Flask application or factory in module"
|
"Failed to find Flask application or factory in module"
|
||||||
|
|
@ -86,39 +86,6 @@ def find_best_app(script_info, module):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def call_factory(script_info, app_factory, args=None, kwargs=None):
|
|
||||||
"""Takes an app factory, a ``script_info` object and optionally a tuple
|
|
||||||
of arguments. Checks for the existence of a script_info argument and calls
|
|
||||||
the app_factory depending on that and the arguments provided.
|
|
||||||
"""
|
|
||||||
sig = inspect.signature(app_factory)
|
|
||||||
args = [] if args is None else args
|
|
||||||
kwargs = {} if kwargs is None else kwargs
|
|
||||||
|
|
||||||
if "script_info" in sig.parameters:
|
|
||||||
warnings.warn(
|
|
||||||
"The 'script_info' argument is deprecated and will not be"
|
|
||||||
" passed to the app factory function in Flask 2.1.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
kwargs["script_info"] = script_info
|
|
||||||
|
|
||||||
if (
|
|
||||||
not args
|
|
||||||
and len(sig.parameters) == 1
|
|
||||||
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty
|
|
||||||
):
|
|
||||||
warnings.warn(
|
|
||||||
"Script info is deprecated and will not be passed as the"
|
|
||||||
" single argument to the app factory function in Flask"
|
|
||||||
" 2.1.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
args.append(script_info)
|
|
||||||
|
|
||||||
return app_factory(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _called_with_wrong_args(f):
|
def _called_with_wrong_args(f):
|
||||||
"""Check whether calling a function raised a ``TypeError`` because
|
"""Check whether calling a function raised a ``TypeError`` because
|
||||||
the call failed or because something in the factory raised the
|
the call failed or because something in the factory raised the
|
||||||
|
|
@ -145,7 +112,7 @@ def _called_with_wrong_args(f):
|
||||||
del tb
|
del tb
|
||||||
|
|
||||||
|
|
||||||
def find_app_by_string(script_info, module, app_name):
|
def find_app_by_string(module, app_name):
|
||||||
"""Check if the given string is a variable name or a function. Call
|
"""Check if the given string is a variable name or a function. Call
|
||||||
a function to get the app instance, or return the variable directly.
|
a function to get the app instance, or return the variable directly.
|
||||||
"""
|
"""
|
||||||
|
|
@ -158,11 +125,12 @@ def find_app_by_string(script_info, module, app_name):
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"Failed to parse {app_name!r} as an attribute name or function call."
|
f"Failed to parse {app_name!r} as an attribute name or function call."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
if isinstance(expr, ast.Name):
|
if isinstance(expr, ast.Name):
|
||||||
name = expr.id
|
name = expr.id
|
||||||
args = kwargs = None
|
args = []
|
||||||
|
kwargs = {}
|
||||||
elif isinstance(expr, ast.Call):
|
elif isinstance(expr, ast.Call):
|
||||||
# Ensure the function name is an attribute name only.
|
# Ensure the function name is an attribute name only.
|
||||||
if not isinstance(expr.func, ast.Name):
|
if not isinstance(expr.func, ast.Name):
|
||||||
|
|
@ -181,7 +149,7 @@ def find_app_by_string(script_info, module, app_name):
|
||||||
# message with the full expression instead.
|
# message with the full expression instead.
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"Failed to parse arguments as literal values: {app_name!r}."
|
f"Failed to parse arguments as literal values: {app_name!r}."
|
||||||
)
|
) from None
|
||||||
else:
|
else:
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"Failed to parse {app_name!r} as an attribute name or function call."
|
f"Failed to parse {app_name!r} as an attribute name or function call."
|
||||||
|
|
@ -189,17 +157,17 @@ def find_app_by_string(script_info, module, app_name):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
attr = getattr(module, name)
|
attr = getattr(module, name)
|
||||||
except AttributeError:
|
except AttributeError as e:
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"Failed to find attribute {name!r} in {module.__name__!r}."
|
f"Failed to find attribute {name!r} in {module.__name__!r}."
|
||||||
)
|
) from e
|
||||||
|
|
||||||
# If the attribute is a function, call it with any args and kwargs
|
# If the attribute is a function, call it with any args and kwargs
|
||||||
# to get the real application.
|
# to get the real application.
|
||||||
if inspect.isfunction(attr):
|
if inspect.isfunction(attr):
|
||||||
try:
|
try:
|
||||||
app = call_factory(script_info, attr, args, kwargs)
|
app = attr(*args, **kwargs)
|
||||||
except TypeError:
|
except TypeError as e:
|
||||||
if not _called_with_wrong_args(attr):
|
if not _called_with_wrong_args(attr):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -207,7 +175,7 @@ def find_app_by_string(script_info, module, app_name):
|
||||||
f"The factory {app_name!r} in module"
|
f"The factory {app_name!r} in module"
|
||||||
f" {module.__name__!r} could not be called with the"
|
f" {module.__name__!r} could not be called with the"
|
||||||
" specified arguments."
|
" specified arguments."
|
||||||
)
|
) from e
|
||||||
else:
|
else:
|
||||||
app = attr
|
app = attr
|
||||||
|
|
||||||
|
|
@ -249,7 +217,7 @@ def prepare_import(path):
|
||||||
return ".".join(module_name[::-1])
|
return ".".join(module_name[::-1])
|
||||||
|
|
||||||
|
|
||||||
def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
|
def locate_app(module_name, app_name, raise_if_not_found=True):
|
||||||
__traceback_hide__ = True # noqa: F841
|
__traceback_hide__ = True # noqa: F841
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -261,18 +229,18 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
|
||||||
raise NoAppException(
|
raise NoAppException(
|
||||||
f"While importing {module_name!r}, an ImportError was"
|
f"While importing {module_name!r}, an ImportError was"
|
||||||
f" raised:\n\n{traceback.format_exc()}"
|
f" raised:\n\n{traceback.format_exc()}"
|
||||||
)
|
) from None
|
||||||
elif raise_if_not_found:
|
elif raise_if_not_found:
|
||||||
raise NoAppException(f"Could not import {module_name!r}.")
|
raise NoAppException(f"Could not import {module_name!r}.") from None
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
module = sys.modules[module_name]
|
module = sys.modules[module_name]
|
||||||
|
|
||||||
if app_name is None:
|
if app_name is None:
|
||||||
return find_best_app(script_info, module)
|
return find_best_app(module)
|
||||||
else:
|
else:
|
||||||
return find_app_by_string(script_info, module, app_name)
|
return find_app_by_string(module, app_name)
|
||||||
|
|
||||||
|
|
||||||
def get_version(ctx, param, value):
|
def get_version(ctx, param, value):
|
||||||
|
|
@ -312,7 +280,7 @@ class DispatchingApp:
|
||||||
self.loader = loader
|
self.loader = loader
|
||||||
self._app = None
|
self._app = None
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
self._bg_loading_exc_info = None
|
self._bg_loading_exc = None
|
||||||
|
|
||||||
if use_eager_loading is None:
|
if use_eager_loading is None:
|
||||||
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
|
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
|
||||||
|
|
@ -328,23 +296,24 @@ class DispatchingApp:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
try:
|
try:
|
||||||
self._load_unlocked()
|
self._load_unlocked()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self._bg_loading_exc_info = sys.exc_info()
|
self._bg_loading_exc = e
|
||||||
|
|
||||||
t = Thread(target=_load_app, args=())
|
t = Thread(target=_load_app, args=())
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def _flush_bg_loading_exception(self):
|
def _flush_bg_loading_exception(self):
|
||||||
__traceback_hide__ = True # noqa: F841
|
__traceback_hide__ = True # noqa: F841
|
||||||
exc_info = self._bg_loading_exc_info
|
exc = self._bg_loading_exc
|
||||||
if exc_info is not None:
|
|
||||||
self._bg_loading_exc_info = None
|
if exc is not None:
|
||||||
raise exc_info
|
self._bg_loading_exc = None
|
||||||
|
raise exc
|
||||||
|
|
||||||
def _load_unlocked(self):
|
def _load_unlocked(self):
|
||||||
__traceback_hide__ = True # noqa: F841
|
__traceback_hide__ = True # noqa: F841
|
||||||
self._app = rv = self.loader()
|
self._app = rv = self.loader()
|
||||||
self._bg_loading_exc_info = None
|
self._bg_loading_exc = None
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
|
@ -392,18 +361,18 @@ class ScriptInfo:
|
||||||
return self._loaded_app
|
return self._loaded_app
|
||||||
|
|
||||||
if self.create_app is not None:
|
if self.create_app is not None:
|
||||||
app = call_factory(self, self.create_app)
|
app = self.create_app()
|
||||||
else:
|
else:
|
||||||
if self.app_import_path:
|
if self.app_import_path:
|
||||||
path, name = (
|
path, name = (
|
||||||
re.split(r":(?![\\/])", self.app_import_path, 1) + [None]
|
re.split(r":(?![\\/])", self.app_import_path, 1) + [None]
|
||||||
)[:2]
|
)[:2]
|
||||||
import_name = prepare_import(path)
|
import_name = prepare_import(path)
|
||||||
app = locate_app(self, import_name, name)
|
app = locate_app(import_name, name)
|
||||||
else:
|
else:
|
||||||
for path in ("wsgi.py", "app.py"):
|
for path in ("wsgi.py", "app.py"):
|
||||||
import_name = prepare_import(path)
|
import_name = prepare_import(path)
|
||||||
app = locate_app(self, import_name, None, raise_if_not_found=False)
|
app = locate_app(import_name, None, raise_if_not_found=False)
|
||||||
|
|
||||||
if app:
|
if app:
|
||||||
break
|
break
|
||||||
|
|
@ -721,7 +690,7 @@ class CertParamType(click.ParamType):
|
||||||
"Using ad-hoc certificates requires the cryptography library.",
|
"Using ad-hoc certificates requires the cryptography library.",
|
||||||
ctx,
|
ctx,
|
||||||
param,
|
param,
|
||||||
)
|
) from None
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -979,15 +948,7 @@ debug mode.
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if int(click.__version__[0]) < 8:
|
cli.main()
|
||||||
warnings.warn(
|
|
||||||
"Using the `flask` cli with Click 7 is deprecated and"
|
|
||||||
" will not be supported starting with Flask 2.1."
|
|
||||||
" Please upgrade to Click 8 as soon as possible.",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
# TODO omit sys.argv once https://github.com/pallets/click/issues/536 is fixed
|
|
||||||
cli.main(args=sys.argv[1:])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class Config(dict):
|
||||||
:param variable_name: name of the environment variable
|
:param variable_name: name of the environment variable
|
||||||
:param silent: set to ``True`` if you want silent failure for missing
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
files.
|
files.
|
||||||
:return: bool. ``True`` if able to load config, ``False`` otherwise.
|
:return: ``True`` if the file was loaded successfully.
|
||||||
"""
|
"""
|
||||||
rv = os.environ.get(variable_name)
|
rv = os.environ.get(variable_name)
|
||||||
if not rv:
|
if not rv:
|
||||||
|
|
@ -107,6 +107,7 @@ class Config(dict):
|
||||||
root path.
|
root path.
|
||||||
:param silent: set to ``True`` if you want silent failure for missing
|
:param silent: set to ``True`` if you want silent failure for missing
|
||||||
files.
|
files.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
.. versionadded:: 0.7
|
||||||
`silent` parameter.
|
`silent` parameter.
|
||||||
|
|
@ -175,6 +176,9 @@ class Config(dict):
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
import json
|
||||||
|
app.config.from_file("config.json", load=json.load)
|
||||||
|
|
||||||
import toml
|
import toml
|
||||||
app.config.from_file("config.toml", load=toml.load)
|
app.config.from_file("config.toml", load=toml.load)
|
||||||
|
|
||||||
|
|
@ -185,6 +189,7 @@ class Config(dict):
|
||||||
:type load: ``Callable[[Reader], Mapping]`` where ``Reader``
|
:type load: ``Callable[[Reader], Mapping]`` where ``Reader``
|
||||||
implements a ``read`` method.
|
implements a ``read`` method.
|
||||||
:param silent: Ignore the file if it doesn't exist.
|
:param silent: Ignore the file if it doesn't exist.
|
||||||
|
:return: ``True`` if the file was loaded successfully.
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
.. versionadded:: 2.0
|
||||||
"""
|
"""
|
||||||
|
|
@ -202,36 +207,12 @@ class Config(dict):
|
||||||
|
|
||||||
return self.from_mapping(obj)
|
return self.from_mapping(obj)
|
||||||
|
|
||||||
def from_json(self, filename: str, silent: bool = False) -> bool:
|
|
||||||
"""Update the values in the config from a JSON file. The loaded
|
|
||||||
data is passed to the :meth:`from_mapping` method.
|
|
||||||
|
|
||||||
:param filename: The path to the JSON file. This can be an
|
|
||||||
absolute path or relative to the config root path.
|
|
||||||
:param silent: Ignore the file if it doesn't exist.
|
|
||||||
|
|
||||||
.. deprecated:: 2.0.0
|
|
||||||
Will be removed in Flask 2.1. Use :meth:`from_file` instead.
|
|
||||||
This was removed early in 2.0.0, was added back in 2.0.1.
|
|
||||||
|
|
||||||
.. versionadded:: 0.11
|
|
||||||
"""
|
|
||||||
import warnings
|
|
||||||
from . import json
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"'from_json' is deprecated and will be removed in Flask"
|
|
||||||
" 2.1. Use 'from_file(path, json.load)' instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
return self.from_file(filename, json.load, silent=silent)
|
|
||||||
|
|
||||||
def from_mapping(
|
def from_mapping(
|
||||||
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
|
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Updates the config like :meth:`update` ignoring items with non-upper
|
"""Updates the config like :meth:`update` ignoring items with non-upper
|
||||||
keys.
|
keys.
|
||||||
|
:return: Always returns ``True``.
|
||||||
|
|
||||||
.. versionadded:: 0.11
|
.. versionadded:: 0.11
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,15 @@ def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable:
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_request_ctx_stack.top._after_request_functions.append(f)
|
top = _request_ctx_stack.top
|
||||||
|
|
||||||
|
if top is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"This decorator can only be used when a request context is"
|
||||||
|
" active, such as within a view function."
|
||||||
|
)
|
||||||
|
|
||||||
|
top._after_request_functions.append(f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -159,17 +167,18 @@ def copy_current_request_context(f: t.Callable) -> t.Callable:
|
||||||
.. versionadded:: 0.10
|
.. versionadded:: 0.10
|
||||||
"""
|
"""
|
||||||
top = _request_ctx_stack.top
|
top = _request_ctx_stack.top
|
||||||
|
|
||||||
if top is None:
|
if top is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"This decorator can only be used at local scopes "
|
"This decorator can only be used when a request context is"
|
||||||
"when a request context is on the stack. For instance within "
|
" active, such as within a view function."
|
||||||
"view functions."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reqctx = top.copy()
|
reqctx = top.copy()
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
with reqctx:
|
with reqctx:
|
||||||
return f(*args, **kwargs)
|
return reqctx.app.ensure_sync(f)(*args, **kwargs)
|
||||||
|
|
||||||
return update_wrapper(wrapper, f)
|
return update_wrapper(wrapper, f)
|
||||||
|
|
||||||
|
|
@ -332,11 +341,29 @@ class RequestContext:
|
||||||
self._after_request_functions: t.List[AfterRequestCallable] = []
|
self._after_request_functions: t.List[AfterRequestCallable] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def g(self) -> AppContext:
|
def g(self) -> _AppCtxGlobals:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Accessing 'g' on the request context is deprecated and"
|
||||||
|
" will be removed in Flask 2.2. Access `g` directly or from"
|
||||||
|
"the application context instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
return _app_ctx_stack.top.g
|
return _app_ctx_stack.top.g
|
||||||
|
|
||||||
@g.setter
|
@g.setter
|
||||||
def g(self, value: AppContext) -> None:
|
def g(self, value: _AppCtxGlobals) -> None:
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.warn(
|
||||||
|
"Setting 'g' on the request context is deprecated and"
|
||||||
|
" will be removed in Flask 2.2. Set it on the application"
|
||||||
|
" context instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
_app_ctx_stack.top.g = value
|
_app_ctx_stack.top.g = value
|
||||||
|
|
||||||
def copy(self) -> "RequestContext":
|
def copy(self) -> "RequestContext":
|
||||||
|
|
|
||||||
|
|
@ -83,10 +83,11 @@ def attach_enctype_error_multidict(request):
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
try:
|
try:
|
||||||
return oldcls.__getitem__(self, key)
|
return oldcls.__getitem__(self, key)
|
||||||
except KeyError:
|
except KeyError as e:
|
||||||
if key not in request.form:
|
if key not in request.form:
|
||||||
raise
|
raise
|
||||||
raise DebugFilesKeyError(request, key)
|
|
||||||
|
raise DebugFilesKeyError(request, key) from e
|
||||||
|
|
||||||
newcls.__name__ = oldcls.__name__
|
newcls.__name__ = oldcls.__name__
|
||||||
newcls.__module__ = oldcls.__module__
|
newcls.__module__ = oldcls.__module__
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,11 @@ import sys
|
||||||
import typing as t
|
import typing as t
|
||||||
import warnings
|
import warnings
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
import werkzeug.utils
|
import werkzeug.utils
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
from werkzeug.routing import BuildError
|
from werkzeug.routing import BuildError
|
||||||
from werkzeug.urls import url_quote
|
from werkzeug.urls import url_quote
|
||||||
|
|
||||||
|
|
@ -454,7 +452,7 @@ def _prepare_send_file_kwargs(
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'attachment_filename' parameter has been renamed to"
|
"The 'attachment_filename' parameter has been renamed to"
|
||||||
" 'download_name'. The old name will be removed in Flask"
|
" 'download_name'. The old name will be removed in Flask"
|
||||||
" 2.1.",
|
" 2.2.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
)
|
)
|
||||||
|
|
@ -463,7 +461,7 @@ def _prepare_send_file_kwargs(
|
||||||
if cache_timeout is not None:
|
if cache_timeout is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'cache_timeout' parameter has been renamed to"
|
"The 'cache_timeout' parameter has been renamed to"
|
||||||
" 'max_age'. The old name will be removed in Flask 2.1.",
|
" 'max_age'. The old name will be removed in Flask 2.2.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
)
|
)
|
||||||
|
|
@ -472,7 +470,7 @@ def _prepare_send_file_kwargs(
|
||||||
if add_etags is not None:
|
if add_etags is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'add_etags' parameter has been renamed to 'etag'. The"
|
"The 'add_etags' parameter has been renamed to 'etag'. The"
|
||||||
" old name will be removed in Flask 2.1.",
|
" old name will be removed in Flask 2.2.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=3,
|
stacklevel=3,
|
||||||
)
|
)
|
||||||
|
|
@ -627,29 +625,6 @@ def send_file(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def safe_join(directory: str, *pathnames: str) -> str:
|
|
||||||
"""Safely join zero or more untrusted path components to a base
|
|
||||||
directory to avoid escaping the base directory.
|
|
||||||
|
|
||||||
:param directory: The trusted base directory.
|
|
||||||
:param pathnames: The untrusted path components relative to the
|
|
||||||
base directory.
|
|
||||||
:return: A safe path, otherwise ``None``.
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"'flask.helpers.safe_join' is deprecated and will be removed in"
|
|
||||||
" Flask 2.1. Use 'werkzeug.utils.safe_join' instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
path = werkzeug.utils.safe_join(directory, *pathnames)
|
|
||||||
|
|
||||||
if path is None:
|
|
||||||
raise NotFound()
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def send_from_directory(
|
def send_from_directory(
|
||||||
directory: t.Union[os.PathLike, str],
|
directory: t.Union[os.PathLike, str],
|
||||||
path: t.Union[os.PathLike, str],
|
path: t.Union[os.PathLike, str],
|
||||||
|
|
@ -691,7 +666,7 @@ def send_from_directory(
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"The 'filename' parameter has been renamed to 'path'. The"
|
"The 'filename' parameter has been renamed to 'path'. The"
|
||||||
" old name will be removed in Flask 2.1.",
|
" old name will be removed in Flask 2.2.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|
@ -714,7 +689,7 @@ def get_root_path(import_name: str) -> str:
|
||||||
# Module already imported and has a file attribute. Use that first.
|
# Module already imported and has a file attribute. Use that first.
|
||||||
mod = sys.modules.get(import_name)
|
mod = sys.modules.get(import_name)
|
||||||
|
|
||||||
if mod is not None and hasattr(mod, "__file__"):
|
if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
|
||||||
return os.path.dirname(os.path.abspath(mod.__file__))
|
return os.path.dirname(os.path.abspath(mod.__file__))
|
||||||
|
|
||||||
# Next attempt: check the loader.
|
# Next attempt: check the loader.
|
||||||
|
|
@ -785,27 +760,6 @@ class locked_cached_property(werkzeug.utils.cached_property):
|
||||||
super().__delete__(obj)
|
super().__delete__(obj)
|
||||||
|
|
||||||
|
|
||||||
def total_seconds(td: timedelta) -> int:
|
|
||||||
"""Returns the total seconds from a timedelta object.
|
|
||||||
|
|
||||||
:param timedelta td: the timedelta to be converted in seconds
|
|
||||||
|
|
||||||
:returns: number of seconds
|
|
||||||
:rtype: int
|
|
||||||
|
|
||||||
.. deprecated:: 2.0
|
|
||||||
Will be removed in Flask 2.1. Use
|
|
||||||
:meth:`timedelta.total_seconds` instead.
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"'total_seconds' is deprecated and will be removed in Flask"
|
|
||||||
" 2.1. Use 'timedelta.total_seconds' instead.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
return td.days * 60 * 60 * 24 + td.seconds
|
|
||||||
|
|
||||||
|
|
||||||
def is_ip(value: str) -> bool:
|
def is_ip(value: str) -> bool:
|
||||||
"""Determine if the given string is an IP address.
|
"""Determine if the given string is an IP address.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import io
|
import dataclasses
|
||||||
|
import decimal
|
||||||
import json as _json
|
import json as _json
|
||||||
import typing as t
|
import typing as t
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
|
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
|
||||||
|
|
@ -15,12 +15,6 @@ if t.TYPE_CHECKING:
|
||||||
from ..app import Flask
|
from ..app import Flask
|
||||||
from ..wrappers import Response
|
from ..wrappers import Response
|
||||||
|
|
||||||
try:
|
|
||||||
import dataclasses
|
|
||||||
except ImportError:
|
|
||||||
# Python < 3.7
|
|
||||||
dataclasses = None # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(_json.JSONEncoder):
|
class JSONEncoder(_json.JSONEncoder):
|
||||||
"""The default JSON encoder. Handles extra types compared to the
|
"""The default JSON encoder. Handles extra types compared to the
|
||||||
|
|
@ -47,7 +41,7 @@ class JSONEncoder(_json.JSONEncoder):
|
||||||
"""
|
"""
|
||||||
if isinstance(o, date):
|
if isinstance(o, date):
|
||||||
return http_date(o)
|
return http_date(o)
|
||||||
if isinstance(o, uuid.UUID):
|
if isinstance(o, (decimal.Decimal, uuid.UUID)):
|
||||||
return str(o)
|
return str(o)
|
||||||
if dataclasses and dataclasses.is_dataclass(o):
|
if dataclasses and dataclasses.is_dataclass(o):
|
||||||
return dataclasses.asdict(o)
|
return dataclasses.asdict(o)
|
||||||
|
|
@ -80,6 +74,11 @@ def _dump_arg_defaults(
|
||||||
if bp is not None and bp.json_encoder is not None:
|
if bp is not None and bp.json_encoder is not None:
|
||||||
cls = bp.json_encoder
|
cls = bp.json_encoder
|
||||||
|
|
||||||
|
# Only set a custom encoder if it has custom behavior. This is
|
||||||
|
# faster on PyPy.
|
||||||
|
if cls is not _json.JSONEncoder:
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
kwargs.setdefault("cls", cls)
|
kwargs.setdefault("cls", cls)
|
||||||
kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
|
kwargs.setdefault("ensure_ascii", app.config["JSON_AS_ASCII"])
|
||||||
kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
|
kwargs.setdefault("sort_keys", app.config["JSON_SORT_KEYS"])
|
||||||
|
|
@ -101,9 +100,10 @@ def _load_arg_defaults(
|
||||||
if bp is not None and bp.json_decoder is not None:
|
if bp is not None and bp.json_decoder is not None:
|
||||||
cls = bp.json_decoder
|
cls = bp.json_decoder
|
||||||
|
|
||||||
kwargs.setdefault("cls", cls)
|
# Only set a custom decoder if it has custom behavior. This is
|
||||||
else:
|
# faster on PyPy.
|
||||||
kwargs.setdefault("cls", JSONDecoder)
|
if cls not in {JSONDecoder, _json.JSONDecoder}:
|
||||||
|
kwargs.setdefault("cls", cls)
|
||||||
|
|
||||||
|
|
||||||
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
|
def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
|
||||||
|
|
@ -117,6 +117,9 @@ def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
|
||||||
or defaults.
|
or defaults.
|
||||||
:param kwargs: Extra arguments passed to :func:`json.dumps`.
|
:param kwargs: Extra arguments passed to :func:`json.dumps`.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.2
|
||||||
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
.. versionchanged:: 2.0
|
.. versionchanged:: 2.0
|
||||||
``encoding`` is deprecated and will be removed in Flask 2.1.
|
``encoding`` is deprecated and will be removed in Flask 2.1.
|
||||||
|
|
||||||
|
|
@ -125,20 +128,7 @@ def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
|
||||||
context for configuration.
|
context for configuration.
|
||||||
"""
|
"""
|
||||||
_dump_arg_defaults(kwargs, app=app)
|
_dump_arg_defaults(kwargs, app=app)
|
||||||
encoding = kwargs.pop("encoding", None)
|
return _json.dumps(obj, **kwargs)
|
||||||
rv = _json.dumps(obj, **kwargs)
|
|
||||||
|
|
||||||
if encoding is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"'encoding' is deprecated and will be removed in Flask 2.1.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(rv, str):
|
|
||||||
return rv.encode(encoding) # type: ignore
|
|
||||||
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def dump(
|
def dump(
|
||||||
|
|
@ -160,23 +150,6 @@ def dump(
|
||||||
deprecated and will be removed in Flask 2.1.
|
deprecated and will be removed in Flask 2.1.
|
||||||
"""
|
"""
|
||||||
_dump_arg_defaults(kwargs, app=app)
|
_dump_arg_defaults(kwargs, app=app)
|
||||||
encoding = kwargs.pop("encoding", None)
|
|
||||||
show_warning = encoding is not None
|
|
||||||
|
|
||||||
try:
|
|
||||||
fp.write("")
|
|
||||||
except TypeError:
|
|
||||||
show_warning = True
|
|
||||||
fp = io.TextIOWrapper(fp, encoding or "utf-8") # type: ignore
|
|
||||||
|
|
||||||
if show_warning:
|
|
||||||
warnings.warn(
|
|
||||||
"Writing to a binary file, and the 'encoding' argument, is"
|
|
||||||
" deprecated and will be removed in Flask 2.1.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
_json.dump(obj, fp, **kwargs)
|
_json.dump(obj, fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -200,19 +173,6 @@ def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
|
||||||
context for configuration.
|
context for configuration.
|
||||||
"""
|
"""
|
||||||
_load_arg_defaults(kwargs, app=app)
|
_load_arg_defaults(kwargs, app=app)
|
||||||
encoding = kwargs.pop("encoding", None)
|
|
||||||
|
|
||||||
if encoding is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"'encoding' is deprecated and will be removed in Flask 2.1."
|
|
||||||
" The data must be a string or UTF-8 bytes.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(s, bytes):
|
|
||||||
s = s.decode(encoding)
|
|
||||||
|
|
||||||
return _json.loads(s, **kwargs)
|
return _json.loads(s, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -232,20 +192,6 @@ def load(fp: t.IO[str], app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.A
|
||||||
file must be text mode, or binary mode with UTF-8 bytes.
|
file must be text mode, or binary mode with UTF-8 bytes.
|
||||||
"""
|
"""
|
||||||
_load_arg_defaults(kwargs, app=app)
|
_load_arg_defaults(kwargs, app=app)
|
||||||
encoding = kwargs.pop("encoding", None)
|
|
||||||
|
|
||||||
if encoding is not None:
|
|
||||||
warnings.warn(
|
|
||||||
"'encoding' is deprecated and will be removed in Flask 2.1."
|
|
||||||
" The file must be text mode, or binary mode with UTF-8"
|
|
||||||
" bytes.",
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(fp.read(0), bytes):
|
|
||||||
fp = io.TextIOWrapper(fp, encoding) # type: ignore
|
|
||||||
|
|
||||||
return _json.load(fp, **kwargs)
|
return _json.load(fp, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -324,6 +270,9 @@ def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
|
||||||
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
|
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
|
||||||
the output will be formatted to be easier to read.
|
the output will be formatted to be easier to read.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0.2
|
||||||
|
:class:`decimal.Decimal` is supported by converting to a string.
|
||||||
|
|
||||||
.. versionchanged:: 0.11
|
.. versionchanged:: 0.11
|
||||||
Added support for serializing top-level arrays. This introduces
|
Added support for serializing top-level arrays. This introduces
|
||||||
a security risk in ancient browsers. See :ref:`security-json`.
|
a security risk in ancient browsers. See :ref:`security-json`.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from .templating import _default_template_ctx_processor
|
||||||
from .typing import AfterRequestCallable
|
from .typing import AfterRequestCallable
|
||||||
from .typing import AppOrBlueprintKey
|
from .typing import AppOrBlueprintKey
|
||||||
from .typing import BeforeRequestCallable
|
from .typing import BeforeRequestCallable
|
||||||
from .typing import ErrorHandlerCallable
|
from .typing import GenericException
|
||||||
from .typing import TeardownCallable
|
from .typing import TeardownCallable
|
||||||
from .typing import TemplateContextProcessorCallable
|
from .typing import TemplateContextProcessorCallable
|
||||||
from .typing import URLDefaultCallable
|
from .typing import URLDefaultCallable
|
||||||
|
|
@ -29,6 +29,7 @@ from .typing import URLValuePreprocessorCallable
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
from .wrappers import Response
|
from .wrappers import Response
|
||||||
|
from .typing import ErrorHandlerCallable
|
||||||
|
|
||||||
# a singleton sentinel value for parameter defaults
|
# a singleton sentinel value for parameter defaults
|
||||||
_sentinel = object()
|
_sentinel = object()
|
||||||
|
|
@ -91,7 +92,7 @@ class Scaffold:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
import_name: str,
|
import_name: str,
|
||||||
static_folder: t.Optional[str] = None,
|
static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
|
||||||
static_url_path: t.Optional[str] = None,
|
static_url_path: t.Optional[str] = None,
|
||||||
template_folder: t.Optional[str] = None,
|
template_folder: t.Optional[str] = None,
|
||||||
root_path: t.Optional[str] = None,
|
root_path: t.Optional[str] = None,
|
||||||
|
|
@ -100,7 +101,7 @@ class Scaffold:
|
||||||
#: to. Do not change this once it is set by the constructor.
|
#: to. Do not change this once it is set by the constructor.
|
||||||
self.import_name = import_name
|
self.import_name = import_name
|
||||||
|
|
||||||
self.static_folder = static_folder
|
self.static_folder = static_folder # type: ignore
|
||||||
self.static_url_path = static_url_path
|
self.static_url_path = static_url_path
|
||||||
|
|
||||||
#: The path to the templates folder, relative to
|
#: The path to the templates folder, relative to
|
||||||
|
|
@ -144,7 +145,10 @@ class Scaffold:
|
||||||
#: directly and its format may change at any time.
|
#: directly and its format may change at any time.
|
||||||
self.error_handler_spec: t.Dict[
|
self.error_handler_spec: t.Dict[
|
||||||
AppOrBlueprintKey,
|
AppOrBlueprintKey,
|
||||||
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ErrorHandlerCallable]],
|
t.Dict[
|
||||||
|
t.Optional[int],
|
||||||
|
t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
|
||||||
|
],
|
||||||
] = defaultdict(lambda: defaultdict(dict))
|
] = defaultdict(lambda: defaultdict(dict))
|
||||||
|
|
||||||
#: A data structure of functions to call at the beginning of
|
#: A data structure of functions to call at the beginning of
|
||||||
|
|
@ -253,7 +257,7 @@ class Scaffold:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@static_folder.setter
|
@static_folder.setter
|
||||||
def static_folder(self, value: t.Optional[str]) -> None:
|
def static_folder(self, value: t.Optional[t.Union[str, os.PathLike]]) -> None:
|
||||||
if value is not None:
|
if value is not None:
|
||||||
value = os.fspath(value).rstrip(r"\/")
|
value = os.fspath(value).rstrip(r"\/")
|
||||||
|
|
||||||
|
|
@ -585,10 +589,10 @@ class Scaffold:
|
||||||
stack of active contexts. This becomes relevant if you are using
|
stack of active contexts. This becomes relevant if you are using
|
||||||
such constructs in tests.
|
such constructs in tests.
|
||||||
|
|
||||||
Teardown functions must avoid raising exceptions, since they . If they
|
Teardown functions must avoid raising exceptions. If
|
||||||
execute code that might fail they
|
they execute code that might fail they
|
||||||
will have to surround the execution of these code by try/except
|
will have to surround the execution of that code with try/except
|
||||||
statements and log occurring errors.
|
statements and log any errors.
|
||||||
|
|
||||||
When a teardown function was called because of an exception it will
|
When a teardown function was called because of an exception it will
|
||||||
be passed an error object.
|
be passed an error object.
|
||||||
|
|
@ -643,8 +647,11 @@ class Scaffold:
|
||||||
|
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def errorhandler(
|
def errorhandler(
|
||||||
self, code_or_exception: t.Union[t.Type[Exception], int]
|
self, code_or_exception: t.Union[t.Type[GenericException], int]
|
||||||
) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]:
|
) -> t.Callable[
|
||||||
|
["ErrorHandlerCallable[GenericException]"],
|
||||||
|
"ErrorHandlerCallable[GenericException]",
|
||||||
|
]:
|
||||||
"""Register a function to handle errors by code or exception class.
|
"""Register a function to handle errors by code or exception class.
|
||||||
|
|
||||||
A decorator that is used to register a function given an
|
A decorator that is used to register a function given an
|
||||||
|
|
@ -674,7 +681,9 @@ class Scaffold:
|
||||||
an arbitrary exception
|
an arbitrary exception
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
|
def decorator(
|
||||||
|
f: "ErrorHandlerCallable[GenericException]",
|
||||||
|
) -> "ErrorHandlerCallable[GenericException]":
|
||||||
self.register_error_handler(code_or_exception, f)
|
self.register_error_handler(code_or_exception, f)
|
||||||
return f
|
return f
|
||||||
|
|
||||||
|
|
@ -683,8 +692,8 @@ class Scaffold:
|
||||||
@setupmethod
|
@setupmethod
|
||||||
def register_error_handler(
|
def register_error_handler(
|
||||||
self,
|
self,
|
||||||
code_or_exception: t.Union[t.Type[Exception], int],
|
code_or_exception: t.Union[t.Type[GenericException], int],
|
||||||
f: ErrorHandlerCallable,
|
f: "ErrorHandlerCallable[GenericException]",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Alternative error attach function to the :meth:`errorhandler`
|
"""Alternative error attach function to the :meth:`errorhandler`
|
||||||
decorator that is more straightforward to use for non decorator
|
decorator that is more straightforward to use for non decorator
|
||||||
|
|
@ -706,9 +715,11 @@ class Scaffold:
|
||||||
f"'{code_or_exception}' is not a recognized HTTP error"
|
f"'{code_or_exception}' is not a recognized HTTP error"
|
||||||
" code. Use a subclass of HTTPException with that code"
|
" code. Use a subclass of HTTPException with that code"
|
||||||
" instead."
|
" instead."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
self.error_handler_spec[None][code][exc_class] = f
|
self.error_handler_spec[None][code][exc_class] = t.cast(
|
||||||
|
"ErrorHandlerCallable[Exception]", f
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_exc_class_and_code(
|
def _get_exc_class_and_code(
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,13 @@ class SessionInterface:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.session_interface = MySessionInterface()
|
app.session_interface = MySessionInterface()
|
||||||
|
|
||||||
|
Multiple requests with the same session may be sent and handled
|
||||||
|
concurrently. When implementing a new session interface, consider
|
||||||
|
whether reads or writes to the backing store must be synchronized.
|
||||||
|
There is no guarantee on the order in which the session for each
|
||||||
|
request is opened or saved, it will occur in the order that requests
|
||||||
|
begin and end processing.
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -292,20 +299,25 @@ class SessionInterface:
|
||||||
def open_session(
|
def open_session(
|
||||||
self, app: "Flask", request: "Request"
|
self, app: "Flask", request: "Request"
|
||||||
) -> t.Optional[SessionMixin]:
|
) -> t.Optional[SessionMixin]:
|
||||||
"""This method has to be implemented and must either return ``None``
|
"""This is called at the beginning of each request, after
|
||||||
in case the loading failed because of a configuration error or an
|
pushing the request context, before matching the URL.
|
||||||
instance of a session object which implements a dictionary like
|
|
||||||
interface + the methods and attributes on :class:`SessionMixin`.
|
This must return an object which implements a dictionary-like
|
||||||
|
interface as well as the :class:`SessionMixin` interface.
|
||||||
|
|
||||||
|
This will return ``None`` to indicate that loading failed in
|
||||||
|
some way that is not immediately an error. The request
|
||||||
|
context will fall back to using :meth:`make_null_session`
|
||||||
|
in this case.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save_session(
|
def save_session(
|
||||||
self, app: "Flask", session: SessionMixin, response: "Response"
|
self, app: "Flask", session: SessionMixin, response: "Response"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""This is called for actual sessions returned by :meth:`open_session`
|
"""This is called at the end of each request, after generating
|
||||||
at the end of the request. This is still called during a request
|
a response, before removing the request context. It is skipped
|
||||||
context so if you absolutely need access to the request you can do
|
if :meth:`is_null_session` returns ``True``.
|
||||||
that.
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ except ImportError:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Signalling support is unavailable because the blinker"
|
"Signalling support is unavailable because the blinker"
|
||||||
" library is not installed."
|
" library is not installed."
|
||||||
)
|
) from None
|
||||||
|
|
||||||
connect = connect_via = connected_to = temporarily_connected_to = _fail
|
connect = connect_via = connected_to = temporarily_connected_to = _fail
|
||||||
disconnect = _fail
|
disconnect = _fail
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@ from werkzeug.test import Client
|
||||||
from werkzeug.urls import url_parse
|
from werkzeug.urls import url_parse
|
||||||
from werkzeug.wrappers import Request as BaseRequest
|
from werkzeug.wrappers import Request as BaseRequest
|
||||||
|
|
||||||
from . import _request_ctx_stack
|
|
||||||
from .cli import ScriptInfo
|
from .cli import ScriptInfo
|
||||||
|
from .globals import _request_ctx_stack
|
||||||
from .json import dumps as json_dumps
|
from .json import dumps as json_dumps
|
||||||
from .sessions import SessionMixin
|
from .sessions import SessionMixin
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
|
from werkzeug.test import TestResponse
|
||||||
|
|
||||||
from .app import Flask
|
from .app import Flask
|
||||||
from .wrappers import Response
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironBuilder(werkzeug.test.EnvironBuilder):
|
class EnvironBuilder(werkzeug.test.EnvironBuilder):
|
||||||
|
|
@ -171,14 +172,15 @@ class FlaskClient(Client):
|
||||||
headers = resp.get_wsgi_headers(c.request.environ)
|
headers = resp.get_wsgi_headers(c.request.environ)
|
||||||
self.cookie_jar.extract_wsgi(c.request.environ, headers)
|
self.cookie_jar.extract_wsgi(c.request.environ, headers)
|
||||||
|
|
||||||
def open( # type: ignore
|
def open(
|
||||||
self,
|
self,
|
||||||
*args: t.Any,
|
*args: t.Any,
|
||||||
as_tuple: bool = False,
|
|
||||||
buffered: bool = False,
|
buffered: bool = False,
|
||||||
follow_redirects: bool = False,
|
follow_redirects: bool = False,
|
||||||
**kwargs: t.Any,
|
**kwargs: t.Any,
|
||||||
) -> "Response":
|
) -> "TestResponse":
|
||||||
|
as_tuple = kwargs.pop("as_tuple", None)
|
||||||
|
|
||||||
# Same logic as super.open, but apply environ_base and preserve_context.
|
# Same logic as super.open, but apply environ_base and preserve_context.
|
||||||
request = None
|
request = None
|
||||||
|
|
||||||
|
|
@ -213,12 +215,28 @@ class FlaskClient(Client):
|
||||||
finally:
|
finally:
|
||||||
builder.close()
|
builder.close()
|
||||||
|
|
||||||
return super().open( # type: ignore
|
if as_tuple is not None:
|
||||||
request,
|
import warnings
|
||||||
as_tuple=as_tuple,
|
|
||||||
buffered=buffered,
|
warnings.warn(
|
||||||
follow_redirects=follow_redirects,
|
"'as_tuple' is deprecated and will be removed in"
|
||||||
)
|
" Werkzeug 2.1 and Flask 2.1. Use"
|
||||||
|
" 'response.request.environ' instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=3,
|
||||||
|
)
|
||||||
|
return super().open(
|
||||||
|
request,
|
||||||
|
as_tuple=as_tuple,
|
||||||
|
buffered=buffered,
|
||||||
|
follow_redirects=follow_redirects,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return super().open(
|
||||||
|
request,
|
||||||
|
buffered=buffered,
|
||||||
|
follow_redirects=follow_redirects,
|
||||||
|
)
|
||||||
|
|
||||||
def __enter__(self) -> "FlaskClient":
|
def __enter__(self) -> "FlaskClient":
|
||||||
if self.preserve_context:
|
if self.preserve_context:
|
||||||
|
|
@ -272,7 +290,7 @@ class FlaskCliRunner(CliRunner):
|
||||||
:return: a :class:`~click.testing.Result` object.
|
:return: a :class:`~click.testing.Result` object.
|
||||||
"""
|
"""
|
||||||
if cli is None:
|
if cli is None:
|
||||||
cli = self.app.cli
|
cli = self.app.cli # type: ignore
|
||||||
|
|
||||||
if "obj" not in kwargs:
|
if "obj" not in kwargs:
|
||||||
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
|
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,12 @@ ResponseReturnValue = t.Union[
|
||||||
"WSGIApplication",
|
"WSGIApplication",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
|
||||||
|
|
||||||
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
|
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
|
||||||
AfterRequestCallable = t.Callable[["Response"], "Response"]
|
AfterRequestCallable = t.Callable[["Response"], "Response"]
|
||||||
BeforeFirstRequestCallable = t.Callable[[], None]
|
BeforeFirstRequestCallable = t.Callable[[], None]
|
||||||
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
|
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
|
||||||
ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
|
|
||||||
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
|
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
|
||||||
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
|
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
|
||||||
TemplateFilterCallable = t.Callable[..., t.Any]
|
TemplateFilterCallable = t.Callable[..., t.Any]
|
||||||
|
|
@ -45,3 +46,4 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
|
||||||
TemplateTestCallable = t.Callable[..., bool]
|
TemplateTestCallable = t.Callable[..., bool]
|
||||||
URLDefaultCallable = t.Callable[[str, dict], None]
|
URLDefaultCallable = t.Callable[[str, dict], None]
|
||||||
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
|
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
|
||||||
|
ErrorHandlerCallable = t.Callable[[GenericException], ResponseReturnValue]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import typing as t
|
import typing as t
|
||||||
|
|
||||||
|
from .globals import current_app
|
||||||
from .globals import request
|
from .globals import request
|
||||||
from .typing import ResponseReturnValue
|
from .typing import ResponseReturnValue
|
||||||
|
|
||||||
|
|
@ -80,7 +81,7 @@ class View:
|
||||||
|
|
||||||
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
|
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
|
||||||
self = view.view_class(*class_args, **class_kwargs) # type: ignore
|
self = view.view_class(*class_args, **class_kwargs) # type: ignore
|
||||||
return self.dispatch_request(*args, **kwargs)
|
return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
|
||||||
|
|
||||||
if cls.decorators:
|
if cls.decorators:
|
||||||
view.__name__ = name
|
view.__name__ = name
|
||||||
|
|
@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType):
|
||||||
meth = getattr(self, "get", None)
|
meth = getattr(self, "get", None)
|
||||||
|
|
||||||
assert meth is not None, f"Unimplemented method {request.method!r}"
|
assert meth is not None, f"Unimplemented method {request.method!r}"
|
||||||
return meth(*args, **kwargs)
|
return current_app.ensure_sync(meth)(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import request
|
from flask import request
|
||||||
|
from flask.views import MethodView
|
||||||
|
from flask.views import View
|
||||||
|
|
||||||
pytest.importorskip("asgiref")
|
pytest.importorskip("asgiref")
|
||||||
|
|
||||||
|
|
@ -18,6 +19,24 @@ class BlueprintError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncView(View):
|
||||||
|
methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
async def dispatch_request(self):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return request.method
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMethodView(MethodView):
|
||||||
|
async def get(self):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return "GET"
|
||||||
|
|
||||||
|
async def post(self):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return "POST"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="async_app")
|
@pytest.fixture(name="async_app")
|
||||||
def _async_app():
|
def _async_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
@ -53,11 +72,13 @@ def _async_app():
|
||||||
|
|
||||||
app.register_blueprint(blueprint, url_prefix="/bp")
|
app.register_blueprint(blueprint, url_prefix="/bp")
|
||||||
|
|
||||||
|
app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
|
||||||
|
app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
|
||||||
@pytest.mark.parametrize("path", ["/", "/home", "/bp/"])
|
|
||||||
def test_async_route(path, async_app):
|
def test_async_route(path, async_app):
|
||||||
test_client = async_app.test_client()
|
test_client = async_app.test_client()
|
||||||
response = test_client.get(path)
|
response = test_client.get(path)
|
||||||
|
|
@ -66,7 +87,6 @@ def test_async_route(path, async_app):
|
||||||
assert b"POST" in response.get_data()
|
assert b"POST" in response.get_data()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
|
||||||
@pytest.mark.parametrize("path", ["/error", "/bp/error"])
|
@pytest.mark.parametrize("path", ["/error", "/bp/error"])
|
||||||
def test_async_error_handler(path, async_app):
|
def test_async_error_handler(path, async_app):
|
||||||
test_client = async_app.test_client()
|
test_client = async_app.test_client()
|
||||||
|
|
@ -74,7 +94,6 @@ def test_async_error_handler(path, async_app):
|
||||||
assert response.status_code == 412
|
assert response.status_code == 412
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
|
||||||
def test_async_before_after_request():
|
def test_async_before_after_request():
|
||||||
app_first_called = False
|
app_first_called = False
|
||||||
app_before_called = False
|
app_before_called = False
|
||||||
|
|
@ -131,10 +150,3 @@ def test_async_before_after_request():
|
||||||
test_client.get("/bp/")
|
test_client.get("/bp/")
|
||||||
assert bp_before_called
|
assert bp_before_called
|
||||||
assert bp_after_called
|
assert bp_after_called
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info >= (3, 7), reason="should only raise Python < 3.7")
|
|
||||||
def test_async_runtime_error():
|
|
||||||
app = Flask(__name__)
|
|
||||||
with pytest.raises(RuntimeError):
|
|
||||||
app.async_to_sync(None)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import gc
|
import gc
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import weakref
|
import weakref
|
||||||
|
|
@ -1323,7 +1322,6 @@ def test_jsonify_mimetype(app, req_ctx):
|
||||||
assert rv.mimetype == "application/vnd.api+json"
|
assert rv.mimetype == "application/vnd.api+json"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
|
||||||
def test_json_dump_dataclass(app, req_ctx):
|
def test_json_dump_dataclass(app, req_ctx):
|
||||||
from dataclasses import make_dataclass
|
from dataclasses import make_dataclass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -837,6 +837,86 @@ def test_nested_blueprint(app, client):
|
||||||
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
|
assert client.get("/parent/child/grandchild/no").data == b"Grandchild no"
|
||||||
|
|
||||||
|
|
||||||
|
def test_nested_callback_order(app, client):
|
||||||
|
parent = flask.Blueprint("parent", __name__)
|
||||||
|
child = flask.Blueprint("child", __name__)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def app_before1():
|
||||||
|
flask.g.setdefault("seen", []).append("app_1")
|
||||||
|
|
||||||
|
@app.teardown_request
|
||||||
|
def app_teardown1(e=None):
|
||||||
|
assert flask.g.seen.pop() == "app_1"
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def app_before2():
|
||||||
|
flask.g.setdefault("seen", []).append("app_2")
|
||||||
|
|
||||||
|
@app.teardown_request
|
||||||
|
def app_teardown2(e=None):
|
||||||
|
assert flask.g.seen.pop() == "app_2"
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def app_ctx():
|
||||||
|
return dict(key="app")
|
||||||
|
|
||||||
|
@parent.before_request
|
||||||
|
def parent_before1():
|
||||||
|
flask.g.setdefault("seen", []).append("parent_1")
|
||||||
|
|
||||||
|
@parent.teardown_request
|
||||||
|
def parent_teardown1(e=None):
|
||||||
|
assert flask.g.seen.pop() == "parent_1"
|
||||||
|
|
||||||
|
@parent.before_request
|
||||||
|
def parent_before2():
|
||||||
|
flask.g.setdefault("seen", []).append("parent_2")
|
||||||
|
|
||||||
|
@parent.teardown_request
|
||||||
|
def parent_teardown2(e=None):
|
||||||
|
assert flask.g.seen.pop() == "parent_2"
|
||||||
|
|
||||||
|
@parent.context_processor
|
||||||
|
def parent_ctx():
|
||||||
|
return dict(key="parent")
|
||||||
|
|
||||||
|
@child.before_request
|
||||||
|
def child_before1():
|
||||||
|
flask.g.setdefault("seen", []).append("child_1")
|
||||||
|
|
||||||
|
@child.teardown_request
|
||||||
|
def child_teardown1(e=None):
|
||||||
|
assert flask.g.seen.pop() == "child_1"
|
||||||
|
|
||||||
|
@child.before_request
|
||||||
|
def child_before2():
|
||||||
|
flask.g.setdefault("seen", []).append("child_2")
|
||||||
|
|
||||||
|
@child.teardown_request
|
||||||
|
def child_teardown2(e=None):
|
||||||
|
assert flask.g.seen.pop() == "child_2"
|
||||||
|
|
||||||
|
@child.context_processor
|
||||||
|
def child_ctx():
|
||||||
|
return dict(key="child")
|
||||||
|
|
||||||
|
@child.route("/a")
|
||||||
|
def a():
|
||||||
|
return ", ".join(flask.g.seen)
|
||||||
|
|
||||||
|
@child.route("/b")
|
||||||
|
def b():
|
||||||
|
return flask.render_template_string("{{ key }}")
|
||||||
|
|
||||||
|
parent.register_blueprint(child)
|
||||||
|
app.register_blueprint(parent)
|
||||||
|
assert (
|
||||||
|
client.get("/a").data == b"app_1, app_2, parent_1, parent_2, child_1, child_2"
|
||||||
|
)
|
||||||
|
assert client.get("/b").data == b"child"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"parent_init, child_init, parent_registration, child_registration",
|
"parent_init, child_init, parent_registration, child_registration",
|
||||||
[
|
[
|
||||||
|
|
@ -874,8 +954,8 @@ def test_unique_blueprint_names(app, client) -> None:
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
with pytest.warns(UserWarning):
|
with pytest.raises(ValueError):
|
||||||
app.register_blueprint(bp) # same bp, same name, warning
|
app.register_blueprint(bp) # same bp, same name, error
|
||||||
|
|
||||||
app.register_blueprint(bp, name="again") # same bp, different name, ok
|
app.register_blueprint(bp, name="again") # same bp, different name, ok
|
||||||
|
|
||||||
|
|
@ -899,6 +979,14 @@ def test_blueprint_renaming(app, client) -> None:
|
||||||
def index():
|
def index():
|
||||||
return flask.request.endpoint
|
return flask.request.endpoint
|
||||||
|
|
||||||
|
@bp.get("/error")
|
||||||
|
def error():
|
||||||
|
flask.abort(403)
|
||||||
|
|
||||||
|
@bp.errorhandler(403)
|
||||||
|
def forbidden(_: Exception):
|
||||||
|
return "Error", 403
|
||||||
|
|
||||||
@bp2.get("/")
|
@bp2.get("/")
|
||||||
def index2():
|
def index2():
|
||||||
return flask.request.endpoint
|
return flask.request.endpoint
|
||||||
|
|
@ -911,3 +999,5 @@ def test_blueprint_renaming(app, client) -> None:
|
||||||
assert client.get("/b/").data == b"alt.index"
|
assert client.get("/b/").data == b"alt.index"
|
||||||
assert client.get("/a/a/").data == b"bp.sub.index2"
|
assert client.get("/a/a/").data == b"bp.sub.index2"
|
||||||
assert client.get("/b/a/").data == b"alt.sub.index2"
|
assert client.get("/b/a/").data == b"alt.sub.index2"
|
||||||
|
assert client.get("/a/error").data == b"Error"
|
||||||
|
assert client.get("/b/error").data == b"Error"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# This file was part of Flask-CLI and was modified under the terms of
|
# This file was part of Flask-CLI and was modified under the terms of
|
||||||
# its Revised BSD License. Copyright © 2015 CERN.
|
# its Revised BSD License. Copyright © 2015 CERN.
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
|
@ -17,6 +18,7 @@ from flask import Blueprint
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.cli import AppGroup
|
from flask.cli import AppGroup
|
||||||
|
from flask.cli import DispatchingApp
|
||||||
from flask.cli import dotenv
|
from flask.cli import dotenv
|
||||||
from flask.cli import find_best_app
|
from flask.cli import find_best_app
|
||||||
from flask.cli import FlaskGroup
|
from flask.cli import FlaskGroup
|
||||||
|
|
@ -47,51 +49,36 @@ def test_cli_name(test_apps):
|
||||||
|
|
||||||
|
|
||||||
def test_find_best_app(test_apps):
|
def test_find_best_app(test_apps):
|
||||||
script_info = ScriptInfo()
|
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
app = Flask("appname")
|
app = Flask("appname")
|
||||||
|
|
||||||
assert find_best_app(script_info, Module) == Module.app
|
assert find_best_app(Module) == Module.app
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
application = Flask("appname")
|
application = Flask("appname")
|
||||||
|
|
||||||
assert find_best_app(script_info, Module) == Module.application
|
assert find_best_app(Module) == Module.application
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
myapp = Flask("appname")
|
myapp = Flask("appname")
|
||||||
|
|
||||||
assert find_best_app(script_info, Module) == Module.myapp
|
assert find_best_app(Module) == Module.myapp
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_app():
|
def create_app():
|
||||||
return Flask("appname")
|
return Flask("appname")
|
||||||
|
|
||||||
app = find_best_app(script_info, Module)
|
app = find_best_app(Module)
|
||||||
assert isinstance(app, Flask)
|
assert isinstance(app, Flask)
|
||||||
assert app.name == "appname"
|
assert app.name == "appname"
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_app(foo):
|
def create_app(**kwargs):
|
||||||
return Flask("appname")
|
return Flask("appname")
|
||||||
|
|
||||||
with pytest.deprecated_call(match="Script info"):
|
app = find_best_app(Module)
|
||||||
app = find_best_app(script_info, Module)
|
|
||||||
|
|
||||||
assert isinstance(app, Flask)
|
|
||||||
assert app.name == "appname"
|
|
||||||
|
|
||||||
class Module:
|
|
||||||
@staticmethod
|
|
||||||
def create_app(foo=None, script_info=None):
|
|
||||||
return Flask("appname")
|
|
||||||
|
|
||||||
with pytest.deprecated_call(match="script_info"):
|
|
||||||
app = find_best_app(script_info, Module)
|
|
||||||
|
|
||||||
assert isinstance(app, Flask)
|
assert isinstance(app, Flask)
|
||||||
assert app.name == "appname"
|
assert app.name == "appname"
|
||||||
|
|
||||||
|
|
@ -100,7 +87,7 @@ def test_find_best_app(test_apps):
|
||||||
def make_app():
|
def make_app():
|
||||||
return Flask("appname")
|
return Flask("appname")
|
||||||
|
|
||||||
app = find_best_app(script_info, Module)
|
app = find_best_app(Module)
|
||||||
assert isinstance(app, Flask)
|
assert isinstance(app, Flask)
|
||||||
assert app.name == "appname"
|
assert app.name == "appname"
|
||||||
|
|
||||||
|
|
@ -111,7 +98,7 @@ def test_find_best_app(test_apps):
|
||||||
def create_app():
|
def create_app():
|
||||||
return Flask("appname2")
|
return Flask("appname2")
|
||||||
|
|
||||||
assert find_best_app(script_info, Module) == Module.myapp
|
assert find_best_app(Module) == Module.myapp
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
myapp = Flask("appname1")
|
myapp = Flask("appname1")
|
||||||
|
|
@ -120,32 +107,32 @@ def test_find_best_app(test_apps):
|
||||||
def create_app():
|
def create_app():
|
||||||
return Flask("appname2")
|
return Flask("appname2")
|
||||||
|
|
||||||
assert find_best_app(script_info, Module) == Module.myapp
|
assert find_best_app(Module) == Module.myapp
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
pytest.raises(NoAppException, find_best_app, Module)
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
myapp1 = Flask("appname1")
|
myapp1 = Flask("appname1")
|
||||||
myapp2 = Flask("appname2")
|
myapp2 = Flask("appname2")
|
||||||
|
|
||||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
pytest.raises(NoAppException, find_best_app, Module)
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_app(foo, bar):
|
def create_app(foo, bar):
|
||||||
return Flask("appname2")
|
return Flask("appname2")
|
||||||
|
|
||||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
pytest.raises(NoAppException, find_best_app, Module)
|
||||||
|
|
||||||
class Module:
|
class Module:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_app():
|
def create_app():
|
||||||
raise TypeError("bad bad factory!")
|
raise TypeError("bad bad factory!")
|
||||||
|
|
||||||
pytest.raises(TypeError, find_best_app, script_info, Module)
|
pytest.raises(TypeError, find_best_app, Module)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -209,8 +196,7 @@ def test_prepare_import(request, value, path, result):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_locate_app(test_apps, iname, aname, result):
|
def test_locate_app(test_apps, iname, aname, result):
|
||||||
info = ScriptInfo()
|
assert locate_app(iname, aname).name == result
|
||||||
assert locate_app(info, iname, aname).name == result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|
@ -232,20 +218,17 @@ def test_locate_app(test_apps, iname, aname, result):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def test_locate_app_raises(test_apps, iname, aname):
|
def test_locate_app_raises(test_apps, iname, aname):
|
||||||
info = ScriptInfo()
|
|
||||||
|
|
||||||
with pytest.raises(NoAppException):
|
with pytest.raises(NoAppException):
|
||||||
locate_app(info, iname, aname)
|
locate_app(iname, aname)
|
||||||
|
|
||||||
|
|
||||||
def test_locate_app_suppress_raise(test_apps):
|
def test_locate_app_suppress_raise(test_apps):
|
||||||
info = ScriptInfo()
|
app = locate_app("notanapp.py", None, raise_if_not_found=False)
|
||||||
app = locate_app(info, "notanapp.py", None, raise_if_not_found=False)
|
|
||||||
assert app is None
|
assert app is None
|
||||||
|
|
||||||
# only direct import error is suppressed
|
# only direct import error is suppressed
|
||||||
with pytest.raises(NoAppException):
|
with pytest.raises(NoAppException):
|
||||||
locate_app(info, "cliapp.importerrorapp", None, raise_if_not_found=False)
|
locate_app("cliapp.importerrorapp", None, raise_if_not_found=False)
|
||||||
|
|
||||||
|
|
||||||
def test_get_version(test_apps, capsys):
|
def test_get_version(test_apps, capsys):
|
||||||
|
|
@ -310,6 +293,26 @@ def test_scriptinfo(test_apps, monkeypatch):
|
||||||
assert app.name == "testapp"
|
assert app.name == "testapp"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="flaky on pypy")
|
||||||
|
def test_lazy_load_error(monkeypatch):
|
||||||
|
"""When using lazy loading, the correct exception should be
|
||||||
|
re-raised.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class BadExc(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def bad_load():
|
||||||
|
raise BadExc
|
||||||
|
|
||||||
|
lazy = DispatchingApp(bad_load, use_eager_loading=False)
|
||||||
|
|
||||||
|
with pytest.raises(BadExc):
|
||||||
|
# reduce flakiness by waiting for the internal loading lock
|
||||||
|
with lazy._lock:
|
||||||
|
lazy._flush_bg_loading_exception()
|
||||||
|
|
||||||
|
|
||||||
def test_with_appcontext(runner):
|
def test_with_appcontext(runner):
|
||||||
@click.command()
|
@click.command()
|
||||||
@with_appcontext
|
@with_appcontext
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import decimal
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
@ -187,6 +188,11 @@ def test_jsonify_uuid_types(app, client):
|
||||||
assert rv_uuid == test_uuid
|
assert rv_uuid == test_uuid
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_decimal():
|
||||||
|
rv = flask.json.dumps(decimal.Decimal("0.003"))
|
||||||
|
assert rv == '"0.003"'
|
||||||
|
|
||||||
|
|
||||||
def test_json_attr(app, client):
|
def test_json_attr(app, client):
|
||||||
@app.route("/add", methods=["POST"])
|
@app.route("/add", methods=["POST"])
|
||||||
def add():
|
def add():
|
||||||
|
|
|
||||||
9
tox.ini
9
tox.ini
|
|
@ -1,7 +1,8 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py{39,38,37,36,py3}
|
py3{11,10,9,8,7},pypy3{8,7}
|
||||||
py39-click7
|
py310-min
|
||||||
|
py37-dev
|
||||||
style
|
style
|
||||||
typing
|
typing
|
||||||
docs
|
docs
|
||||||
|
|
@ -10,8 +11,8 @@ skip_missing_interpreters = true
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
-r requirements/tests.txt
|
-r requirements/tests.txt
|
||||||
|
min: -r requirements/tests-pallets-min.txt
|
||||||
click7: click<8
|
dev: -r requirements/tests-pallets-dev.txt
|
||||||
|
|
||||||
examples/tutorial[test]
|
examples/tutorial[test]
|
||||||
examples/javascript[test]
|
examples/javascript[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue