[red-knot] Fix Stack overflow in Type::bool (#15843)

## Summary

This PR adds `Type::call_bound` method for calls that should follow
descriptor protocol calling convention. The PR is intentionally shallow
in scope and only fixes #15672

Couple of obvious things that weren't done:

* Switch to `call_bound` everywhere it should be used
* Address the fact, that red_knot resolves `__bool__ = bool` as a Union,
which includes `Type::Dynamic` and hence fails to infer that the
truthiness is always false for such a class (I've added a todo comment
in mdtests)
* Doesn't try to invent a new type for descriptors, although I have a
gut feeling it may be more convenient in the end, instead of doing
method lookup each time like I did in `call_bound`

## Test Plan

* extended mdtests with 2 examples from the issue
* cargo neatest run
This commit is contained in:
Mike Perlov
2025-02-04 15:40:07 -05:00
committed by GitHub
parent 444b055cec
commit e15419396c
3 changed files with 81 additions and 21 deletions

View File

@@ -1,5 +1,7 @@
# Truthiness
## Literals
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import AlwaysFalsy, AlwaysTruthy
@@ -45,3 +47,31 @@ def _(
reveal_type(bool(c)) # revealed: bool
reveal_type(bool(d)) # revealed: bool
```
## Instances
Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
### __bool__ is bool
```py
class BoolIsBool:
__bool__ = bool
reveal_type(bool(BoolIsBool())) # revealed: bool
```
### Conditional __bool__ method
```py
def flag() -> bool:
return True
class Boom:
if flag():
__bool__ = bool
else:
__bool__ = int
reveal_type(bool(Boom())) # revealed: bool
```

View File

@@ -141,15 +141,6 @@ class AlwaysFalse:
# 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:
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
# https://github.com/astral-sh/ruff/issues/15672
__bool__: type[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: ...