|
7 | 7 | fromtest.supportimportcheck_free_after_iterating,ALWAYS_EQ,NEVER_EQ |
8 | 8 | importpickle |
9 | 9 | importcollections.abc |
| 10 | +importfunctools |
| 11 | +importcontextlib |
| 12 | +importbuiltins |
10 | 13 |
|
11 | 14 | # Test result of triple loop (too big to inline) |
12 | 15 | TRIPLETS= [(0,0,0), (0,0,1), (0,0,2), |
@@ -91,6 +94,12 @@ def __call__(self): |
91 | 94 | raiseIndexError# Emergency stop |
92 | 95 | returni |
93 | 96 |
|
| 97 | +classEmptyIterClass: |
| 98 | +def__len__(self): |
| 99 | +return0 |
| 100 | +def__getitem__(self,i): |
| 101 | +raiseStopIteration |
| 102 | + |
94 | 103 | # Main test suite |
95 | 104 |
|
96 | 105 | classTestCase(unittest.TestCase): |
@@ -238,6 +247,78 @@ def test_mutating_seq_class_exhausted_iter(self): |
238 | 247 | self.assertEqual(list(empit), [5,6]) |
239 | 248 | self.assertEqual(list(a), [0,1,2,3,4,5,6]) |
240 | 249 |
|
| 250 | +deftest_reduce_mutating_builtins_iter(self): |
| 251 | +# This is a reproducer of issue #101765 |
| 252 | +# where iter `__reduce__` calls could lead to a segfault or SystemError |
| 253 | +# depending on the order of C argument evaluation, which is undefined |
| 254 | + |
| 255 | +# Backup builtins |
| 256 | +builtins_dict=builtins.__dict__ |
| 257 | +orig= {"iter":iter,"reversed":reversed} |
| 258 | + |
| 259 | +defrun(builtin_name,item,sentinel=None): |
| 260 | +it=iter(item)ifsentinelisNoneelseiter(item,sentinel) |
| 261 | + |
| 262 | +classCustomStr: |
| 263 | +def__init__(self,name,iterator): |
| 264 | +self.name=name |
| 265 | +self.iterator=iterator |
| 266 | +def__hash__(self): |
| 267 | +returnhash(self.name) |
| 268 | +def__eq__(self,other): |
| 269 | +# Here we exhaust our iterator, possibly changing |
| 270 | +# its `it_seq` pointer to NULL |
| 271 | +# The `__reduce__` call should correctly get |
| 272 | +# the pointers after this call |
| 273 | +list(self.iterator) |
| 274 | +returnother==self.name |
| 275 | + |
| 276 | +# del is required here |
| 277 | +# to not prematurely call __eq__ from |
| 278 | +# the hash collision with the old key |
| 279 | +delbuiltins_dict[builtin_name] |
| 280 | +builtins_dict[CustomStr(builtin_name,it)]=orig[builtin_name] |
| 281 | + |
| 282 | +returnit.__reduce__() |
| 283 | + |
| 284 | +types= [ |
| 285 | + (EmptyIterClass(),), |
| 286 | + (bytes(8),), |
| 287 | + (bytearray(8),), |
| 288 | + ((1,2,3),), |
| 289 | + (lambda:0,0), |
| 290 | + (tuple[int],)# GenericAlias |
| 291 | + ] |
| 292 | + |
| 293 | +try: |
| 294 | +run_iter=functools.partial(run,"iter") |
| 295 | +# The returned value of `__reduce__` should not only be valid |
| 296 | +# but also *empty*, as `it` was exhausted during `__eq__` |
| 297 | +# i.e "xyz" returns (iter, ("",)) |
| 298 | +self.assertEqual(run_iter("xyz"), (orig["iter"], ("",))) |
| 299 | +self.assertEqual(run_iter([1,2,3]), (orig["iter"], ([],))) |
| 300 | + |
| 301 | +# _PyEval_GetBuiltin is also called for `reversed` in a branch of |
| 302 | +# listiter_reduce_general |
| 303 | +self.assertEqual( |
| 304 | +run("reversed",orig["reversed"](list(range(8)))), |
| 305 | + (iter, ([],)) |
| 306 | + ) |
| 307 | + |
| 308 | +forcaseintypes: |
| 309 | +self.assertEqual(run_iter(*case), (orig["iter"], ((),))) |
| 310 | +finally: |
| 311 | +# Restore original builtins |
| 312 | +forkey,funcinorig.items(): |
| 313 | +# need to suppress KeyErrors in case |
| 314 | +# a failed test deletes the key without setting anything |
| 315 | +withcontextlib.suppress(KeyError): |
| 316 | +# del is required here |
| 317 | +# to not invoke our custom __eq__ from |
| 318 | +# the hash collision with the old key |
| 319 | +delbuiltins_dict[key] |
| 320 | +builtins_dict[key]=func |
| 321 | + |
241 | 322 | # Test a new_style class with __iter__ but no next() method |
242 | 323 | deftest_new_style_iter_class(self): |
243 | 324 | classIterClass(object): |
|