plain 0.69.0__py3-none-any.whl → 0.71.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 (128) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +28 -0
  3. plain/assets/compile.py +20 -7
  4. plain/assets/finders.py +15 -11
  5. plain/assets/fingerprints.py +6 -5
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +23 -17
  8. plain/chores/registry.py +14 -9
  9. plain/cli/agent/__init__.py +1 -1
  10. plain/cli/agent/docs.py +7 -6
  11. plain/cli/agent/llmdocs.py +18 -8
  12. plain/cli/agent/md.py +19 -14
  13. plain/cli/agent/prompt.py +1 -1
  14. plain/cli/agent/request.py +37 -17
  15. plain/cli/build.py +2 -2
  16. plain/cli/changelog.py +8 -4
  17. plain/cli/chores.py +4 -4
  18. plain/cli/core.py +8 -5
  19. plain/cli/docs.py +2 -2
  20. plain/cli/formatting.py +10 -7
  21. plain/cli/output.py +6 -2
  22. plain/cli/preflight.py +3 -3
  23. plain/cli/print.py +1 -1
  24. plain/cli/registry.py +10 -6
  25. plain/cli/scaffold.py +1 -1
  26. plain/cli/settings.py +1 -1
  27. plain/cli/shell.py +10 -7
  28. plain/cli/startup.py +3 -3
  29. plain/cli/urls.py +10 -4
  30. plain/cli/utils.py +2 -2
  31. plain/csrf/middleware.py +15 -5
  32. plain/csrf/views.py +11 -8
  33. plain/debug.py +5 -2
  34. plain/exceptions.py +19 -8
  35. plain/forms/__init__.py +1 -1
  36. plain/forms/boundfield.py +14 -7
  37. plain/forms/exceptions.py +1 -1
  38. plain/forms/fields.py +139 -97
  39. plain/forms/forms.py +55 -39
  40. plain/http/README.md +1 -1
  41. plain/http/__init__.py +4 -4
  42. plain/http/cookie.py +15 -7
  43. plain/http/multipartparser.py +50 -30
  44. plain/http/request.py +156 -108
  45. plain/http/response.py +99 -80
  46. plain/internal/__init__.py +8 -1
  47. plain/internal/files/base.py +34 -18
  48. plain/internal/files/locks.py +19 -11
  49. plain/internal/files/move.py +8 -3
  50. plain/internal/files/temp.py +23 -5
  51. plain/internal/files/uploadedfile.py +42 -26
  52. plain/internal/files/uploadhandler.py +50 -29
  53. plain/internal/files/utils.py +13 -6
  54. plain/internal/handlers/base.py +21 -7
  55. plain/internal/handlers/exception.py +19 -5
  56. plain/internal/handlers/wsgi.py +33 -21
  57. plain/internal/middleware/headers.py +11 -2
  58. plain/internal/middleware/hosts.py +12 -4
  59. plain/internal/middleware/https.py +13 -3
  60. plain/internal/middleware/slash.py +15 -5
  61. plain/json.py +2 -1
  62. plain/logs/configure.py +3 -1
  63. plain/logs/debug.py +16 -5
  64. plain/logs/formatters.py +6 -3
  65. plain/logs/loggers.py +56 -52
  66. plain/logs/utils.py +19 -9
  67. plain/packages/config.py +14 -6
  68. plain/packages/registry.py +27 -12
  69. plain/paginator.py +31 -21
  70. plain/preflight/checks.py +3 -1
  71. plain/preflight/files.py +3 -1
  72. plain/preflight/registry.py +25 -10
  73. plain/preflight/results.py +10 -4
  74. plain/preflight/security.py +7 -5
  75. plain/preflight/urls.py +4 -1
  76. plain/runtime/__init__.py +7 -6
  77. plain/runtime/global_settings.py +6 -9
  78. plain/runtime/user_settings.py +26 -17
  79. plain/runtime/utils.py +1 -1
  80. plain/signals/dispatch/dispatcher.py +39 -17
  81. plain/signing.py +49 -30
  82. plain/templates/jinja/__init__.py +13 -5
  83. plain/templates/jinja/environments.py +4 -3
  84. plain/templates/jinja/extensions.py +9 -3
  85. plain/templates/jinja/filters.py +7 -2
  86. plain/templates/jinja/globals.py +1 -1
  87. plain/test/client.py +249 -177
  88. plain/test/encoding.py +9 -6
  89. plain/test/exceptions.py +10 -2
  90. plain/urls/converters.py +13 -10
  91. plain/urls/patterns.py +32 -20
  92. plain/urls/resolvers.py +32 -22
  93. plain/urls/utils.py +5 -1
  94. plain/utils/cache.py +14 -8
  95. plain/utils/crypto.py +21 -5
  96. plain/utils/datastructures.py +84 -54
  97. plain/utils/dateparse.py +10 -7
  98. plain/utils/deconstruct.py +12 -4
  99. plain/utils/decorators.py +5 -1
  100. plain/utils/duration.py +8 -4
  101. plain/utils/encoding.py +14 -7
  102. plain/utils/functional.py +62 -47
  103. plain/utils/hashable.py +5 -1
  104. plain/utils/html.py +21 -14
  105. plain/utils/http.py +16 -9
  106. plain/utils/inspect.py +14 -6
  107. plain/utils/ipv6.py +7 -3
  108. plain/utils/itercompat.py +6 -1
  109. plain/utils/module_loading.py +7 -3
  110. plain/utils/regex_helper.py +23 -13
  111. plain/utils/safestring.py +14 -6
  112. plain/utils/text.py +34 -18
  113. plain/utils/timezone.py +30 -19
  114. plain/utils/tree.py +31 -18
  115. plain/validators.py +71 -44
  116. plain/views/base.py +16 -8
  117. plain/views/errors.py +11 -4
  118. plain/views/exceptions.py +4 -1
  119. plain/views/objects.py +15 -15
  120. plain/views/redirect.py +14 -10
  121. plain/views/templates.py +1 -1
  122. plain/wsgi.py +3 -1
  123. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/METADATA +1 -1
  124. plain-0.71.0.dist-info/RECORD +169 -0
  125. plain-0.69.0.dist-info/RECORD +0 -169
  126. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/WHEEL +0 -0
  127. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/entry_points.txt +0 -0
  128. {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/licenses/LICENSE +0 -0
plain/test/encoding.py CHANGED
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import mimetypes
2
4
  import os
5
+ from typing import Any
3
6
 
4
7
  from plain.runtime import settings
5
8
  from plain.utils.encoding import force_bytes
6
9
  from plain.utils.itercompat import is_iterable
7
10
 
8
11
 
9
- def encode_multipart(boundary, data):
12
+ def encode_multipart(boundary: str, data: dict[str, Any]) -> bytes:
10
13
  """
11
14
  Encode multipart POST data from a dictionary of form values.
12
15
 
@@ -14,13 +17,13 @@ def encode_multipart(boundary, data):
14
17
  as content. If the value is a file, the contents of the file will be sent
15
18
  as an application/octet-stream; otherwise, str(value) will be sent.
16
19
  """
17
- lines = []
20
+ lines: list[bytes] = []
18
21
 
19
- def to_bytes(s):
22
+ def to_bytes(s: str) -> bytes:
20
23
  return force_bytes(s, settings.DEFAULT_CHARSET)
21
24
 
22
25
  # Not by any means perfect, but good enough for our purposes.
23
- def is_file(thing):
26
+ def is_file(thing: Any) -> bool:
24
27
  return hasattr(thing, "read") and callable(thing.read)
25
28
 
26
29
  # Each bit of the multipart form data could be either a form value or a
@@ -68,8 +71,8 @@ def encode_multipart(boundary, data):
68
71
  return b"\r\n".join(lines)
69
72
 
70
73
 
71
- def encode_file(boundary, key, file):
72
- def to_bytes(s):
74
+ def encode_file(boundary: str, key: str, file: Any) -> list[bytes]:
75
+ def to_bytes(s: str) -> bytes:
73
76
  return force_bytes(s, settings.DEFAULT_CHARSET)
74
77
 
75
78
  # file.name might not be a string. For example, it's an int for
plain/test/exceptions.py CHANGED
@@ -1,7 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from plain.http import Response
7
+
8
+
1
9
  class RedirectCycleError(Exception):
2
10
  """The test client has been asked to follow a redirect loop."""
3
11
 
4
- def __init__(self, message, last_response):
12
+ def __init__(self, message: str, last_response: Response) -> None:
5
13
  super().__init__(message)
6
14
  self.last_response = last_response
7
- self.redirect_chain = last_response.redirect_chain
15
+ self.redirect_chain: list[tuple[str, int]] = last_response.redirect_chain # type: ignore[attr-defined]
plain/urls/converters.py CHANGED
@@ -1,34 +1,37 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import uuid
5
+ from typing import Any
3
6
 
4
7
 
5
8
  class IntConverter:
6
9
  regex = "[0-9]+"
7
10
 
8
- def to_python(self, value):
11
+ def to_python(self, value: str) -> int:
9
12
  return int(value)
10
13
 
11
- def to_url(self, value):
14
+ def to_url(self, value: int) -> str:
12
15
  return str(value)
13
16
 
14
17
 
15
18
  class StringConverter:
16
19
  regex = "[^/]+"
17
20
 
18
- def to_python(self, value):
21
+ def to_python(self, value: str) -> str:
19
22
  return value
20
23
 
21
- def to_url(self, value):
24
+ def to_url(self, value: str) -> str:
22
25
  return value
23
26
 
24
27
 
25
28
  class UUIDConverter:
26
29
  regex = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
27
30
 
28
- def to_python(self, value):
31
+ def to_python(self, value: str) -> uuid.UUID:
29
32
  return uuid.UUID(value)
30
33
 
31
- def to_url(self, value):
34
+ def to_url(self, value: uuid.UUID) -> str:
32
35
  return str(value)
33
36
 
34
37
 
@@ -49,18 +52,18 @@ DEFAULT_CONVERTERS = {
49
52
  }
50
53
 
51
54
 
52
- REGISTERED_CONVERTERS = {}
55
+ REGISTERED_CONVERTERS: dict[str, Any] = {}
53
56
 
54
57
 
55
- def register_converter(converter, type_name):
58
+ def register_converter(converter: type, type_name: str) -> None:
56
59
  REGISTERED_CONVERTERS[type_name] = converter()
57
60
  get_converters.cache_clear()
58
61
 
59
62
 
60
63
  @functools.cache
61
- def get_converters():
64
+ def get_converters() -> dict[str, Any]:
62
65
  return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS}
