Skip to content

Commit

Permalink
[red-knot] Treat classes as instances of their respective metaclasses…
Browse files Browse the repository at this point in the history
… in boolean tests (#15105)

## Summary

Follow-up to #15089.

## Test Plan

Markdown tests.

---------

Co-authored-by: Carl Meyer <[email protected]>
  • Loading branch information
InSyncWithFoo and carljm authored Dec 23, 2024
1 parent 3b27d5d commit f764f59
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
30 changes: 14 additions & 16 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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)
}
Expand Down Expand Up @@ -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 }) => {
Expand Down

0 comments on commit f764f59

Please sign in to comment.