plain 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. plain/README.md +33 -0
  2. plain/__main__.py +5 -0
  3. plain/assets/README.md +56 -0
  4. plain/assets/__init__.py +6 -0
  5. plain/assets/finders.py +233 -0
  6. plain/assets/preflight.py +14 -0
  7. plain/assets/storage.py +916 -0
  8. plain/assets/utils.py +52 -0
  9. plain/assets/whitenoise/__init__.py +5 -0
  10. plain/assets/whitenoise/base.py +259 -0
  11. plain/assets/whitenoise/compress.py +189 -0
  12. plain/assets/whitenoise/media_types.py +137 -0
  13. plain/assets/whitenoise/middleware.py +197 -0
  14. plain/assets/whitenoise/responders.py +286 -0
  15. plain/assets/whitenoise/storage.py +178 -0
  16. plain/assets/whitenoise/string_utils.py +13 -0
  17. plain/cli/README.md +123 -0
  18. plain/cli/__init__.py +3 -0
  19. plain/cli/cli.py +439 -0
  20. plain/cli/formatting.py +61 -0
  21. plain/cli/packages.py +73 -0
  22. plain/cli/print.py +9 -0
  23. plain/cli/startup.py +33 -0
  24. plain/csrf/README.md +3 -0
  25. plain/csrf/middleware.py +466 -0
  26. plain/csrf/views.py +10 -0
  27. plain/debug.py +23 -0
  28. plain/exceptions.py +242 -0
  29. plain/forms/README.md +14 -0
  30. plain/forms/__init__.py +8 -0
  31. plain/forms/boundfield.py +58 -0
  32. plain/forms/exceptions.py +11 -0
  33. plain/forms/fields.py +1030 -0
  34. plain/forms/forms.py +297 -0
  35. plain/http/README.md +1 -0
  36. plain/http/__init__.py +51 -0
  37. plain/http/cookie.py +20 -0
  38. plain/http/multipartparser.py +743 -0
  39. plain/http/request.py +754 -0
  40. plain/http/response.py +719 -0
  41. plain/internal/__init__.py +0 -0
  42. plain/internal/files/README.md +3 -0
  43. plain/internal/files/__init__.py +3 -0
  44. plain/internal/files/base.py +161 -0
  45. plain/internal/files/locks.py +127 -0
  46. plain/internal/files/move.py +102 -0
  47. plain/internal/files/temp.py +79 -0
  48. plain/internal/files/uploadedfile.py +150 -0
  49. plain/internal/files/uploadhandler.py +254 -0
  50. plain/internal/files/utils.py +78 -0
  51. plain/internal/handlers/__init__.py +0 -0
  52. plain/internal/handlers/base.py +133 -0
  53. plain/internal/handlers/exception.py +145 -0
  54. plain/internal/handlers/wsgi.py +216 -0
  55. plain/internal/legacy/__init__.py +0 -0
  56. plain/internal/legacy/__main__.py +12 -0
  57. plain/internal/legacy/management/__init__.py +414 -0
  58. plain/internal/legacy/management/base.py +692 -0
  59. plain/internal/legacy/management/color.py +113 -0
  60. plain/internal/legacy/management/commands/__init__.py +0 -0
  61. plain/internal/legacy/management/commands/collectstatic.py +297 -0
  62. plain/internal/legacy/management/sql.py +67 -0
  63. plain/internal/legacy/management/utils.py +175 -0
  64. plain/json.py +40 -0
  65. plain/logs/README.md +24 -0
  66. plain/logs/__init__.py +5 -0
  67. plain/logs/configure.py +39 -0
  68. plain/logs/loggers.py +74 -0
  69. plain/logs/utils.py +46 -0
  70. plain/middleware/README.md +3 -0
  71. plain/middleware/__init__.py +0 -0
  72. plain/middleware/clickjacking.py +52 -0
  73. plain/middleware/common.py +87 -0
  74. plain/middleware/gzip.py +64 -0
  75. plain/middleware/security.py +64 -0
  76. plain/packages/README.md +41 -0
  77. plain/packages/__init__.py +4 -0
  78. plain/packages/config.py +259 -0
  79. plain/packages/registry.py +438 -0
  80. plain/paginator.py +187 -0
  81. plain/preflight/README.md +3 -0
  82. plain/preflight/__init__.py +38 -0
  83. plain/preflight/compatibility/__init__.py +0 -0
  84. plain/preflight/compatibility/django_4_0.py +20 -0
  85. plain/preflight/files.py +19 -0
  86. plain/preflight/messages.py +88 -0
  87. plain/preflight/registry.py +72 -0
  88. plain/preflight/security/__init__.py +0 -0
  89. plain/preflight/security/base.py +268 -0
  90. plain/preflight/security/csrf.py +40 -0
  91. plain/preflight/urls.py +117 -0
  92. plain/runtime/README.md +75 -0
  93. plain/runtime/__init__.py +61 -0
  94. plain/runtime/global_settings.py +199 -0
  95. plain/runtime/user_settings.py +353 -0
  96. plain/signals/README.md +14 -0
  97. plain/signals/__init__.py +5 -0
  98. plain/signals/dispatch/__init__.py +9 -0
  99. plain/signals/dispatch/dispatcher.py +320 -0
  100. plain/signals/dispatch/license.txt +35 -0
  101. plain/signing.py +299 -0
  102. plain/templates/README.md +20 -0
  103. plain/templates/__init__.py +6 -0
  104. plain/templates/core.py +24 -0
  105. plain/templates/jinja/README.md +227 -0
  106. plain/templates/jinja/__init__.py +22 -0
  107. plain/templates/jinja/defaults.py +119 -0
  108. plain/templates/jinja/extensions.py +39 -0
  109. plain/templates/jinja/filters.py +28 -0
  110. plain/templates/jinja/globals.py +19 -0
  111. plain/test/README.md +3 -0
  112. plain/test/__init__.py +16 -0
  113. plain/test/client.py +985 -0
  114. plain/test/utils.py +255 -0
  115. plain/urls/README.md +3 -0
  116. plain/urls/__init__.py +40 -0
  117. plain/urls/base.py +118 -0
  118. plain/urls/conf.py +94 -0
  119. plain/urls/converters.py +66 -0
  120. plain/urls/exceptions.py +9 -0
  121. plain/urls/resolvers.py +731 -0
  122. plain/utils/README.md +3 -0
  123. plain/utils/__init__.py +0 -0
  124. plain/utils/_os.py +52 -0
  125. plain/utils/cache.py +327 -0
  126. plain/utils/connection.py +84 -0
  127. plain/utils/crypto.py +76 -0
  128. plain/utils/datastructures.py +345 -0
  129. plain/utils/dateformat.py +329 -0
  130. plain/utils/dateparse.py +154 -0
  131. plain/utils/dates.py +76 -0
  132. plain/utils/deconstruct.py +54 -0
  133. plain/utils/decorators.py +90 -0
  134. plain/utils/deprecation.py +6 -0
  135. plain/utils/duration.py +44 -0
  136. plain/utils/email.py +12 -0
  137. plain/utils/encoding.py +235 -0
  138. plain/utils/functional.py +456 -0
  139. plain/utils/hashable.py +26 -0
  140. plain/utils/html.py +401 -0
  141. plain/utils/http.py +374 -0
  142. plain/utils/inspect.py +73 -0
  143. plain/utils/ipv6.py +46 -0
  144. plain/utils/itercompat.py +8 -0
  145. plain/utils/module_loading.py +69 -0
  146. plain/utils/regex_helper.py +353 -0
  147. plain/utils/safestring.py +72 -0
  148. plain/utils/termcolors.py +221 -0
  149. plain/utils/text.py +518 -0
  150. plain/utils/timesince.py +138 -0
  151. plain/utils/timezone.py +244 -0
  152. plain/utils/tree.py +126 -0
  153. plain/validators.py +603 -0
  154. plain/views/README.md +268 -0
  155. plain/views/__init__.py +18 -0
  156. plain/views/base.py +107 -0
  157. plain/views/csrf.py +24 -0
  158. plain/views/errors.py +25 -0
  159. plain/views/exceptions.py +4 -0
  160. plain/views/forms.py +76 -0
  161. plain/views/objects.py +229 -0
  162. plain/views/redirect.py +72 -0
  163. plain/views/templates.py +66 -0
  164. plain/wsgi.py +11 -0
  165. plain-0.1.0.dist-info/LICENSE +85 -0
  166. plain-0.1.0.dist-info/METADATA +51 -0
  167. plain-0.1.0.dist-info/RECORD +169 -0
  168. plain-0.1.0.dist-info/WHEEL +4 -0
  169. plain-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,254 @@
