diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index 5559d58ca..e13eb982c 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -6,6 +6,7 @@ import doctest import unittest import weakref import inspect +import textwrap import types from test import support @@ -48,7 +49,6 @@ class SignalAndYieldFromTest(unittest.TestCase): class FinalizationTest(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_frame_resurrect(self): # A generator frame can be resurrected by a generator's finalization. def gen(): @@ -68,7 +68,7 @@ class FinalizationTest(unittest.TestCase): del frame support.gc_collect() - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False is not true def test_refcycle(self): # A generator caught in a refcycle gets finalized anyway. old_garbage = gc.garbage[:] @@ -84,7 +84,7 @@ class FinalizationTest(unittest.TestCase): g = gen() next(g) g.send(g) - self.assertGreater(sys.getrefcount(g), 2) + self.assertGreaterEqual(sys.getrefcount(g), 2) self.assertFalse(finalized) del g support.gc_collect() @@ -114,6 +114,28 @@ class FinalizationTest(unittest.TestCase): gen.send(2) self.assertEqual(cm.exception.value, 2) + @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 1 + def test_generator_resurrect(self): + # Test that a resurrected generator still has a valid gi_code + resurrected = [] + + # Resurrect a generator in a finalizer + exec(textwrap.dedent(""" + def gen(): + try: + yield + except: + resurrected.append(g) + + g = gen() + next(g) + """), {"resurrected": resurrected}) + + support.gc_collect() + + self.assertEqual(len(resurrected), 1) + self.assertIsInstance(resurrected[0].gi_code, types.CodeType) + class GeneratorTest(unittest.TestCase): @@ -248,6 +270,28 @@ class GeneratorTest(unittest.TestCase): #This should not raise loop() + def test_genexpr_only_calls_dunder_iter_once(self): + + class Iterator: + + def __init__(self): + self.val = 0 + + def __next__(self): + if self.val == 2: + raise StopIteration + self.val += 1 + return self.val + + # No __iter__ method + + class C: + + def __iter__(self): + return Iterator() + + self.assertEqual([1, 2], list(i for i in C())) + class ModifyUnderlyingIterableTest(unittest.TestCase): iterables = [ @@ -276,23 +320,27 @@ class ModifyUnderlyingIterableTest(unittest.TestCase): yield x return gen(range(10)) - def process_tests(self, get_generator): + def process_tests(self, get_generator, is_expr): + err_iterator = "'.*' object is not an iterator" + err_iterable = "'.*' object is not iterable" for obj in self.iterables: g_obj = get_generator(obj) with self.subTest(g_obj=g_obj, obj=obj): - self.assertListEqual(list(g_obj), list(obj)) + if is_expr: + self.assertRaisesRegex(TypeError, err_iterator, list, g_obj) + else: + self.assertListEqual(list(g_obj), list(obj)) g_iter = get_generator(iter(obj)) with self.subTest(g_iter=g_iter, obj=obj): self.assertListEqual(list(g_iter), list(obj)) - err_regex = "'.*' object is not iterable" for obj in self.non_iterables: g_obj = get_generator(obj) with self.subTest(g_obj=g_obj): - self.assertRaisesRegex(TypeError, err_regex, list, g_obj) + err = err_iterator if is_expr else err_iterable + self.assertRaisesRegex(TypeError, err, list, g_obj) - @unittest.expectedFailure # AssertionError: TypeError not raised by list def test_modify_f_locals(self): def modify_f_locals(g, local, obj): g.gi_frame.f_locals[local] = obj @@ -304,10 +352,9 @@ class ModifyUnderlyingIterableTest(unittest.TestCase): def get_generator_genfunc(obj): return modify_f_locals(self.genfunc(), 'it', obj) - self.process_tests(get_generator_genexpr) - self.process_tests(get_generator_genfunc) + self.process_tests(get_generator_genexpr, True) + self.process_tests(get_generator_genfunc, False) - @unittest.expectedFailure # AssertionError: "'.*' object is not iterable" does not match "'complex' object is not an iterator" def test_new_gen_from_gi_code(self): def new_gen_from_gi_code(g, obj): generator_func = types.FunctionType(g.gi_code, {}) @@ -319,8 +366,8 @@ class ModifyUnderlyingIterableTest(unittest.TestCase): def get_generator_genfunc(obj): return new_gen_from_gi_code(self.genfunc(), obj) - self.process_tests(get_generator_genexpr) - self.process_tests(get_generator_genfunc) + self.process_tests(get_generator_genexpr, True) + self.process_tests(get_generator_genfunc, False) class ExceptionTest(unittest.TestCase): @@ -451,7 +498,6 @@ class ExceptionTest(unittest.TestCase): self.assertEqual(next(g), "done") self.assertIsNone(sys.exception()) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_except_throw_bad_exception(self): class E(Exception): def __new__(cls, *args, **kwargs): @@ -543,7 +589,6 @@ class GeneratorCloseTest(unittest.TestCase): gen.send(None) self.assertIsNone(gen.close()) - @unittest.expectedFailure # AssertionError: None != 0 def test_close_return_value(self): def f(): try: @@ -590,7 +635,6 @@ class GeneratorCloseTest(unittest.TestCase): next(gen) self.assertIsNone(gen.close()) - @unittest.expectedFailure # AssertionError: None != 0 def test_close_closed(self): def f(): try: @@ -616,7 +660,6 @@ class GeneratorCloseTest(unittest.TestCase): with self.assertRaises(RuntimeError): gen.close() - @unittest.expectedFailure # AssertionError: .Foo object at 0xb400007e3c212160> is not None def test_close_releases_frame_locals(self): # See gh-118272 @@ -679,7 +722,6 @@ class GeneratorDeallocTest(unittest.TestCase): self.assertIn('a', frame_locals) self.assertEqual(frame_locals['a'], 42) - @unittest.expectedFailure # AssertionError: 'a' not found in {'frame_locals1': None} def test_frame_locals_outlive_generator(self): frame_locals1 = None @@ -828,7 +870,8 @@ class GeneratorStackTraceTest(unittest.TestCase): while frame: name = frame.f_code.co_name # Stop checking frames when we get to our test helper. - if name.startswith('check_') or name.startswith('call_'): + if (name.startswith('check_') or name.startswith('call_') + or name.startswith('test')): break names.append(name) @@ -869,9 +912,27 @@ class GeneratorStackTraceTest(unittest.TestCase): self.check_yield_from_example(call_throw) + def test_throw_with_yield_from_custom_generator(self): + + class CustomGen: + def __init__(self, test): + self.test = test + def throw(self, *args): + self.test.check_stack_names(sys._getframe(), ['throw', 'g']) + def __iter__(self): + return self + def __next__(self): + return 42 + + def g(target): + yield from target + + gen = g(CustomGen(self)) + gen.send(None) + gen.throw(RuntimeError) + class YieldFromTests(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON def test_generator_gi_yieldfrom(self): def a(): self.assertEqual(inspect.getgeneratorstate(gen_b), inspect.GEN_RUNNING) @@ -1077,7 +1138,7 @@ Specification: Generators and Exception Propagation File "", line 1, in ? File "", line 2, in g File "", line 2, in f - ZeroDivisionError: integer division or modulo by zero + ZeroDivisionError: division by zero >>> next(k) # and the generator cannot be resumed Traceback (most recent call last): File "", line 1, in ? @@ -2632,14 +2693,18 @@ Our ill-behaved code should be invoked during GC: >>> with support.catch_unraisable_exception() as cm: ... g = f() ... next(g) +... gen_repr = repr(g) ... del g ... +... cm.unraisable.err_msg == (f'Exception ignored while closing ' +... f'generator {gen_repr}') ... cm.unraisable.exc_type == RuntimeError ... "generator ignored GeneratorExit" in str(cm.unraisable.exc_value) ... cm.unraisable.exc_traceback is not None True True True +True And errors thrown during closing should propagate: @@ -2744,10 +2809,12 @@ to test. ... invoke("del failed") ... >>> with support.catch_unraisable_exception() as cm: -... l = Leaker() -... del l +... leaker = Leaker() +... del_repr = repr(type(leaker).__del__) +... del leaker ... -... cm.unraisable.object == Leaker.__del__ +... cm.unraisable.err_msg == (f'Exception ignored while ' +... f'calling deallocator {del_repr}') ... cm.unraisable.exc_type == RuntimeError ... str(cm.unraisable.exc_value) == "del failed" ... cm.unraisable.exc_traceback is not None @@ -2774,7 +2841,7 @@ __test__ = {"tut": tutorial_tests, } def load_tests(loader, tests, pattern): - # tests.addTest(doctest.DocTestSuite()) # TODO: RUSTPYTHON + tests.addTest(doctest.DocTestSuite()) return tests