Merge remote-tracking branch 'origin/2.0.x'

This commit is contained in:
David Lord 2021-08-05 19:48:47 -07:00
commit afc13b9390
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
17 changed files with 139 additions and 79 deletions

View file

@ -21,6 +21,14 @@ Unreleased
- Support View and MethodView instances with async handlers. :issue:`4112` - Support View and MethodView instances with async handlers. :issue:`4112`
- Enhance typing of ``app.errorhandler`` decorator. :issue:`4095` - Enhance typing of ``app.errorhandler`` decorator. :issue:`4095`
- Fix registering a blueprint twice with differing names. :issue:`4124` - 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`
Version 2.0.1 Version 2.0.1

View file

@ -256,7 +256,7 @@ the filter to render data inside ``<script>`` tags.
.. sourcecode:: html+jinja .. sourcecode:: html+jinja
<script type=text/javascript> <script>
const names = {{ names|tosjon }}; const names = {{ names|tosjon }};
renderChart(names, {{ axis_data|tojson }}); renderChart(names, {{ axis_data|tojson }});
</script> </script>

View file

@ -5,7 +5,7 @@ ASGI
If you'd like to use an ASGI server you will need to utilise WSGI to If you'd like to use an ASGI server you will need to utilise WSGI to
ASGI middleware. The asgiref ASGI middleware. The asgiref
[WsgiToAsgi](https://github.com/django/asgiref#wsgi-to-asgi-adapter) `WsgiToAsgi <https://github.com/django/asgiref#wsgi-to-asgi-adapter>`_
adapter is recommended as it integrates with the event loop used for adapter is recommended as it integrates with the event loop used for
Flask's :ref:`async_await` support. You can use the adapter by Flask's :ref:`async_await` support. You can use the adapter by
wrapping the Flask app, wrapping the Flask app,
@ -21,7 +21,7 @@ wrapping the Flask app,
asgi_app = WsgiToAsgi(app) asgi_app = WsgiToAsgi(app)
and then serving the ``asgi_app`` with the asgi server, e.g. using and then serving the ``asgi_app`` with the ASGI server, e.g. using
`Hypercorn <https://gitlab.com/pgjones/hypercorn>`_, `Hypercorn <https://gitlab.com/pgjones/hypercorn>`_,
.. sourcecode:: text .. sourcecode:: text

View file

