plain 0.17.0__py3-none-any.whl → 0.18.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
@@ -180,6 +180,9 @@ class CsrfViewMiddleware:
180
180
  # saved anyways.
181
181
  request.META["CSRF_COOKIE"] = csrf_secret
182
182
 
183
+ if csrf_response := self._get_csrf_response(request):
184
+ return csrf_response
185
+
183
186
  response = self.get_response(request)
184
187
 
185
188
  if request.META.get("CSRF_COOKIE_NEEDS_UPDATE"):
@@ -395,23 +398,16 @@ class CsrfViewMiddleware:
395
398
  reason = self._bad_token_message("incorrect", token_source)
396
399
  raise RejectRequest(reason)
397
400
 
398
- def process_view(self, request, callback, callback_args, callback_kwargs):
401
+ def _get_csrf_response(self, request):
399
402
  # Wait until request.META["CSRF_COOKIE"] has been manipulated before
400
403
  # bailing out, so that get_token still works
401
- if getattr(callback, "csrf_exempt", False):
404
+ if getattr(request, "csrf_exempt", True):
402
405
  return None
403
406
 
404
407
  # Assume that anything not defined as 'safe' by RFC 9110 needs protection
405
408
  if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
406
409
  return None
407
410
 
408
- if getattr(request, "_dont_enforce_csrf_checks", False):
409
- # Mechanism to turn off CSRF checks for test suite. It comes after
410
- # the creation of CSRF cookies, so that everything else continues
411
- # to work exactly the same (e.g. cookies are sent, etc.), but
412
- # before any branches that call the _reject method.
413
- return None
414
-
415
411
  # Reject the request if the Origin header doesn't match an allowed
416
412
  # value.
417
413
  if "HTTP_ORIGIN" in request.META:
plain/csrf/views.py CHANGED
@@ -4,7 +4,28 @@ from plain.views import TemplateView
4
4
  class CsrfFailureView(TemplateView):
5
5
  template_name = "403.html"
6
6
 
7
- def get(self):
8
- response = super().get()
7
+ def get_response(self):
8
+ response = super().get_response()
9
9
  response.status_code = 403
10
10
  return response
11
+
12
+ def post(self):
13
+ return self.get()
14
+
15
+ def put(self):
16
+ return self.get()
17
+
18
+ def patch(self):
19
+ return self.get()
20
+
21
+ def delete(self):
22
+ return self.get()
23
+
24
+ def head(self):
25
+ return self.get()
26
+
27
+ def options(self):
28
+ return self.get()
29
+
30
+ def trace(self):
31
+ return self.get()
@@ -23,7 +23,6 @@ BUILTIN_MIDDLEWARE = [
23
23
 
24
24
 
25
25
  class BaseHandler:
26
- _view_middleware = None
27
26
  _middleware_chain = None
28
27
 
29
28
  def load_middleware(self):
@@ -32,8 +31,6 @@ class BaseHandler:
32
31
 
33
32
  Must be called after the environment is fixed (see __call__ in subclasses).
34
33
  """
35
- self._view_middleware = []
36
-
37
34
  get_response = self._get_response
38
35
  handler = convert_exception_to_response(get_response)
39
36
 
@@ -48,12 +45,6 @@ class BaseHandler:
48
45
  f"Middleware factory {middleware_path} returned None."
49
46
  )
50
47
 
51
- if hasattr(mw_instance, "process_view"):
52
- self._view_middleware.insert(
53
- 0,
54
- mw_instance.process_view,
55
- )
56
-
57
48
  handler = convert_exception_to_response(mw_instance)
58
49
 
59
50
  # We only assign to this when initialization is complete as it is used
@@ -82,19 +73,9 @@ class BaseHandler:
82
73
  template_response middleware. This method is everything that happens
83
74
  inside the request/response middleware.
84
75
  """
85
- response = None
86
76
  callback, callback_args, callback_kwargs = self.resolve_request(request)
87
77
 
88
- # Apply view middleware
89
- for middleware_method in self._view_middleware:
90
- response = middleware_method(
91
- request, callback, callback_args, callback_kwargs
92
- )
93
- if response:
94
- break
95
-
96
- if response is None:
97
- response = callback(request, *callback_args, **callback_kwargs)
78
+ response = callback(request, *callback_args, **callback_kwargs)
98
79
 
99
80
  # Complain if the view returned None (a common error).
100
81
  self.check_response(response, callback)
plain/test/client.py CHANGED
@@ -177,7 +177,7 @@ class ClientHandler(BaseHandler):
177
177
  # CsrfViewMiddleware. This makes life easier, and is probably
178
178
  # required for backwards compatibility with external tests against
179
179
  # admin views.
180
- request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
180
+ request.csrf_exempt = not self.enforce_csrf_checks
181
181
 
182
182
  # Request goes through middleware.
183
183
  response = self.get_response(request)
plain/utils/decorators.py CHANGED
@@ -1,7 +1,5 @@
1
1
  "Functions that help with dynamically creating decorators for views."
2
2
 
3
- from functools import partial, update_wrapper, wraps
4
-
5
3
 
6
4
  class classonlymethod(classmethod):
7
5
  def __get__(self, instance, cls=None):
@@ -10,81 +8,3 @@ class classonlymethod(classmethod):
10
8
  "This method is available only on the class, not on instances."
11
9
  )
