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

497 statements  

1# SPDX-License-Identifier: MIT 

2# SPDX-FileCopyrightText: 2021 Taneli Hukkinen 

3# Licensed to PSF under a Contributor Agreement. 

4 

5from __future__ import annotations 

6 

7# Defer loading regular expressions until we actually need them in 

8# parse_value(). 

9__lazy_modules__ = ["tomli._re", "tomllib._re"] 

10 

11import sys 

12 

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) 

21 

22if sys.version_info < (3, 15): # pragma: no cover 

23 from types import MappingProxyType as frozendict 

24 

25TYPE_CHECKING = False 

26if TYPE_CHECKING: 

27 from collections.abc import Iterable 

28 from typing import IO, Any, Final 

29 

30 from ._types import Key, ParseFloat, Pos 

31 

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 

44 

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() 

51 

52ASCII_CTRL: Final = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) 

53 

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") 

58 

59ILLEGAL_LITERAL_STR_CHARS: Final = ILLEGAL_BASIC_STR_CHARS 

60ILLEGAL_MULTILINE_LITERAL_STR_CHARS: Final = ILLEGAL_MULTILINE_BASIC_STR_CHARS 

61 

62ILLEGAL_COMMENT_CHARS: Final = ILLEGAL_BASIC_STR_CHARS 

63 

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") 

71 

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) 

84 

85 

86class DEPRECATED_DEFAULT: 

87 """Sentinel to be used as default arg during deprecation 

88 period of TOMLDecodeError's free-form arguments.""" 

89 

90 

91class TOMLDecodeError(ValueError): 

92 """An error raised if a document is not valid TOML. 

93 

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 """ 

101 

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 

116 

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 

131 

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) 

137 

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) 

144 

145 self.msg = msg 

146 self.doc = doc 

147 self.pos = pos 

148 self.lineno = lineno 

149 self.colno = colno 

150 

151 

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) 

162 

163 

164def loads(__s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]: 

165 """Parse TOML from a string.""" 

166 

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) 

179 

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) 

185 

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) 

217 

218 # 3. Skip comment 

219 pos = skip_comment(src, pos) 

220 

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 

231 

232 return out.data.dict 

233 

234 

235class Flags: 

236 """Flags that map to parsed keys/namespaces.""" 

237 

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 

243 

244 def __init__(self) -> None: 

245 self._flags: dict[str, dict[Any, Any]] = {} 

246 self._pending_flags: set[tuple[Key, int]] = set() 

247 

248 def add_pending(self, key: Key, flag: int) -> None: 

249 self._pending_flags.add((key, flag)) 

250 

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() 

255 

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) 

263 

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) 

274 

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 

291 

292 

293class NestedDict: 

294 def __init__(self) -> None: 

295 # The parsed content of the TOML document 

296 self.dict: dict[str, Any] = {} 

297 

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] 

314 

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] = [{}] 

325 

326 

327class Output: 

328 def __init__(self) -> None: 

329 self.data = NestedDict() 

330 self.flags = Flags() 

331 

332 

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 

340 

341 

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 

356 

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 

362 

363 

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 

374 

375 

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 

383 

384 

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) 

389 

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 

397 

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 

403 

404 

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) 

409 

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 

420 

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 

426 

427 

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 

434 

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) 

443 

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 ) 

448 

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 

460 

461 

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 

476 

477 

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) 

498 

499 

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) 

514 

515 

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) 

519 

520 

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] = [] 

526 

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) 

534 

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 

541 

542 pos = skip_comments_and_array_ws(src, pos) 

543 if src.startswith("]", pos): 

544 return pos + 1, array 

545 

546 

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() 

553 

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) 

581 

582 

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 

612 

613 

614def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]: 

615 return parse_basic_str_escape(src, pos, multiline=True) 

616 

617 

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) 

629 

630 

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 

638 

639 

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 

644 

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) 

659 

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) 

669 

670 

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 

701 

702 

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 ) 

713 

714 try: 

715 char: str | None = src[pos] 

716 except IndexError: 

717 char = None 

718 

719 # IMPORTANT: order conditions based on speed of checking and likelihood 

720 

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) 

726 

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) 

732 

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 

740 

741 # Arrays 

742 if char == "[": 

743 return parse_array(src, pos, parse_float, nest_lvl + 1) 

744 

745 # Inline tables 

746 if char == "{": 

747 return parse_inline_table(src, pos, parse_float, nest_lvl + 1) 

748 

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) 

760 

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) 

767 

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) 

775 

776 raise TOMLDecodeError("Invalid value", src, pos) 

777 

778 

779def is_unicode_scalar_value(codepoint: int) -> bool: 

780 return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111) 

781 

782 

783def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat: 

784 """A decorator to make `parse_float` safe. 

785 

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 

794 

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 

800 

801 return safe_parse_float