1
+ """
2
+ Base file upload handler classes, and the built-in concrete subclasses
3
+ """
4
+ import os
5
+ from io import BytesIO
6
+
7
+ from plain.internal.files.uploadedfile import (
8
+ InMemoryUploadedFile,
9
+ TemporaryUploadedFile,
10
+ )
11
+ from plain.runtime import settings
12
+ from plain.utils.module_loading import import_string
13
+
14
+ __all__ = [
15
+ "UploadFileException",
16
+ "StopUpload",
17
+ "SkipFile",
18
+ "FileUploadHandler",
19
+ "TemporaryFileUploadHandler",
20
+ "MemoryFileUploadHandler",
21
+ "load_handler",
22
+ "StopFutureHandlers",
23
+ ]
24
+
25
+
26
+ class UploadFileException(Exception):
27
+ """
28
+ Any error having to do with uploading files.
29
+ """
30
+
31
+ pass
32
+
33
+
34
+ class StopUpload(UploadFileException):
35
+ """
36
+ This exception is raised when an upload must abort.
37
+ """
38
+
39
+ def __init__(self, connection_reset=False):
40
+ """
41
+ If ``connection_reset`` is ``True``, Plain knows will halt the upload
42
+ without consuming the rest of the upload. This will cause the browser to
43
+ show a "connection reset" error.
44
+ """
45
+ self.connection_reset = connection_reset
46
+
47
+ def __str__(self):
48
+ if self.connection_reset:
49
+ return "StopUpload: Halt current upload."
50
+ else:
51
+ return "StopUpload: Consume request data, then halt."
52
+
53
+
54
+ class SkipFile(UploadFileException):
55
+ """
56
+ This exception is raised by an upload handler that wants to skip a given file.
57
+ """
58
+
59
+ pass
60
+
61
+
62
+ class StopFutureHandlers(UploadFileException):
63
+ """
64
+ Upload handlers that have handled a file and do not want future handlers to
65
+ run should raise this exception instead of returning None.
66
+ """
67
+
68
+ pass
69
+
70
+
71
+ class FileUploadHandler:
72
+ """
73
+ Base class for streaming upload handlers.
74
+ """
75
+
76
+ chunk_size = 64 * 2**10 # : The default chunk size is 64 KB.
77
+
78
+ def __init__(self, request=None):
79
+ self.file_name = None
80
+ self.content_type = None
81
+ self.content_length = None
82
+ self.charset = None
83
+ self.content_type_extra = None
84
+ self.request = request
85
+
86
+ def handle_raw_input(
87
+ self, input_data, META, content_length, boundary, encoding=None
88
+ ):
89
+ """
90
+ Handle the raw input from the client.
91
+
92
+ Parameters:
93
+
94
+ :input_data:
95
+ An object that supports reading via .read().
96
+ :META:
97
+ ``request.META``.
98
+ :content_length:
99
+ The (integer) value of the Content-Length header from the
100
+ client.
101
+ :boundary: The boundary from the Content-Type header. Be sure to
102
+ prepend two '--'.
103
+ """
104
+ pass
105
+
106
+ def new_file(
107
+ self,
108
+ field_name,
109
+ file_name,
110
+ content_type,
111
+ content_length,
112
+ charset=None,
113
+ content_type_extra=None,
114
+ ):
115
+ """
116
+ Signal that a new file has been started.
117
+
118
+ Warning: As with any data from the client, you should not trust
119
+ content_length (and sometimes won't even get it).
120
+ """
121
+ self.field_name = field_name
122
+ self.file_name = file_name
123
+ self.content_type = content_type
124
+ self.content_length = content_length
125
+ self.charset = charset
126
+ self.content_type_extra = content_type_extra
127
+
128
+ def receive_data_chunk(self, raw_data, start):
129
+ """
130
+ Receive data from the streamed upload parser. ``start`` is the position
131
+ in the file of the chunk.
132
+ """
133
+ raise NotImplementedError(
134
+ "subclasses of FileUploadHandler must provide a receive_data_chunk() method"
135
+ )
136
+
137
+ def file_complete(self, file_size):
138
+ """
139
+ Signal that a file has completed. File size corresponds to the actual
140
+ size accumulated by all the chunks.
141
+
142
+ Subclasses should return a valid ``UploadedFile`` object.
143
+ """
144
+ raise NotImplementedError(
145
+ "subclasses of FileUploadHandler must provide a file_complete() method"
146
+ )
147
+
148
+ def upload_complete(self):
149
+ """
150
+ Signal that the upload is complete. Subclasses should perform cleanup
151
+ that is necessary for this handler.
152
+ """
153
+ pass
154
+
155
+ def upload_interrupted(self):
156
+ """
157
+ Signal that the upload was interrupted. Subclasses should perform
158
+ cleanup that is necessary for this handler.
159
+ """
160
+ pass
161
+
162
+
163
+ class TemporaryFileUploadHandler(FileUploadHandler):
164
+ """
165
+ Upload handler that streams data into a temporary file.
166
+ """
167
+
168
+ def new_file(self, *args, **kwargs):
169
+ """
170
+ Create the file object to append to as data is coming in.
171
+ """
172
+ super().new_file(*args, **kwargs)
173
+ self.file = TemporaryUploadedFile(
174
+ self.file_name, self.content_type, 0, self.charset, self.content_type_extra
175
+ )
176
+
177
+ def receive_data_chunk(self, raw_data, start):
178
+ self.file.write(raw_data)
179
+
180
+ def file_complete(self, file_size):
181
+ self.file.seek(0)
182
+ self.file.size = file_size
183
+ return self.file
184
+
185
+ def upload_interrupted(self):
186
+ if hasattr(self, "file"):
187
+ temp_location = self.file.temporary_file_path()
188
+ try:
189
+ self.file.close()
190
+ os.remove(temp_location)
191
+ except FileNotFoundError:
192
+ pass
193
+
194
+
195
+ class MemoryFileUploadHandler(FileUploadHandler):
196
+ """
197
+ File upload handler to stream uploads into memory (used for small files).
198
+ """
199
+
200
+ def handle_raw_input(
201
+ self, input_data, META, content_length, boundary, encoding=None
202
+ ):
203
+ """
204
+ Use the content_length to signal whether or not this handler should be
205
+ used.
206
+ """
207
+ # Check the content-length header to see if we should
208
+ # If the post is too large, we cannot use the Memory handler.
209
+ self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
210
+
211
+ def new_file(self, *args, **kwargs):
212
+ super().new_file(*args, **kwargs)
213
+ if self.activated:
214
+ self.file = BytesIO()
215
+ raise StopFutureHandlers()
216
+
217
+ def receive_data_chunk(self, raw_data, start):
218
+ """Add the data to the BytesIO file."""
219
+ if self.activated:
220
+ self.file.write(raw_data)
221
+ else:
222
+ return raw_data
223
+
224
+ def file_complete(self, file_size):
225
+ """Return a file object if this handler is activated."""
226
+ if not self.activated:
227
+ return
228
+
229
+ self.file.seek(0)
230
+ return InMemoryUploadedFile(
231
+ file=self.file,
232
+ field_name=self.field_name,
233
+ name=self.file_name,
234
+ content_type=self.content_type,
235
+ size=file_size,
236
+ charset=self.charset,
237
+ content_type_extra=self.content_type_extra,
238
+ )
239
+
240
+
241
+ def load_handler(path, *args, **kwargs):
242
+ """
243
+ Given a path to a handler, return an instance of that handler.
244
+
245
+ E.g.::
246
+ >>> from plain.http import HttpRequest
247
+ >>> request = HttpRequest()
248
+ >>> load_handler(
249
+ ... 'plain.internal.files.uploadhandler.TemporaryFileUploadHandler',
250
+ ... request,
251
+ ... )
252
+ <TemporaryFileUploadHandler object at 0x...>
253
+ """
254
+ return import_string(path)(*args, **kwargs)
@@ -0,0 +1,78 @@
1
+ import os
2
+ import pathlib
3
+
4
+ from plain.exceptions import SuspiciousFileOperation
5
+
6
+
7
+ def validate_file_name(name, allow_relative_path=False):
8
+ # Remove potentially dangerous names
9
+ if os.path.basename(name) in {"", ".", ".."}:
10
+ raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
11
+
12
+ if allow_relative_path:
13
+ # Use PurePosixPath() because this branch is checked only in
14
+ # FileField.generate_filename() where all file paths are expected to be
15
+ # Unix style (with forward slashes).
16
+ path = pathlib.PurePosixPath(name)
17
+ if path.is_absolute() or ".." in path.parts:
18
+ raise SuspiciousFileOperation(
19
+ "Detected path traversal attempt in '%s'" % name
20
+ )
21
+ elif name != os.path.basename(name):
22
+ raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
23
+
24
+ return name
25
+
26
+
27
+ class FileProxyMixin:
28
+ """
29
+ A mixin class used to forward file methods to an underlying file
30
+ object. The internal file object has to be called "file"::
31
+
32
+ class FileProxy(FileProxyMixin):
33
+ def __init__(self, file):
34
+ self.file = file
35
+ """
36
+
37
+ encoding = property(lambda self: self.file.encoding)
38
+ fileno = property(lambda self: self.file.fileno)
39
+ flush = property(lambda self: self.file.flush)
40
+ isatty = property(lambda self: self.file.isatty)
41
+ newlines = property(lambda self: self.file.newlines)
42
+ read = property(lambda self: self.file.read)
43
+ readinto = property(lambda self: self.file.readinto)
44
+ readline = property(lambda self: self.file.readline)
45
+ readlines = property(lambda self: self.file.readlines)
46
+ seek = property(lambda self: self.file.seek)
47
+ tell = property(lambda self: self.file.tell)
48
+ truncate = property(lambda self: self.file.truncate)
49
+ write = property(lambda self: self.file.write)
50
+ writelines = property(lambda self: self.file.writelines)
51
+
52
+ @property
53
+ def closed(self):
54
+ return not self.file or self.file.closed
55
+
56
+ def readable(self):
57
+ if self.closed:
58
+ return False
59
+ if hasattr(self.file, "readable"):
60
+ return self.file.readable()
61
+ return True
62
+
63
+ def writable(self):
64
+ if self.closed:
65
+ return False
66
+ if hasattr(self.file, "writable"):
67
+ return self.file.writable()
68
+ return "w" in getattr(self.file, "mode", "")
69
+
70
+ def seekable(self):
71
+ if self.closed:
72
+ return False
73
+ if hasattr(self.file, "seekable"):
74
+ return self.file.seekable()
75
+ return True
76
+
77
+ def __iter__(self):
78
+ return iter(self.file)
File without changes
@@ -0,0 +1,133 @@
1
+ import logging
2
+ import types
3
+
4
+ from plain.exceptions import ImproperlyConfigured
5
+ from plain.logs import log_response
6
+ from plain.runtime import settings
7
+ from plain.signals import request_finished
8
+ from plain.urls import get_resolver, set_urlconf
9
+ from plain.utils.module_loading import import_string
10
+
11
+ from .exception import convert_exception_to_response
12
+
13
+ logger = logging.getLogger("plain.request")
14
+
15
+
16
+ class BaseHandler:
17
+ _view_middleware = None
18
+ _middleware_chain = None
19
+
20
+ def load_middleware(self):
21
+ """
22
+ Populate middleware lists from settings.MIDDLEWARE.
23
+
24
+ Must be called after the environment is fixed (see __call__ in subclasses).
25
+ """
26
+ self._view_middleware = []
27
+
28
+ get_response = self._get_response
29
+ handler = convert_exception_to_response(get_response)
30
+ for middleware_path in reversed(settings.MIDDLEWARE):
31
+ middleware = import_string(middleware_path)
32
+ mw_instance = middleware(handler)
33
+
34
+ if mw_instance is None:
35
+ raise ImproperlyConfigured(
36
+ "Middleware factory %s returned None." % middleware_path
37
+ )
38
+
39
+ if hasattr(mw_instance, "process_view"):
40
+ self._view_middleware.insert(
41
+ 0,
42
+ mw_instance.process_view,
43
+ )
44
+
45
+ handler = convert_exception_to_response(mw_instance)
46
+
47
+ # We only assign to this when initialization is complete as it is used
48
+ # as a flag for initialization being complete.
49
+ self._middleware_chain = handler
50
+
51
+ def get_response(self, request):
52
+ """Return a Response object for the given HttpRequest."""
53
+ # Setup default url resolver for this thread
54
+ set_urlconf(settings.ROOT_URLCONF)
55
+ response = self._middleware_chain(request)
56
+ response._resource_closers.append(request.close)
57
+ if response.status_code >= 400:
58
+ log_response(
59
+ "%s: %s",
60
+ response.reason_phrase,
61
+ request.path,
62
+ response=response,
63
+ request=request,
64
+ )
65
+ return response
66
+
67
+ def _get_response(self, request):
68
+ """
69
+ Resolve and call the view, then apply view, exception, and
70
+ template_response middleware. This method is everything that happens
71
+ inside the request/response middleware.
72
+ """
73
+ response = None
74
+ callback, callback_args, callback_kwargs = self.resolve_request(request)
75
+
76
+ # Apply view middleware
77
+ for middleware_method in self._view_middleware:
78
+ response = middleware_method(
79
+ request, callback, callback_args, callback_kwargs
80
+ )
81
+ if response:
82
+ break
83
+
84
+ if response is None:
85
+ response = callback(request, *callback_args, **callback_kwargs)
86
+
87
+ # Complain if the view returned None (a common error).
88
+ self.check_response(response, callback)
89
+
90
+ return response
91
+
92
+ def resolve_request(self, request):
93
+ """
94
+ Retrieve/set the urlconf for the request. Return the view resolved,
95
+ with its args and kwargs.
96
+ """
97
+ # Work out the resolver.
98
+ if hasattr(request, "urlconf"):
99
+ urlconf = request.urlconf
100
+ set_urlconf(urlconf)
101
+ resolver = get_resolver(urlconf)
102
+ else:
103
+ resolver = get_resolver()
104
+ # Resolve the view, and assign the match object back to the request.
105
+ resolver_match = resolver.resolve(request.path_info)
106
+ request.resolver_match = resolver_match
107
+ return resolver_match
108
+
109
+ def check_response(self, response, callback, name=None):
110
+ """
111
+ Raise an error if the view returned None or an uncalled coroutine.
112
+ """
113
+ if not name:
114
+ if isinstance(callback, types.FunctionType): # FBV
115
+ name = f"The view {callback.__module__}.{callback.__name__}"
116
+ else: # CBV
117
+ name = "The view {}.{}.__call__".format(
118
+ callback.__module__,
119
+ callback.__class__.__name__,
120
+ )
121
+ if response is None:
122
+ raise ValueError(
123
+ "%s didn't return a Response object. It returned None "
124
+ "instead." % name
125
+ )
126
+
127
+
128
+ def reset_urlconf(sender, **kwargs):
129
+ """Reset the URLconf after each request is finished."""
130
+ set_urlconf(None)
131
+
132
+
133
+ request_finished.connect(reset_urlconf)
@@ -0,0 +1,145 @@
1
+ import logging
2
+ from functools import wraps
3
+
4
+ from plain import signals
5
+ from plain.exceptions import (
6
+ BadRequest,
7
+ PermissionDenied,
8
+ RequestDataTooBig,
9
+ SuspiciousOperation,
10
+ TooManyFieldsSent,
11
+ TooManyFilesSent,
12
+ )
13
+ from plain.http import Http404, ResponseServerError
14
+ from plain.http.multipartparser import MultiPartParserError
15
+ from plain.logs import log_response
16
+ from plain.runtime import settings
17
+ from plain.utils.module_loading import import_string
18
+ from plain.views.errors import ErrorView
19
+
20
+
21
+ def convert_exception_to_response(get_response):
22
+ """
23
+ Wrap the given get_response callable in exception-to-response conversion.
24
+
25
+ All exceptions will be converted. All known 4xx exceptions (Http404,
26
+ PermissionDenied, MultiPartParserError, SuspiciousOperation) will be
27
+ converted to the appropriate response, and all other exceptions will be
28
+ converted to 500 responses.
29
+
30
+ This decorator is automatically applied to all middleware to ensure that
31
+ no middleware leaks an exception and that the next middleware in the stack
32
+ can rely on getting a response instead of an exception.
33
+ """
34
+
35
+ @wraps(get_response)
36
+ def inner(request):
37
+ try:
38
+ response = get_response(request)
39
+ except Exception as exc:
40
+ response = response_for_exception(request, exc)
41
+ return response
42
+
43
+ return inner
44
+
45
+
46
+ def response_for_exception(request, exc):
47
+ if isinstance(exc, Http404):
48
+ response = get_exception_response(request, 404)
49
+
50
+ elif isinstance(exc, PermissionDenied):
51
+ response = get_exception_response(request, 403)
52
+ log_response(
53
+ "Forbidden (Permission denied): %s",
54
+ request.path,
55
+ response=response,
56
+ request=request,
57
+ exception=exc,
58
+ )
59
+
60
+ elif isinstance(exc, MultiPartParserError):
61
+ response = get_exception_response(request, 400)
62
+ log_response(
63
+ "Bad request (Unable to parse request body): %s",
64
+ request.path,
65
+ response=response,
66
+ request=request,
67
+ exception=exc,
68
+ )
69
+
70
+ elif isinstance(exc, BadRequest):
71
+ response = get_exception_response(request, 400)
72
+ log_response(
73
+ "%s: %s",
74
+ str(exc),
75
+ request.path,
76
+ response=response,
77
+ request=request,
78
+ exception=exc,
79
+ )
80
+ elif isinstance(exc, SuspiciousOperation):
81
+ if isinstance(exc, RequestDataTooBig | TooManyFieldsSent | TooManyFilesSent):
82
+ # POST data can't be accessed again, otherwise the original
83
+ # exception would be raised.
84
+ request._mark_post_parse_error()
85
+
86
+ # The request logger receives events for any problematic request
87
+ # The security logger receives events for all SuspiciousOperations
88
+ security_logger = logging.getLogger(
89
+ "plain.security.%s" % exc.__class__.__name__
90
+ )
91
+ security_logger.error(
92
+ str(exc),
93
+ exc_info=exc,
94
+ extra={"status_code": 400, "request": request},
95
+ )
96
+ response = get_exception_response(request, 400)
97
+
98
+ else:
99
+ signals.got_request_exception.send(sender=None, request=request)
100
+ response = get_exception_response(request, 500)
101
+ log_response(
102
+ "%s: %s",
103
+ response.reason_phrase,
104
+ request.path,
105
+ response=response,
106
+ request=request,
107
+ exception=exc,
108
+ )
109
+
110
+ # Force a TemplateResponse to be rendered.
111
+ if not getattr(response, "is_rendered", True) and callable(
112
+ getattr(response, "render", None)
113
+ ):
114
+ response = response.render()
115
+
116
+ return response
117
+
118
+
119
+ def get_exception_response(request, status_code):
120
+ try:
121
+ return get_error_view(status_code)(request)
122
+ except Exception:
123
+ signals.got_request_exception.send(sender=None, request=request)
124
+ return handle_uncaught_exception()
125
+
126
+
127
+ def handle_uncaught_exception():
128
+ """
129
+ Processing for any otherwise uncaught exceptions (those that will
130
+ generate HTTP 500 responses).
131
+ """
132
+ return ResponseServerError()
133
+
134
+
135
+ def get_error_view(status_code):
136
+ views_by_status = settings.HTTP_ERROR_VIEWS
137
+ if status_code in views_by_status:
138
+ view = views_by_status[status_code]
139
+ if isinstance(view, str):
140
+ # Import the view if it's a string
141
+ view = import_string(view)
142
+ return view.as_view()
143
+
144
+ # Create a standard view for any other status code
145
+ return ErrorView.as_view(status_code=status_code)