Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/tomli/_parser.py: 73%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# SPDX-License-Identifier: MIT
2# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
3# Licensed to PSF under a Contributor Agreement.
5from __future__ import annotations
7# Defer loading regular expressions until we actually need them in
8# parse_value().
9__lazy_modules__ = ["tomli._re", "tomllib._re"]
11import sys
13from ._re import (
14 RE_DATETIME,
15 RE_LOCALTIME,
16 RE_NUMBER,
17 match_to_datetime,
18 match_to_localtime,
19 match_to_number,
20)
22if sys.version_info < (3, 15): # pragma: no cover
23 from types import MappingProxyType as frozendict
25TYPE_CHECKING = False
26if TYPE_CHECKING:
27 from collections.abc import Iterable
28 from typing import IO, Any, Final
30 from ._types import Key, ParseFloat, Pos
32# Inline tables/arrays are implemented using recursion. Pathologically
33# nested documents cause pure Python to raise RecursionError (which is OK),
34# but mypyc binary wheels will crash unrecoverably (not OK). According to
35# mypyc docs this will be fixed in the future:
36# https://mypyc.readthedocs.io/en/latest/differences_from_python.html#stack-overflows
37# Before mypyc's fix is in, recursion needs to be limited by this library.
38# Choosing 400 as maximum inline table/array nesting level, as it's 4x the
39# spec recommended minimum (https://github.com/toml-lang/toml/pull/1087)
40# (unreleased at the time of writing) and less than half of what causes an
41# unrecoverable crash in a worker (not main) thread on a python:3.13-alpine
42# image.
43MAX_INLINE_NESTING: Final = 400
45# Pathologically excessive number of parts in a key runs into quadratic
46# behavior (e.g. in Flags.is_).
47# Even if keys aren't currently parsed using recursion, they name a
48# recursive structure, so it makes sense to limit it using getrecursionlimit()
49# and RecursionError.
50MAX_KEY_PARTS: Final = sys.getrecursionlimit()
52ASCII_CTRL: Final = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
54# Neither of these sets include quotation mark or backslash. They are
55# currently handled as separate cases in the parser functions.
56ILLEGAL_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t")
57ILLEGAL_MULTILINE_BASIC_STR_CHARS: Final = ASCII_CTRL - frozenset("\t\n")
59ILLEGAL_LITERAL_STR_CHARS: Final = ILLEGAL_BASIC_STR_CHARS
60ILLEGAL_MULTILINE_LITERAL_STR_CHARS: Final = ILLEGAL_MULTILINE_BASIC_STR_CHARS
62ILLEGAL_COMMENT_CHARS: Final = ILLEGAL_BASIC_STR_CHARS
64TOML_WS: Final = frozenset(" \t")
65TOML_WS_AND_NEWLINE: Final = TOML_WS | frozenset("\n")
66BARE_KEY_CHARS: Final = frozenset(
67 "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "-_"
68)
69KEY_INITIAL_CHARS: Final = BARE_KEY_CHARS | frozenset("\"'")
70HEXDIGIT_CHARS: Final = frozenset("abcdef" "ABCDEF" "0123456789")
72BASIC_STR_ESCAPE_REPLACEMENTS: Final = frozendict(
73 {
74 "\\b": "\u0008", # backspace
75 "\\t": "\u0009", # tab
76 "\\n": "\u000a", # linefeed
77 "\\f": "\u000c", # form feed
78 "\\r": "\u000d", # carriage return
79 "\\e": "\u001b", # escape
80 '\\"': "\u0022", # quote
81 "\\\\": "\u005c", # backslash
82 }
83)
86class DEPRECATED_DEFAULT:
87 """Sentinel to be used as default arg during deprecation
88 period of TOMLDecodeError's free-form arguments."""
91class TOMLDecodeError(ValueError):
92 """An error raised if a document is not valid TOML.
94 Adds the following attributes to ValueError:
95 msg: The unformatted error message
96 doc: The TOML document being parsed
97 pos: The index of doc where parsing failed
98 lineno: The line corresponding to pos
99 colno: The column corresponding to pos
100 """
102 def __init__(
103 self,
104 msg: str | type[DEPRECATED_DEFAULT] = DEPRECATED_DEFAULT,
105 doc: str | type[DEPRECATED_DEFAULT] = DEPRECATED_DEFAULT,
106 pos: Pos | type[DEPRECATED_DEFAULT] = DEPRECATED_DEFAULT,
107 *args: Any,
108 ):
109 if (
110 args
111 or not isinstance(msg, str)
112 or not isinstance(doc, str)
113 or not isinstance(pos, int)
114 ):
115 import warnings
117 warnings.warn(
118 "Free-form arguments for TOMLDecodeError are deprecated. "
119 "Please set 'msg' (str), 'doc' (str) and 'pos' (int) arguments only.",
120 DeprecationWarning,
121 stacklevel=2,
122 )
123 if pos is not DEPRECATED_DEFAULT:
124 args = pos, *args
125 if doc is not DEPRECATED_DEFAULT:
126 args = doc, *args
127 if msg is not DEPRECATED_DEFAULT:
128 args = msg, *args
129 ValueError.__init__(self, *args)
130 return
132 lineno = doc.count("\n", 0, pos) + 1
133 if lineno == 1:
134 colno = pos + 1
135 else:
136 colno = pos - doc.rindex("\n", 0, pos)
138 if pos >= len(doc):
139 coord_repr = "end of document"
140 else:
141 coord_repr = f"line {lineno}, column {colno}"
142 errmsg = f"{msg} (at {coord_repr})"
143 ValueError.__init__(self, errmsg)
145 self.msg = msg
146 self.doc = doc
147 self.pos = pos
148 self.lineno = lineno
149 self.colno = colno
152def load(__fp: IO[bytes], *, parse_float: ParseFloat = float) -> dict[str, Any]:
153 """Parse TOML from a binary file object."""
154 b = __fp.read()
155 try:
156 s = b.decode()
157 except AttributeError:
158 raise TypeError(
159 "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
160 ) from None
161 return loads(s, parse_float=parse_float)
164def loads(__s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]:
165 """Parse TOML from a string."""
167 # The spec allows converting "\r\n" to "\n", even in string
168 # literals. Let's do so to simplify parsing.
169 try:
170 src = __s.replace("\r\n", "\n")
171 except (AttributeError, TypeError):
172 raise TypeError(
173 f"Expected str object, not '{type(__s).__qualname__}'"
174 ) from None
175 pos = 0
176 out = Output()
177 header: Key = ()
178 parse_float = make_safe_parse_float(parse_float)
180 # Parse one statement at a time
181 # (typically means one line in TOML source)
182 while True:
183 # 1. Skip line leading whitespace
184 pos = skip_chars(src, pos, TOML_WS)
186 # 2. Parse rules. Expect one of the following:
187 # - end of file
188 # - end of line
189 # - comment
190 # - key/value pair
191 # - append dict to list (and move to its namespace)
192 # - create dict (and move to its namespace)
193 # Skip trailing whitespace when applicable.
194 try:
195 char = src[pos]
196 except IndexError:
197 break
198 if char == "\n":
199 pos += 1
200 continue
201 if char in KEY_INITIAL_CHARS:
202 pos = key_value_rule(src, pos, out, header, parse_float)
203 pos = skip_chars(src, pos, TOML_WS)
204 elif char == "[":
205 try:
206 second_char: str | None = src[pos + 1]
207 except IndexError:
208 second_char = None
209 out.flags.finalize_pending()
210 if second_char == "[":
211 pos, header = create_list_rule(src, pos, out)
212 else:
213 pos, header = create_dict_rule(src, pos, out)
214 pos = skip_chars(src, pos, TOML_WS)
215 elif char != "#":
216 raise TOMLDecodeError("Invalid statement", src, pos)
218 # 3. Skip comment
219 pos = skip_comment(src, pos)
221 # 4. Expect end of line or end of file
222 try:
223 char = src[pos]
224 except IndexError:
225 break
226 if char != "\n":
227 raise TOMLDecodeError(
228 "Expected newline or end of document after a statement", src, pos
229 )
230 pos += 1
232 return out.data.dict
235class Flags:
236 """Flags that map to parsed keys/namespaces."""
238 # Marks an immutable namespace (inline array or inline table).
239 FROZEN: Final = 0
240 # Marks a nest that has been explicitly created and can no longer
241 # be opened using the "[table]" syntax.
242 EXPLICIT_NEST: Final = 1
244 def __init__(self) -> None:
245 self._flags: dict[str, dict[Any, Any]] = {}
246 self._pending_flags: set[tuple[Key, int]] = set()
248 def add_pending(self, key: Key, flag: int) -> None:
249 self._pending_flags.add((key, flag))
251 def finalize_pending(self) -> None:
252 for key, flag in self._pending_flags:
253 self.set(key, flag, recursive=False)
254 self._pending_flags.clear()
256 def unset_all(self, key: Key) -> None:
257 cont = self._flags
258 for k in key[:-1]:
259 if k not in cont:
260 return
261 cont = cont[k]["nested"]
262 cont.pop(key[-1], None)
264 def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003
265 cont = self._flags
266 key_parent, key_stem = key[:-1], key[-1]
267 for k in key_parent:
268 if k not in cont:
269 cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
270 cont = cont[k]["nested"]
271 if key_stem not in cont:
272 cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}}
273 cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)
275 def is_(self, key: Key, flag: int) -> bool:
276 if not key:
277 return False # document root has no flags
278 cont = self._flags
279 for k in key[:-1]:
280 if k not in cont:
281 return False
282 inner_cont = cont[k]
283 if flag in inner_cont["recursive_flags"]:
284 return True
285 cont = inner_cont["nested"]
286 key_stem = key[-1]
287 if key_stem in cont:
288 inner_cont = cont[key_stem]
289 return flag in inner_cont["flags"] or flag in inner_cont["recursive_flags"]
290 return False
293class NestedDict:
294 def __init__(self) -> None:
295 # The parsed content of the TOML document
296 self.dict: dict[str, Any] = {}
298 def get_or_create_nest(
299 self,
300 key: Key,
301 *,
302 access_lists: bool = True,
303 ) -> dict[str, Any]:
304 cont: Any = self.dict
305 for k in key:
306 if k not in cont:
307 cont[k] = {}
308 cont = cont[k]
309 if access_lists and isinstance(cont, list):
310 cont = cont[-1]
311 if not isinstance(cont, dict):
312 raise KeyError("There is no nest behind this key")
313 return cont # type: ignore[no-any-return]
315 def append_nest_to_list(self, key: Key) -> None:
316 cont = self.get_or_create_nest(key[:-1])
317 last_key = key[-1]
318 if last_key in cont:
319 list_ = cont[last_key]
320 if not isinstance(list_, list):
321 raise KeyError("An object other than list found behind this key")
322 list_.append({})
323 else:
324 cont[last_key] = [{}]
327class Output:
328 def __init__(self) -> None:
329 self.data = NestedDict()
330 self.flags = Flags()
333def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:
334 try:
335 while src[pos] in chars:
336 pos += 1
337 except IndexError:
338 pass
339 return pos
342def skip_until(
343 src: str,
344 pos: Pos,
345 expect: str,
346 *,
347 error_on: frozenset[str],
348 error_on_eof: bool,
349) -> Pos:
350 try:
351 new_pos = src.index(expect, pos)
352 except ValueError:
353 new_pos = len(src)
354 if error_on_eof:
355 raise TOMLDecodeError(f"Expected {expect!r}", src, new_pos) from None
357 if not error_on.isdisjoint(src[pos:new_pos]):
358 while src[pos] not in error_on:
359 pos += 1
360 raise TOMLDecodeError(f"Found invalid character {src[pos]!r}", src, pos)
361 return new_pos
364def skip_comment(src: str, pos: Pos) -> Pos:
365 try:
366 char: str | None = src[pos]
367 except IndexError:
368 char = None
369 if char == "#":
370 return skip_until(
371 src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False
372 )
373 return pos
376def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
377 while True:
378 pos_before_skip = pos
379 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
380 pos = skip_comment(src, pos)
381 if pos == pos_before_skip:
382 return pos
385def create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
386 pos += 1 # Skip "["
387 pos = skip_chars(src, pos, TOML_WS)
388 pos, key = parse_key(src, pos)
390 if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
391 raise TOMLDecodeError(f"Cannot declare {key} twice", src, pos)
392 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
393 try:
394 out.data.get_or_create_nest(key)
395 except KeyError:
396 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None
398 if not src.startswith("]", pos):
399 raise TOMLDecodeError(
400 "Expected ']' at the end of a table declaration", src, pos
401 )
402 return pos + 1, key
405def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
406 pos += 2 # Skip "[["
407 pos = skip_chars(src, pos, TOML_WS)
408 pos, key = parse_key(src, pos)
410 if out.flags.is_(key, Flags.FROZEN):
411 raise TOMLDecodeError(f"Cannot mutate immutable namespace {key}", src, pos)
412 # Free the namespace now that it points to another empty list item...
413 out.flags.unset_all(key)
414 # ...but this key precisely is still prohibited from table declaration
415 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
416 try:
417 out.data.append_nest_to_list(key)
418 except KeyError:
419 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None
421 if not src.startswith("]]", pos):
422 raise TOMLDecodeError(
423 "Expected ']]' at the end of an array declaration", src, pos
424 )
425 return pos + 2, key
428def key_value_rule(
429 src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat
430) -> Pos:
431 pos, key, value = parse_key_value_pair(src, pos, parse_float, nest_lvl=0)
432 key_parent, key_stem = key[:-1], key[-1]
433 abs_key_parent = header + key_parent
435 relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
436 for cont_key in relative_path_cont_keys:
437 # Check that dotted key syntax does not redefine an existing table
438 if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):
439 raise TOMLDecodeError(f"Cannot redefine namespace {cont_key}", src, pos)
440 # Containers in the relative path can't be opened with the table syntax or
441 # dotted key/value syntax in following table sections.
442 out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)
444 if out.flags.is_(abs_key_parent, Flags.FROZEN):
445 raise TOMLDecodeError(
446 f"Cannot mutate immutable namespace {abs_key_parent}", src, pos
447 )
449 try:
450 nest = out.data.get_or_create_nest(abs_key_parent)
451 except KeyError:
452 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None
453 if key_stem in nest:
454 raise TOMLDecodeError("Cannot overwrite a value", src, pos)
455 # Mark inline table and array namespaces recursively immutable
456 if isinstance(value, (dict, list)):
457 out.flags.set(header + key, Flags.FROZEN, recursive=True)
458 nest[key_stem] = value
459 return pos
462def parse_key_value_pair(
463 src: str, pos: Pos, parse_float: ParseFloat, nest_lvl: int
464) -> tuple[Pos, Key, Any]:
465 pos, key = parse_key(src, pos)
466 try:
467 char: str | None = src[pos]
468 except IndexError:
469 char = None
470 if char != "=":
471 raise TOMLDecodeError("Expected '=' after a key in a key/value pair", src, pos)
472 pos += 1
473 pos = skip_chars(src, pos, TOML_WS)
474 pos, value = parse_value(src, pos, parse_float, nest_lvl)
475 return pos, key, value
478def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:
479 pos, key_part = parse_key_part(src, pos)
480 key: Key = (key_part,)
481 pos = skip_chars(src, pos, TOML_WS)
482 while True:
483 try:
484 char: str | None = src[pos]
485 except IndexError:
486 char = None
487 if char != ".":
488 return pos, key
489 pos += 1
490 pos = skip_chars(src, pos, TOML_WS)
491 pos, key_part = parse_key_part(src, pos)
492 key += (key_part,)
493 if len(key) > MAX_KEY_PARTS:
494 raise RecursionError(
495 f"TOML key has more than the allowed {MAX_KEY_PARTS} parts"
496 )
497 pos = skip_chars(src, pos, TOML_WS)
500def parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:
501 try:
502 char: str | None = src[pos]
503 except IndexError:
504 char = None
505 if char in BARE_KEY_CHARS:
506 start_pos = pos
507 pos = skip_chars(src, pos, BARE_KEY_CHARS)
508 return pos, src[start_pos:pos]
509 if char == "'":
510 return parse_literal_str(src, pos)
511 if char == '"':
512 return parse_one_line_basic_str(src, pos)
513 raise TOMLDecodeError("Invalid initial character for a key part", src, pos)
516def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
517 pos += 1
518 return parse_basic_str(src, pos, multiline=False)
521def parse_array(
522 src: str, pos: Pos, parse_float: ParseFloat, nest_lvl: int
523) -> tuple[Pos, list[Any]]:
524 pos += 1
525 array: list[Any] = []
527 pos = skip_comments_and_array_ws(src, pos)
528 if src.startswith("]", pos):
529 return pos + 1, array
530 while True:
531 pos, val = parse_value(src, pos, parse_float, nest_lvl)
532 array.append(val)
533 pos = skip_comments_and_array_ws(src, pos)
535 c = src[pos : pos + 1]
536 if c == "]":
537 return pos + 1, array
538 if c != ",":
539 raise TOMLDecodeError("Unclosed array", src, pos)
540 pos += 1
542 pos = skip_comments_and_array_ws(src, pos)
543 if src.startswith("]", pos):
544 return pos + 1, array
547def parse_inline_table(
548 src: str, pos: Pos, parse_float: ParseFloat, nest_lvl: int
549) -> tuple[Pos, dict[str, Any]]:
550 pos += 1
551 nested_dict = NestedDict()
552 flags = Flags()
554 pos = skip_comments_and_array_ws(src, pos)
555 if src.startswith("}", pos):
556 return pos + 1, nested_dict.dict
557 while True:
558 pos, key, value = parse_key_value_pair(src, pos, parse_float, nest_lvl)
559 key_parent, key_stem = key[:-1], key[-1]
560 if flags.is_(key, Flags.FROZEN):
561 raise TOMLDecodeError(f"Cannot mutate immutable namespace {key}", src, pos)
562 try:
563 nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
564 except KeyError:
565 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None
566 if key_stem in nest:
567 raise TOMLDecodeError(f"Duplicate inline table key {key_stem!r}", src, pos)
568 nest[key_stem] = value
569 pos = skip_comments_and_array_ws(src, pos)
570 c = src[pos : pos + 1]
571 if c == "}":
572 return pos + 1, nested_dict.dict
573 if c != ",":
574 raise TOMLDecodeError("Unclosed inline table", src, pos)
575 pos += 1
576 pos = skip_comments_and_array_ws(src, pos)
577 if src.startswith("}", pos):
578 return pos + 1, nested_dict.dict
579 if isinstance(value, (dict, list)):
580 flags.set(key, Flags.FROZEN, recursive=True)
583def parse_basic_str_escape(
584 src: str, pos: Pos, *, multiline: bool = False
585) -> tuple[Pos, str]:
586 escape_id = src[pos : pos + 2]
587 pos += 2
588 if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
589 # Skip whitespace until next non-whitespace character or end of
590 # the doc. Error if non-whitespace is found before newline.
591 if escape_id != "\\\n":
592 pos = skip_chars(src, pos, TOML_WS)
593 try:
594 char = src[pos]
595 except IndexError:
596 return pos, ""
597 if char != "\n":
598 raise TOMLDecodeError("Unescaped '\\' in a string", src, pos)
599 pos += 1
600 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
601 return pos, ""
602 if escape_id == "\\x":
603 return parse_hex_char(src, pos, 2)
604 if escape_id == "\\u":
605 return parse_hex_char(src, pos, 4)
606 if escape_id == "\\U":
607 return parse_hex_char(src, pos, 8)
608 try:
609 return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
610 except KeyError:
611 raise TOMLDecodeError("Unescaped '\\' in a string", src, pos) from None
614def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:
615 return parse_basic_str_escape(src, pos, multiline=True)
618def parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:
619 hex_str = src[pos : pos + hex_len]
620 if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
621 raise TOMLDecodeError("Invalid hex value", src, pos)
622 pos += hex_len
623 hex_int = int(hex_str, 16)
624 if not is_unicode_scalar_value(hex_int):
625 raise TOMLDecodeError(
626 "Escaped character is not a Unicode scalar value", src, pos
627 )
628 return pos, chr(hex_int)
631def parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:
632 pos += 1 # Skip starting apostrophe
633 start_pos = pos
634 pos = skip_until(
635 src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True
636 )
637 return pos + 1, src[start_pos:pos] # Skip ending apostrophe
640def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:
641 pos += 3
642 if src.startswith("\n", pos):
643 pos += 1
645 if literal:
646 delim = "'"
647 end_pos = skip_until(
648 src,
649 pos,
650 "'''",
651 error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
652 error_on_eof=True,
653 )
654 result = src[pos:end_pos]
655 pos = end_pos + 3
656 else:
657 delim = '"'
658 pos, result = parse_basic_str(src, pos, multiline=True)
660 # Add at maximum two extra apostrophes/quotes if the end sequence
661 # is 4 or 5 chars long instead of just 3.
662 if not src.startswith(delim, pos):
663 return pos, result
664 pos += 1
665 if not src.startswith(delim, pos):
666 return pos, result + delim
667 pos += 1
668 return pos, result + (delim * 2)
671def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
672 if multiline:
673 error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
674 parse_escapes = parse_basic_str_escape_multiline
675 else:
676 error_on = ILLEGAL_BASIC_STR_CHARS
677 parse_escapes = parse_basic_str_escape
678 result = ""
679 start_pos = pos
680 while True:
681 try:
682 char = src[pos]
683 except IndexError:
684 raise TOMLDecodeError("Unterminated string", src, pos) from None
685 if char == '"':
686 if not multiline:
687 return pos + 1, result + src[start_pos:pos]
688 if src.startswith('"""', pos):
689 return pos + 3, result + src[start_pos:pos]
690 pos += 1
691 continue
692 if char == "\\":
693 result += src[start_pos:pos]
694 pos, parsed_escape = parse_escapes(src, pos)
695 result += parsed_escape
696 start_pos = pos
697 continue
698 if char in error_on:
699 raise TOMLDecodeError(f"Illegal character {char!r}", src, pos)
700 pos += 1
703def parse_value(
704 src: str, pos: Pos, parse_float: ParseFloat, nest_lvl: int
705) -> tuple[Pos, Any]:
706 if nest_lvl > MAX_INLINE_NESTING:
707 # Pure Python should have raised RecursionError already.
708 # This ensures mypyc binaries eventually do the same.
709 raise RecursionError( # pragma: no cover
710 "TOML inline arrays/tables are nested more than the allowed"
711 f" {MAX_INLINE_NESTING} levels"
712 )
714 try:
715 char: str | None = src[pos]
716 except IndexError:
717 char = None
719 # IMPORTANT: order conditions based on speed of checking and likelihood
721 # Basic strings
722 if char == '"':
723 if src.startswith('"""', pos):
724 return parse_multiline_str(src, pos, literal=False)
725 return parse_one_line_basic_str(src, pos)
727 # Literal strings
728 if char == "'":
729 if src.startswith("'''", pos):
730 return parse_multiline_str(src, pos, literal=True)
731 return parse_literal_str(src, pos)
733 # Booleans
734 if char == "t":
735 if src.startswith("true", pos):
736 return pos + 4, True
737 if char == "f":
738 if src.startswith("false", pos):
739 return pos + 5, False
741 # Arrays
742 if char == "[":
743 return parse_array(src, pos, parse_float, nest_lvl + 1)
745 # Inline tables
746 if char == "{":
747 return parse_inline_table(src, pos, parse_float, nest_lvl + 1)
749 # Dates and times
750 datetime_match = RE_DATETIME.match(src, pos)
751 if datetime_match:
752 try:
753 datetime_obj = match_to_datetime(datetime_match)
754 except ValueError as e:
755 raise TOMLDecodeError("Invalid date or datetime", src, pos) from e
756 return datetime_match.end(), datetime_obj
757 localtime_match = RE_LOCALTIME.match(src, pos)
758 if localtime_match:
759 return localtime_match.end(), match_to_localtime(localtime_match)
761 # Integers and "normal" floats.
762 # The regex will greedily match any type starting with a decimal
763 # char, so needs to be located after handling of dates and times.
764 number_match = RE_NUMBER.match(src, pos)
765 if number_match:
766 return number_match.end(), match_to_number(number_match, parse_float)
768 # Special floats
769 first_three = src[pos : pos + 3]
770 if first_three in {"inf", "nan"}:
771 return pos + 3, parse_float(first_three)
772 first_four = src[pos : pos + 4]
773 if first_four in {"-inf", "+inf", "-nan", "+nan"}:
774 return pos + 4, parse_float(first_four)
776 raise TOMLDecodeError("Invalid value", src, pos)
779def is_unicode_scalar_value(codepoint: int) -> bool:
780 return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
783def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
784 """A decorator to make `parse_float` safe.
786 `parse_float` must not return dicts or lists, because these types
787 would be mixed with parsed TOML tables and arrays, thus confusing
788 the parser. The returned decorated callable raises `ValueError`
789 instead of returning illegal types.
790 """
791 # The default `float` callable never returns illegal types. Optimize it.
792 if parse_float is float:
793 return float
795 def safe_parse_float(float_str: str) -> Any:
796 float_value = parse_float(float_str)
797 if isinstance(float_value, (dict, list)):
798 raise ValueError("parse_float must not return dicts or lists")
799 return float_value
801 return safe_parse_float