@@ -130,6 +130,114 @@ def __str__(self):
130130 self .assertEqual (self .dumps ({'key' : obj }),
131131 '{"key": "nonascii:\\ u00e9"}' )
132132
133+ # The tests below exercise JSONEncoder.iterencode() -- the streaming
134+ # encoder. dumps()/encode() use a separate one-shot code path, so these
135+ # behaviours are not covered by the dumps()-based tests above.
136+
137+ def test_iterencode_streams_in_chunks (self ):
138+ # A non-trivial structure is yielded as several chunks, not buffered
139+ # into a single string.
140+ obj = {"key" : list (range (10 ))}
141+ chunks = list (self .json .JSONEncoder ().iterencode (obj ))
142+ self .assertGreater (len (chunks ), 1 )
143+ self .assertEqual ("" .join (chunks ), self .dumps (obj ))
144+
145+ def test_iterencode_matches_encode (self ):
146+ # The streaming iterator must produce exactly the same output as the
147+ # one-shot encoder for representative inputs and options. This cross
148+ # checks the streaming path without duplicating the encoder tests.
149+ cases = [
150+ None , True , False , 0 , - 1 , 2.5 , "txt" , "esc\" \n \t \\ " ,
151+ [], {}, [1 , [2 , [3 , []]]], {"a" : {"b" : {"c" : 1 }}},
152+ {"nums" : [1 , 2.0 , 3 ], "nested" : {"x" : [True , None ]}},
153+ list (range (50 )), {str (i ): i for i in range (20 )},
154+ ]
155+ for kw in ({}, {"indent" : 2 }, {"sort_keys" : True },
156+ {"separators" : ("," , ":" )}):
157+ enc = self .json .JSONEncoder (** kw )
158+ for obj in cases :
159+ with self .subTest (obj = obj , options = kw ):
160+ streamed = "" .join (enc .iterencode (obj ))
161+ self .assertEqual (streamed , enc .encode (obj ))
162+
163+ def test_iterencode_default_streams_container (self ):
164+ # A container returned by default() is streamed chunk-by-chunk, not
165+ # buffered into a single chunk.
166+ class Wrapped :
167+ def __init__ (self , data ):
168+ self .data = data
169+ def default (o ):
170+ if isinstance (o , Wrapped ):
171+ return o .data
172+ raise TypeError
173+ obj = Wrapped ({"a" : list (range (10 )), "b" : Wrapped ([1 , 2 , 3 ])})
174+ enc = self .json .JSONEncoder (default = default )
175+ chunks = list (enc .iterencode (obj ))
176+ self .assertGreater (len (chunks ), 1 )
177+ self .assertEqual ("" .join (chunks ), enc .encode (obj ))
178+
179+ def test_iterencode_circular_via_default (self ):
180+ # A default() result that refers back to the object passed to
181+ # default() must be reported as a circular reference.
182+ class Wrapped :
183+ pass
184+ w = Wrapped ()
185+ def default (o ):
186+ return [w ]
187+ enc = self .json .JSONEncoder (default = default )
188+ with self .assertRaisesRegex (ValueError , "Circular reference" ):
189+ list (enc .iterencode (w ))
190+
191+ def test_iterencode_dict_mutated_during_streaming (self ):
192+ # Mutating a dict mid-stream must not crash the interpreter. The C
193+ # iterator snapshots the dict's items; the Python iterator raises
194+ # RuntimeError. Either outcome is acceptable.
195+ d = {"k%d" % i : i for i in range (10 )}
196+ it = self .json .JSONEncoder ().iterencode (d )
197+ head = next (it )
198+ d .clear ()
199+ d ["late" ] = 1
200+ try :
201+ result = head + "" .join (it )
202+ except RuntimeError :
203+ return # Python backend: dict changed size during iteration
204+ # C backend: encodes the snapshot taken before the mutation.
205+ self .assertTrue (result .startswith ("{" ) and result .endswith ("}" ))
206+
207+ def test_iterencode_mapping_items_mutated_during_streaming (self ):
208+ # gh-142831: a dict subclass whose items() returns a list the mapping
209+ # retains -- shrunk mid-stream by a default() callback -- must not
210+ # crash. The encoder must snapshot into a list it owns exclusively.
211+ sentinel = object ()
212+
213+ class Evil (dict ):
214+ backing = None
215+ def items (self ):
216+ Evil .backing = list (dict .items (self ))
217+ return Evil .backing
218+
219+ def default (o ):
220+ if o is sentinel :
221+ Evil .backing .clear () # invalidate the items list mid-stream
222+ return None
223+ raise TypeError
224+
225+ d = Evil ()
226+ d ["bad" ] = sentinel # first item, so default() fires before the rest
227+ for i in range (30 ):
228+ d ["k%d" % i ] = i
229+ result = "" .join (self .json .JSONEncoder (default = default ).iterencode (d ))
230+ self .assertTrue (result .startswith ("{" ) and result .endswith ("}" ))
231+
232+ def test_iterencode_mapping_non_2_tuple_items (self ):
233+ # A mapping whose items() does not yield 2-tuples must raise rather
234+ # than crash.
235+ class Weird (dict ):
236+ def items (self ):
237+ return [(1 , 2 , 3 )]
238+ with self .assertRaises ((ValueError , TypeError )):
239+ "" .join (self .json .JSONEncoder ().iterencode (Weird ({"a" : 1 })))
240+
133241
134242class TestPyDump (TestDump , PyTest ): pass
135243
0 commit comments