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
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from functools import wraps
5
+ from typing import TYPE_CHECKING
3
6
 
4
7
  from plain import signals
5
8
  from plain.exceptions import (
@@ -17,8 +20,15 @@ from plain.runtime import settings
17
20
  from plain.utils.module_loading import import_string
18
21
  from plain.views.errors import ErrorView
19
22
 
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+
26
+ from plain.http import HttpRequest, Response
27
+
20
28
 
21
- def convert_exception_to_response(get_response):
29
+ def convert_exception_to_response(
30
+ get_response: Callable[[HttpRequest], Response],
31
+ ) -> Callable[[HttpRequest], Response]:
22
32
  """
23
33
  Wrap the given get_response callable in exception-to-response conversion.
24
34
 
@@ -33,7 +43,7 @@ def convert_exception_to_response(get_response):
33
43
  """
34
44
 
35
45
  @wraps(get_response)
36
- def inner(request):
46
+ def inner(request: HttpRequest) -> Response:
37
47
  try:
38
48
  response = get_response(request)
39
49
  except Exception as exc:
@@ -43,7 +53,7 @@ def convert_exception_to_response(get_response):
43
53
  return inner
44
54
 
45
55
 
46
- def response_for_exception(request, exc):
56
+ def response_for_exception(request: HttpRequest, exc: Exception) -> Response:
47
57
  if isinstance(exc, Http404):
48
58
  response = get_exception_response(
49
59
  request=request, status_code=404, exception=None
@@ -120,7 +130,9 @@ def response_for_exception(request, exc):
120
130
  return response
121
131
 
122
132
 
123
- def get_exception_response(*, request, status_code, exception):
133
+ def get_exception_response(
134
+ *, request: HttpRequest, status_code: int, exception: Exception | None
135
+ ) -> Response:
124
136
  try:
125
137
  view_class = get_error_view(status_code=status_code, exception=exception)
126
138
  return view_class(request)
@@ -135,7 +147,9 @@ def get_exception_response(*, request, status_code, exception):
135
147
  return ResponseServerError()
136
148
 
137
149
 
138
- def get_error_view(*, status_code, exception):
150
+ def get_error_view(
151
+ *, status_code: int, exception: Exception | None
152
+ ) -> Callable[[HttpRequest], Response]:
139
153
  views_by_status = settings.HTTP_ERROR_VIEWS
140
154
  if status_code in views_by_status:
141
155
  view = views_by_status[status_code]
@@ -1,13 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  import uuid
2
4
  from functools import cached_property
3
5
  from io import IOBase
6
+ from typing import TYPE_CHECKING
4
7
  from urllib.parse import quote
5
8
 
6
9
  from plain import signals
7
10
  from plain.http import HttpRequest, QueryDict, parse_cookie
8
11
  from plain.internal.handlers import base
12
+ from plain.utils.datastructures import MultiValueDict
9
13
  from plain.utils.regex_helper import _lazy_re_compile
10
14
 
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Callable, Iterable
17
+ from typing import Any
18
+
11
19
  _slashes_re = _lazy_re_compile(rb"/+")
12
20
 
13
21
 
@@ -19,13 +27,13 @@ class LimitedStream(IOBase):
19
27
  See https://github.com/pallets/werkzeug/blob/dbf78f67/src/werkzeug/wsgi.py#L828
20
28
  """
21
29
 
22
- def __init__(self, stream, limit):
30
+ def __init__(self, stream: Any, limit: int) -> None:
23
31
  self._read = stream.read
24
32
  self._readline = stream.readline
25
33
  self._pos = 0
26
34
  self.limit = limit
27
35
 
28
- def read(self, size=-1, /):
36
+ def read(self, size: int = -1, /) -> bytes:
29
37
  _pos = self._pos
30
38
  limit = self.limit
31
39
  if _pos >= limit:
@@ -38,7 +46,7 @@ class LimitedStream(IOBase):
38
46
  self._pos += len(data)
39
47
  return data
40
48
 
41
- def readline(self, size=-1, /):
49
+ def readline(self, size: int = -1, /) -> bytes:
42
50
  _pos = self._pos
43
51
  limit = self.limit
44
52
  if _pos >= limit:
@@ -56,7 +64,7 @@ class WSGIRequest(HttpRequest):
56
64
  non_picklable_attrs = HttpRequest.non_picklable_attrs | frozenset(["environ"])
57
65
  meta_non_picklable_attrs = frozenset(["wsgi.errors", "wsgi.input"])
58
66
 
59
- def __init__(self, environ):
67
+ def __init__(self, environ: dict[str, Any]) -> None:
60
68
  # A unique ID we can use to trace this request
61
69
  self.unique_id = str(uuid.uuid4())
62
70
 
@@ -86,37 +94,37 @@ class WSGIRequest(HttpRequest):
86
94
  self._read_started = False
87
95
  self.resolver_match = None
88
96
 
89
- def __getstate__(self):
97
+ def __getstate__(self) -> dict[str, Any]:
90
98
  state = super().__getstate__()
91
99
  for attr in self.meta_non_picklable_attrs:
92
100
  if attr in state["meta"]:
93
101
  del state["meta"][attr]
94
102
  return state
95
103
 
96
- def _get_scheme(self):
104
+ def _get_scheme(self) -> str | None:
97
105
  return self.environ.get("wsgi.url_scheme")
98
106
 
99
107
  @cached_property
100
- def query_params(self):
108
+ def query_params(self) -> QueryDict:
101
109
  # The WSGI spec says 'QUERY_STRING' may be absent.
102
110
  raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "")
103
111
  return QueryDict(raw_query_string, encoding=self._encoding)
104
112
 
105
- def _get_data(self):
113
+ def _get_data(self) -> QueryDict:
106
114
  if not hasattr(self, "_data"):
107
115
  self._load_data_and_files()
108
116
  return self._data
109
117
 
110
- def _set_data(self, data):
118
+ def _set_data(self, data: QueryDict) -> None:
111
119
  self._data = data
112
120
 
113
121
  @cached_property
114
- def cookies(self):
122
+ def cookies(self) -> dict[str, str]:
115
123
  raw_cookie = get_str_from_wsgi(self.environ, "HTTP_COOKIE", "")
116
124
  return parse_cookie(raw_cookie)
117
125
 
118
126
  @property
119
- def files(self):
127
+ def files(self) -> MultiValueDict:
120
128
  if not hasattr(self, "_files"):
121
129
  self._load_data_and_files()
122
130
  return self._files
@@ -125,11 +133,15 @@ class WSGIRequest(HttpRequest):
125
133
 
126
134
 
127
135
  class WSGIHandler(base.BaseHandler):
128
- def __init__(self, *args, **kwargs):
136
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
129
137
  super().__init__(*args, **kwargs)
130
138
  self.load_middleware()
131
139
 
132
- def __call__(self, environ, start_response):
140
+ def __call__(
141
+ self,
142
+ environ: dict[str, Any],
143
+ start_response: Callable[[str, list[tuple[str, str]]], Any],
144
+ ) -> Iterable[bytes]:
133
145
  signals.request_started.send(sender=self.__class__, environ=environ)
134
146
  request = WSGIRequest(environ)
135
147
  response = self.get_response(request)
@@ -155,11 +167,11 @@ class WSGIHandler(base.BaseHandler):
155
167
  return response
156
168
 
157
169
 
158
- def get_path_info(environ):
170
+ def get_path_info(environ: dict[str, Any]) -> str:
159
171
  """Return the HTTP request's PATH_INFO as a string."""
160
172
  path_info = get_bytes_from_wsgi(environ, "PATH_INFO", "/")
161
173
 
162
- def repercent_broken_unicode(path):
174
+ def repercent_broken_unicode(path: bytes) -> bytes:
163
175
  """
164
176
  As per RFC 3987 Section 3.2, step three of converting a URI into an IRI,
165
177
  repercent-encode any octet produced that is not part of a strictly legal
@@ -179,7 +191,7 @@ def get_path_info(environ):
179
191
  return repercent_broken_unicode(path_info).decode()
180
192
 
181
193
 
182
- def get_script_name(environ):
194
+ def get_script_name(environ: dict[str, Any]) -> str:
183
195
  """
184
196
  Return the equivalent of the HTTP request's SCRIPT_NAME environment
185
197
  variable. If Apache mod_rewrite is used, return what would have been
@@ -208,7 +220,7 @@ def get_script_name(environ):
208
220
  return script_name.decode()
209
221
 
210
222
 
211
- def get_bytes_from_wsgi(environ, key, default):
223
+ def get_bytes_from_wsgi(environ: dict[str, Any], key: str, default: str) -> bytes:
212
224
  """
213
225
  Get a value from the WSGI environ dictionary as bytes.
214
226
 
@@ -221,7 +233,7 @@ def get_bytes_from_wsgi(environ, key, default):
221
233
  return value.encode("iso-8859-1")
222
234
 
223
235
 
224
- def get_str_from_wsgi(environ, key, default):
236
+ def get_str_from_wsgi(environ: dict[str, Any], key: str, default: str) -> str:
225
237
  """
226
238
  Get a value from the WSGI environ dictionary as str.
227
239
 
@@ -1,11 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from plain.runtime import settings
2
6
 
7
+ if TYPE_CHECKING:
8
+ from collections.abc import Callable
9
+
10
+ from plain.http import HttpRequest, Response
11
+
3
12
 
4
13
  class DefaultHeadersMiddleware:
5
- def __init__(self, get_response):
14
+ def __init__(self, get_response: Callable[[HttpRequest], Response]) -> None:
6
15
  self.get_response = get_response
7
16
 
8
- def __call__(self, request):
17
+ def __call__(self, request: HttpRequest) -> Response:
9
18
  response = self.get_response(request)
10
19
 
11
20
  for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
@@ -1,10 +1,18 @@
1
+ from __future__ import annotations
2
+
1
3
  import ipaddress
2
4
  import logging
5
+ from typing import TYPE_CHECKING
3
6
 
4
7
  from plain.http import HttpRequest, ResponseBadRequest
5
8
  from plain.runtime import settings
6
9
  from plain.utils.regex_helper import _lazy_re_compile
7
10
 
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ from plain.http import Response
15
+
8
16
  logger = logging.getLogger(__name__)
9
17
 
10
18
  host_validation_re = _lazy_re_compile(
@@ -21,10 +29,10 @@ class HostValidationMiddleware:
21
29
  host is not allowed.
22
30
  """
23
31
 
24
- def __init__(self, get_response):
32
+ def __init__(self, get_response: Callable[[HttpRequest], Response]) -> None:
25
33
  self.get_response = get_response
26
34
 
27
- def __call__(self, request):
35
+ def __call__(self, request: HttpRequest) -> Response:
28
36
  if not is_host_valid(request):
29
37
  host = request.host
30
38
  msg = f"Invalid HTTP_HOST header: {host!r}."
@@ -1,15 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from plain.http import ResponseRedirect
2
6
  from plain.runtime import settings
3
7
 
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+ from plain.http import HttpRequest, Response
12
+
4
13
 
5
14
  class HttpsRedirectMiddleware:
6
- def __init__(self, get_response):
15
+ def __init__(self, get_response: Callable[[HttpRequest], Response]) -> None:
7
16
  self.get_response = get_response
8
17
 
9
18
  # Settings for HTTPS
10
19
  self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
11
20
 
12
- def __call__(self, request):
21
+ def __call__(self, request: HttpRequest) -> Response:
13
22
  """
14
23
  Perform a blanket HTTP→HTTPS redirect when enabled.
15
24
  """
@@ -19,8 +28,9 @@ class HttpsRedirectMiddleware:
19
28
 
20
29
  return self.get_response(request)
21
30
 
22
- def maybe_https_redirect(self, request):
31
+ def maybe_https_redirect(self, request: HttpRequest) -> Response | None:
23
32
  if self.https_redirect_enabled and not request.is_https():
24
33
  return ResponseRedirect(
25
34
  f"https://{request.host}{request.get_full_path()}", status_code=301
26
35
  )
36
+ return None
@@ -1,14 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
1
5
  from plain.http import ResponseRedirect
2
6
  from plain.runtime import settings
3
7
  from plain.urls import Resolver404, get_resolver
4
8
  from plain.utils.http import escape_leading_slashes
5
9
 
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ from plain.http import HttpRequest, Response
14
+ from plain.urls import ResolverMatch
15
+
6
16
 
7
17
  class RedirectSlashMiddleware:
8
- def __init__(self, get_response):
18
+ def __init__(self, get_response: Callable[[HttpRequest], Response]) -> None:
9
19
  self.get_response = get_response
10
20
 
11
- def __call__(self, request):
21
+ def __call__(self, request: HttpRequest) -> Response:
12
22
  """
13
23
  Rewrite the URL based on settings.APPEND_SLASH
14
24
  """
@@ -29,7 +39,7 @@ class RedirectSlashMiddleware:
29
39
  return response
30
40
 
31
41
  @staticmethod
32
- def _is_valid_path(path):
42
+ def _is_valid_path(path: str) -> ResolverMatch | bool:
33
43
  """
34
44
  Return the ResolverMatch if the given path resolves against the default URL
35
45
  resolver, False otherwise. This is a convenience method to make working
@@ -40,7 +50,7 @@ class RedirectSlashMiddleware:
40
50
  except Resolver404:
41
51
  return False
42
52
 
43
- def should_redirect_with_slash(self, request):
53
+ def should_redirect_with_slash(self, request: HttpRequest) -> ResolverMatch | bool:
44
54
  """
45
55
  Return True if settings.APPEND_SLASH is True and appending a slash to
46
56
  the request path turns an invalid path into a valid one.
@@ -50,7 +60,7 @@ class RedirectSlashMiddleware:
50
60
  return self._is_valid_path(f"{request.path_info}/")
51
61
  return False
52
62
 
53
- def get_full_path_with_slash(self, request):
63
+ def get_full_path_with_slash(self, request: HttpRequest) -> str:
54
64
  """
55
65
  Return the full path of the request with a trailing slash appended.
56
66
 
plain/json.py CHANGED
@@ -2,6 +2,7 @@ import datetime
2
2
  import decimal
3
3
  import json
4
4
  import uuid
5
+ from typing import Any
5
6
 
6
7
  from plain.utils.duration import duration_iso_string
7
8
  from plain.utils.functional import Promise
@@ -14,7 +15,7 @@ class PlainJSONEncoder(json.JSONEncoder):
14
15
  UUIDs.
15
16
  """
16
17
 
17
- def default(self, o):
18
+ def default(self, o: Any) -> Any:
18
19
  # See "Date Time String Format" in the ECMA-262 specification.
19
20
  if isinstance(o, datetime.datetime):
20
21
  r = o.isoformat()
plain/logs/configure.py CHANGED
@@ -3,7 +3,9 @@ import logging
3
3
  from .formatters import JSONFormatter, KeyValueFormatter
4
4
 
5
5
 
6
- def configure_logging(*, plain_log_level, app_log_level, app_log_format):
6
+ def configure_logging(
7
+ *, plain_log_level: int | str, app_log_level: int | str, app_log_format: str
8
+ ) -> None:
7
9
  # Create and configure the plain logger (uses standard Logger, not AppLogger)
8
10
  plain_logger = logging.Logger("plain")
9
11
  plain_logger.setLevel(plain_log_level)
plain/logs/debug.py CHANGED
@@ -1,26 +1,37 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import threading
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from types import TracebackType
3
9
 
4
10
 
5
11
  class DebugMode:
6
12
  """Context manager to temporarily set DEBUG level on a logger with reference counting."""
7
13
 
8
- def __init__(self, logger):
14
+ def __init__(self, logger: logging.Logger):
9
15
  self.logger = logger
10
16
  self.original_level = None
11
17
  self._ref_count = 0
12
18
  self._lock = threading.Lock()
13
19
 
14
- def __enter__(self):
20
+ def __enter__(self) -> DebugMode:
15
21
  """Store original level and set to DEBUG."""
16
22
  self.start()
17
23
  return self
18
24
 
19
- def __exit__(self, exc_type, exc_val, exc_tb):
25
+ def __exit__(
26
+ self,
27
+ exc_type: type[BaseException] | None,
28
+ exc_val: BaseException | None,
29
+ exc_tb: TracebackType | None,
30
+ ) -> None:
20
31
  """Restore original level."""
21
32
  self.end()
22
33
 
23
- def start(self):
34
+ def start(self) -> None:
24
35
  """Enable DEBUG logging level."""
25
36
  with self._lock:
26
37
  if self._ref_count == 0:
@@ -28,7 +39,7 @@ class DebugMode:
28
39
  self.logger.setLevel(logging.DEBUG)
29
40
  self._ref_count += 1
30
41
 
31
- def end(self):
42
+ def end(self) -> None:
32
43
  """Restore original logging level."""
33
44
  with self._lock:
34
45
  self._ref_count = max(0, self._ref_count - 1)
plain/logs/formatters.py CHANGED
@@ -1,11 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import logging
5
+ from typing import Any
3
6
 
4
7
 
5
8
  class KeyValueFormatter(logging.Formatter):
6
9
  """Formatter that outputs key-value pairs from Plain's context system."""
7
10
 
8
- def format(self, record):
11
+ def format(self, record: logging.LogRecord) -> str:
9
12
  # Build key-value pairs from context
10
13
  kv_pairs = []
11
14
 
@@ -22,7 +25,7 @@ class KeyValueFormatter(logging.Formatter):
22
25
  return super().format(record)
23
26
 
24
27
  @staticmethod
25
- def _format_value(value):
28
+ def _format_value(value: Any) -> str:
26
29
  """Format a value for key-value output."""
27
30
  if isinstance(value, str):
28
31
  s = value
@@ -46,7 +49,7 @@ class KeyValueFormatter(logging.Formatter):
46
49
  class JSONFormatter(logging.Formatter):
47
50
  """Formatter that outputs JSON from Plain's context system, with optional format string."""
48
51
 
49
- def format(self, record):
52
+ def format(self, record: logging.LogRecord) -> str:
50
53
  # Build the JSON object from Plain's context data
51
54
  log_obj = {
52
55
  "timestamp": self.formatTime(record),
plain/logs/loggers.py CHANGED
@@ -1,5 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
4
+ from collections.abc import Generator
2
5
  from contextlib import contextmanager
6
+ from typing import Any
3
7
 
4
8
  from .debug import DebugMode
5
9
 
@@ -7,13 +11,13 @@ from .debug import DebugMode
7
11
  class AppLogger(logging.Logger):
8
12
  """Enhanced logger that supports kwargs-style logging and context management."""
9
13
 
10
- def __init__(self, name):
14
+ def __init__(self, name: str):
11
15
  super().__init__(name)
12
16
  self.context = {} # Public, mutable context dict
13
17
  self.debug_mode = DebugMode(self)
14
18
 
15
19
  @contextmanager
16
- def include_context(self, **kwargs):
20
+ def include_context(self, **kwargs: Any) -> Generator[None, None, None]:
17
21
  """Context manager for temporary context."""
18
22
  # Store original context
19
23
  original_context = self.context.copy()
@@ -27,21 +31,21 @@ class AppLogger(logging.Logger):
27
31
  # Restore original context
28
32
  self.context = original_context
29
33
 
30
- def force_debug(self):
34
+ def force_debug(self) -> DebugMode:
31
35
  """Return context manager for temporarily enabling DEBUG level logging."""
32
36
  return self.debug_mode
33
37
 
34
38
  # Override logging methods with explicit parameters for IDE support
35
39
  def debug(
36
40
  self,
37
- msg,
38
- *args,
39
- exc_info=None,
40
- extra=None,
41
- stack_info=False,
42
- stacklevel=1,
43
- **context,
44
- ):
41
+ msg: object,
42
+ *args: object,
43
+ exc_info: Any = None,
44
+ extra: dict[str, Any] | None = None,
45
+ stack_info: bool = False,
46
+ stacklevel: int = 1,
47
+ **context: Any,
48
+ ) -> None:
45
49
  if self.isEnabledFor(logging.DEBUG):
46
50
  self._log(
47
51
  logging.DEBUG,
@@ -56,14 +60,14 @@ class AppLogger(logging.Logger):
56
60
 
57
61
  def info(
58
62
  self,
59
- msg,
60
- *args,
61
- exc_info=None,
62
- extra=None,
63
- stack_info=False,
64
- stacklevel=1,
65
- **context,
66
- ):
63
+ msg: object,
64
+ *args: object,
65
+ exc_info: Any = None,
66
+ extra: dict[str, Any] | None = None,
67
+ stack_info: bool = False,
68
+ stacklevel: int = 1,
69
+ **context: Any,
70
+ ) -> None:
67
71
  if self.isEnabledFor(logging.INFO):
68
72
  self._log(
69
73
  logging.INFO,
@@ -78,14 +82,14 @@ class AppLogger(logging.Logger):
78
82
 
79
83
  def warning(
80
84
  self,
81
- msg,
82
- *args,
83
- exc_info=None,
84
- extra=None,
85
- stack_info=False,
86
- stacklevel=1,
87
- **context,
88
- ):
85
+ msg: object,
86
+ *args: object,
87
+ exc_info: Any = None,
88
+ extra: dict[str, Any] | None = None,
89
+ stack_info: bool = False,
90
+ stacklevel: int = 1,
91
+ **context: Any,
92
+ ) -> None:
89
93
  if self.isEnabledFor(logging.WARNING):
90
94
  self._log(
91
95
  logging.WARNING,
@@ -100,14 +104,14 @@ class AppLogger(logging.Logger):
100
104
 
101
105
  def error(
102
106
  self,
103
- msg,
104
- *args,
105
- exc_info=None,
106
- extra=None,
107
- stack_info=False,
108
- stacklevel=1,
109
- **context,
110
- ):
107
+ msg: object,
108
+ *args: object,
109
+ exc_info: Any = None,
110
+ extra: dict[str, Any] | None = None,
111
+ stack_info: bool = False,
112
+ stacklevel: int = 1,
113
+ **context: Any,
114
+ ) -> None:
111
115
  if self.isEnabledFor(logging.ERROR):
112
116
  self._log(
113
117
  logging.ERROR,
@@ -122,14 +126,14 @@ class AppLogger(logging.Logger):
122
126
 
123
127
  def critical(
124
128
  self,
125
- msg,
126
- *args,
127
- exc_info=None,
128
- extra=None,
129
- stack_info=False,
130
- stacklevel=1,
131
- **context,
132
- ):
129
+ msg: object,
130
+ *args: object,
131
+ exc_info: Any = None,
132
+ extra: dict[str, Any] | None = None,
133
+ stack_info: bool = False,
134
+ stacklevel: int = 1,
135
+ **context: Any,
136
+ ) -> None:
133
137
  if self.isEnabledFor(logging.CRITICAL):
134
138
  self._log(
135
139
  logging.CRITICAL,
@@ -144,15 +148,15 @@ class AppLogger(logging.Logger):
144
148
 
145
149
  def _log(
146
150
  self,
147
- level,
148
- msg,
149
- args,
150
- exc_info=None,
151
- extra=None,
152
- stack_info=False,
153
- stacklevel=1,
154
- **context,
155
- ):
151
+ level: int,
152
+ msg: object,
153
+ args: tuple[object, ...],
154
+ exc_info: Any = None,
155
+ extra: dict[str, Any] | None = None,
156
+ stack_info: bool = False,
157
+ stacklevel: int = 1,
158
+ **context: Any,
159
+ ) -> None:
156
160
  """Low-level logging routine which creates a LogRecord and then calls all handlers."""
157
161
  # Check if extra already has a 'context' key
158
162
  if extra and "context" in extra: