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
|
||||
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: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
time: "08:00"
|
||||
open-pull-requests-limit: 99
|
||||
interval: "monthly"
|
||||
day: "monday"
|
||||
time: "16:00"
|
||||
timezone: "UTC"
|
||||
|
|
|
|||
6
.github/workflows/lock.yaml
vendored
6
.github/workflows/lock.yaml
vendored
|
|
@ -8,8 +8,8 @@ jobs:
|
|||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: 14
|
||||
pr-lock-inactive-days: 14
|
||||
issue-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
|
||||
matrix:
|
||||
include:
|
||||
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
|
||||
- {name: Windows, python: '3.9', os: windows-latest, tox: py39}
|
||||
- {name: Mac, python: '3.9', os: macos-latest, tox: py39}
|
||||
- {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
|
||||
- {name: Windows, python: '3.10', os: windows-latest, tox: py310}
|
||||
- {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.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
||||
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
|
||||
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
|
||||
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
|
||||
- {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
|
||||
- {name: 'Pallets Minimum Versions', python: '3.10', os: ubuntu-latest, tox: py-min}
|
||||
- {name: 'Pallets Development Versions', python: '3.7', os: ubuntu-latest, tox: py-dev}
|
||||
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
ci:
|
||||
autoupdate_branch: "2.0.x"
|
||||
autoupdate_schedule: monthly
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.15.0
|
||||
rev: v2.29.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py36-plus"]
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.5.0
|
||||
rev: v2.6.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
name: Reorder Python imports (src, tests)
|
||||
files: "^(?!examples/)"
|
||||
args: ["--application-directories", "src"]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.5b1
|
||||
rev: 21.12b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 3.9.2
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-implicit-str-concat
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.4.0
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: fix-byte-order-marker
|
||||
- id: trailing-whitespace
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements/docs.txt
|
||||
|
|
|
|||
66
CHANGES.rst
66
CHANGES.rst
|
|
@ -5,19 +5,79 @@ Version 2.1.0
|
|||
|
||||
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
|
||||
-------------
|
||||
|
||||
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``
|
||||
decorators. :issue:`4104`
|
||||
- Fixed the issue where typing requires template global
|
||||
decorators to accept functions with no arguments. :issue:`4098`
|
||||
- Support View and MethodView instances with async handlers. :issue:`4112`
|
||||
- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
|
||||
- Fix registering a blueprint twice with differing names. :issue:`4124`
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -12,14 +12,16 @@ to address bugs and feature requests in Flask itself. Use one of the
|
|||
following resources for questions about using Flask or issues with your
|
||||
own code:
|
||||
|
||||
- The ``#get-help`` channel on our Discord chat:
|
||||
- The ``#questions`` channel on our Discord chat:
|
||||
https://discord.gg/pallets
|
||||
- The mailing list flask@python.org for long term discussion or larger
|
||||
issues.
|
||||
- Ask on `Stack Overflow`_. Search with Google first using:
|
||||
``site:stackoverflow.com flask {search term, exception message, etc.}``
|
||||
- Ask on our `GitHub Discussions`_.
|
||||
|
||||
.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?tab=Frequent
|
||||
.. _GitHub Discussions: https://github.com/pallets/flask/discussions
|
||||
|
||||
|
||||
Reporting issues
|
||||
|
|
@ -92,7 +94,7 @@ First time setup
|
|||
|
||||
.. code-block:: text
|
||||
|
||||
git remote add fork https://github.com/{username}/flask
|
||||
$ git remote add fork https://github.com/{username}/flask
|
||||
|
||||
- Create a virtualenv.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Install and update using `pip`_:
|
|||
|
||||
$ pip install -U Flask
|
||||
|
||||
.. _pip: https://pip.pypa.io/en/stable/quickstart/
|
||||
.. _pip: https://pip.pypa.io/en/stable/getting-started/
|
||||
|
||||
|
||||
A Simple Example
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ following conditions are met:
|
|||
1. Redistributions of source code must retain the above copyright
|
||||
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
|
||||
this software without specific prior written permission.
|
||||
|
||||
|
|
|
|||
|
|
@ -218,8 +218,6 @@ Useful Functions and Classes
|
|||
|
||||
.. autofunction:: send_from_directory
|
||||
|
||||
.. autofunction:: safe_join
|
||||
|
||||
.. autofunction:: escape
|
||||
|
||||
.. autoclass:: Markup
|
||||
|
|
@ -256,7 +254,7 @@ the filter to render data inside ``<script>`` tags.
|
|||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
<script type=text/javascript>
|
||||
<script>
|
||||
const names = {{ names|tosjon }};
|
||||
renderChart(names, {{ axis_data|tojson }});
|
||||
</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
|
||||
: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
|
||||
is pushed when a request context is pushed.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,24 @@ defined with ``async def`` and use ``await``.
|
|||
data = await async_db_query(...)
|
||||
return jsonify(data)
|
||||
|
||||
Pluggable class-based views also support handlers that are implemented as
|
||||
coroutines. This applies to the :meth:`~flask.views.View.dispatch_request`
|
||||
method in views that inherit from the :class:`flask.views.View` class, as
|
||||
well as all the HTTP method handlers in views that inherit from the
|
||||
:class:`flask.views.MethodView` class.
|
||||
|
||||
.. admonition:: Using ``async`` on Windows on Python 3.8
|
||||
|
||||
Python 3.8 has a bug related to asyncio on Windows. If you encounter
|
||||
something like ``ValueError: set_wakeup_fd only works in main thread``,
|
||||
please upgrade to Python 3.9.
|
||||
|
||||
.. 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
|
||||
-----------
|
||||
|
|
|
|||
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,
|
||||
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
|
||||
------------
|
||||
|
|
@ -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. ::
|
||||
|
||||
$ flask shell
|
||||
Python 3.6.2 (default, Jul 20 2017, 03:52:27)
|
||||
[GCC 7.1.1 20170630] on linux
|
||||
App: example
|
||||
Instance: /home/user/Projects/hello/instance
|
||||
Python 3.10.0 (default, Oct 27 2021, 06:59:51) [GCC 11.1.0] on linux
|
||||
App: example [production]
|
||||
Instance: /home/david/Projects/pallets/flask/instance
|
||||
>>>
|
||||
|
||||
Use :meth:`~Flask.shell_context_processor` to add other automatic imports.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ method::
|
|||
|
||||
app.config.update(
|
||||
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
|
||||
example, copy the output of this to your config::
|
||||
|
||||
$ python -c 'import os; print(os.urandom(16))'
|
||||
b'_5#y2L"F4Q8z\n\xec]/'
|
||||
$ python -c 'import secrets; print(secrets.token_hex())'
|
||||
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||
|
||||
**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::
|
||||
|
||||
# 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
|
||||
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
|
||||
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.
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ ASGI
|
|||
|
||||
If you'd like to use an ASGI server you will need to utilise WSGI to
|
||||
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
|
||||
Flask's :ref:`async_await` support. You can use the adapter by
|
||||
wrapping the Flask app,
|
||||
|
|
@ -21,7 +21,7 @@ wrapping the Flask 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>`_,
|
||||
|
||||
.. sourcecode:: text
|
||||
|
|
|
|||
|
|
@ -234,5 +234,5 @@ python path. Common problems are:
|
|||
|
||||
.. _nginx: https://nginx.org/
|
||||
.. _lighttpd: https://www.lighttpd.net/
|
||||
.. _cherokee: http://cherokee-project.com/
|
||||
.. _cherokee: https://cherokee-project.com/
|
||||
.. _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
|
||||
the file of your flask application (without extension) or the module which
|
||||
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,
|
||||
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/
|
||||
.. _lighttpd: https://www.lighttpd.net/
|
||||
.. _cherokee: http://cherokee-project.com/
|
||||
.. _cherokee: https://cherokee-project.com/
|
||||
.. _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`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork
|
||||
worker model ported from Ruby's Unicorn project. It supports both `eventlet`_
|
||||
and `greenlet`_. Running a Flask application on this server is quite simple::
|
||||
`Gunicorn`_ is a WSGI and HTTP server for UNIX. To run a Flask
|
||||
application, tell Gunicorn how to import your Flask app object.
|
||||
|
||||
$ gunicorn myproject:app
|
||||
.. code-block:: text
|
||||
|
||||
`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``.
|
||||
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 0.0.0.0:5000 your_project:app
|
||||
|
||||
$ 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
|
||||
package and the application instance within the module. If you use the
|
||||
application factory pattern, you can pass a call to that::
|
||||
Gunicorn provides many options for configuring the server, either
|
||||
through a configuration file or with command line options. Use
|
||||
``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/
|
||||
.. _gevent: http://www.gevent.org/
|
||||
.. _eventlet: https://eventlet.net/
|
||||
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
uWSGI
|
||||
--------
|
||||
-----
|
||||
|
||||
`uWSGI`_ is a fast application server written in C. It is very configurable
|
||||
which makes it more complicated to setup than gunicorn.
|
||||
`uWSGI`_ is a fast application server written in C. It is very
|
||||
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 HTTP Router: https://uwsgi-docs.readthedocs.io/en/latest/HTTP.html#the-uwsgi-http-https-router
|
||||
|
||||
|
||||
Gevent
|
||||
-------
|
||||
------
|
||||
|
||||
`Gevent`_ is a coroutine-based Python networking library that uses
|
||||
`greenlet`_ to provide a high-level synchronous API on top of `libev`_
|
||||
event loop::
|
||||
Prefer using `Gunicorn`_ with Gevent workers rather than using Gevent
|
||||
directly. Gunicorn provides a much more configurable and
|
||||
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 yourapplication import app
|
||||
from your_project import app
|
||||
|
||||
http_server = WSGIServer(('', 5000), app)
|
||||
http_server = WSGIServer(("", 5000), app)
|
||||
http_server.serve_forever()
|
||||
|
||||
.. _Gevent: http://www.gevent.org/
|
||||
.. _greenlet: https://greenlet.readthedocs.io/en/latest/
|
||||
.. _libev: http://software.schmorp.de/pkg/libev.html
|
||||
|
||||
Eventlet
|
||||
--------
|
||||
|
||||
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
|
||||
-----------
|
||||
|
|
@ -69,7 +173,9 @@ Twisted Web
|
|||
`Twisted Web`_ is the web server shipped with `Twisted`_, a mature,
|
||||
non-blocking event-driven networking library. Twisted Web comes with a
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
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
|
||||
application from ``myproject``::
|
||||
application from ``myproject``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
$ twistd -n web --port tcp:8080 --wsgi myproject.app
|
||||
|
||||
.. _Twisted: https://twistedmatrix.com/trac/
|
||||
.. _Twisted Web: https://twistedmatrix.com/trac/wiki/TwistedWeb
|
||||
|
||||
|
||||
.. _deploying-proxy-setups:
|
||||
|
||||
Proxy Setups
|
||||
|
|
|
|||
|
|
@ -322,9 +322,9 @@ ecosystem remain consistent and compatible.
|
|||
`Official Pallets Themes`_. A link to the documentation or project
|
||||
website must be in the PyPI metadata or the readme.
|
||||
7. For maximum compatibility, the extension should support the same
|
||||
versions of Python that Flask supports. 3.6+ is recommended as of
|
||||
2020. Use ``python_requires=">= 3.6"`` in ``setup.py`` to indicate
|
||||
supported versions.
|
||||
versions of Python that Flask supports. 3.7+ is recommended as of
|
||||
December 2021. Use ``python_requires=">= 3.7"`` in ``setup.py`` to
|
||||
indicate supported versions.
|
||||
|
||||
.. _PyPI: https://pypi.org/search/?c=Framework+%3A%3A+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
|
||||
3.6 and newer.
|
||||
3.7 and newer.
|
||||
|
||||
|
||||
Dependencies
|
||||
|
|
@ -49,6 +49,18 @@ use them if you install them.
|
|||
.. _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
|
||||
--------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ An example task
|
|||
|
||||
Let's write a task that adds two numbers together and returns the result. We
|
||||
configure Celery's broker and backend to use Redis, create a ``celery``
|
||||
application using the factor from above, and then use it to define the task. ::
|
||||
application using the factory from above, and then use it to define the task. ::
|
||||
|
||||
from flask import Flask
|
||||
|
||||
|
|
|
|||
|
|
@ -175,9 +175,8 @@ are JavaScript libraries like jQuery_ that have form plugins to ease the
|
|||
construction of progress bar.
|
||||
|
||||
Because the common pattern for file uploads exists almost unchanged in all
|
||||
applications dealing with uploads, there is also a Flask extension called
|
||||
`Flask-Uploads`_ that implements a full fledged upload mechanism that
|
||||
allows controlling which file extensions are allowed to be uploaded.
|
||||
applications dealing with uploads, there are also some Flask extensions that
|
||||
implement a full fledged upload mechanism that allows controlling which
|
||||
file extensions are allowed to be uploaded.
|
||||
|
||||
.. _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
|
||||
|
||||
<script type=text/javascript src="{{
|
||||
url_for('static', filename='jquery.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
|
||||
|
||||
Another method is using Google's `AJAX Libraries API
|
||||
<https://developers.google.com/speed/libraries/>`_ to load jQuery:
|
||||
|
|
@ -59,7 +58,7 @@ like this:
|
|||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
<script type=text/javascript>
|
||||
<script>
|
||||
$SCRIPT_ROOT = {{ request.script_root|tojson }};
|
||||
</script>
|
||||
|
||||
|
|
@ -109,7 +108,7 @@ usually a better idea to have that in a separate script file:
|
|||
|
||||
.. sourcecode:: html
|
||||
|
||||
<script type=text/javascript>
|
||||
<script>
|
||||
$(function() {
|
||||
$('a#calculate').bind('click', function() {
|
||||
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
|
||||
|
|
|
|||
|
|
@ -113,16 +113,16 @@ raw cursor and connection objects.
|
|||
Here is how you can use it::
|
||||
|
||||
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::
|
||||
|
||||
user = query_db('select * from users where username = ?',
|
||||
[the_username], one=True)
|
||||
if user is None:
|
||||
print 'No such user'
|
||||
print('No such user')
|
||||
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
|
||||
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
|
||||
<https://pypi.org/project/Flask-WTF/>`_.
|
||||
|
||||
.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/
|
||||
.. _Flask-WTF: https://flask-wtf.readthedocs.io/
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
.. admonition:: Externally Visible Server
|
||||
|
|
@ -444,9 +449,9 @@ Here is an example template:
|
|||
<h1>Hello, World!</h1>
|
||||
{% endif %}
|
||||
|
||||
Inside templates you also have access to the :class:`~flask.request`,
|
||||
:class:`~flask.session` and :class:`~flask.g` [#]_ objects
|
||||
as well as the :func:`~flask.get_flashed_messages` function.
|
||||
Inside templates you also have access to the :data:`~flask.Flask.config`,
|
||||
:class:`~flask.request`, :class:`~flask.session` and :class:`~flask.g` [#]_ objects
|
||||
as well as the :func:`~flask.url_for` and :func:`~flask.get_flashed_messages` functions.
|
||||
|
||||
Templates are especially useful if inheritance is used. If you want to
|
||||
know how that works, see :doc:`patterns/templateinheritance`. Basically
|
||||
|
|
@ -468,7 +473,7 @@ Here is a basic introduction to how the :class:`~markupsafe.Markup` class works:
|
|||
>>> Markup.escape('<blink>hacker</blink>')
|
||||
Markup('<blink>hacker</blink>')
|
||||
>>> Markup('<em>Marked up</em> » HTML').striptags()
|
||||
'Marked up \xbb HTML'
|
||||
'Marked up » HTML'
|
||||
|
||||
.. versionchanged:: 0.5
|
||||
|
||||
|
|
@ -626,7 +631,7 @@ Werkzeug provides for you::
|
|||
def upload_file():
|
||||
if request.method == 'POST':
|
||||
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`.
|
||||
|
|
@ -847,8 +852,8 @@ sessions work::
|
|||
generator. Use the following command to quickly generate a value for
|
||||
:attr:`Flask.secret_key` (or :data:`SECRET_KEY`)::
|
||||
|
||||
$ python -c 'import os; print(os.urandom(16))'
|
||||
b'_5#y2L"F4Q8z\n\xec]/'
|
||||
$ python -c 'import secrets; print(secrets.token_hex())'
|
||||
'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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 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
|
||||
context, which also pushes an :doc:`/appcontext`. When the request ends
|
||||
it pops the request context then the application context.
|
||||
context, which also pushes an :doc:`app context </appcontext>`. When the
|
||||
request ends it pops the request context then the application context.
|
||||
|
||||
The context is unique to each thread (or other worker type).
|
||||
:data:`request` cannot be passed to another thread, the other thread
|
||||
|
|
|
|||
|
|
@ -64,6 +64,54 @@ and using the CLI.
|
|||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
context.
|
||||
|
||||
Generally it's recommended that you read the :doc:`reqcontext`
|
||||
chapter of the documentation first.
|
||||
Generally it's recommended that you read :doc:`reqcontext` first.
|
||||
|
||||
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)
|
||||
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
|
||||
------------
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ by default:
|
|||
.. data:: config
|
||||
:noindex:
|
||||
|
||||
The current configuration object (:data:`flask.config`)
|
||||
The current configuration object (:data:`flask.Flask.config`)
|
||||
|
||||
.. versionadded:: 0.6
|
||||
|
||||
|
|
|
|||
|
|
@ -48,20 +48,21 @@ the application for testing and initializes a new database::
|
|||
import pytest
|
||||
|
||||
from flaskr import create_app
|
||||
from flaskr.db import init_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
|
||||
flaskr.app.config['TESTING'] = True
|
||||
db_fd, db_path = tempfile.mkstemp()
|
||||
app = create_app({'TESTING': True, 'DATABASE': db_path})
|
||||
|
||||
with flaskr.app.test_client() as client:
|
||||
with flaskr.app.app_context():
|
||||
flaskr.init_db()
|
||||
with app.test_client() as client:
|
||||
with app.app_context():
|
||||
init_db()
|
||||
yield client
|
||||
|
||||
os.close(db_fd)
|
||||
os.unlink(flaskr.app.config['DATABASE'])
|
||||
os.unlink(db_path)
|
||||
|
||||
This client fixture will be called by each individual test. It gives us a
|
||||
simple interface to the application, where we can trigger test requests to the
|
||||
|
|
@ -224,13 +225,13 @@ temporarily. With this you can access the :class:`~flask.request`,
|
|||
:class:`~flask.g` and :class:`~flask.session` objects like in view
|
||||
functions. Here is a full example that demonstrates this approach::
|
||||
|
||||
import flask
|
||||
from flask import Flask, request
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context('/?name=Peter'):
|
||||
assert flask.request.path == '/'
|
||||
assert flask.request.args['name'] == 'Peter'
|
||||
assert request.path == '/'
|
||||
assert request.args['name'] == 'Peter'
|
||||
|
||||
All the other objects that are context bound can be used in the same
|
||||
way.
|
||||
|
|
@ -247,7 +248,7 @@ the test request context leaves the ``with`` block. If you do want the
|
|||
:meth:`~flask.Flask.before_request` functions to be called as well, you
|
||||
need to call :meth:`~flask.Flask.preprocess_request` yourself::
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context('/?name=Peter'):
|
||||
app.preprocess_request()
|
||||
|
|
@ -260,7 +261,7 @@ If you want to call the :meth:`~flask.Flask.after_request` functions you
|
|||
need to call into :meth:`~flask.Flask.process_response` which however
|
||||
requires that you pass it a response object::
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_request_context('/?name=Peter'):
|
||||
resp = Response('...')
|
||||
|
|
@ -329,7 +330,7 @@ context around for a little longer so that additional introspection can
|
|||
happen. With Flask 0.4 this is possible by using the
|
||||
:meth:`~flask.Flask.test_client` with a ``with`` block::
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app = Flask(__name__)
|
||||
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/?tequila=42')
|
||||
|
|
@ -353,7 +354,7 @@ keep the context around and access :data:`flask.session`::
|
|||
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/')
|
||||
assert flask.session['foo'] == 42
|
||||
assert session['foo'] == 42
|
||||
|
||||
This however does not make it possible to also modify the session or to
|
||||
access the session before a request was fired. Starting with Flask 0.8 we
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ You can use the following command to output a random secret key:
|
|||
|
||||
.. 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
|
||||
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
|
||||
: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
|
||||
``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
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -57,29 +57,29 @@ By the end, your project layout will look like this:
|
|||
|
||||
/home/user/Projects/flask-tutorial
|
||||
├── flaskr/
|
||||
│ ├── __init__.py
|
||||
│ ├── db.py
|
||||
│ ├── schema.sql
|
||||
│ ├── auth.py
|
||||
│ ├── blog.py
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── login.html
|
||||
│ │ │ └── register.html
|
||||
│ │ └── blog/
|
||||
│ │ ├── create.html
|
||||
│ │ ├── index.html
|
||||
│ │ └── update.html
|
||||
│ └── static/
|
||||
│ └── style.css
|
||||
│ ├── __init__.py
|
||||
│ ├── db.py
|
||||
│ ├── schema.sql
|
||||
│ ├── auth.py
|
||||
│ ├── blog.py
|
||||
│ ├── templates/
|
||||
│ │ ├── base.html
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── login.html
|
||||
│ │ │ └── register.html
|
||||
│ │ └── blog/
|
||||
│ │ ├── create.html
|
||||
│ │ ├── index.html
|
||||
│ │ └── update.html
|
||||
│ └── static/
|
||||
│ └── style.css
|
||||
├── tests/
|
||||
│ ├── conftest.py
|
||||
│ ├── data.sql
|
||||
│ ├── test_factory.py
|
||||
│ ├── test_db.py
|
||||
│ ├── test_auth.py
|
||||
│ └── test_blog.py
|
||||
│ ├── conftest.py
|
||||
│ ├── data.sql
|
||||
│ ├── test_factory.py
|
||||
│ ├── test_db.py
|
||||
│ ├── test_auth.py
|
||||
│ └── test_blog.py
|
||||
├── venv/
|
||||
├── setup.py
|
||||
└── MANIFEST.in
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ messages.
|
|||
|
||||
with app.app_context():
|
||||
assert get_db().execute(
|
||||
"select * from user where username = 'a'",
|
||||
"SELECT * FROM user WHERE username = 'a'",
|
||||
).fetchone() is not None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,18 +91,18 @@ write templates to generate the HTML form.
|
|||
error = 'Username is required.'
|
||||
elif not password:
|
||||
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:
|
||||
db.execute(
|
||||
'INSERT INTO user (username, password) VALUES (?, ?)',
|
||||
(username, generate_password_hash(password))
|
||||
)
|
||||
db.commit()
|
||||
return redirect(url_for('auth.login'))
|
||||
try:
|
||||
db.execute(
|
||||
"INSERT INTO user (username, password) VALUES (?, ?)",
|
||||
(username, generate_password_hash(password)),
|
||||
)
|
||||
db.commit()
|
||||
except db.IntegrityError:
|
||||
error = f"User {username} is already registered."
|
||||
else:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
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`` 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.
|
||||
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.
|
||||
|
||||
- :meth:`db.execute <sqlite3.Connection.execute>` takes a SQL
|
||||
query with ``?`` placeholders for any user input, and a tuple of
|
||||
values to replace the placeholders with. The database library
|
||||
will take care of escaping the values so you are not vulnerable
|
||||
to a *SQL injection attack*.
|
||||
|
||||
- For security, passwords should never be stored in the database
|
||||
directly. Instead,
|
||||
: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.
|
||||
: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.
|
||||
|
||||
: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
|
||||
password in the same way as the stored hash and securely compares
|
||||
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 each HTTP method. With the :class:`flask.views.MethodView` you can
|
||||
easily do that. Each HTTP method maps to a function with the same name
|
||||
(just in lowercase)::
|
||||
easily do that. Each HTTP method maps to a method of the class with the
|
||||
same name (just in lowercase)::
|
||||
|
||||
from flask.views import MethodView
|
||||
|
||||
|
|
|
|||
|
|
@ -60,21 +60,21 @@ def register():
|
|||
error = "Username is required."
|
||||
elif not password:
|
||||
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:
|
||||
# the name is available, store it in the database and go to
|
||||
# the login page
|
||||
db.execute(
|
||||
"INSERT INTO user (username, password) VALUES (?, ?)",
|
||||
(username, generate_password_hash(password)),
|
||||
)
|
||||
db.commit()
|
||||
return redirect(url_for("auth.login"))
|
||||
try:
|
||||
db.execute(
|
||||
"INSERT INTO user (username, password) VALUES (?, ?)",
|
||||
(username, generate_password_hash(password)),
|
||||
)
|
||||
db.commit()
|
||||
except db.IntegrityError:
|
||||
# The username was already taken, which caused the
|
||||
# commit to fail. Show a validation error.
|
||||
error = f"User {username} is already registered."
|
||||
else:
|
||||
# Success, go to the login page.
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
flash(error)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ def test_register(client, app):
|
|||
# test that the user was inserted into the database
|
||||
with app.app_context():
|
||||
assert (
|
||||
get_db().execute("select * from user where username = 'a'").fetchone()
|
||||
get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone()
|
||||
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:
|
||||
#
|
||||
# pip-compile requirements/dev.in
|
||||
#
|
||||
alabaster==0.7.12
|
||||
# via sphinx
|
||||
appdirs==1.4.4
|
||||
# via virtualenv
|
||||
asgiref==3.3.4
|
||||
# via -r tests.in
|
||||
asgiref==3.4.1
|
||||
# via -r requirements/tests.in
|
||||
attrs==21.2.0
|
||||
# via pytest
|
||||
babel==2.9.1
|
||||
# via sphinx
|
||||
backports.entry-points-selectable==1.1.1
|
||||
# via virtualenv
|
||||
blinker==1.4
|
||||
# via -r tests.in
|
||||
certifi==2020.12.5
|
||||
# via -r requirements/tests.in
|
||||
certifi==2021.10.8
|
||||
# via requests
|
||||
cfgv==3.3.0
|
||||
cffi==1.15.0
|
||||
# via cryptography
|
||||
cfgv==3.3.1
|
||||
# via pre-commit
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.9
|
||||
# via requests
|
||||
click==8.0.1
|
||||
click==8.0.3
|
||||
# via pip-tools
|
||||
distlib==0.3.1
|
||||
cryptography==36.0.1
|
||||
# via -r requirements/typing.in
|
||||
distlib==0.3.4
|
||||
# via virtualenv
|
||||
docutils==0.16
|
||||
# via
|
||||
# sphinx
|
||||
# sphinx-tabs
|
||||
filelock==3.0.12
|
||||
filelock==3.4.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
greenlet==1.1.0
|
||||
# via -r tests.in
|
||||
identify==2.2.4
|
||||
greenlet==1.1.2 ; python_version < "3.11"
|
||||
# via -r requirements/tests.in
|
||||
identify==2.4.0
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
idna==3.3
|
||||
# via requests
|
||||
imagesize==1.2.0
|
||||
imagesize==1.3.0
|
||||
# via sphinx
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
jinja2==3.0.1
|
||||
jinja2==3.0.3
|
||||
# via sphinx
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
mypy==0.930
|
||||
# via -r requirements/typing.in
|
||||
mypy-extensions==0.4.3
|
||||
# via mypy
|
||||
mypy==0.812
|
||||
# via -r typing.in
|
||||
nodeenv==1.6.0
|
||||
# via pre-commit
|
||||
packaging==20.9
|
||||
packaging==21.3
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# pytest
|
||||
# sphinx
|
||||
# tox
|
||||
pallets-sphinx-themes==2.0.1
|
||||
# via -r docs.in
|
||||
pep517==0.10.0
|
||||
pallets-sphinx-themes==2.0.2
|
||||
# via -r requirements/docs.in
|
||||
pep517==0.12.0
|
||||
# via pip-tools
|
||||
pip-tools==6.1.0
|
||||
# via -r dev.in
|
||||
pluggy==0.13.1
|
||||
pip-tools==6.4.0
|
||||
# via -r requirements/dev.in
|
||||
platformdirs==2.4.0
|
||||
# via virtualenv
|
||||
pluggy==1.0.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==2.13.0
|
||||
# via -r dev.in
|
||||
py==1.10.0
|
||||
pre-commit==2.16.0
|
||||
# via -r requirements/dev.in
|
||||
py==1.11.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pygments==2.9.0
|
||||
pycparser==2.21
|
||||
# via cffi
|
||||
pygments==2.10.0
|
||||
# via
|
||||
# sphinx
|
||||
# sphinx-tabs
|
||||
pyparsing==2.4.7
|
||||
pyparsing==3.0.6
|
||||
# via packaging
|
||||
pytest==6.2.4
|
||||
# via -r tests.in
|
||||
python-dotenv==0.17.1
|
||||
# via -r tests.in
|
||||
pytz==2021.1
|
||||
pytest==6.2.5
|
||||
# via -r requirements/tests.in
|
||||
python-dotenv==0.19.2
|
||||
# via -r requirements/tests.in
|
||||
pytz==2021.3
|
||||
# via babel
|
||||
pyyaml==5.4.1
|
||||
pyyaml==6.0
|
||||
# via pre-commit
|
||||
requests==2.25.1
|
||||
requests==2.26.0
|
||||
# via sphinx
|
||||
six==1.16.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
snowballstemmer==2.1.0
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx-issues==1.2.0
|
||||
# via -r docs.in
|
||||
sphinx-tabs==3.0.0
|
||||
# via -r docs.in
|
||||
sphinx==4.0.2
|
||||
sphinx==4.3.2
|
||||
# via
|
||||
# -r docs.in
|
||||
# -r requirements/docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinx-tabs
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==1.2.0
|
||||
# via -r requirements/docs.in
|
||||
sphinx-tabs==3.2.0
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==1.0.3
|
||||
sphinxcontrib-htmlhelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.4
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
toml==0.10.2
|
||||
# via
|
||||
# pep517
|
||||
# pre-commit
|
||||
# pytest
|
||||
# tox
|
||||
tox==3.23.1
|
||||
# via -r dev.in
|
||||
typed-ast==1.4.3
|
||||
tomli==2.0.0
|
||||
# via
|
||||
# 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
|
||||
typing-extensions==3.10.0.0
|
||||
# via mypy
|
||||
urllib3==1.26.5
|
||||
urllib3==1.26.7
|
||||
# via requests
|
||||
virtualenv==20.4.6
|
||||
virtualenv==20.10.0
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
wheel==0.37.1
|
||||
# via pip-tools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# 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:
|
||||
#
|
||||
# pip-compile requirements/docs.in
|
||||
|
|
@ -8,66 +8,66 @@ alabaster==0.7.12
|
|||
# via sphinx
|
||||
babel==2.9.1
|
||||
# via sphinx
|
||||
certifi==2020.12.5
|
||||
certifi==2021.10.8
|
||||
# via requests
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.9
|
||||
# via requests
|
||||
docutils==0.16
|
||||
# via
|
||||
# sphinx
|
||||
# sphinx-tabs
|
||||
idna==2.10
|
||||
idna==3.3
|
||||
# via requests
|
||||
imagesize==1.2.0
|
||||
imagesize==1.3.0
|
||||
# via sphinx
|
||||
jinja2==3.0.1
|
||||
jinja2==3.0.3
|
||||
# via sphinx
|
||||
markupsafe==2.0.1
|
||||
# via jinja2
|
||||
packaging==20.9
|
||||
packaging==21.3
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.0.1
|
||||
# via -r docs.in
|
||||
pygments==2.9.0
|
||||
pallets-sphinx-themes==2.0.2
|
||||
# via -r requirements/docs.in
|
||||
pygments==2.10.0
|
||||
# via
|
||||
# sphinx
|
||||
# sphinx-tabs
|
||||
pyparsing==2.4.7
|
||||
pyparsing==3.0.6
|
||||
# via packaging
|
||||
pytz==2021.1
|
||||
pytz==2021.3
|
||||
# via babel
|
||||
requests==2.25.1
|
||||
requests==2.26.0
|
||||
# via sphinx
|
||||
snowballstemmer==2.1.0
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx-issues==1.2.0
|
||||
# via -r docs.in
|
||||
sphinx-tabs==3.0.0
|
||||
# via -r docs.in
|
||||
sphinx==4.0.2
|
||||
sphinx==4.3.2
|
||||
# via
|
||||
# -r docs.in
|
||||
# -r requirements/docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinx-issues
|
||||
# sphinx-tabs
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinx-issues==1.2.0
|
||||
# via -r requirements/docs.in
|
||||
sphinx-tabs==3.2.0
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-applehelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==1.0.3
|
||||
sphinxcontrib-htmlhelp==2.0.0
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
# via -r requirements/docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.4
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
urllib3==1.26.5
|
||||
urllib3==1.26.7
|
||||
# via requests
|
||||
|
||||
# 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
|
||||
asgiref
|
||||
blinker
|
||||
greenlet
|
||||
greenlet ; python_version < "3.11"
|
||||
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:
|
||||
#
|
||||
# pip-compile requirements/tests.in
|
||||
#
|
||||
asgiref==3.3.4
|
||||
asgiref==3.4.1
|
||||
# via -r requirements/tests.in
|
||||
attrs==21.2.0
|
||||
# via pytest
|
||||
blinker==1.4
|
||||
# via -r requirements/tests.in
|
||||
greenlet==1.1.0
|
||||
greenlet==1.1.2 ; python_version < "3.11"
|
||||
# via -r requirements/tests.in
|
||||
iniconfig==1.1.1
|
||||
# via pytest
|
||||
packaging==20.9
|
||||
packaging==21.3
|
||||
# via pytest
|
||||
pluggy==0.13.1
|
||||
pluggy==1.0.0
|
||||
# via pytest
|
||||
py==1.10.0
|
||||
py==1.11.0
|
||||
# via pytest
|
||||
pyparsing==2.4.7
|
||||
pyparsing==3.0.6
|
||||
# via packaging
|
||||
pytest==6.2.4
|
||||
pytest==6.2.5
|
||||
# via -r requirements/tests.in
|
||||
python-dotenv==0.17.1
|
||||
python-dotenv==0.19.2
|
||||
# via -r requirements/tests.in
|
||||
toml==0.10.2
|
||||
# via pytest
|
||||
|
|
|
|||
|
|
@ -1 +1,5 @@
|
|||
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:
|
||||
#
|
||||
# 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
|
||||
# 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
|
||||
typed-ast==1.4.3
|
||||
# via mypy
|
||||
typing-extensions==3.10.0.0
|
||||
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
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ classifiers =
|
|||
packages = find:
|
||||
package_dir = = src
|
||||
include_package_data = true
|
||||
python_requires = >= 3.6
|
||||
python_requires = >= 3.7
|
||||
# Dependencies are in setup.py for GitHub's dependency graph.
|
||||
|
||||
[options.packages.find]
|
||||
|
|
@ -88,7 +88,7 @@ per-file-ignores =
|
|||
|
||||
[mypy]
|
||||
files = src/flask
|
||||
python_version = 3.6
|
||||
python_version = 3.7
|
||||
allow_redefinition = True
|
||||
disallow_subclassing_any = True
|
||||
# disallow_untyped_calls = True
|
||||
|
|
@ -112,3 +112,6 @@ ignore_missing_imports = True
|
|||
|
||||
[mypy-dotenv.*]
|
||||
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_template_attribute as get_template_attribute
|
||||
from .helpers import make_response as make_response
|
||||
from .helpers import safe_join as safe_join
|
||||
from .helpers import send_file as send_file
|
||||
from .helpers import send_from_directory as send_from_directory
|
||||
from .helpers import stream_with_context as stream_with_context
|
||||
|
|
|
|||
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 HTTPException
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from werkzeug.local import ContextVar
|
||||
from werkzeug.routing import BuildError
|
||||
from werkzeug.routing import Map
|
||||
from werkzeug.routing import MapAdapter
|
||||
|
|
@ -58,18 +57,12 @@ from .signals import request_started
|
|||
from .signals import request_tearing_down
|
||||
from .templating import DispatchingJinjaLoader
|
||||
from .templating import Environment
|
||||
from .typing import AfterRequestCallable
|
||||
from .typing import BeforeFirstRequestCallable
|
||||
from .typing import BeforeRequestCallable
|
||||
from .typing import ErrorHandlerCallable
|
||||
from .typing import ResponseReturnValue
|
||||
from .typing import TeardownCallable
|
||||
from .typing import TemplateContextProcessorCallable
|
||||
from .typing import TemplateFilterCallable
|
||||
from .typing import TemplateGlobalCallable
|
||||
from .typing import TemplateTestCallable
|
||||
from .typing import URLDefaultCallable
|
||||
from .typing import URLValuePreprocessorCallable
|
||||
from .wrappers import Request
|
||||
from .wrappers import Response
|
||||
|
||||
|
|
@ -78,6 +71,7 @@ if t.TYPE_CHECKING:
|
|||
from .blueprints import Blueprint
|
||||
from .testing import FlaskClient
|
||||
from .testing import FlaskCliRunner
|
||||
from .typing import ErrorHandlerCallable
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
iscoroutinefunction = inspect.iscoroutinefunction
|
||||
|
|
@ -366,7 +360,8 @@ class Flask(Scaffold):
|
|||
#: .. versionadded:: 1.1.0
|
||||
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
|
||||
test_client_class: t.Optional[t.Type["FlaskClient"]] = None
|
||||
|
|
@ -389,7 +384,7 @@ class Flask(Scaffold):
|
|||
self,
|
||||
import_name: str,
|
||||
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,
|
||||
host_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
|
||||
to add extra variables.
|
||||
"""
|
||||
funcs: t.Iterable[
|
||||
TemplateContextProcessorCallable
|
||||
] = self.template_context_processors[None]
|
||||
reqctx = _request_ctx_stack.top
|
||||
if reqctx is not None:
|
||||
for bp in request.blueprints:
|
||||
if bp in self.template_context_processors:
|
||||
funcs = chain(funcs, self.template_context_processors[bp])
|
||||
names: t.Iterable[t.Optional[str]] = (None,)
|
||||
|
||||
# A template may be rendered outside a request context.
|
||||
if request:
|
||||
names = chain(names, reversed(request.blueprints))
|
||||
|
||||
# The values passed to render_template take precedence. Keep a
|
||||
# copy to re-apply after all context functions.
|
||||
orig_ctx = context.copy()
|
||||
for func in funcs:
|
||||
context.update(func())
|
||||
# make sure the original values win. This makes it possible to
|
||||
# easier add new variables in context processors without breaking
|
||||
# existing views.
|
||||
|
||||
for name in names:
|
||||
if name in self.template_context_processors:
|
||||
for func in self.template_context_processors[name]:
|
||||
context.update(func())
|
||||
|
||||
context.update(orig_ctx)
|
||||
|
||||
def make_shell_context(self) -> dict:
|
||||
|
|
@ -1268,16 +1264,19 @@ class Flask(Scaffold):
|
|||
self.shell_context_processors.append(f)
|
||||
return f
|
||||
|
||||
def _find_error_handler(self, e: Exception) -> t.Optional[ErrorHandlerCallable]:
|
||||
def _find_error_handler(
|
||||
self, e: Exception
|
||||
) -> t.Optional["ErrorHandlerCallable[Exception]"]:
|
||||
"""Return a registered error handler for an exception in this order:
|
||||
blueprint handler for a specific code, app handler for a specific code,
|
||||
blueprint handler for an exception class, app handler for an exception
|
||||
class, or ``None`` if a suitable handler is not found.
|
||||
"""
|
||||
exc_class, code = self._get_exc_class_and_code(type(e))
|
||||
names = (*request.blueprints, None)
|
||||
|
||||
for c in [code, None]:
|
||||
for name in chain(request.blueprints, [None]):
|
||||
for c in (code, None) if code is not None else (None,):
|
||||
for name in names:
|
||||
handler_map = self.error_handler_spec[name][c]
|
||||
|
||||
if not handler_map:
|
||||
|
|
@ -1304,7 +1303,7 @@ class Flask(Scaffold):
|
|||
|
||||
.. versionchanged:: 1.0
|
||||
Exceptions are looked up by code *and* by MRO, so
|
||||
``HTTPExcpetion`` subclasses can be handled with a catch-all
|
||||
``HTTPException`` subclasses can be handled with a catch-all
|
||||
handler for the base ``HTTPException``.
|
||||
|
||||
.. versionadded:: 0.3
|
||||
|
|
@ -1619,14 +1618,7 @@ class Flask(Scaffold):
|
|||
except ImportError:
|
||||
raise RuntimeError(
|
||||
"Install Flask with the 'async' extra in order to use async views."
|
||||
)
|
||||
|
||||
# Check that Werkzeug isn't using its fallback ContextVar class.
|
||||
if ContextVar.__module__ == "werkzeug.local":
|
||||
raise RuntimeError(
|
||||
"Async cannot be used with this combination of Python "
|
||||
"and Greenlet versions."
|
||||
)
|
||||
) from None
|
||||
|
||||
return asgiref_async_to_sync(func)
|
||||
|
||||
|
|
@ -1725,7 +1717,7 @@ class Flask(Scaffold):
|
|||
" response. The return type must be a string,"
|
||||
" dict, tuple, Response instance, or WSGI"
|
||||
f" callable, but it was a {type(rv).__name__}."
|
||||
).with_traceback(sys.exc_info()[2])
|
||||
).with_traceback(sys.exc_info()[2]) from None
|
||||
else:
|
||||
raise TypeError(
|
||||
"The view function did not return a valid"
|
||||
|
|
@ -1797,17 +1789,19 @@ class Flask(Scaffold):
|
|||
|
||||
.. 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:
|
||||
# This is called by url_for, which can be called outside a
|
||||
# request, can't use request.blueprints.
|
||||
bps = _split_blueprint_path(endpoint.rpartition(".")[0])
|
||||
bp_funcs = chain.from_iterable(self.url_default_functions[bp] for bp in bps)
|
||||
funcs = chain(funcs, bp_funcs)
|
||||
names = chain(
|
||||
names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0]))
|
||||
)
|
||||
|
||||
for func in funcs:
|
||||
func(endpoint, values)
|
||||
for name in names:
|
||||
if name in self.url_default_functions:
|
||||
for func in self.url_default_functions[name]:
|
||||
func(endpoint, values)
|
||||
|
||||
def handle_url_build_error(
|
||||
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
|
||||
further request handling is stopped.
|
||||
"""
|
||||
names = (None, *reversed(request.blueprints))
|
||||
|
||||
funcs: t.Iterable[URLValuePreprocessorCallable] = self.url_value_preprocessors[
|
||||
None
|
||||
]
|
||||
for bp in request.blueprints:
|
||||
if bp in self.url_value_preprocessors:
|
||||
funcs = chain(funcs, self.url_value_preprocessors[bp])
|
||||
for func in funcs:
|
||||
func(request.endpoint, request.view_args)
|
||||
for name in names:
|
||||
if name in self.url_value_preprocessors:
|
||||
for url_func in self.url_value_preprocessors[name]:
|
||||
url_func(request.endpoint, request.view_args)
|
||||
|
||||
funcs: t.Iterable[BeforeRequestCallable] = self.before_request_funcs[None]
|
||||
for bp in request.blueprints:
|
||||
if bp in self.before_request_funcs:
|
||||
funcs = chain(funcs, self.before_request_funcs[bp])
|
||||
for func in funcs:
|
||||
rv = self.ensure_sync(func)()
|
||||
if rv is not None:
|
||||
return rv
|
||||
for name in names:
|
||||
if name in self.before_request_funcs:
|
||||
for before_func in self.before_request_funcs[name]:
|
||||
rv = self.ensure_sync(before_func)()
|
||||
|
||||
if rv is not None:
|
||||
return rv
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -1877,16 +1867,18 @@ class Flask(Scaffold):
|
|||
instance of :attr:`response_class`.
|
||||
"""
|
||||
ctx = _request_ctx_stack.top
|
||||
funcs: t.Iterable[AfterRequestCallable] = ctx._after_request_functions
|
||||
for bp in request.blueprints:
|
||||
if bp in self.after_request_funcs:
|
||||
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
|
||||
if None in self.after_request_funcs:
|
||||
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
|
||||
for handler in funcs:
|
||||
response = self.ensure_sync(handler)(response)
|
||||
|
||||
for func in ctx._after_request_functions:
|
||||
response = self.ensure_sync(func)(response)
|
||||
|
||||
for name in chain(request.blueprints, (None,)):
|
||||
if name in self.after_request_funcs:
|
||||
for func in reversed(self.after_request_funcs[name]):
|
||||
response = self.ensure_sync(func)(response)
|
||||
|
||||
if not self.session_interface.is_null_session(ctx.session):
|
||||
self.session_interface.save_session(self, ctx.session, response)
|
||||
|
||||
return response
|
||||
|
||||
def do_teardown_request(
|
||||
|
|
@ -1914,14 +1906,12 @@ class Flask(Scaffold):
|
|||
"""
|
||||
if exc is _sentinel:
|
||||
exc = sys.exc_info()[1]
|
||||
funcs: t.Iterable[TeardownCallable] = reversed(
|
||||
self.teardown_request_funcs[None]
|
||||
)
|
||||
for bp in request.blueprints:
|
||||
if bp in self.teardown_request_funcs:
|
||||
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
|
||||
for func in funcs:
|
||||
self.ensure_sync(func)(exc)
|
||||
|
||||
for name in chain(request.blueprints, (None,)):
|
||||
if name in self.teardown_request_funcs:
|
||||
for func in reversed(self.teardown_request_funcs[name]):
|
||||
self.ensure_sync(func)(exc)
|
||||
|
||||
request_tearing_down.send(self, exc=exc)
|
||||
|
||||
def do_teardown_appcontext(
|
||||
|
|
@ -1943,8 +1933,10 @@ class Flask(Scaffold):
|
|||
"""
|
||||
if exc is _sentinel:
|
||||
exc = sys.exc_info()[1]
|
||||
|
||||
for func in reversed(self.teardown_appcontext_funcs):
|
||||
self.ensure_sync(func)(exc)
|
||||
|
||||
appcontext_tearing_down.send(self, exc=exc)
|
||||
|
||||
def app_context(self) -> AppContext:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import os
|
||||
import typing as t
|
||||
from collections import defaultdict
|
||||
from functools import update_wrapper
|
||||
|
|
@ -8,7 +9,6 @@ from .scaffold import Scaffold
|
|||
from .typing import AfterRequestCallable
|
||||
from .typing import BeforeFirstRequestCallable
|
||||
from .typing import BeforeRequestCallable
|
||||
from .typing import ErrorHandlerCallable
|
||||
from .typing import TeardownCallable
|
||||
from .typing import TemplateContextProcessorCallable
|
||||
from .typing import TemplateFilterCallable
|
||||
|
|
@ -19,6 +19,7 @@ from .typing import URLValuePreprocessorCallable
|
|||
|
||||
if t.TYPE_CHECKING:
|
||||
from .app import Flask
|
||||
from .typing import ErrorHandlerCallable
|
||||
|
||||
DeferredSetupFunction = t.Callable[["BlueprintSetupState"], t.Callable]
|
||||
|
||||
|
|
@ -175,7 +176,7 @@ class Blueprint(Scaffold):
|
|||
self,
|
||||
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,
|
||||
template_folder: 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
|
||||
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", "")
|
||||
self_name = options.get("name", self.name)
|
||||
name = f"{name_prefix}.{self_name}".lstrip(".")
|
||||
|
||||
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 ""
|
||||
|
||||
if app.blueprints[name] is not self:
|
||||
raise ValueError(
|
||||
f"The name '{self_name}' is already registered for"
|
||||
f" a different blueprint{existing_at}. Use 'name='"
|
||||
" to provide a unique name."
|
||||
)
|
||||
else:
|
||||
import warnings
|
||||
raise ValueError(
|
||||
f"The name '{self_name}' is already registered for"
|
||||
f" {bp_desc} blueprint{existing_at}. Use 'name=' to"
|
||||
f" provide a unique name."
|
||||
)
|
||||
|
||||
warnings.warn(
|
||||
f"The name '{self_name}' is already registered for"
|
||||
f" this blueprint{existing_at}. Use 'name=' to"
|
||||
" provide a unique name. This will become an error"
|
||||
" in Flask 2.1.",
|
||||
stacklevel=4,
|
||||
)
|
||||
first_bp_registration = not any(bp is self for bp in app.blueprints.values())
|
||||
first_name_registration = name not in app.blueprints
|
||||
|
||||
app.blueprints[name] = self
|
||||
self._got_registered_once = True
|
||||
state = self.make_setup_state(app, options, first_registration)
|
||||
state = self.make_setup_state(app, options, first_bp_registration)
|
||||
|
||||
if self.has_static_folder:
|
||||
state.add_url_rule(
|
||||
|
|
@ -330,7 +323,7 @@ class Blueprint(Scaffold):
|
|||
)
|
||||
|
||||
# Merge blueprint data into parent.
|
||||
if first_registration:
|
||||
if first_bp_registration or first_name_registration:
|
||||
|
||||
def extend(bp_dict, parent_dict):
|
||||
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.
|
||||
"""
|
||||
|
||||
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
|
||||
def decorator(
|
||||
f: "ErrorHandlerCallable[Exception]",
|
||||
) -> "ErrorHandlerCallable[Exception]":
|
||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||
return f
|
||||
|
||||
|
|
|
|||
107
src/flask/cli.py
107
src/flask/cli.py
|
|
@ -5,7 +5,6 @@ import platform
|
|||
import re
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from functools import update_wrapper
|
||||
from operator import attrgetter
|
||||
from threading import Lock
|
||||
|
|
@ -34,7 +33,7 @@ class NoAppException(click.UsageError):
|
|||
"""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
|
||||
application in the module or raises an exception.
|
||||
"""
|
||||
|
|
@ -65,19 +64,20 @@ def find_best_app(script_info, module):
|
|||
|
||||
if inspect.isfunction(app_factory):
|
||||
try:
|
||||
app = call_factory(script_info, app_factory)
|
||||
app = app_factory()
|
||||
|
||||
if isinstance(app, Flask):
|
||||
return app
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
if not _called_with_wrong_args(app_factory):
|
||||
raise
|
||||
|
||||
raise NoAppException(
|
||||
f"Detected factory {attr_name!r} in module {module.__name__!r},"
|
||||
" but could not call it without arguments. Use"
|
||||
f" \"FLASK_APP='{module.__name__}:{attr_name}(args)'\""
|
||||
" to specify arguments."
|
||||
)
|
||||
) from e
|
||||
|
||||
raise NoAppException(
|
||||
"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):
|
||||
"""Check whether calling a function raised a ``TypeError`` because
|
||||
the call failed or because something in the factory raised the
|
||||
|
|
@ -145,7 +112,7 @@ def _called_with_wrong_args(f):
|
|||
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
|
||||
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:
|
||||
raise NoAppException(
|
||||
f"Failed to parse {app_name!r} as an attribute name or function call."
|
||||
)
|
||||
) from None
|
||||
|
||||
if isinstance(expr, ast.Name):
|
||||
name = expr.id
|
||||
args = kwargs = None
|
||||
args = []
|
||||
kwargs = {}
|
||||
elif isinstance(expr, ast.Call):
|
||||
# Ensure the function name is an attribute name only.
|
||||
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.
|
||||
raise NoAppException(
|
||||
f"Failed to parse arguments as literal values: {app_name!r}."
|
||||
)
|
||||
) from None
|
||||
else:
|
||||
raise NoAppException(
|
||||
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:
|
||||
attr = getattr(module, name)
|
||||
except AttributeError:
|
||||
except AttributeError as e:
|
||||
raise NoAppException(
|
||||
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
|
||||
# to get the real application.
|
||||
if inspect.isfunction(attr):
|
||||
try:
|
||||
app = call_factory(script_info, attr, args, kwargs)
|
||||
except TypeError:
|
||||
app = attr(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if not _called_with_wrong_args(attr):
|
||||
raise
|
||||
|
||||
|
|
@ -207,7 +175,7 @@ def find_app_by_string(script_info, module, app_name):
|
|||
f"The factory {app_name!r} in module"
|
||||
f" {module.__name__!r} could not be called with the"
|
||||
" specified arguments."
|
||||
)
|
||||
) from e
|
||||
else:
|
||||
app = attr
|
||||
|
||||
|
|
@ -249,7 +217,7 @@ def prepare_import(path):
|
|||
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
|
||||
|
||||
try:
|
||||
|
|
@ -261,18 +229,18 @@ def locate_app(script_info, module_name, app_name, raise_if_not_found=True):
|
|||
raise NoAppException(
|
||||
f"While importing {module_name!r}, an ImportError was"
|
||||
f" raised:\n\n{traceback.format_exc()}"
|
||||
)
|
||||
) from None
|
||||
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:
|
||||
return
|
||||
|
||||
module = sys.modules[module_name]
|
||||
|
||||
if app_name is None:
|
||||
return find_best_app(script_info, module)
|
||||
return find_best_app(module)
|
||||
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):
|
||||
|
|
@ -312,7 +280,7 @@ class DispatchingApp:
|
|||
self.loader = loader
|
||||
self._app = None
|
||||
self._lock = Lock()
|
||||
self._bg_loading_exc_info = None
|
||||
self._bg_loading_exc = None
|
||||
|
||||
if use_eager_loading is None:
|
||||
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
|
||||
|
|
@ -328,23 +296,24 @@ class DispatchingApp:
|
|||
with self._lock:
|
||||
try:
|
||||
self._load_unlocked()
|
||||
except Exception:
|
||||
self._bg_loading_exc_info = sys.exc_info()
|
||||
except Exception as e:
|
||||
self._bg_loading_exc = e
|
||||
|
||||
t = Thread(target=_load_app, args=())
|
||||
t.start()
|
||||
|
||||
def _flush_bg_loading_exception(self):
|
||||
__traceback_hide__ = True # noqa: F841
|
||||
exc_info = self._bg_loading_exc_info
|
||||
if exc_info is not None:
|
||||
self._bg_loading_exc_info = None
|
||||
raise exc_info
|
||||
exc = self._bg_loading_exc
|
||||
|
||||
if exc is not None:
|
||||
self._bg_loading_exc = None
|
||||
raise exc
|
||||
|
||||
def _load_unlocked(self):
|
||||
__traceback_hide__ = True # noqa: F841
|
||||
self._app = rv = self.loader()
|
||||
self._bg_loading_exc_info = None
|
||||
self._bg_loading_exc = None
|
||||
return rv
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
|
|
@ -392,18 +361,18 @@ class ScriptInfo:
|
|||
return self._loaded_app
|
||||
|
||||
if self.create_app is not None:
|
||||
app = call_factory(self, self.create_app)
|
||||
app = self.create_app()
|
||||
else:
|
||||
if self.app_import_path:
|
||||
path, name = (
|
||||
re.split(r":(?![\\/])", self.app_import_path, 1) + [None]
|
||||
)[:2]
|
||||
import_name = prepare_import(path)
|
||||
app = locate_app(self, import_name, name)
|
||||
app = locate_app(import_name, name)
|
||||
else:
|
||||
for path in ("wsgi.py", "app.py"):
|
||||
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:
|
||||
break
|
||||
|
|
@ -721,7 +690,7 @@ class CertParamType(click.ParamType):
|
|||
"Using ad-hoc certificates requires the cryptography library.",
|
||||
ctx,
|
||||
param,
|
||||
)
|
||||
) from None
|
||||
|
||||
return value
|
||||
|
||||
|
|
@ -979,15 +948,7 @@ debug mode.
|
|||
|
||||
|
||||
def main() -> None:
|
||||
if int(click.__version__[0]) < 8:
|
||||
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:])
|
||||
cli.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class Config(dict):
|
|||
:param variable_name: name of the environment variable
|
||||
:param silent: set to ``True`` if you want silent failure for missing
|
||||
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)
|
||||
if not rv:
|
||||
|
|
@ -107,6 +107,7 @@ class Config(dict):
|
|||
root path.
|
||||
:param silent: set to ``True`` if you want silent failure for missing
|
||||
files.
|
||||
:return: ``True`` if the file was loaded successfully.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`silent` parameter.
|
||||
|
|
@ -175,6 +176,9 @@ class Config(dict):
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
import json
|
||||
app.config.from_file("config.json", load=json.load)
|
||||
|
||||
import toml
|
||||
app.config.from_file("config.toml", load=toml.load)
|
||||
|
||||
|
|
@ -185,6 +189,7 @@ class Config(dict):
|
|||
:type load: ``Callable[[Reader], Mapping]`` where ``Reader``
|
||||
implements a ``read`` method.
|
||||
:param silent: Ignore the file if it doesn't exist.
|
||||
:return: ``True`` if the file was loaded successfully.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
|
@ -202,36 +207,12 @@ class Config(dict):
|
|||
|
||||
return self.from_mapping(obj)
|
||||
|
||||
def from_json(self, filename: str, silent: bool = False) -> bool:
|
||||
"""Update the values in the config from a JSON file. The loaded
|
||||
data is passed to the :meth:`from_mapping` method.
|
||||
|
||||
:param filename: The path to the JSON file. This can be an
|
||||
absolute path or relative to the config root path.
|
||||
:param silent: Ignore the file if it doesn't exist.
|
||||
|
||||
.. deprecated:: 2.0.0
|
||||
Will be removed in Flask 2.1. Use :meth:`from_file` instead.
|
||||
This was removed early in 2.0.0, was added back in 2.0.1.
|
||||
|
||||
.. versionadded:: 0.11
|
||||
"""
|
||||
import warnings
|
||||
from . import json
|
||||
|
||||
warnings.warn(
|
||||
"'from_json' is deprecated and will be removed in Flask"
|
||||
" 2.1. Use 'from_file(path, json.load)' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.from_file(filename, json.load, silent=silent)
|
||||
|
||||
def from_mapping(
|
||||
self, mapping: t.Optional[t.Mapping[str, t.Any]] = None, **kwargs: t.Any
|
||||
) -> bool:
|
||||
"""Updates the config like :meth:`update` ignoring items with non-upper
|
||||
keys.
|
||||
:return: Always returns ``True``.
|
||||
|
||||
.. versionadded:: 0.11
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -130,7 +130,15 @@ def after_this_request(f: AfterRequestCallable) -> AfterRequestCallable:
|
|||
|
||||
.. 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
|
||||
|
||||
|
||||
|
|
@ -159,17 +167,18 @@ def copy_current_request_context(f: t.Callable) -> t.Callable:
|
|||
.. versionadded:: 0.10
|
||||
"""
|
||||
top = _request_ctx_stack.top
|
||||
|
||||
if top is None:
|
||||
raise RuntimeError(
|
||||
"This decorator can only be used at local scopes "
|
||||
"when a request context is on the stack. For instance within "
|
||||
"view functions."
|
||||
"This decorator can only be used when a request context is"
|
||||
" active, such as within a view function."
|
||||
)
|
||||
|
||||
reqctx = top.copy()
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
with reqctx:
|
||||
return f(*args, **kwargs)
|
||||
return reqctx.app.ensure_sync(f)(*args, **kwargs)
|
||||
|
||||
return update_wrapper(wrapper, f)
|
||||
|
||||
|
|
@ -332,11 +341,29 @@ class RequestContext:
|
|||
self._after_request_functions: t.List[AfterRequestCallable] = []
|
||||
|
||||
@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
|
||||
|
||||
@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
|
||||
|
||||
def copy(self) -> "RequestContext":
|
||||
|
|
|
|||
|
|
@ -83,10 +83,11 @@ def attach_enctype_error_multidict(request):
|
|||
def __getitem__(self, key):
|
||||
try:
|
||||
return oldcls.__getitem__(self, key)
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
if key not in request.form:
|
||||
raise
|
||||
raise DebugFilesKeyError(request, key)
|
||||
|
||||
raise DebugFilesKeyError(request, key) from e
|
||||
|
||||
newcls.__name__ = oldcls.__name__
|
||||
newcls.__module__ = oldcls.__module__
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@ import sys
|
|||
import typing as t
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from functools import lru_cache
|
||||
from functools import update_wrapper
|
||||
from threading import RLock
|
||||
|
||||
import werkzeug.utils
|
||||
from werkzeug.exceptions import NotFound
|
||||
from werkzeug.routing import BuildError
|
||||
from werkzeug.urls import url_quote
|
||||
|
||||
|
|
@ -454,7 +452,7 @@ def _prepare_send_file_kwargs(
|
|||
warnings.warn(
|
||||
"The 'attachment_filename' parameter has been renamed to"
|
||||
" 'download_name'. The old name will be removed in Flask"
|
||||
" 2.1.",
|
||||
" 2.2.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
|
@ -463,7 +461,7 @@ def _prepare_send_file_kwargs(
|
|||
if cache_timeout is not None:
|
||||
warnings.warn(
|
||||
"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,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
|
@ -472,7 +470,7 @@ def _prepare_send_file_kwargs(
|
|||
if add_etags is not None:
|
||||
warnings.warn(
|
||||
"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,
|
||||
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(
|
||||
directory: t.Union[os.PathLike, str],
|
||||
path: t.Union[os.PathLike, str],
|
||||
|
|
@ -691,7 +666,7 @@ def send_from_directory(
|
|||
if filename is not None:
|
||||
warnings.warn(
|
||||
"The 'filename' parameter has been renamed to 'path'. The"
|
||||
" old name will be removed in Flask 2.1.",
|
||||
" old name will be removed in Flask 2.2.",
|
||||
DeprecationWarning,
|
||||
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.
|
||||
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__))
|
||||
|
||||
# Next attempt: check the loader.
|
||||
|
|
@ -785,27 +760,6 @@ class locked_cached_property(werkzeug.utils.cached_property):
|
|||
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:
|
||||
"""Determine if the given string is an IP address.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import io
|
||||
import dataclasses
|
||||
import decimal
|
||||
import json as _json
|
||||
import typing as t
|
||||
import uuid
|
||||
import warnings
|
||||
from datetime import date
|
||||
|
||||
from jinja2.utils import htmlsafe_json_dumps as _jinja_htmlsafe_dumps
|
||||
|
|
@ -15,12 +15,6 @@ if t.TYPE_CHECKING:
|
|||
from ..app import Flask
|
||||
from ..wrappers import Response
|
||||
|
||||
try:
|
||||
import dataclasses
|
||||
except ImportError:
|
||||
# Python < 3.7
|
||||
dataclasses = None # type: ignore
|
||||
|
||||
|
||||
class JSONEncoder(_json.JSONEncoder):
|
||||
"""The default JSON encoder. Handles extra types compared to the
|
||||
|
|
@ -47,7 +41,7 @@ class JSONEncoder(_json.JSONEncoder):
|
|||
"""
|
||||
if isinstance(o, date):
|
||||
return http_date(o)
|
||||
if isinstance(o, uuid.UUID):
|
||||
if isinstance(o, (decimal.Decimal, uuid.UUID)):
|
||||
return str(o)
|
||||
if dataclasses and dataclasses.is_dataclass(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:
|
||||
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("ensure_ascii", app.config["JSON_AS_ASCII"])
|
||||
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:
|
||||
cls = bp.json_decoder
|
||||
|
||||
kwargs.setdefault("cls", cls)
|
||||
else:
|
||||
kwargs.setdefault("cls", JSONDecoder)
|
||||
# Only set a custom decoder if it has custom behavior. This is
|
||||
# faster on PyPy.
|
||||
if cls not in {JSONDecoder, _json.JSONDecoder}:
|
||||
kwargs.setdefault("cls", cls)
|
||||
|
||||
|
||||
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.
|
||||
: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
|
||||
``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.
|
||||
"""
|
||||
_dump_arg_defaults(kwargs, app=app)
|
||||
encoding = kwargs.pop("encoding", None)
|
||||
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
|
||||
return _json.dumps(obj, **kwargs)
|
||||
|
||||
|
||||
def dump(
|
||||
|
|
@ -160,23 +150,6 @@ def dump(
|
|||
deprecated and will be removed in Flask 2.1.
|
||||
"""
|
||||
_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)
|
||||
|
||||
|
||||
|
|
@ -200,19 +173,6 @@ def loads(s: str, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> t.Any:
|
|||
context for configuration.
|
||||
"""
|
||||
_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)
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
"""
|
||||
_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)
|
||||
|
||||
|
||||
|
|
@ -324,6 +270,9 @@ def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
|
|||
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
|
||||
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
|
||||
Added support for serializing top-level arrays. This introduces
|
||||
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 AppOrBlueprintKey
|
||||
from .typing import BeforeRequestCallable
|
||||
from .typing import ErrorHandlerCallable
|
||||
from .typing import GenericException
|
||||
from .typing import TeardownCallable
|
||||
from .typing import TemplateContextProcessorCallable
|
||||
from .typing import URLDefaultCallable
|
||||
|
|
@ -29,6 +29,7 @@ from .typing import URLValuePreprocessorCallable
|
|||
|
||||
if t.TYPE_CHECKING:
|
||||
from .wrappers import Response
|
||||
from .typing import ErrorHandlerCallable
|
||||
|
||||
# a singleton sentinel value for parameter defaults
|
||||
_sentinel = object()
|
||||
|
|
@ -91,7 +92,7 @@ class Scaffold:
|
|||
def __init__(
|
||||
self,
|
||||
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,
|
||||
template_folder: 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.
|
||||
self.import_name = import_name
|
||||
|
||||
self.static_folder = static_folder
|
||||
self.static_folder = static_folder # type: ignore
|
||||
self.static_url_path = static_url_path
|
||||
|
||||
#: The path to the templates folder, relative to
|
||||
|
|
@ -144,7 +145,10 @@ class Scaffold:
|
|||
#: directly and its format may change at any time.
|
||||
self.error_handler_spec: t.Dict[
|
||||
AppOrBlueprintKey,
|
||||
t.Dict[t.Optional[int], t.Dict[t.Type[Exception], ErrorHandlerCallable]],
|
||||
t.Dict[
|
||||
t.Optional[int],
|
||||
t.Dict[t.Type[Exception], "ErrorHandlerCallable[Exception]"],
|
||||
],
|
||||
] = defaultdict(lambda: defaultdict(dict))
|
||||
|
||||
#: A data structure of functions to call at the beginning of
|
||||
|
|
@ -253,7 +257,7 @@ class Scaffold:
|
|||
return None
|
||||
|
||||
@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:
|
||||
value = os.fspath(value).rstrip(r"\/")
|
||||
|
||||
|
|
@ -585,10 +589,10 @@ class Scaffold:
|
|||
stack of active contexts. This becomes relevant if you are using
|
||||
such constructs in tests.
|
||||
|
||||
Teardown functions must avoid raising exceptions, since they . If they
|
||||
execute code that might fail they
|
||||
will have to surround the execution of these code by try/except
|
||||
statements and log occurring errors.
|
||||
Teardown functions must avoid raising exceptions. If
|
||||
they execute code that might fail they
|
||||
will have to surround the execution of that code with try/except
|
||||
statements and log any errors.
|
||||
|
||||
When a teardown function was called because of an exception it will
|
||||
be passed an error object.
|
||||
|
|
@ -643,8 +647,11 @@ class Scaffold:
|
|||
|
||||
@setupmethod
|
||||
def errorhandler(
|
||||
self, code_or_exception: t.Union[t.Type[Exception], int]
|
||||
) -> t.Callable[[ErrorHandlerCallable], ErrorHandlerCallable]:
|
||||
self, code_or_exception: t.Union[t.Type[GenericException], int]
|
||||
) -> t.Callable[
|
||||
["ErrorHandlerCallable[GenericException]"],
|
||||
"ErrorHandlerCallable[GenericException]",
|
||||
]:
|
||||
"""Register a function to handle errors by code or exception class.
|
||||
|
||||
A decorator that is used to register a function given an
|
||||
|
|
@ -674,7 +681,9 @@ class Scaffold:
|
|||
an arbitrary exception
|
||||
"""
|
||||
|
||||
def decorator(f: ErrorHandlerCallable) -> ErrorHandlerCallable:
|
||||
def decorator(
|
||||
f: "ErrorHandlerCallable[GenericException]",
|
||||
) -> "ErrorHandlerCallable[GenericException]":
|
||||
self.register_error_handler(code_or_exception, f)
|
||||
return f
|
||||
|
||||
|
|
@ -683,8 +692,8 @@ class Scaffold:
|
|||
@setupmethod
|
||||
def register_error_handler(
|
||||
self,
|
||||
code_or_exception: t.Union[t.Type[Exception], int],
|
||||
f: ErrorHandlerCallable,
|
||||
code_or_exception: t.Union[t.Type[GenericException], int],
|
||||
f: "ErrorHandlerCallable[GenericException]",
|
||||
) -> None:
|
||||
"""Alternative error attach function to the :meth:`errorhandler`
|
||||
decorator that is more straightforward to use for non decorator
|
||||
|
|
@ -706,9 +715,11 @@ class Scaffold:
|
|||
f"'{code_or_exception}' is not a recognized HTTP error"
|
||||
" code. Use a subclass of HTTPException with that code"
|
||||
" instead."
|
||||
)
|
||||
) from None
|
||||
|
||||
self.error_handler_spec[None][code][exc_class] = f
|
||||
self.error_handler_spec[None][code][exc_class] = t.cast(
|
||||
"ErrorHandlerCallable[Exception]", f
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_exc_class_and_code(
|
||||
|
|
|
|||
|
|
@ -131,6 +131,13 @@ class SessionInterface:
|
|||
app = Flask(__name__)
|
||||
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
|
||||
"""
|
||||
|
||||
|
|
@ -292,20 +299,25 @@ class SessionInterface:
|
|||
def open_session(
|
||||
self, app: "Flask", request: "Request"
|
||||
) -> t.Optional[SessionMixin]:
|
||||
"""This method has to be implemented and must either return ``None``
|
||||
in case the loading failed because of a configuration error or an
|
||||
instance of a session object which implements a dictionary like
|
||||
interface + the methods and attributes on :class:`SessionMixin`.
|
||||
"""This is called at the beginning of each request, after
|
||||
pushing the request context, before matching the URL.
|
||||
|
||||
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()
|
||||
|
||||
def save_session(
|
||||
self, app: "Flask", session: SessionMixin, response: "Response"
|
||||
) -> None:
|
||||
"""This is called for actual sessions returned by :meth:`open_session`
|
||||
at the end of the request. This is still called during a request
|
||||
context so if you absolutely need access to the request you can do
|
||||
that.
|
||||
"""This is called at the end of each request, after generating
|
||||
a response, before removing the request context. It is skipped
|
||||
if :meth:`is_null_session` returns ``True``.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ except ImportError:
|
|||
raise RuntimeError(
|
||||
"Signalling support is unavailable because the blinker"
|
||||
" library is not installed."
|
||||
)
|
||||
) from None
|
||||
|
||||
connect = connect_via = connected_to = temporarily_connected_to = _fail
|
||||
disconnect = _fail
|
||||
|
|
|
|||
|
|
@ -9,14 +9,15 @@ from werkzeug.test import Client
|
|||
from werkzeug.urls import url_parse
|
||||
from werkzeug.wrappers import Request as BaseRequest
|
||||
|
||||
from . import _request_ctx_stack
|
||||
from .cli import ScriptInfo
|
||||
from .globals import _request_ctx_stack
|
||||
from .json import dumps as json_dumps
|
||||
from .sessions import SessionMixin
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from werkzeug.test import TestResponse
|
||||
|
||||
from .app import Flask
|
||||
from .wrappers import Response
|
||||
|
||||
|
||||
class EnvironBuilder(werkzeug.test.EnvironBuilder):
|
||||
|
|
@ -171,14 +172,15 @@ class FlaskClient(Client):
|
|||
headers = resp.get_wsgi_headers(c.request.environ)
|
||||
self.cookie_jar.extract_wsgi(c.request.environ, headers)
|
||||
|
||||
def open( # type: ignore
|
||||
def open(
|
||||
self,
|
||||
*args: t.Any,
|
||||
as_tuple: bool = False,
|
||||
buffered: bool = False,
|
||||
follow_redirects: bool = False,
|
||||
**kwargs: t.Any,
|
||||
) -> "Response":
|
||||
) -> "TestResponse":
|
||||
as_tuple = kwargs.pop("as_tuple", None)
|
||||
|
||||
# Same logic as super.open, but apply environ_base and preserve_context.
|
||||
request = None
|
||||
|
||||
|
|
@ -213,12 +215,28 @@ class FlaskClient(Client):
|
|||
finally:
|
||||
builder.close()
|
||||
|
||||
return super().open( # type: ignore
|
||||
request,
|
||||
as_tuple=as_tuple,
|
||||
buffered=buffered,
|
||||
follow_redirects=follow_redirects,
|
||||
)
|
||||
if as_tuple is not None:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"'as_tuple' is deprecated and will be removed in"
|
||||
" Werkzeug 2.1 and Flask 2.1. Use"
|
||||
" 'response.request.environ' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return super().open(
|
||||
request,
|
||||
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":
|
||||
if self.preserve_context:
|
||||
|
|
@ -272,7 +290,7 @@ class FlaskCliRunner(CliRunner):
|
|||
:return: a :class:`~click.testing.Result` object.
|
||||
"""
|
||||
if cli is None:
|
||||
cli = self.app.cli
|
||||
cli = self.app.cli # type: ignore
|
||||
|
||||
if "obj" not in kwargs:
|
||||
kwargs["obj"] = ScriptInfo(create_app=lambda: self.app)
|
||||
|
|
|
|||
|
|
@ -33,11 +33,12 @@ ResponseReturnValue = t.Union[
|
|||
"WSGIApplication",
|
||||
]
|
||||
|
||||
GenericException = t.TypeVar("GenericException", bound=Exception, contravariant=True)
|
||||
|
||||
AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named
|
||||
AfterRequestCallable = t.Callable[["Response"], "Response"]
|
||||
BeforeFirstRequestCallable = t.Callable[[], None]
|
||||
BeforeRequestCallable = t.Callable[[], t.Optional[ResponseReturnValue]]
|
||||
ErrorHandlerCallable = t.Callable[[Exception], ResponseReturnValue]
|
||||
TeardownCallable = t.Callable[[t.Optional[BaseException]], None]
|
||||
TemplateContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]]
|
||||
TemplateFilterCallable = t.Callable[..., t.Any]
|
||||
|
|
@ -45,3 +46,4 @@ TemplateGlobalCallable = t.Callable[..., t.Any]
|
|||
TemplateTestCallable = t.Callable[..., bool]
|
||||
URLDefaultCallable = t.Callable[[str, dict], None]
|
||||
URLValuePreprocessorCallable = t.Callable[[t.Optional[str], t.Optional[dict]], None]
|
||||
ErrorHandlerCallable = t.Callable[[GenericException], ResponseReturnValue]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import typing as t
|
||||
|
||||
from .globals import current_app
|
||||
from .globals import request
|
||||
from .typing import ResponseReturnValue
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ class View:
|
|||
|
||||
def view(*args: t.Any, **kwargs: t.Any) -> ResponseReturnValue:
|
||||
self = view.view_class(*class_args, **class_kwargs) # type: ignore
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
|
||||
|
||||
if cls.decorators:
|
||||
view.__name__ = name
|
||||
|
|
@ -154,4 +155,4 @@ class MethodView(View, metaclass=MethodViewType):
|
|||
meth = getattr(self, "get", None)
|
||||
|
||||
assert meth is not None, f"Unimplemented method {request.method!r}"
|
||||
return meth(*args, **kwargs)
|
||||
return current_app.ensure_sync(meth)(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import asyncio
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import Flask
|
||||
from flask import request
|
||||
from flask.views import MethodView
|
||||
from flask.views import View
|
||||
|
||||
pytest.importorskip("asgiref")
|
||||
|
||||
|
|
@ -18,6 +19,24 @@ class BlueprintError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class AsyncView(View):
|
||||
methods = ["GET", "POST"]
|
||||
|
||||
async def dispatch_request(self):
|
||||
await asyncio.sleep(0)
|
||||
return request.method
|
||||
|
||||
|
||||
class AsyncMethodView(MethodView):
|
||||
async def get(self):
|
||||
await asyncio.sleep(0)
|
||||
return "GET"
|
||||
|
||||
async def post(self):
|
||||
await asyncio.sleep(0)
|
||||
return "POST"
|
||||
|
||||
|
||||
@pytest.fixture(name="async_app")
|
||||
def _async_app():
|
||||
app = Flask(__name__)
|
||||
|
|
@ -53,11 +72,13 @@ def _async_app():
|
|||
|
||||
app.register_blueprint(blueprint, url_prefix="/bp")
|
||||
|
||||
app.add_url_rule("/view", view_func=AsyncView.as_view("view"))
|
||||
app.add_url_rule("/methodview", view_func=AsyncMethodView.as_view("methodview"))
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
||||
@pytest.mark.parametrize("path", ["/", "/home", "/bp/"])
|
||||
@pytest.mark.parametrize("path", ["/", "/home", "/bp/", "/view", "/methodview"])
|
||||
def test_async_route(path, async_app):
|
||||
test_client = async_app.test_client()
|
||||
response = test_client.get(path)
|
||||
|
|
@ -66,7 +87,6 @@ def test_async_route(path, async_app):
|
|||
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"])
|
||||
def test_async_error_handler(path, async_app):
|
||||
test_client = async_app.test_client()
|
||||
|
|
@ -74,7 +94,6 @@ def test_async_error_handler(path, async_app):
|
|||
assert response.status_code == 412
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires Python >= 3.7")
|
||||
def test_async_before_after_request():
|
||||
app_first_called = False
|
||||
app_before_called = False
|
||||
|
|
@ -131,10 +150,3 @@ def test_async_before_after_request():
|
|||
test_client.get("/bp/")
|
||||
assert bp_before_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 re
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import weakref
|
||||
|
|
@ -1323,7 +1322,6 @@ def test_jsonify_mimetype(app, req_ctx):
|
|||
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):
|
||||
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"
|
||||
|
||||
|
||||
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(
|
||||
"parent_init, child_init, parent_registration, child_registration",
|
||||
[
|
||||
|
|
@ -874,8 +954,8 @@ def test_unique_blueprint_names(app, client) -> None:
|
|||
|
||||
app.register_blueprint(bp)
|
||||
|
||||
with pytest.warns(UserWarning):
|
||||
app.register_blueprint(bp) # same bp, same name, warning
|
||||
with pytest.raises(ValueError):
|
||||
app.register_blueprint(bp) # same bp, same name, error
|
||||
|
||||
app.register_blueprint(bp, name="again") # same bp, different name, ok
|
||||
|
||||
|
|
@ -899,6 +979,14 @@ def test_blueprint_renaming(app, client) -> None:
|
|||
def index():
|
||||
return flask.request.endpoint
|
||||
|
||||
@bp.get("/error")
|
||||
def error():
|
||||
flask.abort(403)
|
||||
|
||||
@bp.errorhandler(403)
|
||||
def forbidden(_: Exception):
|
||||
return "Error", 403
|
||||
|
||||
@bp2.get("/")
|
||||
def index2():
|
||||
return flask.request.endpoint
|
||||
|
|
@ -911,3 +999,5 @@ def test_blueprint_renaming(app, client) -> None:
|
|||
assert client.get("/b/").data == b"alt.index"
|
||||
assert client.get("/a/a/").data == b"bp.sub.index2"
|
||||
assert client.get("/b/a/").data == b"alt.sub.index2"
|
||||
assert client.get("/a/error").data == b"Error"
|
||||
assert client.get("/b/error").data == b"Error"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# This file was part of Flask-CLI and was modified under the terms of
|
||||
# its Revised BSD License. Copyright © 2015 CERN.
|
||||
import os
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
import types
|
||||
|
|
@ -17,6 +18,7 @@ from flask import Blueprint
|
|||
from flask import current_app
|
||||
from flask import Flask
|
||||
from flask.cli import AppGroup
|
||||
from flask.cli import DispatchingApp
|
||||
from flask.cli import dotenv
|
||||
from flask.cli import find_best_app
|
||||
from flask.cli import FlaskGroup
|
||||
|
|
@ -47,51 +49,36 @@ def test_cli_name(test_apps):
|
|||
|
||||
|
||||
def test_find_best_app(test_apps):
|
||||
script_info = ScriptInfo()
|
||||
|
||||
class Module:
|
||||
app = Flask("appname")
|
||||
|
||||
assert find_best_app(script_info, Module) == Module.app
|
||||
assert find_best_app(Module) == Module.app
|
||||
|
||||
class Module:
|
||||
application = Flask("appname")
|
||||
|
||||
assert find_best_app(script_info, Module) == Module.application
|
||||
assert find_best_app(Module) == Module.application
|
||||
|
||||
class Module:
|
||||
myapp = Flask("appname")
|
||||
|
||||
assert find_best_app(script_info, Module) == Module.myapp
|
||||
assert find_best_app(Module) == Module.myapp
|
||||
|
||||
class Module:
|
||||
@staticmethod
|
||||
def create_app():
|
||||
return Flask("appname")
|
||||
|
||||
app = find_best_app(script_info, Module)
|
||||
app = find_best_app(Module)
|
||||
assert isinstance(app, Flask)
|
||||
assert app.name == "appname"
|
||||
|
||||
class Module:
|
||||
@staticmethod
|
||||
def create_app(foo):
|
||||
def create_app(**kwargs):
|
||||
return Flask("appname")
|
||||
|
||||
with pytest.deprecated_call(match="Script info"):
|
||||
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)
|
||||
|
||||
app = find_best_app(Module)
|
||||
assert isinstance(app, Flask)
|
||||
assert app.name == "appname"
|
||||
|
||||
|
|
@ -100,7 +87,7 @@ def test_find_best_app(test_apps):
|
|||
def make_app():
|
||||
return Flask("appname")
|
||||
|
||||
app = find_best_app(script_info, Module)
|
||||
app = find_best_app(Module)
|
||||
assert isinstance(app, Flask)
|
||||
assert app.name == "appname"
|
||||
|
||||
|
|
@ -111,7 +98,7 @@ def test_find_best_app(test_apps):
|
|||
def create_app():
|
||||
return Flask("appname2")
|
||||
|
||||
assert find_best_app(script_info, Module) == Module.myapp
|
||||
assert find_best_app(Module) == Module.myapp
|
||||
|
||||
class Module:
|
||||
myapp = Flask("appname1")
|
||||
|
|
@ -120,32 +107,32 @@ def test_find_best_app(test_apps):
|
|||
def create_app():
|
||||
return Flask("appname2")
|
||||
|
||||
assert find_best_app(script_info, Module) == Module.myapp
|
||||
assert find_best_app(Module) == Module.myapp
|
||||
|
||||
class Module:
|
||||
pass
|
||||
|
||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
||||
pytest.raises(NoAppException, find_best_app, Module)
|
||||
|
||||
class Module:
|
||||
myapp1 = Flask("appname1")
|
||||
myapp2 = Flask("appname2")
|
||||
|
||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
||||
pytest.raises(NoAppException, find_best_app, Module)
|
||||
|
||||
class Module:
|
||||
@staticmethod
|
||||
def create_app(foo, bar):
|
||||
return Flask("appname2")
|
||||
|
||||
pytest.raises(NoAppException, find_best_app, script_info, Module)
|
||||
pytest.raises(NoAppException, find_best_app, Module)
|
||||
|
||||
class Module:
|
||||
@staticmethod
|
||||
def create_app():
|
||||
raise TypeError("bad bad factory!")
|
||||
|
||||
pytest.raises(TypeError, find_best_app, script_info, Module)
|
||||
pytest.raises(TypeError, find_best_app, Module)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -209,8 +196,7 @@ def test_prepare_import(request, value, path, result):
|
|||
),
|
||||
)
|
||||
def test_locate_app(test_apps, iname, aname, result):
|
||||
info = ScriptInfo()
|
||||
assert locate_app(info, iname, aname).name == result
|
||||
assert locate_app(iname, aname).name == result
|
||||
|
||||
|
||||
@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):
|
||||
info = ScriptInfo()
|
||||
|
||||
with pytest.raises(NoAppException):
|
||||
locate_app(info, iname, aname)
|
||||
locate_app(iname, aname)
|
||||
|
||||
|
||||
def test_locate_app_suppress_raise(test_apps):
|
||||
info = ScriptInfo()
|
||||
app = locate_app(info, "notanapp.py", None, raise_if_not_found=False)
|
||||
app = locate_app("notanapp.py", None, raise_if_not_found=False)
|
||||
assert app is None
|
||||
|
||||
# only direct import error is suppressed
|
||||
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):
|
||||
|
|
@ -310,6 +293,26 @@ def test_scriptinfo(test_apps, monkeypatch):
|
|||
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):
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
import decimal
|
||||
import io
|
||||
import uuid
|
||||
|
||||
|
|
@ -187,6 +188,11 @@ def test_jsonify_uuid_types(app, client):
|
|||
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):
|
||||
@app.route("/add", methods=["POST"])
|
||||
def add():
|
||||
|
|
|
|||
9
tox.ini
9
tox.ini
|
|
@ -1,7 +1,8 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py{39,38,37,36,py3}
|
||||
py39-click7
|
||||
py3{11,10,9,8,7},pypy3{8,7}
|
||||
py310-min
|
||||
py37-dev
|
||||
style
|
||||
typing
|
||||
docs
|
||||
|
|
@ -10,8 +11,8 @@ skip_missing_interpreters = true
|
|||
[testenv]
|
||||
deps =
|
||||
-r requirements/tests.txt
|
||||
|
||||
click7: click<8
|
||||
min: -r requirements/tests-pallets-min.txt
|
||||
dev: -r requirements/tests-pallets-dev.txt
|
||||
|
||||
examples/tutorial[test]
|
||||
examples/javascript[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue