views.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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. """Alternative way to use view functions. A subclass has to implement
  10. :meth:`dispatch_request` which is called with the view arguments from
  11. the URL routing system. If :attr:`methods` is provided the methods
  12. do not have to be passed to the :meth:`~flask.Flask.add_url_rule`
  13. method explicitly::
  14. class MyView(View):
  15. methods = ['GET']
  16. def dispatch_request(self, name):
  17. return f"Hello {name}!"
  18. app.add_url_rule('/hello/<name>', view_func=MyView.as_view('myview'))
  19. When you want to decorate a pluggable view you will have to either do that
  20. when the view function is created (by wrapping the return value of
  21. :meth:`as_view`) or you can use the :attr:`decorators` attribute::
  22. class SecretView(View):
  23. methods = ['GET']
  24. decorators = [superuser_required]
  25. def dispatch_request(self):
  26. ...
  27. The decorators stored in the decorators list are applied one after another
  28. when the view function is created. Note that you can *not* use the class
  29. based decorators since those would decorate the view class and not the
  30. generated view function!
  31. """
  32. #: A list of methods this view can handle.
  33. methods: t.Optional[t.List[str]] = None
  34. #: Setting this disables or force-enables the automatic options handling.
  35. provide_automatic_options: t.Optional[bool] = None
  36. #: The canonical way to decorate class-based views is to decorate the
  37. #: return value of as_view(). However since this moves parts of the
  38. #: logic from the class declaration to the place where it's hooked
  39. #: into the routing system.
  40. #:
  41. #: You can place one or more decorators in this list and whenever the
  42. #: view function is created the result is automatically decorated.
  43. #:
  44. #: .. versionadded:: 0.8
  45. decorators: t.List[t.Callable] = []
  46. def dispatch_request(self) -> ft.ResponseReturnValue:
  47. """Subclasses have to override this method to implement the
  48. actual view function code. This method is called with all
  49. the arguments from the URL rule.
  50. """
  51. raise NotImplementedError()
  52. @classmethod
  53. def as_view(
  54. cls, name: str, *class_args: t.Any, **class_kwargs: t.Any
  55. ) -> t.Callable:
  56. """Converts the class into an actual view function that can be used
  57. with the routing system. Internally this generates a function on the
  58. fly which will instantiate the :class:`View` on each request and call
  59. the :meth:`dispatch_request` method on it.
  60. The arguments passed to :meth:`as_view` are forwarded to the
  61. constructor of the class.
  62. """
  63. def view(*args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue:
  64. self = view.view_class(*class_args, **class_kwargs) # type: ignore
  65. return current_app.ensure_sync(self.dispatch_request)(*args, **kwargs)
  66. if cls.decorators:
  67. view.__name__ = name
  68. view.__module__ = cls.__module__
  69. for decorator in cls.decorators:
  70. view = decorator(view)
  71. # We attach the view class to the view function for two reasons:
  72. # first of all it allows us to easily figure out what class-based
  73. # view this thing came from, secondly it's also used for instantiating
  74. # the view class so you can actually replace it with something else
  75. # for testing purposes and debugging.
  76. view.view_class = cls # type: ignore
  77. view.__name__ = name
  78. view.__doc__ = cls.__doc__
  79. view.__module__ = cls.__module__
  80. view.methods = cls.methods # type: ignore
  81. view.provide_automatic_options = cls.provide_automatic_options # type: ignore
  82. return view
  83. class MethodViewType(type):
  84. """Metaclass for :class:`MethodView` that determines what methods the view
  85. defines.
  86. """
  87. def __init__(cls, name, bases, d):
  88. super().__init__(name, bases, d)
  89. if "methods" not in d:
  90. methods = set()
  91. for base in bases:
  92. if getattr(base, "methods", None):
  93. methods.update(base.methods)
  94. for key in http_method_funcs:
  95. if hasattr(cls, key):
  96. methods.add(key.upper())
  97. # If we have no method at all in there we don't want to add a
  98. # method list. This is for instance the case for the base class
  99. # or another subclass of a base method view that does not introduce
  100. # new methods.
  101. if methods:
  102. cls.methods = methods
  103. class MethodView(View, metaclass=MethodViewType):
  104. """A class-based view that dispatches request methods to the corresponding
  105. class methods. For example, if you implement a ``get`` method, it will be
  106. used to handle ``GET`` requests. ::
  107. class CounterAPI(MethodView):
  108. def get(self):
  109. return session.get('counter', 0)
  110. def post(self):
  111. session['counter'] = session.get('counter', 0) + 1
  112. return 'OK'
  113. app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
  114. """
  115. def dispatch_request(self, *args: t.Any, **kwargs: t.Any) -> ft.ResponseReturnValue:
  116. meth = getattr(self, request.method.lower(), None)
  117. # If the request method is HEAD and we don't have a handler for it
  118. # retry with GET.
  119. if meth is None and request.method == "HEAD":
  120. meth = getattr(self, "get", None)
  121. assert meth is not None, f"Unimplemented method {request.method!r}"
  122. return current_app.ensure_sync(meth)(*args, **kwargs)