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
plain/logs/utils.py CHANGED
@@ -1,17 +1,24 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from plain.http.request import HttpRequest
8
+ from plain.http.response import ResponseBase
2
9
 
3
10
  request_logger = logging.getLogger("plain.request")
4
11
 
5
12
 
6
13
  def log_response(
7
- message,
8
- *args,
9
- response=None,
10
- request=None,
11
- logger=request_logger,
12
- level=None,
13
- exception=None,
14
- ):
14
+ message: str,
15
+ *args: Any,
16
+ response: ResponseBase | None = None,
17
+ request: HttpRequest | None = None,
18
+ logger: logging.Logger = request_logger,
19
+ level: str | None = None,
20
+ exception: BaseException | None = None,
21
+ ) -> None:
15
22
  """
16
23
  Log errors based on Response status.
17
24
 
@@ -19,6 +26,9 @@ def log_response(
19
26
  is given as a keyword argument). The Response status_code and the
20
27
  request are passed to the logger's extra parameter.
21
28
  """
29
+ if response is None:
30
+ return
31
+
22
32
  # Check if the response has already been logged. Multiple requests to log
23
33
  # the same response can be received in some cases, e.g., when the
24
34
  # response is the result of an exception and is logged when the exception
@@ -43,4 +53,4 @@ def log_response(
43
53
  },
44
54
  exc_info=exception,
45
55
  )
46
- response._has_been_logged = True
56
+ response._has_been_logged = True # type: ignore[attr-defined]
plain/packages/config.py CHANGED
@@ -1,9 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  from functools import cached_property
3
5
  from importlib import import_module
6
+ from types import ModuleType
7
+ from typing import TYPE_CHECKING
4
8
 
5
9
  from plain.exceptions import ImproperlyConfigured
6
10
 
11
+ if TYPE_CHECKING:
12
+ from plain.packages.registry import PackagesRegistry
13
+
7
14
  CONFIG_MODULE_NAME = "config"
8
15
 
9
16
 
@@ -12,13 +19,13 @@ class PackageConfig:
12
19
 
13
20
  package_label: str
14
21
 
15
- def __init__(self, name):
22
+ def __init__(self, name: str):
16
23
  # Full Python path to the application e.g. 'plain.admin.admin'.
17
24
  self.name = name
18
25
 
19
26
  # Reference to the Packages registry that holds this PackageConfig. Set by the
20
27
  # registry when it registers the PackageConfig instance.
21
- self.packages_registry = None
28
+ self.packages: PackagesRegistry | None = None
22
29
 
23
30
  if not hasattr(self, "package_label"):
24
31
  # Last component of the Python path to the application e.g. 'admin'.
@@ -30,14 +37,14 @@ class PackageConfig:
30
37
  f"The app label '{self.package_label}' is not a valid Python identifier."
31
38
  )
32
39
 
33
- def __repr__(self):
40
+ def __repr__(self) -> str:
34
41
  return f"<{self.__class__.__name__}: {self.package_label}>"
35
42
 
36
43
  @cached_property
37
- def path(self):
44
+ def path(self) -> str:
38
45
  # Filesystem path to the application directory e.g.
39
46
  # '/path/to/admin'.
40
- def _path_from_module(module):
47
+ def _path_from_module(module: ModuleType) -> str:
41
48
  """Attempt to determine app's filesystem path from its module."""
42
49
  # See #21874 for extended discussion of the behavior of this method in
43
50
  # various cases.
@@ -68,7 +75,8 @@ class PackageConfig:
68
75
  module = import_module(self.name)
69
76
  return _path_from_module(module)
70
77
 
71
- def ready(self):
78
+ def ready(self) -> None:
72
79
  """
73
80
  Override this method in subclasses to run code when Plain starts.
74
81
  """
82
+ return None
@@ -1,13 +1,20 @@
1
+ from __future__ import annotations
2
+
1
3
  import sys
2
4
  import threading
3
5
  from collections import Counter
6
+ from collections.abc import Iterable
4
7
  from importlib import import_module
5
8
  from importlib.util import find_spec
9
+ from typing import TYPE_CHECKING
6
10
 
7
11
  from plain.exceptions import ImproperlyConfigured, PackageRegistryNotReady
8
12
 
