Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ def foo() -> int:
return 42

def decorator(func) -> Callable[[], int]:
# TODO: no error
# error: [invalid-return-type]
return foo

@decorator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,26 +494,55 @@ Return types are covariant.

```py
from typing import Callable
from knot_extensions import is_subtype_of, static_assert
from knot_extensions import is_subtype_of, static_assert, TypeOf

static_assert(is_subtype_of(Callable[[], int], Callable[[], float]))
static_assert(not is_subtype_of(Callable[[], float], Callable[[], int]))
```

### Optional return type

```py
from typing import Callable
from knot_extensions import is_subtype_of, static_assert, TypeOf

flag: bool = True

def optional_return_type() -> int | None:
if flag:
return 1
return None

def required_return_type() -> int:
return 1

static_assert(not is_subtype_of(TypeOf[optional_return_type], TypeOf[required_return_type]))
# TypeOf[some_function] is a singleton function-literal type, not a general callable type
static_assert(not is_subtype_of(TypeOf[required_return_type], TypeOf[optional_return_type]))
static_assert(is_subtype_of(TypeOf[optional_return_type], Callable[[], int | None]))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For good measure, we could assert this is not true the other way around.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant both, but after reviewing the rest of the PR I think this is already adequately tested.

```

### Parameter types

Parameter types are contravariant.

#### Positional-only

```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
from typing import Callable
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf

def float_param(a: float, /) -> None: ...
def int_param(a: int, /) -> None: ...

static_assert(is_subtype_of(CallableTypeFromFunction[float_param], CallableTypeFromFunction[int_param]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_param], CallableTypeFromFunction[float_param]))

static_assert(is_subtype_of(TypeOf[int_param], Callable[[int], None]))
static_assert(is_subtype_of(TypeOf[float_param], Callable[[float], None]))

static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_param]))
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_param]))
```

Parameter name is not required to be the same for positional-only parameters at the same position:
Expand All @@ -533,6 +562,10 @@ def multi_param2(b: int, c: bool, a: str, /) -> None: ...

static_assert(is_subtype_of(CallableTypeFromFunction[multi_param1], CallableTypeFromFunction[multi_param2]))
static_assert(not is_subtype_of(CallableTypeFromFunction[multi_param2], CallableTypeFromFunction[multi_param1]))

static_assert(is_subtype_of(TypeOf[multi_param1], Callable[[float, int, str], None]))

static_assert(not is_subtype_of(Callable[[float, int, str], None], TypeOf[multi_param1]))
```

#### Positional-only with default value
Expand All @@ -541,7 +574,8 @@ If the parameter has a default value, it's treated as optional. This means that
corresponding position in the supertype does not need to have a default value.

```py
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert
from typing import Callable
from knot_extensions import CallableTypeFromFunction, is_subtype_of, static_assert, TypeOf

def float_with_default(a: float = 1, /) -> None: ...
def int_with_default(a: int = 1, /) -> None: ...
Expand All @@ -552,6 +586,13 @@ static_assert(not is_subtype_of(CallableTypeFromFunction[int_with_default], Call

static_assert(is_subtype_of(CallableTypeFromFunction[int_with_default], CallableTypeFromFunction[int_without_default]))
static_assert(not is_subtype_of(CallableTypeFromFunction[int_without_default], CallableTypeFromFunction[int_with_default]))

static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[int], None]))
static_assert(is_subtype_of(TypeOf[int_with_default], Callable[[], None]))
static_assert(is_subtype_of(TypeOf[float_with_default], Callable[[float], None]))

static_assert(not is_subtype_of(Callable[[int], None], TypeOf[int_with_default]))
static_assert(not is_subtype_of(Callable[[float], None], TypeOf[float_with_default]))
```

As the parameter itself is optional, it can be omitted in the supertype:
Expand Down
8 changes: 6 additions & 2 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,14 @@ impl<'db> Type<'db> {
KnownClass::Slice.to_instance(db).is_subtype_of(db, target)
}

(Type::FunctionLiteral(self_function_literal), Type::Callable(_)) => {
self_function_literal
.into_callable_type(db)
.is_subtype_of(db, target)
}

// A `FunctionLiteral` type is a single-valued type like the other literals handled above,
// so it also, for now, just delegates to its instance fallback.
// This will change in a way similar to the `LiteralString`/`StringLiteral()` case above
// when we add support for `typing.Callable`.
(Type::FunctionLiteral(_), _) => KnownClass::FunctionType
.to_instance(db)
.is_subtype_of(db, target),
Expand Down
Loading