63
66
 
64
67
 
65
- def get_converter(raw_converter):
68
+ def get_converter(raw_converter: str) -> Any:
66
69
  return get_converters()[raw_converter]
plain/urls/patterns.py CHANGED
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
4
  import string
5
+ from typing import Any
3
6
 
4
7
  from plain.exceptions import ImproperlyConfigured
5
8
  from plain.internal import internalcode
@@ -12,7 +15,7 @@ from .converters import get_converter
12
15
 
13
16
  @internalcode
14
17
  class CheckURLMixin:
15
- def describe(self):
18
+ def describe(self) -> str:
16
19
  """
17
20
  Format the URL pattern for display in warning messages.
18
21
  """
@@ -21,7 +24,7 @@ class CheckURLMixin:
21
24
  description += f" [name='{self.name}']"
22
25
  return description
23
26
 
24
- def _check_pattern_startswith_slash(self):
27
+ def _check_pattern_startswith_slash(self) -> list[PreflightResult]:
25
28
  """
26
29
  Check that the pattern does not begin with a forward slash.
27
30
  """
@@ -44,14 +47,14 @@ class CheckURLMixin:
44
47
 
45
48
 
46
49
  class RegexPattern(CheckURLMixin):
47
- def __init__(self, regex, name=None, is_endpoint=False):
50
+ def __init__(self, regex: str, name: str | None = None, is_endpoint: bool = False):
48
51
  self._regex = regex
