microsoft / pyright

Static Type Checker for Python
Other
13.08k stars 1.4k forks source link

Union resolution fail with `Unknown` elements #8449

Closed minmax closed 1 month ago

minmax commented 1 month ago

After updating pyright 1.1.371 -> 1.1.372, union resolution start to failed in some strange case.

from __future__ import annotations
from collections.abc import Callable
from typing import Generic, TypeVar

T_co = TypeVar("T_co", covariant=True)
E_co = TypeVar("E_co", covariant=True)
F = TypeVar("F")

class Ok(Generic[T_co]):
    def or_else(self, op: object) -> Ok[T_co]: ...

class Err(Generic[E_co]):
    def or_else(self, op: Callable[[E_co], Result[T_co, F]]) -> Result[T_co, F]: ...

Result = Ok[T_co] | Err[E_co]

def inner(func: Callable[[E_co], Err[F]], r: Result[T_co, E_co]) -> Result[T_co, F]:
    match r:
        case Ok():
            return r.or_else(func)
        case Err():
            return r.or_else(func)  # Return type, "Ok[Unknown] | Err[F@inner]", is partially unknown

I found how to fix this, but I'm not sure if the error behavior is correct. So if i change Callable[[E_co], Err[F]] to Callable[[E_co], Result[Never, F]], the error goes away.

def inner(func: Callable[[E_co], Result[Never, F]], r: Result[T_co, E_co]) -> Result[T_co, F]:

ps in real code i use result lib with Ok, Err and Result.

erictraut commented 1 month ago

I think the new behavior is arguably more correct than the old behavior because the type variable T_co is going unsolved in this case and therefore should probably result in an Unknown type.

I can also see how the old behavior would be preferable in your use case.

I did an experiment and restored the old behavior where pyright attempts to use constraints from the full union rather than preferring a single subtype of the full union. This produces a bunch of extra false positive errors in many code bases. This was the reason I made the change in the first place.

I'll need to think about this a bit more, but my inclination at this point is to stick with the new behavior.

erictraut commented 1 month ago

I added some additional heuristics that allow me to strike a balance between the old behavior and the new behavior. This allows your use case to work while also addressing the other bugs that prompted this change. This will be included in the next release.

erictraut commented 1 month ago

This is addressed in pyright 1.1.373.