microsoft / pyright

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

TypeIs does not narrow type in some cases #8691

Closed irgolic closed 3 days ago

irgolic commented 1 month ago

Describe the bug Not sure how wide this bug is, but my Sentinel type used to negate types fine in experimental StrictTypeGuard back on 347. On 375 I'm assuming that behavior was disabled for sake of TypeIs, which doesn't work correctly for my case.

Also note the weird UnionType that appears, and obstructs the use of that object without casting it to what it's supposed to be.

Code or Screenshots

class Sentinel:
    pass

def is_sentinel(value: Any) -> TypeIs[type[Sentinel]]:
    return inspect.isclass(value) and issubclass(value, Sentinel)

def _(a: dict[str, int] | type[Sentinel]):
    if is_sentinel(a):
        reveal_type(a)  # Type of "a" is "dict[str, int] | type[Sentinel]"
    else:
        reveal_type(a)  # Type of "a" is "dict[str, int] | type[Sentinel]"
    b = a | {}
    reveal_type(b)  # Type of "b" is "dict[str, int] | UnionType"

VS Code extension or command-line Running CLI, pyright 1.1.375

loic-simon commented 1 month ago

I encountered a similar issue, there seem to be a bug with TypeIs[type[...]]: considering the guard

def is_str_type(typ: Any) -> TypeIs[type[str]]:
    return typ is str

the following works as expected:

def test_simple(typ: type[Any], val: Any) -> None:
    if is_str_type(typ):
        reveal_type(typ)  # Type of "typ" is "type[str]"

but not when using a type variable (PEP 695 style or not):

def test_typevar[T](typ: type[T], val: T) -> None:
    if is_str_type(typ):
        reveal_type(typ)  # Type of "typ" is "Never"

(playground)

FTR, it works as expected using a TypeGuard instead (playground), or using mypy (playground).

I'm not 100% sure it is the same issue; I can open a new one if necessary.

erictraut commented 3 days ago

This is addressed in pyright 1.1.380.

irgolic commented 3 days ago

Thank you!