plain 0.24.1__py3-none-any.whl → 0.25.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.
plain/utils/inspect.py CHANGED
@@ -25,49 +25,14 @@ def get_func_args(func):
25
25
  ]
26
26
 
27
27
 
28
- def get_func_full_args(func):
29
- """
30
- Return a list of (argument name, default value) tuples. If the argument
31
- does not have a default value, omit it in the tuple. Arguments such as
32
- *args and **kwargs are also included.
33
- """
34
- params = _get_callable_parameters(func)
35
- args = []
36
- for param in params:
37
- name = param.name
38
- # Ignore 'self'
39
- if name == "self":
40
- continue
41
- if param.kind == inspect.Parameter.VAR_POSITIONAL:
42
- name = "*" + name
43
- elif param.kind == inspect.Parameter.VAR_KEYWORD:
44
- name = "**" + name
45
- if param.default != inspect.Parameter.empty:
46
- args.append((name, param.default))
47
- else:
48
- args.append((name,))
49
- return args
50
-
51
-
52
28
  def func_accepts_kwargs(func):
53
29
  """Return True if function 'func' accepts keyword arguments **kwargs."""
54
30
  return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_KEYWORD)
55
31
 
56
32
 
57
- def func_accepts_var_args(func):
58
- """
59
- Return True if function 'func' accepts positional arguments *args.
60
- """
61
- return any(p for p in _get_callable_parameters(func) if p.kind == p.VAR_POSITIONAL)
62
-
63
-
64
33
  def method_has_no_args(meth):
65
34
  """Return True if a method only accepts 'self'."""
66
35
  count = len(
67
36
  [p for p in _get_callable_parameters(meth) if p.kind == p.POSITIONAL_OR_KEYWORD]
68
37
  )
69
38
  return count == 0 if inspect.ismethod(meth) else count == 1
70
-
71
-
72
- def func_supports_parameter(func, name):
73
- return any(param.name == name for param in _get_callable_parameters(func))
plain/utils/safestring.py CHANGED
@@ -44,9 +44,6 @@ class SafeString(str, SafeData):
44
44
  return self
45
45
 
46
46
 
47
- SafeText = SafeString # For backwards compatibility since Plain 2.0.
48
-
49
-
50
47
  def _safety_decorator(safety_marker, func):
51
48
  @wraps(func)
52
49
  def wrapper(*args, **kwargs):
plain/utils/text.py CHANGED
@@ -1,65 +1,13 @@
1
- import gzip
2
1
  import re
3
- import secrets
4
2
  import unicodedata
5
- from gzip import GzipFile
6
- from gzip import compress as gzip_compress
7
- from io import BytesIO
8
3
 
9
- from plain.exceptions import SuspiciousFileOperation
10
4
  from plain.utils.functional import SimpleLazyObject, keep_lazy_text, lazy
11
5
  from plain.utils.regex_helper import _lazy_re_compile
12
6
 
13
-
14
- @keep_lazy_text
15
- def capfirst(x):
16
- """Capitalize the first letter of a string."""
17
- if not x:
18
- return x
19
- if not isinstance(x, str):
20
- x = str(x)
21
- return x[0].upper() + x[1:]
22
-
23
-
24
7
  # Set up regular expressions
25
8
  re_words = _lazy_re_compile(r"<[^>]+?>|([^<>\s]+)", re.S)
26
9
  re_chars = _lazy_re_compile(r"<[^>]+?>|(.)", re.S)
27
10
  re_tag = _lazy_re_compile(r"<(/)?(\S+?)(?:(\s*/)|\s.*?)?>", re.S)
28
- re_newlines = _lazy_re_compile(r"\r\n|\r") # Used in normalize_newlines
29
- re_camel_case = _lazy_re_compile(r"(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))")
30
-
31
-
32
- @keep_lazy_text
33
- def wrap(text, width):
34
- """
35
- A word-wrap function that preserves existing line breaks. Expects that
36
- existing line breaks are posix newlines.
37
-
38
- Preserve all white space except added line breaks consume the space on
39
- which they break the line.
40
-
41
- Don't wrap long words, thus the output text may have lines longer than
42
- ``width``.
43
- """
44
-
45
- def _generator():
46
- for line in text.splitlines(True): # True keeps trailing linebreaks
47
- max_width = min((line.endswith("\n") and width + 1 or width), width)
48
- while len(line) > max_width:
49
- space = line[: max_width + 1].rfind(" ") + 1
50
- if space == 0:
51
- space = line.find(" ") + 1
52
- if space == 0:
53
- yield line
54
- line = ""
55
- break
56
- yield f"{line[: space - 1]}\n"
57
- line = line[space:]
58
- max_width = min((line.endswith("\n") and width + 1 or width), width)
59
- if line:
60
- yield line
61
-
62
- return "".join(_generator())
63
11
 
64
12
 
65
13
  class Truncator(SimpleLazyObject):
@@ -229,189 +177,6 @@ class Truncator(SimpleLazyObject):
229
177
  return out
230
178
 
231
179
 