9
13
  from .config import PackageConfig
10
14
 
15
+ if TYPE_CHECKING:
16
+ pass
17
+
11
18
  CONFIG_MODULE_NAME = "config"
12
19
 
13
20
 
@@ -18,7 +25,7 @@ class PackagesRegistry:
18
25
  It also keeps track of models, e.g. to provide reverse relations.
19
26
  """
20
27
 
21
- def __init__(self, installed_packages=()):
28
+ def __init__(self, installed_packages: Iterable[str | PackageConfig] | None = ()):
22
29
  # installed_packages is set to None when creating the main registry
23
30
  # because it cannot be populated at that point. Other registries must
24
31
  # provide a list of installed packages and are populated immediately.
@@ -28,7 +35,7 @@ class PackagesRegistry:
28
35
  raise RuntimeError("You must supply an installed_packages argument.")
29
36
 
30
37
  # Mapping of labels to PackageConfig instances for installed packages.
31
- self.package_configs = {}
38
+ self.package_configs: dict[str, PackageConfig] = {}
32
39
 
33
40
  # Whether the registry is populated.
34
41
  self.packages_ready = self.ready = False
@@ -41,7 +48,9 @@ class PackagesRegistry:
41
48
  if installed_packages is not None:
42
49
  self.populate(installed_packages)
43
50
 
44
- def populate(self, installed_packages=None):
51
+ def populate(
52
+ self, installed_packages: Iterable[str | PackageConfig] | None = None
53
+ ) -> None:
45
54
  """
46
55
  Load application configurations and models.
47
56
 
@@ -67,6 +76,9 @@ class PackagesRegistry:
67
76
  self.loading = True
68
77
 
69
78
  # Phase 1: initialize app configs and import app modules.
79
+ if installed_packages is None:
80
+ return
81
+
70
82
  for entry in installed_packages:
71
83
  if isinstance(entry, PackageConfig):
72
84
  # Some instances of the registry pass in the
@@ -92,9 +104,10 @@ class PackagesRegistry:
92
104
  entry_config = self.register_config(auto_package_config)
93
105
 
94
106
  # Make sure we have the same number of configs as we have installed packages
95
- if len(self.package_configs) != len(installed_packages):
107
+ installed_packages_list = list(installed_packages)
108
+ if len(self.package_configs) != len(installed_packages_list):
96
109
  raise ImproperlyConfigured(
97
- f"The number of installed packages ({len(installed_packages)}) does not match the number of "
110
+ f"The number of installed packages ({len(installed_packages_list)}) does not match the number of "
98
111
  f"registered configs ({len(self.package_configs)})."
99
112
  )
100
113
 
@@ -118,7 +131,7 @@ class PackagesRegistry:
118
131
 
119
132
  self.ready = True
120
133
 
121
- def check_packages_ready(self):
134
+ def check_packages_ready(self) -> None:
122
135
  """Raise an exception if all packages haven't been imported yet."""
123
136
  if not self.packages_ready:
124
137
  from plain.runtime import settings
@@ -129,12 +142,12 @@ class PackagesRegistry:
129
142
  settings.INSTALLED_PACKAGES
130
143
  raise PackageRegistryNotReady("Packages aren't loaded yet.")
131
144
 
132
- def get_package_configs(self):
145
+ def get_package_configs(self) -> Iterable[PackageConfig]:
133
146
  """Import applications and return an iterable of app configs."""
134
147
  self.check_packages_ready()
135
148
  return self.package_configs.values()
136
149
 
137
- def get_package_config(self, package_label):
150
+ def get_package_config(self, package_label: str) -> PackageConfig:
138
151
  """
139
152
  Import applications and returns an app config for the given label.
140
153
 
@@ -151,7 +164,7 @@ class PackagesRegistry:
151
164
  break
152
165
  raise LookupError(message)
153
166
 
154
- def get_containing_package_config(self, object_name):
167
+ def get_containing_package_config(self, object_name: str) -> PackageConfig | None:
155
168
  """
156
169
  Look for an app config containing a given object.
157
170
 
@@ -169,8 +182,9 @@ class PackagesRegistry:
169
182
  candidates.append(package_config)
170
183
  if candidates:
171
184
  return sorted(candidates, key=lambda ac: -len(ac.name))[0]
