ruff/crates/red_knot_python_semantic/resources/mdtest/expression/boolean.md
Douglas Creager 23ccb52fa6
[red-knot] Handle unions of callables better (#16716)
This cleans up how we handle calling unions of types. #16568 adding a
three-level structure for callable signatures (`Signatures`,
`CallableSignature`, and `Signature`) to handle unions and overloads.

This PR updates the bindings side to mimic that structure. What used to
be called `CallOutcome` is now `Bindings`, and represents the result of
binding actual arguments against a possible union of callables.
`CallableBinding` is the result of binding a single, possibly
overloaded, callable type. `Binding` is the result of binding a single
overload.

While we're here, this also cleans up `CallError` greatly. It was
previously extracting error information from the bindings and storing it
in the error result. It is now a simple enum, carrying no data, that's
used as a status code to talk about whether the overall binding was
successful or not. We are now more consistent about walking the binding
itself to get detailed information about _how_ the binding was
unsucessful.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
2025-03-17 10:35:52 -04:00

3.9 KiB

Expressions

OR

def _(foo: str):
    reveal_type(True or False)  # revealed: Literal[True]
    reveal_type("x" or "y" or "z")  # revealed: Literal["x"]
    reveal_type("" or "y" or "z")  # revealed: Literal["y"]
    reveal_type(False or "z")  # revealed: Literal["z"]
    reveal_type(False or True)  # revealed: Literal[True]
    reveal_type(False or False)  # revealed: Literal[False]
    reveal_type(foo or False)  # revealed: str & ~AlwaysFalsy | Literal[False]
    reveal_type(foo or True)  # revealed: str & ~AlwaysFalsy | Literal[True]

AND

def _(foo: str):
    reveal_type(True and False)  # revealed: Literal[False]
    reveal_type(False and True)  # revealed: Literal[False]
    reveal_type(foo and False)  # revealed: str & ~AlwaysTruthy | Literal[False]
    reveal_type(foo and True)  # revealed: str & ~AlwaysTruthy | Literal[True]
    reveal_type("x" and "y" and "z")  # revealed: Literal["z"]
    reveal_type("x" and "y" and "")  # revealed: Literal[""]
    reveal_type("" and "y")  # revealed: Literal[""]

Simple function calls to bool

def _(flag: bool):
    if flag:
        x = True
    else:
        x = False

    reveal_type(x)  # revealed: bool

Complex

reveal_type("x" and "y" or "z")  # revealed: Literal["y"]
reveal_type("x" or "y" and "z")  # revealed: Literal["x"]
reveal_type("" and "y" or "z")  # revealed: Literal["z"]
reveal_type("" or "y" and "z")  # revealed: Literal["z"]
reveal_type("x" and "y" or "")  # revealed: Literal["y"]
reveal_type("x" or "y" and "")  # revealed: Literal["x"]

bool() function

Evaluates to builtin

a.py:

redefined_builtin_bool: type[bool] = bool

def my_bool(x) -> bool:
    return True
from a import redefined_builtin_bool, my_bool

reveal_type(redefined_builtin_bool(0))  # revealed: Literal[False]
reveal_type(my_bool(0))  # revealed: bool

Truthy values

reveal_type(bool(1))  # revealed: Literal[True]
reveal_type(bool((0,)))  # revealed: Literal[True]
reveal_type(bool("NON EMPTY"))  # revealed: Literal[True]
reveal_type(bool(True))  # revealed: Literal[True]

def foo(): ...

reveal_type(bool(foo))  # revealed: Literal[True]

Falsy values

reveal_type(bool(0))  # revealed: Literal[False]
reveal_type(bool(()))  # revealed: Literal[False]
reveal_type(bool(None))  # revealed: Literal[False]
reveal_type(bool(""))  # revealed: Literal[False]
reveal_type(bool(False))  # revealed: Literal[False]
reveal_type(bool())  # revealed: Literal[False]

Ambiguous values

reveal_type(bool([]))  # revealed: bool
reveal_type(bool({}))  # revealed: bool
reveal_type(bool(set()))  # revealed: bool

__bool__ returning NoReturn

from typing import NoReturn

class NotBoolable:
    def __bool__(self) -> NoReturn:
        raise NotImplementedError("This object can't be converted to a boolean")

# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
#   because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
if NotBoolable():
    ...

Not callable __bool__

class NotBoolable:
    __bool__: None = None

# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
if NotBoolable():
    ...

Not-boolable union

def test(cond: bool):
    class NotBoolable:
        __bool__: int | None = None if cond else 3

    # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
    if NotBoolable():
        ...

Union with some variants implementing __bool__ incorrectly

def test(cond: bool):
    class NotBoolable:
        __bool__: None = None

    a = 10 if cond else NotBoolable()

    # error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`; its `__bool__` method isn't callable"
    if a:
        ...