views.py 6.6 KB

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