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/internal/files/locks.py
CHANGED
@@ -17,14 +17,22 @@ Example Usage::
|
|
17
17
|
... f.write('Plain')
|
18
18
|
"""
|
19
19
|
|
20
|
+
from __future__ import annotations
|
21
|
+
|
20
22
|
import os
|
23
|
+
from typing import TYPE_CHECKING
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from typing import IO
|
21
27
|
|
22
28
|
__all__ = ("LOCK_EX", "LOCK_SH", "LOCK_NB", "lock", "unlock")
|
23
29
|
|
24
30
|
|
25
|
-
def _fd(f):
|
31
|
+
def _fd(f: IO[bytes] | int) -> int:
|
26
32
|
"""Get a filedescriptor from something which could be a file or an fd."""
|
27
|
-
|
33
|
+
if isinstance(f, int):
|
34
|
+
return f
|
35
|
+
return f.fileno()
|
28
36
|
|
29
37
|
|
30
38
|
if os.name == "nt":
|
@@ -33,7 +41,7 @@ if os.name == "nt":
|
|
33
41
|
POINTER,
|
34
42
|
Structure,
|
35
43
|
Union,
|
36
|
-
WinDLL,
|
44
|
+
WinDLL, # type: ignore[attr-defined]
|
37
45
|
byref,
|
38
46
|
c_int64,
|
39
47
|
c_ulong,
|
@@ -82,14 +90,14 @@ if os.name == "nt":
|
|
82
90
|
UnlockFileEx.restype = BOOL
|
83
91
|
UnlockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, LPOVERLAPPED]
|
84
92
|
|
85
|
-
def lock(f, flags):
|
86
|
-
hfile = msvcrt.get_osfhandle(_fd(f))
|
93
|
+
def lock(f: IO[bytes] | int, flags: int) -> bool:
|
94
|
+
hfile = msvcrt.get_osfhandle(_fd(f)) # type: ignore[attr-defined]
|
87
95
|
overlapped = OVERLAPPED()
|
88
96
|
ret = LockFileEx(hfile, flags, 0, 0, 0xFFFF0000, byref(overlapped))
|
89
97
|
return bool(ret)
|
90
98
|
|
91
|
-
def unlock(f):
|
92
|
-
hfile = msvcrt.get_osfhandle(_fd(f))
|
99
|
+
def unlock(f: IO[bytes] | int) -> bool:
|
100
|
+
hfile = msvcrt.get_osfhandle(_fd(f)) # type: ignore[attr-defined]
|
93
101
|
overlapped = OVERLAPPED()
|
94
102
|
ret = UnlockFileEx(hfile, 0, 0, 0xFFFF0000, byref(overlapped))
|
95
103
|
return bool(ret)
|
@@ -106,23 +114,23 @@ else:
|
|
106
114
|
LOCK_EX = LOCK_SH = LOCK_NB = 0
|
107
115
|
|
108
116
|
# Dummy functions that don't do anything.
|
109
|
-
def lock(f, flags):
|
117
|
+
def lock(f: IO[bytes] | int, flags: int) -> bool:
|
110
118
|
# File is not locked
|
111
119
|
return False
|
112
120
|
|
113
|
-
def unlock(f):
|
121
|
+
def unlock(f: IO[bytes] | int) -> bool:
|
114
122
|
# File is unlocked
|
115
123
|
return True
|
116
124
|
|
117
125
|
else:
|
118
126
|
|
119
|
-
def lock(f, flags):
|
127
|
+
def lock(f: IO[bytes] | int, flags: int) -> bool:
|
120
128
|
try:
|
121
129
|
fcntl.flock(_fd(f), flags)
|
122
130
|
return True
|
123
131
|
except BlockingIOError:
|
124
132
|
return False
|
125
133
|
|
126
|
-
def unlock(f):
|
134
|
+
def unlock(f: IO[bytes] | int) -> bool:
|
127
135
|
fcntl.flock(_fd(f), fcntl.LOCK_UN)
|
128
136
|
return True
|
plain/internal/files/move.py
CHANGED
@@ -5,6 +5,8 @@ Move a file in the safest way possible::
|
|
5
5
|
>>> file_move_safe("/tmp/old_file", "/tmp/new_file")
|
6
6
|
"""
|
7
7
|
|
8
|
+
from __future__ import annotations
|
9
|
+
|
8
10
|
import os
|
9
11
|
from shutil import copymode, copystat
|
10
12
|
|
@@ -13,7 +15,7 @@ from plain.internal.files import locks
|
|
13
15
|
__all__ = ["file_move_safe"]
|
14
16
|
|
15
17
|
|
16
|
-
def _samefile(src, dst):
|
18
|
+
def _samefile(src: str, dst: str) -> bool:
|
17
19
|
# Macintosh, Unix.
|
18
20
|
if hasattr(os.path, "samefile"):
|
19
21
|
try:
|
@@ -28,8 +30,11 @@ def _samefile(src, dst):
|
|
28
30
|
|
29
31
|
|
30
32
|
def file_move_safe(
|
31
|
-
old_file_name
|
32
|
-
|
33
|
+
old_file_name: str,
|
34
|
+
new_file_name: str,
|
35
|
+
chunk_size: int = 1024 * 64,
|
36
|
+
allow_overwrite: bool = False,
|
37
|
+
) -> None:
|
33
38
|
"""
|
34
39
|
Move a file from one location to another in the safest way possible.
|
35
40
|
|
plain/internal/files/temp.py
CHANGED
@@ -16,11 +16,17 @@ arguments available in tempfile.NamedTemporaryFile.
|
|
16
16
|
2: https://bugs.python.org/issue14243
|
17
17
|
"""
|
18
18
|
|
19
|
+
from __future__ import annotations
|
20
|
+
|
19
21
|
import os
|
20
22
|
import tempfile
|
23
|
+
from typing import TYPE_CHECKING
|
21
24
|
|
22
25
|
from plain.internal.files.utils import FileProxyMixin
|
23
26
|
|
27
|
+
if TYPE_CHECKING:
|
28
|
+
from typing import Any
|
29
|
+
|
24
30
|
__all__ = (
|
25
31
|
"NamedTemporaryFile",
|
26
32
|
"gettempdir",
|
@@ -39,7 +45,14 @@ if os.name == "nt":
|
|
39
45
|
'newline' keyword arguments.
|
40
46
|
"""
|
41
47
|
|
42
|
-
def __init__(
|
48
|
+
def __init__(
|
49
|
+
self,
|
50
|
+
mode: str = "w+b",
|
51
|
+
bufsize: int = -1,
|
52
|
+
suffix: str = "",
|
53
|
+
prefix: str = "",
|
54
|
+
dir: str | None = None,
|
55
|
+
) -> None:
|
43
56
|
fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
|
44
57
|
self.name = name
|
45
58
|
self.file = os.fdopen(fd, mode, bufsize)
|
@@ -50,7 +63,7 @@ if os.name == "nt":
|
|
50
63
|
# as self.unlink only
|
51
64
|
unlink = os.unlink
|
52
65
|
|
53
|
-
def close(self):
|
66
|
+
def close(self) -> None:
|
54
67
|
if not self.close_called:
|
55
68
|
self.close_called = True
|
56
69
|
try:
|
@@ -62,14 +75,19 @@ if os.name == "nt":
|
|
62
75
|
except OSError:
|
63
76
|
pass
|
64
77
|
|
65
|
-
def __del__(self):
|
78
|
+
def __del__(self) -> None:
|
66
79
|
self.close()
|
67
80
|
|
68
|
-
def __enter__(self):
|
81
|
+
def __enter__(self) -> TemporaryFile:
|
69
82
|
self.file.__enter__()
|
70
83
|
return self
|
71
84
|
|
72
|
-
def __exit__(
|
85
|
+
def __exit__(
|
86
|
+
self,
|
87
|
+
exc: type[BaseException] | None,
|
88
|
+
value: BaseException | None,
|
89
|
+
tb: Any,
|
90
|
+
) -> None:
|
73
91
|
self.file.__exit__(exc, value, tb)
|
74
92
|
|
75
93
|
NamedTemporaryFile = TemporaryFile
|
@@ -2,14 +2,21 @@
|
|
2
2
|
Classes representing uploaded files.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
|
5
7
|
import os
|
6
8
|
from io import BytesIO
|
9
|
+
from typing import TYPE_CHECKING
|
7
10
|
|
8
11
|
from plain.internal.files import temp as tempfile
|
9
12
|
from plain.internal.files.base import File
|
10
13
|
from plain.internal.files.utils import validate_file_name
|
11
14
|
from plain.runtime import settings
|
12
15
|
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
from collections.abc import Iterator
|
18
|
+
from typing import IO, Any
|
19
|
+
|
13
20
|
__all__ = (
|
14
21
|
"UploadedFile",
|
15
22
|
"TemporaryUploadedFile",
|
@@ -29,26 +36,26 @@ class UploadedFile(File):
|
|
29
36
|
|
30
37
|
def __init__(
|
31
38
|
self,
|
32
|
-
file=None,
|
33
|
-
name=None,
|
34
|
-
content_type=None,
|
35
|
-
size=None,
|
36
|
-
charset=None,
|
37
|
-
content_type_extra=None,
|
38
|
-
):
|
39
|
+
file: IO[Any] | None = None,
|
40
|
+
name: str | None = None,
|
41
|
+
content_type: str | None = None,
|
42
|
+
size: int | None = None,
|
43
|
+
charset: str | None = None,
|
44
|
+
content_type_extra: dict[str, str] | None = None,
|
45
|
+
) -> None:
|
39
46
|
super().__init__(file, name)
|
40
47
|
self.size = size
|
41
48
|
self.content_type = content_type
|
42
49
|
self.charset = charset
|
43
50
|
self.content_type_extra = content_type_extra
|
44
51
|
|
45
|
-
def __repr__(self):
|
52
|
+
def __repr__(self) -> str:
|
46
53
|
return f"<{self.__class__.__name__}: {self.name} ({self.content_type})>"
|
47
54
|
|
48
|
-
def _get_name(self):
|
55
|
+
def _get_name(self) -> str:
|
49
56
|
return self._name
|
50
57
|
|
51
|
-
def _set_name(self, name):
|
58
|
+
def _set_name(self, name: str | None) -> None:
|
52
59
|
# Sanitize the file name so that it can't be dangerous.
|
53
60
|
if name is not None:
|
54
61
|
# Just use the basename of the file -- anything else is dangerous.
|
@@ -72,18 +79,25 @@ class TemporaryUploadedFile(UploadedFile):
|
|
72
79
|
A file uploaded to a temporary location (i.e. stream-to-disk).
|
73
80
|
"""
|
74
81
|
|
75
|
-
def __init__(
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
name: str,
|
85
|
+
content_type: str,
|
86
|
+
size: int,
|
87
|
+
charset: str,
|
88
|
+
content_type_extra: dict[str, str] | None = None,
|
89
|
+
) -> None:
|
76
90
|
_, ext = os.path.splitext(name)
|
77
91
|
file = tempfile.NamedTemporaryFile(
|
78
92
|
suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
|
79
93
|
)
|
80
94
|
super().__init__(file, name, content_type, size, charset, content_type_extra)
|
81
95
|
|
82
|
-
def temporary_file_path(self):
|
96
|
+
def temporary_file_path(self) -> str:
|
83
97
|
"""Return the full path of this file."""
|
84
98
|
return self.file.name
|
85
99
|
|
86
|
-
def close(self):
|
100
|
+
def close(self) -> None:
|
87
101
|
try:
|
88
102
|
return self.file.close()
|
89
103
|
except FileNotFoundError:
|
@@ -100,26 +114,26 @@ class InMemoryUploadedFile(UploadedFile):
|
|
100
114
|
|
101
115
|
def __init__(
|
102
116
|
self,
|
103
|
-
file,
|
104
|
-
field_name,
|
105
|
-
name,
|
106
|
-
content_type,
|
107
|
-
size,
|
108
|
-
charset,
|
109
|
-
content_type_extra=None,
|
110
|
-
):
|
117
|
+
file: IO[Any],
|
118
|
+
field_name: str | None,
|
119
|
+
name: str,
|
120
|
+
content_type: str,
|
121
|
+
size: int,
|
122
|
+
charset: str,
|
123
|
+
content_type_extra: dict[str, str] | None = None,
|
124
|
+
) -> None:
|
111
125
|
super().__init__(file, name, content_type, size, charset, content_type_extra)
|
112
126
|
self.field_name = field_name
|
113
127
|
|
114
|
-
def open(self, mode=None):
|
128
|
+
def open(self, mode: str | None = None) -> InMemoryUploadedFile:
|
115
129
|
self.file.seek(0)
|
116
130
|
return self
|
117
131
|
|
118
|
-
def chunks(self, chunk_size=None):
|
132
|
+
def chunks(self, chunk_size: int | None = None) -> Iterator[bytes]:
|
119
133
|
self.file.seek(0)
|
120
134
|
yield self.read()
|
121
135
|
|
122
|
-
def multiple_chunks(self, chunk_size=None):
|
136
|
+
def multiple_chunks(self, chunk_size: int | None = None) -> bool:
|
123
137
|
# Since it's in memory, we'll never have multiple chunks.
|
124
138
|
return False
|
125
139
|
|
@@ -129,14 +143,16 @@ class SimpleUploadedFile(InMemoryUploadedFile):
|
|
129
143
|
A simple representation of a file, which just has content, size, and a name.
|
130
144
|
"""
|
131
145
|
|
132
|
-
def __init__(
|
146
|
+
def __init__(
|
147
|
+
self, name: str, content: bytes | None, content_type: str = "text/plain"
|
148
|
+
) -> None:
|
133
149
|
content = content or b""
|
134
150
|
super().__init__(
|
135
151
|
BytesIO(content), None, name, content_type, len(content), None, None
|
136
152
|
)
|
137
153
|
|
138
154
|
@classmethod
|
139
|
-
def from_dict(cls, file_dict):
|
155
|
+
def from_dict(cls, file_dict: dict[str, Any]) -> SimpleUploadedFile:
|
140
156
|
"""
|
141
157
|
Create a SimpleUploadedFile object from a dictionary with keys:
|
142
158
|
- filename
|
@@ -2,16 +2,25 @@
|
|
2
2
|
Base file upload handler classes, and the built-in concrete subclasses
|
3
3
|
"""
|
4
4
|
|
5
|
+
from __future__ import annotations
|
6
|
+
|
5
7
|
import os
|
6
8
|
from io import BytesIO
|
9
|
+
from typing import TYPE_CHECKING
|
7
10
|
|
8
11
|
from plain.internal.files.uploadedfile import (
|
9
12
|
InMemoryUploadedFile,
|
10
13
|
TemporaryUploadedFile,
|
14
|
+
UploadedFile,
|
11
15
|
)
|
12
16
|
from plain.runtime import settings
|
13
17
|
from plain.utils.module_loading import import_string
|
14
18
|
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
from typing import Any
|
21
|
+
|
22
|
+
from plain.http import Request
|
23
|
+
|
15
24
|
__all__ = [
|
16
25
|
"UploadFileException",
|
17
26
|
"StopUpload",
|
@@ -37,7 +46,7 @@ class StopUpload(UploadFileException):
|
|
37
46
|
This exception is raised when an upload must abort.
|
38
47
|
"""
|
39
48
|
|
40
|
-
def __init__(self, connection_reset=False):
|
49
|
+
def __init__(self, connection_reset: bool = False) -> None:
|
41
50
|
"""
|
42
51
|
If ``connection_reset`` is ``True``, Plain knows will halt the upload
|
43
52
|
without consuming the rest of the upload. This will cause the browser to
|
@@ -45,7 +54,7 @@ class StopUpload(UploadFileException):
|
|
45
54
|
"""
|
46
55
|
self.connection_reset = connection_reset
|
47
56
|
|
48
|
-
def __str__(self):
|
57
|
+
def __str__(self) -> str:
|
49
58
|
if self.connection_reset:
|
50
59
|
return "StopUpload: Halt current upload."
|
51
60
|
else:
|
@@ -76,7 +85,7 @@ class FileUploadHandler:
|
|
76
85
|
|
77
86
|
chunk_size = 64 * 2**10 # : The default chunk size is 64 KB.
|
78
87
|
|
79
|
-
def __init__(self, request=None):
|
88
|
+
def __init__(self, request: Request | None = None) -> None:
|
80
89
|
self.file_name = None
|
81
90
|
self.content_type = None
|
82
91
|
self.content_length = None
|
@@ -85,8 +94,13 @@ class FileUploadHandler:
|
|
85
94
|
self.request = request
|
86
95
|
|
87
96
|
def handle_raw_input(
|
88
|
-
self,
|
89
|
-
|
97
|
+
self,
|
98
|
+
input_data: Any,
|
99
|
+
meta: dict[str, Any],
|
100
|
+
content_length: int,
|
101
|
+
boundary: bytes,
|
102
|
+
encoding: str | None = None,
|
103
|
+
) -> None:
|
90
104
|
"""
|
91
105
|
Handle the raw input from the client.
|
92
106
|
|
@@ -106,13 +120,13 @@ class FileUploadHandler:
|
|
106
120
|
|
107
121
|
def new_file(
|
108
122
|
self,
|
109
|
-
field_name,
|
110
|
-
file_name,
|
111
|
-
content_type,
|
112
|
-
content_length,
|
113
|
-
charset=None,
|
114
|
-
content_type_extra=None,
|
115
|
-
):
|
123
|
+
field_name: str,
|
124
|
+
file_name: str,
|
125
|
+
content_type: str,
|
126
|
+
content_length: int,
|
127
|
+
charset: str | None = None,
|
128
|
+
content_type_extra: dict[str, str] | None = None,
|
129
|
+
) -> None:
|
116
130
|
"""
|
117
131
|
Signal that a new file has been started.
|
118
132
|
|
@@ -126,7 +140,7 @@ class FileUploadHandler:
|
|
126
140
|
self.charset = charset
|
127
141
|
self.content_type_extra = content_type_extra
|
128
142
|
|
129
|
-
def receive_data_chunk(self, raw_data, start):
|
143
|
+
def receive_data_chunk(self, raw_data: bytes, start: int) -> bytes | None:
|
130
144
|
"""
|
131
145
|
Receive data from the streamed upload parser. ``start`` is the position
|
132
146
|
in the file of the chunk.
|
@@ -135,7 +149,7 @@ class FileUploadHandler:
|
|
135
149
|
"subclasses of FileUploadHandler must provide a receive_data_chunk() method"
|
136
150
|
)
|
137
151
|
|
138
|
-
def file_complete(self, file_size):
|
152
|
+
def file_complete(self, file_size: int) -> UploadedFile | None:
|
139
153
|
"""
|
140
154
|
Signal that a file has completed. File size corresponds to the actual
|
141
155
|
size accumulated by all the chunks.
|
@@ -146,14 +160,14 @@ class FileUploadHandler:
|
|
146
160
|
"subclasses of FileUploadHandler must provide a file_complete() method"
|
147
161
|
)
|
148
162
|
|
149
|
-
def upload_complete(self):
|
163
|
+
def upload_complete(self) -> None:
|
150
164
|
"""
|
151
165
|
Signal that the upload is complete. Subclasses should perform cleanup
|
152
166
|
that is necessary for this handler.
|
153
167
|
"""
|
154
168
|
pass
|
155
169
|
|
156
|
-
def upload_interrupted(self):
|
170
|
+
def upload_interrupted(self) -> None:
|
157
171
|
"""
|
158
172
|
Signal that the upload was interrupted. Subclasses should perform
|
159
173
|
cleanup that is necessary for this handler.
|
@@ -166,7 +180,7 @@ class TemporaryFileUploadHandler(FileUploadHandler):
|
|
166
180
|
Upload handler that streams data into a temporary file.
|
167
181
|
"""
|
168
182
|
|
169
|
-
def new_file(self, *args, **kwargs):
|
183
|
+
def new_file(self, *args: Any, **kwargs: Any) -> None:
|
170
184
|
"""
|
171
185
|
Create the file object to append to as data is coming in.
|
172
186
|
"""
|
@@ -175,15 +189,16 @@ class TemporaryFileUploadHandler(FileUploadHandler):
|
|
175
189
|
self.file_name, self.content_type, 0, self.charset, self.content_type_extra
|
176
190
|
)
|
177
191
|
|
178
|
-
def receive_data_chunk(self, raw_data, start):
|
192
|
+
def receive_data_chunk(self, raw_data: bytes, start: int) -> None:
|
179
193
|
self.file.write(raw_data)
|
194
|
+
return None
|
180
195
|
|
181
|
-
def file_complete(self, file_size):
|
196
|
+
def file_complete(self, file_size: int) -> TemporaryUploadedFile:
|
182
197
|
self.file.seek(0)
|
183
198
|
self.file.size = file_size
|
184
199
|
return self.file
|
185
200
|
|
186
|
-
def upload_interrupted(self):
|
201
|
+
def upload_interrupted(self) -> None:
|
187
202
|
if hasattr(self, "file"):
|
188
203
|
temp_location = self.file.temporary_file_path()
|
189
204
|
try:
|
@@ -199,8 +214,13 @@ class MemoryFileUploadHandler(FileUploadHandler):
|
|
199
214
|
"""
|
200
215
|
|
201
216
|
def handle_raw_input(
|
202
|
-
self,
|
203
|
-
|
217
|
+
self,
|
218
|
+
input_data: Any,
|
219
|
+
meta: dict[str, Any],
|
220
|
+
content_length: int,
|
221
|
+
boundary: bytes,
|
222
|
+
encoding: str | None = None,
|
223
|
+
) -> None:
|
204
224
|
"""
|
205
225
|
Use the content_length to signal whether or not this handler should be
|
206
226
|
used.
|
@@ -209,23 +229,24 @@ class MemoryFileUploadHandler(FileUploadHandler):
|
|
209
229
|
# If the post is too large, we cannot use the Memory handler.
|
210
230
|
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
|
211
231
|
|
212
|
-
def new_file(self, *args, **kwargs):
|
232
|
+
def new_file(self, *args: Any, **kwargs: Any) -> None:
|
213
233
|
super().new_file(*args, **kwargs)
|
214
234
|
if self.activated:
|
215
235
|
self.file = BytesIO()
|
216
236
|
raise StopFutureHandlers()
|
217
237
|
|
218
|
-
def receive_data_chunk(self, raw_data, start):
|
238
|
+
def receive_data_chunk(self, raw_data: bytes, start: int) -> bytes | None:
|
219
239
|
"""Add the data to the BytesIO file."""
|
220
240
|
if self.activated:
|
221
241
|
self.file.write(raw_data)
|
242
|
+
return None
|
222
243
|
else:
|
223
244
|
return raw_data
|
224
245
|
|
225
|
-
def file_complete(self, file_size):
|
246
|
+
def file_complete(self, file_size: int) -> InMemoryUploadedFile | None:
|
226
247
|
"""Return a file object if this handler is activated."""
|
227
248
|
if not self.activated:
|
228
|
-
return
|
249
|
+
return None
|
229
250
|
|
230
251
|
self.file.seek(0)
|
231
252
|
return InMemoryUploadedFile(
|
@@ -239,13 +260,13 @@ class MemoryFileUploadHandler(FileUploadHandler):
|
|
239
260
|
)
|
240
261
|
|
241
262
|
|
242
|
-
def load_handler(path, *args, **kwargs):
|
263
|
+
def load_handler(path: str, *args: Any, **kwargs: Any) -> FileUploadHandler:
|
243
264
|
"""
|
244
265
|
Given a path to a handler, return an instance of that handler.
|
245
266
|
|
246
267
|
E.g.::
|
247
|
-
>>> from plain.http import
|
248
|
-
>>> request =
|
268
|
+
>>> from plain.http import Request
|
269
|
+
>>> request = Request()
|
249
270
|
>>> load_handler(
|
250
271
|
... 'plain.internal.files.uploadhandler.TemporaryFileUploadHandler',
|
251
272
|
... request,
|
plain/internal/files/utils.py
CHANGED
@@ -1,10 +1,17 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
import pathlib
|
5
|
+
from typing import TYPE_CHECKING
|
3
6
|
|
4
7
|
from plain.exceptions import SuspiciousFileOperation
|
5
8
|
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from collections.abc import Iterator
|
11
|
+
from typing import Any
|
12
|
+
|
6
13
|
|
7
|
-
def validate_file_name(name, allow_relative_path=False):
|
14
|
+
def validate_file_name(name: str, allow_relative_path: bool = False) -> str:
|
8
15
|
# Remove potentially dangerous names
|
9
16
|
if os.path.basename(name) in {"", ".", ".."}:
|
10
17
|
raise SuspiciousFileOperation(f"Could not derive file name from '{name}'")
|
@@ -50,29 +57,29 @@ class FileProxyMixin:
|
|
50
57
|
writelines = property(lambda self: self.file.writelines)
|
51
58
|
|
52
59
|
@property
|
53
|
-
def closed(self):
|
60
|
+
def closed(self) -> bool:
|
54
61
|
return not self.file or self.file.closed
|
55
62
|
|
56
|
-
def readable(self):
|
63
|
+
def readable(self) -> bool:
|
57
64
|
if self.closed:
|
58
65
|
return False
|
59
66
|
if hasattr(self.file, "readable"):
|
60
67
|
return self.file.readable()
|
61
68
|
return True
|
62
69
|
|
63
|
-
def writable(self):
|
70
|
+
def writable(self) -> bool:
|
64
71
|
if self.closed:
|
65
72
|
return False
|
66
73
|
if hasattr(self.file, "writable"):
|
67
74
|
return self.file.writable()
|
68
75
|
return "w" in getattr(self.file, "mode", "")
|
69
76
|
|
70
|
-
def seekable(self):
|
77
|
+
def seekable(self) -> bool:
|
71
78
|
if self.closed:
|
72
79
|
return False
|
73
80
|
if hasattr(self.file, "seekable"):
|
74
81
|
return self.file.seekable()
|
75
82
|
return True
|
76
83
|
|
77
|
-
def __iter__(self):
|
84
|
+
def __iter__(self) -> Iterator[Any]:
|
78
85
|
return iter(self.file)
|
plain/internal/handlers/base.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
import types
|
5
|
+
from typing import TYPE_CHECKING
|
3
6
|
|
4
7
|
from opentelemetry import baggage, trace
|
5
8
|
from opentelemetry.semconv.attributes import http_attributes, url_attributes
|
@@ -12,6 +15,12 @@ from plain.utils.module_loading import import_string
|
|
12
15
|
|
13
16
|
from .exception import convert_exception_to_response
|
14
17
|
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from collections.abc import Callable
|
20
|
+
|
21
|
+
from plain.http import Request, Response
|
22
|
+
from plain.urls import ResolverMatch
|
23
|
+
|
15
24
|
logger = logging.getLogger("plain.request")
|
16
25
|
|
17
26
|
|
@@ -34,9 +43,9 @@ tracer = trace.get_tracer("plain")
|
|
34
43
|
|
35
44
|
|
36
45
|
class BaseHandler:
|
37
|
-
_middleware_chain = None
|
46
|
+
_middleware_chain: Callable[[Request], Response] | None = None
|
38
47
|
|
39
|
-
def load_middleware(self):
|
48
|
+
def load_middleware(self) -> None:
|
40
49
|
"""
|
41
50
|
Populate middleware lists from settings.MIDDLEWARE.
|
42
51
|
|
@@ -63,8 +72,8 @@ class BaseHandler:
|
|
63
72
|
# as a flag for initialization being complete.
|
64
73
|
self._middleware_chain = handler
|
65
74
|
|
66
|
-
def get_response(self, request):
|
67
|
-
"""Return a Response object for the given
|
75
|
+
def get_response(self, request: Request) -> Response:
|
76
|
+
"""Return a Response object for the given Request."""
|
68
77
|
|
69
78
|
span_attributes = {
|
70
79
|
"plain.request.id": request.unique_id,
|
@@ -115,7 +124,7 @@ class BaseHandler:
|
|
115
124
|
)
|
116
125
|
return response
|
117
126
|
|
118
|
-
def _get_response(self, request):
|
127
|
+
def _get_response(self, request: Request) -> Response:
|
119
128
|
"""
|
120
129
|
Resolve and call the view, then apply view, exception, and
|
121
130
|
template_response middleware. This method is everything that happens
|
@@ -132,7 +141,7 @@ class BaseHandler:
|
|
132
141
|
|
133
142
|
return response
|
134
143
|
|
135
|
-
def resolve_request(self, request):
|
144
|
+
def resolve_request(self, request: Request) -> ResolverMatch:
|
136
145
|
"""
|
137
146
|
Retrieve/set the urlrouter for the request. Return the view resolved,
|
138
147
|
with its args and kwargs.
|
@@ -154,7 +163,12 @@ class BaseHandler:
|
|
154
163
|
request.resolver_match = resolver_match
|
155
164
|
return resolver_match
|
156
165
|
|
157
|
-
def check_response(
|
166
|
+
def check_response(
|
167
|
+
self,
|
168
|
+
response: Response | None,
|
169
|
+
callback: Callable[..., Response],
|
170
|
+
name: str | None = None,
|
171
|
+
) -> None:
|
158
172
|
"""
|
159
173
|
Raise an error if the view returned None or an uncalled coroutine.
|
160
174
|
"""
|