mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 02:39:59 +00:00
[ty] Treat functions, methods, and dynamic types as function-like Callables (#20842)
## Summary Treat functions, methods, and dynamic types as function-like `Callable`s closes https://github.com/astral-sh/ty/issues/1342 closes https://github.com/astral-sh/ty/issues/1344 ## Ecosystem analysis All removed diagnostics look like cases of https://github.com/astral-sh/ty/issues/1344 ## Test Plan Added regression test
This commit is contained in:
parent
513d2996ec
commit
195e8f0684
6 changed files with 101 additions and 19 deletions
|
|
@ -1204,9 +1204,9 @@ python-version = "3.12"
|
|||
from dataclasses import dataclass
|
||||
from typing import Callable
|
||||
from types import FunctionType
|
||||
from ty_extensions import CallableTypeOf, TypeOf, static_assert, is_subtype_of, is_assignable_to
|
||||
from ty_extensions import CallableTypeOf, TypeOf, static_assert, is_subtype_of, is_assignable_to, is_equivalent_to
|
||||
|
||||
@dataclass
|
||||
@dataclass(order=True)
|
||||
class C:
|
||||
x: int
|
||||
|
||||
|
|
@ -1233,8 +1233,20 @@ static_assert(not is_assignable_to(EquivalentPureCallableType, DunderInitType))
|
|||
static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType))
|
||||
static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType))
|
||||
|
||||
static_assert(not is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||
static_assert(not is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||
static_assert(is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||
static_assert(is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||
|
||||
static_assert(is_equivalent_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||
|
||||
static_assert(is_subtype_of(DunderInitType, FunctionType))
|
||||
```
|
||||
|
||||
It should be possible to mock out synthesized methods:
|
||||
|
||||
```py
|
||||
from unittest.mock import Mock
|
||||
|
||||
def test_c():
|
||||
c = C(1)
|
||||
c.__lt__ = Mock()
|
||||
```
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ x = [a, b]
|
|||
reveal_type(x) # revealed: list[Unknown | ((_: int) -> int)]
|
||||
```
|
||||
|
||||
The inferred `Callable` type is function-like, i.e. we can still access attributes like `__name__`:
|
||||
|
||||
```py
|
||||
reveal_type(x[0].__name__) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
## Mixed list
|
||||
|
||||
```py
|
||||
|
|
|
|||
|
|
@ -963,6 +963,40 @@ c: Callable[[Any], str] = f
|
|||
c: Callable[[Any], str] = g
|
||||
```
|
||||
|
||||
A function with no explicit return type should be assignable to a callable with a return type of
|
||||
`Any`.
|
||||
|
||||
```py
|
||||
def h():
|
||||
return
|
||||
|
||||
c: Callable[[], Any] = h
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations:
|
||||
|
||||
```py
|
||||
def i(a, b, /) -> None:
|
||||
return
|
||||
|
||||
c: Callable[[Any, Any], None] = i
|
||||
```
|
||||
|
||||
Additionally, a function definition that includes both `*args` and `**kwargs` parameters that are
|
||||
annotated as `Any` or kept unannotated should be assignable to a callable with `...` as the
|
||||
parameter type.
|
||||
|
||||
```py
|
||||
def variadic_without_annotation(*args, **kwargs):
|
||||
return
|
||||
|
||||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
c: Callable[..., Any] = variadic_without_annotation
|
||||
c: Callable[..., Any] = variadic_with_annotation
|
||||
```
|
||||
|
||||
### Method types
|
||||
|
||||
```py
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ gradual types. The cases with fully static types and using different combination
|
|||
are covered above.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown, CallableTypeOf, is_equivalent_to, static_assert
|
||||
from ty_extensions import Unknown, CallableTypeOf, TypeOf, is_equivalent_to, static_assert
|
||||
from typing import Any, Callable
|
||||
|
||||
static_assert(is_equivalent_to(Callable[..., int], Callable[..., int]))
|
||||
|
|
@ -476,14 +476,17 @@ static_assert(not is_equivalent_to(Callable[[int, str], None], Callable[[int, st
|
|||
static_assert(not is_equivalent_to(Callable[..., None], Callable[[], None]))
|
||||
```
|
||||
|
||||
A function with no explicit return type should be gradual equivalent to a callable with a return
|
||||
type of `Any`.
|
||||
A function with no explicit return type should be gradually equivalent to a function-like callable
|
||||
with a return type of `Any`.
|
||||
|
||||
```py
|
||||
def f1():
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1], Callable[[], Any]))
|
||||
def f1_equivalent() -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f1], CallableTypeOf[f1_equivalent]))
|
||||
```
|
||||
|
||||
And, similarly for parameters with no annotations.
|
||||
|
|
@ -492,12 +495,15 @@ And, similarly for parameters with no annotations.
|
|||
def f2(a, b, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f2], Callable[[Any, Any], None]))
|
||||
def f2_equivalent(a: Any, b: Any, /) -> None:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[f2], CallableTypeOf[f2_equivalent]))
|
||||
```
|
||||
|
||||
Additionally, as per the spec, a function definition that includes both `*args` and `**kwargs`
|
||||
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
|
||||
with `...` as the parameter type.
|
||||
A function definition that includes both `*args` and `**kwargs` parameter that are annotated as
|
||||
`Any` or kept unannotated should be gradual equivalent to a callable with `...` as the parameter
|
||||
type.
|
||||
|
||||
```py
|
||||
def variadic_without_annotation(*args, **kwargs):
|
||||
|
|
@ -506,12 +512,27 @@ def variadic_without_annotation(*args, **kwargs):
|
|||
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||
return
|
||||
|
||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
def _(
|
||||
signature_variadic_without_annotation: CallableTypeOf[variadic_without_annotation],
|
||||
signature_variadic_with_annotation: CallableTypeOf[variadic_with_annotation],
|
||||
) -> None:
|
||||
# revealed: (...) -> Unknown
|
||||
reveal_type(signature_variadic_without_annotation)
|
||||
# revealed: (...) -> Any
|
||||
reveal_type(signature_variadic_with_annotation)
|
||||
```
|
||||
|
||||
But, a function with either `*args` or `**kwargs` (and not both) is not gradual equivalent to a
|
||||
callable with `...` as the parameter type.
|
||||
Note that `variadic_without_annotation` and `variadic_with_annotation` are *not* considered
|
||||
gradually equivalent to `Callable[..., Any]`, because the latter is not a function-like callable
|
||||
type:
|
||||
|
||||
```py
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
||||
```
|
||||
|
||||
A function with either `*args` or `**kwargs` (and not both) is is not equivalent to a callable with
|
||||
`...` as the parameter type.
|
||||
|
||||
```py
|
||||
def variadic_args(*args):
|
||||
|
|
@ -520,6 +541,15 @@ def variadic_args(*args):
|
|||
def variadic_kwargs(**kwargs):
|
||||
return
|
||||
|
||||
def _(
|
||||
signature_variadic_args: CallableTypeOf[variadic_args],
|
||||
signature_variadic_kwargs: CallableTypeOf[variadic_kwargs],
|
||||
) -> None:
|
||||
# revealed: (*args) -> Unknown
|
||||
reveal_type(signature_variadic_args)
|
||||
# revealed: (**kwargs) -> Unknown
|
||||
reveal_type(signature_variadic_kwargs)
|
||||
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_args], Callable[..., Any]))
|
||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1401,7 +1401,7 @@ impl<'db> Type<'db> {
|
|||
match self {
|
||||
Type::Callable(_) => Some(self),
|
||||
|
||||
Type::Dynamic(_) => Some(CallableType::single(db, Signature::dynamic(self))),
|
||||
Type::Dynamic(_) => Some(CallableType::function_like(db, Signature::dynamic(self))),
|
||||
|
||||
Type::FunctionLiteral(function_literal) => {
|
||||
Some(Type::Callable(function_literal.into_callable_type(db)))
|
||||
|
|
@ -9770,7 +9770,7 @@ impl<'db> BoundMethodType<'db> {
|
|||
.iter()
|
||||
.map(|signature| signature.bind_self(db, Some(self_instance))),
|
||||
),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -943,7 +943,7 @@ impl<'db> FunctionType<'db> {
|
|||
|
||||
/// Convert the `FunctionType` into a [`CallableType`].
|
||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
||||
CallableType::new(db, self.signature(db), false)
|
||||
CallableType::new(db, self.signature(db), true)
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`BoundMethodType`].
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue