66
77from collections .abc import Iterable
88import string
9+ import sys
910from types import MappingProxyType
10- from typing import IO , Any , NamedTuple
11+ from typing import IO , Any , Final , NamedTuple
1112import warnings
1213
1314from ._re import (
2021)
2122from ._types import Key , ParseFloat , Pos
2223
24+ # Inline tables/arrays are implemented using recursion. Pathologically
25+ # nested documents cause pure Python to raise RecursionError (which is OK),
26+ # but mypyc binary wheels will crash unrecoverably (not OK). According to
27+ # mypyc docs this will be fixed in the future:
28+ # https://mypyc.readthedocs.io/en/latest/differences_from_python.html#stack-overflows
29+ # Before mypyc's fix is in, recursion needs to be limited by this library.
30+ # Choosing `sys.getrecursionlimit()` as maximum inline table/array nesting
31+ # level, as it allows more nesting than pure Python, but still seems a far
32+ # lower number than where mypyc binaries crash.
33+ MAX_INLINE_NESTING : Final = sys .getrecursionlimit ()
34+
2335ASCII_CTRL = frozenset (chr (i ) for i in range (32 )) | frozenset (chr (127 ))
2436
2537# Neither of these sets include quotation mark or backslash. They are
@@ -69,9 +81,9 @@ class TOMLDecodeError(ValueError):
6981
7082 def __init__ (
7183 self ,
72- msg : str = DEPRECATED_DEFAULT , # type: ignore[assignment]
73- doc : str = DEPRECATED_DEFAULT , # type: ignore[assignment]
74- pos : Pos = DEPRECATED_DEFAULT , # type: ignore[assignment]
84+ msg : str | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
85+ doc : str | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
86+ pos : Pos | type [ DEPRECATED_DEFAULT ] = DEPRECATED_DEFAULT ,
7587 * args : Any ,
7688 ):
7789 if (
@@ -86,11 +98,11 @@ def __init__(
8698 DeprecationWarning ,
8799 stacklevel = 2 ,
88100 )
89- if pos is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
101+ if pos is not DEPRECATED_DEFAULT :
90102 args = pos , * args
91- if doc is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
103+ if doc is not DEPRECATED_DEFAULT :
92104 args = doc , * args
93- if msg is not DEPRECATED_DEFAULT : # type: ignore[comparison-overlap]
105+ if msg is not DEPRECATED_DEFAULT :
94106 args = msg , * args
95107 ValueError .__init__ (self , * args )
96108 return
@@ -202,10 +214,10 @@ class Flags:
202214 """Flags that map to parsed keys/namespaces."""
203215
204216 # Marks an immutable namespace (inline array or inline table).
205- FROZEN = 0
217+ FROZEN : Final = 0
206218 # Marks a nest that has been explicitly created and can no longer
207219 # be opened using the "[table]" syntax.
208- EXPLICIT_NEST = 1
220+ EXPLICIT_NEST : Final = 1
209221
210222 def __init__ (self ) -> None :
211223 self ._flags : dict [str , dict ] = {}
@@ -251,8 +263,8 @@ def is_(self, key: Key, flag: int) -> bool:
251263 cont = inner_cont ["nested" ]
252264 key_stem = key [- 1 ]
253265 if key_stem in cont :
254- cont = cont [key_stem ]
255- return flag in cont ["flags" ] or flag in cont ["recursive_flags" ]
266+ inner_cont = cont [key_stem ]
267+ return flag in inner_cont ["flags" ] or flag in inner_cont ["recursive_flags" ]
256268 return False
257269
258270
@@ -393,7 +405,7 @@ def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
393405def key_value_rule (
394406 src : str , pos : Pos , out : Output , header : Key , parse_float : ParseFloat
395407) -> Pos :
396- pos , key , value = parse_key_value_pair (src , pos , parse_float )
408+ pos , key , value = parse_key_value_pair (src , pos , parse_float , nest_lvl = 0 )
397409 key_parent , key_stem = key [:- 1 ], key [- 1 ]
398410 abs_key_parent = header + key_parent
399411
@@ -425,7 +437,7 @@ def key_value_rule(
425437
426438
427439def parse_key_value_pair (
428- src : str , pos : Pos , parse_float : ParseFloat
440+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
429441) -> tuple [Pos , Key , Any ]:
430442 pos , key = parse_key (src , pos )
431443 try :
@@ -436,7 +448,7 @@ def parse_key_value_pair(
436448 raise TOMLDecodeError ("Expected '=' after a key in a key/value pair" , src , pos )
437449 pos += 1
438450 pos = skip_chars (src , pos , TOML_WS )
439- pos , value = parse_value (src , pos , parse_float )
451+ pos , value = parse_value (src , pos , parse_float , nest_lvl )
440452 return pos , key , value
441453
442454
@@ -479,15 +491,17 @@ def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
479491 return parse_basic_str (src , pos , multiline = False )
480492
481493
482- def parse_array (src : str , pos : Pos , parse_float : ParseFloat ) -> tuple [Pos , list ]:
494+ def parse_array (
495+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
496+ ) -> tuple [Pos , list ]:
483497 pos += 1
484498 array : list = []
485499
486500 pos = skip_comments_and_array_ws (src , pos )
487501 if src .startswith ("]" , pos ):
488502 return pos + 1 , array
489503 while True :
490- pos , val = parse_value (src , pos , parse_float )
504+ pos , val = parse_value (src , pos , parse_float , nest_lvl )
491505 array .append (val )
492506 pos = skip_comments_and_array_ws (src , pos )
493507
@@ -503,7 +517,9 @@ def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]
503517 return pos + 1 , array
504518
505519
506- def parse_inline_table (src : str , pos : Pos , parse_float : ParseFloat ) -> tuple [Pos , dict ]:
520+ def parse_inline_table (
521+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
522+ ) -> tuple [Pos , dict ]:
507523 pos += 1
508524 nested_dict = NestedDict ()
509525 flags = Flags ()
@@ -512,7 +528,7 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos
512528 if src .startswith ("}" , pos ):
513529 return pos + 1 , nested_dict .dict
514530 while True :
515- pos , key , value = parse_key_value_pair (src , pos , parse_float )
531+ pos , key , value = parse_key_value_pair (src , pos , parse_float , nest_lvl )
516532 key_parent , key_stem = key [:- 1 ], key [- 1 ]
517533 if flags .is_ (key , Flags .FROZEN ):
518534 raise TOMLDecodeError (f"Cannot mutate immutable namespace { key } " , src , pos )
@@ -654,8 +670,16 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
654670
655671
656672def parse_value ( # noqa: C901
657- src : str , pos : Pos , parse_float : ParseFloat
673+ src : str , pos : Pos , parse_float : ParseFloat , nest_lvl : int
658674) -> tuple [Pos , Any ]:
675+ if nest_lvl > MAX_INLINE_NESTING :
676+ # Pure Python should have raised RecursionError already.
677+ # This ensures mypyc binaries eventually do the same.
678+ raise RecursionError ( # pragma: no cover
679+ "TOML inline arrays/tables are nested more than the allowed"
680+ f" { MAX_INLINE_NESTING } levels"
681+ )
682+
659683 try :
660684 char : str | None = src [pos ]
661685 except IndexError :
@@ -685,11 +709,11 @@ def parse_value( # noqa: C901
685709
686710 # Arrays
687711 if char == "[" :
688- return parse_array (src , pos , parse_float )
712+ return parse_array (src , pos , parse_float , nest_lvl + 1 )
689713
690714 # Inline tables
691715 if char == "{" :
692- return parse_inline_table (src , pos , parse_float )
716+ return parse_inline_table (src , pos , parse_float , nest_lvl + 1 )
693717
694718 # Dates and times
695719 datetime_match = RE_DATETIME .match (src , pos )
0 commit comments