plain 0.13.1__py3-none-any.whl → 0.13.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 (52) hide show
  1. plain/cli/README.md +2 -2
  2. plain/cli/cli.py +3 -3
  3. plain/cli/startup.py +2 -2
  4. plain/csrf/middleware.py +1 -0
  5. plain/exceptions.py +2 -1
  6. plain/forms/forms.py +5 -5
  7. plain/http/multipartparser.py +5 -5
  8. plain/http/request.py +5 -5
  9. plain/http/response.py +14 -14
  10. plain/internal/files/locks.py +1 -0
  11. plain/internal/files/move.py +1 -2
  12. plain/internal/files/uploadhandler.py +1 -0
  13. plain/internal/files/utils.py +3 -3
  14. plain/internal/handlers/base.py +3 -7
  15. plain/internal/handlers/exception.py +1 -3
  16. plain/internal/handlers/wsgi.py +1 -1
  17. plain/internal/middleware/slash.py +5 -8
  18. plain/packages/config.py +9 -15
  19. plain/packages/registry.py +12 -12
  20. plain/paginator.py +1 -2
  21. plain/preflight/messages.py +3 -10
  22. plain/preflight/registry.py +2 -2
  23. plain/preflight/urls.py +4 -4
  24. plain/runtime/global_settings.py +1 -0
  25. plain/signing.py +4 -4
  26. plain/test/client.py +14 -13
  27. plain/urls/base.py +1 -1
  28. plain/urls/conf.py +2 -1
  29. plain/urls/resolvers.py +20 -33
  30. plain/utils/cache.py +1 -0
  31. plain/utils/crypto.py +2 -1
  32. plain/utils/datastructures.py +2 -2
  33. plain/utils/dateformat.py +13 -12
  34. plain/utils/dateparse.py +1 -1
  35. plain/utils/decorators.py +1 -1
  36. plain/utils/html.py +7 -7
  37. plain/utils/http.py +8 -8
  38. plain/utils/ipv6.py +1 -1
  39. plain/utils/module_loading.py +2 -2
  40. plain/utils/regex_helper.py +7 -6
  41. plain/utils/termcolors.py +4 -4
  42. plain/utils/text.py +7 -7
  43. plain/utils/timesince.py +1 -1
  44. plain/utils/timezone.py +5 -5
  45. plain/validators.py +1 -3
  46. plain/views/forms.py +2 -2
  47. {plain-0.13.1.dist-info → plain-0.13.2.dist-info}/METADATA +7 -12
  48. {plain-0.13.1.dist-info → plain-0.13.2.dist-info}/RECORD +54 -54
  49. {plain-0.13.1.dist-info → plain-0.13.2.dist-info}/WHEEL +1 -1
  50. plain-0.13.2.dist-info/entry_points.txt +2 -0
  51. plain-0.13.1.dist-info/entry_points.txt +0 -3
  52. {plain-0.13.1.dist-info → plain-0.13.2.dist-info/licenses}/LICENSE +0 -0
