forked from orbit-oss/flask
Merge remote-tracking branch 'origin/2.0.x'
This commit is contained in:
commit
afc13b9390
17 changed files with 139 additions and 79 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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"\/")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue