codekingpro/portable-devtools
114k
1# SPDX-License-Identifier: MIT2# SPDX-FileCopyrightText: 2021 Taneli Hukkinen3# Licensed to PSF under a Contributor Agreement.4 5from __future__ import annotations6 7from types import MappingProxyType8 9from ._re import (10 RE_DATETIME,11 RE_LOCALTIME,12 RE_NUMBER,13 match_to_datetime,14 match_to_localtime,15 match_to_number,16)17 18TYPE_CHECKING = False19if TYPE_CHECKING:20 from collections.abc import Iterable21 from typing import IO, Any22 23 from ._types import Key, ParseFloat, Pos24 25ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))26 27# Neither of these sets include quotation mark or backslash. They are28# currently handled as separate cases in the parser functions.29ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")30ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")31 32ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS33ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS34 35ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS36 37TOML_WS = frozenset(" \t")38TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")39BARE_KEY_CHARS = frozenset(40 "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "-_"41)42KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")43HEXDIGIT_CHARS = frozenset("abcdef" "ABCDEF" "0123456789")44 45BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(46 {47 "\\b": "\u0008", # backspace48 "\\t": "\u0009", # tab49 "\\n": "\u000A", # linefeed50 "\\f": "\u000C", # form feed51 "\\r": "\u000D", # carriage return52 '\\"': "\u0022", # quote53 "\\\\": "\u005C", # backslash54 }55)56 57 58class DEPRECATED_DEFAULT:59 """Sentinel to be used as default arg during deprecation60 period of TOMLDecodeError's free-form arguments."""61 62 63class TOMLDecodeError(ValueError):64 """An error raised if a document is not valid TOML.65 66 Adds the following attributes to ValueError:67 msg: The unformatted error message68 doc: The TOML document being parsed69 pos: The index of doc where parsing failed70 lineno: The line corresponding to pos71 colno: The column corresponding to pos72 """73 74 def __init__(75 self,76 msg: str = DEPRECATED_DEFAULT, # type: ignore[assignment]77 doc: str = DEPRECATED_DEFAULT, # type: ignore[assignment]78 pos: Pos = DEPRECATED_DEFAULT, # type: ignore[assignment]79 *args: Any,80 ):81 if (82 args83 or not isinstance(msg, str)84 or not isinstance(doc, str)85 or not isinstance(pos, int)86 ):87 import warnings88 89 warnings.warn(90 "Free-form arguments for TOMLDecodeError are deprecated. "91 "Please set 'msg' (str), 'doc' (str) and 'pos' (int) arguments only.",92 DeprecationWarning,93 stacklevel=2,94 )95 if pos is not DEPRECATED_DEFAULT: # type: ignore[comparison-overlap]96 args = pos, *args97 if doc is not DEPRECATED_DEFAULT: # type: ignore[comparison-overlap]98 args = doc, *args99 if msg is not DEPRECATED_DEFAULT: # type: ignore[comparison-overlap]100 args = msg, *args101 ValueError.__init__(self, *args)102 return103 104 lineno = doc.count("\n", 0, pos) + 1105 if lineno == 1:106 colno = pos + 1107 else:108 colno = pos - doc.rindex("\n", 0, pos)109 110 if pos >= len(doc):111 coord_repr = "end of document"112 else:113 coord_repr = f"line {lineno}, column {colno}"114 errmsg = f"{msg} (at {coord_repr})"115 ValueError.__init__(self, errmsg)116 117 self.msg = msg118 self.doc = doc119 self.pos = pos120 self.lineno = lineno121 self.colno = colno122 123 124def load(fp: IO[bytes], /, *, parse_float: ParseFloat = float) -> dict[str, Any]:125 """Parse TOML from a binary file object."""126 b = fp.read()127 try:128 s = b.decode()129 except AttributeError:130 raise TypeError(131 "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"132 ) from None133 return loads(s, parse_float=parse_float)134 135 136def loads(s: str, /, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa: C901137 """Parse TOML from a string."""138 139 # The spec allows converting "\r\n" to "\n", even in string140 # literals. Let's do so to simplify parsing.141 try:142 src = s.replace("\r\n", "\n")143 except (AttributeError, TypeError):144 raise TypeError(145 f"Expected str object, not '{type(s).__qualname__}'"146 ) from None147 pos = 0148 out = Output()149 header: Key = ()150 parse_float = make_safe_parse_float(parse_float)151 152 # Parse one statement at a time153 # (typically means one line in TOML source)154 while True:155 # 1. Skip line leading whitespace156 pos = skip_chars(src, pos, TOML_WS)157 158 # 2. Parse rules. Expect one of the following:159 # - end of file160 # - end of line161 # - comment162 # - key/value pair163 # - append dict to list (and move to its namespace)164 # - create dict (and move to its namespace)165 # Skip trailing whitespace when applicable.166 try:167 char = src[pos]168 except IndexError:169 break170 if char == "\n":171 pos += 1172 continue173 if char in KEY_INITIAL_CHARS:174 pos = key_value_rule(src, pos, out, header, parse_float)175 pos = skip_chars(src, pos, TOML_WS)176 elif char == "[":177 try:178 second_char: str | None = src[pos + 1]179 except IndexError:180 second_char = None181 out.flags.finalize_pending()182 if second_char == "[":183 pos, header = create_list_rule(src, pos, out)184 else:185 pos, header = create_dict_rule(src, pos, out)186 pos = skip_chars(src, pos, TOML_WS)187 elif char != "#":188 raise TOMLDecodeError("Invalid statement", src, pos)189 190 # 3. Skip comment191 pos = skip_comment(src, pos)192 193 # 4. Expect end of line or end of file194 try:195 char = src[pos]196 except IndexError:197 break198 if char != "\n":199 raise TOMLDecodeError(200 "Expected newline or end of document after a statement", src, pos201 )202 pos += 1203 204 return out.data.dict205 206 207class Flags:208 """Flags that map to parsed keys/namespaces."""209 210 # Marks an immutable namespace (inline array or inline table).211 FROZEN = 0212 # Marks a nest that has been explicitly created and can no longer213 # be opened using the "[table]" syntax.214 EXPLICIT_NEST = 1215 216 def __init__(self) -> None:217 self._flags: dict[str, dict[Any, Any]] = {}218 self._pending_flags: set[tuple[Key, int]] = set()219 220 def add_pending(self, key: Key, flag: int) -> None:221 self._pending_flags.add((key, flag))222 223 def finalize_pending(self) -> None:224 for key, flag in self._pending_flags:225 self.set(key, flag, recursive=False)226 self._pending_flags.clear()227 228 def unset_all(self, key: Key) -> None:229 cont = self._flags230 for k in key[:-1]:231 if k not in cont:232 return233 cont = cont[k]["nested"]234 cont.pop(key[-1], None)235 236 def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003237 cont = self._flags238 key_parent, key_stem = key[:-1], key[-1]239 for k in key_parent:240 if k not in cont:241 cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}242 cont = cont[k]["nested"]243 if key_stem not in cont:244 cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}}245 cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)246 247 def is_(self, key: Key, flag: int) -> bool:248 if not key:249 return False # document root has no flags250 cont = self._flags251 for k in key[:-1]:252 if k not in cont:253 return False254 inner_cont = cont[k]255 if flag in inner_cont["recursive_flags"]:256 return True257 cont = inner_cont["nested"]258 key_stem = key[-1]259 if key_stem in cont:260 cont = cont[key_stem]261 return flag in cont["flags"] or flag in cont["recursive_flags"]262 return False263 264 265class NestedDict:266 def __init__(self) -> None:267 # The parsed content of the TOML document268 self.dict: dict[str, Any] = {}269 270 def get_or_create_nest(271 self,272 key: Key,273 *,274 access_lists: bool = True,275 ) -> dict[str, Any]:276 cont: Any = self.dict277 for k in key:278 if k not in cont:279 cont[k] = {}280 cont = cont[k]281 if access_lists and isinstance(cont, list):282 cont = cont[-1]283 if not isinstance(cont, dict):284 raise KeyError("There is no nest behind this key")285 return cont # type: ignore[no-any-return]286 287 def append_nest_to_list(self, key: Key) -> None:288 cont = self.get_or_create_nest(key[:-1])289 last_key = key[-1]290 if last_key in cont:291 list_ = cont[last_key]292 if not isinstance(list_, list):293 raise KeyError("An object other than list found behind this key")294 list_.append({})295 else:296 cont[last_key] = [{}]297 298 299class Output:300 def __init__(self) -> None:301 self.data = NestedDict()302 self.flags = Flags()303 304 305def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:306 try:307 while src[pos] in chars:308 pos += 1309 except IndexError:310 pass311 return pos312 313 314def skip_until(315 src: str,316 pos: Pos,317 expect: str,318 *,319 error_on: frozenset[str],320 error_on_eof: bool,321) -> Pos:322 try:323 new_pos = src.index(expect, pos)324 except ValueError:325 new_pos = len(src)326 if error_on_eof:327 raise TOMLDecodeError(f"Expected {expect!r}", src, new_pos) from None328 329 if not error_on.isdisjoint(src[pos:new_pos]):330 while src[pos] not in error_on:331 pos += 1332 raise TOMLDecodeError(f"Found invalid character {src[pos]!r}", src, pos)333 return new_pos334 335 336def skip_comment(src: str, pos: Pos) -> Pos:337 try:338 char: str | None = src[pos]339 except IndexError:340 char = None341 if char == "#":342 return skip_until(343 src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False344 )345 return pos346 347 348def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:349 while True:350 pos_before_skip = pos351 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)352 pos = skip_comment(src, pos)353 if pos == pos_before_skip:354 return pos355 356 357def create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:358 pos += 1 # Skip "["359 pos = skip_chars(src, pos, TOML_WS)360 pos, key = parse_key(src, pos)361 362 if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):363 raise TOMLDecodeError(f"Cannot declare {key} twice", src, pos)364 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)365 try:366 out.data.get_or_create_nest(key)367 except KeyError:368 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None369 370 if not src.startswith("]", pos):371 raise TOMLDecodeError(372 "Expected ']' at the end of a table declaration", src, pos373 )374 return pos + 1, key375 376 377def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:378 pos += 2 # Skip "[["379 pos = skip_chars(src, pos, TOML_WS)380 pos, key = parse_key(src, pos)381 382 if out.flags.is_(key, Flags.FROZEN):383 raise TOMLDecodeError(f"Cannot mutate immutable namespace {key}", src, pos)384 # Free the namespace now that it points to another empty list item...385 out.flags.unset_all(key)386 # ...but this key precisely is still prohibited from table declaration387 out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)388 try:389 out.data.append_nest_to_list(key)390 except KeyError:391 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None392 393 if not src.startswith("]]", pos):394 raise TOMLDecodeError(395 "Expected ']]' at the end of an array declaration", src, pos396 )397 return pos + 2, key398 399 400def key_value_rule(401 src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat402) -> Pos:403 pos, key, value = parse_key_value_pair(src, pos, parse_float)404 key_parent, key_stem = key[:-1], key[-1]405 abs_key_parent = header + key_parent406 407 relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))408 for cont_key in relative_path_cont_keys:409 # Check that dotted key syntax does not redefine an existing table410 if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):411 raise TOMLDecodeError(f"Cannot redefine namespace {cont_key}", src, pos)412 # Containers in the relative path can't be opened with the table syntax or413 # dotted key/value syntax in following table sections.414 out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)415 416 if out.flags.is_(abs_key_parent, Flags.FROZEN):417 raise TOMLDecodeError(418 f"Cannot mutate immutable namespace {abs_key_parent}", src, pos419 )420 421 try:422 nest = out.data.get_or_create_nest(abs_key_parent)423 except KeyError:424 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None425 if key_stem in nest:426 raise TOMLDecodeError("Cannot overwrite a value", src, pos)427 # Mark inline table and array namespaces recursively immutable428 if isinstance(value, (dict, list)):429 out.flags.set(header + key, Flags.FROZEN, recursive=True)430 nest[key_stem] = value431 return pos432 433 434def parse_key_value_pair(435 src: str, pos: Pos, parse_float: ParseFloat436) -> tuple[Pos, Key, Any]:437 pos, key = parse_key(src, pos)438 try:439 char: str | None = src[pos]440 except IndexError:441 char = None442 if char != "=":443 raise TOMLDecodeError("Expected '=' after a key in a key/value pair", src, pos)444 pos += 1445 pos = skip_chars(src, pos, TOML_WS)446 pos, value = parse_value(src, pos, parse_float)447 return pos, key, value448 449 450def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:451 pos, key_part = parse_key_part(src, pos)452 key: Key = (key_part,)453 pos = skip_chars(src, pos, TOML_WS)454 while True:455 try:456 char: str | None = src[pos]457 except IndexError:458 char = None459 if char != ".":460 return pos, key461 pos += 1462 pos = skip_chars(src, pos, TOML_WS)463 pos, key_part = parse_key_part(src, pos)464 key += (key_part,)465 pos = skip_chars(src, pos, TOML_WS)466 467 468def parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:469 try:470 char: str | None = src[pos]471 except IndexError:472 char = None473 if char in BARE_KEY_CHARS:474 start_pos = pos475 pos = skip_chars(src, pos, BARE_KEY_CHARS)476 return pos, src[start_pos:pos]477 if char == "'":478 return parse_literal_str(src, pos)479 if char == '"':480 return parse_one_line_basic_str(src, pos)481 raise TOMLDecodeError("Invalid initial character for a key part", src, pos)482 483 484def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:485 pos += 1486 return parse_basic_str(src, pos, multiline=False)487 488 489def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list[Any]]:490 pos += 1491 array: list[Any] = []492 493 pos = skip_comments_and_array_ws(src, pos)494 if src.startswith("]", pos):495 return pos + 1, array496 while True:497 pos, val = parse_value(src, pos, parse_float)498 array.append(val)499 pos = skip_comments_and_array_ws(src, pos)500 501 c = src[pos : pos + 1]502 if c == "]":503 return pos + 1, array504 if c != ",":505 raise TOMLDecodeError("Unclosed array", src, pos)506 pos += 1507 508 pos = skip_comments_and_array_ws(src, pos)509 if src.startswith("]", pos):510 return pos + 1, array511 512 513def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, dict[str, Any]]:514 pos += 1515 nested_dict = NestedDict()516 flags = Flags()517 518 pos = skip_chars(src, pos, TOML_WS)519 if src.startswith("}", pos):520 return pos + 1, nested_dict.dict521 while True:522 pos, key, value = parse_key_value_pair(src, pos, parse_float)523 key_parent, key_stem = key[:-1], key[-1]524 if flags.is_(key, Flags.FROZEN):525 raise TOMLDecodeError(f"Cannot mutate immutable namespace {key}", src, pos)526 try:527 nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)528 except KeyError:529 raise TOMLDecodeError("Cannot overwrite a value", src, pos) from None530 if key_stem in nest:531 raise TOMLDecodeError(f"Duplicate inline table key {key_stem!r}", src, pos)532 nest[key_stem] = value533 pos = skip_chars(src, pos, TOML_WS)534 c = src[pos : pos + 1]535 if c == "}":536 return pos + 1, nested_dict.dict537 if c != ",":538 raise TOMLDecodeError("Unclosed inline table", src, pos)539 if isinstance(value, (dict, list)):540 flags.set(key, Flags.FROZEN, recursive=True)541 pos += 1542 pos = skip_chars(src, pos, TOML_WS)543 544 545def parse_basic_str_escape(546 src: str, pos: Pos, *, multiline: bool = False547) -> tuple[Pos, str]:548 escape_id = src[pos : pos + 2]549 pos += 2550 if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:551 # Skip whitespace until next non-whitespace character or end of552 # the doc. Error if non-whitespace is found before newline.553 if escape_id != "\\\n":554 pos = skip_chars(src, pos, TOML_WS)555 try:556 char = src[pos]557 except IndexError:558 return pos, ""559 if char != "\n":560 raise TOMLDecodeError("Unescaped '\\' in a string", src, pos)561 pos += 1562 pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)563 return pos, ""564 if escape_id == "\\u":565 return parse_hex_char(src, pos, 4)566 if escape_id == "\\U":567 return parse_hex_char(src, pos, 8)568 try:569 return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]570 except KeyError:571 raise TOMLDecodeError("Unescaped '\\' in a string", src, pos) from None572 573 574def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:575 return parse_basic_str_escape(src, pos, multiline=True)576 577 578def parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:579 hex_str = src[pos : pos + hex_len]580 if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):581 raise TOMLDecodeError("Invalid hex value", src, pos)582 pos += hex_len583 hex_int = int(hex_str, 16)584 if not is_unicode_scalar_value(hex_int):585 raise TOMLDecodeError(586 "Escaped character is not a Unicode scalar value", src, pos587 )588 return pos, chr(hex_int)589 590 591def parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:592 pos += 1 # Skip starting apostrophe593 start_pos = pos594 pos = skip_until(595 src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True596 )597 return pos + 1, src[start_pos:pos] # Skip ending apostrophe598 599 600def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:601 pos += 3602 if src.startswith("\n", pos):603 pos += 1604 605 if literal:606 delim = "'"607 end_pos = skip_until(608 src,609 pos,610 "'''",611 error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS,612 error_on_eof=True,613 )614 result = src[pos:end_pos]615 pos = end_pos + 3616 else:617 delim = '"'618 pos, result = parse_basic_str(src, pos, multiline=True)619 620 # Add at maximum two extra apostrophes/quotes if the end sequence621 # is 4 or 5 chars long instead of just 3.622 if not src.startswith(delim, pos):623 return pos, result624 pos += 1625 if not src.startswith(delim, pos):626 return pos, result + delim627 pos += 1628 return pos, result + (delim * 2)629 630 631def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:632 if multiline:633 error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS634 parse_escapes = parse_basic_str_escape_multiline635 else:636 error_on = ILLEGAL_BASIC_STR_CHARS637 parse_escapes = parse_basic_str_escape638 result = ""639 start_pos = pos640 while True:641 try:642 char = src[pos]643 except IndexError:644 raise TOMLDecodeError("Unterminated string", src, pos) from None645 if char == '"':646 if not multiline:647 return pos + 1, result + src[start_pos:pos]648 if src.startswith('"""', pos):649 return pos + 3, result + src[start_pos:pos]650 pos += 1651 continue652 if char == "\\":653 result += src[start_pos:pos]654 pos, parsed_escape = parse_escapes(src, pos)655 result += parsed_escape656 start_pos = pos657 continue658 if char in error_on:659 raise TOMLDecodeError(f"Illegal character {char!r}", src, pos)660 pos += 1661 662 663def parse_value( # noqa: C901664 src: str, pos: Pos, parse_float: ParseFloat665) -> tuple[Pos, Any]:666 try:667 char: str | None = src[pos]668 except IndexError:669 char = None670 671 # IMPORTANT: order conditions based on speed of checking and likelihood672 673 # Basic strings674 if char == '"':675 if src.startswith('"""', pos):676 return parse_multiline_str(src, pos, literal=False)677 return parse_one_line_basic_str(src, pos)678 679 # Literal strings680 if char == "'":681 if src.startswith("'''", pos):682 return parse_multiline_str(src, pos, literal=True)683 return parse_literal_str(src, pos)684 685 # Booleans686 if char == "t":687 if src.startswith("true", pos):688 return pos + 4, True689 if char == "f":690 if src.startswith("false", pos):691 return pos + 5, False692 693 # Arrays694 if char == "[":695 return parse_array(src, pos, parse_float)696 697 # Inline tables698 if char == "{":699 return parse_inline_table(src, pos, parse_float)700 701 # Dates and times702 datetime_match = RE_DATETIME.match(src, pos)703 if datetime_match:704 try:705 datetime_obj = match_to_datetime(datetime_match)706 except ValueError as e:707 raise TOMLDecodeError("Invalid date or datetime", src, pos) from e708 return datetime_match.end(), datetime_obj709 localtime_match = RE_LOCALTIME.match(src, pos)710 if localtime_match:711 return localtime_match.end(), match_to_localtime(localtime_match)712 713 # Integers and "normal" floats.714 # The regex will greedily match any type starting with a decimal715 # char, so needs to be located after handling of dates and times.716 number_match = RE_NUMBER.match(src, pos)717 if number_match:718 return number_match.end(), match_to_number(number_match, parse_float)719 720 # Special floats721 first_three = src[pos : pos + 3]722 if first_three in {"inf", "nan"}:723 return pos + 3, parse_float(first_three)724 first_four = src[pos : pos + 4]725 if first_four in {"-inf", "+inf", "-nan", "+nan"}:726 return pos + 4, parse_float(first_four)727 728 raise TOMLDecodeError("Invalid value", src, pos)729 730 731def is_unicode_scalar_value(codepoint: int) -> bool:732 return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)733 734 735def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:736 """A decorator to make `parse_float` safe.737 738 `parse_float` must not return dicts or lists, because these types739 would be mixed with parsed TOML tables and arrays, thus confusing740 the parser. The returned decorated callable raises `ValueError`741 instead of returning illegal types.742 """743 # The default `float` callable never returns illegal types. Optimize it.744 if parse_float is float:745 return float746 747 def safe_parse_float(float_str: str) -> Any:748 float_value = parse_float(float_str)749 if isinstance(float_value, (dict, list)):750 raise ValueError("parse_float must not return dicts or lists")751 return float_value752 753 return safe_parse_float754 