plain 0.69.0__py3-none-any.whl → 0.70.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. plain/AGENTS.md +1 -1
  2. plain/CHANGELOG.md +11 -0
  3. plain/assets/compile.py +20 -7
  4. plain/assets/finders.py +15 -11
  5. plain/assets/fingerprints.py +6 -5
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +23 -17
  8. plain/chores/registry.py +14 -9
  9. plain/cli/agent/__init__.py +1 -1
  10. plain/cli/agent/docs.py +7 -6
  11. plain/cli/agent/llmdocs.py +18 -8
  12. plain/cli/agent/md.py +19 -14
  13. plain/cli/agent/prompt.py +1 -1
  14. plain/cli/agent/request.py +37 -17
  15. plain/cli/build.py +2 -2
  16. plain/cli/changelog.py +8 -4
  17. plain/cli/chores.py +4 -4
  18. plain/cli/core.py +8 -5
  19. plain/cli/docs.py +2 -2
  20. plain/cli/formatting.py +10 -7
  21. plain/cli/output.py +6 -2
  22. plain/cli/preflight.py +3 -3
  23. plain/cli/print.py +1 -1
  24. plain/cli/registry.py +10 -6
  25. plain/cli/scaffold.py +1 -1
  26. plain/cli/settings.py +1 -1
  27. plain/cli/shell.py +10 -7
  28. plain/cli/startup.py +3 -3
  29. plain/cli/urls.py +10 -4
  30. plain/cli/utils.py +2 -2
  31. plain/csrf/middleware.py +15 -5
  32. plain/csrf/views.py +11 -8
  33. plain/debug.py +5 -2
  34. plain/exceptions.py +19 -8
  35. plain/forms/__init__.py +1 -1
  36. plain/forms/boundfield.py +14 -7
  37. plain/forms/exceptions.py +1 -1
  38. plain/forms/fields.py +139 -97
  39. plain/forms/forms.py +55 -39
  40. plain/http/cookie.py +15 -7
  41. plain/http/multipartparser.py +50 -30
  42. plain/http/request.py +97 -73
  43. plain/http/response.py +99 -80
  44. plain/internal/__init__.py +8 -1
  45. plain/internal/files/base.py +34 -18
  46. plain/internal/files/locks.py +19 -11
  47. plain/internal/files/move.py +8 -3
  48. plain/internal/files/temp.py +23 -5
  49. plain/internal/files/uploadedfile.py +42 -26
  50. plain/internal/files/uploadhandler.py +48 -27
  51. plain/internal/files/utils.py +13 -6
  52. plain/internal/handlers/base.py +20 -6
  53. plain/internal/handlers/exception.py +19 -5
  54. plain/internal/handlers/wsgi.py +30 -18
  55. plain/internal/middleware/headers.py +11 -2
  56. plain/internal/middleware/hosts.py +10 -2
  57. plain/internal/middleware/https.py +13 -3
  58. plain/internal/middleware/slash.py +15 -5
  59. plain/json.py +2 -1
  60. plain/logs/configure.py +3 -1
  61. plain/logs/debug.py +16 -5
  62. plain/logs/formatters.py +6 -3
  63. plain/logs/loggers.py +56 -52
  64. plain/logs/utils.py +19 -9
  65. plain/packages/config.py +14 -6
  66. plain/packages/registry.py +27 -12
  67. plain/paginator.py +31 -21
  68. plain/preflight/checks.py +3 -1
  69. plain/preflight/files.py +3 -1
  70. plain/preflight/registry.py +25 -10
  71. plain/preflight/results.py +10 -4
  72. plain/preflight/security.py +7 -5
  73. plain/preflight/urls.py +4 -1
  74. plain/runtime/__init__.py +4 -3
  75. plain/runtime/global_settings.py +1 -1
  76. plain/runtime/user_settings.py +26 -17
  77. plain/runtime/utils.py +1 -1
  78. plain/signals/dispatch/dispatcher.py +39 -17
  79. plain/signing.py +49 -30
  80. plain/templates/jinja/__init__.py +13 -5
  81. plain/templates/jinja/environments.py +4 -3
  82. plain/templates/jinja/extensions.py +9 -3
  83. plain/templates/jinja/filters.py +7 -2
  84. plain/templates/jinja/globals.py +1 -1
  85. plain/test/client.py +246 -174
  86. plain/test/encoding.py +9 -6
  87. plain/test/exceptions.py +10 -2
  88. plain/urls/converters.py +13 -10
  89. plain/urls/patterns.py +32 -20
  90. plain/urls/resolvers.py +32 -22
  91. plain/urls/utils.py +5 -1
  92. plain/utils/cache.py +14 -8
  93. plain/utils/crypto.py +21 -5
  94. plain/utils/datastructures.py +84 -54
  95. plain/utils/dateparse.py +10 -7
  96. plain/utils/deconstruct.py +12 -4
  97. plain/utils/decorators.py +5 -1
  98. plain/utils/duration.py +8 -4
  99. plain/utils/encoding.py +14 -7
  100. plain/utils/functional.py +62 -47
  101. plain/utils/hashable.py +5 -1
  102. plain/utils/html.py +21 -14
  103. plain/utils/http.py +16 -9
  104. plain/utils/inspect.py +14 -6
  105. plain/utils/ipv6.py +7 -3
  106. plain/utils/itercompat.py +6 -1
  107. plain/utils/module_loading.py +7 -3
  108. plain/utils/regex_helper.py +23 -13
  109. plain/utils/safestring.py +14 -6
  110. plain/utils/text.py +34 -18
  111. plain/utils/timezone.py +30 -19
  112. plain/utils/tree.py +31 -18
  113. plain/validators.py +71 -44
  114. plain/views/base.py +16 -6
  115. plain/views/errors.py +11 -4
  116. plain/views/exceptions.py +4 -1
  117. plain/views/objects.py +15 -15
  118. plain/views/redirect.py +14 -10
  119. plain/views/templates.py +1 -1
  120. plain/wsgi.py +3 -1
  121. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/METADATA +1 -1
  122. plain-0.70.0.dist-info/RECORD +169 -0
  123. plain-0.69.0.dist-info/RECORD +0 -169
  124. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/WHEEL +0 -0
  125. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/entry_points.txt +0 -0
  126. {plain-0.69.0.dist-info → plain-0.70.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,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__(self, mode="w+b", bufsize=-1, suffix="", prefix="", dir=None):
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__(self, exc, value, tb):
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__(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,
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__(self, name, content, content_type="text/plain"):
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 HttpRequest
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: HttpRequest | 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, input_data, meta, content_length, boundary, encoding=None
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, input_data, meta, content_length, boundary, encoding=None
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,7 +260,7 @@ 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
 
@@ -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)
@@ -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 HttpRequest, 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[[HttpRequest], 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,7 +72,7 @@ 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):
75
+ def get_response(self, request: HttpRequest) -> Response:
67
76
  """Return a Response object for the given HttpRequest."""
68
77
 
69
78
  span_attributes = {
@@ -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: HttpRequest) -> 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: HttpRequest) -> 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(self, response, callback, name=None):
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
  """