49
52
  self._is_endpoint = is_endpoint
50
53
  self.name = name
51
- self.converters = {}
54
+ self.converters: dict[str, Any] = {}
52
55
  self.regex = self._compile(str(regex))
53
56
 
54
- def match(self, path):
57
+ def match(self, path: str) -> tuple[str, tuple[Any, ...], dict[str, Any]] | None:
55
58
  match = (
56
59
  self.regex.fullmatch(path)
57
60
  if self._is_endpoint and self.regex.pattern.endswith("$")
@@ -67,14 +70,14 @@ class RegexPattern(CheckURLMixin):
67
70
  return path[match.end() :], args, kwargs
68
71
  return None
69
72
 
70
- def preflight(self):
73
+ def preflight(self) -> list[PreflightResult]:
71
74
  warnings = []
72
75
  warnings.extend(self._check_pattern_startswith_slash())
73
76
  if not self._is_endpoint:
74
77
  warnings.extend(self._check_include_trailing_dollar())
75
78
  return warnings
76
79
 
77
- def _check_include_trailing_dollar(self):
80
+ def _check_include_trailing_dollar(self) -> list[PreflightResult]:
78
81
  regex_pattern = self.regex.pattern
79
82
  if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
80
83
  return [
@@ -87,7 +90,7 @@ class RegexPattern(CheckURLMixin):
87
90
  else:
88
91
  return []
89
92
 
90
- def _compile(self, regex):
93
+ def _compile(self, regex: str) -> re.Pattern[str]:
91
94
  """Compile and return the given regular expression."""
92
95
  try:
93
96
  return re.compile(regex)
@@ -96,7 +99,7 @@ class RegexPattern(CheckURLMixin):
96
99
  f'"{regex}" is not a valid regular expression: {e}'
97
100
  ) from e
98
101
 
99
- def __str__(self):
102
+ def __str__(self) -> str:
100
103
  return str(self._regex)
101
104
 
102
105
 
@@ -105,7 +108,9 @@ _PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
105
108
  )
