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`
- 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`
Version 2.0.1

View file

@ -256,7 +256,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>

View file

@ -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

View file

@ -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', {

View file

@ -626,7 +626,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`.

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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
)

View file

@ -389,7 +389,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,

View file

@ -1,3 +1,4 @@
import os
import typing as t
from collections import defaultdict
from functools import update_wrapper
@ -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,

View file

@ -103,18 +103,21 @@ def call_factory(script_info, app_factory, args=None, kwargs=None):
)
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)
if not args and len(sig.parameters) == 1:
first_parameter = next(iter(sig.parameters.values()))
if (
first_parameter.default is inspect.Parameter.empty
# **kwargs is reported as an empty default, ignore it
and first_parameter.kind is not inspect.Parameter.VAR_KEYWORD
):
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)
@ -312,7 +315,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 +331,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):

View file

@ -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.
@ -185,6 +186,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
"""
@ -209,6 +211,7 @@ class Config(dict):
: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.
:return: ``True`` if the file was loaded successfully.
.. deprecated:: 2.0.0
Will be removed in Flask 2.1. Use :meth:`from_file` instead.
@ -232,6 +235,7 @@ class Config(dict):
) -> bool:
"""Updates the config like :meth:`update` ignoring items with non-upper
keys.
:return: Always returns ``True``.
.. versionadded:: 0.11
"""

View file

@ -1,3 +1,4 @@
import decimal
import io
import json as _json
import typing as t
@ -47,7 +48,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)
@ -117,6 +118,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.
@ -324,6 +328,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`.

View file

@ -92,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,
@ -101,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
@ -257,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"\/")

View file

@ -17,6 +17,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
@ -73,6 +74,15 @@ def test_find_best_app(test_apps):
assert isinstance(app, Flask)
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:
@staticmethod
def create_app(foo):
@ -310,6 +320,23 @@ def test_scriptinfo(test_apps, monkeypatch):
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):
@click.command()
@with_appcontext

View file

@ -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():