185
+ return None
172
186
 
173
- def register_config(self, package_config):
187
+ def register_config(self, package_config: PackageConfig) -> PackageConfig:
174
188
  """
175
189
  Add a config to the registry.
176
190
 
@@ -190,9 +204,10 @@ class PackagesRegistry:
190
204
  return package_config
191
205
 
192
206
  def autodiscover_modules(self, module_name: str, *, include_app: bool) -> None:
193
- def _import_if_exists(name):
207
+ def _import_if_exists(name: str) -> None:
194
208
  if find_spec(name):
195
209
  import_module(name)
210
+ return None
196
211
 
197
212
  # Load from all packages
198
213
  for package_config in self.get_package_configs():
@@ -206,7 +221,7 @@ class PackagesRegistry:
206
221
  packages_registry = PackagesRegistry(installed_packages=None)
207
222
 
208
223
 
209
- def register_config(package_config_class):
224
+ def register_config(package_config_class: type[PackageConfig]) -> type[PackageConfig]:
210
225
  """A decorator to register a PackageConfig subclass."""
211
226
  module_name = package_config_class.__module__
212
227
 
plain/paginator.py CHANGED
@@ -1,8 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import collections.abc
2
4
  import inspect
3
5
  import warnings
6
+ from collections.abc import Iterator
4
7
  from functools import cached_property
5
8
  from math import ceil
9
+ from typing import Any
6
10
 
7
11
  from plain.utils.inspect import method_has_no_args
8
12
 
@@ -24,18 +28,24 @@ class EmptyPage(InvalidPage):
24
28
 
25
29
 
26
30
  class Paginator:
27
- def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
31
+ def __init__(
32
+ self,
33
+ object_list: Any,
34
+ per_page: int,
35
+ orphans: int = 0,
36
+ allow_empty_first_page: bool = True,
37
+ ) -> None:
28
38
  self.object_list = object_list
29
39
  self._check_object_list_is_ordered()
30
40
  self.per_page = int(per_page)
31
41
  self.orphans = int(orphans)
32
42
  self.allow_empty_first_page = allow_empty_first_page
33
43
 
34
- def __iter__(self):
44
+ def __iter__(self) -> Iterator[Page]:
35
45
  for page_number in self.page_range:
36
46
  yield self.page(page_number)
37
47
 
38
- def validate_number(self, number):
48
+ def validate_number(self, number: Any) -> int:
39
49
  """Validate the given 1-based page number."""
40
50
  try:
41
51
  if isinstance(number, float) and not number.is_integer():
@@ -49,7 +59,7 @@ class Paginator:
49
59
  raise EmptyPage("That page contains no results")
50
60
  return number
51
61
 
52
- def get_page(self, number):
62
+ def get_page(self, number: Any) -> Page:
53
63
  """
54
64
  Return a valid page, even if the page argument isn't a number or isn't
55
65
  in range.
@@ -62,7 +72,7 @@ class Paginator:
62
72
  number = self.num_pages
63
73
  return self.page(number)
64
74
 
65
- def page(self, number):
75
+ def page(self, number: Any) -> Page:
66
76
  """Return a Page object for the given 1-based page number."""
67
77
  number = self.validate_number(number)
68
78
  bottom = (number - 1) * self.per_page
@@ -71,7 +81,7 @@ class Paginator:
71
81
  top = self.count
72
82
  return self._get_page(self.object_list[bottom:top], number, self)
73
83
 
74
- def _get_page(self, *args, **kwargs):
84
+ def _get_page(self, *args: Any, **kwargs: Any) -> Page:
75
85
  """
76
86
  Return an instance of a single page.
77
87
 
@@ -81,7 +91,7 @@ class Paginator:
81
91
  return Page(*args, **kwargs)
82
92
 
83
93
  @cached_property
84
- def count(self):
94
+ def count(self) -> int:
85
95
  """Return the total number of objects, across all pages."""
86
96
  c = getattr(self.object_list, "count", None)
87
97
  if callable(c) and not inspect.isbuiltin(c) and method_has_no_args(c):
@@ -89,7 +99,7 @@ class Paginator:
89
99
  return len(self.object_list)
90
100
 
91
101
  @cached_property
92
- def num_pages(self):
102
+ def num_pages(self) -> int:
93
103
  """Return the total number of pages."""
94
104
  if self.count == 0 and not self.allow_empty_first_page:
95
105
  return 0
@@ -97,14 +107,14 @@ class Paginator:
97
107
  return ceil(hits / self.per_page)
98
108
 
99
109
  @property
100
- def page_range(self):
110
+ def page_range(self) -> range:
101
111
  """