106
109
 
107
110
 
108
- def _route_to_regex(route, is_endpoint=False):
111
+ def _route_to_regex(
112
+ route: str, is_endpoint: bool = False
113
+ ) -> tuple[str, dict[str, Any]]:
109
114
  """
110
115
  Convert a path pattern into a regular expression. Return the regular
111
116
  expression and a dictionary mapping the capture names to the converters.
@@ -151,14 +156,14 @@ def _route_to_regex(route, is_endpoint=False):
151
156
 
152
157
 
153
158
  class RoutePattern(CheckURLMixin):
154
- def __init__(self, route, name=None, is_endpoint=False):
159
+ def __init__(self, route: str, name: str | None = None, is_endpoint: bool = False):
155
160
  self._route = route
156
161
  self._is_endpoint = is_endpoint
157
162
  self.name = name
158
163
  self.converters = _route_to_regex(str(route), is_endpoint)[1]
159
164
  self.regex = self._compile(str(route))
160
165
 
161
- def match(self, path):
166
+ def match(self, path: str) -> tuple[str, tuple[()], dict[str, Any]] | None:
162
167
  match = self.regex.search(path)
163
168
  if match:
164
169
  # RoutePattern doesn't allow non-named groups so args are ignored.
@@ -172,7 +177,7 @@ class RoutePattern(CheckURLMixin):
172
177
  return path[match.end() :], (), kwargs
173
178
  return None
174
179
 
175
- def preflight(self):
180
+ def preflight(self) -> list[PreflightResult]:
176
181
  warnings = self._check_pattern_startswith_slash()
177
182
  route = self._route
178
183
  if "(?P<" in route or route.startswith("^") or route.endswith("$"):
@@ -187,28 +192,34 @@ class RoutePattern(CheckURLMixin):
187
192
  )
188
193
  return warnings
189
194
 
190
- def _compile(self, route):
195
+ def _compile(self, route: str) -> re.Pattern[str]:
191
196
  return re.compile(_route_to_regex(route, self._is_endpoint)[0])
192
197
 
193
- def __str__(self):
198
+ def __str__(self) -> str:
194
199
  return str(self._route)
195
200
 
196
201
 
197
202
  class URLPattern:
198
- def __init__(self, *, pattern, view, name=None):
203
+ def __init__(
204
+ self,
205
+ *,
206
+ pattern: RegexPattern | RoutePattern,
207
+ view: Any,
208
+ name: str | None = None,
209
+ ):
199
210
  self.pattern = pattern
200
211
  self.view = view
201
212
  self.name = name
202
213
 
203
- def __repr__(self):
214
+ def __repr__(self) -> str:
204
215
  return f"<{self.__class__.__name__} {self.pattern.describe()}>"
205
216
 
206
- def preflight(self):
217
+ def preflight(self) -> list[PreflightResult]:
207
218
  warnings = self._check_pattern_name()
208
219
  warnings.extend(self.pattern.preflight())
209
220
  return warnings
210
221
 
211
- def _check_pattern_name(self):
222
+ def _check_pattern_name(self) -> list[PreflightResult]:
212
223
  """
