mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
[ty] prefer declared type if mutually assignable (#24802)
This commit is contained in:
@@ -251,6 +251,46 @@ a: list[str] = [1, 2, 3]
|
||||
b: set[int] = {1, 2, "3"}
|
||||
```
|
||||
|
||||
## Mutually assignable annotated assignments use the declared type
|
||||
|
||||
When an annotated assignment has a value whose inferred type is assignable to the declared type, the
|
||||
binding uses the declared type if the declared type is also assignable back to the inferred type.
|
||||
This indicates that we are dealing with difference in precision (graduality) rather than a narrowed
|
||||
static type; in that case we want to prefer the user's annotation.
|
||||
|
||||
The actual inferred type of the right-hand side is still used to validate the assignment.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def returns_list_any() -> list[Any]:
|
||||
return [1]
|
||||
|
||||
def returns_list_int() -> list[int]:
|
||||
return [1]
|
||||
|
||||
def returns_any() -> Any:
|
||||
return 1
|
||||
|
||||
v1: Any = 1
|
||||
reveal_type(v1) # revealed: Any
|
||||
|
||||
v2: int = returns_any()
|
||||
reveal_type(v2) # revealed: int
|
||||
|
||||
v3: list[Any] = returns_list_int()
|
||||
reveal_type(v3) # revealed: list[Any]
|
||||
|
||||
v4: list[int] = returns_list_any()
|
||||
reveal_type(v4) # revealed: list[int]
|
||||
|
||||
v4: object = returns_list_int()
|
||||
reveal_type(v4) # revealed: list[int]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `list[int]` is not assignable to `list[str]`"
|
||||
invalid: list[str] = returns_list_int()
|
||||
```
|
||||
|
||||
## Generic constructor annotations are understood
|
||||
|
||||
```toml
|
||||
@@ -531,28 +571,28 @@ class TD2(TypedDict):
|
||||
|
||||
def _(dt: dict[str, Any], key: str):
|
||||
x1: TD = dt.get(key, {})
|
||||
reveal_type(x1) # revealed: Any
|
||||
reveal_type(x1) # revealed: TD
|
||||
|
||||
x2: TD = dt.get(key, {"x": 0})
|
||||
reveal_type(x2) # revealed: Any
|
||||
reveal_type(x2) # revealed: TD
|
||||
|
||||
x3: TD | None = dt.get(key, {})
|
||||
reveal_type(x3) # revealed: Any
|
||||
reveal_type(x3) # revealed: TD | None
|
||||
|
||||
x4: TD | None = dt.get(key, {"x": 0})
|
||||
reveal_type(x4) # revealed: Any
|
||||
reveal_type(x4) # revealed: TD | None
|
||||
|
||||
x5: TD2 = dt.get(key, {})
|
||||
reveal_type(x5) # revealed: Any
|
||||
reveal_type(x5) # revealed: TD2
|
||||
|
||||
x6: TD2 = dt.get(key, {"x": 0})
|
||||
reveal_type(x6) # revealed: Any
|
||||
reveal_type(x6) # revealed: TD2
|
||||
|
||||
x7: TD2 | None = dt.get(key, {})
|
||||
reveal_type(x7) # revealed: Any
|
||||
reveal_type(x7) # revealed: TD2 | None
|
||||
|
||||
x8: TD2 | None = dt.get(key, {"x": 0})
|
||||
reveal_type(x8) # revealed: Any
|
||||
reveal_type(x8) # revealed: TD2 | None
|
||||
```
|
||||
|
||||
Partially specialized type context is not ignored:
|
||||
@@ -642,11 +682,12 @@ f: list[Any] | None = f2(1)
|
||||
reveal_type(f) # revealed: list[Any] | None
|
||||
|
||||
g: list[Any] | dict[Any, Any] = f3(1)
|
||||
# TODO: Better constraint solver.
|
||||
reveal_type(g) # revealed: list[int] | dict[int, int]
|
||||
reveal_type(g) # revealed: list[Any] | dict[Any, Any]
|
||||
```
|
||||
|
||||
We only prefer the declared type if it is in non-covariant position.
|
||||
When inferring a generic call, we only use the declared type as type context if it is in
|
||||
non-covariant position. The final annotated assignment binding still uses the declared type if the
|
||||
inferred and declared types are mutually assignable.
|
||||
|
||||
```py
|
||||
class Bivariant[T]:
|
||||
@@ -685,15 +726,25 @@ reveal_type(x2) # revealed: Covariant[Literal[1]]
|
||||
reveal_type(x3) # revealed: Contravariant[int]
|
||||
reveal_type(x4) # revealed: Invariant[int]
|
||||
|
||||
x5: Bivariant[Any] = bivariant(1)
|
||||
x6: Covariant[Any] = covariant(1)
|
||||
x7: Contravariant[Any] = contravariant(1)
|
||||
x8: Invariant[Any] = invariant(1)
|
||||
x5: Bivariant[int | None] = bivariant(1)
|
||||
x6: Covariant[int | None] = covariant(1)
|
||||
x7: Contravariant[int | None] = contravariant(1)
|
||||
x8: Invariant[int | None] = invariant(1)
|
||||
|
||||
reveal_type(x5) # revealed: Bivariant[Literal[1]]
|
||||
reveal_type(x5) # revealed: Bivariant[int | None]
|
||||
reveal_type(x6) # revealed: Covariant[Literal[1]]
|
||||
reveal_type(x7) # revealed: Contravariant[Any]
|
||||
reveal_type(x8) # revealed: Invariant[Any]
|
||||
reveal_type(x7) # revealed: Contravariant[int | None]
|
||||
reveal_type(x8) # revealed: Invariant[int | None]
|
||||
|
||||
x9: Bivariant[Any] = bivariant(1)
|
||||
x10: Covariant[Any] = covariant(1)
|
||||
x11: Contravariant[Any] = contravariant(1)
|
||||
x12: Invariant[Any] = invariant(1)
|
||||
|
||||
reveal_type(x9) # revealed: Bivariant[Any]
|
||||
reveal_type(x10) # revealed: Covariant[Any]
|
||||
reveal_type(x11) # revealed: Contravariant[Any]
|
||||
reveal_type(x12) # revealed: Invariant[Any]
|
||||
```
|
||||
|
||||
```py
|
||||
|
||||
@@ -35,8 +35,7 @@ def _(l: list[int] | None = None):
|
||||
reveal_type(l1) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown]
|
||||
|
||||
l2: list[int] = l or list()
|
||||
# it would be better if this were `list[int]`? (https://github.com/astral-sh/ty/issues/136)
|
||||
reveal_type(l2) # revealed: (list[int] & ~AlwaysFalsy) | list[Unknown]
|
||||
reveal_type(l2) # revealed: list[int]
|
||||
|
||||
def f[T](x: T, cond: bool) -> T | list[T]:
|
||||
return x if cond else [x]
|
||||
|
||||
@@ -29,6 +29,9 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
|
||||
| possibly-unbound | | `possibly-missing-import` | ? |
|
||||
| unbound | | ? | `unresolved-import` |
|
||||
|
||||
When the declared and inferred types are mutually assignable, we use `T_declared` directly instead
|
||||
of unioning it with `T_inferred`.
|
||||
|
||||
## Declared
|
||||
|
||||
### Declared and bound
|
||||
@@ -137,8 +140,7 @@ Public.a = None
|
||||
|
||||
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
|
||||
inferred types. This case is interesting because the "possibly declared" definition might not be the
|
||||
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-missing-import`
|
||||
error for both `a` and `b`:
|
||||
same as the "possibly bound" definition:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
@@ -148,13 +150,10 @@ def flag() -> bool:
|
||||
|
||||
class Public:
|
||||
if flag():
|
||||
a: Any = 1
|
||||
b = 2
|
||||
else:
|
||||
b: str
|
||||
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(Public.a) # revealed: Literal[1] | Any
|
||||
# error: [possibly-missing-attribute]
|
||||
reveal_type(Public.b) # revealed: Literal[2] | str
|
||||
|
||||
|
||||
@@ -23,6 +23,31 @@ alice.role = "moderator"
|
||||
bob = Member(name="Bob", tag="VIP")
|
||||
```
|
||||
|
||||
## `Any` annotations preserve field specifier metadata
|
||||
|
||||
Even when a field is explicitly annotated as `Any`, `field(...)` should still be recognized as a
|
||||
dataclass field specifier. The synthesized field metadata comes from the right-hand side, not from
|
||||
the declared type.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class AnyFieldSpecifier:
|
||||
proto: Any = field(repr=False)
|
||||
key: str | None
|
||||
|
||||
reveal_type(AnyFieldSpecifier.__init__) # revealed: (self: AnyFieldSpecifier, proto: Any, key: str | None) -> None
|
||||
|
||||
@dataclass
|
||||
class AnyInitFalseField:
|
||||
key: str | None
|
||||
proto: Any = field(init=False)
|
||||
|
||||
reveal_type(AnyInitFalseField.__init__) # revealed: (self: AnyInitFalseField, key: str | None) -> None
|
||||
```
|
||||
|
||||
## Inheritance with defaults
|
||||
|
||||
```py
|
||||
|
||||
@@ -246,7 +246,7 @@ x11: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3]
|
||||
reveal_type(x11) # revealed: list[Literal[1, 2, 3]]
|
||||
|
||||
x12: Y[Y[Literal[1]]] = [[1]]
|
||||
reveal_type(x12) # revealed: list[list[Literal[1]]]
|
||||
reveal_type(x12) # revealed: list[Y[Literal[1]]]
|
||||
|
||||
x13: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)]
|
||||
reveal_type(x13) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]]
|
||||
@@ -459,7 +459,7 @@ type X = Literal["hello"]
|
||||
|
||||
x4: X = "hello"
|
||||
reveal_type(x4) # revealed: Literal["hello"]
|
||||
reveal_type([x4]) # revealed: list[Literal["hello"]]
|
||||
reveal_type([x4]) # revealed: list[X]
|
||||
|
||||
class MyEnum(Enum):
|
||||
A = 1
|
||||
|
||||
@@ -165,6 +165,12 @@ impl<'db> DeclaredAndInferredType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn should_preserve_inferred_binding_type(ty: Type<'_>) -> bool {
|
||||
// Dataclass field specifiers carry metadata in the inferred RHS type; replacing it with the
|
||||
// declared field type would lose settings like `init=False`.
|
||||
matches!(ty, Type::KnownInstance(KnownInstanceType::Field(_)))
|
||||
}
|
||||
|
||||
/// We currently store one dataclass field-specifiers inline, because that covers standard
|
||||
/// dataclasses. attrs uses 2 specifiers, pydantic and strawberry use 3 specifiers. SQLAlchemy
|
||||
/// uses 7 field specifiers. We could probably store more inline if this turns out to be a
|
||||
@@ -1345,19 +1351,31 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
}
|
||||
if inferred_ty.is_assignable_to(self.db(), declared_ty.inner_type()) {
|
||||
(declared_ty, inferred_ty)
|
||||
let declared_type = declared_ty.inner_type();
|
||||
if inferred_ty.is_assignable_to(self.db(), declared_type) {
|
||||
if !should_preserve_inferred_binding_type(inferred_ty)
|
||||
// TODO We currently can't distinguish here between "no declared type" and
|
||||
// "declared types is `Unknown` (e.g. due to a bad annotation, missing
|
||||
// import, etc.)". Ideally we would still prefer `Unknown` declared type,
|
||||
// but use inferred type if there is no declared type.
|
||||
&& !matches!(declared_type, Type::Dynamic(DynamicType::Unknown))
|
||||
&& declared_type.is_assignable_to(self.db(), inferred_ty)
|
||||
{
|
||||
(declared_ty, declared_type)
|
||||
} else {
|
||||
(declared_ty, inferred_ty)
|
||||
}
|
||||
} else {
|
||||
report_invalid_assignment(
|
||||
&self.context,
|
||||
node,
|
||||
definition,
|
||||
declared_ty.inner_type(),
|
||||
declared_type,
|
||||
inferred_ty,
|
||||
);
|
||||
|
||||
// if the assignment is invalid, fall back to assuming the annotation is correct
|
||||
(declared_ty, declared_ty.inner_type())
|
||||
(declared_ty, declared_type)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user