12
10
  return super().__get__(instance, cls)
13
-
14
-
15
- def _update_method_wrapper(_wrapper, decorator):
16
- # _multi_decorate()'s bound_method isn't available in this scope. Cheat by
17
- # using it on a dummy function.
18
- @decorator
19
- def dummy(*args, **kwargs):
20
- pass
21
-
22
- update_wrapper(_wrapper, dummy)
23
-
24
-
25
- def _multi_decorate(decorators, method):
26
- """
27
- Decorate `method` with one or more function decorators. `decorators` can be
28
- a single decorator or an iterable of decorators.
29
- """
30
- if hasattr(decorators, "__iter__"):
31
- # Apply a list/tuple of decorators if 'decorators' is one. Decorator
32
- # functions are applied so that the call order is the same as the
33
- # order in which they appear in the iterable.
34
- decorators = decorators[::-1]
35
- else:
36
- decorators = [decorators]
37
-
38
- def _wrapper(self, *args, **kwargs):
39
- # bound_method has the signature that 'decorator' expects i.e. no
40
- # 'self' argument, but it's a closure over self so it can call
41
- # 'func'. Also, wrap method.__get__() in a function because new
42
- # attributes can't be set on bound method objects, only on functions.
43
- bound_method = wraps(method)(partial(method.__get__(self, type(self))))
44
- for dec in decorators:
45
- bound_method = dec(bound_method)
46
- return bound_method(*args, **kwargs)
47
-
48
- # Copy any attributes that a decorator adds to the function it decorates.
49
- for dec in decorators:
50
- _update_method_wrapper(_wrapper, dec)
51
- # Preserve any existing attributes of 'method', including the name.
52
- update_wrapper(_wrapper, method)
53
- return _wrapper
54
-
55
-
56
- def method_decorator(decorator, name=""):
57
- """
58
- Convert a function decorator into a method decorator
59
- """
60
-
61
- # 'obj' can be a class or a function. If 'obj' is a function at the time it
62
- # is passed to _dec, it will eventually be a method of the class it is
63
- # defined on. If 'obj' is a class, the 'name' is required to be the name
64
- # of the method that will be decorated.
65
- def _dec(obj):
66
- if not isinstance(obj, type):
67
- return _multi_decorate(decorator, obj)
68
- if not (name and hasattr(obj, name)):
69
- raise ValueError(
70
- "The keyword argument `name` must be the name of a method "
71
- f"of the decorated class: {obj}. Got '{name}' instead."
72
- )
73
- method = getattr(obj, name)
74
- if not callable(method):
75
- raise TypeError(
76
- f"Cannot decorate '{name}' as it isn't a callable attribute of "
77
- f"{obj} ({method})."
78
- )
79
- _wrapper = _multi_decorate(decorator, method)
80
- setattr(obj, name, _wrapper)
81
- return obj
82
-
83
- # Don't worry about making _dec look similar to a list/tuple as it's rather
84
- # meaningless.
85
- if not hasattr(decorator, "__iter__"):
86
- update_wrapper(_dec, decorator)
87
- # Change the name to aid debugging.
88
- obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
89
- _dec.__name__ = f"method_decorator({obj.__name__})"
90
- return _dec
plain/views/base.py CHANGED
@@ -19,6 +19,18 @@ class View:
19
19
  url_args: tuple
20
20
  url_kwargs: dict
21
21
 
