mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
[ty] Add precise inference for unpacking a TypeVar if the TypeVar has an upper bound with a precise tuple spec (#19985)
This commit is contained in:
parent
c82e255ca8
commit
662d18bd05
3 changed files with 94 additions and 2 deletions
|
@ -455,3 +455,45 @@ def overloaded_outer(t: T | None = None) -> None:
|
||||||
if t is not None:
|
if t is not None:
|
||||||
inner(t)
|
inner(t)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Unpacking a TypeVar
|
||||||
|
|
||||||
|
We can infer precise heterogeneous types from the result of an unpacking operation applied to a type
|
||||||
|
variable if the type variable's upper bound is a type with a precise tuple spec:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple, Final, TypeVar, Generic
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=tuple[int, str])
|
||||||
|
|
||||||
|
def f(x: T) -> T:
|
||||||
|
a, b = x
|
||||||
|
reveal_type(a) # revealed: int
|
||||||
|
reveal_type(b) # revealed: str
|
||||||
|
return x
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Team(Generic[T]):
|
||||||
|
employees: list[T]
|
||||||
|
|
||||||
|
def x(team: Team[T]) -> Team[T]:
|
||||||
|
age, name = team.employees[0]
|
||||||
|
reveal_type(age) # revealed: int
|
||||||
|
reveal_type(name) # revealed: str
|
||||||
|
return team
|
||||||
|
|
||||||
|
class Age(int): ...
|
||||||
|
class Name(str): ...
|
||||||
|
|
||||||
|
class Employee(NamedTuple):
|
||||||
|
age: Age
|
||||||
|
name: Name
|
||||||
|
|
||||||
|
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
|
||||||
|
team = Team(employees=list(EMPLOYEES))
|
||||||
|
reveal_type(team.employees) # revealed: list[Employee]
|
||||||
|
age, name = team.employees[0]
|
||||||
|
reveal_type(age) # revealed: Age
|
||||||
|
reveal_type(name) # revealed: Name
|
||||||
|
```
|
||||||
|
|
|
@ -454,3 +454,43 @@ def overloaded_outer[T](t: T | None = None) -> None:
|
||||||
if t is not None:
|
if t is not None:
|
||||||
inner(t)
|
inner(t)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Unpacking a TypeVar
|
||||||
|
|
||||||
|
We can infer precise heterogeneous types from the result of an unpacking operation applied to a
|
||||||
|
TypeVar if the TypeVar's upper bound is a type with a precise tuple spec:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple, Final
|
||||||
|
|
||||||
|
def f[T: tuple[int, str]](x: T) -> T:
|
||||||
|
a, b = x
|
||||||
|
reveal_type(a) # revealed: int
|
||||||
|
reveal_type(b) # revealed: str
|
||||||
|
return x
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Team[T: tuple[int, str]]:
|
||||||
|
employees: list[T]
|
||||||
|
|
||||||
|
def x[T: tuple[int, str]](team: Team[T]) -> Team[T]:
|
||||||
|
age, name = team.employees[0]
|
||||||
|
reveal_type(age) # revealed: int
|
||||||
|
reveal_type(name) # revealed: str
|
||||||
|
return team
|
||||||
|
|
||||||
|
class Age(int): ...
|
||||||
|
class Name(str): ...
|
||||||
|
|
||||||
|
class Employee(NamedTuple):
|
||||||
|
age: Age
|
||||||
|
name: Name
|
||||||
|
|
||||||
|
EMPLOYEES: Final = (Employee(name=Name("alice"), age=Age(42)),)
|
||||||
|
team = Team(employees=list(EMPLOYEES))
|
||||||
|
reveal_type(team.employees) # revealed: list[Employee]
|
||||||
|
age, name = team.employees[0]
|
||||||
|
reveal_type(age) # revealed: Age
|
||||||
|
reveal_type(name) # revealed: Name
|
||||||
|
```
|
||||||
|
|
|
@ -4870,6 +4870,18 @@ impl<'db> Type<'db> {
|
||||||
Type::TypeAlias(alias) => {
|
Type::TypeAlias(alias) => {
|
||||||
return alias.value_type(db).try_iterate_with_mode(db, mode);
|
return alias.value_type(db).try_iterate_with_mode(db, mode);
|
||||||
}
|
}
|
||||||
|
Type::NonInferableTypeVar(tvar) => match tvar.typevar(db).bound_or_constraints(db) {
|
||||||
|
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||||
|
return bound.try_iterate_with_mode(db, mode);
|
||||||
|
}
|
||||||
|
// TODO: could we create a "union of tuple specs"...?
|
||||||
|
// (Same question applies to the `Type::Union()` branch lower down)
|
||||||
|
Some(TypeVarBoundOrConstraints::Constraints(_)) | None => {}
|
||||||
|
},
|
||||||
|
Type::TypeVar(_) => unreachable!(
|
||||||
|
"should not be able to iterate over type variable {} in inferable position",
|
||||||
|
self.display(db)
|
||||||
|
),
|
||||||
Type::Dynamic(_)
|
Type::Dynamic(_)
|
||||||
| Type::FunctionLiteral(_)
|
| Type::FunctionLiteral(_)
|
||||||
| Type::GenericAlias(_)
|
| Type::GenericAlias(_)
|
||||||
|
@ -4895,8 +4907,6 @@ impl<'db> Type<'db> {
|
||||||
| Type::EnumLiteral(_)
|
| Type::EnumLiteral(_)
|
||||||
| Type::LiteralString
|
| Type::LiteralString
|
||||||
| Type::BytesLiteral(_)
|
| Type::BytesLiteral(_)
|
||||||
| Type::TypeVar(_)
|
|
||||||
| Type::NonInferableTypeVar(_)
|
|
||||||
| Type::BoundSuper(_)
|
| Type::BoundSuper(_)
|
||||||
| Type::TypeIs(_)
|
| Type::TypeIs(_)
|
||||||
| Type::TypedDict(_) => {}
|
| Type::TypedDict(_) => {}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue