plain 0.17.0__tar.gz → 0.19.0__tar.gz

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 (160) hide show
  1. {plain-0.17.0 → plain-0.19.0}/PKG-INFO +1 -1
  2. {plain-0.17.0 → plain-0.19.0}/plain/assets/views.py +4 -1
  3. {plain-0.17.0 → plain-0.19.0}/plain/cli/cli.py +16 -15
  4. {plain-0.17.0 → plain-0.19.0}/plain/csrf/middleware.py +5 -9
  5. plain-0.19.0/plain/csrf/views.py +31 -0
  6. {plain-0.17.0 → plain-0.19.0}/plain/internal/handlers/base.py +1 -20
  7. {plain-0.17.0 → plain-0.19.0}/plain/test/client.py +1 -1
  8. plain-0.19.0/plain/utils/decorators.py +10 -0
  9. {plain-0.17.0 → plain-0.19.0}/plain/views/base.py +14 -15
  10. plain-0.19.0/plain/views/csrf.py +4 -0
  11. {plain-0.17.0 → plain-0.19.0}/pyproject.toml +1 -1
  12. plain-0.17.0/plain/csrf/views.py +0 -10
  13. plain-0.17.0/plain/utils/decorators.py +0 -90
  14. plain-0.17.0/plain/views/csrf.py +0 -24
  15. {plain-0.17.0 → plain-0.19.0}/.gitignore +0 -0
  16. {plain-0.17.0 → plain-0.19.0}/LICENSE +0 -0
  17. {plain-0.17.0 → plain-0.19.0}/README.md +0 -0
  18. {plain-0.17.0 → plain-0.19.0}/plain/README.md +0 -0
  19. {plain-0.17.0 → plain-0.19.0}/plain/__main__.py +0 -0
  20. {plain-0.17.0 → plain-0.19.0}/plain/assets/README.md +0 -0
  21. {plain-0.17.0 → plain-0.19.0}/plain/assets/__init__.py +0 -0
  22. {plain-0.17.0 → plain-0.19.0}/plain/assets/compile.py +0 -0
  23. {plain-0.17.0 → plain-0.19.0}/plain/assets/finders.py +0 -0
  24. {plain-0.17.0 → plain-0.19.0}/plain/assets/fingerprints.py +0 -0
  25. {plain-0.17.0 → plain-0.19.0}/plain/assets/urls.py +0 -0
  26. {plain-0.17.0 → plain-0.19.0}/plain/cli/README.md +0 -0
  27. {plain-0.17.0 → plain-0.19.0}/plain/cli/__init__.py +0 -0
  28. {plain-0.17.0 → plain-0.19.0}/plain/cli/formatting.py +0 -0
  29. {plain-0.17.0 → plain-0.19.0}/plain/cli/packages.py +0 -0
  30. {plain-0.17.0 → plain-0.19.0}/plain/cli/print.py +0 -0
  31. {plain-0.17.0 → plain-0.19.0}/plain/cli/startup.py +0 -0
  32. {plain-0.17.0 → plain-0.19.0}/plain/csrf/README.md +0 -0
  33. {plain-0.17.0 → plain-0.19.0}/plain/debug.py +0 -0
  34. {plain-0.17.0 → plain-0.19.0}/plain/exceptions.py +0 -0
  35. {plain-0.17.0 → plain-0.19.0}/plain/forms/README.md +0 -0
  36. {plain-0.17.0 → plain-0.19.0}/plain/forms/__init__.py +0 -0
  37. {plain-0.17.0 → plain-0.19.0}/plain/forms/boundfield.py +0 -0
  38. {plain-0.17.0 → plain-0.19.0}/plain/forms/exceptions.py +0 -0
  39. {plain-0.17.0 → plain-0.19.0}/plain/forms/fields.py +0 -0
  40. {plain-0.17.0 → plain-0.19.0}/plain/forms/forms.py +0 -0
  41. {plain-0.17.0 → plain-0.19.0}/plain/http/README.md +0 -0
  42. {plain-0.17.0 → plain-0.19.0}/plain/http/__init__.py +0 -0
  43. {plain-0.17.0 → plain-0.19.0}/plain/http/cookie.py +0 -0
  44. {plain-0.17.0 → plain-0.19.0}/plain/http/multipartparser.py +0 -0
  45. {plain-0.17.0 → plain-0.19.0}/plain/http/request.py +0 -0
  46. {plain-0.17.0 → plain-0.19.0}/plain/http/response.py +0 -0
  47. {plain-0.17.0 → plain-0.19.0}/plain/internal/__init__.py +0 -0
  48. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/README.md +0 -0
  49. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/__init__.py +0 -0
  50. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/base.py +0 -0
  51. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/locks.py +0 -0
  52. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/move.py +0 -0
  53. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/temp.py +0 -0
  54. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/uploadedfile.py +0 -0
  55. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/uploadhandler.py +0 -0
  56. {plain-0.17.0 → plain-0.19.0}/plain/internal/files/utils.py +0 -0
  57. {plain-0.17.0 → plain-0.19.0}/plain/internal/handlers/__init__.py +0 -0
  58. {plain-0.17.0 → plain-0.19.0}/plain/internal/handlers/exception.py +0 -0
  59. {plain-0.17.0 → plain-0.19.0}/plain/internal/handlers/wsgi.py +0 -0
  60. {plain-0.17.0 → plain-0.19.0}/plain/internal/middleware/__init__.py +0 -0
  61. {plain-0.17.0 → plain-0.19.0}/plain/internal/middleware/headers.py +0 -0
  62. {plain-0.17.0 → plain-0.19.0}/plain/internal/middleware/https.py +0 -0
  63. {plain-0.17.0 → plain-0.19.0}/plain/internal/middleware/slash.py +0 -0
  64. {plain-0.17.0 → plain-0.19.0}/plain/json.py +0 -0
  65. {plain-0.17.0 → plain-0.19.0}/plain/logs/README.md +0 -0
  66. {plain-0.17.0 → plain-0.19.0}/plain/logs/__init__.py +0 -0
  67. {plain-0.17.0 → plain-0.19.0}/plain/logs/configure.py +0 -0
  68. {plain-0.17.0 → plain-0.19.0}/plain/logs/loggers.py +0 -0
  69. {plain-0.17.0 → plain-0.19.0}/plain/logs/utils.py +0 -0
  70. {plain-0.17.0 → plain-0.19.0}/plain/packages/README.md +0 -0
  71. {plain-0.17.0 → plain-0.19.0}/plain/packages/__init__.py +0 -0
  72. {plain-0.17.0 → plain-0.19.0}/plain/packages/config.py +0 -0
  73. {plain-0.17.0 → plain-0.19.0}/plain/packages/registry.py +0 -0
  74. {plain-0.17.0 → plain-0.19.0}/plain/paginator.py +0 -0
  75. {plain-0.17.0 → plain-0.19.0}/plain/preflight/README.md +0 -0
  76. {plain-0.17.0 → plain-0.19.0}/plain/preflight/__init__.py +0 -0
  77. {plain-0.17.0 → plain-0.19.0}/plain/preflight/files.py +0 -0
  78. {plain-0.17.0 → plain-0.19.0}/plain/preflight/messages.py +0 -0
  79. {plain-0.17.0 → plain-0.19.0}/plain/preflight/registry.py +0 -0
  80. {plain-0.17.0 → plain-0.19.0}/plain/preflight/security.py +0 -0
  81. {plain-0.17.0 → plain-0.19.0}/plain/preflight/urls.py +0 -0
  82. {plain-0.17.0 → plain-0.19.0}/plain/runtime/README.md +0 -0
  83. {plain-0.17.0 → plain-0.19.0}/plain/runtime/__init__.py +0 -0
  84. {plain-0.17.0 → plain-0.19.0}/plain/runtime/global_settings.py +0 -0
  85. {plain-0.17.0 → plain-0.19.0}/plain/runtime/user_settings.py +0 -0
  86. {plain-0.17.0 → plain-0.19.0}/plain/signals/README.md +0 -0
  87. {plain-0.17.0 → plain-0.19.0}/plain/signals/__init__.py +0 -0
  88. {plain-0.17.0 → plain-0.19.0}/plain/signals/dispatch/__init__.py +0 -0
  89. {plain-0.17.0 → plain-0.19.0}/plain/signals/dispatch/dispatcher.py +0 -0
  90. {plain-0.17.0 → plain-0.19.0}/plain/signals/dispatch/license.txt +0 -0
  91. {plain-0.17.0 → plain-0.19.0}/plain/signing.py +0 -0
  92. {plain-0.17.0 → plain-0.19.0}/plain/templates/README.md +0 -0
  93. {plain-0.17.0 → plain-0.19.0}/plain/templates/__init__.py +0 -0
  94. {plain-0.17.0 → plain-0.19.0}/plain/templates/core.py +0 -0
  95. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/README.md +0 -0
  96. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/__init__.py +0 -0
  97. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/environments.py +0 -0
  98. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/extensions.py +0 -0
  99. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/filters.py +0 -0
  100. {plain-0.17.0 → plain-0.19.0}/plain/templates/jinja/globals.py +0 -0
  101. {plain-0.17.0 → plain-0.19.0}/plain/test/README.md +0 -0
  102. {plain-0.17.0 → plain-0.19.0}/plain/test/__init__.py +0 -0
  103. {plain-0.17.0 → plain-0.19.0}/plain/urls/README.md +0 -0
  104. {plain-0.17.0 → plain-0.19.0}/plain/urls/__init__.py +0 -0
  105. {plain-0.17.0 → plain-0.19.0}/plain/urls/base.py +0 -0
  106. {plain-0.17.0 → plain-0.19.0}/plain/urls/conf.py +0 -0
  107. {plain-0.17.0 → plain-0.19.0}/plain/urls/converters.py +0 -0
  108. {plain-0.17.0 → plain-0.19.0}/plain/urls/exceptions.py +0 -0
  109. {plain-0.17.0 → plain-0.19.0}/plain/urls/resolvers.py +0 -0
  110. {plain-0.17.0 → plain-0.19.0}/plain/utils/README.md +0 -0
  111. {plain-0.17.0 → plain-0.19.0}/plain/utils/__init__.py +0 -0
  112. {plain-0.17.0 → plain-0.19.0}/plain/utils/_os.py +0 -0
  113. {plain-0.17.0 → plain-0.19.0}/plain/utils/cache.py +0 -0
  114. {plain-0.17.0 → plain-0.19.0}/plain/utils/connection.py +0 -0
  115. {plain-0.17.0 → plain-0.19.0}/plain/utils/crypto.py +0 -0
  116. {plain-0.17.0 → plain-0.19.0}/plain/utils/datastructures.py +0 -0
  117. {plain-0.17.0 → plain-0.19.0}/plain/utils/dateformat.py +0 -0
  118. {plain-0.17.0 → plain-0.19.0}/plain/utils/dateparse.py +0 -0
  119. {plain-0.17.0 → plain-0.19.0}/plain/utils/dates.py +0 -0
  120. {plain-0.17.0 → plain-0.19.0}/plain/utils/deconstruct.py +0 -0
  121. {plain-0.17.0 → plain-0.19.0}/plain/utils/deprecation.py +0 -0
  122. {plain-0.17.0 → plain-0.19.0}/plain/utils/duration.py +0 -0
  123. {plain-0.17.0 → plain-0.19.0}/plain/utils/email.py +0 -0
  124. {plain-0.17.0 → plain-0.19.0}/plain/utils/encoding.py +0 -0
  125. {plain-0.17.0 → plain-0.19.0}/plain/utils/functional.py +0 -0
  126. {plain-0.17.0 → plain-0.19.0}/plain/utils/hashable.py +0 -0
  127. {plain-0.17.0 → plain-0.19.0}/plain/utils/html.py +0 -0
  128. {plain-0.17.0 → plain-0.19.0}/plain/utils/http.py +0 -0
  129. {plain-0.17.0 → plain-0.19.0}/plain/utils/inspect.py +0 -0
  130. {plain-0.17.0 → plain-0.19.0}/plain/utils/ipv6.py +0 -0
  131. {plain-0.17.0 → plain-0.19.0}/plain/utils/itercompat.py +0 -0
  132. {plain-0.17.0 → plain-0.19.0}/plain/utils/module_loading.py +0 -0
  133. {plain-0.17.0 → plain-0.19.0}/plain/utils/regex_helper.py +0 -0
  134. {plain-0.17.0 → plain-0.19.0}/plain/utils/safestring.py +0 -0
  135. {plain-0.17.0 → plain-0.19.0}/plain/utils/text.py +0 -0
  136. {plain-0.17.0 → plain-0.19.0}/plain/utils/timesince.py +0 -0
  137. {plain-0.17.0 → plain-0.19.0}/plain/utils/timezone.py +0 -0
  138. {plain-0.17.0 → plain-0.19.0}/plain/utils/tree.py +0 -0
  139. {plain-0.17.0 → plain-0.19.0}/plain/validators.py +0 -0
  140. {plain-0.17.0 → plain-0.19.0}/plain/views/README.md +0 -0
  141. {plain-0.17.0 → plain-0.19.0}/plain/views/__init__.py +0 -0
  142. {plain-0.17.0 → plain-0.19.0}/plain/views/errors.py +0 -0
  143. {plain-0.17.0 → plain-0.19.0}/plain/views/exceptions.py +0 -0
  144. {plain-0.17.0 → plain-0.19.0}/plain/views/forms.py +0 -0
  145. {plain-0.17.0 → plain-0.19.0}/plain/views/objects.py +0 -0
  146. {plain-0.17.0 → plain-0.19.0}/plain/views/redirect.py +0 -0
  147. {plain-0.17.0 → plain-0.19.0}/plain/views/templates.py +0 -0
  148. {plain-0.17.0 → plain-0.19.0}/plain/wsgi.py +0 -0
  149. {plain-0.17.0 → plain-0.19.0}/tests/.bolt/assets_collected/assets.json +0 -0
  150. {plain-0.17.0 → plain-0.19.0}/tests/.gitignore +0 -0
  151. {plain-0.17.0 → plain-0.19.0}/tests/app/.gitignore +0 -0
  152. {plain-0.17.0 → plain-0.19.0}/tests/app/settings.py +0 -0
  153. {plain-0.17.0 → plain-0.19.0}/tests/app/test/__init__.py +0 -0
  154. {plain-0.17.0 → plain-0.19.0}/tests/app/test/default_settings.py +0 -0
  155. {plain-0.17.0 → plain-0.19.0}/tests/app/urls.py +0 -0
  156. {plain-0.17.0 → plain-0.19.0}/tests/conftest.py +0 -0
  157. {plain-0.17.0 → plain-0.19.0}/tests/test_cli.py +0 -0
  158. {plain-0.17.0 → plain-0.19.0}/tests/test_runtime.py +0 -0
  159. {plain-0.17.0 → plain-0.19.0}/tests/test_wsgi.py +0 -0
  160. {plain-0.17.0 → plain-0.19.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.17.0
3
+ Version: 0.19.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
@@ -28,8 +28,11 @@ class AssetView(View):
28
28
  This class could be subclassed to further tweak the responses or behavior.
29
29
  """
30
30
 
31
+ def get_url_path(self):
32
+ return self.url_kwargs["path"]
33
+
31
34
  def get(self):
32
- url_path = self.url_kwargs["path"]
35
+ url_path = self.get_url_path()
33
36
 
34
37
  # Make a trailing slash work, but we don't expect it
35
38
  url_path = url_path.rstrip("/")
@@ -1,9 +1,9 @@
1
1
  import importlib
2
- import json
3
2
  import os
4
3
  import shutil
5
4
  import subprocess
6
5
  import sys
6
+ import tomllib
7
7
  import traceback
8
8
  from importlib.metadata import entry_points
9
9
  from importlib.util import find_spec
@@ -292,22 +292,23 @@ def compile(keep_original, fingerprint, compress):
292
292
  result = entry_point.load()()
293
293
  print()
294
294
 
295
- # TODO also look in [tool.plain.compile.run]
296
-
297
- # Run a "compile" script from package.json automatically
298
- package_json = Path("package.json")
299
- if package_json.exists():
300
- with package_json.open() as f:
301
- package = json.load(f)
302
-
303
- if package.get("scripts", {}).get("compile"):
304
- click.secho("Running `npm run compile`", bold=True)
305
- result = subprocess.run(["npm", "run", "compile"])
295
+ pyproject_path = plain.runtime.APP_PATH.parent / "pyproject.toml"
296
+ if pyproject_path.exists():
297
+ with pyproject_path.open("rb") as f:
298
+ pyproject = tomllib.load(f)
299
+
300
+ for name, data in (
301
+ pyproject.get("tool", {})
302
+ .get("plain", {})
303
+ .get("compile", {})
304
+ .get("run", {})
305
+ .items()
306
+ ):
307
+ click.secho(f"Running {name} from pyproject.toml", bold=True)
308
+ result = subprocess.run(data["cmd"], shell=True)
306
309
  print()
307
310
  if result.returncode:
308
- click.secho(
309
- f"Error in `npm run compile` (exit {result.returncode})", fg="red"
310
- )
311
+ click.secho(f"Error in {name} (exit {result.returncode})", fg="red")
311
312
  sys.exit(result.returncode)
312
313
 
313
314
  # Compile our assets
@@ -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:
@@ -0,0 +1,31 @@
1
+ from plain.views import TemplateView
2
+
3
+
4
+ class CsrfFailureView(TemplateView):
5
+ template_name = "403.html"
6
+
7
+ def get_response(self):
8
+ response = super().get_response()
9
+ response.status_code = 403
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)
@@ -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)
@@ -0,0 +1,10 @@
1
+ "Functions that help with dynamically creating decorators for views."
2
+
3
+
4
+ class classonlymethod(classmethod):
5
+ def __get__(self, instance, cls=None):
6
+ if instance is not None:
7
+ raise AttributeError(
8
+ "This method is available only on the class, not on instances."
9
+ )
10
+ return super().__get__(instance, cls)
@@ -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)]
@@ -0,0 +1,4 @@
1
+ class CsrfExemptViewMixin:
2
+ def setup(self, *args, **kwargs):
3
+ super().setup(*args, **kwargs)
4
+ self.request.csrf_exempt = True
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.17.0"
3
+ version = "0.19.0"
4
4
  description = "A web framework for building products with Python."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -1,10 +0,0 @@
1
- from plain.views import TemplateView
2
-
3
-
4
- class CsrfFailureView(TemplateView):
5
- template_name = "403.html"
6
-
7
- def get(self):
8
- response = super().get()
9
- response.status_code = 403
10
- return response
@@ -1,90 +0,0 @@
1
- "Functions that help with dynamically creating decorators for views."
2
-
3
- from functools import partial, update_wrapper, wraps
4
-
5
-
6
- class classonlymethod(classmethod):
7
- def __get__(self, instance, cls=None):
8
- if instance is not None:
9
- raise AttributeError(
10
- "This method is available only on the class, not on instances."
11
- )
12
- 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
@@ -1,24 +0,0 @@
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
- class CsrfExemptViewMixin:
21
- """CsrfExemptViewMixin needs to come before View in the class definition"""
22
-
23
- def get_response(self):
24
- return super().get_response()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes