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

59 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1import logging 

2 

3from plain.http import ( 

4 HttpRequest, 

5 JsonResponse, 

6 Response, 

7 ResponseBase, 

8 ResponseNotAllowed, 

9) 

10from plain.utils.decorators import classonlymethod 

11 

12from .exceptions import ResponseException 

13 

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

15 

16 

17class 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)]