plain 0.13.0__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.
- plain/cli/README.md +2 -2
- plain/cli/cli.py +8 -17
- plain/cli/startup.py +2 -2
- plain/csrf/middleware.py +1 -0
- plain/exceptions.py +2 -1
- plain/forms/forms.py +5 -5
- plain/http/multipartparser.py +5 -5
- plain/http/request.py +5 -5
- plain/http/response.py +14 -14
- plain/internal/files/locks.py +1 -0
- plain/internal/files/move.py +1 -2
- plain/internal/files/uploadhandler.py +1 -0
- plain/internal/files/utils.py +3 -3
- plain/internal/handlers/base.py +3 -7
- plain/internal/handlers/exception.py +1 -3
- plain/internal/handlers/wsgi.py +1 -1
- plain/internal/middleware/slash.py +5 -8
- plain/packages/config.py +9 -15
- plain/packages/registry.py +12 -12
- plain/paginator.py +1 -2
- plain/preflight/messages.py +3 -10
- plain/preflight/registry.py +2 -2
- plain/preflight/urls.py +4 -4
- plain/runtime/global_settings.py +1 -0
- plain/signing.py +4 -4
- plain/test/client.py +14 -13
- plain/urls/base.py +1 -1
- plain/urls/conf.py +2 -1
- plain/urls/resolvers.py +20 -33
- plain/utils/cache.py +1 -0
- plain/utils/crypto.py +2 -1
- plain/utils/datastructures.py +2 -2
- plain/utils/dateformat.py +13 -12
- plain/utils/dateparse.py +1 -1
- plain/utils/decorators.py +1 -1
- plain/utils/html.py +7 -7
- plain/utils/http.py +8 -8
- plain/utils/ipv6.py +1 -1
- plain/utils/module_loading.py +2 -2
- plain/utils/regex_helper.py +7 -6
- plain/utils/termcolors.py +4 -4
- plain/utils/text.py +7 -7
- plain/utils/timesince.py +1 -1
- plain/utils/timezone.py +5 -5
- plain/validators.py +1 -3
- plain/views/forms.py +2 -2
- {plain-0.13.0.dist-info → plain-0.13.2.dist-info}/METADATA +7 -12
- {plain-0.13.0.dist-info → plain-0.13.2.dist-info}/RECORD +54 -54
- {plain-0.13.0.dist-info → plain-0.13.2.dist-info}/WHEEL +1 -1
- plain-0.13.2.dist-info/entry_points.txt +2 -0
- plain-0.13.0.dist-info/entry_points.txt +0 -3
- {plain-0.13.0.dist-info → plain-0.13.2.dist-info/licenses}/LICENSE +0 -0
plain/preflight/registry.py
CHANGED
@@ -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
|
58
|
-
"registered with the checks registry must return a list."
|
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"
|
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."
|
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
|
)
|
plain/runtime/global_settings.py
CHANGED
plain/signing.py
CHANGED
@@ -196,8 +196,8 @@ class Signer:
|
|
196
196
|
|
197
197
|
if _SEP_UNSAFE.match(self.sep):
|
198
198
|
raise ValueError(
|
199
|
-
"Unsafe Signer separator:
|
200
|
-
"only A-z0-9-_=)"
|
199
|
+
f"Unsafe Signer separator: {sep!r} (cannot be empty or consist of "
|
200
|
+
"only A-z0-9-_=)",
|
201
201
|
)
|
202
202
|
|
203
203
|
def signature(self, value, key=None):
|
@@ -209,12 +209,12 @@ class Signer:
|
|
209
209
|
|
210
210
|
def unsign(self, signed_value):
|
211
211
|
if self.sep not in signed_value:
|
212
|
-
raise BadSignature('No "
|
212
|
+
raise BadSignature(f'No "{self.sep}" found in value')
|
213
213
|
value, sig = signed_value.rsplit(self.sep, 1)
|
214
214
|
for key in [self.key, *self.fallback_keys]:
|
215
215
|
if constant_time_compare(sig, self.signature(value, key)):
|
216
216
|
return value
|
217
|
-
raise BadSignature('Signature "
|
217
|
+
raise BadSignature(f'Signature "{sig}" does not match')
|
218
218
|
|
219
219
|
def sign_object(self, obj, serializer=JSONSerializer, compress=False):
|
220
220
|
"""
|
plain/test/client.py
CHANGED
@@ -33,7 +33,7 @@ __all__ = (
|
|
33
33
|
|
34
34
|
|
35
35
|
BOUNDARY = "BoUnDaRyStRiNg"
|
36
|
-
MULTIPART_CONTENT = "multipart/form-data; boundary
|
36
|
+
MULTIPART_CONTENT = f"multipart/form-data; boundary={BOUNDARY}"
|
37
37
|
CONTENT_TYPE_RE = _lazy_re_compile(r".*; charset=([\w-]+);?")
|
38
38
|
# Structured suffix spec: https://tools.ietf.org/html/rfc6838#section-4.2.8
|
39
39
|
JSON_CONTENT_TYPE_RE = _lazy_re_compile(r"^application\/(.+\+)?json")
|
@@ -218,8 +218,8 @@ def encode_multipart(boundary, data):
|
|
218
218
|
for key, value in data.items():
|
219
219
|
if value is None:
|
220
220
|
raise TypeError(
|
221
|
-
"Cannot encode None for key '
|
222
|
-
"to pass an empty string or omit the value?"
|
221
|
+
f"Cannot encode None for key '{key}' as POST data. Did you mean "
|
222
|
+
"to pass an empty string or omit the value?"
|
223
223
|
)
|
224
224
|
elif is_file(value):
|
225
225
|
lines.extend(encode_file(boundary, key, value))
|
@@ -231,8 +231,8 @@ def encode_multipart(boundary, data):
|
|
231
231
|
lines.extend(
|
232
232
|
to_bytes(val)
|
233
233
|
for val in [
|
234
|
-
"
|
235
|
-
'Content-Disposition: form-data; name="
|
234
|
+
f"--{boundary}",
|
235
|
+
f'Content-Disposition: form-data; name="{key}"',
|
236
236
|
"",
|
237
237
|
item,
|
238
238
|
]
|
@@ -241,8 +241,8 @@ def encode_multipart(boundary, data):
|
|
241
241
|
lines.extend(
|
242
242
|
to_bytes(val)
|
243
243
|
for val in [
|
244
|
-
"
|
245
|
-
'Content-Disposition: form-data; name="
|
244
|
+
f"--{boundary}",
|
245
|
+
f'Content-Disposition: form-data; name="{key}"',
|
246
246
|
"",
|
247
247
|
value,
|
248
248
|
]
|
@@ -250,7 +250,7 @@ def encode_multipart(boundary, data):
|
|
250
250
|
|
251
251
|
lines.extend(
|
252
252
|
[
|
253
|
-
to_bytes("
|
253
|
+
to_bytes(f"--{boundary}--"),
|
254
254
|
b"",
|
255
255
|
]
|
256
256
|
)
|
@@ -277,11 +277,11 @@ def encode_file(boundary, key, file):
|
|
277
277
|
content_type = "application/octet-stream"
|
278
278
|
filename = filename or key
|
279
279
|
return [
|
280
|
-
to_bytes("
|
280
|
+
to_bytes(f"--{boundary}"),
|
281
281
|
to_bytes(
|
282
282
|
f'Content-Disposition: form-data; name="{key}"; filename="{filename}"'
|
283
283
|
),
|
284
|
-
to_bytes("Content-Type:
|
284
|
+
to_bytes(f"Content-Type: {content_type}"),
|
285
285
|
b"",
|
286
286
|
to_bytes(file.read()),
|
287
287
|
]
|
@@ -617,8 +617,9 @@ class ClientMixin:
|
|
617
617
|
if not hasattr(response, "_json"):
|
618
618
|
if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")):
|
619
619
|
raise ValueError(
|
620
|
-
'Content-Type header is "
|
621
|
-
|
620
|
+
'Content-Type header is "{}", not "application/json"'.format(
|
621
|
+
response.get("Content-Type")
|
622
|
+
)
|
622
623
|
)
|
623
624
|
response._json = json.loads(
|
624
625
|
response.content.decode(response.charset), **extra
|
@@ -670,7 +671,7 @@ class Client(ClientMixin, RequestFactory):
|
|
670
671
|
environ = self._base_environ(**request)
|
671
672
|
|
672
673
|
# Capture exceptions created by the handler.
|
673
|
-
exception_uid = "request-exception
|
674
|
+
exception_uid = f"request-exception-{id(request)}"
|
674
675
|
got_request_exception.connect(self.store_exc_info, dispatch_uid=exception_uid)
|
675
676
|
try:
|
676
677
|
response = self.handler(environ)
|
plain/urls/base.py
CHANGED
@@ -69,7 +69,7 @@ def reverse(viewname, urlconf=None, args=None, kwargs=None, using_namespace=None
|
|
69
69
|
)
|
70
70
|
)
|
71
71
|
else:
|
72
|
-
raise NoReverseMatch("
|
72
|
+
raise NoReverseMatch(f"{key} is not a registered namespace")
|
73
73
|
if ns_pattern:
|
74
74
|
resolver = get_ns_resolver(
|
75
75
|
ns_pattern, resolver, tuple(ns_converters.items())
|
plain/urls/conf.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Functions for use in URLsconfs."""
|
2
|
+
|
2
3
|
from functools import partial
|
3
4
|
|
4
5
|
from plain.exceptions import ImproperlyConfigured
|
@@ -24,7 +25,7 @@ def include(arg, namespace=None):
|
|
24
25
|
"provides a namespace."
|
25
26
|
)
|
26
27
|
raise ImproperlyConfigured(
|
27
|
-
"Passing a %d-tuple to include() is not supported. Pass a "
|
28
|
+
"Passing a %d-tuple to include() is not supported. Pass a " # noqa: UP031
|
28
29
|
"2-tuple containing the list of patterns and default_namespace, and "
|
29
30
|
"provide the namespace argument to include() instead." % len(arg)
|
30
31
|
)
|
plain/urls/resolvers.py
CHANGED
@@ -5,6 +5,7 @@ URLResolver is the main class here. Its resolve() method takes a URL (as
|
|
5
5
|
a string) and returns a ResolverMatch object which provides access to all
|
6
6
|
attributes of the resolved URL match.
|
7
7
|
"""
|
8
|
+
|
8
9
|
import functools
|
9
10
|
import inspect
|
10
11
|
import re
|
@@ -145,11 +146,9 @@ class CheckURLMixin:
|
|
145
146
|
"/"
|
146
147
|
):
|
147
148
|
warning = Warning(
|
148
|
-
"Your URL pattern {} has a route beginning with a '/'. Remove this "
|
149
|
+
f"Your URL pattern {self.describe()} has a route beginning with a '/'. Remove this "
|
149
150
|
"slash as it is unnecessary. If this pattern is targeted in an "
|
150
|
-
"include(), ensure the include() pattern has a trailing '/'."
|
151
|
-
self.describe()
|
152
|
-
),
|
151
|
+
"include(), ensure the include() pattern has a trailing '/'.",
|
153
152
|
id="urls.W002",
|
154
153
|
)
|
155
154
|
return [warning]
|
@@ -194,9 +193,9 @@ class RegexPattern(CheckURLMixin):
|
|
194
193
|
if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
|
195
194
|
return [
|
196
195
|
Warning(
|
197
|
-
"Your URL pattern {} uses include with a route ending with a '$'. "
|
196
|
+
f"Your URL pattern {self.describe()} uses include with a route ending with a '$'. "
|
198
197
|
"Remove the dollar from the route to avoid problems including "
|
199
|
-
"URLs."
|
198
|
+
"URLs.",
|
200
199
|
id="urls.W001",
|
201
200
|
)
|
202
201
|
]
|
@@ -238,16 +237,16 @@ def _route_to_regex(route, is_endpoint=False):
|
|
238
237
|
break
|
239
238
|
elif not set(match.group()).isdisjoint(string.whitespace):
|
240
239
|
raise ImproperlyConfigured(
|
241
|
-
"URL route '
|
242
|
-
"<…>."
|
240
|
+
f"URL route '{original_route}' cannot contain whitespace in angle brackets "
|
241
|
+
"<…>."
|
243
242
|
)
|
244
243
|
parts.append(re.escape(route[: match.start()]))
|
245
244
|
route = route[match.end() :]
|
246
245
|
parameter = match["parameter"]
|
247
246
|
if not parameter.isidentifier():
|
248
247
|
raise ImproperlyConfigured(
|
249
|
-
"URL route '{}' uses parameter name {!r} which isn't a valid "
|
250
|
-
"Python identifier."
|
248
|
+
f"URL route '{original_route}' uses parameter name {parameter!r} which isn't a valid "
|
249
|
+
"Python identifier."
|
251
250
|
)
|
252
251
|
raw_converter = match["converter"]
|
253
252
|
if raw_converter is None:
|
@@ -257,9 +256,7 @@ def _route_to_regex(route, is_endpoint=False):
|
|
257
256
|
converter = get_converter(raw_converter)
|
258
257
|
except KeyError as e:
|
259
258
|
raise ImproperlyConfigured(
|
260
|
-
"URL route {!r} uses invalid converter {!r}."
|
261
|
-
original_route, raw_converter
|
262
|
-
)
|
259
|
+
f"URL route {original_route!r} uses invalid converter {raw_converter!r}."
|
263
260
|
) from e
|
264
261
|
converters[parameter] = converter
|
265
262
|
parts.append("(?P<" + parameter + ">" + converter.regex + ")")
|
@@ -297,9 +294,9 @@ class RoutePattern(CheckURLMixin):
|
|
297
294
|
if "(?P<" in route or route.startswith("^") or route.endswith("$"):
|
298
295
|
warnings.append(
|
299
296
|
Warning(
|
300
|
-
"Your URL pattern {} has a route that contains '(?P<', begins "
|
297
|
+
f"Your URL pattern {self.describe()} has a route that contains '(?P<', begins "
|
301
298
|
"with a '^', or ends with a '$'. This was likely an oversight "
|
302
|
-
"when migrating to plain.urls.path()."
|
299
|
+
"when migrating to plain.urls.path().",
|
303
300
|
id="2_0.W001",
|
304
301
|
)
|
305
302
|
)
|
@@ -334,8 +331,8 @@ class URLPattern:
|
|
334
331
|
"""
|
335
332
|
if self.pattern.name is not None and ":" in self.pattern.name:
|
336
333
|
warning = Warning(
|
337
|
-
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
|
338
|
-
"avoid ambiguous namespace references."
|
334
|
+
f"Your URL pattern {self.pattern.describe()} has a name including a ':'. Remove the colon, to "
|
335
|
+
"avoid ambiguous namespace references.",
|
339
336
|
id="urls.W003",
|
340
337
|
)
|
341
338
|
return [warning]
|
@@ -349,12 +346,8 @@ class URLPattern:
|
|
349
346
|
if inspect.isclass(view) and issubclass(view, View):
|
350
347
|
return [
|
351
348
|
Error(
|
352
|
-
"Your URL pattern {} has an invalid view, pass {}.as_view() "
|
353
|
-
"instead of {}."
|
354
|
-
self.pattern.describe(),
|
355
|
-
view.__name__,
|
356
|
-
view.__name__,
|
357
|
-
),
|
349
|
+
f"Your URL pattern {self.pattern.describe()} has an invalid view, pass {view.__name__}.as_view() "
|
350
|
+
f"instead of {view.__name__}.",
|
358
351
|
id="urls.E009",
|
359
352
|
)
|
360
353
|
]
|
@@ -422,16 +415,10 @@ class URLResolver:
|
|
422
415
|
def __repr__(self):
|
423
416
|
if isinstance(self.urlconf_name, list) and self.urlconf_name:
|
424
417
|
# Don't bother to output the whole list, it can be huge
|
425
|
-
urlconf_repr = "
|
418
|
+
urlconf_repr = f"<{self.urlconf_name[0].__class__.__name__} list>"
|
426
419
|
else:
|
427
420
|
urlconf_repr = repr(self.urlconf_name)
|
428
|
-
return "<{} {} ({}:{}) {}>"
|
429
|
-
self.__class__.__name__,
|
430
|
-
urlconf_repr,
|
431
|
-
self.default_namespace,
|
432
|
-
self.namespace,
|
433
|
-
self.pattern.describe(),
|
434
|
-
)
|
421
|
+
return f"<{self.__class__.__name__} {urlconf_repr} ({self.default_namespace}:{self.namespace}) {self.pattern.describe()}>"
|
435
422
|
|
436
423
|
def check(self):
|
437
424
|
messages = []
|
@@ -714,10 +701,10 @@ class URLResolver:
|
|
714
701
|
if args:
|
715
702
|
arg_msg = f"arguments '{args}'"
|
716
703
|
elif kwargs:
|
717
|
-
arg_msg = "keyword arguments '
|
704
|
+
arg_msg = f"keyword arguments '{kwargs}'"
|
718
705
|
else:
|
719
706
|
arg_msg = "no arguments"
|
720
|
-
msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
|
707
|
+
msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % ( # noqa: UP031
|
721
708
|
lookup_view_s,
|
722
709
|
arg_msg,
|
723
710
|
len(patterns),
|
plain/utils/cache.py
CHANGED
plain/utils/crypto.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Plain's standard crypto functions and utilities.
|
3
3
|
"""
|
4
|
+
|
4
5
|
import hashlib
|
5
6
|
import hmac
|
6
7
|
import secrets
|
@@ -32,7 +33,7 @@ def salted_hmac(key_salt, value, secret=None, *, algorithm="sha1"):
|
|
32
33
|
hasher = getattr(hashlib, algorithm)
|
33
34
|
except AttributeError as e:
|
34
35
|
raise InvalidAlgorithm(
|
35
|
-
"
|
36
|
+
f"{algorithm!r} is not an algorithm accepted by the hashlib module."
|
36
37
|
) from e
|
37
38
|
# We need to generate a derived key from our base key. We can do this by
|
38
39
|
# passing the key_salt and our base key through a pseudo-random function.
|
plain/utils/datastructures.py
CHANGED
@@ -198,7 +198,7 @@ class MultiValueDict(dict):
|
|
198
198
|
def update(self, *args, **kwargs):
|
199
199
|
"""Extend rather than replace existing key lists."""
|
200
200
|
if len(args) > 1:
|
201
|
-
raise TypeError("update expected at most 1 argument, got %d" % len(args))
|
201
|
+
raise TypeError("update expected at most 1 argument, got %d" % len(args)) # noqa: UP031
|
202
202
|
if args:
|
203
203
|
arg = args[0]
|
204
204
|
if isinstance(arg, MultiValueDict):
|
@@ -340,6 +340,6 @@ class CaseInsensitiveMapping(Mapping):
|
|
340
340
|
)
|
341
341
|
if not isinstance(elem[0], str):
|
342
342
|
raise ValueError(
|
343
|
-
"Element key
|
343
|
+
f"Element key {elem[0]!r} invalid, only strings are allowed"
|
344
344
|
)
|
345
345
|
yield elem
|
plain/utils/dateformat.py
CHANGED
@@ -10,6 +10,7 @@ Usage:
|
|
10
10
|
7th October 2003 11:39
|
11
11
|
>>>
|
12
12
|
"""
|
13
|
+
|
13
14
|
import calendar
|
14
15
|
from datetime import date, datetime, time
|
15
16
|
from email.utils import format_datetime as format_datetime_rfc5322
|
@@ -42,7 +43,7 @@ class Formatter:
|
|
42
43
|
if type(self.data) is date and hasattr(TimeFormat, piece):
|
43
44
|
raise TypeError(
|
44
45
|
"The format for date objects may not contain "
|
45
|
-
"time-related format specifiers (found '
|
46
|
+
f"time-related format specifiers (found '{piece}')."
|
46
47
|
)
|
47
48
|
pieces.append(str(getattr(self, piece)()))
|
48
49
|
elif piece:
|
@@ -103,7 +104,7 @@ class TimeFormat(Formatter):
|
|
103
104
|
"""
|
104
105
|
hour = self.data.hour % 12 or 12
|
105
106
|
minute = self.data.minute
|
106
|
-
return "%d:%02d" % (hour, minute) if minute else hour
|
107
|
+
return "%d:%02d" % (hour, minute) if minute else hour # noqa: UP031
|
107
108
|
|
108
109
|
def g(self):
|
109
110
|
"Hour, 12-hour format without leading zeros; i.e. '1' to '12'"
|
@@ -115,15 +116,15 @@ class TimeFormat(Formatter):
|
|
115
116
|
|
116
117
|
def h(self):
|
117
118
|
"Hour, 12-hour format; i.e. '01' to '12'"
|
118
|
-
return "%02d" % (self.data.hour % 12 or 12)
|
119
|
+
return "%02d" % (self.data.hour % 12 or 12) # noqa: UP031
|
119
120
|
|
120
121
|
def H(self):
|
121
122
|
"Hour, 24-hour format; i.e. '00' to '23'"
|
122
|
-
return "%02d" % self.data.hour
|
123
|
+
return "%02d" % self.data.hour # noqa: UP031
|
123
124
|
|
124
125
|
def i(self):
|
125
126
|
"Minutes; i.e. '00' to '59'"
|
126
|
-
return "%02d" % self.data.minute
|
127
|
+
return "%02d" % self.data.minute # noqa: UP031
|
127
128
|
|
128
129
|
def O(self): # NOQA: E743, E741
|
129
130
|
"""
|
@@ -138,7 +139,7 @@ class TimeFormat(Formatter):
|
|
138
139
|
seconds = offset.days * 86400 + offset.seconds
|
139
140
|
sign = "-" if seconds < 0 else "+"
|
140
141
|
seconds = abs(seconds)
|
141
|
-
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60)
|
142
|
+
return "%s%02d%02d" % (sign, seconds // 3600, (seconds // 60) % 60) # noqa: UP031
|
142
143
|
|
143
144
|
def P(self):
|
144
145
|
"""
|
@@ -155,7 +156,7 @@ class TimeFormat(Formatter):
|
|
155
156
|
|
156
157
|
def s(self):
|
157
158
|
"Seconds; i.e. '00' to '59'"
|
158
|
-
return "%02d" % self.data.second
|
159
|
+
return "%02d" % self.data.second # noqa: UP031
|
159
160
|
|
160
161
|
def T(self):
|
161
162
|
"""
|
@@ -170,7 +171,7 @@ class TimeFormat(Formatter):
|
|
170
171
|
|
171
172
|
def u(self):
|
172
173
|
"Microseconds; i.e. '000000' to '999999'"
|
173
|
-
return "%06d" % self.data.microsecond
|
174
|
+
return "%06d" % self.data.microsecond # noqa: UP031
|
174
175
|
|
175
176
|
def Z(self):
|
176
177
|
"""
|
@@ -206,7 +207,7 @@ class DateFormat(TimeFormat):
|
|
206
207
|
|
207
208
|
def d(self):
|
208
209
|
"Day of the month, 2 digits with leading zeros; i.e. '01' to '31'"
|
209
|
-
return "%02d" % self.data.day
|
210
|
+
return "%02d" % self.data.day # noqa: UP031
|
210
211
|
|
211
212
|
def D(self):
|
212
213
|
"Day of the week, textual, 3 letters; e.g. 'Fri'"
|
@@ -240,7 +241,7 @@ class DateFormat(TimeFormat):
|
|
240
241
|
|
241
242
|
def m(self):
|
242
243
|
"Month; i.e. '01' to '12'"
|
243
|
-
return "%02d" % self.data.month
|
244
|
+
return "%02d" % self.data.month # noqa: UP031
|
244
245
|
|
245
246
|
def M(self):
|
246
247
|
"Month, textual, 3 letters; e.g. 'Jan'"
|
@@ -306,11 +307,11 @@ class DateFormat(TimeFormat):
|
|
306
307
|
|
307
308
|
def y(self):
|
308
309
|
"""Year, 2 digits with leading zeros; e.g. '99'."""
|
309
|
-
return "%02d" % (self.data.year % 100)
|
310
|
+
return "%02d" % (self.data.year % 100) # noqa: UP031
|
310
311
|
|
311
312
|
def Y(self):
|
312
313
|
"""Year, 4 digits with leading zeros; e.g. '1999'."""
|
313
|
-
return "%04d" % self.data.year
|
314
|
+
return "%04d" % self.data.year # noqa: UP031
|
314
315
|
|
315
316
|
def z(self):
|
316
317
|
"""Day of the year, i.e. 1 to 366."""
|
plain/utils/dateparse.py
CHANGED
@@ -118,7 +118,7 @@ def parse_datetime(value):
|
|
118
118
|
kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
|
119
119
|
tzinfo = kw.pop("tzinfo")
|
120
120
|
if tzinfo == "Z":
|
121
|
-
tzinfo = datetime.
|
121
|
+
tzinfo = datetime.UTC
|
122
122
|
elif tzinfo is not None:
|
123
123
|
offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
|
124
124
|
offset = 60 * int(tzinfo[1:3]) + offset_mins
|
plain/utils/decorators.py
CHANGED
@@ -86,5 +86,5 @@ def method_decorator(decorator, name=""):
|
|
86
86
|
update_wrapper(_dec, decorator)
|
87
87
|
# Change the name to aid debugging.
|
88
88
|
obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
|
89
|
-
_dec.__name__ = "method_decorator(
|
89
|
+
_dec.__name__ = f"method_decorator({obj.__name__})"
|
90
90
|
return _dec
|
plain/utils/html.py
CHANGED
@@ -43,7 +43,7 @@ _js_escapes = {
|
|
43
43
|
}
|
44
44
|
|
45
45
|
# Escape every ASCII character with a value less than 32.
|
46
|
-
_js_escapes.update((ord("%c" % z), "\\u
|
46
|
+
_js_escapes.update((ord("%c" % z), f"\\u{z:04X}") for z in range(32)) # noqa: UP031
|
47
47
|
|
48
48
|
|
49
49
|
@keep_lazy(SafeString)
|
@@ -132,9 +132,9 @@ def linebreaks(value, autoescape=False):
|
|
132
132
|
value = normalize_newlines(value)
|
133
133
|
paras = re.split("\n{2,}", str(value))
|
134
134
|
if autoescape:
|
135
|
-
paras = ["<p
|
135
|
+
paras = ["<p>{}</p>".format(escape(p).replace("\n", "<br>")) for p in paras]
|
136
136
|
else:
|
137
|
-
paras = ["<p
|
137
|
+
paras = ["<p>{}</p>".format(p.replace("\n", "<br>")) for p in paras]
|
138
138
|
return "\n\n".join(paras)
|
139
139
|
|
140
140
|
|
@@ -148,10 +148,10 @@ class MLStripper(HTMLParser):
|
|
148
148
|
self.fed.append(d)
|
149
149
|
|
150
150
|
def handle_entityref(self, name):
|
151
|
-
self.fed.append("
|
151
|
+
self.fed.append(f"&{name};")
|
152
152
|
|
153
153
|
def handle_charref(self, name):
|
154
|
-
self.fed.append("
|
154
|
+
self.fed.append(f"&#{name};")
|
155
155
|
|
156
156
|
def get_data(self):
|
157
157
|
return "".join(self.fed)
|
@@ -293,7 +293,7 @@ class Urlizer:
|
|
293
293
|
if self.simple_url_re.match(middle):
|
294
294
|
url = smart_urlquote(html.unescape(middle))
|
295
295
|
elif self.simple_url_2_re.match(middle):
|
296
|
-
url = smart_urlquote("http
|
296
|
+
url = smart_urlquote(f"http://{html.unescape(middle)}")
|
297
297
|
elif ":" not in middle and self.is_email_simple(middle):
|
298
298
|
local, domain = middle.rsplit("@", 1)
|
299
299
|
try:
|
@@ -328,7 +328,7 @@ class Urlizer:
|
|
328
328
|
def trim_url(self, x, *, limit):
|
329
329
|
if limit is None or len(x) <= limit:
|
330
330
|
return x
|
331
|
-
return "
|
331
|
+
return f"{x[: max(0, limit - 1)]}…"
|
332
332
|
|
333
333
|
def trim_punctuation(self, word):
|
334
334
|
"""
|
plain/utils/http.py
CHANGED
@@ -51,8 +51,8 @@ def urlencode(query, doseq=False):
|
|
51
51
|
for key, value in query:
|
52
52
|
if value is None:
|
53
53
|
raise TypeError(
|
54
|
-
"Cannot encode None for key '
|
55
|
-
"mean to pass an empty string or omit the value?"
|
54
|
+
f"Cannot encode None for key '{key}' in a query string. Did you "
|
55
|
+
"mean to pass an empty string or omit the value?"
|
56
56
|
)
|
57
57
|
elif not doseq or isinstance(value, str | bytes):
|
58
58
|
query_val = value
|
@@ -68,9 +68,9 @@ def urlencode(query, doseq=False):
|
|
68
68
|
for item in itr:
|
69
69
|
if item is None:
|
70
70
|
raise TypeError(
|
71
|
-
"Cannot encode None for key '
|
71
|
+
f"Cannot encode None for key '{key}' in a query "
|
72
72
|
"string. Did you mean to pass an empty string or "
|
73
|
-
"omit the value?"
|
73
|
+
"omit the value?"
|
74
74
|
)
|
75
75
|
elif not isinstance(item, bytes):
|
76
76
|
item = str(item)
|
@@ -110,9 +110,9 @@ def parse_http_date(date):
|
|
110
110
|
if m is not None:
|
111
111
|
break
|
112
112
|
else:
|
113
|
-
raise ValueError("
|
113
|
+
raise ValueError(f"{date!r} is not in a valid HTTP date format")
|
114
114
|
try:
|
115
|
-
tz = datetime.
|
115
|
+
tz = datetime.UTC
|
116
116
|
year = int(m["year"])
|
117
117
|
if year < 100:
|
118
118
|
current_year = datetime.datetime.now(tz=tz).year
|
@@ -131,7 +131,7 @@ def parse_http_date(date):
|
|
131
131
|
result = datetime.datetime(year, month, day, hour, min, sec, tzinfo=tz)
|
132
132
|
return int(result.timestamp())
|
133
133
|
except Exception as exc:
|
134
|
-
raise ValueError("
|
134
|
+
raise ValueError(f"{date!r} is not a valid date") from exc
|
135
135
|
|
136
136
|
|
137
137
|
def parse_http_date_safe(date):
|
@@ -216,7 +216,7 @@ def quote_etag(etag_str):
|
|
216
216
|
if ETAG_MATCH.match(etag_str):
|
217
217
|
return etag_str
|
218
218
|
else:
|
219
|
-
return '"
|
219
|
+
return f'"{etag_str}"'
|
220
220
|
|
221
221
|
|
222
222
|
def is_same_domain(host, pattern):
|
plain/utils/ipv6.py
CHANGED
plain/utils/module_loading.py
CHANGED
@@ -23,7 +23,7 @@ def import_string(dotted_path):
|
|
23
23
|
try:
|
24
24
|
module_path, class_name = dotted_path.rsplit(".", 1)
|
25
25
|
except ValueError as err:
|
26
|
-
raise ImportError("
|
26
|
+
raise ImportError(f"{dotted_path} doesn't look like a module path") from err
|
27
27
|
|
28
28
|
try:
|
29
29
|
return cached_import(module_path, class_name)
|
@@ -66,4 +66,4 @@ def module_dir(module):
|
|
66
66
|
filename = getattr(module, "__file__", None)
|
67
67
|
if filename is not None:
|
68
68
|
return os.path.dirname(filename)
|
69
|
-
raise ValueError("Cannot determine directory containing
|
69
|
+
raise ValueError(f"Cannot determine directory containing {module}")
|