mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 03:36:18 +00:00
## Summary
cf. https://github.com/astral-sh/ruff/pull/20962
In the following code, `foo` in the comprehension was not reported as
unresolved:
```python
# error: [unresolved-reference] "Name `foo` used when not defined"
foo
foo = [
# no error!
# revealed: Divergent
reveal_type(x) for _ in () for x in [foo]
]
baz = [
# error: [unresolved-reference] "Name `baz` used when not defined"
# revealed: Unknown
reveal_type(x) for _ in () for x in [baz]
]
```
In fact, this is a more serious bug than it looks: for `foo`,
[`explicit_global_symbol` is
called](6cc3393ccd/crates/ty_python_semantic/src/types/infer/builder.rs (L8052)),
causing a symbol that should actually be `Undefined` to be reported as
being of type `Divergent`.
This PR fixes this bug. As a result, the code in
`mdtest/regression/pr_20962_comprehension_panics.md` no longer panics.
## Test Plan
`corpus\cyclic_symbol_in_comprehension.py` is added.
New tests are added in `mdtest/comprehensions/basic.md`.
---------
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Carl Meyer <carl@astral.sh>
5.5 KiB
5.5 KiB
Comprehensions
Basic comprehensions
# revealed: int
[reveal_type(x) for x in range(3)]
class Row:
def __next__(self) -> range:
return range(3)
class Table:
def __iter__(self) -> Row:
return Row()
# revealed: tuple[int, range]
[reveal_type((cell, row)) for row in Table() for cell in row]
# revealed: int
{reveal_type(x): 0 for x in range(3)}
# revealed: int
{0: reveal_type(x) for x in range(3)}
Nested comprehension
# revealed: tuple[int, int]
[[reveal_type((x, y)) for x in range(3)] for y in range(3)]
Comprehension referencing outer comprehension
class Row:
def __next__(self) -> range:
return range(3)
class Table:
def __iter__(self) -> Row:
return Row()
# revealed: tuple[int, range]
[[reveal_type((cell, row)) for cell in row] for row in Table()]
Comprehension with unbound iterable
Iterating over an unbound iterable yields Unknown:
# error: [unresolved-reference] "Name `x` used when not defined"
# revealed: Unknown
[reveal_type(z) for z in x]
# error: [not-iterable] "Object of type `int` is not iterable"
# revealed: tuple[int, Unknown]
[reveal_type((x, z)) for x in range(3) for z in x]
# error: [unresolved-reference] "Name `foo` used when not defined"
foo
foo = [
# revealed: tuple[int, Unknown]
reveal_type((x, z))
for x in range(3)
# error: [unresolved-reference] "Name `foo` used when not defined"
for z in [foo]
]
baz = [
# revealed: tuple[int, Unknown]
reveal_type((x, z))
for x in range(3)
# error: [unresolved-reference] "Name `baz` used when not defined"
for z in [baz]
]
Starred expressions
Starred expressions must be iterable
class NotIterable: ...
# This is fine:
x = [*range(3)]
# error: [not-iterable] "Object of type `NotIterable` is not iterable"
y = [*NotIterable()]
Async comprehensions
Basic
class AsyncIterator:
async def __anext__(self) -> int:
return 42
class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
async def _():
# revealed: int
[reveal_type(x) async for x in AsyncIterable()]
Invalid async comprehension
This tests that we understand that async comprehensions do not work according to the synchronous
iteration protocol
async def _():
# error: [not-iterable] "Object of type `range` is not async-iterable"
# revealed: Unknown
[reveal_type(x) async for x in range(3)]
Comprehension expression types
The type of the comprehension expression itself should reflect the inferred element type:
from typing import TypedDict, Literal
# revealed: list[int | Unknown]
reveal_type([x for x in range(10)])
# revealed: set[int | Unknown]
reveal_type({x for x in range(10)})
# revealed: dict[int | Unknown, str | Unknown]
reveal_type({x: str(x) for x in range(10)})
# revealed: list[tuple[int, Unknown | str] | Unknown]
reveal_type([(x, y) for x in range(5) for y in ["a", "b", "c"]])
squares: list[int | None] = [x**2 for x in range(10)]
reveal_type(squares) # revealed: list[int | None]
Inference for comprehensions takes the type context into account:
# Without type context:
reveal_type([x for x in [1, 2, 3]]) # revealed: list[Unknown | int]
reveal_type({x: "a" for x in [1, 2, 3]}) # revealed: dict[Unknown | int, str | Unknown]
reveal_type({str(x): x for x in [1, 2, 3]}) # revealed: dict[str | Unknown, Unknown | int]
reveal_type({x for x in [1, 2, 3]}) # revealed: set[Unknown | int]
# With type context:
xs: list[int] = [x for x in [1, 2, 3]]
reveal_type(xs) # revealed: list[int]
ys: dict[int, str] = {x: str(x) for x in [1, 2, 3]}
reveal_type(ys) # revealed: dict[int, str]
zs: set[int] = {x for x in [1, 2, 3]}
This also works for nested comprehensions:
table = [[(x, y) for x in range(3)] for y in range(3)]
reveal_type(table) # revealed: list[list[tuple[int, int] | Unknown] | Unknown]
table_with_content: list[list[tuple[int, int, str | None]]] = [[(x, y, None) for x in range(3)] for y in range(3)]
reveal_type(table_with_content) # revealed: list[list[tuple[int, int, str | None]]]
The type context is propagated down into the comprehension:
class Person(TypedDict):
name: str
# TODO: This should not error.
# error: [invalid-assignment]
persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]]
reveal_type(persons) # revealed: list[Person]
# TODO: This should be an invalid-key error.
# error: [invalid-assignment]
invalid: list[Person] = [{"misspelled": n} for n in ["Alice", "Bob"]]
We promote literals to avoid overly-precise types in invariant positions:
reveal_type([x for x in ("a", "b", "c")]) # revealed: list[str | Unknown]
reveal_type({x for x in (1, 2, 3)}) # revealed: set[int | Unknown]
reveal_type({k: 0 for k in ("a", "b", "c")}) # revealed: dict[str | Unknown, int | Unknown]
Type context can prevent this promotion from happening:
list_of_literals: list[Literal["a", "b", "c"]] = [x for x in ("a", "b", "c")]
reveal_type(list_of_literals) # revealed: list[Literal["a", "b", "c"]]
dict_with_literal_keys: dict[Literal["a", "b", "c"], int] = {k: 0 for k in ("a", "b", "c")}
reveal_type(dict_with_literal_keys) # revealed: dict[Literal["a", "b", "c"], int]
dict_with_literal_values: dict[str, Literal[1, 2, 3]] = {str(k): k for k in (1, 2, 3)}
reveal_type(dict_with_literal_values) # revealed: dict[str, Literal[1, 2, 3]]
set_with_literals: set[Literal[1, 2, 3]] = {k for k in (1, 2, 3)}
reveal_type(set_with_literals) # revealed: set[Literal[1, 2, 3]]