plain 0.66.0__py3-none-any.whl → 0.101.2__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/CHANGELOG.md +684 -0
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -13
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +56 -40
- plain/urls/resolvers.py +38 -28
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- 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 +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/internal/files/base.py
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
from functools import cached_property
|
|
3
5
|
from io import UnsupportedOperation
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
4
7
|
|
|
5
8
|
from plain.internal.files.utils import FileProxyMixin
|
|
6
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from typing import IO, Any
|
|
13
|
+
|
|
7
14
|
|
|
8
15
|
class File(FileProxyMixin):
|
|
9
16
|
DEFAULT_CHUNK_SIZE = 64 * 2**10
|
|
10
17
|
|
|
11
|
-
def __init__(self, file, name=None):
|
|
18
|
+
def __init__(self, file: IO[Any], name: str | None = None) -> None:
|
|
12
19
|
self.file = file
|
|
13
20
|
if name is None:
|
|
14
21
|
name = getattr(file, "name", None)
|
|
@@ -16,22 +23,22 @@ class File(FileProxyMixin):
|
|
|
16
23
|
if hasattr(file, "mode"):
|
|
17
24
|
self.mode = file.mode
|
|
18
25
|
|
|
19
|
-
def __str__(self):
|
|
26
|
+
def __str__(self) -> str:
|
|
20
27
|
return self.name or ""
|
|
21
28
|
|
|
22
|
-
def __repr__(self):
|
|
29
|
+
def __repr__(self) -> str:
|
|
23
30
|
return "<{}: {}>".format(self.__class__.__name__, self or "None")
|
|
24
31
|
|
|
25
|
-
def __bool__(self):
|
|
32
|
+
def __bool__(self) -> bool:
|
|
26
33
|
return bool(self.name)
|
|
27
34
|
|
|
28
|
-
def __len__(self):
|
|
35
|
+
def __len__(self) -> int:
|
|
29
36
|
return self.size
|
|
30
37
|
|
|
31
38
|
@cached_property
|
|
32
|
-
def size(self):
|
|
39
|
+
def size(self) -> int:
|
|
33
40
|
if hasattr(self.file, "size"):
|
|
34
|
-
return self.file.size
|
|
41
|
+
return self.file.size # type: ignore[return-value]
|
|
35
42
|
if hasattr(self.file, "name"):
|
|
36
43
|
try:
|
|
37
44
|
return os.path.getsize(self.file.name)
|
|
@@ -45,7 +52,7 @@ class File(FileProxyMixin):
|
|
|
45
52
|
return size
|
|
46
53
|
raise AttributeError("Unable to determine the file's size.")
|
|
47
54
|
|
|
48
|
-
def chunks(self, chunk_size=None):
|
|
55
|
+
def chunks(self, chunk_size: int | None = None) -> Iterator[bytes]:
|
|
49
56
|
"""
|
|
50
57
|
Read the file and yield chunks of ``chunk_size`` bytes (defaults to
|
|
51
58
|
``File.DEFAULT_CHUNK_SIZE``).
|
|
@@ -62,7 +69,7 @@ class File(FileProxyMixin):
|
|
|
62
69
|
break
|
|
63
70
|
yield data
|
|
64
71
|
|
|
65
|
-
def multiple_chunks(self, chunk_size=None):
|
|
72
|
+
def multiple_chunks(self, chunk_size: int | None = None) -> bool:
|
|
66
73
|
"""
|
|
67
74
|
Return ``True`` if you can expect multiple chunks.
|
|
68
75
|
|
|
@@ -72,7 +79,7 @@ class File(FileProxyMixin):
|
|
|
72
79
|
"""
|
|
73
80
|
return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)
|
|
74
81
|
|
|
75
|
-
def __iter__(self):
|
|
82
|
+
def __iter__(self) -> Iterator[bytes | str]:
|
|
76
83
|
# Iterate over this file-like object by newlines
|
|
77
84
|
buffer_ = None
|
|
78
85
|
for chunk in self.chunks():
|
|
@@ -99,13 +106,18 @@ class File(FileProxyMixin):
|
|
|
99
106
|
if buffer_ is not None:
|
|
100
107
|
yield buffer_
|
|
101
108
|
|
|
102
|
-
def __enter__(self):
|
|
109
|
+
def __enter__(self) -> File:
|
|
103
110
|
return self
|
|
104
111
|
|
|
105
|
-
def __exit__(
|
|
112
|
+
def __exit__(
|
|
113
|
+
self,
|
|
114
|
+
exc_type: type[BaseException] | None,
|
|
115
|
+
exc_value: BaseException | None,
|
|
116
|
+
tb: Any,
|
|
117
|
+
) -> None:
|
|
106
118
|
self.close()
|
|
107
119
|
|
|
108
|
-
def open(self, mode=None):
|
|
120
|
+
def open(self, mode: str | None = None) -> File:
|
|
109
121
|
if not self.closed:
|
|
110
122
|
self.seek(0)
|
|
111
123
|
elif self.name and os.path.exists(self.name):
|
|
@@ -114,20 +126,24 @@ class File(FileProxyMixin):
|
|
|
114
126
|
raise ValueError("The file cannot be reopened.")
|
|
115
127
|
return self
|
|
116
128
|
|
|
117
|
-
def close(self):
|
|
129
|
+
def close(self) -> None:
|
|
118
130
|
self.file.close()
|
|
119
131
|
|
|
120
132
|
|
|
121
|
-
def endswith_cr(line):
|
|
133
|
+
def endswith_cr(line: str | bytes) -> bool:
|
|
122
134
|
"""Return True if line (a text or bytestring) ends with '\r'."""
|
|
123
|
-
|
|
135
|
+
if isinstance(line, str):
|
|
136
|
+
return line.endswith("\r")
|
|
137
|
+
return line.endswith(b"\r")
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def endswith_lf(line):
|
|
140
|
+
def endswith_lf(line: str | bytes) -> bool:
|
|
127
141
|
"""Return True if line (a text or bytestring) ends with '\n'."""
|
|
128
|
-
|
|
142
|
+
if isinstance(line, str):
|
|
143
|
+
return line.endswith("\n")
|
|
144
|
+
return line.endswith(b"\n")
|
|
129
145
|
|
|
130
146
|
|
|
131
|
-
def equals_lf(line):
|
|
147
|
+
def equals_lf(line: str | bytes) -> bool:
|
|
132
148
|
"""Return True if line (a text or bytestring) equals '\n'."""
|
|
133
149
|
return line == ("\n" if isinstance(line, str) else b"\n")
|
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,18 @@ 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 collections.abc import Callable
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
21
25
|
|
|
22
26
|
from plain.internal.files.utils import FileProxyMixin
|
|
23
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
24
31
|
__all__ = (
|
|
25
32
|
"NamedTemporaryFile",
|
|
26
33
|
"gettempdir",
|
|
@@ -39,7 +46,14 @@ if os.name == "nt":
|
|
|
39
46
|
'newline' keyword arguments.
|
|
40
47
|
"""
|
|
41
48
|
|
|
42
|
-
def __init__(
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
mode: str = "w+b",
|
|
52
|
+
bufsize: int = -1,
|
|
53
|
+
suffix: str = "",
|
|
54
|
+
prefix: str = "",
|
|
55
|
+
dir: str | None = None,
|
|
56
|
+
) -> None:
|
|
43
57
|
fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
|
|
44
58
|
self.name = name
|
|
45
59
|
self.file = os.fdopen(fd, mode, bufsize)
|
|
@@ -48,9 +62,9 @@ if os.name == "nt":
|
|
|
48
62
|
# Because close can be called during shutdown
|
|
49
63
|
# we need to cache os.unlink and access it
|
|
50
64
|
# as self.unlink only
|
|
51
|
-
unlink = os.unlink
|
|
65
|
+
unlink: Callable[[str], None] = os.unlink
|
|
52
66
|
|
|
53
|
-
def close(self):
|
|
67
|
+
def close(self) -> None:
|
|
54
68
|
if not self.close_called:
|
|
55
69
|
self.close_called = True
|
|
56
70
|
try:
|
|
@@ -62,14 +76,19 @@ if os.name == "nt":
|
|
|
62
76
|
except OSError:
|
|
63
77
|
pass
|
|
64
78
|
|
|
65
|
-
def __del__(self):
|
|
79
|
+
def __del__(self) -> None:
|
|
66
80
|
self.close()
|
|
67
81
|
|
|
68
|
-
def __enter__(self):
|
|
82
|
+
def __enter__(self) -> TemporaryFile:
|
|
69
83
|
self.file.__enter__()
|
|
70
84
|
return self
|
|
71
85
|
|
|
72
|
-
def __exit__(
|
|
86
|
+
def __exit__(
|
|
87
|
+
self,
|
|
88
|
+
exc: type[BaseException] | None,
|
|
89
|
+
value: BaseException | None,
|
|
90
|
+
tb: Any,
|
|
91
|
+
) -> None:
|
|
73
92
|
self.file.__exit__(exc, value, tb)
|
|
74
93
|
|
|
75
94
|
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, cast
|
|
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
|
|
33
|
-
name=None,
|
|
34
|
-
content_type=None,
|
|
35
|
-
size=None,
|
|
36
|
-
charset=None,
|
|
37
|
-
content_type_extra=None,
|
|
38
|
-
):
|
|
39
|
+
file: IO[Any],
|
|
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 | None:
|
|
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,28 @@ 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 | None,
|
|
88
|
+
content_type_extra: dict[str, str] | None = None,
|
|
89
|
+
) -> None:
|
|
76
90
|
_, ext = os.path.splitext(name)
|
|
77
|
-
file =
|
|
78
|
-
|
|
91
|
+
file = cast(
|
|
92
|
+
IO[Any],
|
|
93
|
+
tempfile.NamedTemporaryFile(
|
|
94
|
+
suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
|
|
95
|
+
),
|
|
79
96
|
)
|
|
80
97
|
super().__init__(file, name, content_type, size, charset, content_type_extra)
|
|
81
98
|
|
|
82
|
-
def temporary_file_path(self):
|
|
99
|
+
def temporary_file_path(self) -> str:
|
|
83
100
|
"""Return the full path of this file."""
|
|
84
101
|
return self.file.name
|
|
85
102
|
|
|
86
|
-
def close(self):
|
|
103
|
+
def close(self) -> None:
|
|
87
104
|
try:
|
|
88
105
|
return self.file.close()
|
|
89
106
|
except FileNotFoundError:
|
|
@@ -100,26 +117,26 @@ class InMemoryUploadedFile(UploadedFile):
|
|
|
100
117
|
|
|
101
118
|
def __init__(
|
|
102
119
|
self,
|
|
103
|
-
file,
|
|
104
|
-
field_name,
|
|
105
|
-
name,
|
|
106
|
-
content_type,
|
|
107
|
-
size,
|
|
108
|
-
charset,
|
|
109
|
-
content_type_extra=None,
|
|
110
|
-
):
|
|
120
|
+
file: IO[Any],
|
|
121
|
+
field_name: str | None,
|
|
122
|
+
name: str,
|
|
123
|
+
content_type: str,
|
|
124
|
+
size: int,
|
|
125
|
+
charset: str | None,
|
|
126
|
+
content_type_extra: dict[str, str] | None = None,
|
|
127
|
+
) -> None:
|
|
111
128
|
super().__init__(file, name, content_type, size, charset, content_type_extra)
|
|
112
129
|
self.field_name = field_name
|
|
113
130
|
|
|
114
|
-
def open(self, mode=None):
|
|
131
|
+
def open(self, mode: str | None = None) -> InMemoryUploadedFile:
|
|
115
132
|
self.file.seek(0)
|
|
116
133
|
return self
|
|
117
134
|
|
|
118
|
-
def chunks(self, chunk_size=None):
|
|
135
|
+
def chunks(self, chunk_size: int | None = None) -> Iterator[bytes]:
|
|
119
136
|
self.file.seek(0)
|
|
120
137
|
yield self.read()
|
|
121
138
|
|
|
122
|
-
def multiple_chunks(self, chunk_size=None):
|
|
139
|
+
def multiple_chunks(self, chunk_size: int | None = None) -> bool:
|
|
123
140
|
# Since it's in memory, we'll never have multiple chunks.
|
|
124
141
|
return False
|
|
125
142
|
|
|
@@ -129,14 +146,16 @@ class SimpleUploadedFile(InMemoryUploadedFile):
|
|
|
129
146
|
A simple representation of a file, which just has content, size, and a name.
|
|
130
147
|
"""
|
|
131
148
|
|
|
132
|
-
def __init__(
|
|
149
|
+
def __init__(
|
|
150
|
+
self, name: str, content: bytes | None, content_type: str = "text/plain"
|
|
151
|
+
) -> None:
|
|
133
152
|
content = content or b""
|
|
134
153
|
super().__init__(
|
|
135
154
|
BytesIO(content), None, name, content_type, len(content), None, None
|
|
136
155
|
)
|
|
137
156
|
|
|
138
157
|
@classmethod
|
|
139
|
-
def from_dict(cls, file_dict):
|
|
158
|
+
def from_dict(cls, file_dict: dict[str, Any]) -> SimpleUploadedFile:
|
|
140
159
|
"""
|
|
141
160
|
Create a SimpleUploadedFile object from a dictionary with keys:
|
|
142
161
|
- filename
|