views.py 6.6 KB

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