[red-knot] Treat classes as instances of their respective metaclasses in boolean tests (#15105)

## Summary

Follow-up to #15089.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
InSync 2024-12-23 08:30:51 +07:00 committed by GitHub
parent 3b27d5dbad
commit f764f59971
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 24 deletions

View file

@ -220,6 +220,57 @@ else:
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
``` ```
## Truthiness of classes
```py
class MetaAmbiguous(type):
def __bool__(self) -> bool: ...
class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type):
def __bool__(self) -> Literal[True]: ...
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous: ...
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...
class DeferredClass(metaclass=MetaDeferred): ...
def _(
a: type[AmbiguousClass],
t: type[TruthyClass],
f: type[FalsyClass],
d: type[DeferredClass],
ta: type[TruthyClass | AmbiguousClass],
af: type[AmbiguousClass] | type[FalsyClass],
flag: bool,
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
tf = TruthyClass if flag else FalsyClass
reveal_type(tf) # revealed: Literal[TruthyClass, FalsyClass]
if tf:
reveal_type(tf) # revealed: Literal[TruthyClass]
else:
reveal_type(tf) # revealed: Literal[FalsyClass]
```
## Narrowing in chained boolean expressions ## Narrowing in chained boolean expressions
```py ```py
@ -253,17 +304,12 @@ class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ... def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type): class MetaTruthy(type):
def __bool__(self) -> Literal[False]: ... def __bool__(self) -> Literal[True]: ...
class FalsyClass(metaclass=MetaFalsy): ... class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ... class TruthyClass(metaclass=MetaTruthy): ...
def _(x: type[FalsyClass] | type[TruthyClass]): def _(x: type[FalsyClass] | type[TruthyClass]):
# TODO: Should be `type[TruthyClass] | A` reveal_type(x or A()) # revealed: type[TruthyClass] | A
# revealed: type[FalsyClass] & ~AlwaysFalsy | type[TruthyClass] & ~AlwaysFalsy | A reveal_type(x and A()) # revealed: type[FalsyClass] | A
reveal_type(x or A())
# TODO: Should be `type[FalsyClass] | A`
# revealed: type[FalsyClass] & ~AlwaysTruthy | type[TruthyClass] & ~AlwaysTruthy | A
reveal_type(x and A())
``` ```

View file

@ -1206,14 +1206,6 @@ impl<'db> Type<'db> {
Type::SubclassOf(_), Type::SubclassOf(_),
) => true, ) => true,
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
// TODO: Once we have support for final classes, we can determine disjointness in some cases
// here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into
// `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for
// final classes inside `Type::SubclassOf` everywhere.
false
}
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => { (Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint. // `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`. // Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
@ -1224,6 +1216,14 @@ impl<'db> Type<'db> {
matches!(ty.bool(db), Truthiness::AlwaysTrue) matches!(ty.bool(db), Truthiness::AlwaysTrue)
} }
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
// TODO: Once we have support for final classes, we can determine disjointness in some cases
// here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into
// `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for
// final classes inside `Type::SubclassOf` everywhere.
false
}
(Type::KnownInstance(left), right) => { (Type::KnownInstance(left), right) => {
left.instance_fallback(db).is_disjoint_from(db, right) left.instance_fallback(db).is_disjoint_from(db, right)
} }
@ -1678,15 +1678,13 @@ impl<'db> Type<'db> {
Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous, Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue, Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue, Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(_) => { Type::ClassLiteral(ClassLiteralType { class }) => {
// TODO: lookup `__bool__` and `__len__` methods on the class's metaclass class.metaclass(db).to_instance(db).bool(db)
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
Truthiness::Ambiguous
}
Type::SubclassOf(_) => {
// TODO: see above
Truthiness::Ambiguous
} }
Type::SubclassOf(SubclassOfType { base }) => base
.into_class()
.map(|class| Type::class_literal(class).bool(db))
.unwrap_or(Truthiness::Ambiguous),
Type::AlwaysTruthy => Truthiness::AlwaysTrue, Type::AlwaysTruthy => Truthiness::AlwaysTrue,
Type::AlwaysFalsy => Truthiness::AlwaysFalse, Type::AlwaysFalsy => Truthiness::AlwaysFalse,
instance_ty @ Type::Instance(InstanceType { class }) => { instance_ty @ Type::Instance(InstanceType { class }) => {