From ca2bfbb0ac66fbbeeedbf6bbe317bee45cb23d29 Mon Sep 17 00:00:00 2001 From: Grey Li Date: Sat, 2 Jul 2022 10:39:18 +0800 Subject: [PATCH 1/3] Support returning list as JSON --- src/flask/app.py | 10 +++++++--- src/flask/typing.py | 2 +- tests/test_basic.py | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/flask/app.py b/src/flask/app.py index 5a8223e5..f9db3b00 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1830,6 +1830,9 @@ class Flask(Scaffold): ``dict`` A dictionary that will be jsonify'd before being returned. + ``list`` + A list that will be jsonify'd before being returned. + ``generator`` or ``iterator`` A generator that returns ``str`` or ``bytes`` to be streamed as the response. @@ -1855,6 +1858,7 @@ class Flask(Scaffold): .. versionchanged:: 2.2 A generator will be converted to a streaming response. + A list will be converted to a JSON response. .. versionchanged:: 1.1 A dict will be converted to a JSON response. @@ -1907,7 +1911,7 @@ class Flask(Scaffold): headers=headers, # type: ignore[arg-type] ) status = headers = None - elif isinstance(rv, dict): + elif isinstance(rv, (dict, list)): rv = jsonify(rv) elif isinstance(rv, BaseResponse) or callable(rv): # evaluate a WSGI callable, or coerce a different response @@ -1920,14 +1924,14 @@ class Flask(Scaffold): raise TypeError( f"{e}\nThe view function did not return a valid" " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" + " dict, list, tuple, Response instance, or WSGI" f" callable, but it was a {type(rv).__name__}." ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" " response. The return type must be a string," - " dict, tuple, Response instance, or WSGI" + " dict, list, tuple, Response instance, or WSGI" f" callable, but it was a {type(rv).__name__}." ) diff --git a/src/flask/typing.py b/src/flask/typing.py index 4fb96545..30c9398c 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -7,7 +7,7 @@ if t.TYPE_CHECKING: # pragma: no cover # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ - "Response", str, bytes, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] + "Response", str, bytes, list, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] ] # the possible types for an individual HTTP header diff --git a/tests/test_basic.py b/tests/test_basic.py index 91ba042f..dca48e2d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1166,6 +1166,10 @@ def test_response_types(app, client): def from_dict(): return {"foo": "bar"}, 201 + @app.route("/list") + def from_list(): + return ["foo", "bar"], 201 + assert client.get("/text").data == "Hällo Wörld".encode() assert client.get("/bytes").data == "Hällo Wörld".encode() @@ -1205,6 +1209,10 @@ def test_response_types(app, client): assert rv.json == {"foo": "bar"} assert rv.status_code == 201 + rv = client.get("/list") + assert rv.json == ["foo", "bar"] + assert rv.status_code == 201 + def test_response_type_errors(): app = flask.Flask(__name__) From f8cb0b0dd5ea30729db6b131aeeb30915ea4860b Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 2 Jul 2022 21:32:55 -0700 Subject: [PATCH 2/3] update docs about json --- CHANGES.rst | 3 +++ docs/quickstart.rst | 41 ++++++++++++++++++++++------------------- src/flask/app.py | 10 ++++++---- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 825b2e27..504e319f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -65,6 +65,9 @@ Unreleased ``with client`` block. It will be cleaned up when ``response.get_data()`` or ``response.close()`` is called. +- Allow returning a list from a view function, to convert it to a + JSON response like a dict is. :issue:`4672` + Version 2.1.3 ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7c6c594c..ed5f4557 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -681,22 +681,25 @@ The return value from a view function is automatically converted into a response object for you. If the return value is a string it's converted into a response object with the string as response body, a ``200 OK`` status code and a :mimetype:`text/html` mimetype. If the -return value is a dict, :func:`jsonify` is called to produce a response. -The logic that Flask applies to converting return values into response -objects is as follows: +return value is a dict or list, :func:`jsonify` is called to produce a +response. The logic that Flask applies to converting return values into +response objects is as follows: 1. If a response object of the correct type is returned it's directly returned from the view. 2. If it's a string, a response object is created with that data and the default parameters. -3. If it's a dict, a response object is created using ``jsonify``. -4. If a tuple is returned the items in the tuple can provide extra +3. If it's an iterator or generator returning strings or bytes, it is + treated as a streaming response. +4. If it's a dict or list, a response object is created using + :func:`~flask.json.jsonify`. +5. If a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form ``(response, status)``, ``(response, headers)``, or ``(response, status, headers)``. The ``status`` value will override the status code and ``headers`` can be a list or dictionary of additional header values. -5. If none of that works, Flask will assume the return value is a +6. If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object. If you want to get hold of the resulting response object inside the view @@ -727,8 +730,8 @@ APIs with JSON `````````````` A common response format when writing an API is JSON. It's easy to get -started writing such an API with Flask. If you return a ``dict`` from a -view, it will be converted to a JSON response. +started writing such an API with Flask. If you return a ``dict`` or +``list`` from a view, it will be converted to a JSON response. .. code-block:: python @@ -741,20 +744,20 @@ view, it will be converted to a JSON response. "image": url_for("user_image", filename=user.image), } -Depending on your API design, you may want to create JSON responses for -types other than ``dict``. In that case, use the -:func:`~flask.json.jsonify` function, which will serialize any supported -JSON data type. Or look into Flask community extensions that support -more complex applications. - -.. code-block:: python - - from flask import jsonify - @app.route("/users") def users_api(): users = get_all_users() - return jsonify([user.to_json() for user in users]) + return [user.to_json() for user in users] + +This is a shortcut to passing the data to the +:func:`~flask.json.jsonify` function, which will serialize any supported +JSON data type. That means that all the data in the dict or list must be +JSON serializable. + +For complex types such as database models, you'll want to use a +serialization library to convert the data to valid JSON types first. +There are many serialization libraries and Flask API extensions +maintained by the community that support more complex applications. .. _sessions: diff --git a/src/flask/app.py b/src/flask/app.py index f9db3b00..8726a6e8 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -1924,15 +1924,17 @@ class Flask(Scaffold): raise TypeError( f"{e}\nThe view function did not return a valid" " response. The return type must be a string," - " dict, list, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." ).with_traceback(sys.exc_info()[2]) from None else: raise TypeError( "The view function did not return a valid" " response. The return type must be a string," - " dict, list, tuple, Response instance, or WSGI" - f" callable, but it was a {type(rv).__name__}." + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." ) rv = t.cast(Response, rv) From 60b845ebabeb77e401186f4b83de21d3f9c6a9d7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 2 Jul 2022 21:40:20 -0700 Subject: [PATCH 3/3] update typing tests for json --- src/flask/typing.py | 8 +++++++- tests/typing/typing_route.py | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/flask/typing.py b/src/flask/typing.py index 30c9398c..6bbdb1dd 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -7,7 +7,13 @@ if t.TYPE_CHECKING: # pragma: no cover # The possible types that are directly convertible or are a Response object. ResponseValue = t.Union[ - "Response", str, bytes, list, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] + "Response", + str, + bytes, + t.List[t.Any], + t.Dict[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], ] # the possible types for an individual HTTP header diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index 9c518938..41973c26 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -25,7 +25,17 @@ def hello_bytes() -> bytes: @app.route("/json") def hello_json() -> Response: - return jsonify({"response": "Hello, World!"}) + return jsonify("Hello, World!") + + +@app.route("/json/dict") +def hello_json_dict() -> t.Dict[str, t.Any]: + return {"response": "Hello, World!"} + + +@app.route("/json/dict") +def hello_json_list() -> t.List[t.Any]: + return [{"message": "Hello"}, {"message": "World"}] @app.route("/generator")