mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:45:24 +00:00
[red-knot] Infer unary not operation for instances (#13827)
Handle unary `not` on instances by calling the `__bool__` dunder. ## Test Plan Added a new test case with some examples from these resources: - https://docs.python.org/3/library/stdtypes.html#truth-value-testing - <https://docs.python.org/3/reference/datamodel.html#object.__len__> - <https://docs.python.org/3/reference/datamodel.html#object.__bool__> --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
77e8da7497
commit
924741cb11
3 changed files with 132 additions and 6 deletions
|
@ -1,4 +1,6 @@
|
||||||
# Unary Operations
|
# Invert, UAdd, USub
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Literal
|
from typing import Literal
|
|
@ -113,3 +113,101 @@ reveal_type(not ()) # revealed: Literal[True]
|
||||||
reveal_type(not ("hello",)) # revealed: Literal[False]
|
reveal_type(not ("hello",)) # revealed: Literal[False]
|
||||||
reveal_type(not (1, "hello")) # revealed: Literal[False]
|
reveal_type(not (1, "hello")) # revealed: Literal[False]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Instance
|
||||||
|
|
||||||
|
Not operator is inferred based on
|
||||||
|
<https://docs.python.org/3/library/stdtypes.html#truth-value-testing>. An instance is True or False
|
||||||
|
if the `__bool__` method says so.
|
||||||
|
|
||||||
|
At runtime, the `__len__` method is a fallback for `__bool__`, but we can't make use of that. If we
|
||||||
|
have a class that defines `__len__` but not `__bool__`, it is possible that any subclass could add a
|
||||||
|
`__bool__` method that would invalidate whatever conclusion we drew from `__len__`. So instances of
|
||||||
|
classes without a `__bool__` method, with or without `__len__`, must be inferred as unknown
|
||||||
|
truthiness.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class AlwaysTrue:
|
||||||
|
def __bool__(self) -> Literal[True]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# revealed: Literal[False]
|
||||||
|
reveal_type(not AlwaysTrue())
|
||||||
|
|
||||||
|
class AlwaysFalse:
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# revealed: Literal[True]
|
||||||
|
reveal_type(not AlwaysFalse())
|
||||||
|
|
||||||
|
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||||
|
class BoolIsBool:
|
||||||
|
__bool__ = bool
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not BoolIsBool())
|
||||||
|
|
||||||
|
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
|
||||||
|
# a subclass could add a `__bool__` method.
|
||||||
|
class NoBoolMethod: ...
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not NoBoolMethod())
|
||||||
|
|
||||||
|
# And we can't rely on `__len__` for the same reason: a subclass could add `__bool__`.
|
||||||
|
class LenZero:
|
||||||
|
def __len__(self) -> Literal[0]:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not LenZero())
|
||||||
|
|
||||||
|
class LenNonZero:
|
||||||
|
def __len__(self) -> Literal[1]:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not LenNonZero())
|
||||||
|
|
||||||
|
class WithBothLenAndBool1:
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __len__(self) -> Literal[2]:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# revealed: Literal[True]
|
||||||
|
reveal_type(not WithBothLenAndBool1())
|
||||||
|
|
||||||
|
class WithBothLenAndBool2:
|
||||||
|
def __bool__(self) -> Literal[True]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __len__(self) -> Literal[0]:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# revealed: Literal[False]
|
||||||
|
reveal_type(not WithBothLenAndBool2())
|
||||||
|
|
||||||
|
# TODO: raise diagnostic when __bool__ method is not valid: [unsupported-operator] "Method __bool__ for type `MethodBoolInvalid` should return `bool`, returned type `int`"
|
||||||
|
# https://docs.python.org/3/reference/datamodel.html#object.__bool__
|
||||||
|
class MethodBoolInvalid:
|
||||||
|
def __bool__(self) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not MethodBoolInvalid())
|
||||||
|
|
||||||
|
# Don't trust a possibly-unbound `__bool__` method:
|
||||||
|
def get_flag() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class PossiblyUnboundBool:
|
||||||
|
if get_flag():
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# revealed: bool
|
||||||
|
reveal_type(not PossiblyUnboundBool())
|
||||||
|
```
|
||||||
|
|
|
@ -1197,14 +1197,40 @@ impl<'db> Type<'db> {
|
||||||
// TODO: see above
|
// TODO: see above
|
||||||
Truthiness::Ambiguous
|
Truthiness::Ambiguous
|
||||||
}
|
}
|
||||||
Type::Instance(InstanceType { class }) => {
|
instance_ty @ Type::Instance(InstanceType { class }) => {
|
||||||
// TODO: lookup `__bool__` and `__len__` methods on the instance's class
|
|
||||||
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
|
|
||||||
// For now, we only special-case some builtin classes
|
|
||||||
if class.is_known(db, KnownClass::NoneType) {
|
if class.is_known(db, KnownClass::NoneType) {
|
||||||
Truthiness::AlwaysFalse
|
Truthiness::AlwaysFalse
|
||||||
} else {
|
} else {
|
||||||
Truthiness::Ambiguous
|
// We only check the `__bool__` method for truth testing, even though at
|
||||||
|
// runtime there is a fallback to `__len__`, since `__bool__` takes precedence
|
||||||
|
// and a subclass could add a `__bool__` method. We don't use
|
||||||
|
// `Type::call_dunder` here because of the need to check for `__bool__ = bool`.
|
||||||
|
|
||||||
|
// Don't trust a maybe-unbound `__bool__` method.
|
||||||
|
let Symbol::Type(bool_method, Boundness::Bound) =
|
||||||
|
instance_ty.to_meta_type(db).member(db, "__bool__")
|
||||||
|
else {
|
||||||
|
return Truthiness::Ambiguous;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the class has `__bool__ = bool` and avoid infinite recursion, since
|
||||||
|
// `Type::call` on `bool` will call `Type::bool` on the argument.
|
||||||
|
if bool_method
|
||||||
|
.into_class_literal()
|
||||||
|
.is_some_and(|ClassLiteralType { class }| {
|
||||||
|
class.is_known(db, KnownClass::Bool)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Truthiness::Ambiguous;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Type::BooleanLiteral(bool_val)) =
|
||||||
|
bool_method.call(db, &[*instance_ty]).return_ty(db)
|
||||||
|
{
|
||||||
|
bool_val.into()
|
||||||
|
} else {
|
||||||
|
Truthiness::Ambiguous
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::KnownInstance(known_instance) => known_instance.bool(),
|
Type::KnownInstance(known_instance) => known_instance.bool(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue