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.
- plain/AGENTS.md +1 -1
- plain/CHANGELOG.md +28 -0
- plain/assets/compile.py +20 -7
- plain/assets/finders.py +15 -11
- plain/assets/fingerprints.py +6 -5
- plain/assets/urls.py +1 -1
- plain/assets/views.py +23 -17
- plain/chores/registry.py +14 -9
- plain/cli/agent/__init__.py +1 -1
- plain/cli/agent/docs.py +7 -6
- plain/cli/agent/llmdocs.py +18 -8
- plain/cli/agent/md.py +19 -14
- plain/cli/agent/prompt.py +1 -1
- plain/cli/agent/request.py +37 -17
- plain/cli/build.py +2 -2
- plain/cli/changelog.py +8 -4
- plain/cli/chores.py +4 -4
- plain/cli/core.py +8 -5
- plain/cli/docs.py +2 -2
- plain/cli/formatting.py +10 -7
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +3 -3
- plain/cli/print.py +1 -1
- plain/cli/registry.py +10 -6
- plain/cli/scaffold.py +1 -1
- plain/cli/settings.py +1 -1
- plain/cli/shell.py +10 -7
- plain/cli/startup.py +3 -3
- plain/cli/urls.py +10 -4
- plain/cli/utils.py +2 -2
- plain/csrf/middleware.py +15 -5
- plain/csrf/views.py +11 -8
- plain/debug.py +5 -2
- plain/exceptions.py +19 -8
- plain/forms/__init__.py +1 -1
- plain/forms/boundfield.py +14 -7
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +139 -97
- plain/forms/forms.py +55 -39
- plain/http/README.md +1 -1
- plain/http/__init__.py +4 -4
- plain/http/cookie.py +15 -7
- plain/http/multipartparser.py +50 -30
- plain/http/request.py +156 -108
- plain/http/response.py +99 -80
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +34 -18
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +23 -5
- plain/internal/files/uploadedfile.py +42 -26
- plain/internal/files/uploadhandler.py +50 -29
- plain/internal/files/utils.py +13 -6
- plain/internal/handlers/base.py +21 -7
- plain/internal/handlers/exception.py +19 -5
- plain/internal/handlers/wsgi.py +33 -21
- plain/internal/middleware/headers.py +11 -2
- plain/internal/middleware/hosts.py +12 -4
- plain/internal/middleware/https.py +13 -3
- plain/internal/middleware/slash.py +15 -5
- plain/json.py +2 -1
- plain/logs/configure.py +3 -1
- plain/logs/debug.py +16 -5
- plain/logs/formatters.py +6 -3
- plain/logs/loggers.py +56 -52
- plain/logs/utils.py +19 -9
- plain/packages/config.py +14 -6
- plain/packages/registry.py +27 -12
- plain/paginator.py +31 -21
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +25 -10
- plain/preflight/results.py +10 -4
- plain/preflight/security.py +7 -5
- plain/preflight/urls.py +4 -1
- plain/runtime/__init__.py +7 -6
- plain/runtime/global_settings.py +6 -9
- plain/runtime/user_settings.py +26 -17
- plain/runtime/utils.py +1 -1
- plain/signals/dispatch/dispatcher.py +39 -17
- plain/signing.py +49 -30
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +4 -3
- plain/templates/jinja/extensions.py +9 -3
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +1 -1
- plain/test/client.py +249 -177
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +10 -2
- plain/urls/converters.py +13 -10
- plain/urls/patterns.py +32 -20
- plain/urls/resolvers.py +32 -22
- plain/urls/utils.py +5 -1
- plain/utils/cache.py +14 -8
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +84 -54
- plain/utils/dateparse.py +10 -7
- plain/utils/deconstruct.py +12 -4
- plain/utils/decorators.py +5 -1
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +62 -47
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +21 -14
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +23 -13
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +34 -18
- plain/utils/timezone.py +30 -19
- plain/utils/tree.py +31 -18
- plain/validators.py +71 -44
- plain/views/base.py +16 -8
- plain/views/errors.py +11 -4
- plain/views/exceptions.py +4 -1
- plain/views/objects.py +15 -15
- plain/views/redirect.py +14 -10
- plain/views/templates.py +1 -1
- plain/wsgi.py +3 -1
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/METADATA +1 -1
- plain-0.71.0.dist-info/RECORD +169 -0
- plain-0.69.0.dist-info/RECORD +0 -169
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/WHEEL +0 -0
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/entry_points.txt +0 -0
- {plain-0.69.0.dist-info → plain-0.71.0.dist-info}/licenses/LICENSE +0 -0
plain/validators.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import ipaddress
|
2
4
|
import math
|
3
5
|
import re
|
6
|
+
from collections.abc import Callable
|
4
7
|
from pathlib import Path
|
8
|
+
from typing import Any
|
5
9
|
from urllib.parse import urlsplit, urlunsplit
|
6
10
|
|
7
11
|
from plain.exceptions import ValidationError
|
@@ -24,8 +28,13 @@ class RegexValidator:
|
|
24
28
|
flags = 0
|
25
29
|
|
26
30
|
def __init__(
|
27
|
-
self,
|
28
|
-
|
31
|
+
self,
|
32
|
+
regex: str | re.Pattern[str] | None = None,
|
33
|
+
message: str | None = None,
|
34
|
+
code: str | None = None,
|
35
|
+
inverse_match: bool | None = None,
|
36
|
+
flags: int | None = None,
|
37
|
+
) -> None:
|
29
38
|
if regex is not None:
|
30
39
|
self.regex = regex
|
31
40
|
if message is not None:
|
@@ -43,7 +52,7 @@ class RegexValidator:
|
|
43
52
|
|
44
53
|
self.regex = _lazy_re_compile(self.regex, self.flags)
|
45
54
|
|
46
|
-
def __call__(self, value):
|
55
|
+
def __call__(self, value: Any) -> None:
|
47
56
|
"""
|
48
57
|
Validate that the input contains (or does *not* contain, if
|
49
58
|
inverse_match is True) a match for the regular expression.
|
@@ -53,11 +62,11 @@ class RegexValidator:
|
|
53
62
|
if invalid_input:
|
54
63
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
55
64
|
|
56
|
-
def __eq__(self, other):
|
65
|
+
def __eq__(self, other: object) -> bool:
|
57
66
|
return (
|
58
67
|
isinstance(other, RegexValidator)
|
59
|
-
and self.regex.pattern == other.regex.pattern
|
60
|
-
and self.regex.flags == other.regex.flags
|
68
|
+
and self.regex.pattern == other.regex.pattern # type: ignore[attr-defined]
|
69
|
+
and self.regex.flags == other.regex.flags # type: ignore[attr-defined]
|
61
70
|
and (self.message == other.message)
|
62
71
|
and (self.code == other.code)
|
63
72
|
and (self.inverse_match == other.inverse_match)
|
@@ -104,12 +113,12 @@ class URLValidator(RegexValidator):
|
|
104
113
|
schemes = ["http", "https", "ftp", "ftps"]
|
105
114
|
unsafe_chars = frozenset("\t\r\n")
|
106
115
|
|
107
|
-
def __init__(self, schemes=None, **kwargs):
|
116
|
+
def __init__(self, schemes: list[str] | None = None, **kwargs: Any) -> None:
|
108
117
|
super().__init__(**kwargs)
|
109
118
|
if schemes is not None:
|
110
119
|
self.schemes = schemes
|
111
120
|
|
112
|
-
def __call__(self, value):
|
121
|
+
def __call__(self, value: Any) -> None:
|
113
122
|
if not isinstance(value, str):
|
114
123
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
115
124
|
if self.unsafe_chars.intersection(value):
|
@@ -182,7 +191,12 @@ class EmailValidator:
|
|
182
191
|
)
|
183
192
|
domain_allowlist = ["localhost"]
|
184
193
|
|
185
|
-
def __init__(
|
194
|
+
def __init__(
|
195
|
+
self,
|
196
|
+
message: str | None = None,
|
197
|
+
code: str | None = None,
|
198
|
+
allowlist: list[str] | None = None,
|
199
|
+
) -> None:
|
186
200
|
if message is not None:
|
187
201
|
self.message = message
|
188
202
|
if code is not None:
|
@@ -190,7 +204,7 @@ class EmailValidator:
|
|
190
204
|
if allowlist is not None:
|
191
205
|
self.domain_allowlist = allowlist
|
192
206
|
|
193
|
-
def __call__(self, value):
|
207
|
+
def __call__(self, value: Any) -> None:
|
194
208
|
if not value or "@" not in value:
|
195
209
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
196
210
|
|
@@ -209,10 +223,11 @@ class EmailValidator:
|
|
209
223
|
pass
|
210
224
|
else:
|
211
225
|
if self.validate_domain_part(domain_part):
|
212
|
-
return
|
226
|
+
return None
|
213
227
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
228
|
+
return None
|
214
229
|
|
215
|
-
def validate_domain_part(self, domain_part):
|
230
|
+
def validate_domain_part(self, domain_part: str) -> bool:
|
216
231
|
if self.domain_regex.match(domain_part):
|
217
232
|
return True
|
218
233
|
|
@@ -226,7 +241,7 @@ class EmailValidator:
|
|
226
241
|
pass
|
227
242
|
return False
|
228
243
|
|
229
|
-
def __eq__(self, other):
|
244
|
+
def __eq__(self, other: object) -> bool:
|
230
245
|
return (
|
231
246
|
isinstance(other, EmailValidator)
|
232
247
|
and (self.domain_allowlist == other.domain_allowlist)
|
@@ -238,7 +253,7 @@ class EmailValidator:
|
|
238
253
|
validate_email = EmailValidator()
|
239
254
|
|
240
255
|
|
241
|
-
def validate_ipv4_address(value):
|
256
|
+
def validate_ipv4_address(value: str) -> None:
|
242
257
|
try:
|
243
258
|
ipaddress.IPv4Address(value)
|
244
259
|
except ValueError:
|
@@ -247,14 +262,14 @@ def validate_ipv4_address(value):
|
|
247
262
|
)
|
248
263
|
|
249
264
|
|
250
|
-
def validate_ipv6_address(value):
|
265
|
+
def validate_ipv6_address(value: str) -> None:
|
251
266
|
if not is_valid_ipv6_address(value):
|
252
267
|
raise ValidationError(
|
253
268
|
"Enter a valid IPv6 address.", code="invalid", params={"value": value}
|
254
269
|
)
|
255
270
|
|
256
271
|
|
257
|
-
def validate_ipv46_address(value):
|
272
|
+
def validate_ipv46_address(value: str) -> None:
|
258
273
|
try:
|
259
274
|
validate_ipv4_address(value)
|
260
275
|
except ValidationError:
|
@@ -275,7 +290,9 @@ ip_address_validator_map = {
|
|
275
290
|
}
|
276
291
|
|
277
292
|
|
278
|
-
def ip_address_validators(
|
293
|
+
def ip_address_validators(
|
294
|
+
protocol: str, unpack_ipv4: bool
|
295
|
+
) -> tuple[list[Callable[[str], None]], str]:
|
279
296
|
"""
|
280
297
|
Depending on the given parameters, return the appropriate validators for
|
281
298
|
the GenericIPAddressField.
|
@@ -292,14 +309,19 @@ def ip_address_validators(protocol, unpack_ipv4):
|
|
292
309
|
)
|
293
310
|
|
294
311
|
|
295
|
-
def int_list_validator(
|
312
|
+
def int_list_validator(
|
313
|
+
sep: str = ",",
|
314
|
+
message: str | None = None,
|
315
|
+
code: str = "invalid",
|
316
|
+
allow_negative: bool = False,
|
317
|
+
) -> RegexValidator:
|
296
318
|
regexp = _lazy_re_compile(
|
297
319
|
r"^{neg}\d+(?:{sep}{neg}\d+)*\Z".format(
|
298
320
|
neg="(-)?" if allow_negative else "",
|
299
321
|
sep=re.escape(sep),
|
300
322
|
)
|
301
323
|
)
|
302
|
-
return RegexValidator(regexp, message=message, code=code)
|
324
|
+
return RegexValidator(regexp, message=message, code=code) # type: ignore[arg-type]
|
303
325
|
|
304
326
|
|
305
327
|
validate_comma_separated_integer_list = int_list_validator(
|
@@ -312,12 +334,12 @@ class BaseValidator:
|
|
312
334
|
message = "Ensure this value is %(limit_value)s (it is %(show_value)s)."
|
313
335
|
code = "limit_value"
|
314
336
|
|
315
|
-
def __init__(self, limit_value, message=None):
|
337
|
+
def __init__(self, limit_value: Any, message: str | None = None) -> None:
|
316
338
|
self.limit_value = limit_value
|
317
339
|
if message:
|
318
340
|
self.message = message
|
319
341
|
|
320
|
-
def __call__(self, value):
|
342
|
+
def __call__(self, value: Any) -> None:
|
321
343
|
cleaned = self.clean(value)
|
322
344
|
limit_value = (
|
323
345
|
self.limit_value() if callable(self.limit_value) else self.limit_value
|
@@ -326,7 +348,7 @@ class BaseValidator:
|
|
326
348
|
if self.compare(cleaned, limit_value):
|
327
349
|
raise ValidationError(self.message, code=self.code, params=params)
|
328
350
|
|
329
|
-
def __eq__(self, other):
|
351
|
+
def __eq__(self, other: object) -> bool:
|
330
352
|
if not isinstance(other, self.__class__):
|
331
353
|
return NotImplemented
|
332
354
|
return (
|
@@ -335,10 +357,10 @@ class BaseValidator:
|
|
335
357
|
and self.code == other.code
|
336
358
|
)
|
337
359
|
|
338
|
-
def compare(self, a, b):
|
360
|
+
def compare(self, a: Any, b: Any) -> bool:
|
339
361
|
return a is not b
|
340
362
|
|
341
|
-
def clean(self, x):
|
363
|
+
def clean(self, x: Any) -> Any:
|
342
364
|
return x
|
343
365
|
|
344
366
|
|
@@ -347,7 +369,7 @@ class MaxValueValidator(BaseValidator):
|
|
347
369
|
message = "Ensure this value is less than or equal to %(limit_value)s."
|
348
370
|
code = "max_value"
|
349
371
|
|
350
|
-
def compare(self, a, b):
|
372
|
+
def compare(self, a: Any, b: Any) -> bool:
|
351
373
|
return a > b
|
352
374
|
|
353
375
|
|
@@ -356,7 +378,7 @@ class MinValueValidator(BaseValidator):
|
|
356
378
|
message = "Ensure this value is greater than or equal to %(limit_value)s."
|
357
379
|
code = "min_value"
|
358
380
|
|
359
|
-
def compare(self, a, b):
|
381
|
+
def compare(self, a: Any, b: Any) -> bool:
|
360
382
|
return a < b
|
361
383
|
|
362
384
|
|
@@ -365,7 +387,7 @@ class StepValueValidator(BaseValidator):
|
|
365
387
|
message = "Ensure this value is a multiple of step size %(limit_value)s."
|
366
388
|
code = "step_size"
|
367
389
|
|
368
|
-
def compare(self, a, b):
|
390
|
+
def compare(self, a: Any, b: Any) -> bool:
|
369
391
|
return not math.isclose(math.remainder(a, b), 0, abs_tol=1e-9)
|
370
392
|
|
371
393
|
|
@@ -380,10 +402,10 @@ class MinLengthValidator(BaseValidator):
|
|
380
402
|
)
|
381
403
|
code = "min_length"
|
382
404
|
|
383
|
-
def compare(self, a, b):
|
405
|
+
def compare(self, a: Any, b: Any) -> bool:
|
384
406
|
return a < b
|
385
407
|
|
386
|
-
def clean(self, x):
|
408
|
+
def clean(self, x: Any) -> int:
|
387
409
|
return len(x)
|
388
410
|
|
389
411
|
|
@@ -398,10 +420,10 @@ class MaxLengthValidator(BaseValidator):
|
|
398
420
|
)
|
399
421
|
code = "max_length"
|
400
422
|
|
401
|
-
def compare(self, a, b):
|
423
|
+
def compare(self, a: Any, b: Any) -> bool:
|
402
424
|
return a > b
|
403
425
|
|
404
|
-
def clean(self, x):
|
426
|
+
def clean(self, x: Any) -> int:
|
405
427
|
return len(x)
|
406
428
|
|
407
429
|
|
@@ -433,11 +455,11 @@ class DecimalValidator:
|
|
433
455
|
),
|
434
456
|
}
|
435
457
|
|
436
|
-
def __init__(self, max_digits, decimal_places):
|
458
|
+
def __init__(self, max_digits: int | None, decimal_places: int | None) -> None:
|
437
459
|
self.max_digits = max_digits
|
438
460
|
self.decimal_places = decimal_places
|
439
461
|
|
440
|
-
def __call__(self, value):
|
462
|
+
def __call__(self, value: Any) -> None:
|
441
463
|
digit_tuple, exponent = value.as_tuple()[1:]
|
442
464
|
if exponent in {"F", "n", "N"}:
|
443
465
|
raise ValidationError(
|
@@ -485,7 +507,7 @@ class DecimalValidator:
|
|
485
507
|
params={"max": (self.max_digits - self.decimal_places), "value": value},
|
486
508
|
)
|
487
509
|
|
488
|
-
def __eq__(self, other):
|
510
|
+
def __eq__(self, other: object) -> bool:
|
489
511
|
return (
|
490
512
|
isinstance(other, self.__class__)
|
491
513
|
and self.max_digits == other.max_digits
|
@@ -495,10 +517,15 @@ class DecimalValidator:
|
|
495
517
|
|
496
518
|
@deconstructible
|
497
519
|
class FileExtensionValidator:
|
498
|
-
message =
|
520
|
+
message = 'File extension "%(extension)s" is not allowed. Allowed extensions are: %(allowed_extensions)s.'
|
499
521
|
code = "invalid_extension"
|
500
522
|
|
501
|
-
def __init__(
|
523
|
+
def __init__(
|
524
|
+
self,
|
525
|
+
allowed_extensions: list[str] | None = None,
|
526
|
+
message: str | None = None,
|
527
|
+
code: str | None = None,
|
528
|
+
) -> None:
|
502
529
|
if allowed_extensions is not None:
|
503
530
|
allowed_extensions = [
|
504
531
|
allowed_extension.lower() for allowed_extension in allowed_extensions
|
@@ -509,7 +536,7 @@ class FileExtensionValidator:
|
|
509
536
|
if code is not None:
|
510
537
|
self.code = code
|
511
538
|
|
512
|
-
def __call__(self, value):
|
539
|
+
def __call__(self, value: Any) -> None:
|
513
540
|
extension = Path(value.name).suffix[1:].lower()
|
514
541
|
if (
|
515
542
|
self.allowed_extensions is not None
|
@@ -525,7 +552,7 @@ class FileExtensionValidator:
|
|
525
552
|
},
|
526
553
|
)
|
527
554
|
|
528
|
-
def __eq__(self, other):
|
555
|
+
def __eq__(self, other: object) -> bool:
|
529
556
|
return (
|
530
557
|
isinstance(other, self.__class__)
|
531
558
|
and self.allowed_extensions == other.allowed_extensions
|
@@ -534,9 +561,9 @@ class FileExtensionValidator:
|
|
534
561
|
)
|
535
562
|
|
536
563
|
|
537
|
-
def get_available_image_extensions():
|
564
|
+
def get_available_image_extensions() -> list[str]:
|
538
565
|
try:
|
539
|
-
from PIL import Image
|
566
|
+
from PIL import Image # type: ignore[import-untyped]
|
540
567
|
except ImportError:
|
541
568
|
return []
|
542
569
|
else:
|
@@ -544,7 +571,7 @@ def get_available_image_extensions():
|
|
544
571
|
return [ext.lower()[1:] for ext in Image.EXTENSION]
|
545
572
|
|
546
573
|
|
547
|
-
def validate_image_file_extension(value):
|
574
|
+
def validate_image_file_extension(value: Any) -> None:
|
548
575
|
return FileExtensionValidator(allowed_extensions=get_available_image_extensions())(
|
549
576
|
value
|
550
577
|
)
|
@@ -557,17 +584,17 @@ class ProhibitNullCharactersValidator:
|
|
557
584
|
message = "Null characters are not allowed."
|
558
585
|
code = "null_characters_not_allowed"
|
559
586
|
|
560
|
-
def __init__(self, message=None, code=None):
|
587
|
+
def __init__(self, message: str | None = None, code: str | None = None) -> None:
|
561
588
|
if message is not None:
|
562
589
|
self.message = message
|
563
590
|
if code is not None:
|
564
591
|
self.code = code
|
565
592
|
|
566
|
-
def __call__(self, value):
|
593
|
+
def __call__(self, value: Any) -> None:
|
567
594
|
if "\x00" in str(value):
|
568
595
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
569
596
|
|
570
|
-
def __eq__(self, other):
|
597
|
+
def __eq__(self, other: object) -> bool:
|
571
598
|
return (
|
572
599
|
isinstance(other, self.__class__)
|
573
600
|
and self.message == other.message
|
plain/views/base.py
CHANGED
@@ -1,5 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
4
|
+
from collections.abc import Callable
|
2
5
|
from http import HTTPMethod
|
6
|
+
from typing import Any
|
3
7
|
|
4
8
|
from opentelemetry import trace
|
5
9
|
from opentelemetry.semconv._incubating.attributes.code_attributes import (
|
@@ -8,8 +12,8 @@ from opentelemetry.semconv._incubating.attributes.code_attributes import (
|
|
8
12
|
)
|
9
13
|
|
10
14
|
from plain.http import (
|
11
|
-
HttpRequest,
|
12
15
|
JsonResponse,
|
16
|
+
Request,
|
13
17
|
Response,
|
14
18
|
ResponseBase,
|
15
19
|
ResponseNotAllowed,
|
@@ -26,14 +30,14 @@ tracer = trace.get_tracer("plain")
|
|
26
30
|
|
27
31
|
|
28
32
|
class View:
|
29
|
-
request:
|
33
|
+
request: Request
|
30
34
|
url_args: tuple
|
31
35
|
url_kwargs: dict
|
32
36
|
|
33
37
|
# View.as_view(example="foo") usage can be customized by defining your own __init__ method.
|
34
38
|
# def __init__(self, *args, **kwargs):
|
35
39
|
|
36
|
-
def setup(self, request:
|
40
|
+
def setup(self, request: Request, *url_args: object, **url_kwargs: object) -> None:
|
37
41
|
if hasattr(self, "get") and not hasattr(self, "head"):
|
38
42
|
self.head = self.get
|
39
43
|
|
@@ -42,8 +46,12 @@ class View:
|
|
42
46
|
self.url_kwargs = url_kwargs
|
43
47
|
|
44
48
|
@classonlymethod
|
45
|
-
def as_view(
|
46
|
-
|
49
|
+
def as_view(
|
50
|
+
cls, *init_args: object, **init_kwargs: object
|
51
|
+
) -> Callable[[Request, Any, Any], ResponseBase]:
|
52
|
+
def view(
|
53
|
+
request: Request, *url_args: object, **url_kwargs: object
|
54
|
+
) -> ResponseBase:
|
47
55
|
with tracer.start_as_current_span(
|
48
56
|
f"{cls.__name__}",
|
49
57
|
kind=trace.SpanKind.INTERNAL,
|
@@ -62,11 +70,11 @@ class View:
|
|
62
70
|
)
|
63
71
|
return response
|
64
72
|
|
65
|
-
view.view_class = cls
|
73
|
+
view.view_class = cls # type: ignore[attr-defined]
|
66
74
|
|
67
75
|
return view
|
68
76
|
|
69
|
-
def get_request_handler(self) ->
|
77
|
+
def get_request_handler(self) -> Callable[[], Any] | None:
|
70
78
|
"""Return the handler for the current request method."""
|
71
79
|
|
72
80
|
if not self.request.method:
|
@@ -93,7 +101,7 @@ class View:
|
|
93
101
|
|
94
102
|
return self.convert_value_to_response(result)
|
95
103
|
|
96
|
-
def convert_value_to_response(self, value) -> ResponseBase:
|
104
|
+
def convert_value_to_response(self, value: object) -> ResponseBase:
|
97
105
|
"""Convert a return value to a Response."""
|
98
106
|
if isinstance(value, ResponseBase):
|
99
107
|
return value
|
plain/views/errors.py
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
5
|
+
|
1
6
|
from plain.http import ResponseBase
|
2
7
|
from plain.templates import TemplateFileMissing
|
3
8
|
|
@@ -7,7 +12,9 @@ from .templates import TemplateView
|
|
7
12
|
class ErrorView(TemplateView):
|
8
13
|
status_code: int
|
9
14
|
|
10
|
-
def __init__(
|
15
|
+
def __init__(
|
16
|
+
self, *, status_code: int | None = None, exception: Any | None = None
|
17
|
+
) -> None:
|
11
18
|
# Allow creating an ErrorView with a status code
|
12
19
|
# e.g. ErrorView.as_view(status_code=404)
|
13
20
|
self.status_code = status_code or self.status_code
|
@@ -15,7 +22,7 @@ class ErrorView(TemplateView):
|
|
15
22
|
# Allow creating an ErrorView with an exception
|
16
23
|
self.exception = exception
|
17
24
|
|
18
|
-
def get_template_context(self):
|
25
|
+
def get_template_context(self) -> dict:
|
19
26
|
context = super().get_template_context()
|
20
27
|
context["status_code"] = self.status_code
|
21
28
|
context["exception"] = self.exception
|
@@ -24,7 +31,7 @@ class ErrorView(TemplateView):
|
|
24
31
|
def get_template_names(self) -> list[str]:
|
25
32
|
return [f"{self.status_code}.html", "error.html"]
|
26
33
|
|
27
|
-
def get_request_handler(self):
|
34
|
+
def get_request_handler(self) -> Callable[[], Any]:
|
28
35
|
return self.get # All methods (post, patch, etc.) will use the get()
|
29
36
|
|
30
37
|
def get_response(self) -> ResponseBase:
|
@@ -33,7 +40,7 @@ class ErrorView(TemplateView):
|
|
33
40
|
response.status_code = self.status_code
|
34
41
|
return response
|
35
42
|
|
36
|
-
def get(self):
|
43
|
+
def get(self) -> ResponseBase:
|
37
44
|
try:
|
38
45
|
return super().get()
|
39
46
|
except TemplateFileMissing:
|
plain/views/exceptions.py
CHANGED
plain/views/objects.py
CHANGED
@@ -6,7 +6,7 @@ from plain.exceptions import ImproperlyConfigured
|
|
6
6
|
try:
|
7
7
|
from plain.models.exceptions import ObjectDoesNotExist
|
8
8
|
except ImportError:
|
9
|
-
ObjectDoesNotExist = None
|
9
|
+
ObjectDoesNotExist = None # type: ignore[assignment]
|
10
10
|
|
11
11
|
from plain.forms import Form
|
12
12
|
from plain.http import Http404
|
@@ -21,7 +21,7 @@ class CreateView(FormView):
|
|
21
21
|
"""
|
22
22
|
|
23
23
|
# TODO? would rather you have to specify this...
|
24
|
-
def get_success_url(self, form):
|
24
|
+
def get_success_url(self, form: Form) -> str:
|
25
25
|
"""Return the URL to redirect to after processing a valid form."""
|
26
26
|
if self.success_url:
|
27
27
|
url = self.success_url.format(**self.object.__dict__)
|
@@ -35,9 +35,9 @@ class CreateView(FormView):
|
|
35
35
|
)
|
36
36
|
return url
|
37
37
|
|
38
|
-
def form_valid(self, form):
|
38
|
+
def form_valid(self, form: Form) -> Any:
|
39
39
|
"""If the form is valid, save the associated model."""
|
40
|
-
self.object = form.save()
|
40
|
+
self.object = form.save() # type: ignore[attr-defined]
|
41
41
|
return super().form_valid(form)
|
42
42
|
|
43
43
|
|
@@ -91,7 +91,7 @@ class DetailView(ObjectTemplateViewMixin, TemplateView):
|
|
91
91
|
class UpdateView(ObjectTemplateViewMixin, FormView):
|
92
92
|
"""View for updating an object, with a response rendered by a template."""
|
93
93
|
|
94
|
-
def get_success_url(self, form):
|
94
|
+
def get_success_url(self, form: Form) -> str:
|
95
95
|
"""Return the URL to redirect to after processing a valid form."""
|
96
96
|
if self.success_url:
|
97
97
|
url = self.success_url.format(**self.object.__dict__)
|
@@ -105,12 +105,12 @@ class UpdateView(ObjectTemplateViewMixin, FormView):
|
|
105
105
|
)
|
106
106
|
return url
|
107
107
|
|
108
|
-
def form_valid(self, form):
|
108
|
+
def form_valid(self, form: Form) -> Any:
|
109
109
|
"""If the form is valid, save the associated model."""
|
110
|
-
form.save()
|
110
|
+
form.save() # type: ignore[attr-defined]
|
111
111
|
return super().form_valid(form)
|
112
112
|
|
113
|
-
def get_form_kwargs(self):
|
113
|
+
def get_form_kwargs(self) -> dict:
|
114
114
|
"""Return the keyword arguments for instantiating the form."""
|
115
115
|
kwargs = super().get_form_kwargs()
|
116
116
|
kwargs.update({"instance": self.object})
|
@@ -124,24 +124,24 @@ class DeleteView(ObjectTemplateViewMixin, FormView):
|
|
124
124
|
"""
|
125
125
|
|
126
126
|
class EmptyDeleteForm(Form):
|
127
|
-
def __init__(self, instance, *args, **kwargs):
|
127
|
+
def __init__(self, instance: Any, *args: object, **kwargs: object) -> None:
|
128
128
|
self.instance = instance
|
129
129
|
super().__init__(*args, **kwargs)
|
130
130
|
|
131
|
-
def save(self):
|
131
|
+
def save(self) -> None:
|
132
132
|
self.instance.delete()
|
133
133
|
|
134
134
|
form_class = EmptyDeleteForm
|
135
135
|
|
136
|
-
def get_form_kwargs(self):
|
136
|
+
def get_form_kwargs(self) -> dict:
|
137
137
|
"""Return the keyword arguments for instantiating the form."""
|
138
138
|
kwargs = super().get_form_kwargs()
|
139
139
|
kwargs.update({"instance": self.object})
|
140
140
|
return kwargs
|
141
141
|
|
142
|
-
def form_valid(self, form):
|
142
|
+
def form_valid(self, form: Form) -> Any:
|
143
143
|
"""If the form is valid, save the associated model."""
|
144
|
-
form.save()
|
144
|
+
form.save() # type: ignore[attr-defined]
|
145
145
|
return super().form_valid(form)
|
146
146
|
|
147
147
|
|
@@ -154,10 +154,10 @@ class ListView(TemplateView):
|
|
154
154
|
context_object_name = ""
|
155
155
|
|
156
156
|
@cached_property
|
157
|
-
def objects(self):
|
157
|
+
def objects(self) -> Any:
|
158
158
|
return self.get_objects()
|
159
159
|
|
160
|
-
def get_objects(self):
|
160
|
+
def get_objects(self) -> Any:
|
161
161
|
raise NotImplementedError(
|
162
162
|
f"get_objects() is not implemented on {self.__class__.__name__}"
|
163
163
|
)
|
plain/views/redirect.py
CHANGED
@@ -13,8 +13,12 @@ class RedirectView(View):
|
|
13
13
|
preserve_query_params = False
|
14
14
|
|
15
15
|
def __init__(
|
16
|
-
self,
|
17
|
-
|
16
|
+
self,
|
17
|
+
url: str | None = None,
|
18
|
+
status_code: int | None = None,
|
19
|
+
url_name: str | None = None,
|
20
|
+
preserve_query_params: bool | None = None,
|
21
|
+
) -> None:
|
18
22
|
# Allow attributes to be set in RedirectView.as_view(url="...", status_code=301, etc.)
|
19
23
|
self.url = url or self.url
|
20
24
|
self.status_code = status_code if status_code is not None else self.status_code
|
@@ -25,7 +29,7 @@ class RedirectView(View):
|
|
25
29
|
else self.preserve_query_params
|
26
30
|
)
|
27
31
|
|
28
|
-
def get_redirect_url(self):
|
32
|
+
def get_redirect_url(self) -> str:
|
29
33
|
"""
|
30
34
|
Return the URL redirect to. Keyword arguments from the URL pattern
|
31
35
|
match generating the redirect request are provided as kwargs to this
|
@@ -43,24 +47,24 @@ class RedirectView(View):
|
|
43
47
|
url = f"{url}?{args}"
|
44
48
|
return url
|
45
49
|
|
46
|
-
def get(self):
|
50
|
+
def get(self) -> ResponseRedirect:
|
47
51
|
url = self.get_redirect_url()
|
48
52
|
return ResponseRedirect(url, status_code=self.status_code)
|
49
53
|
|
50
|
-
def head(self):
|
54
|
+
def head(self) -> ResponseRedirect:
|
51
55
|
return self.get()
|
52
56
|
|
53
|
-
def post(self):
|
57
|
+
def post(self) -> ResponseRedirect:
|
54
58
|
return self.get()
|
55
59
|
|
56
|
-
def options(self):
|
60
|
+
def options(self) -> ResponseRedirect:
|
57
61
|
return self.get()
|
58
62
|
|
59
|
-
def delete(self):
|
63
|
+
def delete(self) -> ResponseRedirect:
|
60
64
|
return self.get()
|
61
65
|
|
62
|
-
def put(self):
|
66
|
+
def put(self) -> ResponseRedirect:
|
63
67
|
return self.get()
|
64
68
|
|
65
|
-
def patch(self):
|
69
|
+
def patch(self) -> ResponseRedirect:
|
66
70
|
return self.get()
|
plain/views/templates.py
CHANGED
@@ -13,7 +13,7 @@ class TemplateView(View):
|
|
13
13
|
|
14
14
|
template_name: str | None = None
|
15
15
|
|
16
|
-
def __init__(self, template_name=None):
|
16
|
+
def __init__(self, template_name: str | None = None) -> None:
|
17
17
|
# Allow template_name to be passed in as_view()
|
18
18
|
self.template_name = template_name or self.template_name
|
19
19
|
|
plain/wsgi.py
CHANGED