Plain
Plain is a web framework for building products with Python.
With the core plain
package you can build an app that:
- Matches URL patterns to Python views
- Handles HTTP requests and responses
- Renders HTML templates with Jinja
- Processes user input via forms
- Has a CLI interface
- Serves static assets (CSS, JS, images)
- Can be modified with middleware
- Integrates first-party and third-party packages
- Has a preflight check system
With the official Plain ecosystem packages you can:
- Integrate a full-featured database ORM
- Use a built-in user authentication system
- Lint and format code
- Run a database-backed cache
- Send emails
- Streamline local development
- Manage feature flags
- Integrate HTMX
- Style with Tailwind CSS
- Add OAuth login and API access
- Run tests with pytest
- Run a background job worker
- Build staff tooling and admin dashboards
Learn more at plainframework.com.
1"""
2Global Plain exception and warning classes.
3"""
4import operator
5
6from plain.utils.hashable import make_hashable
7
8
9class FieldDoesNotExist(Exception):
10 """The requested model field does not exist"""
11
12 pass
13
14
15class PackageRegistryNotReady(Exception):
16 """The plain.packages registry is not populated yet"""
17
18 pass
19
20
21class ObjectDoesNotExist(Exception):
22 """The requested object does not exist"""
23
24 silent_variable_failure = True
25
26
27class MultipleObjectsReturned(Exception):
28 """The query returned multiple objects when only one was expected."""
29
30 pass
31
32
33class SuspiciousOperation(Exception):
34 """The user did something suspicious"""
35
36
37class SuspiciousMultipartForm(SuspiciousOperation):
38 """Suspect MIME request in multipart form data"""
39
40 pass
41
42
43class SuspiciousFileOperation(SuspiciousOperation):
44 """A Suspicious filesystem operation was attempted"""
45
46 pass
47
48
49class DisallowedHost(SuspiciousOperation):
50 """HTTP_HOST header contains invalid value"""
51
52 pass
53
54
55class DisallowedRedirect(SuspiciousOperation):
56 """Redirect to scheme not in allowed list"""
57
58 pass
59
60
61class TooManyFieldsSent(SuspiciousOperation):
62 """
63 The number of fields in a GET or POST request exceeded
64 settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.
65 """
66
67 pass
68
69
70class TooManyFilesSent(SuspiciousOperation):
71 """
72 The number of fields in a GET or POST request exceeded
73 settings.DATA_UPLOAD_MAX_NUMBER_FILES.
74 """
75
76 pass
77
78
79class RequestDataTooBig(SuspiciousOperation):
80 """
81 The size of the request (excluding any file uploads) exceeded
82 settings.DATA_UPLOAD_MAX_MEMORY_SIZE.
83 """
84
85 pass
86
87
88class RequestAborted(Exception):
89 """The request was closed before it was completed, or timed out."""
90
91 pass
92
93
94class BadRequest(Exception):
95 """The request is malformed and cannot be processed."""
96
97 pass
98
99
100class PermissionDenied(Exception):
101 """The user did not have permission to do that"""
102
103 pass
104
105
106class ViewDoesNotExist(Exception):
107 """The requested view does not exist"""
108
109 pass
110
111
112class ImproperlyConfigured(Exception):
113 """Plain is somehow improperly configured"""
114
115 pass
116
117
118class FieldError(Exception):
119 """Some kind of problem with a model field."""
120
121 pass
122
123
124NON_FIELD_ERRORS = "__all__"
125
126
127class ValidationError(Exception):
128 """An error while validating data."""
129
130 def __init__(self, message, code=None, params=None):
131 """
132 The `message` argument can be a single error, a list of errors, or a
133 dictionary that maps field names to lists of errors. What we define as
134 an "error" can be either a simple string or an instance of
135 ValidationError with its message attribute set, and what we define as
136 list or dictionary can be an actual `list` or `dict` or an instance
137 of ValidationError with its `error_list` or `error_dict` attribute set.
138 """
139 super().__init__(message, code, params)
140
141 if isinstance(message, ValidationError):
142 if hasattr(message, "error_dict"):
143 message = message.error_dict
144 elif not hasattr(message, "message"):
145 message = message.error_list
146 else:
147 message, code, params = message.message, message.code, message.params
148
149 if isinstance(message, dict):
150 self.error_dict = {}
151 for field, messages in message.items():
152 if not isinstance(messages, ValidationError):
153 messages = ValidationError(messages)
154 self.error_dict[field] = messages.error_list
155
156 elif isinstance(message, list):
157 self.error_list = []
158 for message in message:
159 # Normalize plain strings to instances of ValidationError.
160 if not isinstance(message, ValidationError):
161 message = ValidationError(message)
162 if hasattr(message, "error_dict"):
163 self.error_list.extend(sum(message.error_dict.values(), []))
164 else:
165 self.error_list.extend(message.error_list)
166
167 else:
168 self.message = message
169 self.code = code
170 self.params = params
171 self.error_list = [self]
172
173 @property
174 def message_dict(self):
175 # Trigger an AttributeError if this ValidationError
176 # doesn't have an error_dict.
177 getattr(self, "error_dict")
178
179 return dict(self)
180
181 @property
182 def messages(self):
183 if hasattr(self, "error_dict"):
184 return sum(dict(self).values(), [])
185 return list(self)
186
187 def update_error_dict(self, error_dict):
188 if hasattr(self, "error_dict"):
189 for field, error_list in self.error_dict.items():
190 error_dict.setdefault(field, []).extend(error_list)
191 else:
192 error_dict.setdefault(NON_FIELD_ERRORS, []).extend(self.error_list)
193 return error_dict
194
195 def __iter__(self):
196 if hasattr(self, "error_dict"):
197 for field, errors in self.error_dict.items():
198 yield field, list(ValidationError(errors))
199 else:
200 for error in self.error_list:
201 message = error.message
202 if error.params:
203 message %= error.params
204 yield str(message)
205
206 def __str__(self):
207 if hasattr(self, "error_dict"):
208 return repr(dict(self))
209 return repr(list(self))
210
211 def __repr__(self):
212 return "ValidationError(%s)" % self
213
214 def __eq__(self, other):
215 if not isinstance(other, ValidationError):
216 return NotImplemented
217 return hash(self) == hash(other)
218
219 def __hash__(self):
220 if hasattr(self, "message"):
221 return hash(
222 (
223 self.message,
224 self.code,
225 make_hashable(self.params),
226 )
227 )
228 if hasattr(self, "error_dict"):
229 return hash(make_hashable(self.error_dict))
230 return hash(tuple(sorted(self.error_list, key=operator.attrgetter("message"))))
231
232
233class EmptyResultSet(Exception):
234 """A database query predicate is impossible."""
235
236 pass
237
238
239class FullResultSet(Exception):
240 """A database query predicate is matches everything."""
241
242 pass