[ty] Infer better specializations of unions with None (etc) (#20749)

This PR adds a specialization inference special case that lets us handle
the following examples better:

```py
def f[T](t: T | None) -> T: ...
def g[T](t: T | int | None) -> T | int: ...

def _(x: str | None):
    reveal_type(f(x))  # revealed: str (previously str | None)

def _(y: str | int | None):
    reveal_type(g(x))  # revealed: str | int (previously str | int | None)
```

We already have a special case for when the formal is a union where one
element is a typevar, but it maps the entire actual type to the typevar
(as you can see in the "previously" results above).

The new special case kicks in when the actual is also a union. Now, we
filter out any actual union elements that are already subtypes of the
formal, and only bind whatever types remain to the typevar. (The `|
None` pattern appears quite often in the ecosystem results, but it's
more general and works with any number of non-typevar union elements.)

The new constraint solver should handle this case as well, but it's
worth adding this heuristic now with the old solver because it
eliminates some false positives from the ecosystem report, and makes the
ecosystem report less noisy on the other constraint solver PRs.
This commit is contained in:
Douglas Creager 2025-10-07 13:33:42 -04:00 committed by GitHub
parent 88c0ce3e38
commit 416e956fe0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 74 additions and 19 deletions

View file

@ -441,7 +441,23 @@ def g[T: A](b: B[T]):
return f(b.x) # Fine
```
## Constrained TypeVar in a union
## Typevars in a union
```py
def takes_in_union[T](t: T | None) -> T:
raise NotImplementedError
def takes_in_bigger_union[T](t: T | int | None) -> T:
raise NotImplementedError
def _(x: str | None) -> None:
reveal_type(takes_in_union(x)) # revealed: str
reveal_type(takes_in_bigger_union(x)) # revealed: str
def _(x: str | int | None) -> None:
reveal_type(takes_in_union(x)) # revealed: str | int
reveal_type(takes_in_bigger_union(x)) # revealed: str
```
This is a regression test for an issue that surfaced in the primer report of an early version of
<https://github.com/astral-sh/ruff/pull/19811>, where we failed to solve the `TypeVar` here due to