From 762382e436062183b1e1fa6f3bda594090a452e7 Mon Sep 17 00:00:00 2001 From: pgjones Date: Thu, 9 Jun 2022 09:30:21 +0100 Subject: [PATCH] view functions can return generators as responses directly --- CHANGES.rst | 2 ++ src/flask/app.py | 13 ++++++++++++- src/flask/typing.py | 4 +++- tests/test_basic.py | 5 +++++ tests/typing/typing_route.py | 26 ++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7286f2b7..694d7a56 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -38,6 +38,8 @@ Unreleased context will already be active at that point. :issue:`2410` - ``SessionInterface.get_expiration_time`` uses a timezone-aware value. :pr:`4645` +- View functions can return generators directly instead of wrapping + them in a ``Response``. :pr:`4629` Version 2.1.3 diff --git a/src/flask/app.py b/src/flask/app.py index 360916db..236a47a8 100644 --- a/src/flask/app.py +++ b/src/flask/app.py @@ -5,6 +5,7 @@ import os import sys import typing as t import weakref +from collections.abc import Iterator as _abc_Iterator from datetime import timedelta from itertools import chain from threading import Lock @@ -1843,6 +1844,10 @@ class Flask(Scaffold): ``dict`` A dictionary that will be jsonify'd before being returned. + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + ``tuple`` Either ``(body, status, headers)``, ``(body, status)``, or ``(body, headers)``, where ``body`` is any of the other types @@ -1862,6 +1867,12 @@ class Flask(Scaffold): The function is called as a WSGI application. The result is used to create a response object. + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + .. versionchanged:: 0.9 Previously a tuple was interpreted as the arguments for the response object. @@ -1900,7 +1911,7 @@ class Flask(Scaffold): # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): - if isinstance(rv, (str, bytes, bytearray)): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, _abc_Iterator): # let the response class set the status and headers instead of # waiting to do it manually, so that the class can handle any # special logic diff --git a/src/flask/typing.py b/src/flask/typing.py index 18c2b10e..4fb96545 100644 --- a/src/flask/typing.py +++ b/src/flask/typing.py @@ -6,7 +6,9 @@ if t.TYPE_CHECKING: # pragma: no cover from werkzeug.wrappers import Response # noqa: F401 # The possible types that are directly convertible or are a Response object. -ResponseValue = t.Union["Response", str, bytes, t.Dict[str, t.Any]] +ResponseValue = t.Union[ + "Response", str, bytes, t.Dict[str, t.Any], t.Iterator[str], t.Iterator[bytes] +] # the possible types for an individual HTTP header # This should be a Union, but mypy doesn't pass unless it's a TypeVar. diff --git a/tests/test_basic.py b/tests/test_basic.py index 68141d03..916b7038 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1276,6 +1276,11 @@ def test_make_response(app, req_ctx): assert rv.data == b"W00t" assert rv.mimetype == "text/html" + rv = flask.make_response(c for c in "Hello") + assert rv.status_code == 200 + assert rv.data == b"Hello" + assert rv.mimetype == "text/html" + def test_make_response_with_response_instance(app, req_ctx): rv = flask.make_response(flask.jsonify({"msg": "W00t"}), 400) diff --git a/tests/typing/typing_route.py b/tests/typing/typing_route.py index ba49d132..9c518938 100644 --- a/tests/typing/typing_route.py +++ b/tests/typing/typing_route.py @@ -1,9 +1,11 @@ from __future__ import annotations +import typing as t from http import HTTPStatus from flask import Flask from flask import jsonify +from flask import stream_template from flask.templating import render_template from flask.views import View from flask.wrappers import Response @@ -26,6 +28,25 @@ def hello_json() -> Response: return jsonify({"response": "Hello, World!"}) +@app.route("/generator") +def hello_generator() -> t.Generator[str, None, None]: + def show() -> t.Generator[str, None, None]: + for x in range(100): + yield f"data:{x}\n\n" + + return show() + + +@app.route("/generator-expression") +def hello_generator_expression() -> t.Iterator[bytes]: + return (f"data:{x}\n\n".encode() for x in range(100)) + + +@app.route("/iterator") +def hello_iterator() -> t.Iterator[str]: + return iter([f"data:{x}\n\n" for x in range(100)]) + + @app.route("/status") @app.route("/status/") def tuple_status(code: int = 200) -> tuple[str, int]: @@ -48,6 +69,11 @@ def return_template(name: str | None = None) -> str: return render_template("index.html", name=name) +@app.route("/template") +def return_template_stream() -> t.Iterator[str]: + return stream_template("index.html", name="Hello") + + class RenderTemplateView(View): def __init__(self: RenderTemplateView, template_name: str) -> None: self.template_name = template_name