mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00

## Summary This changeset adds support for precise type-inference and boundness-handling of definitions inside control-flow branches with statically-known conditions, i.e. test-expressions whose truthiness we can unambiguously infer as *always false* or *always true*. This branch also includes: - `sys.platform` support - statically-known branches handling for Boolean expressions and while loops - new `target-version` requirements in some Markdown tests which were now required due to the understanding of `sys.version_info` branches. closes #12700 closes #15034 ## Performance ### `tomllib`, -7%, needs to resolve one additional module (sys) | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `./red_knot_main --project /home/shark/tomllib` | 22.2 ± 1.3 | 19.1 | 25.6 | 1.00 | | `./red_knot_feature --project /home/shark/tomllib` | 23.8 ± 1.6 | 20.8 | 28.6 | 1.07 ± 0.09 | ### `black`, -6% | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `./red_knot_main --project /home/shark/black` | 129.3 ± 5.1 | 119.0 | 137.8 | 1.00 | | `./red_knot_feature --project /home/shark/black` | 136.5 ± 6.8 | 123.8 | 147.5 | 1.06 ± 0.07 | ## Test Plan - New Markdown tests for the main feature in `statically-known-branches.md` - New Markdown tests for `sys.platform` - Adapted tests for `EllipsisType`, `Never`, etc
5.3 KiB
5.3 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 _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 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 _(flag1: bool, flag2: bool, flag3: bool):
t1 = Derived1 if flag1 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 flag2 else Base
if issubclass(t2, Base):
reveal_type(t2) # revealed: Literal[Derived1, Base]
t3 = Derived1 if flag3 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 _(t: type[object]):
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
types.NoneType
is only available in Python 3.10 and later:
[environment]
python-version = "3.10"
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 _(flag1: bool, flag2: bool):
t = int if flag1 else str if flag2 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: ...
t = 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]