mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-02 18:02:23 +00:00
[red-knot] Infer unary not operation for instances (#13827)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz (push) Blocked by required conditions
CI / Fuzz the parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz (push) Blocked by required conditions
CI / Fuzz the parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
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
|
||||
from typing import Literal
|
|
@ -113,3 +113,101 @@ reveal_type(not ()) # revealed: Literal[True]
|
|||
reveal_type(not ("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
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
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
|
||||
instance_ty @ Type::Instance(InstanceType { class }) => {
|
||||
if class.is_known(db, KnownClass::NoneType) {
|
||||
Truthiness::AlwaysFalse
|
||||
} 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(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue