Format: text/x-python
⬇ Download File (6.8 KB)
   1  from __future__ import annotations
   2  
   3  import typing as t
   4  
   5  from . import typing as ft
   6  from .globals import current_app
   7  from .globals import request
   8  
   9  F = t.TypeVar("F", bound=t.Callable[..., t.Any])
  10  
  11  http_method_funcs = frozenset(
  12      ["get", "post", "head", "options", "delete", "put", "trace", "patch"]
  13  )
  14  
  15  
  16  class View:
  17      """Subclass this class and override :meth:`dispatch_request` to
  18      create a generic class-based view. Call :meth:`as_view` to create a
  19      view function that creates an instance of the class with the given
  20      arguments and calls its ``dispatch_request`` method with any URL
  21      variables.
  22  
  23      See :doc:`views` for a detailed guide.
  24  
  25      .. code-block:: python
  26  
  27          class Hello(View):
  28              init_every_request = False
  29  
  30              def dispatch_request(self, name):
  31                  return f"Hello, {name}!"
  32  
  33          app.add_url_rule(
  34              "/hello/<name>", view_func=Hello.as_view("hello")
  35          )
  36  
  37      Set :attr:`methods` on the class to change what methods the view
  38      accepts.
  39  
  40      Set :attr:`decorators` on the class to apply a list of decorators to
  41      the generated view function. Decorators applied to the class itself
  42      will not be applied to the generated view function!
  43  
  44      Set :attr:`init_every_request` to ``False`` for efficiency, unless
  45      you need to store request-global data on ``self``.
  46      """
  47  
  48      #: The methods this view is registered for. Uses the same default
  49      #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and
  50      #: ``add_url_rule`` by default.
  51      methods: t.ClassVar[t.Collection[str] | None] = None
  52  
  53      #: Control whether the ``OPTIONS`` method is handled automatically.
  54      #: Uses the same default (``True``) as ``route`` and
  55      #: ``add_url_rule`` by default.
  56      provide_automatic_options: t.ClassVar[bool | None] = None
  57  
  58      #: A list of decorators to apply, in order, to the generated view
  59      #: function. Remember that ``@decorator`` syntax is applied bottom
  60      #: to top, so the first decorator in the list would be the bottom
  61      #: decorator.
  62      #:
  63      #: .. versionadded:: 0.8
  64      decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = []
  65  
  66      #: Create a new instance of this view class for every request by
  67      #: default. If a view subclass sets this to ``False``, the same
  68      #: instance is used for every request.
  69      #:
  70      #: A single instance is more efficient, especially if complex setup
  71      #: is done during init. However, storing data on ``self`` is no
  72      #: longer safe across requests, and :data:`~flask.g` should be used
  73      #: instead.
  74      #:
  75      #: .. versionadded:: 2.2
  76      init_every_request: t.ClassVar[bool] = True
  77  
  78      def dispatch_request(self) -> ft.ResponseReturnValue:
  79          """The actual view function behavior. Subclasses must override
  80          this and return a valid response. Any variables from the URL
  81          rule are passed as keyword arguments.
  82          """
  83          raise NotImplementedError()
  84  
  85      @classmethod
  86      def as_view(
  87          cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
  88      ) -> ft.RouteCallable:
  89          """Convert the class into a view function that can be registered
  90          for a route.
  91  
  92          By default, the generated view will create a new instance of the
  93          view class for every request and call its
  94          :meth:`dispatch_request` method. If the view class sets
  95          :attr:`init_every_request` to ``False``, the same instance will
  96          be used for every request.
  97  
  98          Except for ``name``, all other arguments passed to this method
  99          are forwarded to the view class ``__init__`` method.
 100  
 101          .. versionchanged:: 2.2
 102              Added the ``init_every_request`` class attribute.
 103          """
 104          if cls.init_every_request:
 105  
 106              def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
 107                  self = view.view_class(  # type: ignore[attr-defined]
 108                      *class_args, **class_kwargs
 109                  )
 110                  return current_app.ensure_sync(self.dispatch_request)(**kwargs)  # type: ignore[no-any-return]
 111  
 112          else:
 113              self = cls(*class_args, **class_kwargs)  # pyright: ignore
 114  
 115              def view(**kwargs: t.Any) -> ft.ResponseReturnValue:
 116                  return current_app.ensure_sync(self.dispatch_request)(**kwargs)  # type: ignore[no-any-return]
 117  
 118          if cls.decorators:
 119              view.__name__ = name
 120              view.__module__ = cls.__module__
 121              for decorator in cls.decorators:
 122                  view = decorator(view)
 123  
 124          # We attach the view class to the view function for two reasons:
 125          # first of all it allows us to easily figure out what class-based
 126          # view this thing came from, secondly it's also used for instantiating
 127          # the view class so you can actually replace it with something else
 128          # for testing purposes and debugging.
 129          view.view_class = cls  # type: ignore
 130          view.__name__ = name
 131          view.__doc__ = cls.__doc__
 132          view.__module__ = cls.__module__
 133          view.methods = cls.methods  # type: ignore
 134          view.provide_automatic_options = cls.provide_automatic_options  # type: ignore
 135          return view
 136  
 137  
 138  class MethodView(View):
 139      """Dispatches request methods to the corresponding instance methods.
 140      For example, if you implement a ``get`` method, it will be used to
 141      handle ``GET`` requests.
 142  
 143      This can be useful for defining a REST API.
 144  
 145      :attr:`methods` is automatically set based on the methods defined on
 146      the class.
 147  
 148      See :doc:`views` for a detailed guide.
 149  
 150      .. code-block:: python
 151  
 152          class CounterAPI(MethodView):
 153              def get(self):
 154                  return str(session.get("counter", 0))
 155  
 156              def post(self):
 157                  session["counter"] = session.get("counter", 0) + 1
 158                  return redirect(url_for("counter"))
 159  
 160          app.add_url_rule(
 161              "/counter", view_func=CounterAPI.as_view("counter")
 162          )
 163      """
 164  
 165      def __init_subclass__(cls, **kwargs: t.Any) -> None:
 166          super().__init_subclass__(**kwargs)
 167  
 168          if "methods" not in cls.__dict__:
 169              methods = set()
 170  
 171              for base in cls.__bases__:
 172                  if getattr(base, "methods", None):
 173                      methods.update(base.methods)  # type: ignore[attr-defined]
 174  
 175              for key in http_method_funcs:
 176                  if hasattr(cls, key):
 177                      methods.add(key.upper())
 178  
 179              if methods:
 180                  cls.methods = methods
 181  
 182      def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue:
 183          meth = getattr(self, request.method.lower(), None)
 184  
 185          # If the request method is HEAD and we don't have a handler for it
 186          # retry with GET.
 187          if meth is None and request.method == "HEAD":
 188              meth = getattr(self, "get", None)
 189  
 190          assert meth is not None, f"Unimplemented method {request.method!r}"
 191          return current_app.ensure_sync(meth)(**kwargs)  # type: ignore[no-any-return]
 192