213
224
  Check that the pattern name does not contain a colon.
214
225
  """
@@ -223,7 +234,7 @@ class URLPattern:
223
234
  else:
224
235
  return []
225
236
 
226
- def resolve(self, path):
237
+ def resolve(self, path: str) -> Any:
227
238
  match = self.pattern.match(path)
228
239
  if match:
229
240
  new_path, args, captured_kwargs = match
@@ -236,3 +247,4 @@ class URLPattern:
236
247
  url_name=self.pattern.name,
237
248
  route=str(self.pattern),
238
249
  )
250
+ return None
plain/urls/resolvers.py CHANGED
@@ -6,9 +6,12 @@ a string) and returns a ResolverMatch object which provides access to all
6
6
  attributes of the resolved URL match.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import functools
10
12
  import re
11
13
  from threading import local
14
+ from typing import TYPE_CHECKING, Any
12
15
  from urllib.parse import quote
13
16
 
14
17
  from plain.runtime import settings
@@ -18,19 +21,24 @@ from plain.utils.module_loading import import_string
18
21
  from plain.utils.regex_helper import normalize
19
22
 
20
23
  from .exceptions import NoReverseMatch, Resolver404
21
- from .patterns import RegexPattern, URLPattern
24
+ from .patterns import RegexPattern, RoutePattern, URLPattern
25
+
26
+ if TYPE_CHECKING:
27
+ from plain.preflight import PreflightResult
28
+
29
+ from .routers import Router
22
30
 
23
31
 
24
32
  class ResolverMatch:
25
33
  def __init__(
26
34
  self,
27
35
  *,
28
- view,
29
- args,
30
- kwargs,
31
- url_name=None,
32
- namespaces=None,
33
- route=None,
36
+ view: Any,
37
+ args: tuple[Any, ...],
38
+ kwargs: dict[str, Any],
39
+ url_name: str | None = None,
40
+ namespaces: list[str] | None = None,
41
+ route: str | None = None,
34
42
  ):
35
43
  self.view = view
36
44
  self.args = args
@@ -48,7 +56,7 @@ class ResolverMatch:
48
56
  )
49
57
 
50
58
 
51
- def get_resolver(router=None):
59
+ def get_resolver(router: str | Router | None = None) -> URLResolver:
52
60
  if router is None:
53
61
  router = settings.URLS_ROUTER
54
62
 
@@ -56,7 +64,7 @@ def get_resolver(router=None):
56
64
 
57
65
 
58
66
  @functools.cache
59
- def _get_cached_resolver(router):
67
+ def _get_cached_resolver(router: str | Router) -> URLResolver:
60
68
  if isinstance(router, str):
61
69
  # Do this inside the cached call, primarily for the URLS_ROUTER
62
70
  router_class = import_string(router)
@@ -66,7 +74,9 @@ def _get_cached_resolver(router):
66
74
 
67
75
 
68
76
  @functools.cache
69
- def get_ns_resolver(ns_pattern, resolver, converters):
77
+ def get_ns_resolver(
78
+ ns_pattern: str, resolver: URLResolver, converters: tuple[tuple[str, Any], ...]
79
+ ) -> URLResolver:
70
80
  from .routers import Router
71
81
 
72
82
  # Build a namespaced resolver for the given parent urls_module pattern.
@@ -95,13 +105,13 @@ class URLResolver:
95
105
  def __init__(
96
106
  self,
97
107
  *,
98
- pattern,
99
- router,
108
+ pattern: RegexPattern | RoutePattern,
109
+ router: Router,
100
110
  ):
101
111
  self.pattern = pattern
102
112
  self.router = router
103
- self._reverse_dict = {}
104
- self._namespace_dict = {}
113
+ self._reverse_dict: dict[Any, Any] = {}
114
+ self._namespace_dict: dict[str, tuple[str, URLResolver]] = {}
105
115
  self._populated = False
106
116
  self._local = local()
107
117
 
@@ -110,17 +120,17 @@ class URLResolver:
110
120
  self.namespace = self.router.namespace
111
121
  self.url_patterns = self.router.urls
112
122
 
113
- def __repr__(self):
123
+ def __repr__(self) -> str:
114
124
  return f"<{self.__class__.__name__} {repr(self.router)} ({self.namespace}) {self.pattern.describe()}>"
115
125
 
116
- def preflight(self):
126
+ def preflight(self) -> list[PreflightResult]:
117
127
  messages = []
118
128
  messages.extend(self.pattern.preflight())
119
129
  for pattern in self.url_patterns:
120
130
  messages.extend(pattern.preflight())
121
131
  return messages
122
132
 
123
- def _populate(self):
133
+ def _populate(self) -> None:
124
134
  # Short-circuit if called recursively in this thread to prevent
125
135
  # infinite recursion. Concurrent threads may call this at the same
126
136
  # time and will need to continue, so set 'populating' on a
@@ -191,26 +201,26 @@ class URLResolver:
191
201
  self._local.populating = False
192
202
 
193
203
  @property
194
- def reverse_dict(self):
204
+ def reverse_dict(self) -> MultiValueDict:
195
205
  if not self._reverse_dict:
196
206
  self._populate()
197
207
  return self._reverse_dict
198
208
 
199
209
  @property
200
- def namespace_dict(self):
210
+ def namespace_dict(self) -> dict[str, tuple[str, URLResolver]]:
201
211
  if not self._namespace_dict:
202
212
  self._populate()
203
213
  return self._namespace_dict
204
214
 
205
215
  @staticmethod
206
- def _join_route(route1, route2):
216
+ def _join_route(route1: str, route2: str) -> str:
207
217
  """Join two routes, without the starting ^ in the second route."""
208
218
  if not route1:
209
219
  return route2
210
220
  route2 = route2.removeprefix("^")
211
221
  return route1 + route2
212
222
 
213
- def resolve(self, path):
223
+ def resolve(self, path: str) -> ResolverMatch:
214
224
  path = str(path) # path may be a reverse_lazy object
215
225
  match = self.pattern.match(path)
216
226
  if match:
@@ -247,7 +257,7 @@ class URLResolver:
247
257
  raise Resolver404({"path": new_path})
248
258
  raise Resolver404({"path": path})
249
259
 
250
- def reverse(self, lookup_view, *args, **kwargs):
260
+ def reverse(self, lookup_view: Any, *args: Any, **kwargs: Any) -> str:
251
261
  if args and kwargs:
252
262
  raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
253
263
 
plain/urls/utils.py CHANGED
@@ -1,10 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
1
5
  from plain.utils.functional import lazy
2
6
 
3
7
  from .exceptions import NoReverseMatch
4
8
  from .resolvers import get_ns_resolver, get_resolver
5
9
 
6
10
 
7
- def reverse(url_name: str, *args, **kwargs):
11
+ def reverse(url_name: str, *args: Any, **kwargs: Any) -> str:
8
12
  resolver = get_resolver()
9
13
 
10
14
  *path, view = url_name.split(":")
plain/utils/cache.py CHANGED
@@ -15,16 +15,22 @@ An example: i18n middleware would need to distinguish caches by the
15
15
  "Accept-language" header.
16
16
  """