plain/cli/README.md CHANGED
@@ -108,11 +108,11 @@ Some packages, like [plain-dev](https://plainframework.com/docs/plain-dev/),
108
108
  never show up in `INSTALLED_PACKAGES` but still have CLI commands.
109
109
  These are detected via Python entry points.
110
110
 
111
- An example with `pyproject.toml` and Poetry:
111
+ An example with `pyproject.toml` and UV:
112
112
 
113
113
  ```toml
114
114
  # pyproject.toml
115
- [tool.poetry.plugins."plain.cli"]
115
+ [project.entry-points."plain.cli"]
116
116
  "dev" = "plain.dev:cli"
117
117
  "pre-commit" = "plain.dev.precommit:cli"
118
118
  "contrib" = "plain.dev.contribute:cli"
plain/cli/cli.py CHANGED
@@ -232,10 +232,10 @@ def preflight_checks(package_label, deploy, fail_level, databases):
232
232
  if visible_issue_count == 0
233
233
  else "1 issue"
234
234
  if visible_issue_count == 1
235
- else "%s issues" % visible_issue_count,
235
+ else f"{visible_issue_count} issues",
236
236
  len(all_issues) - visible_issue_count,
237
237
  )
238
- msg = click.style("SystemCheckError: %s" % header, fg="red") + body + footer
238
+ msg = click.style(f"SystemCheckError: {header}", fg="red") + body + footer
239
239
  raise click.ClickException(msg)
240
240
  else:
241
241
  if visible_issue_count:
@@ -245,7 +245,7 @@ def preflight_checks(package_label, deploy, fail_level, databases):
245
245
  if visible_issue_count == 0
246
246
  else "1 issue"
247
247
  if visible_issue_count == 1
248
- else "%s issues" % visible_issue_count,
248
+ else f"{visible_issue_count} issues",
249
249
  len(all_issues) - visible_issue_count,
250
250
  )
251
251
  msg = header + body + footer
plain/cli/startup.py CHANGED
@@ -10,9 +10,9 @@ def _print_bold(s):
10
10
 
11
11
 
12
12
  def _print_italic(s):
13
- print("\x1B[3m", end="")
13
+ print("\x1b[3m", end="")
14
14
  print(s)
15
- print("\x1B[0m", end="")
15
+ print("\x1b[0m", end="")
16
16
 
17
17
 
18
18
  _print_bold("\n⬣ Welcome to the Plain shell! ⬣")
plain/csrf/middleware.py CHANGED
@@ -4,6 +4,7 @@ Cross Site Request Forgery Middleware.
4
4
  This module provides a middleware that implements protection
5
5
  against request forgeries from other sites.
6
6
  """
7
+
7
8
  import logging
8
9
  import string
9
10
  from collections import defaultdict
plain/exceptions.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Global Plain exception and warning classes.
3
3
  """
4
+
4
5
  import operator
5
6
 
6
7
  from plain.utils.hashable import make_hashable
@@ -209,7 +210,7 @@ class ValidationError(Exception):
209
210
  return repr(list(self))
210
211
 
211
212
  def __repr__(self):
212
- return "ValidationError(%s)" % self
213
+ return f"ValidationError({self})"
213
214
 
214
215
  def __eq__(self, other):
215
216
  if not isinstance(other, ValidationError):
plain/forms/forms.py CHANGED
@@ -172,7 +172,7 @@ class BaseForm:
172
172
  if not isinstance(error, ValidationError):
173
173
  raise TypeError(
174
174
  "The argument `error` must be an instance of "
175
- "`ValidationError`, not `%s`." % type(error).__name__
175
+ f"`ValidationError`, not `{type(error).__name__}`."
176
176
  )
177
177
 
178
178
  if hasattr(error, "error_dict"):
@@ -221,9 +221,9 @@ class BaseForm:
221
221
  self._post_clean()
222
222
 
223
223
  def _field_data_value(self, field, html_name):
224
- if hasattr(self, "parse_%s" % html_name):
224
+ if hasattr(self, f"parse_{html_name}"):
225
225
  # Allow custom parsing from form data/files at the form level
226
- return getattr(self, "parse_%s" % html_name)()
226
+ return getattr(self, f"parse_{html_name}")()
227
227
 
228
228
  return field.value_from_form_data(self.data, self.files, html_name)
229
229
 
@@ -242,8 +242,8 @@ class BaseForm:
242
242
  else:
243
243
  value = field.clean(value)
244
244
  self.cleaned_data[name] = value
245
- if hasattr(self, "clean_%s" % name):
246
- value = getattr(self, "clean_%s" % name)()
245
+ if hasattr(self, f"clean_{name}"):
246
+ value = getattr(self, f"clean_{name}")()
247
247
  self.cleaned_data[name] = value
248
248
  except ValidationError as e:
249
249
  self.add_error(name, e)
@@ -4,6 +4,7 @@ Multi-part parsing for file uploads.
4
4
  Exposes one class, ``MultiPartParser``, which feeds chunks of uploaded data to
5
5
  file upload handlers for processing.
6
6
  """
7
+
7
8
  import base64
8
9
  import binascii
9
10
  import collections
@@ -70,14 +71,13 @@ class MultiPartParser:
70
71
  # Content-Type should contain multipart and the boundary information.
71
72
  content_type = META.get("CONTENT_TYPE", "")
72
73
  if not content_type.startswith("multipart/"):
73
- raise MultiPartParserError("Invalid Content-Type: %s" % content_type)
74
+ raise MultiPartParserError(f"Invalid Content-Type: {content_type}")
74
75
 
75
76
  try:
76
77
  content_type.encode("ascii")
77
78
  except UnicodeEncodeError:
78
79
  raise MultiPartParserError(
79
- "Invalid non-ASCII Content-Type in multipart: %s"
80
- % force_str(content_type)
80
+ f"Invalid non-ASCII Content-Type in multipart: {force_str(content_type)}"
81
81
  )
82
82
 
83
83
  # Parse the header to get the boundary to split the parts.
@@ -85,7 +85,7 @@ class MultiPartParser:
85
85
  boundary = opts.get("boundary")
86
86
  if not boundary or not self.boundary_re.fullmatch(boundary):
87
87
  raise MultiPartParserError(
88
- "Invalid boundary in multipart: %s" % force_str(boundary)
88
+ f"Invalid boundary in multipart: {force_str(boundary)}"
89
89
  )
90
90
 
91
91
  # Content-Length should contain the length of the body we are about
@@ -97,7 +97,7 @@ class MultiPartParser:
97
97
 
98
98
  if content_length < 0:
99
99
  # This means we shouldn't continue...raise an error.
100
- raise MultiPartParserError("Invalid content length: %r" % content_length)
100
+ raise MultiPartParserError(f"Invalid content length: {content_length!r}")
101
101
 
102
102
  self._boundary = boundary.encode("ascii")
103
103
  self._input_data = input_data
plain/http/request.py CHANGED
@@ -81,7 +81,7 @@ class HttpRequest:
81
81
 
82
82
  def __repr__(self):
83
83
  if self.method is None or not self.get_full_path():
84
- return "<%s>" % self.__class__.__name__
84
+ return f"<{self.__class__.__name__}>"
85
85
  return f"<{self.__class__.__name__}: {self.method} {self.get_full_path()!r}>"
86
86
 
87
87
  def __getstate__(self):
@@ -157,9 +157,9 @@ class HttpRequest:
157
157
  if domain and validate_host(domain, allowed_hosts):
158
158
  return host
159
159
  else:
160
- msg = "Invalid HTTP_HOST header: %r." % host
160
+ msg = f"Invalid HTTP_HOST header: {host!r}."
161
161
  if domain:
162
- msg += " You may need to add %r to ALLOWED_HOSTS." % domain
162
+ msg += f" You may need to add {domain!r} to ALLOWED_HOSTS."
163
163
  else:
164
164
  msg += (
165
165
  " The domain name provided is not valid according to RFC 1034/1035."
@@ -227,7 +227,7 @@ class HttpRequest:
227
227
  if location is None:
228
228
  # Make it an absolute url (but schemeless and domainless) for the
229
229
  # edge case that the path starts with '//'.
230
- location = "//%s" % self.get_full_path()
230
+ location = f"//{self.get_full_path()}"
231
231
  else:
232
232
  # Coerce lazy locations.
233
233
  location = str(location)
@@ -671,7 +671,7 @@ class MediaType:
671
671
  params_str = "".join(f"; {k}={v}" for k, v in self.params.items())
672
672
  return "{}{}{}".format(
673
673
  self.main_type,
674
- ("/%s" % self.sub_type) if self.sub_type else "",
674
+ (f"/{self.sub_type}") if self.sub_type else "",
675
675
  params_str,
676
676
  )
677
677
 
plain/http/response.py CHANGED
@@ -72,7 +72,7 @@ class ResponseHeaders(CaseInsensitiveMapping):
72
72
  if mime_encode:
73
73
  value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
74
74
  else:
75
- e.reason += ", HTTP response headers must be in %s format" % charset
75
+ e.reason += f", HTTP response headers must be in {charset} format"
76
76
  raise
77
77
  return value
78
78
 
@@ -181,7 +181,7 @@ class ResponseBase:
181
181
  @property
182
182
  def _content_type_for_repr(self):
183
183
  return (
184
- ', "%s"' % self.headers["Content-Type"]
184
+ ', "{}"'.format(self.headers["Content-Type"])
185
185
  if "Content-Type" in self.headers
186
186
  else ""
187
187
  )
@@ -236,8 +236,8 @@ class ResponseBase:
236
236
  if expires is not None:
237
237
  if isinstance(expires, datetime.datetime):
238
238
  if timezone.is_naive(expires):
239
- expires = timezone.make_aware(expires, datetime.timezone.utc)
240
- delta = expires - datetime.datetime.now(tz=datetime.timezone.utc)
239
+ expires = timezone.make_aware(expires, datetime.UTC)
240
+ delta = expires - datetime.datetime.now(tz=datetime.UTC)
241
241
  # Add one second so the date matches exactly (a fraction of
242
242
  # time gets lost between converting to a timedelta and
243
243
  # then the date string).
@@ -332,14 +332,14 @@ class ResponseBase:
332
332
  signals.request_finished.send(sender=self._handler_class)
333
333
 
334
334
  def write(self, content):
335
- raise OSError("This %s instance is not writable" % self.__class__.__name__)
335
+ raise OSError(f"This {self.__class__.__name__} instance is not writable")
336
336
 
337
337
  def flush(self):
338
338
  pass
339
339
 
340
340
  def tell(self):
341
341
  raise OSError(
342
- "This %s instance cannot tell its position" % self.__class__.__name__
342
+ f"This {self.__class__.__name__} instance cannot tell its position"
343
343
  )
344
344
 
345
345
  # These methods partially implement a stream-like object interface.
@@ -355,7 +355,7 @@ class ResponseBase:
355
355
  return False
356
356
 
357
357
  def writelines(self, lines):
358
- raise OSError("This %s instance is not writable" % self.__class__.__name__)
358
+ raise OSError(f"This {self.__class__.__name__} instance is not writable")
359
359
 
360
360
 
361
361
  class Response(ResponseBase):
@@ -390,7 +390,7 @@ class Response(ResponseBase):
390
390
  return obj_dict
391
391
 
392
392
  def __repr__(self):
393
- return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
393
+ return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
394
394
  "cls": self.__class__.__name__,
395
395
  "status_code": self.status_code,
396
396
  "content_type": self._content_type_for_repr,
@@ -461,7 +461,7 @@ class StreamingResponse(ResponseBase):
461
461
  self.streaming_content = streaming_content
462
462
 
463
463
  def __repr__(self):
464
- return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
464
+ return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
465
465
  "cls": self.__class__.__qualname__,
466
466
  "status_code": self.status_code,
467
467
  "content_type": self._content_type_for_repr,
@@ -470,8 +470,8 @@ class StreamingResponse(ResponseBase):
470
470
  @property
471
471
  def content(self):
472
472
  raise AttributeError(
473
- "This %s instance has no `content` attribute. Use "
474
- "`streaming_content` instead." % self.__class__.__name__
473
+ f"This {self.__class__.__name__} instance has no `content` attribute. Use "
474
+ "`streaming_content` instead."
475
475
  )
476
476
 
477
477
  @property
@@ -586,14 +586,14 @@ class ResponseRedirectBase(Response):
586
586
  parsed = urlparse(str(redirect_to))
587
587
  if parsed.scheme and parsed.scheme not in self.allowed_schemes:
588
588
  raise DisallowedRedirect(
589
- "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
589
+ f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
590
590
  )
591
591
 
592
592
  url = property(lambda self: self["Location"])
593
593
 
594
594
  def __repr__(self):
595
595
  return (
596
- '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
596
+ '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">' # noqa: UP031
597
597
  % {
598
598
  "cls": self.__class__.__name__,
599
599
  "status_code": self.status_code,
@@ -661,7 +661,7 @@ class ResponseNotAllowed(Response):
661
661
  self["Allow"] = ", ".join(permitted_methods)
662
662
 
663
663
  def __repr__(self):
664
- return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
664
+ return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
665
665
  "cls": self.__class__.__name__,
666
666
  "status_code": self.status_code,
667
667
  "content_type": self._content_type_for_repr,
@@ -16,6 +16,7 @@ Example Usage::
16
16
  ... locks.lock(f, locks.LOCK_EX)
17
17
  ... f.write('Plain')
18
18
  """
19
+
19
20
  import os
20
21
 
21
22
  __all__ = ("LOCK_EX", "LOCK_SH", "LOCK_NB", "lock", "unlock")
@@ -46,8 +46,7 @@ def file_move_safe(
46
46
  try:
47
47
  if not allow_overwrite and os.access(new_file_name, os.F_OK):
48
48
  raise FileExistsError(
49
- "Destination file %s exists and allow_overwrite is False."
50
- % new_file_name
49
+ f"Destination file {new_file_name} exists and allow_overwrite is False."
51
50
  )
52
51
 
53
52
  os.rename(old_file_name, new_file_name)
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Base file upload handler classes, and the built-in concrete subclasses
3
3
  """
4
+
4
5
  import os
5
6
  from io import BytesIO
6
7
 
@@ -7,7 +7,7 @@ from plain.exceptions import SuspiciousFileOperation
7
7
  def validate_file_name(name, allow_relative_path=False):
8
8
  # Remove potentially dangerous names
9
9
  if os.path.basename(name) in {"", ".", ".."}:
10
- raise SuspiciousFileOperation("Could not derive file name from '%s'" % name)
10
+ raise SuspiciousFileOperation(f"Could not derive file name from '{name}'")
11
11
 
12
12
  if allow_relative_path:
13
13
  # Use PurePosixPath() because this branch is checked only in
@@ -16,10 +16,10 @@ def validate_file_name(name, allow_relative_path=False):
16
16
  path = pathlib.PurePosixPath(name)
17
17
  if path.is_absolute() or ".." in path.parts:
18
18
  raise SuspiciousFileOperation(
19
- "Detected path traversal attempt in '%s'" % name
19
+ f"Detected path traversal attempt in '{name}'"
20
20
  )
21
21
  elif name != os.path.basename(name):
22
- raise SuspiciousFileOperation("File name '%s' includes path elements" % name)
22
+ raise SuspiciousFileOperation(f"File name '{name}' includes path elements")
23
23
 
24
24
  return name
25
25
 
@@ -45,7 +45,7 @@ class BaseHandler:
45
45
 
46
46
  if mw_instance is None:
47
47
  raise ImproperlyConfigured(
48
- "Middleware factory %s returned None." % middleware_path
48
+ f"Middleware factory {middleware_path} returned None."
49
49
  )
50
50
 
51
51
  if hasattr(mw_instance, "process_view"):
@@ -126,14 +126,10 @@ class BaseHandler:
126
126
  if isinstance(callback, types.FunctionType): # FBV
127
127
  name = f"The view {callback.__module__}.{callback.__name__}"
128
128
  else: # CBV
129
- name = "The view {}.{}.__call__".format(
130
- callback.__module__,
131
- callback.__class__.__name__,
132
- )
129
+ name = f"The view {callback.__module__}.{callback.__class__.__name__}.__call__"
133
130
  if response is None:
134
131
  raise ValueError(
135
- "%s didn't return a Response object. It returned None "
136
- "instead." % name
132
+ f"{name} didn't return a Response object. It returned None " "instead."
137
133
  )
138
134
 
139
135
 
@@ -85,9 +85,7 @@ def response_for_exception(request, exc):
85
85
 
86
86
  # The request logger receives events for any problematic request
87
87
  # The security logger receives events for all SuspiciousOperations
88
- security_logger = logging.getLogger(
89
- "plain.security.%s" % exc.__class__.__name__
90
- )
88
+ security_logger = logging.getLogger(f"plain.security.{exc.__class__.__name__}")
91
89
  security_logger.error(
92
90
  str(exc),
93
91
  exc_info=exc,
@@ -138,7 +138,7 @@ class WSGIHandler(base.BaseHandler):
138
138
 
139
139
  response._handler_class = self.__class__
140
140
 
141
- status = "%d %s" % (response.status_code, response.reason_phrase)
141
+ status = "%d %s" % (response.status_code, response.reason_phrase) # noqa: UP031
142
142
  response_headers = [
143
143
  *response.items(),
144
144
  *(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
@@ -34,7 +34,7 @@ class RedirectSlashMiddleware:
34
34
  if settings.APPEND_SLASH and not request.path_info.endswith("/"):
35
35
  urlconf = getattr(request, "urlconf", None)
36
36
  if not is_valid_path(request.path_info, urlconf):
37
- match = is_valid_path("%s/" % request.path_info, urlconf)
37
+ match = is_valid_path(f"{request.path_info}/", urlconf)
38
38
  if match:
39
39
  view = match.func
40
40
  return getattr(view, "should_append_slash", True)
@@ -52,13 +52,10 @@ class RedirectSlashMiddleware:
52
52
  new_path = escape_leading_slashes(new_path)
53
53
  if settings.DEBUG and request.method in ("POST", "PUT", "PATCH"):
54
54
  raise RuntimeError(
55
- "You called this URL via {method}, but the URL doesn't end "
55
+ f"You called this URL via {request.method}, but the URL doesn't end "
56
56
  "in a slash and you have APPEND_SLASH set. Plain can't "
57
- "redirect to the slash URL while maintaining {method} data. "
58
- "Change your form to point to {url} (note the trailing "
59
- "slash), or set APPEND_SLASH=False in your Plain settings.".format(
60
- method=request.method,
61
- url=request.get_host() + new_path,
62
- )
57
+ f"redirect to the slash URL while maintaining {request.method} data. "
58
+ f"Change your form to point to {request.get_host() + new_path} (note the trailing "
59
+ "slash), or set APPEND_SLASH=False in your Plain settings."
63
60
  )
64
61
  return new_path
plain/packages/config.py CHANGED
@@ -35,7 +35,7 @@ class PackageConfig:
35
35
  self.label = package_name.rpartition(".")[2]
36
36
  if not self.label.isidentifier():
37
37
  raise ImproperlyConfigured(
38
- "The app label '%s' is not a valid Python identifier." % self.label
38
+ f"The app label '{self.label}' is not a valid Python identifier."
39
39
  )
40
40
 
41
41
  # Filesystem path to the application directory e.g.
@@ -71,15 +71,15 @@ class PackageConfig:
71
71
  paths = list(set(paths))
72
72
  if len(paths) > 1:
73
73
  raise ImproperlyConfigured(
74
- "The app module {!r} has multiple filesystem locations ({!r}); "
74
+ f"The app module {module!r} has multiple filesystem locations ({paths!r}); "
75
75
  "you must configure this app with an PackageConfig subclass "
76
- "with a 'path' class attribute.".format(module, paths)
76
+ "with a 'path' class attribute."
77
77
  )
78
78
  elif not paths:
79
79
  raise ImproperlyConfigured(
80
- "The app module %r has no filesystem location, "
80
+ f"The app module {module!r} has no filesystem location, "
81
81
  "you must configure this app with an PackageConfig subclass "
82
- "with a 'path' class attribute." % module
82
+ "with a 'path' class attribute."
83
83
  )
84
84
  return paths[0]
85
85
 
@@ -170,7 +170,7 @@ class PackageConfig:
170
170
  ]
171
171
  msg = f"Module '{mod_path}' does not contain a '{cls_name}' class."
172
172
  if candidates:
173
- msg += " Choices are: %s." % ", ".join(candidates)
173
+ msg += " Choices are: {}.".format(", ".join(candidates))
174
174
  raise ImportError(msg)
175
175
  else:
176
176
  # Re-trigger the module import exception.
@@ -179,9 +179,7 @@ class PackageConfig:
179
179
  # Check for obvious errors. (This check prevents duck typing, but
180
180
  # it could be removed if it became a problem in practice.)
181
181
  if not issubclass(package_config_class, PackageConfig):
182
- raise ImproperlyConfigured(
183
- "'%s' isn't a subclass of PackageConfig." % entry
184
- )
182
+ raise ImproperlyConfigured(f"'{entry}' isn't a subclass of PackageConfig.")
185
183
 
186
184
  # Obtain package name here rather than in PackageClass.__init__ to keep
187
185
  # all error checking for entries in INSTALLED_PACKAGES in one place.
@@ -189,18 +187,14 @@ class PackageConfig:
189
187
  try:
190
188
  package_name = package_config_class.name
191
189
  except AttributeError:
192
- raise ImproperlyConfigured("'%s' must supply a name attribute." % entry)
190
+ raise ImproperlyConfigured(f"'{entry}' must supply a name attribute.")
193
191
 
194
192
  # Ensure package_name points to a valid module.
195
193
  try:
196
194
  package_module = import_module(package_name)
197
195
  except ImportError:
198
196
  raise ImproperlyConfigured(
199
- "Cannot import '{}'. Check that '{}.{}.name' is correct.".format(
200
- package_name,
201
- package_config_class.__module__,
202
- package_config_class.__qualname__,
203
- )
197
+ f"Cannot import '{package_name}'. Check that '{package_config_class.__module__}.{package_config_class.__qualname__}.name' is correct."
204
198
  )
205
199
 
206
200
  # Entry is a path to an app config class.
@@ -90,7 +90,7 @@ class Packages:
90
90
  if package_config.label in self.package_configs:
91
91
  raise ImproperlyConfigured(
92
92
  "Package labels aren't unique, "
93
- "duplicates: %s" % package_config.label
93
+ f"duplicates: {package_config.label}"
94
94
  )
95
95
 
96
96
  self.package_configs[package_config.label] = package_config
@@ -103,8 +103,9 @@ class Packages:
103
103
  duplicates = [name for name, count in counts.most_common() if count > 1]
104
104
  if duplicates:
105
105
  raise ImproperlyConfigured(
106
- "Package names aren't unique, "
107
- "duplicates: %s" % ", ".join(duplicates)
106
+ "Package names aren't unique, " "duplicates: {}".format(
107
+ ", ".join(duplicates)
108
+ )
108
109
  )
109
110
 
110
111
  self.packages_ready = True
@@ -154,10 +155,10 @@ class Packages:
154
155
  try:
155
156
  return self.package_configs[package_label]
156
157
  except KeyError:
157
- message = "No installed app with label '%s'." % package_label
158
+ message = f"No installed app with label '{package_label}'."
158
159
  for package_config in self.get_package_configs():
159
160
  if package_config.name == package_label:
160
- message += " Did you mean '%s'?" % package_config.label
161
+ message += f" Did you mean '{package_config.label}'?"
161
162
  break
162
163
  raise LookupError(message)
163
164
 
@@ -223,17 +224,15 @@ class Packages:
223
224
  and model.__module__ == app_models[model_name].__module__
224
225
  ):
225
226
  warnings.warn(
226
- "Model '{}.{}' was already registered. Reloading models is not "
227
+ f"Model '{package_label}.{model_name}' was already registered. Reloading models is not "
227
228
  "advised as it can lead to inconsistencies, most notably with "
228
- "related models.".format(package_label, model_name),
229
+ "related models.",
229
230
  RuntimeWarning,
230
231
  stacklevel=2,
231
232
  )
232
233
  else:
233
234
  raise RuntimeError(
234
- "Conflicting '{}' models in application '{}': {} and {}.".format(
235
- model_name, package_label, app_models[model_name], model
236
- )
235
+ f"Conflicting '{model_name}' models in application '{package_label}': {app_models[model_name]} and {model}."
237
236
  )
238
237
  app_models[model_name] = model
239
238
  self.do_pending_operations(model)
@@ -321,8 +320,9 @@ class Packages:
321
320
  }
322
321
  if not available.issubset(installed):
323
322
  raise ValueError(
324
- "Available packages isn't a subset of installed packages, extra packages: %s"
325
- % ", ".join(available - installed)
323
+ "Available packages isn't a subset of installed packages, extra packages: {}".format(
324
+ ", ".join(available - installed)
325
+ )
326
326
  )
327
327
 
328
328
  self.stored_package_configs.append(self.package_configs)
plain/paginator.py CHANGED
@@ -142,8 +142,7 @@ class Page(collections.abc.Sequence):
142
142
  def __getitem__(self, index):
143
143
  if not isinstance(index, int | slice):
144
144
  raise TypeError(
145
- "Page indices must be integers or slices, not %s."
146
- % type(index).__name__
145
+ f"Page indices must be integers or slices, not {type(index).__name__}."
147
146
  )
148
147
  # The object_list is converted to a list so that if it was a QuerySet
149
148
  # it won't be a database hit per __getitem__.
@@ -40,19 +40,12 @@ class CheckMessage:
40
40
  obj = self.obj._meta.label
41
41
  else:
42
42
  obj = str(self.obj)
43
- id = "(%s) " % self.id if self.id else ""
44
- hint = "\n\tHINT: %s" % self.hint if self.hint else ""
43
+ id = f"({self.id}) " if self.id else ""
44
+ hint = f"\n\tHINT: {self.hint}" if self.hint else ""
45
45
  return f"{obj}: {id}{self.msg}{hint}"
46
46
 
47
47
  def __repr__(self):
48
- return "<{}: level={!r}, msg={!r}, hint={!r}, obj={!r}, id={!r}>".format(
49
- self.__class__.__name__,
50
- self.level,
51
- self.msg,
52
- self.hint,
53
- self.obj,
54
- self.id,
55
- )
48
+ return f"<{self.__class__.__name__}: level={self.level!r}, msg={self.msg!r}, hint={self.hint!r}, obj={self.obj!r}, id={self.id!r}>"
56
49
 
57
50
  def is_serious(self, level=ERROR):
58
51
  return self.level >= level
@@ -54,8 +54,8 @@ class CheckRegistry:
54
54
  new_errors = check(package_configs=package_configs, databases=databases)
55
55
  if not is_iterable(new_errors):
56
56
  raise TypeError(
57
- "The function %r did not return a list. All functions "
58
- "registered with the checks registry must return a list." % check,
57
+ f"The function {check!r} did not return a list. All functions "
58
+ "registered with the checks registry must return a list.",
59
59
  )
60
60
  errors.extend(new_errors)
61
61
  return errors
plain/preflight/urls.py CHANGED
@@ -46,8 +46,8 @@ def check_url_namespaces_unique(package_configs, **kwargs):
46
46
  for namespace in non_unique_namespaces:
47
47
  errors.append(
48
48
  Warning(
49
- "URL namespace '{}' isn't unique. You may not be able to reverse "
50
- "all URLs in this namespace".format(namespace),
49
+ f"URL namespace '{namespace}' isn't unique. You may not be able to reverse "
50
+ "all URLs in this namespace",
51
51
  id="urls.W005",
52
52
  )
53
53
  )
@@ -92,8 +92,8 @@ def get_warning_for_invalid_pattern(pattern):
92
92
 
93
93
  return [
94
94
  Error(
95
- "Your URL pattern {!r} is invalid. Ensure that urlpatterns is a list "
96
- "of path() and/or re_path() instances.".format(pattern),
95
+ f"Your URL pattern {pattern!r} is invalid. Ensure that urlpatterns is a list "
96
+ "of path() and/or re_path() instances.",
97
97
  hint=hint,
98
98
  id="urls.E004",
99
99
  )
@@ -2,6 +2,7 @@
2
2
  Default Plain settings. Override these with settings in the module pointed to
3
3
  by the PLAIN_SETTINGS_MODULE environment variable.
4
4
  """
5
+
5
6
  from pathlib import Path
6
7
 
7
8
  from plain.runtime import APP_PATH as default_app_path