mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 18:53:25 +00:00
[ty] Consider __len__ when determining the truthiness of an instance of a tuple class or a @final class (#21049)
This commit is contained in:
parent
4522f35ea7
commit
e196c2ab37
6 changed files with 349 additions and 65 deletions
|
|
@ -1293,6 +1293,156 @@ class Test:
|
|||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __invert__(self) -> 'Test': ...
|
||||
|
||||
a = Test()
|
||||
|
||||
<CURSOR>~a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __invert__(self) -> 'Test': ...
|
||||
| ^^^^^^^^^^
|
||||
4 |
|
||||
5 | a = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | a = Test()
|
||||
6 |
|
||||
7 | ~a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// We jump to the `__invert__` definition here even though its signature is incorrect.
|
||||
#[test]
|
||||
fn goto_definition_unary_operator_with_bad_dunder_definition() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __invert__(self, extra_arg) -> 'Test': ...
|
||||
|
||||
a = Test()
|
||||
|
||||
<CURSOR>~a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __invert__(self, extra_arg) -> 'Test': ...
|
||||
| ^^^^^^^^^^
|
||||
4 |
|
||||
5 | a = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | a = Test()
|
||||
6 |
|
||||
7 | ~a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_definition_unary_after_operator() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __invert__(self) -> 'Test': ...
|
||||
|
||||
a = Test()
|
||||
|
||||
~<CURSOR> a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __invert__(self) -> 'Test': ...
|
||||
| ^^^^^^^^^^
|
||||
4 |
|
||||
5 | a = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | a = Test()
|
||||
6 |
|
||||
7 | ~ a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_definition_unary_between_operator_and_operand() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __invert__(self) -> 'Test': ...
|
||||
|
||||
a = Test()
|
||||
|
||||
-<CURSOR>a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:5:1
|
||||
|
|
||||
3 | def __invert__(self) -> 'Test': ...
|
||||
4 |
|
||||
5 | a = Test()
|
||||
| ^
|
||||
6 |
|
||||
7 | -a
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:2
|
||||
|
|
||||
5 | a = Test()
|
||||
6 |
|
||||
7 | -a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_definition_unary_not_with_dunder_bool() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __bool__(self) -> bool: ...
|
||||
|
||||
|
|
@ -1325,17 +1475,17 @@ a = Test()
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn goto_definition_unary_after_operator() {
|
||||
fn goto_definition_unary_not_with_dunder_len() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __bool__(self) -> bool: ...
|
||||
def __len__(self) -> 42: ...
|
||||
|
||||
a = Test()
|
||||
|
||||
not<CURSOR> a
|
||||
<CURSOR>not a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
|
@ -1345,8 +1495,8 @@ not<CURSOR> a
|
|||
--> main.py:3:9
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __bool__(self) -> bool: ...
|
||||
| ^^^^^^^^
|
||||
3 | def __len__(self) -> 42: ...
|
||||
| ^^^^^^^
|
||||
4 |
|
||||
5 | a = Test()
|
||||
|
|
||||
|
|
@ -1361,40 +1511,83 @@ not<CURSOR> a
|
|||
");
|
||||
}
|
||||
|
||||
/// If `__bool__` is defined incorrectly, `not` does not fallback to `__len__`.
|
||||
/// Instead, we jump to the `__bool__` definition as usual.
|
||||
/// The fallback only occurs if `__bool__` is not defined at all.
|
||||
#[test]
|
||||
fn goto_definition_unary_between_operator_and_operand() {
|
||||
fn goto_definition_unary_not_with_bad_dunder_bool_and_dunder_len() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __bool__(self) -> bool: ...
|
||||
def __bool__(self, extra_arg) -> bool: ...
|
||||
def __len__(self) -> 42: ...
|
||||
|
||||
a = Test()
|
||||
|
||||
-<CURSOR>a
|
||||
<CURSOR>not a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:5:1
|
||||
--> main.py:3:9
|
||||
|
|
||||
3 | def __bool__(self) -> bool: ...
|
||||
4 |
|
||||
5 | a = Test()
|
||||
| ^
|
||||
6 |
|
||||
7 | -a
|
||||
2 | class Test:
|
||||
3 | def __bool__(self, extra_arg) -> bool: ...
|
||||
| ^^^^^^^^
|
||||
4 | def __len__(self) -> 42: ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:2
|
||||
--> main.py:8:1
|
||||
|
|
||||
6 | a = Test()
|
||||
7 |
|
||||
8 | not a
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// Same as for unary operators that only use a single dunder,
|
||||
/// we still jump to `__len__` for `not` goto-definition even if
|
||||
/// the `__len__` signature is incorrect (but only if there is no
|
||||
/// `__bool__` definition).
|
||||
#[test]
|
||||
fn goto_definition_unary_not_with_no_dunder_bool_and_bad_dunder_len() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
class Test:
|
||||
def __len__(self, extra_arg) -> 42: ...
|
||||
|
||||
a = Test()
|
||||
|
||||
<CURSOR>not a
|
||||
",
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_definition(), @r"
|
||||
info[goto-definition]: Definition
|
||||
--> main.py:3:9
|
||||
|
|
||||
2 | class Test:
|
||||
3 | def __len__(self, extra_arg) -> 42: ...
|
||||
| ^^^^^^^
|
||||
4 |
|
||||
5 | a = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:1
|
||||
|
|
||||
5 | a = Test()
|
||||
6 |
|
||||
7 | -a
|
||||
| ^
|
||||
7 | not a
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ python-version = "3.11"
|
|||
```
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from typing import Literal, final
|
||||
|
||||
reveal_type(bool(1)) # revealed: Literal[True]
|
||||
reveal_type(bool((0,))) # revealed: Literal[True]
|
||||
|
|
@ -92,15 +92,11 @@ reveal_type(bool(foo)) # revealed: Literal[True]
|
|||
class SingleElementTupleSubclass(tuple[int]): ...
|
||||
|
||||
reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True]
|
||||
reveal_type(SingleElementTupleSubclass((1,)).__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length, but we know the length is guaranteed to be >=2
|
||||
class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ...
|
||||
|
||||
reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True]
|
||||
reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True]
|
||||
reveal_type(MixedTupleSubclass((1, b"foo")).__bool__) # revealed: () -> Literal[True]
|
||||
|
||||
# Unknown length with an overridden `__bool__`:
|
||||
class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
|
||||
|
|
@ -108,10 +104,6 @@ class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]):
|
|||
return True
|
||||
|
||||
reveal_type(bool(VariadicTupleSubclassWithDunderBoolOverride((1,)))) # revealed: Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def __bool__(self) -> Literal[True]
|
||||
|
||||
# revealed: bound method VariadicTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(VariadicTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
|
||||
# Same again but for a subclass of a fixed-length tuple:
|
||||
class EmptyTupleSubclassWithDunderBoolOverride(tuple[()]):
|
||||
|
|
@ -124,11 +116,28 @@ reveal_type(EmptyTupleSubclassWithDunderBoolOverride.__bool__) # revealed: def
|
|||
|
||||
# revealed: bound method EmptyTupleSubclassWithDunderBoolOverride.__bool__() -> Literal[True]
|
||||
reveal_type(EmptyTupleSubclassWithDunderBoolOverride().__bool__)
|
||||
|
||||
@final
|
||||
class FinalClassOverridingLenAndNotBool:
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[True]
|
||||
|
||||
@final
|
||||
class FinalClassWithNoLenOrBool: ...
|
||||
|
||||
reveal_type(bool(FinalClassWithNoLenOrBool())) # revealed: Literal[True]
|
||||
|
||||
def f(x: SingleElementTupleSubclass | FinalClassOverridingLenAndNotBool | FinalClassWithNoLenOrBool):
|
||||
reveal_type(bool(x)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
|
||||
```py
|
||||
from typing import final, Literal
|
||||
|
||||
reveal_type(bool(0)) # revealed: Literal[False]
|
||||
reveal_type(bool(())) # revealed: Literal[False]
|
||||
reveal_type(bool(None)) # revealed: Literal[False]
|
||||
|
|
@ -139,13 +148,23 @@ reveal_type(bool()) # revealed: Literal[False]
|
|||
class EmptyTupleSubclass(tuple[()]): ...
|
||||
|
||||
reveal_type(bool(EmptyTupleSubclass())) # revealed: Literal[False]
|
||||
reveal_type(EmptyTupleSubclass.__bool__) # revealed: (self: tuple[()], /) -> Literal[False]
|
||||
reveal_type(EmptyTupleSubclass().__bool__) # revealed: () -> Literal[False]
|
||||
|
||||
@final
|
||||
class FinalClassOverridingLenAndNotBool:
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
reveal_type(bool(FinalClassOverridingLenAndNotBool())) # revealed: Literal[False]
|
||||
|
||||
def f(x: EmptyTupleSubclass | FinalClassOverridingLenAndNotBool):
|
||||
reveal_type(bool(x)) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
|
|
@ -154,8 +173,15 @@ class VariadicTupleSubclass(tuple[int, ...]): ...
|
|||
|
||||
def f(x: tuple[int, ...], y: VariadicTupleSubclass):
|
||||
reveal_type(bool(x)) # revealed: bool
|
||||
reveal_type(x.__bool__) # revealed: () -> bool
|
||||
reveal_type(y.__bool__) # revealed: () -> bool
|
||||
|
||||
class NonFinalOverridingLenAndNotBool:
|
||||
def __len__(self) -> Literal[42]:
|
||||
return 42
|
||||
|
||||
# We cannot consider `__len__` for a non-`@final` type,
|
||||
# because a subclass might override `__bool__`,
|
||||
# and `__bool__` takes precedence over `__len__`
|
||||
reveal_type(bool(NonFinalOverridingLenAndNotBool())) # revealed: bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ def _(flag: bool):
|
|||
## Narrowing in `and`
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
def _(flag: bool):
|
||||
class A: ...
|
||||
x: A | None = A() if flag else None
|
||||
|
|
@ -28,6 +30,15 @@ def _(flag: bool):
|
|||
isinstance(x, A) and reveal_type(x) # revealed: A
|
||||
x is None and reveal_type(x) # revealed: None
|
||||
reveal_type(x) # revealed: A | None
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
# We know that no subclass of `FinalClass` can exist,
|
||||
# therefore no subtype of `FinalClass` can define `__bool__`
|
||||
# or `__len__`, therefore `FinalClass` can safely be considered
|
||||
# always-truthy, therefore this always resolves to `None`
|
||||
reveal_type(FinalClass() and None) # revealed: None
|
||||
```
|
||||
|
||||
## Multiple `and` arms
|
||||
|
|
|
|||
|
|
@ -4463,18 +4463,15 @@ impl<'db> Type<'db> {
|
|||
visitor: &TryBoolVisitor<'db>,
|
||||
) -> Result<Truthiness, BoolError<'db>> {
|
||||
let type_to_truthiness = |ty| {
|
||||
if let Type::BooleanLiteral(bool_val) = ty {
|
||||
Truthiness::from(bool_val)
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
match ty {
|
||||
Type::BooleanLiteral(bool_val) => Truthiness::from(bool_val),
|
||||
Type::IntLiteral(int_val) => Truthiness::from(int_val != 0),
|
||||
// anything else is handled lower down
|
||||
_ => Truthiness::Ambiguous,
|
||||
}
|
||||
};
|
||||
|
||||
let try_dunder_bool = || {
|
||||
// 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.
|
||||
|
||||
let try_dunders = || {
|
||||
match self.try_call_dunder(
|
||||
db,
|
||||
"__bool__",
|
||||
|
|
@ -4509,18 +4506,67 @@ impl<'db> Type<'db> {
|
|||
Ok(Truthiness::Ambiguous)
|
||||
}
|
||||
|
||||
Err(CallDunderError::MethodNotAvailable) => Ok(Truthiness::Ambiguous),
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
// We only consider `__len__` for tuples and `@final` types,
|
||||
// since `__bool__` takes precedence
|
||||
// and a subclass could add a `__bool__` method.
|
||||
//
|
||||
// TODO: with regards to tuple types, we intend to emit a diagnostic
|
||||
// if a tuple subclass defines a `__bool__` method with a return type
|
||||
// that is inconsistent with the tuple's length. Otherwise, the special
|
||||
// handling for tuples here isn't sound.
|
||||
if let Some(instance) = self.into_nominal_instance() {
|
||||
if let Some(tuple_spec) = instance.tuple_spec(db) {
|
||||
Ok(tuple_spec.truthiness())
|
||||
} else if instance.class(db).is_final(db) {
|
||||
match self.try_call_dunder(
|
||||
db,
|
||||
"__len__",
|
||||
CallArguments::none(),
|
||||
TypeContext::default(),
|
||||
) {
|
||||
Ok(outcome) => {
|
||||
let return_type = outcome.return_type(db);
|
||||
if return_type.is_assignable_to(
|
||||
db,
|
||||
KnownClass::SupportsIndex.to_instance(db),
|
||||
) {
|
||||
Ok(type_to_truthiness(return_type))
|
||||
} else {
|
||||
// TODO: should report a diagnostic similar to if return type of `__bool__`
|
||||
// is not assignable to `bool`
|
||||
Ok(Truthiness::Ambiguous)
|
||||
}
|
||||
}
|
||||
// if a `@final` type does not define `__bool__` or `__len__`, it is always truthy
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
Ok(Truthiness::AlwaysTrue)
|
||||
}
|
||||
// TODO: errors during a `__len__` call (if `__len__` exists) should be reported
|
||||
// as diagnostics similar to errors during a `__bool__` call (when `__bool__` exists)
|
||||
Err(_) => Ok(Truthiness::Ambiguous),
|
||||
}
|
||||
} else {
|
||||
Ok(Truthiness::Ambiguous)
|
||||
}
|
||||
} else {
|
||||
Ok(Truthiness::Ambiguous)
|
||||
}
|
||||
}
|
||||
|
||||
Err(CallDunderError::CallError(CallErrorKind::BindingError, bindings)) => {
|
||||
Err(BoolError::IncorrectArguments {
|
||||
truthiness: type_to_truthiness(bindings.return_type(db)),
|
||||
not_boolable_type: *self,
|
||||
})
|
||||
}
|
||||
|
||||
Err(CallDunderError::CallError(CallErrorKind::NotCallable, _)) => {
|
||||
Err(BoolError::NotCallable {
|
||||
not_boolable_type: *self,
|
||||
})
|
||||
}
|
||||
|
||||
Err(CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _)) => {
|
||||
Err(BoolError::Other {
|
||||
not_boolable_type: *self,
|
||||
|
|
@ -4635,9 +4681,9 @@ impl<'db> Type<'db> {
|
|||
.known_class(db)
|
||||
.and_then(KnownClass::bool)
|
||||
.map(Ok)
|
||||
.unwrap_or_else(try_dunder_bool)?,
|
||||
.unwrap_or_else(try_dunders)?,
|
||||
|
||||
Type::ProtocolInstance(_) => try_dunder_bool()?,
|
||||
Type::ProtocolInstance(_) => try_dunders()?,
|
||||
|
||||
Type::Union(union) => try_union(*union)?,
|
||||
|
||||
|
|
|
|||
|
|
@ -745,17 +745,6 @@ impl<'db> ClassType<'db> {
|
|||
})
|
||||
};
|
||||
|
||||
let synthesize_simple_tuple_method = |return_type| {
|
||||
let parameters =
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(Type::instance(db, self))]);
|
||||
|
||||
let synthesized_dunder_method =
|
||||
CallableType::function_like(db, Signature::new(parameters, Some(return_type)));
|
||||
|
||||
Member::definitely_declared(synthesized_dunder_method)
|
||||
};
|
||||
|
||||
match name {
|
||||
"__len__" if class_literal.is_tuple(db) => {
|
||||
let return_type = specialization
|
||||
|
|
@ -765,16 +754,14 @@ impl<'db> ClassType<'db> {
|
|||
.map(Type::IntLiteral)
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(db));
|
||||
|
||||
synthesize_simple_tuple_method(return_type)
|
||||
}
|
||||
let parameters =
|
||||
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(Type::instance(db, self))]);
|
||||
|
||||
"__bool__" if class_literal.is_tuple(db) => {
|
||||
let return_type = specialization
|
||||
.and_then(|spec| spec.tuple(db))
|
||||
.map(|tuple| tuple.truthiness().into_type(db))
|
||||
.unwrap_or_else(|| KnownClass::Bool.to_instance(db));
|
||||
let synthesized_dunder_method =
|
||||
CallableType::function_like(db, Signature::new(parameters, Some(return_type)));
|
||||
|
||||
synthesize_simple_tuple_method(return_type)
|
||||
Member::definitely_declared(synthesized_dunder_method)
|
||||
}
|
||||
|
||||
"__getitem__" if class_literal.is_tuple(db) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::semantic_index::scope::ScopeId;
|
|||
use crate::semantic_index::{
|
||||
attribute_scopes, global_scope, place_table, semantic_index, use_def_map,
|
||||
};
|
||||
use crate::types::CallDunderError;
|
||||
use crate::types::call::{CallArguments, MatchedArgument};
|
||||
use crate::types::signatures::Signature;
|
||||
use crate::types::{
|
||||
|
|
@ -973,13 +974,33 @@ pub fn definitions_for_unary_op<'db>(
|
|||
ast::UnaryOp::Not => "__bool__",
|
||||
};
|
||||
|
||||
let Ok(bindings) = operand_ty.try_call_dunder(
|
||||
let bindings = match operand_ty.try_call_dunder(
|
||||
db,
|
||||
unary_dunder_method,
|
||||
CallArguments::none(),
|
||||
TypeContext::default(),
|
||||
) else {
|
||||
return None;
|
||||
) {
|
||||
Ok(bindings) => bindings,
|
||||
Err(CallDunderError::MethodNotAvailable) if unary_op.op == ast::UnaryOp::Not => {
|
||||
// The runtime falls back to `__len__` for `not` if `__bool__` is not defined.
|
||||
match operand_ty.try_call_dunder(
|
||||
db,
|
||||
"__len__",
|
||||
CallArguments::none(),
|
||||
TypeContext::default(),
|
||||
) {
|
||||
Ok(bindings) => bindings,
|
||||
Err(CallDunderError::MethodNotAvailable) => return None,
|
||||
Err(
|
||||
CallDunderError::PossiblyUnbound(bindings)
|
||||
| CallDunderError::CallError(_, bindings),
|
||||
) => *bindings,
|
||||
}
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => return None,
|
||||
Err(
|
||||
CallDunderError::PossiblyUnbound(bindings) | CallDunderError::CallError(_, bindings),
|
||||
) => *bindings,
|
||||
};
|
||||
|
||||
let callable_type = promote_literals_for_self(db, bindings.callable_type());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue