plain 0.39.1__py3-none-any.whl → 0.41.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.
plain/csrf/middleware.py CHANGED
@@ -74,9 +74,9 @@ def _unmask_cipher_token(token):
74
74
 
75
75
 
76
76
  def _add_new_csrf_cookie(request):
77
- """Generate a new random CSRF_COOKIE value, and add it to request.META."""
77
+ """Generate a new random CSRF_COOKIE value, and add it to request.meta."""
78
78
  csrf_secret = _get_new_csrf_string()
79
- request.META.update(
79
+ request.meta.update(
80
80
  {
81
81
  "CSRF_COOKIE": csrf_secret,
82
82
  "CSRF_COOKIE_NEEDS_UPDATE": True,
@@ -95,12 +95,12 @@ def get_token(request):
95
95
  header to the outgoing response. For this reason, you may need to use this
96
96
  function lazily, as is done by the csrf context processor.
97
97
  """
98
- if "CSRF_COOKIE" in request.META:
99
- csrf_secret = request.META["CSRF_COOKIE"]
98
+ if "CSRF_COOKIE" in request.meta:
99
+ csrf_secret = request.meta["CSRF_COOKIE"]
100
100
  # Since the cookie is being used, flag to send the cookie in
101
101
  # process_response() (even if the client already has it) in order to
102
102
  # renew the expiry timer.
103
- request.META["CSRF_COOKIE_NEEDS_UPDATE"] = True
103
+ request.meta["CSRF_COOKIE_NEEDS_UPDATE"] = True
104
104
  else:
105
105
  csrf_secret = _add_new_csrf_cookie(request)
106
106
  return _mask_cipher_secret(csrf_secret)
@@ -177,14 +177,14 @@ class CsrfViewMiddleware:
177
177
  # masked, this also causes it to be replaced with the unmasked
178
178
  # form, but only in cases where the secret is already getting
179
179
  # saved anyways.
180
- request.META["CSRF_COOKIE"] = csrf_secret
180
+ request.meta["CSRF_COOKIE"] = csrf_secret
181
181
 
182
182
  if csrf_response := self._get_csrf_response(request):
183
183
  return csrf_response
184
184
 
185
185
  response = self.get_response(request)
186
186
 
187
- if request.META.get("CSRF_COOKIE_NEEDS_UPDATE"):
187
+ if request.meta.get("CSRF_COOKIE_NEEDS_UPDATE"):
188
188
  self._set_csrf_cookie(request, response)
189
189
  # Unset the flag to prevent _set_csrf_cookie() from being
190
190
  # unnecessarily called again in process_response() by other
@@ -193,7 +193,7 @@ class CsrfViewMiddleware:
193
193
  # CSRF_COOKIE_NEEDS_UPDATE is still respected in subsequent calls
194
194
  # e.g. in case rotate_token() is called in process_response() later
195
195
  # by custom middleware but before those subsequent calls.
196
- request.META["CSRF_COOKIE_NEEDS_UPDATE"] = False
196
+ request.meta["CSRF_COOKIE_NEEDS_UPDATE"] = False
197
197
 
198
198
  return response
199
199
 
@@ -246,7 +246,7 @@ class CsrfViewMiddleware:
246
246
  the request's secret has invalid characters or an invalid length.
247
247
  """
248
248
  try:
249
- csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
249
+ csrf_secret = request.cookies[settings.CSRF_COOKIE_NAME]
250
250
  except KeyError:
251
251
  csrf_secret = None
252
252
  else:
@@ -260,7 +260,7 @@ class CsrfViewMiddleware:
260
260
  def _set_csrf_cookie(self, request, response):
261
261
  response.set_cookie(
262
262
  settings.CSRF_COOKIE_NAME,
263
- request.META["CSRF_COOKIE"],
263
+ request.meta["CSRF_COOKIE"],
264
264
  max_age=settings.CSRF_COOKIE_AGE,
265
265
  domain=settings.CSRF_COOKIE_DOMAIN,
266
266
  path=settings.CSRF_COOKIE_PATH,
@@ -272,7 +272,7 @@ class CsrfViewMiddleware:
272
272
  patch_vary_headers(response, ("Cookie",))
273
273
 
274
274
  def _origin_verified(self, request):
275
- request_origin = request.META["HTTP_ORIGIN"]
275
+ request_origin = request.meta["HTTP_ORIGIN"]
276
276
  try:
277
277
  good_host = request.get_host()
278
278
  except DisallowedHost:
@@ -298,7 +298,7 @@ class CsrfViewMiddleware:
298
298
  )
299
299
 
300
300
  def _check_referer(self, request):
301
- referer = request.META.get("HTTP_REFERER")
301
+ referer = request.meta.get("HTTP_REFERER")
302
302
  if referer is None:
303
303
  raise RejectRequest(REASON_NO_REFERER)
304
304
 
@@ -364,7 +364,7 @@ class CsrfViewMiddleware:
364
364
  request_csrf_token = ""
365
365
  if request.method == "POST":
366
366
  try:
367
- request_csrf_token = request.POST.get(settings.CSRF_POST_NAME, "")
367
+ request_csrf_token = request.data.get(settings.CSRF_FIELD_NAME, "")
368
368
  except UnreadablePostError:
369
369
  # Handle a broken connection before we've completed reading the
370
370
  # POST data. process_view shouldn't raise any exceptions, so
@@ -398,7 +398,7 @@ class CsrfViewMiddleware:
398
398
  raise RejectRequest(reason)
399
399
 
400
400
  def _get_csrf_response(self, request):
401
- # Wait until request.META["CSRF_COOKIE"] has been manipulated before
401
+ # Wait until request.meta["CSRF_COOKIE"] has been manipulated before
402
402
  # bailing out, so that get_token still works
403
403
  if getattr(request, "csrf_exempt", True):
404
404
  return None
@@ -409,10 +409,10 @@ class CsrfViewMiddleware:
409
409
 
410
410
  # Reject the request if the Origin header doesn't match an allowed
411
411
  # value.
412
- if "HTTP_ORIGIN" in request.META:
412
+ if "HTTP_ORIGIN" in request.meta:
413
413
  if not self._origin_verified(request):
414
414
  return self._reject(
415
- request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
415
+ request, REASON_BAD_ORIGIN % request.meta["HTTP_ORIGIN"]
416
416
  )
417
417
  elif request.is_https():
418
418
  # If the Origin header wasn't provided, reject HTTPS requests if
plain/forms/forms.py CHANGED
@@ -3,10 +3,8 @@ Form classes
3
3
  """
4
4
 
5
5
  import copy
6
- import json
7
6
 
8
7
  from plain.exceptions import NON_FIELD_ERRORS
9
- from plain.utils.datastructures import MultiValueDict
10
8
  from plain.utils.functional import cached_property
11
9
 
12
10
  from .exceptions import ValidationError
@@ -64,27 +62,12 @@ class BaseForm:
64
62
  prefix=None,
65
63
  initial=None,
66
64
  ):
67
- if request.method in ("POST", "PUT", "PATCH"):
68
- if request.headers.get("Content-Type") == "application/json":
69
- self.data = json.loads(request.body)
70
- self.is_json_request = True
71
- elif request.headers.get("Content-Type") in [
72
- "application/x-www-form-urlencoded",
73
- "multipart/form-data",
74
- ]:
75
- self.data = request.POST
76
- self.is_json_request = False
77
- else:
78
- raise ValueError(
79
- "Unsupported Content-Type. Supported types are "
80
- "'application/json', 'application/x-www-form-urlencoded', "
81
- "and 'multipart/form-data'."
82
- )
83
- else:
84
- self.data = MultiValueDict()
85
- self.is_json_request = False
65
+ self.data = request.data
66
+ self.files = request.files
86
67
 
87
- self.files = request.FILES
68
+ self.is_json_request = request.headers.get("Content-Type", "").startswith(
69
+ "application/json"
70
+ )
88
71
 
89
72
  self.is_bound = bool(self.data or self.files)
90
73
 
plain/http/README.md CHANGED
@@ -14,7 +14,7 @@ class ExampleView(View):
14
14
  print(self.request.headers.get("Example-Header"))
15
15
 
16
16
  # Accessing a query parameter
17
- print(self.request.GET.get("example"))
17
+ print(self.request.query_params.get("example"))
18
18
 
19
19
  # Creating a response
20
20
  response = Response("Hello, world!", status_code=200)
@@ -54,12 +54,12 @@ class MultiPartParser:
54
54
 
55
55
  boundary_re = _lazy_re_compile(r"[ -~]{0,200}[!-~]")
56
56
 
57
- def __init__(self, META, input_data, upload_handlers, encoding=None):
57
+ def __init__(self, meta, input_data, upload_handlers, encoding=None):
58
58
  """
59
59
  Initialize the MultiPartParser object.
60
60
 
61
- :META:
62
- The standard ``META`` dictionary in Plain request objects.
61
+ :meta:
62
+ The standard ``meta`` dictionary in Plain request objects.
63
63
  :input_data:
64
64
  The raw post data, as a file-like object.
65
65
  :upload_handlers:
@@ -69,7 +69,7 @@ class MultiPartParser:
69
69
  The encoding with which to treat the incoming data.
70
70
  """
71
71
  # Content-Type should contain multipart and the boundary information.
72
- content_type = META.get("CONTENT_TYPE", "")
72
+ content_type = meta.get("CONTENT_TYPE", "")
73
73
  if not content_type.startswith("multipart/"):
74
74
  raise MultiPartParserError(f"Invalid Content-Type: {content_type}")
75
75
 
@@ -91,7 +91,7 @@ class MultiPartParser:
91
91
  # Content-Length should contain the length of the body we are about
92
92
  # to receive.
93
93
  try:
94
- content_length = int(META.get("CONTENT_LENGTH", 0))
94
+ content_length = int(meta.get("CONTENT_LENGTH", 0))
95
95
  except (ValueError, TypeError):
96
96
  content_length = 0
97
97
 
@@ -107,7 +107,7 @@ class MultiPartParser:
107
107
  possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
108
108
  self._chunk_size = min([2**31 - 4] + possible_sizes)
109
109
 
110
- self._meta = META
110
+ self._meta = meta
111
111
  self._encoding = encoding or settings.DEFAULT_CHARSET
112
112
  self._content_length = content_length
113
113
  self._upload_handlers = upload_handlers
plain/http/request.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import codecs
2
2
  import copy
3
+ import json
3
4
  import uuid
4
5
  from io import BytesIO
5
6
  from itertools import chain
@@ -64,11 +65,11 @@ class HttpRequest:
64
65
  # A unique ID we can use to trace this request
65
66
  self.unique_id = str(uuid.uuid4())
66
67
 
67
- self.GET = QueryDict(mutable=True)
68
- self.POST = QueryDict(mutable=True)
69
- self.COOKIES = {}
70
- self.META = {}
71
- self.FILES = MultiValueDict()
68
+ self.query_params = QueryDict(mutable=True)
69
+ self.data = QueryDict(mutable=True)
70
+ self.cookies = {}
71
+ self.meta = {}
72
+ self.files = MultiValueDict()
72
73
 
73
74
  self.path = ""
74
75
  self.path_info = ""
@@ -99,7 +100,7 @@ class HttpRequest:
99
100
 
100
101
  @cached_property
101
102
  def headers(self):
102
- return HttpHeaders(self.META)
103
+ return HttpHeaders(self.meta)
103
104
 
104
105
  @cached_property
105
106
  def accepted_types(self):
@@ -130,13 +131,13 @@ class HttpRequest:
130
131
  allowed hosts protection, so may return an insecure host.
131
132
  """
132
133
  # We try three options, in order of decreasing preference.
133
- if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.META):
134
- host = self.META["HTTP_X_FORWARDED_HOST"]
135
- elif "HTTP_HOST" in self.META:
136
- host = self.META["HTTP_HOST"]
134
+ if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.meta):
135
+ host = self.meta["HTTP_X_FORWARDED_HOST"]
136
+ elif "HTTP_HOST" in self.meta:
137
+ host = self.meta["HTTP_HOST"]
137
138
  else:
138
139
  # Reconstruct the host using the algorithm from PEP 333.
139
- host = self.META["SERVER_NAME"]
140
+ host = self.meta["SERVER_NAME"]
140
141
  server_port = self.get_port()
141
142
  if server_port != ("443" if self.is_https() else "80"):
142
143
  host = f"{host}:{server_port}"
@@ -166,10 +167,10 @@ class HttpRequest:
166
167
 
167
168
  def get_port(self):
168
169
  """Return the port number for the request as a string."""
169
- if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.META:
170
- port = self.META["HTTP_X_FORWARDED_PORT"]
170
+ if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.meta:
171
+ port = self.meta["HTTP_X_FORWARDED_PORT"]
171
172
  else:
172
- port = self.META["SERVER_PORT"]
173
+ port = self.meta["SERVER_PORT"]
173
174
  return str(port)
174
175
 
175
176
  def get_full_path(self, force_append_slash=False):
@@ -198,8 +199,8 @@ class HttpRequest:
198
199
  return "{}{}{}".format(
199
200
  escape_uri_path(path),
200
201
  "/" if force_append_slash and not path.endswith("/") else "",
201
- ("?" + iri_to_uri(self.META.get("QUERY_STRING", "")))
202
- if self.META.get("QUERY_STRING", "")
202
+ ("?" + iri_to_uri(self.meta.get("QUERY_STRING", "")))
203
+ if self.meta.get("QUERY_STRING", "")
203
204
  else "",
204
205
  )
205
206
 
@@ -263,7 +264,7 @@ class HttpRequest:
263
264
  "The HTTPS_PROXY_HEADER setting must be a tuple containing "
264
265
  "two values."
265
266
  )
266
- header_value = self.META.get(header)
267
+ header_value = self.meta.get(header)
267
268
  if header_value is not None:
268
269
  header_value, *_ = header_value.split(",", 1)
269
270
  return "https" if header_value.strip() == secure_value else "http"
@@ -279,15 +280,15 @@ class HttpRequest:
279
280
  @encoding.setter
280
281
  def encoding(self, val):
281
282
  """
282
- Set the encoding used for GET/POST accesses. If the GET or POST
283
+ Set the encoding used for query_params/data accesses. If the query_params or data
283
284
  dictionary has already been created, remove and recreate it on the
284
285
  next access (so that it is decoded correctly).
285
286
  """
286
287
  self._encoding = val
287
- if hasattr(self, "GET"):
288
- del self.GET
289
- if hasattr(self, "_post"):
290
- del self._post
288
+ if hasattr(self, "query_params"):
289
+ del self.query_params
290
+ if hasattr(self, "_data"):
291
+ del self._data
291
292
 
292
293
  def _initialize_handlers(self):
293
294
  self._upload_handlers = [
@@ -311,15 +312,15 @@ class HttpRequest:
311
312
  )
312
313
  self._upload_handlers = upload_handlers
313
314
 
314
- def parse_file_upload(self, META, post_data):
315
- """Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
315
+ def parse_file_upload(self, meta, post_data):
316
+ """Return a tuple of (data QueryDict, files MultiValueDict)."""
316
317
  self.upload_handlers = ImmutableList(
317
318
  self.upload_handlers,
318
319
  warning=(
319
320
  "You cannot alter upload handlers after the upload has been processed."
320
321
  ),
321
322
  )
322
- parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding)
323
+ parser = MultiPartParser(meta, post_data, self.upload_handlers, self.encoding)
323
324
  return parser.parse()
324
325
 
325
326
  @property
@@ -333,7 +334,7 @@ class HttpRequest:
333
334
  # Limit the maximum request data size that will be handled in-memory.
334
335
  if (
335
336
  settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
336
- and int(self.META.get("CONTENT_LENGTH") or 0)
337
+ and int(self.meta.get("CONTENT_LENGTH") or 0)
337
338
  > settings.DATA_UPLOAD_MAX_MEMORY_SIZE
338
339
  ):
339
340
  raise RequestDataTooBig(
@@ -350,29 +351,31 @@ class HttpRequest:
350
351
  return self._body
351
352
 
352
353
  def _mark_post_parse_error(self):
353
- self._post = QueryDict()
354
+ self._data = QueryDict()
354
355
  self._files = MultiValueDict()
355
356
 
356
- def _load_post_and_files(self):
357
- """Populate self._post and self._files if the content-type is a form type"""
358
- if self.method != "POST":
359
- self._post, self._files = (
360
- QueryDict(encoding=self._encoding),
361
- MultiValueDict(),
362
- )
363
- return
357
+ def _load_data_and_files(self):
358
+ """Populate self._data and self._files"""
359
+
364
360
  if self._read_started and not hasattr(self, "_body"):
365
361
  self._mark_post_parse_error()
366
362
  return
367
363
 
368
- if self.content_type == "multipart/form-data":
364
+ if self.content_type.startswith("application/json"):
365
+ try:
366
+ self._data = json.loads(self.body)
367
+ self._files = MultiValueDict()
368
+ except json.JSONDecodeError:
369
+ self._mark_post_parse_error()
370
+ raise
371
+ elif self.content_type == "multipart/form-data":
369
372
  if hasattr(self, "_body"):
370
373
  # Use already read data
371
374
  data = BytesIO(self._body)
372
375
  else:
373
376
  data = self
374
377
  try:
375
- self._post, self._files = self.parse_file_upload(self.META, data)
378
+ self._data, self._files = self.parse_file_upload(self.meta, data)
376
379
  except (MultiPartParserError, TooManyFilesSent):
377
380
  # An error occurred while parsing POST data. Since when
378
381
  # formatting the error the request handler might access
@@ -381,12 +384,12 @@ class HttpRequest:
381
384
  self._mark_post_parse_error()
382
385
  raise
383
386
  elif self.content_type == "application/x-www-form-urlencoded":
384
- self._post, self._files = (
387
+ self._data, self._files = (
385
388
  QueryDict(self.body, encoding=self._encoding),
386
389
  MultiValueDict(),
387
390
  )
388
391
  else:
389
- self._post, self._files = (
392
+ self._data, self._files = (
390
393
  QueryDict(encoding=self._encoding),
391
394
  MultiValueDict(),
392
395
  )
@@ -400,7 +403,7 @@ class HttpRequest:
400
403
  #
401
404
  # Expects self._stream to be set to an appropriate source of bytes by
402
405
  # a corresponding request subclass (e.g. WSGIRequest).
403
- # Also when request data has already been read by request.POST or
406
+ # Also when request data has already been read by request.data or
404
407
  # request.body, self._stream points to a BytesIO instance
405
408
  # containing that data.
406
409
 
@@ -85,7 +85,7 @@ class FileUploadHandler:
85
85
  self.request = request
86
86
 
87
87
  def handle_raw_input(
88
- self, input_data, META, content_length, boundary, encoding=None
88
+ self, input_data, meta, content_length, boundary, encoding=None
89
89
  ):
90
90
  """
