Skip to content

Commit 1cffb32

Browse files
authored
[red-knot] Check assignability for two callable types (#16845)
## Summary Part of #15382 This PR adds support for checking the assignability of two general callable types. This is built on top of #16804 by including the gradual parameters check and accepting a function that performs the check between the two types. ## Test Plan Update `is_assignable_to.md` with callable types section.
1 parent 92028ef commit 1cffb32

File tree

3 files changed

+149
-17
lines changed

3 files changed

+149
-17
lines changed

crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,22 @@ expression.
9898
```py
9999
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
100100
```
101+
102+
## Assignment
103+
104+
This does not enumerate all combinations of parameter kinds as that should be covered by the
105+
[subtype tests for callable types](./../type_properties/is_subtype_of.md#callable).
106+
107+
```py
108+
from typing import Callable
109+
110+
a1: Callable[[], None] = lambda: None
111+
a2: Callable[[int], None] = lambda x: None
112+
a3: Callable[[int, int], None] = lambda x, y, z=1: None
113+
a4: Callable[[int, int], None] = lambda *args: None
114+
115+
# error: [invalid-assignment]
116+
a5: Callable[[], None] = lambda x: None
117+
# error: [invalid-assignment]
118+
a6: Callable[[int], None] = lambda: None
119+
```

crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,87 @@ static_assert(is_assignable_to(Never, type[str]))
393393
static_assert(is_assignable_to(Never, type[Any]))
394394
```
395395

396+
## Callable
397+
398+
The examples provided below are only a subset of the possible cases and include the ones with
399+
gradual types. The cases with fully static types and using different combinations of parameter kinds
400+
are covered in the [subtyping tests](./is_subtype_of.md#callable).
401+
402+
### Return type
403+
404+
```py
405+
from knot_extensions import CallableTypeFromFunction, Unknown, static_assert, is_assignable_to
406+
from typing import Any, Callable
407+
408+
static_assert(is_assignable_to(Callable[[], Any], Callable[[], int]))
409+
static_assert(is_assignable_to(Callable[[], int], Callable[[], Any]))
410+
411+
static_assert(is_assignable_to(Callable[[], int], Callable[[], float]))
412+
static_assert(not is_assignable_to(Callable[[], float], Callable[[], int]))
413+
```
414+
415+
The return types should be checked even if the parameter types uses gradual form (`...`).
416+
417+
```py
418+
static_assert(is_assignable_to(Callable[..., int], Callable[..., float]))
419+
static_assert(not is_assignable_to(Callable[..., float], Callable[..., int]))
420+
```
421+
422+
And, if there is no return type, the return type is `Unknown`.
423+
424+
```py
425+
static_assert(is_assignable_to(Callable[[], Unknown], Callable[[], int]))
426+
static_assert(is_assignable_to(Callable[[], int], Callable[[], Unknown]))
427+
```
428+
429+
### Parameter types
430+
431+
A `Callable` which uses the gradual form (`...`) for the parameter types is consistent with any
432+
input signature.
433+
434+
```py
435+
from knot_extensions import CallableTypeFromFunction, static_assert, is_assignable_to
436+
from typing import Any, Callable
437+
438+
static_assert(is_assignable_to(Callable[[], None], Callable[..., None]))
439+
static_assert(is_assignable_to(Callable[..., None], Callable[..., None]))
440+
static_assert(is_assignable_to(Callable[[int, float, str], None], Callable[..., None]))
441+
```
442+
443+
Even if it includes any other parameter kinds.
444+
445+
```py
446+
def positional_only(a: int, b: int, /) -> None: ...
447+
def positional_or_keyword(a: int, b: int) -> None: ...
448+
def variadic(*args: int) -> None: ...
449+
def keyword_only(*, a: int, b: int) -> None: ...
450+
def keyword_variadic(**kwargs: int) -> None: ...
451+
def mixed(a: int, /, b: int, *args: int, c: int, **kwargs: int) -> None: ...
452+
453+
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
454+
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
455+
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
456+
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
457+
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
458+
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
459+
```
460+
461+
And, even if the parameters are unannotated.
462+
463+
```py
464+
def positional_only(a, b, /) -> None: ...
465+
def positional_or_keyword(a, b) -> None: ...
466+
def variadic(*args) -> None: ...
467+
def keyword_only(*, a, b) -> None: ...
468+
def keyword_variadic(**kwargs) -> None: ...
469+
def mixed(a, /, b, *args, c, **kwargs) -> None: ...
470+
471+
static_assert(is_assignable_to(CallableTypeFromFunction[positional_only], Callable[..., None]))
472+
static_assert(is_assignable_to(CallableTypeFromFunction[positional_or_keyword], Callable[..., None]))
473+
static_assert(is_assignable_to(CallableTypeFromFunction[variadic], Callable[..., None]))
474+
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_only], Callable[..., None]))
475+
static_assert(is_assignable_to(CallableTypeFromFunction[keyword_variadic], Callable[..., None]))
476+
static_assert(is_assignable_to(CallableTypeFromFunction[mixed], Callable[..., None]))
477+
```
478+
396479
[typing documentation]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation

crates/red_knot_python_semantic/src/types.rs

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,11 @@ impl<'db> Type<'db> {
887887
}
888888
}
889889

890+
(
891+
Type::Callable(CallableType::General(self_callable)),
892+
Type::Callable(CallableType::General(target_callable)),
893+
) => self_callable.is_assignable_to(db, target_callable),
894+
890895
// TODO other types containing gradual forms (e.g. generics containing Any/Unknown)
891896
_ => self.is_subtype_of(db, target),
892897
}
@@ -4442,8 +4447,32 @@ impl<'db> GeneralCallableType<'db> {
44424447
})
44434448
}
44444449

4450+
/// Return `true` if `self` is assignable to `other`.
4451+
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
4452+
self.is_assignable_to_impl(db, other, |type1, type2| {
4453+
// In the context of a callable type, the `None` variant represents an `Unknown` type.
4454+
type1
4455+
.unwrap_or(Type::unknown())
4456+
.is_assignable_to(db, type2.unwrap_or(Type::unknown()))
4457+
})
4458+
}
4459+
44454460
/// Return `true` if `self` is a subtype of `other`.
44464461
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
4462+
self.is_assignable_to_impl(db, other, |type1, type2| {
4463+
// SAFETY: Subtype relation is only checked for fully static types.
4464+
type1.unwrap().is_subtype_of(db, type2.unwrap())
4465+
})
4466+
}
4467+
4468+
/// Implementation for the [`is_assignable_to`] and [`is_subtype_of`] for callable types.
4469+
///
4470+
/// [`is_assignable_to`]: Self::is_assignable_to
4471+
/// [`is_subtype_of`]: Self::is_subtype_of
4472+
fn is_assignable_to_impl<F>(self, db: &'db dyn Db, other: Self, check_types: F) -> bool
4473+
where
4474+
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
4475+
{
44474476
/// A helper struct to zip two slices of parameters together that provides control over the
44484477
/// two iterators individually. It also keeps track of the current parameter in each
44494478
/// iterator.
@@ -4508,18 +4537,17 @@ impl<'db> GeneralCallableType<'db> {
45084537
let self_signature = self.signature(db);
45094538
let other_signature = other.signature(db);
45104539

4511-
// Check if `type1` is a subtype of `type2`. This is mainly to avoid `unwrap` calls
4512-
// scattered throughout the function.
4513-
let is_subtype = |type1: Option<Type<'db>>, type2: Option<Type<'db>>| {
4514-
// SAFETY: Subtype relation is only checked for fully static types.
4515-
type1.unwrap().is_subtype_of(db, type2.unwrap())
4516-
};
4517-
45184540
// Return types are covariant.
4519-
if !is_subtype(self_signature.return_ty, other_signature.return_ty) {
4541+
if !check_types(self_signature.return_ty, other_signature.return_ty) {
45204542
return false;
45214543
}
45224544

4545+
if self_signature.parameters().is_gradual() || other_signature.parameters().is_gradual() {
4546+
// If either of the parameter lists contains a gradual form (`...`), then it is
4547+
// assignable / subtype to and from any other callable type.
4548+
return true;
4549+
}
4550+
45234551
let mut parameters = ParametersZip {
45244552
current_self: None,
45254553
current_other: None,
@@ -4577,7 +4605,7 @@ impl<'db> GeneralCallableType<'db> {
45774605
if self_default.is_none() && other_default.is_some() {
45784606
return false;
45794607
}
4580-
if !is_subtype(
4608+
if !check_types(
45814609
other_parameter.annotated_type(),
45824610
self_parameter.annotated_type(),
45834611
) {
@@ -4602,7 +4630,7 @@ impl<'db> GeneralCallableType<'db> {
46024630
if self_default.is_none() && other_default.is_some() {
46034631
return false;
46044632
}
4605-
if !is_subtype(
4633+
if !check_types(
46064634
other_parameter.annotated_type(),
46074635
self_parameter.annotated_type(),
46084636
) {
@@ -4611,7 +4639,7 @@ impl<'db> GeneralCallableType<'db> {
46114639
}
46124640

46134641
(ParameterKind::Variadic { .. }, ParameterKind::PositionalOnly { .. }) => {
4614-
if !is_subtype(
4642+
if !check_types(
46154643
other_parameter.annotated_type(),
46164644
self_parameter.annotated_type(),
46174645
) {
@@ -4641,7 +4669,7 @@ impl<'db> GeneralCallableType<'db> {
46414669
// variadic parameter and is deferred to the next iteration.
46424670
break;
46434671
}
4644-
if !is_subtype(
4672+
if !check_types(
46454673
other_parameter.annotated_type(),
46464674
self_parameter.annotated_type(),
46474675
) {
@@ -4652,7 +4680,7 @@ impl<'db> GeneralCallableType<'db> {
46524680
}
46534681

46544682
(ParameterKind::Variadic { .. }, ParameterKind::Variadic { .. }) => {
4655-
if !is_subtype(
4683+
if !check_types(
46564684
other_parameter.annotated_type(),
46574685
self_parameter.annotated_type(),
46584686
) {
@@ -4730,7 +4758,7 @@ impl<'db> GeneralCallableType<'db> {
47304758
if self_default.is_none() && other_default.is_some() {
47314759
return false;
47324760
}
4733-
if !is_subtype(
4761+
if !check_types(
47344762
other_parameter.annotated_type(),
47354763
self_parameter.annotated_type(),
47364764
) {
@@ -4742,8 +4770,10 @@ impl<'db> GeneralCallableType<'db> {
47424770
),
47434771
}
47444772
} else if let Some(self_keyword_variadic_type) = self_keyword_variadic {
4745-
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type)
4746-
{
4773+
if !check_types(
4774+
other_parameter.annotated_type(),
4775+
self_keyword_variadic_type,
4776+
) {
47474777
return false;
47484778
}
47494779
} else {
@@ -4756,7 +4786,7 @@ impl<'db> GeneralCallableType<'db> {
47564786
// parameter, `self` must also have a keyword variadic parameter.
47574787
return false;
47584788
};
4759-
if !is_subtype(other_parameter.annotated_type(), self_keyword_variadic_type) {
4789+
if !check_types(other_parameter.annotated_type(), self_keyword_variadic_type) {
47604790
return false;
47614791
}
47624792
}

0 commit comments

Comments
 (0)