diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md index a252ba32dc..80c4a90831 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/truthiness.md @@ -220,6 +220,57 @@ else: 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 ```py @@ -253,17 +304,12 @@ class MetaFalsy(type): def __bool__(self) -> Literal[False]: ... class MetaTruthy(type): - def __bool__(self) -> Literal[False]: ... + def __bool__(self) -> Literal[True]: ... class FalsyClass(metaclass=MetaFalsy): ... class TruthyClass(metaclass=MetaTruthy): ... def _(x: type[FalsyClass] | type[TruthyClass]): - # TODO: Should be `type[TruthyClass] | A` - # revealed: type[FalsyClass] & ~AlwaysFalsy | type[TruthyClass] & ~AlwaysFalsy | 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()) + reveal_type(x or A()) # revealed: type[TruthyClass] | A + reveal_type(x and A()) # revealed: type[FalsyClass] | A ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d0ee70e5b5..59dcb09eba 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1206,14 +1206,6 @@ impl<'db> Type<'db> { Type::SubclassOf(_), ) => 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) => { // `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`. @@ -1224,6 +1216,14 @@ impl<'db> Type<'db> { 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) => { 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::FunctionLiteral(_) => Truthiness::AlwaysTrue, Type::ModuleLiteral(_) => Truthiness::AlwaysTrue, - Type::ClassLiteral(_) => { - // TODO: lookup `__bool__` and `__len__` methods on the class's metaclass - // More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing - Truthiness::Ambiguous - } - Type::SubclassOf(_) => { - // TODO: see above - Truthiness::Ambiguous + Type::ClassLiteral(ClassLiteralType { class }) => { + class.metaclass(db).to_instance(db).bool(db) } + Type::SubclassOf(SubclassOfType { base }) => base + .into_class() + .map(|class| Type::class_literal(class).bool(db)) + .unwrap_or(Truthiness::Ambiguous), Type::AlwaysTruthy => Truthiness::AlwaysTrue, Type::AlwaysFalsy => Truthiness::AlwaysFalse, instance_ty @ Type::Instance(InstanceType { class }) => {