102
112
  Return a 1-based range of pages for iterating through within
103
113
  a template for loop.
104
114
  """
105
115
  return range(1, self.num_pages + 1)
106
116
 
107
- def _check_object_list_is_ordered(self):
117
+ def _check_object_list_is_ordered(self) -> None:
108
118
  """
109
119
  Warn if self.object_list is unordered (typically a QuerySet).
110
120
  """
@@ -124,18 +134,18 @@ class Paginator:
124
134
 
125
135
 
126
136
  class Page(collections.abc.Sequence):
127
- def __init__(self, object_list, number, paginator):
137
+ def __init__(self, object_list: Any, number: int, paginator: Paginator) -> None:
128
138
  self.object_list = object_list
129
139
  self.number = number
130
140
  self.paginator = paginator
131
141
 
132
- def __repr__(self):
142
+ def __repr__(self) -> str:
133
143
  return f"<Page {self.number} of {self.paginator.num_pages}>"
134
144
 
135
- def __len__(self):
145
+ def __len__(self) -> int:
136
146
  return len(self.object_list)
137
147
 
138
- def __getitem__(self, index):
148
+ def __getitem__(self, index: int | slice) -> Any:
139
149
  if not isinstance(index, int | slice):
140
150
  raise TypeError(
141
151
  f"Page indices must be integers or slices, not {type(index).__name__}."
@@ -146,22 +156,22 @@ class Page(collections.abc.Sequence):
146
156
  self.object_list = list(self.object_list)
147
157
  return self.object_list[index]
148
158
 
149
- def has_next(self):
159
+ def has_next(self) -> bool:
150
160
  return self.number < self.paginator.num_pages
151
161
 
152
- def has_previous(self):
162
+ def has_previous(self) -> bool:
153
163
  return self.number > 1
154
164
 
155
- def has_other_pages(self):
165
+ def has_other_pages(self) -> bool:
156
166
  return self.has_previous() or self.has_next()
157
167
 
158
- def next_page_number(self):
168
+ def next_page_number(self) -> int:
159
169
  return self.paginator.validate_number(self.number + 1)
160
170
 
161
- def previous_page_number(self):
171
+ def previous_page_number(self) -> int:
162
172
  return self.paginator.validate_number(self.number - 1)
163
173
 
164
- def start_index(self):
174
+ def start_index(self) -> int:
165
175
  """
166
176
  Return the 1-based index of the first object on this page,
167
177
  relative to total objects in the paginator.
@@ -171,7 +181,7 @@ class Page(collections.abc.Sequence):
171
181
  return 0
172
182
  return (self.paginator.per_page * (self.number - 1)) + 1
173
183
 
