add View.init_every_request attribute

This commit is contained in:
David Lord 2022-06-06 11:04:04 -07:00
parent aab1d9935e
commit 6e23239567
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
3 changed files with 61 additions and 13 deletions

View File

@ -22,7 +22,9 @@ Unreleased
:issue:`4571` :issue:`4571`
- ``before_first_request`` is deprecated. Run setup code when creating - ``before_first_request`` is deprecated. Run setup code when creating
the application instead. :issue:`4605` the application instead. :issue:`4605`
- Added the ``View.init_every_request`` class attribute. If a view
subclass sets this to ``False``, the view will not create a new
instance on every request. :issue:`2520`.
Version 2.1.3 Version 2.1.3
------------- -------------

View File

@ -59,6 +59,18 @@ class View:
#: .. versionadded:: 0.8 #: .. versionadded:: 0.8
decorators: t.List[t.Callable] = [] decorators: t.List[t.Callable] = []
#: Create a new instance of this view class for every request by
#: default. If a view subclass sets this to ``False``, the same
#: instance is used for every request.
#:
#: A single instance is more efficient, especially if complex setup
#: is done during init. However, storing data on ``self`` is no
#: longer safe across requests, and :data:`~flask.g` should be used
#: instead.
#:
#: .. versionadded:: 2.2
init_every_request: t.ClassVar[bool] = True
def dispatch_request(self) -> ft.ResponseReturnValue: def dispatch_request(self) -> ft.ResponseReturnValue:
"""Subclasses have to override this method to implement the """Subclasses have to override this method to implement the
actual view function code. This method is called with all actual view function code. This method is called with all
@ -69,19 +81,35 @@ class View:
@classmethod @classmethod
def as_view( def as_view(
cls, name: str, *class_args: t.Any, **class_kwargs: t.Any cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
) -> t.Callable: ) -> ft.ViewCallable:
"""Converts the class into an actual view function that can be used """Convert the class into a view function that can be registered
with the routing system. Internally this generates a function on the for a route.
fly which will instantiate the :class:`View` on each request and call
the :meth:`dispatch_request` method on it.
The arguments passed to :meth:`as_view` are forwarded to the By default, the generated view will create a new instance of the
constructor of the class. view class for every request and call its
:meth:`dispatch_request` method. If the view class sets
:attr:`init_every_request` to ``False``, the same instance will
be used for every request.
The arguments passed to this method are forwarded to the view
class ``__init__`` method.
.. versionchanged:: 2.2
Added the ``init_every_request`` class attribute.
""" """
if cls.init_every_request:
def view(*args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
self = view.view_class(*class_args, **class_kwargs) # type: ignore self = view.view_class( # type: ignore[attr-defined]
return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs) *class_args, **class_kwargs
)
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
else:
self = cls(*class_args, **class_kwargs)
def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
if cls.decorators: if cls.decorators:
view.__name__ = name view.__name__ = name
@ -146,7 +174,7 @@ class MethodView(View, metaclass=MethodViewType):
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
""" """
def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue: def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
meth = getattr(self, request.method.lower(), None) meth = getattr(self, request.method.lower(), None)
# If the request method is HEAD and we don't have a handler for it # If the request method is HEAD and we don't have a handler for it
@ -155,4 +183,4 @@ class MethodView(View, metaclass=MethodViewType):
meth = getattr(self, "get", None) meth = getattr(self, "get", None)
assert meth is not None, f"Unimplemented method {request.method!r}" assert meth is not None, f"Unimplemented method {request.method!r}"
return current_app.ensure_sync(meth)(*args, **kwargs) return current_app.ensure_sync(meth)(**kwargs)

View File

@ -240,3 +240,21 @@ def test_remove_method_from_parent(app, client):
assert client.get("/").data == b"GET" assert client.get("/").data == b"GET"
assert client.post("/").status_code == 405 assert client.post("/").status_code == 405
assert sorted(View.methods) == ["GET"] assert sorted(View.methods) == ["GET"]
def test_init_once(app, client):
n = 0
class CountInit(flask.views.View):
init_every_request = False
def __init__(self):
nonlocal n
n += 1
def dispatch_request(self):
return str(n)
app.add_url_rule("/", view_func=CountInit.as_view("index"))
assert client.get("/").data == b"1"
assert client.get("/").data == b"1"