[ty] improve typevar solving from constraint sets (#22411)

## Summary

Fixes https://github.com/astral-sh/ty/issues/2292

When solving a bounded typevar, we preferred the upper bound over the
actual type seen in the call. This change fixes that.

## Test Plan

Added mdtest, existing tests pass.
This commit is contained in:
Carl Meyer
2026-01-06 13:10:51 -08:00
committed by GitHub
parent bc191f59b9
commit f97da18267
2 changed files with 37 additions and 5 deletions

View File

@@ -752,3 +752,32 @@ reveal_type(x) # revealed: list[Sub]
y: list[Sub] = f2(Sub())
reveal_type(y) # revealed: list[Sub]
```
## Bounded TypeVar with callable parameter
When a bounded TypeVar appears in a `Callable` parameter's return type, the inferred type should be
the actual type from the call, not the TypeVar's upper bound.
See: <https://github.com/astral-sh/ty/issues/2292>
```py
from typing import Callable, TypeVar
class Base:
pass
class Derived(Base):
attr: int
T = TypeVar("T", bound=Base)
def takes_factory(factory: Callable[[], T]) -> T:
return factory()
# Passing a class as a factory: should infer Derived, not Base
result = takes_factory(Derived)
reveal_type(result) # revealed: Derived
# Accessing an attribute that only exists on Derived should work
print(result.attr) # No error
```

View File

@@ -1662,14 +1662,17 @@ impl<'db> SpecializationBuilder<'db> {
for (bound_typevar, bounds) in mappings.drain() {
let variance = formal.variance_of(self.db, bound_typevar);
let upper = IntersectionType::from_elements(self.db, bounds.upper);
if !upper.is_object() {
self.add_type_mapping(bound_typevar, upper, variance, &mut f);
continue;
}
// Prefer the lower bound (often the concrete actual type seen) over the
// upper bound (which may include TypeVar bounds/constraints). The upper bound
// should only be used as a fallback when no concrete type was inferred.
let lower = UnionType::from_elements(self.db, bounds.lower);
if !lower.is_never() {
self.add_type_mapping(bound_typevar, lower, variance, &mut f);
continue;
}
let upper = IntersectionType::from_elements(self.db, bounds.upper);
if !upper.is_object() {
self.add_type_mapping(bound_typevar, upper, variance, &mut f);
}
}
}