Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/views/base.py: 69%

1 import logging

2

3 from plain.http import (

4 HttpRequest,

5 JsonResponse,

6 Response,

7 ResponseBase,

8 ResponseNotAllowed,

9)

10 from plain.utils.decorators import classonlymethod

11

12 from .exceptions import ResponseException

13

14 logger = logging.getLogger("plain.request")

15

16

17 class View:

18 request: HttpRequest

19 url_args: tuple

20 url_kwargs: dict

21

22 def __init__(self, *args, **kwargs) -> None:

23 # Views can customize their init, which receives

24 # the args and kwargs from as_view()

25 pass

26

27 def setup(self, request: HttpRequest, *args, **kwargs) -> None:

28 if hasattr(self, "get") and not hasattr(self, "head"):

29 self.head = self.get

30

31 self.request = request

32 self.url_args = args

33 self.url_kwargs = kwargs

34

35 @classonlymethod

36 def as_view(cls, *init_args, **init_kwargs):

37 def view(request, *args, **kwargs):

38 v = cls(*init_args, **init_kwargs)

39 v.setup(request, *args, **kwargs)

40 try:

41 return v.get_response()

42 except ResponseException as e:

43 return e.response

44

45 # Copy possible attributes set by decorators, e.g. @csrf_exempt, from

46 # the dispatch method.

47 view.__dict__.update(cls.get_response.__dict__)

48 view.view_class = cls

49

50 return view

51

52 def get_request_handler(self) -> callable:

53 """Return the handler for the current request method."""

54

55 if not self.request.method:

56 raise AttributeError("HTTP method is not set")

57

58 handler = getattr(self, self.request.method.lower(), None)

59

60 if not handler:

61 logger.warning(

62 "Method Not Allowed (%s): %s",

63 self.request.method,

64 self.request.path,

65 extra={"status_code": 405, "request": self.request},

66 )

67 raise ResponseException(ResponseNotAllowed(self._allowed_methods()))

68

69 return handler

70

71 def get_response(self) -> ResponseBase:

72 handler = self.get_request_handler()

73

74 result = handler()

75

76 if isinstance(result, ResponseBase):

77 return result

78

79 if isinstance(result, str):

80 return Response(result)

81

82 if isinstance(result, list):

83 return JsonResponse(result, safe=False)

84

85 if isinstance(result, dict):

86 return JsonResponse(result)

87

88 if isinstance(result, int):

89 return Response(status=result)

90

91 # Allow tuple for (status_code, content)?

92

93 raise ValueError(f"Unexpected view return type: {type(result)}")

94

95 def options(self) -> Response:

96 """Handle responding to requests for the OPTIONS HTTP verb."""

97 response = Response()

98 response.headers["Allow"] = ", ".join(self._allowed_methods())

99 response.headers["Content-Length"] = "0"

100 return response

101

102 def _allowed_methods(self) -> list[str]:

103 known_http_method_names = [

104 "get",

105 "post",

106 "put",

107 "patch",

108 "delete",

109 "head",

110 "options",

111 "trace",

112 ]

113 return [m.upper() for m in known_http_method_names if hasattr(self, m)]