174
- def end_index(self):
184
+ def end_index(self) -> int:
175
185
  """
176
186
  Return the 1-based index of the last object on this page,
177
187
  relative to total objects found (hits).
plain/preflight/checks.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC, abstractmethod
2
4
 
3
5
 
@@ -5,6 +7,6 @@ class PreflightCheck(ABC):
5
7
  """Base class for all preflight checks."""
6
8
 
7
9
  @abstractmethod
8
- def run(self):
10
+ def run(self) -> list:
9
11
  """Must return a list of Warning/Error results."""
10
12
  raise NotImplementedError
plain/preflight/files.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
  from plain.runtime import settings
@@ -11,7 +13,7 @@ from .results import PreflightResult
11
13
  class CheckSettingFileUploadTempDir(PreflightCheck):
12
14
  """Validates that the FILE_UPLOAD_TEMP_DIR setting points to an existing directory."""
13
15
 
14
- def run(self):
16
+ def run(self) -> list[PreflightResult]:
15
17
  setting = settings.FILE_UPLOAD_TEMP_DIR
16
18
  if setting and not Path(setting).is_dir():
17
19
  return [
@@ -1,11 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Generator
4
+ from typing import Any, TypeVar
5
+
1
6
  from plain.runtime import settings
2
7
 
8
+ from .results import PreflightResult
3
9
 
4
- class CheckRegistry:
5
- def __init__(self):
6
- self.checks = {} # name -> (check_class, deploy)
10
+ T = TypeVar("T")
7
11
 
8
- def register_check(self, check_class, name, deploy=False):
12
+
13
+ class CheckRegistry:
14
+ def __init__(self) -> None:
15
+ self.checks: dict[
16
+ str, tuple[type[Any], bool]
17
+ ] = {} # name -> (check_class, deploy)
18
+
19
+ def register_check(
20
+ self, check_class: type[Any], name: str, deploy: bool = False
21
+ ) -> None:
9
22
  """Register a check class with a unique name."""
10
23
  if name in self.checks:
11
24
  raise ValueError(f"Check {name} already registered")
@@ -13,8 +26,8 @@ class CheckRegistry:
13
26
 
14
27
  def run_checks(
15
28
  self,
16
- include_deploy_checks=False,
17
- ):
29
+ include_deploy_checks: bool = False,
30
+ ) -> Generator[tuple[type[Any], str, list[PreflightResult]]]:
18
31
  """
19
32
  Run all registered checks and yield (check_class, name, results) tuples.
20
33
  """
@@ -42,9 +55,11 @@ class CheckRegistry:
42
55
  results = check.run()
43
56
  yield check_class, name, results
44
57
 
45
- def get_checks(self, include_deploy_checks=False):
58
+ def get_checks(
59
+ self, include_deploy_checks: bool = False
60
+ ) -> list[tuple[type[Any], str]]:
46
61
  """Get list of (check_class, name) tuples."""
47
- result = []
62
+ result: list[tuple[type[Any], str]] = []
48
63
  for name, (check_class, deploy) in self.checks.items():
49
64
  if deploy and not include_deploy_checks:
50
65
  continue
@@ -55,7 +70,7 @@ class CheckRegistry:
55
70
  checks_registry = CheckRegistry()
56
71
 
57
72
 
58
- def register_check(name: str, *, deploy: bool = False):
73
+ def register_check(name: str, *, deploy: bool = False) -> Callable[[type[T]], type[T]]:
59
74
  """
60
75
  Decorator to register a check class.
61
76
 
@@ -69,7 +84,7 @@ def register_check(name: str, *, deploy: bool = False):
69
84
  pass
