Files
ruff/crates/red_knot_python_semantic/resources/mdtest/narrow/issubclass.md
David Peter 3bef23669f [red-knot] Diagnostic for possibly unbound imports (#14281)
## Summary

This adds a new diagnostic when possibly unbound symbols are imported.
The `TODO` comment had a question mark, do I'm not sure if this is
really something that we want.

This does not touch the un*declared* case, yet.

relates to: #14022

## Test Plan

Updated already existing tests with new diagnostics
2024-11-11 20:26:01 +01:00

5.2 KiB

Narrowing for issubclass checks

Narrowing for issubclass(class, classinfo) expressions.

classinfo is a single type

Basic example

def flag() -> bool: ...

t = int if flag() else str

if issubclass(t, bytes):
    reveal_type(t)  # revealed: Never

if issubclass(t, object):
    reveal_type(t)  # revealed: Literal[int, str]

if issubclass(t, int):
    reveal_type(t)  # revealed: Literal[int]
else:
    reveal_type(t)  # revealed: Literal[str]

if issubclass(t, str):
    reveal_type(t)  # revealed: Literal[str]
    if issubclass(t, int):
        reveal_type(t)  # revealed: Never

Proper narrowing in elif and else branches

def flag() -> bool: ...

t = int if flag() else str if flag() else bytes

if issubclass(t, int):
    reveal_type(t)  # revealed: Literal[int]
else:
    reveal_type(t)  # revealed: Literal[str, bytes]

if issubclass(t, int):
    reveal_type(t)  # revealed: Literal[int]
elif issubclass(t, str):
    reveal_type(t)  # revealed: Literal[str]
else:
    reveal_type(t)  # revealed: Literal[bytes]

Multiple derived classes

class Base: ...
class Derived1(Base): ...
class Derived2(Base): ...
class Unrelated: ...

def flag() -> bool: ...

t1 = Derived1 if flag() else Derived2

if issubclass(t1, Base):
    reveal_type(t1)  # revealed: Literal[Derived1, Derived2]

if issubclass(t1, Derived1):
    reveal_type(t1)  # revealed: Literal[Derived1]
else:
    reveal_type(t1)  # revealed: Literal[Derived2]

t2 = Derived1 if flag() else Base

if issubclass(t2, Base):
    reveal_type(t2)  # revealed: Literal[Derived1, Base]

t3 = Derived1 if flag() else Unrelated

if issubclass(t3, Base):
    reveal_type(t3)  # revealed: Literal[Derived1]
else:
    reveal_type(t3)  # revealed: Literal[Unrelated]

Narrowing for non-literals

class A: ...
class B: ...

def get_class() -> type[object]: ...

t = get_class()

if issubclass(t, A):
    reveal_type(t)  # revealed: type[A]
    if issubclass(t, B):
        reveal_type(t)  # revealed: type[A] & type[B]
else:
    reveal_type(t)  # revealed: type[object] & ~type[A]

Handling of None

# TODO: this error should ideally go away once we (1) understand `sys.version_info` branches,
# and (2) set the target Python version for this test to 3.10.
# error: [possibly-unbound-import] "Member `NoneType` of module `types` is possibly unbound"
from types import NoneType

def flag() -> bool: ...

t = int if flag() else NoneType

if issubclass(t, NoneType):
    reveal_type(t)  # revealed: Literal[NoneType]

if issubclass(t, type(None)):
    # TODO: this should be just `Literal[NoneType]`
    reveal_type(t)  # revealed: Literal[int, NoneType]

classinfo contains multiple types

(Nested) tuples of types

class Unrelated: ...

def flag() -> bool: ...

t = int if flag() else str if flag() else bytes

if issubclass(t, (int, (Unrelated, (bytes,)))):
    reveal_type(t)  # revealed: Literal[int, bytes]
else:
    reveal_type(t)  # revealed: Literal[str]

Special cases

Emit a diagnostic if the first argument is of wrong type

Too wide

type[object] is a subtype of object, but not every object can be passed as the first argument to issubclass:

class A: ...

def get_object() -> object: ...

t = get_object()

# TODO: we should emit a diagnostic here
if issubclass(t, A):
    reveal_type(t)  # revealed: type[A]

Wrong

Literal[1] and type are entirely disjoint, so the inferred type of Literal[1] & type[int] is eagerly simplified to Never as a result of the type narrowing in the if issubclass(t, int) branch:

t = 1

# TODO: we should emit a diagnostic here
if issubclass(t, int):
    reveal_type(t)  # revealed: Never

Do not use custom issubclass for narrowing

def issubclass(c, ci):
    return True

def flag() -> bool: ...

t = int if flag() else str
if issubclass(t, int):
    reveal_type(t)  # revealed: Literal[int, str]

Do support narrowing if issubclass is aliased

issubclass_alias = issubclass

def flag() -> bool: ...

t = int if flag() else str
if issubclass_alias(t, int):
    reveal_type(t)  # revealed: Literal[int]

Do support narrowing if issubclass is imported

from builtins import issubclass as imported_issubclass

def flag() -> bool: ...

t = int if flag() else str
if imported_issubclass(t, int):
    reveal_type(t)  # revealed: Literal[int]

Do not narrow if second argument is not a proper classinfo argument

from typing import Any

def flag() -> bool: ...

t = int if flag() else str

# TODO: this should cause us to emit a diagnostic during
# type checking
if issubclass(t, "str"):
    reveal_type(t)  # revealed: Literal[int, str]

# TODO: this should cause us to emit a diagnostic during
# type checking
if issubclass(t, (bytes, "str")):
    reveal_type(t)  # revealed: Literal[int, str]

# TODO: this should cause us to emit a diagnostic during
# type checking
if issubclass(t, Any):
    reveal_type(t)  # revealed: Literal[int, str]

Do not narrow if there are keyword arguments

def flag() -> bool: ...

t = int if flag() else str

# TODO: this should cause us to emit a diagnostic
# (`issubclass` has no `foo` parameter)
if issubclass(t, int, foo="bar"):
    reveal_type(t)  # revealed: Literal[int, str]