5858PROTOCOL = 'tywrap/1'
5959PROTOCOL_VERSION = 1
6060CODEC_VERSION = 1
61- MAX_SERIALIZE_DEPTH = 2048
61+ MAX_SERIALIZE_DEPTH = 900
62+ MAX_SERIALIZE_NODES = 1_000_000
6263_SERIALIZE_PATH_IDENTIFIER = re .compile (r'^[A-Za-z_$][\w$]*$' , re .ASCII )
6364
6465
@@ -827,6 +828,14 @@ def _check_serialize_depth(depth, path):
827828 )
828829
829830
831+ def _check_serialize_nodes (nodes , path ):
832+ if nodes > MAX_SERIALIZE_NODES :
833+ raise RuntimeError (
834+ f'Scientific envelope serialization maximum visited nodes '
835+ f'{ MAX_SERIALIZE_NODES } exceeded at { path } '
836+ )
837+
838+
830839def _serialize_scientific (obj , * , force_json_markers , torch_allow_copy , depth , path ):
831840 """Serialize a supported scientific value, or return _NO_SCIENTIFIC."""
832841 package = type (obj ).__module__ .split ('.' , 1 )[0 ]
@@ -878,6 +887,31 @@ def _invalid_key_path(base, key):
878887 return f'{ base } [{ key !r} ]'
879888
880889
890+ def _needs_serialize_visit (value ):
891+ """Return whether value needs container or scientific traversal work."""
892+ if type (value ) in (type (None ), bool , int , float , str ):
893+ return False
894+ if type (value ) in (dict , list , tuple ):
895+ return True
896+ package = type (value ).__module__ .split ('.' , 1 )[0 ]
897+ if package in ('numpy' , 'pandas' , 'scipy' , 'torch' ):
898+ return True
899+ return 'sklearn.base' in sys .modules and is_sklearn_estimator (value )
900+
901+
902+ def _serialize_leaf (value ):
903+ """Apply non-container conversions without allocating a traversal frame."""
904+ if type (value ) in (type (None ), bool , int , float , str ):
905+ return value
906+ pydantic_value = serialize_pydantic (value )
907+ if pydantic_value is not _NO_PYDANTIC :
908+ return pydantic_value
909+ stdlib_value = serialize_stdlib (value )
910+ if stdlib_value is not None :
911+ return stdlib_value
912+ return value
913+
914+
881915def serialize (obj , * , force_json_markers , torch_allow_copy = False ):
882916 """
883917 Top-level result serializer.
@@ -893,16 +927,51 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
893927 root = [None ]
894928 active_ids = set ()
895929 stack = [('visit' , obj , 0 , 'result' , root , 0 )]
930+ visited_nodes = 0
896931
897932 # Repeated aliases have value semantics and are intentionally serialized twice.
898933 while stack :
899- action , current , depth , path , parent , key = stack .pop ()
900- if action == 'exit' :
901- active_ids .remove (id (current ))
934+ frame = stack .pop ()
935+ action = frame [0 ]
936+ if action == 'dict' :
937+ _ , current , depth , path , parent , key , output , iterator = frame
938+ try :
939+ item_key , item = next (iterator )
940+ except StopIteration :
941+ active_ids .remove (id (current ))
942+ parent [key ] = output
943+ continue
944+ stack .append (frame )
945+ if not (isinstance (item_key , (str , int , float , bool )) or item_key is None ):
946+ invalid_path = _invalid_key_path (path , item_key )
947+ raise TypeError (
948+ f'keys must be str, int, float, bool or None, not '
949+ f'{ type (item_key ).__name__ } at { invalid_path } '
950+ )
951+ child_key = next (iter (json .loads (json .dumps ({item_key : None }))))
952+ child_path = _serialize_path (path , child_key )
953+ if _needs_serialize_visit (item ):
954+ stack .append (('visit' , item , depth + 1 , child_path , output , item_key ))
955+ else :
956+ output [item_key ] = _serialize_leaf (item )
902957 continue
903- if action == 'tuple' :
904- parent [key ] = tuple (current )
958+ if action == 'sequence' :
959+ _ , current , depth , path , parent , key , output , index = frame
960+ if index == len (output ):
961+ active_ids .remove (id (current ))
962+ parent [key ] = output if type (current ) is list else tuple (output )
963+ continue
964+ stack .append (('sequence' , current , depth , path , parent , key , output , index + 1 ))
965+ item = current [index ]
966+ if _needs_serialize_visit (item ):
967+ stack .append (
968+ ('visit' , item , depth + 1 , _serialize_path (path , index ), output , index )
969+ )
970+ else :
971+ output [index ] = _serialize_leaf (item )
905972 continue
973+
974+ _ , current , depth , path , parent , key = frame
906975 try :
907976 scientific = _serialize_scientific (
908977 current ,
@@ -916,49 +985,48 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
916985 raise
917986 raise RuntimeError (f'Scientific value serialization failed at { path } : { exc } ' ) from exc
918987 if scientific is not _NO_SCIENTIFIC :
988+ # Recognized envelopes are terminal containers to the JS decoder.
989+ visited_nodes += 1
990+ _check_serialize_nodes (visited_nodes , path )
991+ if scientific .get ('__tywrap__' ) == 'torch.tensor' :
992+ nested_path = _serialize_path (path , 'value' )
993+ try :
994+ visited_nodes += 1
995+ _check_serialize_nodes (visited_nodes , nested_path )
996+ except Exception as exc :
997+ if path == 'result' :
998+ raise
999+ raise RuntimeError (
1000+ f'Scientific value serialization failed at { path } : { exc } '
1001+ ) from exc
9191002 parent [key ] = scientific
9201003 continue
9211004
9221005 container_type = type (current )
9231006 if container_type in (dict , list , tuple ):
9241007 _check_serialize_depth (depth , path )
1008+ visited_nodes += 1
1009+ _check_serialize_nodes (visited_nodes , path )
9251010 current_id = id (current )
9261011 if current_id in active_ids :
9271012 raise RuntimeError (f'Circular reference detected at { path } ' )
9281013 active_ids .add (current_id )
929- stack .append (('exit' , current , depth , path , parent , key ))
9301014
9311015 if container_type is dict :
9321016 output = {}
9331017 parent [key ] = output
934- items = list (current .items ())
935- for item_key , item in reversed (items ):
936- if not (
937- isinstance (item_key , (str , int , float , bool )) or item_key is None
938- ):
939- invalid_path = _invalid_key_path (path , item_key )
940- raise TypeError (
941- f'keys must be str, int, float, bool or None, not '
942- f'{ type (item_key ).__name__ } at { invalid_path } '
943- )
944- child_key = next (iter (json .loads (json .dumps ({item_key : None }))))
945- stack .append (
946- ('visit' , item , depth + 1 , _serialize_path (path , child_key ), output , item_key )
947- )
1018+ stack .append (
1019+ ('dict' , current , depth , path , parent , key , output , iter (current .items ()))
1020+ )
9481021 continue
9491022
9501023 output = [None ] * len (current )
9511024 if container_type is list :
9521025 parent [key ] = output
953- else :
954- stack .append (('tuple' , output , depth , path , parent , key ))
955- for index in range (len (current ) - 1 , - 1 , - 1 ):
956- stack .append (
957- ('visit' , current [index ], depth + 1 , _serialize_path (path , index ), output , index )
958- )
1026+ stack .append (('sequence' , current , depth , path , parent , key , output , 0 ))
9591027 continue
9601028
961- parent [key ] = current
1029+ parent [key ] = _serialize_leaf ( current )
9621030
9631031 return root [0 ]
9641032
@@ -1082,12 +1150,10 @@ def encode_value(value, *, allow_nan):
10821150 # ("...not JSON compliant: nan"), but 3.10/3.11 emit only the canonical
10831151 # "Out of range float values are not JSON compliant". Match that phrase too
10841152 # so the typed error message is stable across versions.
1153+ nonfinite_token = re .search (r'(^|[^a-z])(nan|inf|infinity)([^a-z]|$)' , error_msg )
10851154 if (
1086- 'nan' in error_msg
1087- or 'infinity' in error_msg
1088- or 'inf' in error_msg
1089- or 'out of range float' in error_msg
1090- ):
1155+ not allow_nan and 'out of range float values are not json compliant' in error_msg
1156+ ) or nonfinite_token :
10911157 raise CodecError ('Cannot serialize NaN - NaN/Infinity not allowed in JSON' ) from exc
10921158 raise CodecError (f'JSON encoding failed: { exc } ' ) from exc
10931159 except TypeError as exc :
0 commit comments