70
85
  """
71
86
 
72
- def wrapper(cls):
87
+ def wrapper(cls: type[T]) -> type[T]:
73
88
  checks_registry.register_check(cls, name=name, deploy=deploy)
74
89
  return cls
75
90
 
@@ -1,20 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
1
5
  from plain.runtime import settings
2
6
 
3
7
 
4
8
  class PreflightResult:
5
- def __init__(self, *, fix: str, id: str, obj=None, warning: bool = False):
9
+ def __init__(
10
+ self, *, fix: str, id: str, obj: Any = None, warning: bool = False
11
+ ) -> None:
6
12
  self.fix = fix
7
13
  self.obj = obj
8
14
  self.id = id
9
15
  self.warning = warning
10
16
 
11
- def __eq__(self, other):
17
+ def __eq__(self, other: object) -> bool:
12
18
  return isinstance(other, self.__class__) and all(
13
19
  getattr(self, attr) == getattr(other, attr)
14
20
  for attr in ["fix", "obj", "id", "warning"]
15
21
  )
16
22
 
17
- def __str__(self):
23
+ def __str__(self) -> str:
18
24
  if self.obj is None:
19
25
  obj = ""
20
26
  elif hasattr(self.obj, "_meta") and hasattr(self.obj._meta, "label"):
@@ -25,5 +31,5 @@ class PreflightResult:
25
31
  id_part = f"({self.id}) " if self.id else ""
26
32
  return f"{obj}: {id_part}{self.fix}"
27
33
 
28
- def is_silenced(self):
34
+ def is_silenced(self) -> bool:
29
35
  return self.id and self.id in settings.PREFLIGHT_SILENCED_RESULTS
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from plain.runtime import settings
2
4
 
3
5
  from .checks import PreflightCheck
@@ -8,7 +10,7 @@ SECRET_KEY_MIN_LENGTH = 50
8
10
  SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5
9
11
 
10
12
 
11
- def _check_secret_key(secret_key):
13
+ def _check_secret_key(secret_key: str) -> bool:
12
14
  return (
13
15
  len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
14
16
  and len(secret_key) >= SECRET_KEY_MIN_LENGTH
@@ -19,7 +21,7 @@ def _check_secret_key(secret_key):
19
21
  class CheckSecretKey(PreflightCheck):
20
22
  """Validates that SECRET_KEY is long and random enough for security."""
21
23
 
22
- def run(self):
24
+ def run(self) -> list[PreflightResult]:
23
25
  if not _check_secret_key(settings.SECRET_KEY):
24
26
  return [
25
27
  PreflightResult(
@@ -36,7 +38,7 @@ class CheckSecretKey(PreflightCheck):
36
38
  class CheckSecretKeyFallbacks(PreflightCheck):
37
39
  """Validates that SECRET_KEY_FALLBACKS are long and random enough for security."""
38
40
 
39
- def run(self):
41
+ def run(self) -> list[PreflightResult]:
40
42
  errors = []
41
43
  for index, key in enumerate(settings.SECRET_KEY_FALLBACKS):
42
44
  if not _check_secret_key(key):
@@ -55,7 +57,7 @@ class CheckSecretKeyFallbacks(PreflightCheck):
55
57
  class CheckDebug(PreflightCheck):
56
58
  """Ensures DEBUG is False in production deployment."""
57
59
 
58
- def run(self):
60
+ def run(self) -> list[PreflightResult]:
59
61
  if settings.DEBUG:
60
62
  return [
61
63
  PreflightResult(
@@ -70,7 +72,7 @@ class CheckDebug(PreflightCheck):
70
72
  class CheckAllowedHosts(PreflightCheck):
71
73
  """Ensures ALLOWED_HOSTS is not empty in production deployment."""
72
74
 
73
- def run(self):
75
+ def run(self) -> list[PreflightResult]:
74
76
  if not settings.ALLOWED_HOSTS:
75
77
  return [
76
78
  PreflightResult(
plain/preflight/urls.py CHANGED
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  from .checks import PreflightCheck
2
4
  from .registry import register_check
5
+ from .results import PreflightResult
3
6
 
4
7
 
5
8
  @register_check("urls.config")
6
9
  class CheckUrlConfig(PreflightCheck):
7
10
  """Validates the URL configuration for common issues."""
8
11
 
9
- def run(self):
12
+ def run(self) -> list[PreflightResult]:
10
13
  from plain.urls import get_resolver
11
14
 
12
15
  resolver = get_resolver()
plain/runtime/__init__.py CHANGED
@@ -2,6 +2,7 @@ import importlib.metadata
2
2
  import sys
3
3
  from importlib.metadata import entry_points
4
4
  from pathlib import Path
5
+ from typing import Self
5
6
 
6
7
  from plain.logs.configure import configure_logging
7
8
  from plain.packages import packages_registry
@@ -32,7 +33,7 @@ class SetupError(RuntimeError):
32
33
  pass
33
34
 
34
35
 
35
- def setup():
36
+ def setup() -> None:
36
37
  """
37
38
  Configure the settings (this happens as a side effect of accessing the
38
39
  first setting), configure logging and populate the app registry.
@@ -77,11 +78,11 @@ class SettingsReference(str):
77
78
  the value in memory but serializes to a settings.NAME attribute reference.
78
79
  """
79
80
 
80
- def __new__(self, setting_name):
81
+ def __new__(self, setting_name: str) -> Self:
81
82
  value = getattr(settings, setting_name)
82
83
  return str.__new__(self, value)
83
84
 
84
- def __init__(self, setting_name):
85
+ def __init__(self, setting_name: str):
85
86
  self.setting_name = setting_name
86
87
 
87
88
 
@@ -114,7 +114,7 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
114
114
  FILE_UPLOAD_TEMP_DIR = None
115
115
 
116
116
  # User-defined overrides for error views by status code
117
- HTTP_ERROR_VIEWS: dict[int] = {}
117
+ HTTP_ERROR_VIEWS: dict[int, type] = {}
118
118
 
119
119
  # MARK: Middleware
120
120