mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-03 05:13:00 +00:00
[ty] Extend tuple __len__ and __bool__ special casing to also cover tuple subclasses (#19289)
Co-authored-by: Brent Westbrook
This commit is contained in:
parent
4dec44ae49
commit
c2380fa0e2
6 changed files with 194 additions and 16 deletions
|
|
@ -72,7 +72,14 @@ reveal_type(my_bool(0)) # revealed: bool
|
|||
|
||||
## Truthy values
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
reveal_type(bool(1)) # revealed: Literal[True]
|
||||
reveal_type(bool((0,))) # revealed: Literal[True]
|
||||
reveal_type(bool("NON EMPTY")) # revealed: Literal[True]
|
||||
|
|
@ -81,6 +88,42 @@ reveal_type(bool(True)) # revealed: Literal[True]
|
|||
def foo(): ...
|
||||
|
||||
reveal_type(bool(foo)) # revealed: Literal[True]
|
||||
|
||||
class SingleElementTupleSubclass(tuple[int]): ...
|
||||
|
||||
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass().__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length, but we know the length is guaranteed to be >=2
|
||||
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
|
||||
|
||||
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
|
||||
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
|
||||
reveal_type(MixedTupleSubclass().__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length with an overridden `__bool__`:
|
||||
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
|
||||
|
||||
# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
|
||||
# Same again but for a subclass of a fixed-length tuple:
|
||||
class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]):
|
||||
# TODO: we should reject this override as a Liskov violation:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
reveal_type(bool(EmptyTupleSubclassWithDunderBoolOverride(()))) # revealed: Literal[True]
|
||||
reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
|
||||
|
||||
# revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
|
|
@ -92,6 +135,12 @@ reveal_type(bool(None)) # revealed: Literal[False]
|
|||
reveal_type(bool("")) # revealed: Literal[False]
|
||||
reveal_type(bool(False)) # revealed: Literal[False]
|
||||
reveal_type(bool()) # revealed: Literal[False]
|
||||
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
|
||||
reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False]
|
||||
reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False]
|
||||
reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
|
|
@ -100,6 +149,13 @@ reveal_type(bool()) # revealed: Literal[False]
|
|||
reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
|
||||
class VariadicTupleSubclass(tuple[int, ...]): ...
|
||||
|
||||
def f(x: tuple[int, ...], y: VariadicTupleSubclass):
|
||||
reveal_type(bool(x)) # revealed: bool
|
||||
reveal_type(x.__bool__) # revealed: () -> bool
|
||||
reveal_type(y.__bool__) # revealed: () -> bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
|
|
|||
|
|
@ -65,6 +65,51 @@ reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
|||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
Tuple subclasses:
|
||||
|
||||
```py
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
class Length1TupleSubclass(tuple[int]): ...
|
||||
class Length2TupleSubclass(tuple[int, str]): ...
|
||||
class UnknownLengthTupleSubclass(tuple[int, ...]): ...
|
||||
|
||||
reveal_type(len(EmptyTupleSubclass())) # revealed: Literal[0]
|
||||
reveal_type(len(Length1TupleSubclass((1,)))) # revealed: Literal[1]
|
||||
reveal_type(len(Length2TupleSubclass((1, "foo")))) # revealed: Literal[2]
|
||||
reveal_type(len(UnknownLengthTupleSubclass((1, 2, 3)))) # revealed: int
|
||||
|
||||
reveal_type(tuple[int, int].__len__) # revealed: (self: tuple[int, int], /) -> Literal[2]
|
||||
reveal_type(tuple[int, ...].__len__) # revealed: (self: tuple[int, ...], /) -> int
|
||||
|
||||
def f(x: tuple[int, int], y: tuple[int, ...]):
|
||||
reveal_type(x.__len__) # revealed: () -> Literal[2]
|
||||
reveal_type(y.__len__) # revealed: () -> int
|
||||
|
||||
reveal_type(EmptyTupleSubclass.__len__) # revealed: (self: tuple[()], /) -> Literal[0]
|
||||
reveal_type(EmptyTupleSubclass().__len__) # revealed: () -> Literal[0]
|
||||
reveal_type(UnknownLengthTupleSubclass.__len__) # revealed: (self: tuple[int, ...], /) -> int
|
||||
reveal_type(UnknownLengthTupleSubclass().__len__) # revealed: () -> int
|
||||
```
|
||||
|
||||
If `__len__` is overridden, we use the overridden return type:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class UnknownLengthSubclassWithDunderLenOverridden(tuple[int, ...]):
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
reveal_type(len(UnknownLengthSubclassWithDunderLenOverridden())) # revealed: Literal[42]
|
||||
|
||||
class FixedLengthSubclassWithDunderLenOverridden(tuple[int]):
|
||||
# TODO: we should complain about this as a Liskov violation (incompatible override)
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
reveal_type(len(FixedLengthSubclassWithDunderLenOverridden((1,)))) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
|
|
|
|||
|
|
@ -551,6 +551,11 @@ static_assert(is_subtype_of(Never, AlwaysFalsy))
|
|||
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import AlwaysTruthy, AlwaysFalsy, Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
|
@ -588,6 +593,30 @@ static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]],
|
|||
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal[""]]], Not[AlwaysFalsy]))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
|
||||
|
||||
class Length2TupleSubclass(tuple[int, str]): ...
|
||||
|
||||
static_assert(is_subtype_of(Length2TupleSubclass, AlwaysTruthy))
|
||||
|
||||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
|
||||
static_assert(is_subtype_of(EmptyTupleSubclass, AlwaysFalsy))
|
||||
|
||||
class TupleSubclassWithAtLeastLength2(tuple[int, *tuple[str, ...], bytes]): ...
|
||||
|
||||
static_assert(is_subtype_of(TupleSubclassWithAtLeastLength2, AlwaysTruthy))
|
||||
|
||||
class UnknownLength(tuple[int, ...]): ...
|
||||
|
||||
static_assert(not is_subtype_of(UnknownLength, AlwaysTruthy))
|
||||
static_assert(not is_subtype_of(UnknownLength, AlwaysFalsy))
|
||||
|
||||
class Invalid(tuple[int, str]):
|
||||
# TODO: we should emit an error here (Liskov violation)
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
static_assert(is_subtype_of(Invalid, AlwaysFalsy))
|
||||
```
|
||||
|
||||
### `TypeGuard` and `TypeIs`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue