omdev 0.0.0.dev192__py3-none-any.whl → 0.0.0.dev193__py3-none-any.whl

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