| # Test cases for generators and yield (compile and run) | |
| [case testYield] | |
| from typing import Callable, Dict, Generator, Iterable, Iterator, Tuple, Union | |
| def yield_three_times() -> Iterable[int]: | |
| yield 1 | |
| yield 2 | |
| yield 3 | |
| def yield_twice_and_return() -> Generator[int, None, int]: | |
| yield 1 | |
| yield 2 | |
| return 4 | |
| def yield_while_loop() -> Generator[int, None, int]: | |
| i = 0 | |
| while i < 5: | |
| if i == 3: | |
| return i | |
| yield i | |
| i += 1 | |
| return -1 | |
| def yield_for_loop() -> Iterable[int]: | |
| l = [i for i in range(3)] | |
| for i in l: | |
| yield i | |
| d = {k: None for k in range(3)} | |
| for k in d: | |
| yield k | |
| for i in range(3): | |
| yield i | |
| for i in range(three()): | |
| yield i | |
| def yield_with_except() -> Generator[int, None, None]: | |
| yield 10 | |
| try: | |
| return | |
| except: | |
| print('Caught exception inside generator function') | |
| def complex_yield(a: int, b: str, c: float) -> Generator[Union[str, int], None, float]: | |
| x = 2 | |
| while x < a: | |
| if x % 2 == 0: | |
| dummy_var = 1 | |
| yield str(x) + ' ' + b | |
| dummy_var = 1 | |
| else: | |
| dummy_var = 1 | |
| yield x | |
| dummy_var = 1 | |
| x += 1 | |
| return c | |
| def yield_with_default(x: bool = False) -> Iterable[int]: | |
| if x: | |
| yield 0 | |
| def yield_dict_methods(d1: Dict[int, int], | |
| d2: Dict[int, int], | |
| d3: Dict[int, int]) -> Iterable[int]: | |
| for k in d1.keys(): | |
| yield k | |
| for k, v in d2.items(): | |
| yield k | |
| yield v | |
| for v in d3.values(): | |
| yield v | |
| def three() -> int: | |
| return 3 | |
| class A(object): | |
| def __init__(self, x: int) -> None: | |
| self.x = x | |
| def generator(self) -> Iterable[int]: | |
| yield self.x | |
| def return_tuple() -> Generator[int, None, Tuple[int, int]]: | |
| yield 0 | |
| return 1, 2 | |
| def call_lambda(fn: Callable[[], None]) -> None: | |
| fn() | |
| def get_iterator() -> Iterator[str]: | |
| call_lambda(lambda: None) | |
| yield "" | |
| def yield_twice_via_generator_reuse() -> Iterator[str]: | |
| identified_imports = get_iterator() | |
| yield from identified_imports | |
| for identified_import in identified_imports: | |
| yield identified_import | |
| [file driver.py] | |
| from native import ( | |
| yield_three_times, | |
| yield_twice_and_return, | |
| yield_while_loop, | |
| yield_for_loop, | |
| yield_with_except, | |
| complex_yield, | |
| yield_with_default, | |
| A, | |
| return_tuple, | |
| yield_dict_methods, | |
| yield_twice_via_generator_reuse, | |
| ) | |
| from testutil import run_generator | |
| from collections import defaultdict | |
| assert run_generator(yield_three_times()) == ((1, 2, 3), None) | |
| assert run_generator(yield_twice_and_return()) == ((1, 2), 4) | |
| assert run_generator(yield_while_loop()) == ((0, 1, 2), 3) | |
| assert run_generator(yield_for_loop()) == (tuple(4 * [i for i in range(3)]), None) | |
| assert run_generator(yield_with_except()) == ((10,), None) | |
| assert run_generator(complex_yield(5, 'foo', 1.0)) == (('2 foo', 3, '4 foo'), 1.0) | |
| assert run_generator(yield_with_default()) == ((), None) | |
| assert run_generator(A(0).generator()) == ((0,), None) | |
| assert run_generator(return_tuple()) == ((0,), (1, 2)) | |
| assert run_generator(yield_dict_methods({}, {}, {})) == ((), None) | |
| assert run_generator(yield_dict_methods({1: 2}, {3: 4}, {5: 6})) == ((1, 3, 4, 6), None) | |
| assert run_generator(yield_twice_via_generator_reuse()) == (("",), None) | |
| dd = defaultdict(int, {0: 1}) | |
| assert run_generator(yield_dict_methods(dd, dd, dd)) == ((0, 0, 1, 1), None) | |
| for i in yield_twice_and_return(): | |
| print(i) | |
| for i in yield_while_loop(): | |
| print(i) | |
| [out] | |
| 1 | |
| 2 | |
| 0 | |
| 1 | |
| 2 | |
| [case testYieldTryFinallyWith] | |
| from typing import Generator, Any | |
| class Thing: | |
| def __init__(self, x: str) -> None: | |
| self.x = x | |
| def __enter__(self) -> str: | |
| print('enter!', self.x) | |
| if self.x == 'crash': | |
| raise Exception('ohno') | |
| return self.x | |
| def __exit__(self, x: Any, y: Any, z: Any) -> None: | |
| print('exit!', self.x, y) | |
| def yield_try_finally() -> Generator[int, None, str]: | |
| try: | |
| yield 1 | |
| yield 2 | |
| return 'lol' | |
| except Exception: | |
| raise | |
| finally: | |
| print('goodbye!') | |
| def yield_with(i: int) -> Generator[int, None, int]: | |
| with Thing('a') as x: | |
| yield 1 | |
| print("yooo?", x) | |
| if i == 0: | |
| yield 2 | |
| return 10 | |
| elif i == 1: | |
| raise Exception('exception!') | |
| return -1 | |
| [file driver.py] | |
| from native import yield_try_finally, yield_with | |
| from testutil import run_generator | |
| print(run_generator(yield_try_finally(), p=True)) | |
| print(run_generator(yield_with(0), p=True)) | |
| print(run_generator(yield_with(1), p=True)) | |
| [out] | |
| 1 | |
| 2 | |
| goodbye! | |
| ((1, 2), 'lol') | |
| enter! a | |
| 1 | |
| yooo? a | |
| 2 | |
| exit! a None | |
| ((1, 2), 10) | |
| enter! a | |
| 1 | |
| yooo? a | |
| exit! a exception! | |
| ((1,), 'exception!') | |
| [case testYieldNested] | |
| from typing import Callable, Generator, Iterator, TypeVar, overload | |
| from testutil import run_generator | |
| def normal(a: int, b: float) -> Callable: | |
| def generator(x: int, y: str) -> Generator: | |
| yield a | |
| yield b | |
| yield x | |
| yield y | |
| return generator | |
| def generator(a: int) -> Generator: | |
| def normal(x: int) -> int: | |
| return a + x | |
| for i in range(3): | |
| yield normal(i) | |
| def triple() -> Callable: | |
| def generator() -> Generator: | |
| x = 0 | |
| def inner() -> int: | |
| x += 1 | |
| return x | |
| while x < 3: | |
| yield inner() | |
| return generator | |
| def another_triple() -> Callable: | |
| def generator() -> Generator: | |
| x = 0 | |
| def inner_generator() -> Generator: | |
| x += 1 | |
| yield x | |
| yield next(inner_generator()) | |
| return generator | |
| def outer() -> Generator: | |
| def recursive(n: int) -> Generator: | |
| if n < 10: | |
| for i in range(n): | |
| yield i | |
| return | |
| for i in recursive(5): | |
| yield i | |
| return recursive(10) | |
| def test_return_nested_generator() -> None: | |
| assert run_generator(normal(1, 2.0)(3, '4.00')) == ((1, 2.0, 3, '4.00'), None) | |
| assert run_generator(generator(1)) == ((1, 2, 3), None) | |
| assert run_generator(triple()()) == ((1, 2, 3), None) | |
| assert run_generator(another_triple()()) == ((1,), None) | |
| assert run_generator(outer()) == ((0, 1, 2, 3, 4), None) | |
| def call_nested(x: int) -> list[int]: | |
| def generator() -> Iterator[int]: | |
| n = int() + 2 | |
| yield x | |
| yield n * x | |
| a = [] | |
| for x in generator(): | |
| a.append(x) | |
| return a | |
| T = TypeVar("T") | |
| def deco(f: T) -> T: | |
| return f | |
| def call_nested_decorated(x: int) -> list[int]: | |
| @deco | |
| def generator() -> Iterator[int]: | |
| n = int() + 3 | |
| yield x | |
| yield n * x | |
| a = [] | |
| for x in generator(): | |
| a.append(x) | |
| return a | |
| def call_nested_recursive(x: int) -> Iterator: | |
| def recursive(x: int) -> Iterator: | |
| if x > 0: | |
| yield from recursive(x - 1) | |
| yield x | |
| yield from recursive(x) | |
| def test_call_nested_generator_in_function() -> None: | |
| assert call_nested_decorated(5) == [5, 15] | |
| assert list(call_nested_recursive(5)) == [0, 1, 2, 3, 4, 5] | |
| [case testYieldThrow] | |
| from typing import Generator, Iterable, Any, Union | |
| from traceback import print_tb | |
| from contextlib import contextmanager | |
| import wrapsys | |
| def generator() -> Generator[int, None, Union[int, None]]: | |
| try: | |
| yield 1 | |
| yield 2 | |
| yield 3 | |
| except Exception as e: | |
| print_tb(wrapsys.exc_info()[2]) | |
| s = str(e) | |
| if s: | |
| print('caught exception with value ' + s) | |
| else: | |
| print('caught exception without value') | |
| return 0 | |
| return None | |
| def no_except() -> Iterable[int]: | |
| yield 1 | |
| yield 2 | |
| def raise_something() -> Iterable[int]: | |
| yield 1 | |
| yield 2 | |
| raise Exception('failure') | |
| def wrapper(x: Any) -> Any: | |
| return (yield from x) | |
| def foo() -> Generator[int, None, None]: | |
| try: | |
| yield 1 | |
| except Exception as e: | |
| print(str(e)) | |
| finally: | |
| print('goodbye') | |
| ctx_manager = contextmanager(foo) | |
| [file wrapsys.py] | |
| # This is a gross hack around some limitations of the test system/mypyc. | |
| from typing import Any | |
| import sys | |
| def exc_info() -> Any: | |
| return sys.exc_info() # type: ignore | |
| [file driver.py] | |
| import sys | |
| from typing import Generator, Tuple, TypeVar, Sequence | |
| from native import generator, ctx_manager, wrapper, no_except, raise_something | |
| T = TypeVar('T') | |
| U = TypeVar('U') | |
| def run_generator_and_throw(gen: Generator[T, None, U], | |
| num_times: int, | |
| value: object = None, | |
| traceback: object = None) -> Tuple[Sequence[T], U]: | |
| res = [] | |
| try: | |
| for i in range(num_times): | |
| res.append(next(gen)) | |
| if value is not None and traceback is not None: | |
| gen.throw(Exception, value, traceback) | |
| elif value is not None: | |
| gen.throw(Exception, value) | |
| else: | |
| gen.throw(Exception) | |
| except StopIteration as e: | |
| return (tuple(res), e.value) | |
| except Exception as e: | |
| return (tuple(res), str(e)) | |
| assert run_generator_and_throw(generator(), 0, 'hello') == ((), 'hello') | |
| assert run_generator_and_throw(generator(), 3) == ((1, 2, 3), 0) | |
| assert run_generator_and_throw(generator(), 2, 'some string') == ((1, 2), 0) | |
| try: | |
| raise Exception | |
| except Exception as e: | |
| tb = sys.exc_info()[2] | |
| assert run_generator_and_throw(generator(), 1, 'some other string', tb) == ((1,), 0) | |
| assert run_generator_and_throw(wrapper(generator()), 0, 'hello') == ((), 'hello') | |
| assert run_generator_and_throw(wrapper(generator()), 3) == ((1, 2, 3), 0) | |
| assert run_generator_and_throw(wrapper(generator()), 2, 'some string') == ((1, 2), 0) | |
| # Make sure we aren't leaking exc_info | |
| assert sys.exc_info()[0] is None | |
| assert run_generator_and_throw(wrapper([1, 2, 3]), 3, 'lol') == ((1, 2, 3), 'lol') | |
| assert run_generator_and_throw(wrapper(no_except()), 2, 'lol') == ((1, 2), 'lol') | |
| assert run_generator_and_throw(wrapper(raise_something()), 3) == ((1, 2), 'failure') | |
| with ctx_manager() as c: | |
| raise Exception('exception') | |
| [out] | |
| File "native.py", line 10, in generator | |
| yield 3 | |
| File "native.py", line 9, in generator | |
| yield 2 | |
| File "native.py", line 8, in generator | |
| yield 1 | |
| File "driver.py", line 31, in <module> | |
| raise Exception | |
| File "native.py", line 10, in generator | |
| yield 3 | |
| File "native.py", line 31, in wrapper | |
| return (yield from x) | |
| File "native.py", line 9, in generator | |
| yield 2 | |
| File "native.py", line 31, in wrapper | |
| return (yield from x) | |
| caught exception without value | |
| caught exception with value some string | |
| caught exception with value some other string | |
| caught exception without value | |
| caught exception with value some string | |
| exception | |
| goodbye | |
| [case testYieldSend] | |
| from typing import Generator | |
| def basic() -> Generator[int, int, int]: | |
| x = yield 1 | |
| y = yield (x + 1) | |
| return y | |
| def use_from() -> Generator[int, int, int]: | |
| return (yield from basic()) | |
| [file driver.py] | |
| from native import basic, use_from | |
| from testutil import run_generator | |
| assert run_generator(basic(), [5, 50]) == ((1, 6), 50) | |
| assert run_generator(use_from(), [5, 50]) == ((1, 6), 50) | |
| [case testYieldFrom] | |
| from typing import Generator, Iterator, List | |
| def basic() -> Iterator[int]: | |
| yield from [1, 2, 3] | |
| def call_next() -> int: | |
| x = [] # type: List[int] | |
| return next(iter(x)) | |
| def inner(b: bool) -> Generator[int, None, int]: | |
| if b: | |
| yield from [1, 2, 3] | |
| return 10 | |
| def with_return(b: bool) -> Generator[int, None, int]: | |
| x = yield from inner(b) | |
| for a in [1, 2]: | |
| pass | |
| return x | |
| [file driver.py] | |
| from native import basic, call_next, with_return | |
| from testutil import run_generator, assertRaises | |
| assert run_generator(basic()) == ((1, 2, 3), None) | |
| with assertRaises(StopIteration): | |
| call_next() | |
| assert run_generator(with_return(True)) == ((1, 2, 3), 10) | |
| assert run_generator(with_return(False)) == ((), 10) | |
| [case testNextGenerator] | |
| from typing import Iterable | |
| def f(x: int) -> int: | |
| print(x) | |
| return x | |
| def call_next_loud(l: Iterable[int], val: int) -> int: | |
| return next(i for i in l if f(i) == val) | |
| def call_next_default(l: Iterable[int], val: int) -> int: | |
| return next((i*2 for i in l if i == val), -1) | |
| def call_next_default_list(l: Iterable[int], val: int) -> int: | |
| return next((i*2 for i in l if i == val), -1) | |
| [file driver.py] | |
| from native import call_next_loud, call_next_default, call_next_default_list | |
| from testutil import assertRaises | |
| assert call_next_default([0, 1, 2], 0) == 0 | |
| assert call_next_default([0, 1, 2], 1) == 2 | |
| assert call_next_default([0, 1, 2], 2) == 4 | |
| assert call_next_default([0, 1, 2], 3) == -1 | |
| assert call_next_default([], 0) == -1 | |
| assert call_next_default_list([0, 1, 2], 0) == 0 | |
| assert call_next_default_list([0, 1, 2], 1) == 2 | |
| assert call_next_default_list([0, 1, 2], 2) == 4 | |
| assert call_next_default_list([0, 1, 2], 3) == -1 | |
| assert call_next_default_list([], 0) == -1 | |
| assert call_next_loud([0, 1, 2], 0) == 0 | |
| assert call_next_loud([0, 1, 2], 1) == 1 | |
| assert call_next_loud([0, 1, 2], 2) == 2 | |
| with assertRaises(StopIteration): | |
| call_next_loud([42], 3) | |
| with assertRaises(StopIteration): | |
| call_next_loud([], 3) | |
| [out] | |
| 0 | |
| 0 | |
| 1 | |
| 0 | |
| 1 | |
| 2 | |
| 42 | |
| [case testGeneratorSuper] | |
| from typing import Iterator, Callable, Any | |
| class A(): | |
| def testA(self) -> int: | |
| return 2 | |
| class B(A): | |
| def testB(self) -> Iterator[int]: | |
| x = super().testA() | |
| while True: | |
| yield x | |
| def testAsserts(): | |
| b = B() | |
| b_gen = b.testB() | |
| assert next(b_gen) == 2 | |
| [file driver.py] | |
| from native import testAsserts | |
| testAsserts() | |
| [case testNameClashIssues] | |
| class A: | |
| def foo(self) -> object: | |
| yield | |
| class B: | |
| def foo(self) -> object: | |
| yield | |
| class C: | |
| def foo(self) -> None: | |
| def bar(self) -> None: | |
| pass | |
| def C___foo() -> None: pass | |
| class D: | |
| def foo(self) -> None: | |
| def bar(self) -> None: | |
| pass | |
| class E: | |
| default: int | |
| switch: int | |
| [file driver.py] | |
| # really I only care it builds | |
| [case testCloseStopIterationRaised] | |
| def g() -> object: | |
| try: | |
| yield 1 | |
| except GeneratorExit: | |
| raise | |
| [file driver.py] | |
| from native import g | |
| gen = g() | |
| next(gen) | |
| gen.close() | |
| [case testCloseGeneratorExitRaised] | |
| def g() -> object: | |
| yield 1 | |
| [file driver.py] | |
| from native import g | |
| gen = g() | |
| next(gen) | |
| gen.close() | |
| [case testCloseGeneratorExitIgnored] | |
| def g() -> object: | |
| try: | |
| yield 1 | |
| except GeneratorExit: | |
| pass | |
| yield 2 | |
| [file driver.py] | |
| from native import g | |
| gen = g() | |
| next(gen) | |
| try: | |
| gen.close() | |
| except RuntimeError as e: | |
| assert str(e) == 'generator ignored GeneratorExit' | |
| else: | |
| assert False | |
| [case testCloseGeneratorRaisesAnotherException] | |
| def g() -> object: | |
| try: | |
| yield 1 | |
| except GeneratorExit: | |
| raise RuntimeError("error") | |
| [file driver.py] | |
| from native import g | |
| gen = g() | |
| next(gen) | |
| try: | |
| gen.close() | |
| except RuntimeError as e: | |
| assert str(e) == 'error' | |
| else: | |
| assert False | |
| [case testBorrowingInGeneratorNearYield] | |
| from typing import Iterator | |
| class Foo: | |
| flag = False | |
| class C: | |
| foo = Foo() | |
| def genf(self) -> Iterator[None]: | |
| self.foo.flag = True | |
| yield | |
| self.foo.flag = False | |
| def test_near_yield() -> None: | |
| c = C() | |
| for x in c.genf(): | |
| pass | |
| assert c.foo.flag == False | |
| class Box: | |
| def __init__(self, value: str) -> None: | |
| self.value = value | |
| def select(first: Box, second: Box) -> str: | |
| return first.value + second.value | |
| def walrus_gen(value: Box, replacement: Box) -> Iterator[str]: | |
| # The first argument is read before the walrus rebinds the same frame field, | |
| # but it is consumed by select() only after the rebind. | |
| yield select(value, (value := replacement)) | |
| def test_generator_attribute_borrow_killed_by_walrus() -> None: | |
| it = walrus_gen(Box("old"), Box("new")) | |
| assert next(it) == "oldnew" | |
| [case testGeneratorEarlyReturnWithBorrows] | |
| from typing import Iterator | |
| class Bar: | |
| bar = 0 | |
| class Foo: | |
| bar = Bar() | |
| def f(self) -> Iterator[int]: | |
| if self: | |
| self.bar.bar += 1 | |
| return | |
| yield 0 | |
| def test_early_return() -> None: | |
| foo = Foo() | |
| for x in foo.f(): | |
| pass | |
| assert foo.bar.bar == 1 | |
| [case testBorrowingInGeneratorInTupleAssignment] | |
| from typing import Iterator | |
| class Foo: | |
| flag1: bool | |
| flag2: bool | |
| class C: | |
| foo: Foo | |
| def genf(self) -> Iterator[None]: | |
| self.foo.flag1, self.foo.flag2 = True, True | |
| yield | |
| self.foo.flag1, self.foo.flag2 = False, False | |
| def test_generator() -> None: | |
| c = C() | |
| c.foo = Foo() | |
| gen = c.genf() | |
| next(gen) | |
| assert c.foo.flag1 == c.foo.flag2 == True | |
| assert list(gen) == [] | |
| assert c.foo.flag1 == c.foo.flag2 == False | |
| [case testYieldInFinally] | |
| from typing import Generator | |
| def finally_yield() -> Generator[str, None, str]: | |
| try: | |
| return 'test' | |
| finally: | |
| yield 'x' | |
| [file driver.py] | |
| from native import finally_yield | |
| from testutil import run_generator | |
| yields, val = run_generator(finally_yield()) | |
| assert yields == ('x',) | |
| assert val == 'test', val | |
| [case testUnreachableComprehensionNoCrash] | |
| from typing import List | |
| def list_comp() -> List[int]: | |
| if True: | |
| return [5] | |
| return [i for i in [5]] | |
| [file driver.py] | |
| from native import list_comp | |
| assert list_comp() == [5] | |
| [case testWithNative] | |
| class DummyContext: | |
| def __init__(self) -> None: | |
| self.x = 0 | |
| def __enter__(self) -> None: | |
| self.x += 1 | |
| def __exit__(self, exc_type, exc_value, exc_tb) -> None: | |
| self.x -= 1 | |
| def test_basic() -> None: | |
| context = DummyContext() | |
| with context: | |
| assert context.x == 1 | |
| assert context.x == 0 | |
| [case testYieldSpill] | |
| import sys | |
| from typing import Generator | |
| from testutil import run_generator | |
| def f() -> int: | |
| return 1 | |
| def yield_spill() -> Generator[str, int, int]: | |
| return f() + (yield "foo") | |
| def consume(first: object, second: object) -> None: | |
| pass | |
| # Bytes literals are mortal on GIL-enabled CPython builds, so a missing reference | |
| # to the spilled value is observable through sys.getrefcount(). | |
| def get_spilled_literal() -> bytes: | |
| return b"mypyc borrowed generator spill regression" | |
| def yield_with_borrowed_literal() -> Generator[None, None, None]: | |
| consume(b"mypyc borrowed generator spill regression", (yield)) | |
| def test_basic() -> None: | |
| x = run_generator(yield_spill(), [2]) | |
| yields, val = x | |
| assert yields == ('foo',) | |
| assert val == 3, val | |
| def test_borrowed_literal_spill() -> None: | |
| literal = get_spilled_literal() | |
| getrefcount = getattr(sys, "getrefcount") | |
| refcount = getrefcount(literal) | |
| assert run_generator(yield_with_borrowed_literal(), [None]) == ((None,), None) | |
| assert getrefcount(literal) == refcount | |
| [case testNestedGeneratorPrivateSpill] | |
| from typing import Generator | |
| from testutil import run_generator | |
| def make() -> str: | |
| return "left" | |
| def outer() -> Generator[str, str, str]: | |
| def nested(captured: str, private: str) -> Generator[str, str, str]: | |
| def callback() -> str: | |
| return captured | |
| private_local = private | |
| for item in [private_local]: | |
| # The loop iterator and the result of make() are compiler-generated | |
| # temporaries live across the yield. Only 'captured' stays in the | |
| # separate environment; the other source bindings are frame-private. | |
| return callback() + make() + (yield item) | |
| return "unreachable" | |
| return nested("captured", "right") | |
| def test_nested_generator_private_spill() -> None: | |
| yields, value = run_generator(outer(), ["sent"]) | |
| assert yields == ("right",) | |
| assert value == "capturedleftsent" | |
| [case testGeneratorPrivateAndEscapedBindings] | |
| from typing import Any, Callable, Generator, Iterator | |
| callbacks: list[Callable[[], int]] = [] | |
| def gen(shared: list[int], private: int) -> Generator[int, int, None]: | |
| callback = lambda: shared[0] | |
| callbacks.append(callback) | |
| private_local = private | |
| sent = yield private_local | |
| shared = [shared[0] + sent] | |
| yield callback() | |
| def make_recursive(prefix: str) -> Callable[[int], Generator[object, None, None]]: | |
| def nested(private: int) -> Generator[object, None, None]: | |
| def recurse(count: int) -> str: | |
| if count: | |
| return recurse(count - 1) | |
| return prefix | |
| yield private | |
| yield recurse(2) | |
| return nested | |
| def comprehension_lambdas() -> Iterator[list[int]]: | |
| offset = 10 | |
| functions = [lambda: i + offset for i in range(3)] | |
| yield [function() for function in functions] | |
| def test_private_and_escaped_bindings() -> None: | |
| shared = [1] | |
| g: Any = gen(shared, 7) | |
| assert next(g) == 7 | |
| assert callbacks[-1]() == 1 | |
| shared[0] = 5 | |
| assert callbacks[-1]() == 5 | |
| assert g.send(4) == 9 | |
| assert list(g) == [] | |
| assert callbacks[-1]() == 9 | |
| assert list(make_recursive("outer")(7)) == [7, "outer"] | |
| assert list(comprehension_lambdas()) == [[12, 12, 12]] | |
| [case testGeneratorOverridePrivateLocals] | |
| from typing import Generator | |
| def make_text() -> str: | |
| return "base" | |
| def make_number() -> int: | |
| return 42 | |
| class Base: | |
| def gen(self) -> Generator[None, None, object]: | |
| # The source local is private and must not alias the differently typed | |
| # local in the derived generator frame. | |
| value = make_text() | |
| yield None | |
| return value | |
| class Derived(Base): | |
| def gen(self) -> Generator[None, None, object]: | |
| value = make_number() | |
| yield None | |
| return value | |
| def run(g: Generator[None, None, object]) -> object: | |
| assert next(g) is None | |
| try: | |
| next(g) | |
| except StopIteration as e: | |
| return e.value | |
| assert False | |
| def test_generator_override_private_locals() -> None: | |
| assert run(Base().gen()) == "base" | |
| assert run(Derived().gen()) == 42 | |
| [case testGeneratorOverridePrivateSpillsAcrossModules] | |
| from typing import Generator | |
| import other_base | |
| def make_number() -> int: | |
| return 42 | |
| class Same(other_base.Same): | |
| def gen(self) -> Generator[None, None, object]: | |
| # The builder and transform spills have different native representations from the | |
| # corresponding base class spills. | |
| # Both generator frames have the same short name, but different full names. | |
| for _ in range(1): | |
| return (make_number(), (yield None))[0] | |
| return None | |
| def run(g: Generator[None, None, object]) -> object: | |
| assert next(g) is None | |
| try: | |
| next(g) | |
| except StopIteration as e: | |
| return e.value | |
| assert False | |
| def test_generator_override_private_spills_across_modules() -> None: | |
| assert run(other_base.Same().gen()) == "base" | |
| assert run(Same().gen()) == 42 | |
| [file other_base.py] | |
| from typing import Generator | |
| def make_text() -> str: | |
| return "base" | |
| class Same: | |
| def gen(self) -> Generator[None, None, object]: | |
| # The loop state and call result are live across the yield. | |
| for _ in [None]: | |
| return (make_text(), (yield None))[0] | |
| return None | |
| [case testGeneratorReuse] | |
| from typing import Iterator, Any | |
| def gen(x: list[int]) -> Iterator[list[int]]: | |
| y = [9] | |
| for z in x: | |
| yield y + [z] | |
| yield y | |
| def gen_range(n: int) -> Iterator[int]: | |
| for x in range(n): | |
| yield x | |
| def test_use_generator_multiple_times_one_at_a_time() -> None: | |
| for i in range(100): | |
| a = [] | |
| for x in gen([2, i]): | |
| a.append(x) | |
| assert a == [[9, 2], [9, i], [9]] | |
| def test_use_multiple_generator_instances_at_same_time() -> None: | |
| a = [] | |
| for x in gen([2]): | |
| a.append(x) | |
| for y in gen([3, 4]): | |
| a.append(y) | |
| assert a == [[9, 2], [9, 3], [9, 4], [9], [9], [9, 3], [9, 4], [9]] | |
| def test_use_multiple_generator_instances_at_same_time_2() -> None: | |
| a = [] | |
| for x in gen_range(2): | |
| a.append(x) | |
| b = [] | |
| for y in gen_range(3): | |
| b.append(y) | |
| c = [] | |
| for z in gen_range(4): | |
| c.append(z) | |
| assert c == [0, 1, 2, 3] | |
| assert b == [0, 1, 2] | |
| assert a == [0, 1] | |
| assert list(gen_range(5)) == list(range(5)) | |
| def gen_a(x: int) -> Iterator[int]: | |
| yield x + 1 | |
| def gen_b(x: int) -> Iterator[int]: | |
| yield x + 2 | |
| def test_generator_identities() -> None: | |
| # Sanity check: two distinct live objects can't reuse the same memory location | |
| g1 = gen_a(1) | |
| g2 = gen_a(1) | |
| assert g1 is not g2 | |
| # If two generators have non-overlapping lifetimes, they should reuse a memory location | |
| g3 = gen_b(1) | |
| id1 = id(g3) | |
| g3 = gen_b(1) | |
| assert id(g3) == id1 | |
| # More complex case of reuse: allocate other objects in between | |
| g4: Any = gen_a(1) | |
| id2 = id(g4) | |
| g4 = gen_b(1) | |
| g4 = [gen_b(n) for n in range(100)] | |
| g4 = gen_a(1) | |
| assert id(g4) == id2 | |
| [case testGeneratorReuseWithGilDisabled] | |
| import sys | |
| import threading | |
| from typing import Iterator | |
| def gen() -> Iterator[int]: | |
| yield 1 | |
| def is_gil_disabled() -> bool: | |
| return hasattr(sys, "_is_gil_enabled") and not sys._is_gil_enabled() | |
| def test_each_thread_gets_separate_instance() -> None: | |
| if not is_gil_disabled(): | |
| # This only makes sense if GIL is disabled | |
| return | |
| g = gen() | |
| id1 = id(g) | |
| id2 = 0 | |
| def run() -> None: | |
| nonlocal id2 | |
| g = gen() | |
| id2 = id(g) | |
| t = threading.Thread(target=run) | |
| t.start() | |
| t.join() | |
| # Each thread should get a separate reused instance | |
| assert id1 != id2 | |
| [case testGeneratorWithUndefinedLocalInEnvironment] | |
| from typing import Iterator | |
| from testutil import assertRaises | |
| def gen(set: bool) -> Iterator[float]: | |
| if set: | |
| y = float("-113.0") | |
| yield 1.0 | |
| yield y | |
| def test_bitmap_is_cleared_when_object_is_reused() -> None: | |
| # This updates the bitmap of the shared instance. | |
| list(gen(True)) | |
| # Ensure bitmap has been cleared. | |
| with assertRaises(AttributeError): # TODO: Should be UnboundLocalError | |
| list(gen(False)) | |
| def gen2(set: bool) -> Iterator[int]: | |
| if set: | |
| y = int("5") | |
| yield 1 | |
| yield y | |
| def test_undefined_int_in_environment() -> None: | |
| list(gen2(True)) | |
| with assertRaises(AttributeError): # TODO: Should be UnboundLocalError | |
| list(gen2(False)) | |
| [case testVariableWithSameNameAsHelperMethod] | |
| from testutil import assertRaises | |
| from typing import Iterator | |
| def gen_send() -> Iterator[int]: | |
| send = 1 | |
| yield send + 1 | |
| def gen_throw() -> Iterator[int]: | |
| throw = 42 | |
| yield throw * 2 | |
| def undefined() -> Iterator[int]: | |
| if int(): | |
| send = 1 | |
| yield send + 1 | |
| def test_same_names() -> None: | |
| assert list(gen_send()) == [2] | |
| assert list(gen_throw()) == [84] | |
| with assertRaises(AttributeError, "attribute 'send' of 'undefined_gen' undefined"): | |
| # TODO: Should be UnboundLocalError, this test verifies that the attribute name | |
| # matches the variable name in the input code, since internally it's generated | |
| # with a prefix. | |
| list(undefined()) | |
| [case testGeneratorInheritance] | |
| from typing import Iterator | |
| class Base1: | |
| def foo(self) -> Iterator[int]: | |
| yield 1 | |
| class Derived1(Base1): | |
| def foo(self) -> Iterator[int]: | |
| yield 2 | |
| yield 3 | |
| def base1_foo(b: Base1) -> list[int]: | |
| a = [] | |
| for x in b.foo(): | |
| a.append(x) | |
| return a | |
| def derived1_foo(b: Derived1) -> list[int]: | |
| a = [] | |
| for x in b.foo(): | |
| a.append(x) | |
| return a | |
| def test_generator_override() -> None: | |
| assert base1_foo(Base1()) == [1] | |
| assert base1_foo(Derived1()) == [2, 3] | |
| assert derived1_foo(Derived1()) == [2, 3] | |
| [case testGeneratorEmptyTuple] | |
| from collections.abc import Generator | |
| from typing import Optional, Union | |
| def test_compiledGeneratorEmptyTuple() -> None: | |
| jobs: Generator[Optional[str], None, None] = (_ for _ in ()) | |
| assert list(jobs) == [] | |
| [case testBorrowedFinalAttrAcrossYield] | |
| from typing import Final, Generator | |
| class C: | |
| def __init__(self, s: str) -> None: | |
| self.s: Final = s | |
| def f(c: C) -> Generator[str, str, None]: | |
| # c.s is a final attr; it must not be borrowed across the yield, which is | |
| # a suspension point in the middle of the expression. | |
| x = c.s + (yield "first") | |
| yield x | |
| def test_borrow_across_yield() -> None: | |
| for i in range(50): | |
| c = C("value" + str(i)) | |
| g = f(c) | |
| assert next(g) == "first" | |
| assert g.send("world") == "value" + str(i) + "world" | |
| def yield_from_gen(c: C) -> Generator[str, str, str]: | |
| r = yield "inner" | |
| return c.s + r | |
| def g2(c: C) -> Generator[str, str, str]: | |
| # Borrow a final attr across a yield from expression. | |
| x = c.s + (yield from yield_from_gen(c)) | |
| return x | |
| def test_borrow_across_yield_from() -> None: | |
| for i in range(50): | |
| c = C("v" + str(i)) | |
| gen = g2(c) | |
| assert next(gen) == "inner" | |
| try: | |
| gen.send("!") | |
| except StopIteration as e: | |
| assert e.value == "v" + str(i) + "v" + str(i) + "!", e.value | |
| else: | |
| assert False | |
| [typing fixtures/typing-full.pyi] | |
| [case testReentrantResume] | |
| from typing import Any, Iterator, Optional | |
| from testutil import assertRaises | |
| box: list[Optional[Iterator[int]]] = [None] | |
| def self_resuming() -> Iterator[int]: | |
| g = box[0] | |
| assert g is not None | |
| yield next(g) | |
| def test_reentrant_next() -> None: | |
| g = self_resuming() | |
| box[0] = g | |
| with assertRaises(ValueError, "generator already executing"): | |
| next(g) | |
| # The generator was left in a completed state by the propagating exception. | |
| with assertRaises(StopIteration): | |
| next(g) | |
| cbox: list[Any] = [None] | |
| async def self_sending() -> int: | |
| cbox[0].send(None) | |
| return 1 | |
| def test_reentrant_send_to_coroutine() -> None: | |
| c: Any = self_sending() | |
| cbox[0] = c | |
| with assertRaises(ValueError, "coroutine already executing"): | |
| c.send(None) | |
| [case testReentrantResumeWithSeparateEnvironment] | |
| from typing import Any, Iterator, Optional | |
| from testutil import assertRaises | |
| def outer() -> int: | |
| box: list[Any] = [] | |
| def nested() -> Iterator[int]: | |
| # Captures 'box', so the environment is a separate class and the locals | |
| # are not private to the generator object. The running flag still applies. | |
| yield next(box[0]) | |
| g = nested() | |
| box.append(g) | |
| return next(g) | |
| def test_reentrant_next_in_nested_generator() -> None: | |
| with assertRaises(ValueError, "generator already executing"): | |
| outer() | |
| class Base: | |
| def gen(self) -> Iterator[int]: | |
| yield 1 | |
| class Derived(Base): | |
| def gen(self) -> Iterator[int]: | |
| g = box[0] | |
| assert g is not None | |
| yield next(g) | |
| box: list[Optional[Iterator[int]]] = [None] | |
| def test_generator_method_override_still_works() -> None: | |
| assert list(Base().gen()) == [1] | |
| b: Base = Derived() | |
| g = b.gen() | |
| box[0] = g | |
| with assertRaises(ValueError, "generator already executing"): | |
| next(g) | |
| [case testExceptionSurvivesYieldInFinally] | |
| from typing import Iterator | |
| from testutil import assertRaises | |
| class Marker(Exception): | |
| pass | |
| def gen() -> Iterator[int]: | |
| try: | |
| raise Marker | |
| finally: | |
| yield 1 | |
| def test_exception_survives_yield_in_finally() -> None: | |
| it = gen() | |
| assert next(it) == 1 | |
| with assertRaises(Marker): | |
| next(it) | |
| [case testCrossingLocalOnExceptionOnlyPath] | |
| from typing import Iterator | |
| class InnerError(Exception): | |
| pass | |
| class AssignmentError(Exception): | |
| pass | |
| def fail_assignment() -> str: | |
| raise AssignmentError | |
| def through_handler() -> Iterator[str]: | |
| value = "old handler value" | |
| try: | |
| try: | |
| yield "first" | |
| raise InnerError | |
| except InnerError: | |
| value = fail_assignment() | |
| except AssignmentError: | |
| yield value | |
| def through_finally() -> Iterator[str]: | |
| value = "old finally value" | |
| try: | |
| try: | |
| yield "first" | |
| raise InnerError | |
| finally: | |
| value = fail_assignment() | |
| except AssignmentError: | |
| yield value | |
| def test_crossing_local_on_exception_only_path() -> None: | |
| for it, expected in [ | |
| (through_handler(), "old handler value"), | |
| (through_finally(), "old finally value"), | |
| ]: | |
| assert next(it) == "first" | |
| assert next(it) == expected | |
| [case testDelLocalInGenerator] | |
| from typing import Iterator | |
| from testutil import assertRaises | |
| def crossing(delete: bool) -> Iterator[str]: | |
| value = "crossing" | |
| yield value | |
| if delete: | |
| del value | |
| yield "pause" | |
| yield value | |
| def non_crossing(delete: bool) -> Iterator[str]: | |
| value = "local" | |
| if delete: | |
| del value | |
| yield value | |
| def test_del_local_in_generator() -> None: | |
| assert list(crossing(False)) == ["crossing", "pause", "crossing"] | |
| assert list(non_crossing(False)) == ["local"] | |
| it = crossing(True) | |
| assert next(it) == "crossing" | |
| assert next(it) == "pause" | |
| with assertRaises(AttributeError, "attribute 'value' of 'crossing_gen' undefined"): | |
| next(it) | |
| with assertRaises(UnboundLocalError, 'local variable "value" referenced before assignment'): | |
| list(non_crossing(True)) | |
| [case testYieldFromNativeGeneratorInInterpretedCode] | |
| from typing import Generator | |
| def native(n: int) -> Generator[int, int, str]: | |
| x = yield n | |
| yield x + 1 | |
| return "done" | |
| def exhausted() -> Generator[int, None, int]: | |
| yield 1 | |
| return 5 | |
| def raises_stop_iteration() -> Generator[int, None, object]: | |
| if int() == 1: | |
| yield 1 | |
| raise StopIteration("seven") | |
| def native_yield_from_stop_iteration() -> Generator[int, None, object]: | |
| x = yield from raises_stop_iteration() | |
| return x | |
| [file driver.py] | |
| from native import ( | |
| native, exhausted, raises_stop_iteration, native_yield_from_stop_iteration | |
| ) | |
| def outer(gen): | |
| x = yield from gen | |
| return x | |
| def run(gen, sends): | |
| g = outer(gen) | |
| yields = [] | |
| try: | |
| value = g.send(None) | |
| for s in sends: | |
| yields.append(value) | |
| value = g.send(s) | |
| yields.append(value) | |
| except StopIteration as e: | |
| return yields, e.value | |
| assert False, "generator did not stop" | |
| # Exercise a native generator through interpreted 'yield from', including yielding, | |
| # receiving a sent value, and returning. | |
| assert run(native(1), [10, None]) == ([1, 11], "done") | |
| # An already exhausted native generator completes with None, like a | |
| # CPython generator does. | |
| g = exhausted() | |
| assert next(g) == 1 | |
| try: | |
| next(g) | |
| except StopIteration as e: | |
| assert e.value == 5 | |
| assert run(g, []) == ([], None) | |
| # TODO: Apply the PEP 479 conversion to RuntimeError("generator raised StopIteration"). | |
| # Native generators currently treat StopIteration raised in the generator body as a | |
| # return value, so 'yield from' receives the exception's value and a 'for' loop | |
| # silently ends. | |
| assert run(raises_stop_iteration(), []) == ([], "seven") | |
| assert run(native_yield_from_stop_iteration(), []) == ([], "seven") | |
| assert list(raises_stop_iteration()) == [] | |
| [case testAmSendSlotOnNativeGenerator] | |
| from typing import Generator, Iterable | |
| def gen() -> Generator[int, int, str]: | |
| x = yield 1 | |
| return "ret" + str(x) | |
| def gen_no_yield() -> Generator[int, None, int]: | |
| if int() == 1: | |
| yield 1 | |
| return 5 | |
| def gen_raises() -> Generator[int, None, int]: | |
| if int() == 1: | |
| yield 1 | |
| raise ValueError("boom") | |
| def gen_delegate(it: Iterable[int]) -> Generator[int, None, None]: | |
| yield from it | |
| [file driver.py] | |
| from native import gen, gen_no_yield, gen_raises, gen_delegate | |
| from testutil import assertRaises, pyiter_send, PYGEN_NEXT, PYGEN_RETURN | |
| # Drive a native generator's am_send slot through PyIter_Send. The generic compiled | |
| # 'await' and 'yield from' path does the same; the native fast path calls the generator | |
| # helper directly. | |
| # Completion without yielding returns PYGEN_RETURN. | |
| assert pyiter_send(gen_no_yield(), None) == (PYGEN_RETURN, 5) | |
| # A yield returns PYGEN_NEXT; the next send completes with PYGEN_RETURN. Further sends | |
| # to the exhausted generator complete with None, like a CPython generator. | |
| g = gen() | |
| assert pyiter_send(g, None) == (PYGEN_NEXT, 1) | |
| assert pyiter_send(g, 3) == (PYGEN_RETURN, "ret3") | |
| assert pyiter_send(g, None) == (PYGEN_RETURN, None) | |
| # PYGEN_ERROR leaves the body exception pending, so ctypes propagates it. | |
| with assertRaises(ValueError, "boom"): | |
| pyiter_send(gen_raises(), None) | |
| # A closed generator completes with None. | |
| g = gen() | |
| assert pyiter_send(g, None) == (PYGEN_NEXT, 1) | |
| g.close() | |
| assert pyiter_send(g, None) == (PYGEN_RETURN, None) | |
| # Resuming a generator that is currently executing is an error. | |
| box = [] | |
| def reenter(): | |
| with assertRaises(ValueError, "generator already executing"): | |
| pyiter_send(box[0], None) | |
| yield 2 | |
| g2 = gen_delegate(reenter()) | |
| box.append(g2) | |
| assert pyiter_send(g2, None) == (PYGEN_NEXT, 2) | |
| assert pyiter_send(g2, None) == (PYGEN_RETURN, None) |