232
- @keep_lazy_text
233
- def get_valid_filename(name):
234
- """
235
- Return the given string converted to a string that can be used for a clean
236
- filename. Remove leading and trailing spaces; convert other spaces to
237
- underscores; and remove anything that is not an alphanumeric, dash,
238
- underscore, or dot.
239
- >>> get_valid_filename("john's portrait in 2004.jpg")
240
- 'johns_portrait_in_2004.jpg'
241
- """
242
- s = str(name).strip().replace(" ", "_")
243
- s = re.sub(r"(?u)[^-\w.]", "", s)
244
- if s in {"", ".", ".."}:
245
- raise SuspiciousFileOperation(f"Could not derive file name from '{name}'")
246
- return s
247
-
248
-
249
- @keep_lazy_text
250
- def get_text_list(list_, last_word="or"):
251
- """
252
- >>> get_text_list(['a', 'b', 'c', 'd'])
253
- 'a, b, c or d'
254
- >>> get_text_list(['a', 'b', 'c'], 'and')
255
- 'a, b and c'
256
- >>> get_text_list(['a', 'b'], 'and')
257
- 'a and b'
258
- >>> get_text_list(['a'])
259
- 'a'
260
- >>> get_text_list([])
261
- ''
262
- """
263
- if not list_:
264
- return ""
265
- if len(list_) == 1:
266
- return str(list_[0])
267
- return "{} {} {}".format(
268
- # Translators: This string is used as a separator between list elements
269
- ", ".join(str(i) for i in list_[:-1]),
270
- str(last_word),
271
- str(list_[-1]),
272
- )
273
-
274
-
275
- @keep_lazy_text
276
- def normalize_newlines(text):
277
- """Normalize CRLF and CR newlines to just LF."""
278
- return re_newlines.sub("\n", str(text))
279
-
280
-
281
- @keep_lazy_text
282
- def phone2numeric(phone):
283
- """Convert a phone number with letters into its numeric equivalent."""
284
- char2number = {
285
- "a": "2",
286
- "b": "2",
287
- "c": "2",
288
- "d": "3",
289
- "e": "3",
290
- "f": "3",
291
- "g": "4",
292
- "h": "4",
293
- "i": "4",
294
- "j": "5",
295
- "k": "5",
296
- "l": "5",
297
- "m": "6",
298
- "n": "6",
299
- "o": "6",
300
- "p": "7",
301
- "q": "7",
302
- "r": "7",
303
- "s": "7",
304
- "t": "8",
305
- "u": "8",
306
- "v": "8",
307
- "w": "9",
308
- "x": "9",
309
- "y": "9",
310
- "z": "9",
311
- }
312
- return "".join(char2number.get(c, c) for c in phone.lower())
313
-
314
-
315
- def _get_random_filename(max_random_bytes):
316
- return b"a" * secrets.randbelow(max_random_bytes)
317
-
318
-
319
- def compress_string(s, *, max_random_bytes=None):
320
- compressed_data = gzip_compress(s, compresslevel=6, mtime=0)
321
-
322
- if not max_random_bytes:
323
- return compressed_data
324
-
325
- compressed_view = memoryview(compressed_data)
326
- header = bytearray(compressed_view[:10])
327
- header[3] = gzip.FNAME
328
-
329
- filename = _get_random_filename(max_random_bytes) + b"\x00"
330
-
331
- return bytes(header) + filename + compressed_view[10:]
332
-
333
-
334
- class StreamingBuffer(BytesIO):
335
- def read(self):
336
- ret = self.getvalue()
337
- self.seek(0)
338
- self.truncate()
339
- return ret
340
-
341
-
342
- # Like compress_string, but for iterators of strings.
343
- def compress_sequence(sequence, *, max_random_bytes=None):
344
- buf = StreamingBuffer()
345
- filename = _get_random_filename(max_random_bytes) if max_random_bytes else None
346
- with GzipFile(
347
- filename=filename, mode="wb", compresslevel=6, fileobj=buf, mtime=0
348
- ) as zfile:
349
- # Output headers...
350
- yield buf.read()
351
- for item in sequence:
352
- zfile.write(item)
353
- data = buf.read()
354
- if data:
355
- yield data
356
- yield buf.read()
357
-
358
-
359
- # Expression to match some_token and some_token="with spaces" (and similarly
360
- # for single-quoted strings).
361
- smart_split_re = _lazy_re_compile(
362
- r"""
363
- ((?:
364
- [^\s'"]*
365
- (?:
366
- (?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*')
367
- [^\s'"]*
368
- )+
369
- ) | \S+)
370
- """,
371
- re.VERBOSE,
372
- )
373
-
374
-
375
- def smart_split(text):
376
- r"""
377
- Generator that splits a string by spaces, leaving quoted phrases together.
378
- Supports both single and double quotes, and supports escaping quotes with
379
- backslashes. In the output, strings will keep their initial and trailing
380
- quote marks and escaped quotes will remain escaped (the results can then
381
- be further processed with unescape_string_literal()).
382
-
383
- >>> list(smart_split(r'This is "a person\'s" test.'))
384
- ['This', 'is', '"a person\\\'s"', 'test.']
385
- >>> list(smart_split(r"Another 'person\'s' test."))
386
- ['Another', "'person\\'s'", 'test.']
387
- >>> list(smart_split(r'A "\"funky\" style" test.'))
388
- ['A', '"\\"funky\\" style"', 'test.']
389
- """
390
- for bit in smart_split_re.finditer(str(text)):
391
- yield bit[0]
392
-
393
-
394
- @keep_lazy_text
395
- def unescape_string_literal(s):
396
- r"""
397
- Convert quoted string literals to unquoted strings with escaped quotes and
398
- backslashes unquoted::
399
-
400
- >>> unescape_string_literal('"abc"')
401
- 'abc'
402
- >>> unescape_string_literal("'abc'")
403
- 'abc'
404
- >>> unescape_string_literal('"a \"bc\""')
405
- 'a "bc"'
406
- >>> unescape_string_literal("'\'ab\' c'")
407
- "'ab' c"
408
- """
409
- if not s or s[0] not in "\"'" or s[-1] != s[0]:
410
- raise ValueError(f"Not a string literal: {s!r}")
411
- quote = s[0]
412
- return s[1:-1].replace(rf"\{quote}", quote).replace(r"\\", "\\")
413
-
414
-
415
180
  @keep_lazy_text
416
181
  def slugify(value, allow_unicode=False):
