mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[ty] Fix false positive for bounded type parameters with NewType (#22542)
Fixes https://github.com/astral-sh/ty/issues/2467 When calling a method on an instance of a generic class with bounded type parameters (e.g., `C[T: K]` where `K` is a NewType), ty was incorrectly reporting: "Argument type `C[K]` does not satisfy upper bound `C[T@C]` of type variable `Self`" The issue was introduced by PR #22105, which moved the catch-all case for NewType assignments that falls back to the concrete base type. This case was moved before the TypeVar handling cases, so when checking `K <: T@C` (where K is a NewType and T@C is a TypeVar with upper bound K): 1. The NewType fallback matched first 2. It delegated to `int` (K's concrete base type) 3. Then checked `int <: T@C`, which checks if `int` satisfies bound `K` 4. But `int` is not assignable to `K` (NewTypes are distinct from their bases) The fix moves the NewType fallback case after the TypeVar cases, so TypeVar handling takes precedence. Now when checking `K <: T@C`, we use the TypeVar case at line 828 which returns `false` for non-inferable typevars - but this is correct because the *other* direction (`T@C <: K`) passes, and for the overall specialization comparison both directions are checked.
This commit is contained in:
@@ -456,6 +456,40 @@ reveal_type(int_container) # revealed: Container[int]
|
||||
reveal_type(int_container.set_value(1)) # revealed: Container[int]
|
||||
```
|
||||
|
||||
## Generic class with bounded type variable
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/2467>.
|
||||
|
||||
Calling a method on a generic class instance should work when the type parameter is specialized with
|
||||
a type that satisfies a bound.
|
||||
|
||||
```py
|
||||
from typing import NewType
|
||||
|
||||
class Base: ...
|
||||
|
||||
class C[T: Base]:
|
||||
x: T
|
||||
|
||||
def g(self) -> None:
|
||||
pass
|
||||
|
||||
# Calling a method on a specialized instance should not produce an error
|
||||
C[Base]().g()
|
||||
|
||||
# Test with a NewType bound
|
||||
K = NewType("K", int)
|
||||
|
||||
class D[T: K]:
|
||||
x: T
|
||||
|
||||
def h(self) -> None:
|
||||
pass
|
||||
|
||||
# Calling a method on a specialized instance should not produce an error
|
||||
D[K]().h()
|
||||
```
|
||||
|
||||
## Protocols
|
||||
|
||||
TODO: <https://typing.python.org/en/latest/spec/generics.html#use-in-protocols>
|
||||
|
||||
@@ -715,17 +715,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
})
|
||||
}
|
||||
// All other `NewType` assignments fall back to the concrete base type.
|
||||
(Type::NewTypeInstance(self_newtype), _) => {
|
||||
self_newtype.concrete_base_type(db).has_relation_to_impl(
|
||||
db,
|
||||
target,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
}
|
||||
|
||||
(Type::Union(union), _) => union.elements(db).iter().when_all(db, |&elem_ty| {
|
||||
elem_ty.has_relation_to_impl(
|
||||
@@ -869,6 +858,21 @@ impl<'db> Type<'db> {
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// All other `NewType` assignments fall back to the concrete base type.
|
||||
// This case must come after the TypeVar cases above, so that when checking
|
||||
// `NewType <: TypeVar`, we use the TypeVar handling rather than falling back
|
||||
// to the NewType's concrete base type.
|
||||
(Type::NewTypeInstance(self_newtype), _) => {
|
||||
self_newtype.concrete_base_type(db).has_relation_to_impl(
|
||||
db,
|
||||
target,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
}
|
||||
|
||||
// Note that the definition of `Type::AlwaysFalsy` depends on the return value of `__bool__`.
|
||||
// If `__bool__` always returns True or False, it can be treated as a subtype of `AlwaysTruthy` or `AlwaysFalsy`, respectively.
|
||||
(left, Type::AlwaysFalsy) => ConstraintSet::from(left.bool(db).is_always_false()),
|
||||
|
||||
Reference in New Issue
Block a user