22
+ # By default, any of these are allowed if a method is defined for it.
23
+ allowed_http_methods = [
24
+ "get",
25
+ "post",
26
+ "put",
27
+ "patch",
28
+ "delete",
29
+ "head",
30
+ "options",
31
+ "trace",
32
+ ]
33
+
22
34
  def __init__(self, *args, **kwargs) -> None:
23
35
  # Views can customize their init, which receives
24
36
  # the args and kwargs from as_view()
@@ -42,9 +54,6 @@ class View:
42
54
  except ResponseException as e:
43
55
  return e.response
44
56
 
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
57
  view.view_class = cls
49
58
 
50
59
  return view
@@ -57,7 +66,7 @@ class View:
57
66
 
58
67
  handler = getattr(self, self.request.method.lower(), None)
59
68
 
60
- if not handler:
69
+ if not handler or self.request.method.lower() not in self.allowed_http_methods:
61
70
  logger.warning(
62
71
  "Method Not Allowed (%s): %s",
63
72
  self.request.method,
@@ -100,14 +109,4 @@ class View:
100
109
  return response
101
110
 
102
111
  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)]
112
+ return [m.upper() for m in self.allowed_http_methods if hasattr(self, m)]
plain/views/csrf.py CHANGED
@@ -1,24 +1,4 @@
1
- from functools import wraps
2
-
3
- from plain.utils.decorators import method_decorator
4
-
5
-
6
- def csrf_exempt(view_func):
7
- """Mark a view function as being exempt from the CSRF view protection."""
8
-
9
- # view_func.csrf_exempt = True would also work, but decorators are nicer
10
- # if they don't have side effects, so return a new function.
11
- @wraps(view_func)
12
- def wrapper_view(*args, **kwargs):
13
- return view_func(*args, **kwargs)
14
-
15
- wrapper_view.csrf_exempt = True
16
- return wrapper_view
17
-
18
-
19
- @method_decorator(csrf_exempt, name="get_response")
20
1
  class CsrfExemptViewMixin:
21
- """CsrfExemptViewMixin needs to come before View in the class definition"""
22
-
23
- def get_response(self):
24
- return super().get_response()
2
+ def setup(self, *args, **kwargs):
3
+ super().setup(*args, **kwargs)
4
+ self.request.csrf_exempt = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -22,8 +22,8 @@ plain/cli/packages.py,sha256=FqRJaizaxpQ8lSS4nYNjqIGpYGDbeTmCXCvkGxusGOM,2160
22
22
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
23
23
  plain/cli/startup.py,sha256=3LIz9JrIZoF52Sa0j0SCypQwEaBDkhvuGaBdtiQLr5Q,680
24
24
  plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
25
- plain/csrf/middleware.py,sha256=pD9un9oLK6YNAEPW0rsVDsHyhyxmAA_IDE5hXs6mHiA,17776
26
- plain/csrf/views.py,sha256=YDgT451X16iUdCxpQ6rcHIy7nD0u7DAvCQl5-Mx5i9Y,219
25
+ plain/csrf/middleware.py,sha256=1szNRF-kKJdjhTw_Jw1Cp2IldtrLtbUqAApJOrVP1qc,17449
26
+ plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
27
27
  plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
28
28
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
29
29
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
@@ -47,7 +47,7 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
47
47
  plain/internal/files/uploadhandler.py,sha256=eEnd5onstypjHYtg367PnVWwCaF1kAPlLPSV7goIf_E,7198
48
48
  plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
49
49
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- plain/internal/handlers/base.py,sha256=I6549DBZUb-anqox7arzL9wpypH4bUCxZkitO76hljI,4794
50
+ plain/internal/handlers/base.py,sha256=_1aRNrNgqdx8kkUlXYRJNOTnZ_Z236JNYNcTdp7RRjc,4228
51
51
  plain/internal/handlers/exception.py,sha256=DH9gh1FyqgetFpMaB8yLIVE6phBTVPKQLA1GIn9MOeI,4555
52
52
  plain/internal/handlers/wsgi.py,sha256=estA1QKHTk3ZqziWxenHsw5UO2cwPp3Zr0XjkDeM5TY,7561
53
53
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -90,7 +90,7 @@ plain/templates/jinja/filters.py,sha256=3KJKKbxcv9dLzUDWPcaa88k3NU2m1GG3iMIgFhzX
90
90
  plain/templates/jinja/globals.py,sha256=qhvQuikkRkOTpHSW5FwdsvoViJNlRgHq3-O7ZyeajsE,669