17
17
 
18
+ from __future__ import annotations
19
+
18
20
  import time
19
21
  from collections import defaultdict
22
+ from typing import TYPE_CHECKING, Any
20
23
 
21
24
  from .http import http_date
22
25
  from .regex_helper import _lazy_re_compile
23
26
 
27
+ if TYPE_CHECKING:
28
+ from plain.http import Response
29
+
24
30
  cc_delim_re = _lazy_re_compile(r"\s*,\s*")
25
31
 
26
32
 
27
- def patch_response_headers(response, cache_timeout):
33
+ def patch_response_headers(response: Response, cache_timeout: int | float) -> None:
28
34
  """
29
35
  Add HTTP caching headers to the given HttpResponse: Expires and
30
36
  Cache-Control.
@@ -38,7 +44,7 @@ def patch_response_headers(response, cache_timeout):
38
44
  patch_cache_control(response, max_age=cache_timeout)
39
45
 
40
46
 
41
- def add_never_cache_headers(response):
47
+ def add_never_cache_headers(response: Response) -> None:
42
48
  """
43
49
  Add headers to a response to indicate that a page should never be cached.
44
50
  """
@@ -48,7 +54,7 @@ def add_never_cache_headers(response):
48
54
  )
49
55
 
50
56
 
51
- def patch_cache_control(response, **kwargs):
57
+ def patch_cache_control(response: Response, **kwargs: Any) -> None:
52
58
  """