417
182
  """
@@ -433,21 +198,6 @@ def slugify(value, allow_unicode=False):
433
198
  return re.sub(r"[-\s]+", "-", value).strip("-_")
434
199
 
435
200
 
436
- def camel_case_to_spaces(value):
437
- """
438
- Split CamelCase and convert to lowercase. Strip surrounding whitespace.
439
- """
440
- return re_camel_case.sub(r" \1", value).strip().lower()
441
-
442
-
443
- def _format_lazy(format_string, *args, **kwargs):
444
- """
445
- Apply str.format() on 'format_string' where format_string, args,
446
- and/or kwargs might be lazy.
447
- """
448
- return format_string.format(*args, **kwargs)
449
-
450
-
451
201
  def pluralize(singular, plural, number):
452
202
  if number == 1:
453
203
  return singular
@@ -513,6 +263,3 @@ def pluralize_lazy(singular, plural, number):
513
263
  return proxy
514
264
 
515
265
  return lazy_number(pluralize, str, singular=singular, plural=plural, number=number)
516
-
517
-
518
- format_lazy = lazy(_format_lazy, str)
plain/validators.py CHANGED
@@ -158,17 +158,6 @@ class URLValidator(RegexValidator):
158
158
  raise ValidationError(self.message, code=self.code, params={"value": value})
159
159
 
160
160
 
161
- integer_validator = RegexValidator(
162
- _lazy_re_compile(r"^-?\d+\Z"),
163
- message="Enter a valid integer.",
164
- code="invalid",
165
- )
166
-
167
-
168
- def validate_integer(value):
169
- return integer_validator(value)
170
-
171
-
172
161
  @deconstructible
173
162
  class EmailValidator:
174
163
  message = "Enter a valid email address."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.24.1
3
+ Version: 0.25.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -1,45 +1,45 @@
1
1
  plain/README.md,sha256=nW3Ioj3IxPb6aoCGaFMN2n7Cd7LMx0s8Lph6pMkKnh4,8
2
2
  plain/__main__.py,sha256=BiYbF-txGNbeRqp_CHQ9EZ_bCbbKq2iw51Z8RRUgIBY,105
3
3
  plain/debug.py,sha256=abPkJY4aSbBYGEYSZST_ZY3ohXPGDdz9uWQBYRqfd3M,730
4
- plain/exceptions.py,sha256=pPAO8cIzNoqa2YrxG0d31C4P2V9SZ2OprHcjY9-qhIU,6332
4
+ plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
5
5
  plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
6
- plain/paginator.py,sha256=zCHCmCG8FEet6C28w2rUko9yiGFnOGCQxRbM_ssa9XY,6037
6
+ plain/paginator.py,sha256=-fpLJd6c-V8bLCaNCHfTqPtm-Lm2Y1TuKqFDfy7n3ZE,5857
7
7
  plain/signing.py,sha256=sf7g1Mp-FzdjFAEoLxHyu2YvbUl5w4FOtTVDAfq6TO0,8733
8
- plain/validators.py,sha256=2tZh2Bvp955gejltwVtFGwfqw5-9VAOhKqos3Me4paY,19923
8
+ plain/validators.py,sha256=byAdFophb3Mfs09IwqzpIgumQHIil76ZDj2suYNaUNQ,19723
9
9
  plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
10
- plain/assets/README.md,sha256=048BzyQ2-BcsRiv6NiuLHHijOw96RK-e6lJ_Eq7g2pc,2857
10
+ plain/assets/README.md,sha256=Ukm_gU7Xj-itAmEjsWUXXDtU5d8BSRpy7ZgGB2LbSo0,2847
11
11
  plain/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  plain/assets/compile.py,sha256=Qg-rMWykij_Jheq4THrPFWlmYv07ihHzWiNsD815HYE,3336
13
13
  plain/assets/finders.py,sha256=rhkHG5QW3H3IlBGHB5WJf9J6VTdDWgUC0qEs6u2Z4RQ,1233
14
14
  plain/assets/fingerprints.py,sha256=1NKAnnXVlncY5iimXztr0NL3RIjBKsNlZRIe6nmItJc,931
15
15
  plain/assets/urls.py,sha256=lW7VzKNzTKY11JqbszhJQ1Yy0HtljZlsHDnnkTPdLOM,992
16
16
  plain/assets/views.py,sha256=z7noLzoelGw_8-MXcvGKjXs9KZ43Tivmy2TIfnZIpgw,9253
17
- plain/cli/README.md,sha256=TvWCnNwb1rNthPzJglCRMKacN5H_RLeEjYBMe62Uz4M,2461
17
+ plain/cli/README.md,sha256=CwrqK-NV5ZK1JDYfR4480uHXmJEyxWF6jA_K40_llE8,2366
18
18
  plain/cli/__init__.py,sha256=9ByBOIdM8DebChjNz-RH2atdz4vWe8somlwNEsbhwh4,40
19
- plain/cli/cli.py,sha256=40ewy2p3lyfgjrRrBU5QDBXd4BOtfejRO34nymNYmfc,18401
19
+ plain/cli/cli.py,sha256=NbkgCiJk-QaAGUBU7Dne37B_Xv3sZPcGyA5IBG4nFIo,18420
20
20
  plain/cli/formatting.py,sha256=1hZH13y1qwHcU2K2_Na388nw9uvoeQH8LrWL-O9h8Yc,2207
21
21
  plain/cli/packages.py,sha256=GLvDgQ1o93tSHae_B2i0YNimpt3LGu4QMQpFYrO48d8,2758
22
22
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
23
23
  plain/cli/startup.py,sha256=3LIz9JrIZoF52Sa0j0SCypQwEaBDkhvuGaBdtiQLr5Q,680
24
24
  plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
25
- plain/csrf/middleware.py,sha256=1szNRF-kKJdjhTw_Jw1Cp2IldtrLtbUqAApJOrVP1qc,17449
25
+ plain/csrf/middleware.py,sha256=FYhT7KPJ664Sm0nKjeej1OIXalvVTYiotQX3ytI0dfY,17417
26
26
  plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
27
27
  plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
28
28
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
29
29
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
30
30
  plain/forms/exceptions.py,sha256=XCLDRl5snIEDu5-8mLB0NnU_tegcBfyIHMiJxqvbxnc,164
31
- plain/forms/fields.py,sha256=OiZnJPANsy-sEpiE5JduxVFsQebWpDuB531VBY1Tfpk,35328
32
- plain/forms/forms.py,sha256=CiONLo9VhE7E1-u-UejX4XWxxwu6MvR-lVPaVAY7VQM,10441
31
+ plain/forms/fields.py,sha256=rsKPsb9cSbvSKhu-eg4yGV1gI-Lv_LaFK_6U22D7oJI,35327
32
+ plain/forms/forms.py,sha256=fEKBee1b8I_DJ-FufzWJGtSQoUoyieYfqUaGEre9B4Q,10418
33
33
  plain/http/README.md,sha256=HjEtoAhn14OoMdgb-wK-uc8No7C4d4gZUhzseOp7Fg4,236
34
34
  plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
35
35
  plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
36
- plain/http/multipartparser.py,sha256=k6BhpilFENQQ1cuGix6aa-jGwbhBVms2A2O01-s3_4c,27304
37
- plain/http/request.py,sha256=Kp0q-1obg7ZUJBz7iuyHwPmNMnssZmoIdLp8ylxJNAo,25965
36
+ plain/http/multipartparser.py,sha256=Cyk_UZhxf8JwNza_Yl4_nKCYkmnG7xY9PSVcf9Us57U,27266
37
+ plain/http/request.py,sha256=kq3AuM0EWyAD_kqMlorTccm5mzIQ6ZefkCa-jXUntnI,25514
38
38
  plain/http/response.py,sha256=RR2sUG-ONWKWcZyIbztjWvtFyh0cR-CoxQvnWOyN0io,23619
39
39
  plain/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  plain/internal/files/README.md,sha256=kMux-NU5qiH0o1K8IajYQT8VjrYl_jLk9LkGG_kGuSc,45
41
41
  plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
42
- plain/internal/files/base.py,sha256=CSq_BPKWY2kUib7pnoIvzi40isEyrKdkx1yHb8nPtLc,4817
42
+ plain/internal/files/base.py,sha256=-JpRMzv2bgVSZ9dcxh13gGRTVeEd_Tjd02iQMOXsRgQ,4126
43
43
  plain/internal/files/locks.py,sha256=z03q7IZD4tPMK3s1HKF3w_uetkFj6w6FTheLUxZsfB0,3616
44
44
  plain/internal/files/move.py,sha256=jfdD29QhamxZjXRgqmZS4dJoJ4sK6M7QK1Km-69jWeo,3238
45
45
  plain/internal/files/temp.py,sha256=UJJnCI8dqPIC8XXHU3-jG2-0svbkrgGlBs4yhciLm4c,2506
@@ -49,7 +49,7 @@ plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg
49
49
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  plain/internal/handlers/base.py,sha256=oRKni79ATI_u7sywGFExrzKvP5dpJTqIp1m521A90Ew,4169
51
51
  plain/internal/handlers/exception.py,sha256=rv8shMlTJdIhTm99VacILIiu5JRcmtumg8yWuy7GYto,4592
52
- plain/internal/handlers/wsgi.py,sha256=estA1QKHTk3ZqziWxenHsw5UO2cwPp3Zr0XjkDeM5TY,7561
52
+ plain/internal/handlers/wsgi.py,sha256=aOGCd9hJEMTVMGfgIDlSFvevd8_XCzZa2dtlR4peqZg,8253
53
53
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
55
55
  plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
@@ -69,7 +69,7 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
69
69
  plain/preflight/messages.py,sha256=HwatjA6MRFfzFAnSOa_uAw1Pvk_CLuNfW3IYi71_1Mk,2322
70
70
  plain/preflight/registry.py,sha256=7s7f_iEwURzv-Ye515P5lJWcHltd5Ca2fsX1Wpbf1wQ,2306
71
71
  plain/preflight/security.py,sha256=sNpv5AHobPcaO48cOUGRNe2EjusTducjY8vyShR8EhI,2645
72
- plain/preflight/urls.py,sha256=q3ikavMB8ZFMzzm_JOnm1YKwSONgaHjH4Is4-4mmTKM,3001
72
+ plain/preflight/urls.py,sha256=OSTLvCpftAD_8VbQ0V3p1CTPlRRwtlnXVBZeWgr7l2k,2881
73
73
  plain/runtime/README.md,sha256=Q8VVO7JRGuYrDxzuYL6ptoilhclbecxKzpRXKgbWGkU,2061
74
74
  plain/runtime/__init__.py,sha256=FyGTIx-633bNPrPv8IBarBKvu_XspbCiqj8W8eOd_mA,1528
75
75
  plain/runtime/global_settings.py,sha256=SfOhwzpZe2zpNqSpdx3hHgCN89xdbW9KJVR4KJfS_Gk,5498
@@ -96,35 +96,31 @@ plain/urls/__init__.py,sha256=XF-W2GqLMA4bHbDRKnpZ7tiUtJ-BhWN-yAzw4nNnHdc,590
96
96
  plain/urls/converters.py,sha256=s2JZVOdzZC16lgobsI93hygcdH5L0Kj4742WEkXsVcs,1193
97
97
  plain/urls/exceptions.py,sha256=q4iPh3Aa-zHbA-tw8v6WyX1J1n5WdAady2xvxFuyXB0,114
98
98
  plain/urls/patterns.py,sha256=bU_xfhZbKMSgRG9OJ8w_NSuYRm_9zGnqoz_WY44fhUk,9358
99
- plain/urls/resolvers.py,sha256=Jcve0_5G2IgLyMKA_cxccViQBhAkFS4X4Z-ozIXZM9w,15472
99
+ plain/urls/resolvers.py,sha256=3ntfYe2-mQa1tR8jF1-UesnnbZaUp_aVNY3OtiSi5R4,15466
100
100
  plain/urls/routers.py,sha256=J7v-o4BbTk_iPy_kMP_hOMNOPk-D2lockUmSD0Wx1R0,4056
101
101
  plain/urls/utils.py,sha256=WiGq6hHI-5DLFOxCQTAZ2qm0J-UdGosLcjuxlfK6_Tg,2137
102
102
  plain/utils/README.md,sha256=Bf5OG-MkOJDz_U8RGVreDfAI4M4nnPaLtk-LdinxHSc,99
103
103
  plain/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
- plain/utils/_os.py,sha256=oqfiKZRbmHwwWSZP36SIajQnFDbImbB6gcyK0idAGl4,1988
105
- plain/utils/cache.py,sha256=3GtF8EU07LmD7CSvWSuF7YTThgeNxVkuU68wBK_35Wk,11489
104
+ plain/utils/cache.py,sha256=0qziMJfzulWnNWlY4MBNfaYHIbKkAXRwy4QZNDr7s3o,4497
106
105
  plain/utils/connection.py,sha256=NN7xRhy6qIWuOOhi1x9YdGcFcYhKepTiMUETeEMS0vY,2501
107
106
  plain/utils/crypto.py,sha256=zFDydnaqNMGYFHUc-CAn8f93685a17BhGusAcITH1lI,2662
108
107
  plain/utils/datastructures.py,sha256=g4UYTbxIb_n8F9JWMP4dHPwUz71591fHreGATPO4qEc,10240
109
- plain/utils/dateformat.py,sha256=nsy71l16QuPN0ozGpVlCU5Et101fhk9L38F-wqT5p5I,10203
110
108
  plain/utils/dateparse.py,sha256=u9_tF85YteXSjW9KQzNg_pcCEFDZS3EGorCddcWU0vE,5351
111
- plain/utils/dates.py,sha256=hSDKz8eIb3W5QjmGiklFZZELB0inYXsfiRUy49Cx-2Q,1226
112
109
  plain/utils/deconstruct.py,sha256=7NwEFIDCiadAArUBFmiErzDgfIgDWeKqqQFDXwSgQoQ,1830
113
110
  plain/utils/decorators.py,sha256=mLHOo2jLdvYRo2z8lkeVn2vQErlj7xC6XoLwZBYf_z8,358
114
111
  plain/utils/duration.py,sha256=l0Gc41-DeyyAmpdy2XG-YO5UKxMf1NDpWIlQuD5hAn0,1162
115
- plain/utils/email.py,sha256=puRTBVuz44YvpnqV3LT4nNIKqdqfY3L8zbDJIkqHk2Y,328
116
- plain/utils/encoding.py,sha256=z8c7HxYW6wQiE4csx5Ui3WvzgbDzLGXY2aCP04_GZd4,7900
117
- plain/utils/functional.py,sha256=Xf51mt5bB8zehR8pTRVnRfV21vJ4n1IGpcwj88SSsUA,14791
112
+ plain/utils/encoding.py,sha256=T0Shb2xRAR3NPwwoqhpUOB55gDprWzqu72aRiiulv9Y,4251
113
+ plain/utils/functional.py,sha256=9sfuPSX1RalDkLNLjR7k-OjooHXjHSWB9ya2kDLOWtE,14652
118
114
  plain/utils/hashable.py,sha256=uLWobCCh7VcEPJ7xzVGPgigNVuTazYJbyzRzHTCI_wo,739
119
- plain/utils/html.py,sha256=K8nJSiNRU54htQOfhezZAYspXaDGN3LjOE3SxmnQml8,13513
120
- plain/utils/http.py,sha256=OmnqW_nYUaFN-pAEwjTEh9AkpfjKGmE51Ge6inNGB10,12710
121
- plain/utils/inspect.py,sha256=lhDEOtmSLEub5Jj__MIgW3AyWOEVkaA6doJKKwBhZ6A,2235
115
+ plain/utils/html.py,sha256=SR8oNrungB5gxJaHbvAaCw_bAiqLQOk09fj-iIXY0i0,3679
116
+ plain/utils/http.py,sha256=VOOnwRXnDp5PL_qEmkInLTm10fF58vlhVjeSTdzV2cQ,6031
117
+ plain/utils/inspect.py,sha256=O3VMH5f4aGOrVpXJBKtQOxx01XrKnjjz6VO_MCV0xkE,1140
122
118
  plain/utils/ipv6.py,sha256=pISQ2AIlG8xXlxpphn388q03fq-fOrlu4GZR0YYjQXw,1267
123
119
  plain/utils/itercompat.py,sha256=lacIDjczhxbwG4ON_KfG1H6VNPOGOpbRhnVhbedo2CY,184
124
120
  plain/utils/module_loading.py,sha256=CWl7Shoax9Zkevf1pM9PpS_0V69J5Cukjyj078UPCAw,2252
125
121
  plain/utils/regex_helper.py,sha256=pAdh_xG52BOyXLsiuIMPFgduUAoWOEje1ZpjhcefxiA,12769
126
- plain/utils/safestring.py,sha256=SHGhpbX6FFDKSYOY9zYAgAQX0g0exzRba7dM2bJalWs,1873
127
- plain/utils/text.py,sha256=qX7vECGH4Xk96qZRH9A1IyZA-mrJ-j62j3kDcLTdWK0,16586
122
+ plain/utils/safestring.py,sha256=sawOehuWjr4bkF5jXXCcziILQGoqUcA_eEfsURrAyN0,1801
123
+ plain/utils/text.py,sha256=42hJv06sadbWfsaAHNhqCQaP1W9qZ69trWDTS-Xva7k,9496
128
124
  plain/utils/timesince.py,sha256=a_-ZoPK_s3Pt998CW4rWp0clZ1XyK2x04hCqak2giII,5928
129
125
  plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
130
126
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
@@ -138,8 +134,8 @@ plain/views/forms.py,sha256=RhlaUcZCkeqokY_fvv-NOS-kgZAG4XhDLOPbf9K_Zlc,2691
138
134
  plain/views/objects.py,sha256=g5Lzno0Zsv0K449UpcCtxwCoO7WMRAWqKlxxV2V0_qg,8263
139
135
  plain/views/redirect.py,sha256=9zHZgKvtSkdrMX9KmsRM8hJTPmBktxhc4d8OitbuniI,1724
140
136
  plain/views/templates.py,sha256=cBkFNCSXgVi8cMqQbhsqJ4M_rIQYVl8cUvq9qu4YIes,1951
141
- plain-0.24.1.dist-info/METADATA,sha256=fv18aykx9W3GX_VzeOOf3wWAozsqUPzUUbJ-LV7OaQM,319
142
- plain-0.24.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
- plain-0.24.1.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
144
- plain-0.24.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
- plain-0.24.1.dist-info/RECORD,,
137
+ plain-0.25.0.dist-info/METADATA,sha256=ErgfCIZpGA9-NDGsjKHueIWtZ2LhJquSTQ37ycDo2BQ,319
138
+ plain-0.25.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
+ plain-0.25.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
140
+ plain-0.25.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
+ plain-0.25.0.dist-info/RECORD,,
plain/utils/_os.py DELETED
@@ -1,52 +0,0 @@
1
- import os
2
- import tempfile
3
- from os.path import abspath, dirname, join, normcase, sep
4
-
5
- from plain.exceptions import SuspiciousFileOperation
6
-
7
-
8
- def safe_join(base, *paths):
9
- """
10
- Join one or more path components to the base path component intelligently.
11
- Return a normalized, absolute version of the final path.
12
-
13
- Raise ValueError if the final path isn't located inside of the base path
14
- component.
15
- """
16
- final_path = abspath(join(base, *paths))
17
- base_path = abspath(base)
18
- # Ensure final_path starts with base_path (using normcase to ensure we
19
- # don't false-negative on case insensitive operating systems like Windows),
20
- # further, one of the following conditions must be true:
21
- # a) The next character is the path separator (to prevent conditions like
22
- # safe_join("/dir", "/../d"))
23
- # b) The final path must be the same as the base path.
24
- # c) The base path must be the most root path (meaning either "/" or "C:\\")
25
- if (
26
- not normcase(final_path).startswith(normcase(base_path + sep))
27
- and normcase(final_path) != normcase(base_path)
28
- and dirname(normcase(base_path)) != normcase(base_path)
29
- ):
30
- raise SuspiciousFileOperation(
31
- f"The joined path ({final_path}) is located outside of the base path "
32
- f"component ({base_path})"
33
- )
34
- return final_path
35
-
36
-
37
- def symlinks_supported():
38
- """
39
- Return whether or not creating symlinks are supported in the host platform
40
- and/or if they are allowed to be created (e.g. on Windows it requires admin
41
- permissions).
42
- """
43
- with tempfile.TemporaryDirectory() as temp_dir:
44
- original_path = os.path.join(temp_dir, "original")
45
- symlink_path = os.path.join(temp_dir, "symlink")
46
- os.makedirs(original_path)
47
- try:
48
- os.symlink(original_path, symlink_path)
49
- supported = True
50
- except (OSError, NotImplementedError):
51
- supported = False
52
- return supported