omlish 0.0.0.dev192__py3-none-any.whl → 0.0.0.dev194__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. omlish/.manifests.json +17 -3
  2. omlish/__about__.py +2 -2
  3. omlish/codecs/base.py +2 -0
  4. omlish/configs/__init__.py +0 -5
  5. omlish/configs/formats.py +247 -0
  6. omlish/configs/nginx.py +72 -0
  7. omlish/configs/processing/__init__.py +0 -0
  8. omlish/configs/{flattening.py → processing/flattening.py} +31 -33
  9. omlish/configs/processing/inheritance.py +58 -0
  10. omlish/configs/processing/matching.py +53 -0
  11. omlish/configs/processing/names.py +50 -0
  12. omlish/configs/processing/rewriting.py +141 -0
  13. omlish/configs/processing/strings.py +45 -0
  14. omlish/configs/types.py +9 -0
  15. omlish/formats/__init__.py +4 -0
  16. omlish/formats/ini/__init__.py +0 -0
  17. omlish/formats/ini/codec.py +26 -0
  18. omlish/formats/ini/sections.py +45 -0
  19. omlish/formats/toml/__init__.py +0 -0
  20. omlish/formats/{toml.py → toml/codec.py} +2 -2
  21. omlish/formats/toml/parser.py +827 -0
  22. omlish/formats/toml/writer.py +124 -0
  23. omlish/lang/__init__.py +4 -1
  24. omlish/lang/iterables.py +0 -7
  25. omlish/lite/configs.py +38 -0
  26. omlish/lite/types.py +9 -0
  27. omlish/text/glyphsplit.py +25 -10
  28. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/METADATA +1 -1
  29. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/RECORD +33 -17
  30. omlish/configs/strings.py +0 -96
  31. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/LICENSE +0 -0
  32. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/WHEEL +0 -0
  33. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/entry_points.txt +0 -0
  34. {omlish-0.0.0.dev192.dist-info → omlish-0.0.0.dev194.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,827 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ # SPDX-License-Identifier: MIT
4
+ # SPDX-FileCopyrightText: 2021 Taneli Hukkinen
5
+ # Licensed to PSF under a Contributor Agreement.
6
+ #
7
+ # PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
8
+ # --------------------------------------------
9
+ #
10
+ # 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
11
+ # ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
12
+ # documentation.
13
+ #
14
+ # 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
15
+ # royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
16
+ # works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
17
+ # Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
18
+ # 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
19
+ # Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
20
+ #
21
+ # 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
22
+ # wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
23
+ # any such work a brief summary of the changes made to Python.
24
+ #
25
+ # 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
26
+ # EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
27
+ # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
28
+ # RIGHTS.
29
+ #
30
+ # 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
31
+ # DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
32
+ # ADVISED OF THE POSSIBILITY THEREOF.
33
+ #
34
+ # 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
35
+ #
36
+ # 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
37
+ # venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
38
+ # name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
39
+ #
40
+ # 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
41
+ # License Agreement.
42
+ #
43
+ # https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
44
+ import datetime
45
+ import functools
46
+ import re
47
+ import string
48
+ import types
49
+ import typing as ta
50
+
51
+
52
+ TomlParseFloat = ta.Callable[[str], ta.Any]
53
+ TomlKey = ta.Tuple[str, ...]
54
+ TomlPos = int # ta.TypeAlias
55
+
56
+
57
+ ##
58
+
59
+
60
+ _TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
61
+
62
+ TOML_RE_NUMBER = re.compile(
63
+ r"""
64
+ 0
65
+ (?:
66
+ x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
67
+ |
68
+ b[01](?:_?[01])* # bin
69
+ |
70
+ o[0-7](?:_?[0-7])* # oct
71
+ )
72
+ |
73
+ [+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
74
+ (?P<floatpart>
75
+ (?:\.[0-9](?:_?[0-9])*)? # optional fractional part
76
+ (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
77
+ )
78
+ """,
79
+ flags=re.VERBOSE,
80
+ )
81
+ TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
82
+ TOML_RE_DATETIME = re.compile(
83
+ rf"""
84
+ ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
85
+ (?:
86
+ [Tt ]
87
+ {_TOML_TIME_RE_STR}
88
+ (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
89
+ )?
90
+ """,
91
+ flags=re.VERBOSE,
92
+ )
93
+
94
+
95
+ def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
96
+ """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
97
+
98
+ Raises ValueError if the match does not correspond to a valid date or datetime.
99
+ """
100
+ (
101
+ year_str,
102
+ month_str,
103
+ day_str,
104
+ hour_str,
105
+ minute_str,
106
+ sec_str,
107
+ micros_str,
108
+ zulu_time,
109
+ offset_sign_str,
110
+ offset_hour_str,
111
+ offset_minute_str,
112
+ ) = match.groups()
113
+ year, month, day = int(year_str), int(month_str), int(day_str)
114
+ if hour_str is None:
115
+ return datetime.date(year, month, day)
116
+ hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
117
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
118
+ if offset_sign_str:
119
+ tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
120
+ offset_hour_str, offset_minute_str, offset_sign_str,
121
+ )
122
+ elif zulu_time:
123
+ tz = datetime.UTC
124
+ else: # local date-time
125
+ tz = None
126
+ return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
127
+
128
+
129
+ @functools.lru_cache() # noqa
130
+ def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
131
+ sign = 1 if sign_str == '+' else -1
132
+ return datetime.timezone(
133
+ datetime.timedelta(
134
+ hours=sign * int(hour_str),
135
+ minutes=sign * int(minute_str),
136
+ ),
137
+ )
138
+
139
+
140
+ def toml_match_to_localtime(match: re.Match) -> datetime.time:
141
+ hour_str, minute_str, sec_str, micros_str = match.groups()
142
+ micros = int(micros_str.ljust(6, '0')) if micros_str else 0
143
+ return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
144
+
145
+
146
+ def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
147
+ if match.group('floatpart'):
148
+ return parse_float(match.group())
149
+ return int(match.group(), 0)
150
+
151
+
152
+ TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
153
+
154
+ # Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
155
+ # functions.
156
+ TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
157
+ TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
158
+
159
+ TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
160
+ TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
161
+
162
+ TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
163
+
164
+ TOML_WS = frozenset(' \t')
165
+ TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
166
+ TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
167
+ TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
168
+ TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
169
+
170
+ TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
171
+ {
172
+ '\\b': '\u0008', # backspace
173
+ '\\t': '\u0009', # tab
174
+ '\\n': '\u000A', # linefeed
175
+ '\\f': '\u000C', # form feed
176
+ '\\r': '\u000D', # carriage return
177
+ '\\"': '\u0022', # quote
178
+ '\\\\': '\u005C', # backslash
179
+ },
180
+ )
181
+
182
+
183
+ class TomlDecodeError(ValueError):
184
+ """An error raised if a document is not valid TOML."""
185
+
186
+
187
+ def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
188
+ """Parse TOML from a binary file object."""
189
+ b = fp.read()
190
+ try:
191
+ s = b.decode()
192
+ except AttributeError:
193
+ raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
194
+ return toml_loads(s, parse_float=parse_float)
195
+
196
+
197
+ def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
198
+ """Parse TOML from a string."""
199
+
200
+ # The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
201
+ try:
202
+ src = s.replace('\r\n', '\n')
203
+ except (AttributeError, TypeError):
204
+ raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
205
+ pos = 0
206
+ out = TomlOutput(TomlNestedDict(), TomlFlags())
207
+ header: TomlKey = ()
208
+ parse_float = toml_make_safe_parse_float(parse_float)
209
+
210
+ # Parse one statement at a time (typically means one line in TOML source)
211
+ while True:
212
+ # 1. Skip line leading whitespace
213
+ pos = toml_skip_chars(src, pos, TOML_WS)
214
+
215
+ # 2. Parse rules. Expect one of the following:
216
+ # - end of file
217
+ # - end of line
218
+ # - comment
219
+ # - key/value pair
220
+ # - append dict to list (and move to its namespace)
221
+ # - create dict (and move to its namespace)
222
+ # Skip trailing whitespace when applicable.
223
+ try:
224
+ char = src[pos]
225
+ except IndexError:
226
+ break
227
+ if char == '\n':
228
+ pos += 1
229
+ continue
230
+ if char in TOML_KEY_INITIAL_CHARS:
231
+ pos = toml_key_value_rule(src, pos, out, header, parse_float)
232
+ pos = toml_skip_chars(src, pos, TOML_WS)
233
+ elif char == '[':
234
+ try:
235
+ second_char: ta.Optional[str] = src[pos + 1]
236
+ except IndexError:
237
+ second_char = None
238
+ out.flags.finalize_pending()
239
+ if second_char == '[':
240
+ pos, header = toml_create_list_rule(src, pos, out)
241
+ else:
242
+ pos, header = toml_create_dict_rule(src, pos, out)
243
+ pos = toml_skip_chars(src, pos, TOML_WS)
244
+ elif char != '#':
245
+ raise toml_suffixed_err(src, pos, 'Invalid statement')
246
+
247
+ # 3. Skip comment
248
+ pos = toml_skip_comment(src, pos)
249
+
250
+ # 4. Expect end of line or end of file
251
+ try:
252
+ char = src[pos]
253
+ except IndexError:
254
+ break
255
+ if char != '\n':
256
+ raise toml_suffixed_err(
257
+ src, pos, 'Expected newline or end of document after a statement',
258
+ )
259
+ pos += 1
260
+
261
+ return out.data.dict
262
+
263
+
264
+ class TomlFlags:
265
+ """Flags that map to parsed keys/namespaces."""
266
+
267
+ # Marks an immutable namespace (inline array or inline table).
268
+ FROZEN = 0
269
+ # Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
270
+ EXPLICIT_NEST = 1
271
+
272
+ def __init__(self) -> None:
273
+ self._flags: ta.Dict[str, dict] = {}
274
+ self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
275
+
276
+ def add_pending(self, key: TomlKey, flag: int) -> None:
277
+ self._pending_flags.add((key, flag))
278
+
279
+ def finalize_pending(self) -> None:
280
+ for key, flag in self._pending_flags:
281
+ self.set(key, flag, recursive=False)
282
+ self._pending_flags.clear()
283
+
284
+ def unset_all(self, key: TomlKey) -> None:
285
+ cont = self._flags
286
+ for k in key[:-1]:
287
+ if k not in cont:
288
+ return
289
+ cont = cont[k]['nested']
290
+ cont.pop(key[-1], None)
291
+
292
+ def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
293
+ cont = self._flags
294
+ key_parent, key_stem = key[:-1], key[-1]
295
+ for k in key_parent:
296
+ if k not in cont:
297
+ cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
298
+ cont = cont[k]['nested']
299
+ if key_stem not in cont:
300
+ cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
301
+ cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
302
+
303
+ def is_(self, key: TomlKey, flag: int) -> bool:
304
+ if not key:
305
+ return False # document root has no flags
306
+ cont = self._flags
307
+ for k in key[:-1]:
308
+ if k not in cont:
309
+ return False
310
+ inner_cont = cont[k]
311
+ if flag in inner_cont['recursive_flags']:
312
+ return True
313
+ cont = inner_cont['nested']
314
+ key_stem = key[-1]
315
+ if key_stem in cont:
316
+ cont = cont[key_stem]
317
+ return flag in cont['flags'] or flag in cont['recursive_flags']
318
+ return False
319
+
320
+
321
+ class TomlNestedDict:
322
+ def __init__(self) -> None:
323
+ # The parsed content of the TOML document
324
+ self.dict: ta.Dict[str, ta.Any] = {}
325
+
326
+ def get_or_create_nest(
327
+ self,
328
+ key: TomlKey,
329
+ *,
330
+ access_lists: bool = True,
331
+ ) -> dict:
332
+ cont: ta.Any = self.dict
333
+ for k in key:
334
+ if k not in cont:
335
+ cont[k] = {}
336
+ cont = cont[k]
337
+ if access_lists and isinstance(cont, list):
338
+ cont = cont[-1]
339
+ if not isinstance(cont, dict):
340
+ raise KeyError('There is no nest behind this key')
341
+ return cont
342
+
343
+ def append_nest_to_list(self, key: TomlKey) -> None:
344
+ cont = self.get_or_create_nest(key[:-1])
345
+ last_key = key[-1]
346
+ if last_key in cont:
347
+ list_ = cont[last_key]
348
+ if not isinstance(list_, list):
349
+ raise KeyError('An object other than list found behind this key')
350
+ list_.append({})
351
+ else:
352
+ cont[last_key] = [{}]
353
+
354
+
355
+ class TomlOutput(ta.NamedTuple):
356
+ data: TomlNestedDict
357
+ flags: TomlFlags
358
+
359
+
360
+ def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
361
+ try:
362
+ while src[pos] in chars:
363
+ pos += 1
364
+ except IndexError:
365
+ pass
366
+ return pos
367
+
368
+
369
+ def toml_skip_until(
370
+ src: str,
371
+ pos: TomlPos,
372
+ expect: str,
373
+ *,
374
+ error_on: ta.FrozenSet[str],
375
+ error_on_eof: bool,
376
+ ) -> TomlPos:
377
+ try:
378
+ new_pos = src.index(expect, pos)
379
+ except ValueError:
380
+ new_pos = len(src)
381
+ if error_on_eof:
382
+ raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
383
+
384
+ if not error_on.isdisjoint(src[pos:new_pos]):
385
+ while src[pos] not in error_on:
386
+ pos += 1
387
+ raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
388
+ return new_pos
389
+
390
+
391
+ def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
392
+ try:
393
+ char: ta.Optional[str] = src[pos]
394
+ except IndexError:
395
+ char = None
396
+ if char == '#':
397
+ return toml_skip_until(
398
+ src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
399
+ )
400
+ return pos
401
+
402
+
403
+ def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
404
+ while True:
405
+ pos_before_skip = pos
406
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
407
+ pos = toml_skip_comment(src, pos)
408
+ if pos == pos_before_skip:
409
+ return pos
410
+
411
+
412
+ def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
413
+ pos += 1 # Skip "["
414
+ pos = toml_skip_chars(src, pos, TOML_WS)
415
+ pos, key = toml_parse_key(src, pos)
416
+
417
+ if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
418
+ raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
419
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
420
+ try:
421
+ out.data.get_or_create_nest(key)
422
+ except KeyError:
423
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
424
+
425
+ if not src.startswith(']', pos):
426
+ raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
427
+ return pos + 1, key
428
+
429
+
430
+ def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
431
+ pos += 2 # Skip "[["
432
+ pos = toml_skip_chars(src, pos, TOML_WS)
433
+ pos, key = toml_parse_key(src, pos)
434
+
435
+ if out.flags.is_(key, TomlFlags.FROZEN):
436
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
437
+ # Free the namespace now that it points to another empty list item...
438
+ out.flags.unset_all(key)
439
+ # ...but this key precisely is still prohibited from table declaration
440
+ out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
441
+ try:
442
+ out.data.append_nest_to_list(key)
443
+ except KeyError:
444
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
445
+
446
+ if not src.startswith(']]', pos):
447
+ raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
448
+ return pos + 2, key
449
+
450
+
451
+ def toml_key_value_rule(
452
+ src: str,
453
+ pos: TomlPos,
454
+ out: TomlOutput,
455
+ header: TomlKey,
456
+ parse_float: TomlParseFloat,
457
+ ) -> TomlPos:
458
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
459
+ key_parent, key_stem = key[:-1], key[-1]
460
+ abs_key_parent = header + key_parent
461
+
462
+ relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
463
+ for cont_key in relative_path_cont_keys:
464
+ # Check that dotted key syntax does not redefine an existing table
465
+ if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
466
+ raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
467
+ # Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
468
+ # table sections.
469
+ out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
470
+
471
+ if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
472
+ raise toml_suffixed_err(
473
+ src,
474
+ pos,
475
+ f'Cannot mutate immutable namespace {abs_key_parent}',
476
+ )
477
+
478
+ try:
479
+ nest = out.data.get_or_create_nest(abs_key_parent)
480
+ except KeyError:
481
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
482
+ if key_stem in nest:
483
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
484
+ # Mark inline table and array namespaces recursively immutable
485
+ if isinstance(value, (dict, list)):
486
+ out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
487
+ nest[key_stem] = value
488
+ return pos
489
+
490
+
491
+ def toml_parse_key_value_pair(
492
+ src: str,
493
+ pos: TomlPos,
494
+ parse_float: TomlParseFloat,
495
+ ) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
496
+ pos, key = toml_parse_key(src, pos)
497
+ try:
498
+ char: ta.Optional[str] = src[pos]
499
+ except IndexError:
500
+ char = None
501
+ if char != '=':
502
+ raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
503
+ pos += 1
504
+ pos = toml_skip_chars(src, pos, TOML_WS)
505
+ pos, value = toml_parse_value(src, pos, parse_float)
506
+ return pos, key, value
507
+
508
+
509
+ def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
510
+ pos, key_part = toml_parse_key_part(src, pos)
511
+ key: TomlKey = (key_part,)
512
+ pos = toml_skip_chars(src, pos, TOML_WS)
513
+ while True:
514
+ try:
515
+ char: ta.Optional[str] = src[pos]
516
+ except IndexError:
517
+ char = None
518
+ if char != '.':
519
+ return pos, key
520
+ pos += 1
521
+ pos = toml_skip_chars(src, pos, TOML_WS)
522
+ pos, key_part = toml_parse_key_part(src, pos)
523
+ key += (key_part,)
524
+ pos = toml_skip_chars(src, pos, TOML_WS)
525
+
526
+
527
+ def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
528
+ try:
529
+ char: ta.Optional[str] = src[pos]
530
+ except IndexError:
531
+ char = None
532
+ if char in TOML_BARE_KEY_CHARS:
533
+ start_pos = pos
534
+ pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
535
+ return pos, src[start_pos:pos]
536
+ if char == "'":
537
+ return toml_parse_literal_str(src, pos)
538
+ if char == '"':
539
+ return toml_parse_one_line_basic_str(src, pos)
540
+ raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
541
+
542
+
543
+ def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
544
+ pos += 1
545
+ return toml_parse_basic_str(src, pos, multiline=False)
546
+
547
+
548
+ def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
549
+ pos += 1
550
+ array: list = []
551
+
552
+ pos = toml_skip_comments_and_array_ws(src, pos)
553
+ if src.startswith(']', pos):
554
+ return pos + 1, array
555
+ while True:
556
+ pos, val = toml_parse_value(src, pos, parse_float)
557
+ array.append(val)
558
+ pos = toml_skip_comments_and_array_ws(src, pos)
559
+
560
+ c = src[pos:pos + 1]
561
+ if c == ']':
562
+ return pos + 1, array
563
+ if c != ',':
564
+ raise toml_suffixed_err(src, pos, 'Unclosed array')
565
+ pos += 1
566
+
567
+ pos = toml_skip_comments_and_array_ws(src, pos)
568
+ if src.startswith(']', pos):
569
+ return pos + 1, array
570
+
571
+
572
+ def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
573
+ pos += 1
574
+ nested_dict = TomlNestedDict()
575
+ flags = TomlFlags()
576
+
577
+ pos = toml_skip_chars(src, pos, TOML_WS)
578
+ if src.startswith('}', pos):
579
+ return pos + 1, nested_dict.dict
580
+ while True:
581
+ pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
582
+ key_parent, key_stem = key[:-1], key[-1]
583
+ if flags.is_(key, TomlFlags.FROZEN):
584
+ raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
585
+ try:
586
+ nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
587
+ except KeyError:
588
+ raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
589
+ if key_stem in nest:
590
+ raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
591
+ nest[key_stem] = value
592
+ pos = toml_skip_chars(src, pos, TOML_WS)
593
+ c = src[pos:pos + 1]
594
+ if c == '}':
595
+ return pos + 1, nested_dict.dict
596
+ if c != ',':
597
+ raise toml_suffixed_err(src, pos, 'Unclosed inline table')
598
+ if isinstance(value, (dict, list)):
599
+ flags.set(key, TomlFlags.FROZEN, recursive=True)
600
+ pos += 1
601
+ pos = toml_skip_chars(src, pos, TOML_WS)
602
+
603
+
604
+ def toml_parse_basic_str_escape(
605
+ src: str,
606
+ pos: TomlPos,
607
+ *,
608
+ multiline: bool = False,
609
+ ) -> ta.Tuple[TomlPos, str]:
610
+ escape_id = src[pos:pos + 2]
611
+ pos += 2
612
+ if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
613
+ # Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
614
+ # newline.
615
+ if escape_id != '\\\n':
616
+ pos = toml_skip_chars(src, pos, TOML_WS)
617
+ try:
618
+ char = src[pos]
619
+ except IndexError:
620
+ return pos, ''
621
+ if char != '\n':
622
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
623
+ pos += 1
624
+ pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
625
+ return pos, ''
626
+ if escape_id == '\\u':
627
+ return toml_parse_hex_char(src, pos, 4)
628
+ if escape_id == '\\U':
629
+ return toml_parse_hex_char(src, pos, 8)
630
+ try:
631
+ return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
632
+ except KeyError:
633
+ raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
634
+
635
+
636
+ def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
637
+ return toml_parse_basic_str_escape(src, pos, multiline=True)
638
+
639
+
640
+ def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
641
+ hex_str = src[pos:pos + hex_len]
642
+ if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
643
+ raise toml_suffixed_err(src, pos, 'Invalid hex value')
644
+ pos += hex_len
645
+ hex_int = int(hex_str, 16)
646
+ if not toml_is_unicode_scalar_value(hex_int):
647
+ raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
648
+ return pos, chr(hex_int)
649
+
650
+
651
+ def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
652
+ pos += 1 # Skip starting apostrophe
653
+ start_pos = pos
654
+ pos = toml_skip_until(
655
+ src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
656
+ )
657
+ return pos + 1, src[start_pos:pos] # Skip ending apostrophe
658
+
659
+
660
+ def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
661
+ pos += 3
662
+ if src.startswith('\n', pos):
663
+ pos += 1
664
+
665
+ if literal:
666
+ delim = "'"
667
+ end_pos = toml_skip_until(
668
+ src,
669
+ pos,
670
+ "'''",
671
+ error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
672
+ error_on_eof=True,
673
+ )
674
+ result = src[pos:end_pos]
675
+ pos = end_pos + 3
676
+ else:
677
+ delim = '"'
678
+ pos, result = toml_parse_basic_str(src, pos, multiline=True)
679
+
680
+ # Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
681
+ if not src.startswith(delim, pos):
682
+ return pos, result
683
+ pos += 1
684
+ if not src.startswith(delim, pos):
685
+ return pos, result + delim
686
+ pos += 1
687
+ return pos, result + (delim * 2)
688
+
689
+
690
+ def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
691
+ if multiline:
692
+ error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
693
+ parse_escapes = toml_parse_basic_str_escape_multiline
694
+ else:
695
+ error_on = TOML_ILLEGAL_BASIC_STR_CHARS
696
+ parse_escapes = toml_parse_basic_str_escape
697
+ result = ''
698
+ start_pos = pos
699
+ while True:
700
+ try:
701
+ char = src[pos]
702
+ except IndexError:
703
+ raise toml_suffixed_err(src, pos, 'Unterminated string') from None
704
+ if char == '"':
705
+ if not multiline:
706
+ return pos + 1, result + src[start_pos:pos]
707
+ if src.startswith('"""', pos):
708
+ return pos + 3, result + src[start_pos:pos]
709
+ pos += 1
710
+ continue
711
+ if char == '\\':
712
+ result += src[start_pos:pos]
713
+ pos, parsed_escape = parse_escapes(src, pos)
714
+ result += parsed_escape
715
+ start_pos = pos
716
+ continue
717
+ if char in error_on:
718
+ raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
719
+ pos += 1
720
+
721
+
722
+ def toml_parse_value( # noqa: C901
723
+ src: str,
724
+ pos: TomlPos,
725
+ parse_float: TomlParseFloat,
726
+ ) -> ta.Tuple[TomlPos, ta.Any]:
727
+ try:
728
+ char: ta.Optional[str] = src[pos]
729
+ except IndexError:
730
+ char = None
731
+
732
+ # IMPORTANT: order conditions based on speed of checking and likelihood
733
+
734
+ # Basic strings
735
+ if char == '"':
736
+ if src.startswith('"""', pos):
737
+ return toml_parse_multiline_str(src, pos, literal=False)
738
+ return toml_parse_one_line_basic_str(src, pos)
739
+
740
+ # Literal strings
741
+ if char == "'":
742
+ if src.startswith("'''", pos):
743
+ return toml_parse_multiline_str(src, pos, literal=True)
744
+ return toml_parse_literal_str(src, pos)
745
+
746
+ # Booleans
747
+ if char == 't':
748
+ if src.startswith('true', pos):
749
+ return pos + 4, True
750
+ if char == 'f':
751
+ if src.startswith('false', pos):
752
+ return pos + 5, False
753
+
754
+ # Arrays
755
+ if char == '[':
756
+ return toml_parse_array(src, pos, parse_float)
757
+
758
+ # Inline tables
759
+ if char == '{':
760
+ return toml_parse_inline_table(src, pos, parse_float)
761
+
762
+ # Dates and times
763
+ datetime_match = TOML_RE_DATETIME.match(src, pos)
764
+ if datetime_match:
765
+ try:
766
+ datetime_obj = toml_match_to_datetime(datetime_match)
767
+ except ValueError as e:
768
+ raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
769
+ return datetime_match.end(), datetime_obj
770
+ localtime_match = TOML_RE_LOCALTIME.match(src, pos)
771
+ if localtime_match:
772
+ return localtime_match.end(), toml_match_to_localtime(localtime_match)
773
+
774
+ # Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
775
+ # located after handling of dates and times.
776
+ number_match = TOML_RE_NUMBER.match(src, pos)
777
+ if number_match:
778
+ return number_match.end(), toml_match_to_number(number_match, parse_float)
779
+
780
+ # Special floats
781
+ first_three = src[pos:pos + 3]
782
+ if first_three in {'inf', 'nan'}:
783
+ return pos + 3, parse_float(first_three)
784
+ first_four = src[pos:pos + 4]
785
+ if first_four in {'-inf', '+inf', '-nan', '+nan'}:
786
+ return pos + 4, parse_float(first_four)
787
+
788
+ raise toml_suffixed_err(src, pos, 'Invalid value')
789
+
790
+
791
+ def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
792
+ """Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
793
+
794
+ def coord_repr(src: str, pos: TomlPos) -> str:
795
+ if pos >= len(src):
796
+ return 'end of document'
797
+ line = src.count('\n', 0, pos) + 1
798
+ if line == 1:
799
+ column = pos + 1
800
+ else:
801
+ column = pos - src.rindex('\n', 0, pos)
802
+ return f'line {line}, column {column}'
803
+
804
+ return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
805
+
806
+
807
+ def toml_is_unicode_scalar_value(codepoint: int) -> bool:
808
+ return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
809
+
810
+
811
+ def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
812
+ """A decorator to make `parse_float` safe.
813
+
814
+ `parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
815
+ thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
816
+ """
817
+ # The default `float` callable never returns illegal types. Optimize it.
818
+ if parse_float is float:
819
+ return float
820
+
821
+ def safe_parse_float(float_str: str) -> ta.Any:
822
+ float_value = parse_float(float_str)
823
+ if isinstance(float_value, (dict, list)):
824
+ raise ValueError('parse_float must not return dicts or lists') # noqa
825
+ return float_value
826
+
827
+ return safe_parse_float