53
59
  Patch the Cache-Control header by adding all keyword arguments to it.
54
60
  The transformation is as follows:
@@ -61,16 +67,16 @@ def patch_cache_control(response, **kwargs):
61
67
  str() to it.
62
68
  """
63
69
 
64
- def dictitem(s):
70
+ def dictitem(s: str) -> tuple[str, str | bool]:
65
71
  t = s.split("=", 1)
66
72
  if len(t) > 1:
67
73
  return (t[0].lower(), t[1])
68
74
  else:
69
75
  return (t[0].lower(), True)
70
76
 
71
- def dictvalue(*t):
77
+ def dictvalue(*t: str | bool) -> str:
72
78
  if t[1] is True:
73
- return t[0]
79
+ return str(t[0])
74
80
  else:
75
81
  return f"{t[0]}={t[1]}"
76
82
 
@@ -117,7 +123,7 @@ def patch_cache_control(response, **kwargs):
117
123
  response.headers["Cache-Control"] = cc
118
124
 
119
125
 
120
- def patch_vary_headers(response, newheaders):
126
+ def patch_vary_headers(response: Response, newheaders: list[str]) -> None:
121
127
  """
122
128
  Add (or update) the "Vary" header in the given Response object.
123
129
  newheaders is a list of header names that should be in "Vary". If headers
@@ -145,7 +151,7 @@ def patch_vary_headers(response, newheaders):
145
151
  response.headers["Vary"] = ", ".join(vary_headers)
146
152
 
147
153
 
148
- def _to_tuple(s):
154
+ def _to_tuple(s: str) -> tuple[str, str | bool]:
149
155
  t = s.split("=", 1)
150
156
  if len(t) == 2:
151
157
  return t[0].lower(), t[1]
plain/utils/crypto.py CHANGED
@@ -2,9 +2,13 @@
2
2
  Plain's standard crypto functions and utilities.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import hashlib
6
8
  import hmac
7
9
  import secrets
10
+ from collections.abc import Callable
11
+ from typing import Any
8
12
 
9
13
  from plain.runtime import settings
10
14
  from plain.utils.encoding import force_bytes
@@ -16,7 +20,13 @@ class InvalidAlgorithm(ValueError):
16
20
  pass
17
21
 
18
22
 
19
- def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
23
+ def salted_hmac(
24
+ key_salt: str | bytes,
25
+ value: str | bytes,
26
+ secret: str | bytes | None = None,
27
+ *,
28
+ algorithm: str = "sha1",
29
+ ) -> hmac.HMAC:
20
30
  """
21
31
  Return the HMAC of 'value', using a key generated from key_salt and a
22
32
  secret (which defaults to settings.SECRET_KEY). Default algorithm is SHA1,
@@ -48,7 +58,7 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
48
58
  RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
49
59
 
50
60
 
51
- def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
61
+ def get_random_string(length: int, allowed_chars: str = RANDOM_STRING_CHARS) -> str:
52
62
  """
53
63
  Return a securely generated random string.
54
64
 
@@ -62,11 +72,17 @@ def get_random_string(length, allowed_chars=RANDOM_STRING_CHARS):
62
72
  return "".join(secrets.choice(allowed_chars) for i in range(length))
63
73
 
64
74
 
65
- def pbkdf2(password, salt, iterations, dklen=0, digest=None):
75
+ def pbkdf2(
76
+ password: str | bytes,
77
+ salt: str | bytes,
78
+ iterations: int,
79
+ dklen: int = 0,
80
+ digest: Callable[[], Any] | None = None,
81
+ ) -> bytes:
66
82
  """Return the hash of password using pbkdf2."""
67
83
  if digest is None:
68
84
  digest = hashlib.sha256
69
- dklen = dklen or None
85
+ dklen_value: int | None = dklen if dklen else None
70
86
  password = force_bytes(password)
71
87
  salt = force_bytes(salt)
72
- return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen)
88
+ return hashlib.pbkdf2_hmac(digest().name, password, salt, iterations, dklen_value)