91
91
  Handle the raw input from the client.
@@ -94,8 +94,8 @@ class FileUploadHandler:
94
94
 
95
95
  :input_data:
96
96
  An object that supports reading via .read().
97
- :META:
98
- ``request.META``.
97
+ :meta:
98
+ ``request.meta``.
99
99
  :content_length:
100
100
  The (integer) value of the Content-Length header from the
101
101
  client.
@@ -199,7 +199,7 @@ class MemoryFileUploadHandler(FileUploadHandler):
199
199
  """
200
200
 
201
201
  def handle_raw_input(
202
- self, input_data, META, content_length, boundary, encoding=None
202
+ self, input_data, meta, content_length, boundary, encoding=None
203
203
  ):
204
204
  """
205
205
  Use the content_length to signal whether or not this handler should be
@@ -80,12 +80,12 @@ class BaseHandler:
80
80
  """
81
81
  resolver_match = self.resolve_request(request)
82
82
 
83
- response = resolver_match.func(
83
+ response = resolver_match.view(
84
84
  request, *resolver_match.args, **resolver_match.kwargs
85
85
  )
86
86
 
87
87
  # Complain if the view returned None (a common error).
88
- self.check_response(response, resolver_match.func)
88
+ self.check_response(response, resolver_match.view)
89
89
 
90
90
  return response
91
91
 
@@ -72,9 +72,9 @@ class WSGIRequest(HttpRequest):
72
72
  self.path = "{}/{}".format(
73
73
  script_name.rstrip("/"), path_info.replace("/", "", 1)
74
74
  )
75
- self.META = environ
76
- self.META["PATH_INFO"] = path_info
77
- self.META["SCRIPT_NAME"] = script_name
75
+ self.meta = environ
76
+ self.meta["PATH_INFO"] = path_info
77
+ self.meta["SCRIPT_NAME"] = script_name
78
78
  self.method = environ["REQUEST_METHOD"].upper()
79
79
  # Set content_type, content_params, and encoding.
80
80
  self._set_content_type_params(environ)
@@ -89,39 +89,39 @@ class WSGIRequest(HttpRequest):
89
89
  def __getstate__(self):
90
90
  state = super().__getstate__()
91
91
  for attr in self.meta_non_picklable_attrs:
92
- if attr in state["META"]:
93
- del state["META"][attr]
92
+ if attr in state["meta"]:
93
+ del state["meta"][attr]
94
94
  return state
95
95
 
96
96
  def _get_scheme(self):
97
97
  return self.environ.get("wsgi.url_scheme")
98
98
 
99
99
  @cached_property
100
- def GET(self):
100
+ def query_params(self):
101
101
  # The WSGI spec says 'QUERY_STRING' may be absent.
102
102
  raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "")
103
103
  return QueryDict(raw_query_string, encoding=self._encoding)
104
104
 
105
- def _get_post(self):
106
- if not hasattr(self, "_post"):
107
- self._load_post_and_files()
108
- return self._post
105
+ def _get_data(self):
106
+ if not hasattr(self, "_data"):
107
+ self._load_data_and_files()
108
+ return self._data
109
109
 
110
- def _set_post(self, post):
111
- self._post = post
110
+ def _set_data(self, data):
111
+ self._data = data
112
112
 
113
113
  @cached_property
114
- def COOKIES(self):
114
+ def cookies(self):
115
115
  raw_cookie = get_str_from_wsgi(self.environ, "HTTP_COOKIE", "")
116
116
  return parse_cookie(raw_cookie)
117
117
 
118
118
  @property
119
- def FILES(self):
119
+ def files(self):
120
120
  if not hasattr(self, "_files"):
121
- self._load_post_and_files()
121
+ self._load_data_and_files()
122
122
  return self._files
123
123
 
124
- POST = property(_get_post, _set_post)
124
+ data = property(_get_data, _set_data)
125
125
 
126
126
 
127
127
  class WSGIHandler(base.BaseHandler):
@@ -134,7 +134,7 @@ CSRF_COOKIE_SECURE = True
134
134
  CSRF_COOKIE_HTTPONLY = False
135
135
  CSRF_COOKIE_SAMESITE = "Lax"
136
136
  CSRF_HEADER_NAME = "CSRF-Token"
137
- CSRF_POST_NAME = "_csrftoken"
137
+ CSRF_FIELD_NAME = "_csrftoken"
138
138
  CSRF_TRUSTED_ORIGINS: list[str] = []
139
139
 
140
140
  ###########
plain/urls/patterns.py CHANGED
@@ -247,12 +247,11 @@ class URLPattern:
247
247
  from .resolvers import ResolverMatch
248
248
 
249
249
  return ResolverMatch(
250
- self.view,
251
- args,
252
- captured_kwargs,
253
- self.pattern.name,
250
+ view=self.view,
251
+ args=args,
252
+ kwargs=captured_kwargs,
253
+ url_name=self.pattern.name,
254
254
  route=str(self.pattern),
255
- captured_kwargs=captured_kwargs,
256
255
  )
257
256
 
258
257
  @cached_property
plain/urls/resolvers.py CHANGED
@@ -8,7 +8,6 @@ attributes of the resolved URL match.
8
8
 
9
9
  import functools
10
10
  import re
11
- from pickle import PicklingError
12
11
  from threading import local
13
12
  from urllib.parse import quote
14
13
 
@@ -26,66 +25,29 @@ from .patterns import RegexPattern, URLPattern
26
25
  class ResolverMatch:
27
26
  def __init__(
28
27
  self,
29
- func,
28
+ *,
29
+ view,
30
30
  args,
31
31
  kwargs,
32
32
  url_name=None,
33
33
  namespaces=None,
34
34
  route=None,
35
- tried=None,
36
- captured_kwargs=None,
37
- extra_kwargs=None,
38
35
  ):
39
- self.func = func
36
+ self.view = view
40
37
  self.args = args
41
38
  self.kwargs = kwargs
42
39
  self.url_name = url_name
43
40
  self.route = route
44
- self.tried = tried
45
- self.captured_kwargs = captured_kwargs
46
- self.extra_kwargs = extra_kwargs
47
41
 
48
42
  # If a URLRegexResolver doesn't have a namespace or namespace, it passes
49
43
  # in an empty value.
50
44
  self.namespaces = [x for x in namespaces if x] if namespaces else []
51
45
  self.namespace = ":".join(self.namespaces)
52
46
 
53
- if hasattr(func, "view_class"):
54
- func = func.view_class
55
- if not hasattr(func, "__name__"):
56
- # A class-based view
57
- self._func_path = func.__class__.__module__ + "." + func.__class__.__name__
58
- else:
59
- # A function-based view
60
- self._func_path = func.__module__ + "." + func.__name__
61
-
62
- view_path = url_name or self._func_path
63
- self.view_name = ":".join(self.namespaces + [view_path])
64
-
65
- def __repr__(self):
66
- if isinstance(self.func, functools.partial):
67
- func = repr(self.func)
68
- else:
69
- func = self._func_path
70
- return (
71
- "ResolverMatch(func={}, args={!r}, kwargs={!r}, url_name={!r}, "
72
- "namespaces={!r}, route={!r}{}{})".format(
73
- func,
74
- self.args,
75
- self.kwargs,
76
- self.url_name,
77
- self.namespaces,
78
- self.route,
79
- f", captured_kwargs={self.captured_kwargs!r}"
80
- if self.captured_kwargs
81
- else "",
82
- f", extra_kwargs={self.extra_kwargs!r}" if self.extra_kwargs else "",
83
- )
47
+ self.namespaced_url_name = (
48
+ ":".join(self.namespaces + [url_name]) if url_name else None
84
49
  )
85
50
 
86
- def __reduce_ex__(self, protocol):
87
- raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
88
-
89
51
 
90
52
  def get_resolver(router=None):
91
53
  if router is None:
@@ -257,13 +219,6 @@ class URLResolver:
257
219
  self._populate()
258
220
  return self._app_dict
259
221
 
260
- @staticmethod
261
- def _extend_tried(tried, pattern, sub_tried=None):
262
- if sub_tried is None:
263
- tried.append([pattern])
264
- else:
265
- tried.extend([pattern, *t] for t in sub_tried)
266
-
267
222
  @staticmethod
268
223
  def _join_route(route1, route2):
269
224
  """Join two routes, without the starting ^ in the second route."""
@@ -274,15 +229,14 @@ class URLResolver:
274
229
 
275
230
  def resolve(self, path):
276
231
  path = str(path) # path may be a reverse_lazy object
277
- tried = []
278
232
  match = self.pattern.match(path)
279
233
  if match:
280
234
  new_path, args, kwargs = match
281
235
  for pattern in self.url_patterns:
282
236
  try:
283
237
  sub_match = pattern.resolve(new_path)
284
- except Resolver404 as e:
285
- self._extend_tried(tried, pattern, e.args[0].get("tried"))
238
+ except Resolver404:
239
+ pass
286
240
  else:
287
241
  if sub_match:
288
242
  # Merge captured arguments in match with submatch
@@ -299,20 +253,15 @@ class URLResolver:
299
253
  if isinstance(pattern, URLPattern)
300
254
  else str(pattern.pattern)
301
255
  )
302
- self._extend_tried(tried, pattern, sub_match.tried)
303
256
  return ResolverMatch(
304
- sub_match.func,
305
- sub_match_args,
306
- sub_match_dict,
307
- sub_match.url_name,
308
- [self.namespace] + sub_match.namespaces,
309
- self._join_route(current_route, sub_match.route),
310
- tried,
311
- captured_kwargs=sub_match.captured_kwargs,
312
- extra_kwargs=sub_match.extra_kwargs,
257
+ view=sub_match.view,
258
+ args=sub_match_args,
259
+ kwargs=sub_match_dict,
260
+ url_name=sub_match.url_name,
261
+ namespaces=[self.namespace] + sub_match.namespaces,
262
+ route=self._join_route(current_route, sub_match.route),
313
263
  )
314
- tried.append([pattern])
315
- raise Resolver404({"tried": tried, "path": new_path})
264
+ raise Resolver404({"path": new_path})
316
265
  raise Resolver404({"path": path})
317
266
 
318
267
  def reverse(self, lookup_view, *args, **kwargs):
plain/views/redirect.py CHANGED
@@ -38,7 +38,7 @@ class RedirectView(View):
38
38
  else:
39
39
  return None
40
40
 
41
- args = self.request.META.get("QUERY_STRING", "")
41
+ args = self.request.meta.get("QUERY_STRING", "")
42
42
  if args and self.query_string:
43
43
  url = f"{url}?{args}"
44
44
  return url
plain/views/templates.py CHANGED
@@ -12,7 +12,7 @@ from .base import View
12
12
  def csrf_input(request):
13
13
  return format_html(
14
14
  '<input type="hidden" name="{}" value="{}">',
15
- settings.CSRF_POST_NAME,
15
+ settings.CSRF_FIELD_NAME,
16
16
  get_token(request),
17
17
  )
18
18
 
@@ -35,6 +35,7 @@ class TemplateView(View):
35
35
  def get_template_context(self) -> dict:
36
36
  return {
37
37
  "request": self.request,
38
+ "template_names": self.get_template_names(),
38
39
  "csrf_input": csrf_input_lazy(self.request),
39
40
  "csrf_token": csrf_token_lazy(self.request),
40
41
  "DEBUG": settings.DEBUG,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.39.1
3
+ Version: 0.41.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -34,19 +34,19 @@ plain/cli/startup.py,sha256=wLaFuyUb4ewWhtehBCGicrRCXIIGCRbeCT3ce9hUv-A,1022
34
34
  plain/cli/urls.py,sha256=CS9NFpwZBWveAR8F3YsoUNySDEK_PwF73oSgLDfkOdI,3776
35
35
  plain/cli/utils.py,sha256=VwlIh0z7XxzVV8I3qM2kZo07fkJFPoeeVZa1ODG616k,258
36
36
  plain/csrf/README.md,sha256=nxCpPk1HF5eAM-7paxg9D-9RVCU9jXsSPAVHkJvA_DU,717
37
- plain/csrf/middleware.py,sha256=bn8KOE45aucSam0m3F9g8FVfnmjjt-6jxS6iSNwHamU,17413
37
+ plain/csrf/middleware.py,sha256=MJusxqfa_SVpzHchfkTOCzccCl8twT8Cr_ymxLb5EvY,17414
38
38
  plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
39
39
  plain/forms/README.md,sha256=TawBdy1jP0-8HUsfUzd7vvgkwl3EJGejDxFhWR8F-80,2242
40
40
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
41
41
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
42
42
  plain/forms/exceptions.py,sha256=NYk1wjYDkk-lA_XMJQDItOebQcL_m_r2eNRc2mkLQkg,315
43
43
  plain/forms/fields.py,sha256=C71ed7m8VBnsgS-eIC6Xsmz7ruNSkdGa1BuSo7V3QKc,34574
44
- plain/forms/forms.py,sha256=Zcl5euWWxuE-CTcOHsOVYixWESdVk7no2Sz40AxmudY,11204
45
- plain/http/README.md,sha256=G662f56feQWiEHw9tTncZqK8kNB_nbR1HTGeOR1ygNM,713
44
+ plain/forms/forms.py,sha256=gyDUrl7wh7n3fqKMB6eH0-AvCmWXST3oc-jBWUf9rmU,10454
45
+ plain/http/README.md,sha256=F9wbahgSU3jIDEG14gJjdPJRem4weUNvwnwhb7o3cu0,722
46
46
  plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
47
47
  plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
48
- plain/http/multipartparser.py,sha256=Cyk_UZhxf8JwNza_Yl4_nKCYkmnG7xY9PSVcf9Us57U,27266
49
- plain/http/request.py,sha256=kq3AuM0EWyAD_kqMlorTccm5mzIQ6ZefkCa-jXUntnI,25514
48
+ plain/http/multipartparser.py,sha256=Z1dFJNAd8N5RHUuF67jh1jBfZOFepORsre_3ee6CgOQ,27266
49
+ plain/http/request.py,sha256=Xlolhpp2ZfZIi0TOz960U16CdqGi8jMhmHsBtxpkR_4,25631
50
50
  plain/http/response.py,sha256=IGSg22ioob6kSRkKGQsQF_exb2Lch8RKiZr_Xoi1_xA,23644
51
51
  plain/internal/__init__.py,sha256=fVBaYLCXEQc-7riHMSlw3vMTTuF7-0Bj2I8aGzv0o0w,171
52
52
  plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
@@ -55,12 +55,12 @@ plain/internal/files/locks.py,sha256=z03q7IZD4tPMK3s1HKF3w_uetkFj6w6FTheLUxZsfB0
55
55
  plain/internal/files/move.py,sha256=jfdD29QhamxZjXRgqmZS4dJoJ4sK6M7QK1Km-69jWeo,3238
56
56
  plain/internal/files/temp.py,sha256=UJJnCI8dqPIC8XXHU3-jG2-0svbkrgGlBs4yhciLm4c,2506
57
57
  plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uFPIxvl7c,4192
58
- plain/internal/files/uploadhandler.py,sha256=eEnd5onstypjHYtg367PnVWwCaF1kAPlLPSV7goIf_E,7198
58
+ plain/internal/files/uploadhandler.py,sha256=63_QUwAwfq3bevw79i0S7zt2EB2UBoO7MaauvezaVMY,7198
59
59
  plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
60
60
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- plain/internal/handlers/base.py,sha256=oRKni79ATI_u7sywGFExrzKvP5dpJTqIp1m521A90Ew,4169
61
+ plain/internal/handlers/base.py,sha256=6K8wd5DojOYA-UEQTd1jxdDggm5ThHXQahVcuJqd2qo,4169
62
62
  plain/internal/handlers/exception.py,sha256=vfha_6-fz6S6VYCP1PMBfue2Gw-_th6jqaTE372fGlw,4809
63
- plain/internal/handlers/wsgi.py,sha256=aOGCd9hJEMTVMGfgIDlSFvevd8_XCzZa2dtlR4peqZg,8253
63
+ plain/internal/handlers/wsgi.py,sha256=sOgqnE4fNzKD2a5EMubiEpHEK9Mi69FWx3BVWxGgMw0,8262
64
64
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
65
65
  plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
66
66
  plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
@@ -83,7 +83,7 @@ plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2
83
83
  plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
84
84
  plain/runtime/README.md,sha256=S_FIOmSq8LkVQHh9Xm6s3EJWKTVdlSr5A_bNXgh02X8,4740
85
85
  plain/runtime/__init__.py,sha256=o2RVETiL8U0lMFBpbtfnxflhw_4MFllMV6CEpX3RqZs,1965
86
- plain/runtime/global_settings.py,sha256=gg9XhMJmzbQqfxzst5JaI0hDIzPvTwX5M0AonJk2ISY,5579
86
+ plain/runtime/global_settings.py,sha256=Vi7jabMF-SBolkP5qx1nLnGoDt_kD1KT_wgtbjNlPHw,5580
87
87
  plain/runtime/user_settings.py,sha256=uRHHVfzUvHon91_fOKj7K2WaBYwJ1gCPLfeXqKj5CTs,10902
88
88
  plain/signals/README.md,sha256=FInfJXdVQkb7u93PvD8XgPbz_f6m0l2xIu_4PyttV1E,234
89
89
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
@@ -107,8 +107,8 @@ plain/urls/README.md,sha256=ijFGmrkUY9buBqO_i1GZaN3V55vl9xwEADtHOx_ZHPY,3724
107
107
  plain/urls/__init__.py,sha256=DFO2OL1IllHW5USPIb5uYvvzf_G-Bl0Qu1zrRLHmWyM,542
108
108
  plain/urls/converters.py,sha256=s2JZVOdzZC16lgobsI93hygcdH5L0Kj4742WEkXsVcs,1193
109
109
  plain/urls/exceptions.py,sha256=q4iPh3Aa-zHbA-tw8v6WyX1J1n5WdAady2xvxFuyXB0,114
110
- plain/urls/patterns.py,sha256=jTfZ4hLfUhp9DR3UGA1cMog36DDheLSccSW8fGcirRU,9352
111
- plain/urls/resolvers.py,sha256=PyqbO1JIoJq2ayCSmONW_6O8a3vM7cTVbqQJdCJHIK0,15218
110
+ plain/urls/patterns.py,sha256=cvWF9vY8sKPVXyalFHZSX0d6rP4oVffov0Fdu2thyfk,9329
111
+ plain/urls/resolvers.py,sha256=3I10pLpLvhL4NamABJGH8e5Se6Iqa2y7V6hMAFQxVSg,13264
112
112
  plain/urls/routers.py,sha256=iEsQtTpPNDDVn7r_BQX84FESGSjOeD5qgyO_ep5rzaU,2819
113
113
  plain/urls/utils.py,sha256=WiGq6hHI-5DLFOxCQTAZ2qm0J-UdGosLcjuxlfK6_Tg,2137
114
114
  plain/utils/README.md,sha256=hRRkcg4CxMX-zz8d4Bn6V2uJr_VKgTLurc1jY7QlEx8,198
@@ -144,10 +144,10 @@ plain/views/errors.py,sha256=jbNCJIzowwCsEvqyJ3opMeZpPDqTyhtrbqb0VnAm2HE,1263
144
144
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
145
145
  plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
146
146
  plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
147
- plain/views/redirect.py,sha256=vMXx8430FtyKcT0V0gyY92SkLtyULBX52KhX4eu4gEA,1985
148
- plain/views/templates.py,sha256=kMcHKkKNvucF91SFGkaq-ugjrCwn4zJBpFV1JkwA544,2027
149
- plain-0.39.1.dist-info/METADATA,sha256=e8tEYf656PoEDJU624eCgDYMAT9_Jc1ewukJa45-_7g,4297
150
- plain-0.39.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
- plain-0.39.1.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
152
- plain-0.39.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
153
- plain-0.39.1.dist-info/RECORD,,
147
+ plain/views/redirect.py,sha256=daq2cQIkdDF78bt43sjuZxRAyJm_t_SKw6tyPmiXPIc,1985
148
+ plain/views/templates.py,sha256=SU1fO9gVMp-gEQHYeFplxvmgeMyrLgT8MJ12WNVmQC8,2085
149
+ plain-0.41.0.dist-info/METADATA,sha256=-LiS6PzInw1Ji5WpAs6BCpEG3qfck2KqenAJUrVJoXI,4297
150
+ plain-0.41.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
+ plain-0.41.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
152
+ plain-0.41.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
153
+ plain-0.41.0.dist-info/RECORD,,
File without changes