91
91
  plain/test/README.md,sha256=Zso3Ir7a8vQerzKB6egjROQWkpveLAbscn7VTROPAiU,37
92
92
  plain/test/__init__.py,sha256=rXe88Y602NP8DBnReSyXb7dUzKoWweLuT43j-qwOUl4,138
93
- plain/test/client.py,sha256=Z9PQHLY-dg303P0bXJFIpmJuCjyB6oaD4opvla4yh-Q,31365
93
+ plain/test/client.py,sha256=wqypz5iIu28ubaEqMdx4wO7n6IPcBRkgkVXeEdr_qZg,31351
94
94
  plain/urls/README.md,sha256=pWnCvgYkWN7rG7hSyBOtX4ZUP3iO7FhqM6lvwwYll6c,33
95
95
  plain/urls/__init__.py,sha256=3UzwIufXjIks2K_X_Vms2MV19IqvyPLrXUeHU3WP47c,753
96
96
  plain/urls/base.py,sha256=Q1TzI5WvqYkN1U81fDom1q-AID4dXbszEMF6wAeTAI0,3717
@@ -109,7 +109,7 @@ plain/utils/dateformat.py,sha256=nsy71l16QuPN0ozGpVlCU5Et101fhk9L38F-wqT5p5I,102
109
109
  plain/utils/dateparse.py,sha256=u9_tF85YteXSjW9KQzNg_pcCEFDZS3EGorCddcWU0vE,5351
110
110
  plain/utils/dates.py,sha256=hSDKz8eIb3W5QjmGiklFZZELB0inYXsfiRUy49Cx-2Q,1226
111
111
  plain/utils/deconstruct.py,sha256=7NwEFIDCiadAArUBFmiErzDgfIgDWeKqqQFDXwSgQoQ,1830
112
- plain/utils/decorators.py,sha256=6pcFQ5TezYZoO5Ys1KnImOsv0nUMsFCdxal-S6brUOc,3468
112
+ plain/utils/decorators.py,sha256=mLHOo2jLdvYRo2z8lkeVn2vQErlj7xC6XoLwZBYf_z8,358
113
113
  plain/utils/deprecation.py,sha256=qtj33kHEmJU7mGSrNcKidZsMo5W8MN-zrXzUq3aVVy8,131
114
114
  plain/utils/duration.py,sha256=l0Gc41-DeyyAmpdy2XG-YO5UKxMf1NDpWIlQuD5hAn0,1162
115
115
  plain/utils/email.py,sha256=puRTBVuz44YvpnqV3LT4nNIKqdqfY3L8zbDJIkqHk2Y,328
@@ -130,16 +130,16 @@ plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
130
130
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
131
131
  plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
132
132
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
133
- plain/views/base.py,sha256=wMkCAbr3XqXyP8dJr-O9atA1-N6K4-cTFflLhSYGOpY,3227
134
- plain/views/csrf.py,sha256=gO9npd_Ut_LoYF_u7Qb_ZsPRfSeE3aTPG97XlMp4oEo,724
133
+ plain/views/base.py,sha256=HxOOOKZDCCZDmy2cJCEhYswVeIHZ9TukYh1zibeUn6w,3160
134
+ plain/views/csrf.py,sha256=7q6l5xzLWhRnMY64aNj0hR6G-3pxI2yhRwG6k_5j00E,144
135
135
  plain/views/errors.py,sha256=Y4oGX4Z6D2COKcDEfINvXE1acE8Ad15KwNNWPs5BCfc,967
136
136
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
137
137
  plain/views/forms.py,sha256=RhlaUcZCkeqokY_fvv-NOS-kgZAG4XhDLOPbf9K_Zlc,2691
138
138
  plain/views/objects.py,sha256=fRfS6KNehIGqkbPw4nSafj8HStxYExHmbggolBbzcxs,7921
139
139
  plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
140
140
  plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
141
- plain-0.17.0.dist-info/METADATA,sha256=97ESRcFn2YsQ3VM9sIv5rO7j489XgP70TNssLtu5c08,2518
142
- plain-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
- plain-0.17.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
144
- plain-0.17.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
- plain-0.17.0.dist-info/RECORD,,
141
+ plain-0.18.0.dist-info/METADATA,sha256=Qik7zQyhLtbB8QcE6qwCar14RSDbiFaalX_6-VejaBc,2518
142
+ plain-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
+ plain-0.18.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
144
+ plain-0.18.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
+ plain-0.18.0.dist-info/RECORD,,
File without changes