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.
Files changed (197) hide show
  1. plain/CHANGELOG.md +684 -0
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -53
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +112 -28
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -13
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +14 -27
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +56 -40
  145. plain/urls/resolvers.py +38 -28
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
@@ -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__(self, exc_type, exc_value, tb):
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
- return line.endswith("\r" if isinstance(line, str) else b"\r")
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
- return line.endswith("\n" if isinstance(line, str) else b"\n")
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")
@@ -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
- return f.fileno() if hasattr(f, "fileno") else f
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
@@ -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, new_file_name, chunk_size=1024 * 64, allow_overwrite=False
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
 
@@ -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__(self, mode="w+b", bufsize=-1, suffix="", prefix="", dir=None):
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__(self, exc, value, tb):
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=None,
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__(self, name, content_type, size, charset, content_type_extra=None):
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 = tempfile.NamedTemporaryFile(
78
- suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
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__(self, name, content, content_type="text/plain"):
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