mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-09 05:08:05 +00:00
[red-knot] Fix gradual equivalence for callable types (#16887)
## Summary As mentioned in https://github.com/astral-sh/ruff/pull/16698#discussion_r2004920075, part of #15382, this PR updates the `is_gradual_equivalent_to` implementation between callable types to be similar to `is_equivalent_to` and checks other attributes of parameters like name, optionality, and parameter kind. ## Test Plan Expand the existing test cases to consider other properties but not all similar to how the tests are structured for subtyping and assignability.
This commit is contained in:
parent
68ea2b8b5b
commit
dd5b02aaa2
2 changed files with 67 additions and 60 deletions
|
@ -68,6 +68,10 @@ static_assert(not is_gradual_equivalent_to(tuple[str, int], tuple[int, str]))
|
||||||
|
|
||||||
## Callable
|
## Callable
|
||||||
|
|
||||||
|
The examples provided below are only a subset of the possible cases and only include the ones with
|
||||||
|
gradual types. The cases with fully static types and using different combinations of parameter kinds
|
||||||
|
are covered in the [equivalence tests](./is_equivalent_to.md#callable).
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
|
from knot_extensions import Unknown, CallableTypeFromFunction, is_gradual_equivalent_to, static_assert
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
@ -94,7 +98,7 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f1], Callable[[]
|
||||||
And, similarly for parameters with no annotations.
|
And, similarly for parameters with no annotations.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def f2(a, b) -> None:
|
def f2(a, b, /) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
|
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f2], Callable[[Any, Any], None]))
|
||||||
|
@ -115,8 +119,8 @@ static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_without
|
||||||
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
|
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[variadic_with_annotation], Callable[..., Any]))
|
||||||
```
|
```
|
||||||
|
|
||||||
But, a function with either `*args` or `**kwargs` is not gradual equivalent to a callable with `...`
|
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||||
as the parameter type.
|
callable with `...` as the parameter type.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def variadic_args(*args):
|
def variadic_args(*args):
|
||||||
|
@ -129,4 +133,25 @@ static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_arg
|
||||||
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
|
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[variadic_kwargs], Callable[..., Any]))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Parameter names, default values, and it's kind should also be considered when checking for gradual
|
||||||
|
equivalence.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f1(a): ...
|
||||||
|
def f2(b): ...
|
||||||
|
|
||||||
|
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f2]))
|
||||||
|
|
||||||
|
def f3(a=1): ...
|
||||||
|
def f4(a=2): ...
|
||||||
|
def f5(a): ...
|
||||||
|
|
||||||
|
static_assert(is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f4]))
|
||||||
|
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f3], CallableTypeFromFunction[f5]))
|
||||||
|
|
||||||
|
def f6(a, /): ...
|
||||||
|
|
||||||
|
static_assert(not is_gradual_equivalent_to(CallableTypeFromFunction[f1], CallableTypeFromFunction[f6]))
|
||||||
|
```
|
||||||
|
|
||||||
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
[materializations]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-materialize
|
||||||
|
|
|
@ -4346,11 +4346,44 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
.is_some_and(|return_type| return_type.is_fully_static(db))
|
.is_some_and(|return_type| return_type.is_fully_static(db))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return `true` if `self` has exactly the same set of possible static materializations as
|
||||||
|
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
|
||||||
|
/// `other`).
|
||||||
|
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
|
self.is_equivalent_to_impl(db, other, |self_type, other_type| {
|
||||||
|
self_type
|
||||||
|
.unwrap_or(Type::unknown())
|
||||||
|
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
|
/// Return `true` if `self` represents the exact same set of possible runtime objects as `other`.
|
||||||
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
pub(crate) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
|
self.is_equivalent_to_impl(db, other, |self_type, other_type| {
|
||||||
|
match (self_type, other_type) {
|
||||||
|
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
|
||||||
|
// We need the catch-all case here because it's not guaranteed that this is a fully
|
||||||
|
// static type.
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation for the [`is_equivalent_to`] and [`is_gradual_equivalent_to`] for callable
|
||||||
|
/// types.
|
||||||
|
///
|
||||||
|
/// [`is_equivalent_to`]: Self::is_equivalent_to
|
||||||
|
/// [`is_gradual_equivalent_to`]: Self::is_gradual_equivalent_to
|
||||||
|
fn is_equivalent_to_impl<F>(self, db: &'db dyn Db, other: Self, check_types: F) -> bool
|
||||||
|
where
|
||||||
|
F: Fn(Option<Type<'db>>, Option<Type<'db>>) -> bool,
|
||||||
|
{
|
||||||
let self_signature = self.signature(db);
|
let self_signature = self.signature(db);
|
||||||
let other_signature = other.signature(db);
|
let other_signature = other.signature(db);
|
||||||
|
|
||||||
|
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
|
||||||
|
// parameters because it is internally represented by adding `*Any` and `**Any` to the
|
||||||
|
// parameter list.
|
||||||
let self_parameters = self_signature.parameters();
|
let self_parameters = self_signature.parameters();
|
||||||
let other_parameters = other_signature.parameters();
|
let other_parameters = other_signature.parameters();
|
||||||
|
|
||||||
|
@ -4358,20 +4391,7 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self_parameters.is_gradual() || other_parameters.is_gradual() {
|
if !check_types(self_signature.return_ty, other_signature.return_ty) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check equivalence relationship between two optional types. If either of them is `None`,
|
|
||||||
// then it is not a fully static type which means it's not equivalent either.
|
|
||||||
let is_equivalent = |self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| match (
|
|
||||||
self_type, other_type,
|
|
||||||
) {
|
|
||||||
(Some(self_type), Some(other_type)) => self_type.is_equivalent_to(db, other_type),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_equivalent(self_signature.return_ty, other_signature.return_ty) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4419,7 +4439,7 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
_ => return false,
|
_ => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_equivalent(
|
if !check_types(
|
||||||
self_parameter.annotated_type(),
|
self_parameter.annotated_type(),
|
||||||
other_parameter.annotated_type(),
|
other_parameter.annotated_type(),
|
||||||
) {
|
) {
|
||||||
|
@ -4430,48 +4450,6 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` has exactly the same set of possible static materializations as
|
|
||||||
/// `other` (if `self` represents the same set of possible sets of possible runtime objects as
|
|
||||||
/// `other`).
|
|
||||||
pub(crate) fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
|
||||||
let self_signature = self.signature(db);
|
|
||||||
let other_signature = other.signature(db);
|
|
||||||
|
|
||||||
if self_signature.parameters().len() != other_signature.parameters().len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check gradual equivalence between the two optional types. In the context of a callable
|
|
||||||
// type, the `None` type represents an `Unknown` type.
|
|
||||||
let are_optional_types_gradually_equivalent =
|
|
||||||
|self_type: Option<Type<'db>>, other_type: Option<Type<'db>>| {
|
|
||||||
self_type
|
|
||||||
.unwrap_or(Type::unknown())
|
|
||||||
.is_gradual_equivalent_to(db, other_type.unwrap_or(Type::unknown()))
|
|
||||||
};
|
|
||||||
|
|
||||||
if !are_optional_types_gradually_equivalent(
|
|
||||||
self_signature.return_ty,
|
|
||||||
other_signature.return_ty,
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// N.B. We don't need to explicitly check for the use of gradual form (`...`) in the
|
|
||||||
// parameters because it is internally represented by adding `*Any` and `**Any` to the
|
|
||||||
// parameter list.
|
|
||||||
self_signature
|
|
||||||
.parameters()
|
|
||||||
.iter()
|
|
||||||
.zip(other_signature.parameters().iter())
|
|
||||||
.all(|(self_param, other_param)| {
|
|
||||||
are_optional_types_gradually_equivalent(
|
|
||||||
self_param.annotated_type(),
|
|
||||||
other_param.annotated_type(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return `true` if `self` is assignable to `other`.
|
/// Return `true` if `self` is assignable to `other`.
|
||||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
self.is_assignable_to_impl(db, other, |type1, type2| {
|
self.is_assignable_to_impl(db, other, |type1, type2| {
|
||||||
|
@ -4483,6 +4461,10 @@ impl<'db> GeneralCallableType<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` is a subtype of `other`.
|
/// Return `true` if `self` is a subtype of `other`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if `self` or `other` is not a fully static type.
|
||||||
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
self.is_assignable_to_impl(db, other, |type1, type2| {
|
self.is_assignable_to_impl(db, other, |type1, type2| {
|
||||||
// SAFETY: Subtype relation is only checked for fully static types.
|
// SAFETY: Subtype relation is only checked for fully static types.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue