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