@ -23,8 +23,7 @@ to add a script statement to the bottom of your ``<body>`` to load jQuery:
.. sourcecode:: html .. sourcecode:: html
<script type=text/javascript src="{{ <script src="{{ url_for('static', filename='jquery.js') }}"></script>
url_for('static', filename='jquery.js') }}"></script>
Another method is using Google's `AJAX Libraries API Another method is using Google's `AJAX Libraries API
<https://developers.google.com/speed/libraries/>`_ to load jQuery: <https://developers.google.com/speed/libraries/>`_ to load jQuery:
@ -59,7 +58,7 @@ like this:
.. sourcecode:: html+jinja .. sourcecode:: html+jinja
<script type=text/javascript> <script>
$SCRIPT_ROOT = {{ request.script_root|tojson }}; $SCRIPT_ROOT = {{ request.script_root|tojson }};
</script> </script>
@ -109,7 +108,7 @@ usually a better idea to have that in a separate script file:
.. sourcecode:: html .. sourcecode:: html
<script type=text/javascript> <script>
$(function() { $(function() {
$('a#calculate').bind('click', function() { $('a#calculate').bind('click', function() {
$.getJSON($SCRIPT_ROOT + '/_add_numbers', { $.getJSON($SCRIPT_ROOT + '/_add_numbers', {

View file

@ -626,7 +626,7 @@ Werkzeug provides for you::
def upload_file(): def upload_file():
if request.method == 'POST': if request.method == 'POST':
file = request.files['the_file'] file = request.files['the_file']
file.save(f"/var/www/uploads/{secure_filename(f.filename)}") file.save(f"/var/www/uploads/{secure_filename(file.filename)}")
... ...
For some better examples, see :doc:`patterns/fileuploads`. For some better examples, see :doc:`patterns/fileuploads`.

View file

@ -270,7 +270,7 @@ messages.
with app.app_context(): with app.app_context():
assert get_db().execute( assert get_db().execute(
"select * from user where username = 'a'", "SELECT * FROM user WHERE username = 'a'",
).fetchone() is not None ).fetchone() is not None

View file

@ -91,18 +91,18 @@ write templates to generate the HTML form.
error = 'Username is required.' error = 'Username is required.'
elif not password: elif not password:
error = 'Password is required.' error = 'Password is required.'
elif db.execute(
'SELECT id FROM user WHERE username = ?', (username,)
).fetchone() is not None:
error = f"User {username} is already registered."
if error is None: if error is None:
db.execute( try:
'INSERT INTO user (username, password) VALUES (?, ?)', db.execute(
(username, generate_password_hash(password)) "INSERT INTO user (username, password) VALUES (?, ?)",
) (username, generate_password_hash(password)),
db.commit() )
return redirect(url_for('auth.login')) db.commit()
except db.IntegrityError:
error = f"User {username} is already registered."
else:
return redirect(url_for("auth.login"))
flash(error) flash(error)
@ -125,26 +125,25 @@ Here's what the ``register`` view function is doing:
#. Validate that ``username`` and ``password`` are not empty. #. Validate that ``username`` and ``password`` are not empty.
#. Validate that ``username`` is not already registered by querying the
database and checking if a result is returned.
:meth:`db.execute <sqlite3.Connection.execute>` takes a SQL query
with ``?`` placeholders for any user input, and a tuple of values
to replace the placeholders with. The database library will take
care of escaping the values so you are not vulnerable to a
*SQL injection attack*.
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
If the query returned no results, it returns ``None``. Later,
:meth:`~sqlite3.Cursor.fetchall` is used, which returns a list of
all results.
#. If validation succeeds, insert the new user data into the database. #. If validation succeeds, insert the new user data into the database.
For security, passwords should never be stored in the database
directly. Instead, - :meth:`db.execute <sqlite3.Connection.execute>` takes a SQL
:func:`~werkzeug.security.generate_password_hash` is used to query with ``?`` placeholders for any user input, and a tuple of
securely hash the password, and that hash is stored. Since this values to replace the placeholders with. The database library
query modifies data, :meth:`db.commit() <sqlite3.Connection.commit>` will take care of escaping the values so you are not vulnerable
needs to be called afterwards to save the changes. to a *SQL injection attack*.
- For security, passwords should never be stored in the database
directly. Instead,
:func:`~werkzeug.security.generate_password_hash` is used to
securely hash the password, and that hash is stored. Since this
query modifies data,
:meth:`db.commit() <sqlite3.Connection.commit>` needs to be
called afterwards to save the changes.
- An :exc:`sqlite3.IntegrityError` will occur if the username
already exists, which should be shown to the user as another
validation error.
#. After storing the user, they are redirected to the login page. #. After storing the user, they are redirected to the login page.
:func:`url_for` generates the URL for the login view based on its :func:`url_for` generates the URL for the login view based on its
@ -200,6 +199,11 @@ There are a few differences from the ``register`` view:
#. The user is queried first and stored in a variable for later use. #. The user is queried first and stored in a variable for later use.
:meth:`~sqlite3.Cursor.fetchone` returns one row from the query.
If the query returned no results, it returns ``None``. Later,
:meth:`~sqlite3.Cursor.fetchall` will be used, which returns a list
of all results.
#. :func:`~werkzeug.security.check_password_hash` hashes the submitted #. :func:`~werkzeug.security.check_password_hash` hashes the submitted
password in the same way as the stored hash and securely compares password in the same way as the stored hash and securely compares
them. If they match, the password is valid. them. If they match, the password is valid.

View file

@ -60,21 +60,21 @@ def register():
error = "Username is required." error = "Username is required."
elif not password: elif not password:
error = "Password is required." error = "Password is required."
elif (
db.execute("SELECT id FROM user WHERE username = ?", (username,)).fetchone()
is not None
):
error = f"User {username} is already registered."
if error is None: if error is None:
# the name is available, store it in the database and go to try:
# the login page db.execute(
db.execute( "INSERT INTO user (username, password) VALUES (?, ?)",
"INSERT INTO user (username, password) VALUES (?, ?)", (username, generate_password_hash(password)),
(username, generate_password_hash(password)), )
) db.commit()
db.commit() except db.IntegrityError:
return redirect(url_for("auth.login")) # The username was already taken, which caused the
# commit to fail. Show a validation error.
error = f"User {username} is already registered."
else:
# Success, go to the login page.
return redirect(url_for("auth.login"))
flash(error) flash(error)

View file

@ -16,7 +16,7 @@ def test_register(client, app):
# test that the user was inserted into the database # test that the user was inserted into the database
with app.app_context(): with app.app_context():
assert ( assert (
get_db().execute("select * from user where username = 'a'").fetchone() get_db().execute("SELECT * FROM user WHERE username = 'a'").fetchone()
is not None is not None
) )

View file

@ -389,7 +389,7 @@ class Flask(Scaffold):
self, self,
import_name: str, import_name: str,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
static_folder: t.Optional[str] = "static", static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
static_host: t.Optional[str] = None, static_host: t.Optional[str] = None,
host_matching: bool = False, host_matching: bool = False,
subdomain_matching: bool = False, subdomain_matching: bool = False,

View file

@ -1,3 +1,4 @@
import os
import typing as t import typing as t
from collections import defaultdict from collections import defaultdict
from functools import update_wrapper from functools import update_wrapper
@ -175,7 +176,7 @@ class Blueprint(Scaffold):
self, self,
name: str, name: str,
import_name: str, import_name: str,
static_folder: t.Optional[str] = None, static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
template_folder: t.Optional[str] = None, template_folder: t.Optional[str] = None,
url_prefix: t.Optional[str] = None, url_prefix: t.Optional[str] = None,

View file

@ -103,18 +103,21 @@ def call_factory(script_info, app_factory, args=None, kwargs=None):
) )
kwargs["script_info"] = script_info kwargs["script_info"] = script_info
if ( if not args and len(sig.parameters) == 1:
not args first_parameter = next(iter(sig.parameters.values()))
and len(sig.parameters) == 1
and next(iter(sig.parameters.values())).default is inspect.Parameter.empty if (
): first_parameter.default is inspect.Parameter.empty
warnings.warn( # **kwargs is reported as an empty default, ignore it
"Script info is deprecated and will not be passed as the" and first_parameter.kind is not inspect.Parameter.VAR_KEYWORD
" single argument to the app factory function in Flask" ):
" 2.1.", warnings.warn(
DeprecationWarning, "Script info is deprecated and will not be passed as the"
) " single argument to the app factory function in Flask"
args.append(script_info) " 2.1.",
DeprecationWarning,
)
args.append(script_info)
return app_factory(*args, **kwargs) return app_factory(*args, **kwargs)
@ -312,7 +315,7 @@ class DispatchingApp:
self.loader = loader self.loader = loader
self._app = None self._app = None
self._lock = Lock() self._lock = Lock()
self._bg_loading_exc_info = None self._bg_loading_exc = None
if use_eager_loading is None: if use_eager_loading is None:
use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true" use_eager_loading = os.environ.get("WERKZEUG_RUN_MAIN") != "true"
@ -328,23 +331,24 @@ class DispatchingApp:
with self._lock: with self._lock:
try: try:
self._load_unlocked() self._load_unlocked()
except Exception: except Exception as e:
self._bg_loading_exc_info = sys.exc_info() self._bg_loading_exc = e
t = Thread(target=_load_app, args=()) t = Thread(target=_load_app, args=())
t.start() t.start()
def _flush_bg_loading_exception(self): def _flush_bg_loading_exception(self):
__traceback_hide__ = True # noqa: F841 __traceback_hide__ = True # noqa: F841
exc_info = self._bg_loading_exc_info exc = self._bg_loading_exc
if exc_info is not None:
self._bg_loading_exc_info = None if exc is not None:
raise exc_info self._bg_loading_exc = None
raise exc
def _load_unlocked(self): def _load_unlocked(self):
__traceback_hide__ = True # noqa: F841 __traceback_hide__ = True # noqa: F841
self._app = rv = self.loader() self._app = rv = self.loader()
self._bg_loading_exc_info = None self._bg_loading_exc = None
return rv return rv
def __call__(self, environ, start_response): def __call__(self, environ, start_response):

View file

@ -83,7 +83,7 @@ class Config(dict):
:param variable_name: name of the environment variable :param variable_name: name of the environment variable
:param silent: set to ``True`` if you want silent failure for missing :param silent: set to ``True`` if you want silent failure for missing
files. files.
:return: bool. ``True`` if able to load config, ``False`` otherwise. :return: ``True`` if the file was loaded successfully.
""" """
rv = os.environ.get(variable_name) rv = os.environ.get(variable_name)
if not rv: if not rv:
@ -107,6 +107,7 @@ class Config(dict):
root path. root path.
:param silent: set to ``True`` if you want silent failure for missing :param silent: set to ``True`` if you want silent failure for missing
files. files.
:return: ``True`` if the file was loaded successfully.
.. versionadded:: 0.7 .. versionadded:: 0.7
`silent` parameter. `silent` parameter.
@ -185,6 +186,7 @@ class Config(dict):
:type load: ``Callable[[Reader], Mapping]`` where ``Reader`` :type load: ``Callable[[Reader], Mapping]`` where ``Reader``
implements a ``read`` method. implements a ``read`` method.
:param silent: Ignore the file if it doesn't exist. :param silent: Ignore the file if it doesn't exist.
:return: ``True`` if the file was loaded successfully.
.. versionadded:: 2.0 .. versionadded:: 2.0
""" """
@ -209,6 +211,7 @@ class Config(dict):
:param filename: The path to the JSON file. This can be an :param filename: The path to the JSON file. This can be an
absolute path or relative to the config root path. absolute path or relative to the config root path.
:param silent: Ignore the file if it doesn't exist. :param silent: Ignore the file if it doesn't exist.
:return: ``True`` if the file was loaded successfully.
.. deprecated:: 2.0.0 .. deprecated:: 2.0.0
Will be removed in Flask 2.1. Use :meth:`from_file` instead. Will be removed in Flask 2.1. Use :meth:`from_file` instead.
@ -232,6 +235,7 @@ class Config(dict):
) -> bool: ) -> bool:
"""Updates the config like :meth:`update` ignoring items with non-upper """Updates the config like :meth:`update` ignoring items with non-upper
keys. keys.
:return: Always returns ``True``.
.. versionadded:: 0.11 .. versionadded:: 0.11
""" """

View file

@ -1,3 +1,4 @@
import decimal
import io import io
import json as _json import json as _json
import typing as t import typing as t
@ -47,7 +48,7 @@ class JSONEncoder(_json.JSONEncoder):
""" """
if isinstance(o, date): if isinstance(o, date):
return http_date(o) return http_date(o)
if isinstance(o, uuid.UUID): if isinstance(o, (decimal.Decimal, uuid.UUID)):
return str(o) return str(o)
if dataclasses and dataclasses.is_dataclass(o): if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o) return dataclasses.asdict(o)
@ -117,6 +118,9 @@ def dumps(obj: t.Any, app: t.Optional["Flask"] = None, **kwargs: t.Any) -> str:
or defaults. or defaults.
:param kwargs: Extra arguments passed to :func:`json.dumps`. :param kwargs: Extra arguments passed to :func:`json.dumps`.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 2.0 .. versionchanged:: 2.0
``encoding`` is deprecated and will be removed in Flask 2.1. ``encoding`` is deprecated and will be removed in Flask 2.1.
@ -324,6 +328,9 @@ def jsonify(*args: t.Any, **kwargs: t.Any) -> "Response":
debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``, debug mode or if :data:`JSONIFY_PRETTYPRINT_REGULAR` is ``True``,
the output will be formatted to be easier to read. the output will be formatted to be easier to read.
.. versionchanged:: 2.0.2
:class:`decimal.Decimal` is supported by converting to a string.
.. versionchanged:: 0.11 .. versionchanged:: 0.11
Added support for serializing top-level arrays. This introduces Added support for serializing top-level arrays. This introduces
a security risk in ancient browsers. See :ref:`security-json`. a security risk in ancient browsers. See :ref:`security-json`.

View file

@ -92,7 +92,7 @@ class Scaffold:
def __init__( def __init__(
self, self,
import_name: str, import_name: str,
static_folder: t.Optional[str] = None, static_folder: t.Optional[t.Union[str, os.PathLike]] = None,
static_url_path: t.Optional[str] = None, static_url_path: t.Optional[str] = None,
template_folder: t.Optional[str] = None, template_folder: t.Optional[str] = None,
root_path: t.Optional[str] = None, root_path: t.Optional[str] = None,
@ -101,7 +101,7 @@ class Scaffold:
#: to. Do not change this once it is set by the constructor. #: to. Do not change this once it is set by the constructor.
self.import_name = import_name self.import_name = import_name
self.static_folder = static_folder self.static_folder = static_folder # type: ignore
self.static_url_path = static_url_path self.static_url_path = static_url_path
#: The path to the templates folder, relative to #: The path to the templates folder, relative to
@ -257,7 +257,7 @@ class Scaffold:
return None return None
@static_folder.setter @static_folder.setter
def static_folder(self, value: t.Optional[str]) -> None: def static_folder(self, value: t.Optional[t.Union[str, os.PathLike]]) -> None:
if value is not None: if value is not None:
value = os.fspath(value).rstrip(r"\/") value = os.fspath(value).rstrip(r"\/")

View file

@ -17,6 +17,7 @@ from flask import Blueprint
from flask import current_app from flask import current_app
from flask import Flask from flask import Flask
from flask.cli import AppGroup from flask.cli import AppGroup
from flask.cli import DispatchingApp
from flask.cli import dotenv from flask.cli import dotenv
from flask.cli import find_best_app from flask.cli import find_best_app
from flask.cli import FlaskGroup from flask.cli import FlaskGroup
@ -73,6 +74,15 @@ def test_find_best_app(test_apps):
assert isinstance(app, Flask) assert isinstance(app, Flask)
assert app.name == "appname" assert app.name == "appname"
class Module:
@staticmethod
def create_app(**kwargs):
return Flask("appname")
app = find_best_app(script_info, Module)
assert isinstance(app, Flask)
assert app.name == "appname"
class Module: class Module:
@staticmethod @staticmethod
def create_app(foo): def create_app(foo):
@ -310,6 +320,23 @@ def test_scriptinfo(test_apps, monkeypatch):
assert app.name == "testapp" assert app.name == "testapp"
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):
lazy._flush_bg_loading_exception()
def test_with_appcontext(runner): def test_with_appcontext(runner):
@click.command() @click.command()
@with_appcontext @with_appcontext

View file

@ -1,4 +1,5 @@
import datetime import datetime
import decimal
import io import io
import uuid import uuid
@ -187,6 +188,11 @@ def test_jsonify_uuid_types(app, client):
assert rv_uuid == test_uuid assert rv_uuid == test_uuid
def test_json_decimal():
rv = flask.json.dumps(decimal.Decimal("0.003"))
assert rv == '"0.003"'
def test_json_attr(app, client): def test_json_attr(app, client):
@app.route("/add", methods=["POST"]) @app.route("/add", methods=["POST"])
def add(): def add():