| # 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) |