mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 10:50:26 +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 dataclasses import dataclass
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from types import FunctionType
|
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:
|
class C:
|
||||||
x: int
|
x: int
|
||||||
|
|
||||||
|
|
@ -1233,8 +1233,20 @@ static_assert(not is_assignable_to(EquivalentPureCallableType, DunderInitType))
|
||||||
static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType))
|
static_assert(is_subtype_of(DunderInitType, EquivalentFunctionLikeCallableType))
|
||||||
static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType))
|
static_assert(is_assignable_to(DunderInitType, EquivalentFunctionLikeCallableType))
|
||||||
|
|
||||||
static_assert(not is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
|
static_assert(is_subtype_of(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||||
static_assert(not is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
static_assert(is_assignable_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||||
|
|
||||||
|
static_assert(is_equivalent_to(EquivalentFunctionLikeCallableType, DunderInitType))
|
||||||
|
|
||||||
static_assert(is_subtype_of(DunderInitType, FunctionType))
|
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)]
|
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
|
## Mixed list
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -963,6 +963,40 @@ c: Callable[[Any], str] = f
|
||||||
c: Callable[[Any], str] = g
|
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
|
### Method types
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ gradual types. The cases with fully static types and using different combination
|
||||||
are covered above.
|
are covered above.
|
||||||
|
|
||||||
```py
|
```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
|
from typing import Any, Callable
|
||||||
|
|
||||||
static_assert(is_equivalent_to(Callable[..., int], Callable[..., int]))
|
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]))
|
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
|
A function with no explicit return type should be gradually equivalent to a function-like callable
|
||||||
type of `Any`.
|
with a return type of `Any`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def f1():
|
def f1():
|
||||||
return
|
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.
|
And, similarly for parameters with no annotations.
|
||||||
|
|
@ -492,12 +495,15 @@ And, similarly for parameters with no annotations.
|
||||||
def f2(a, b, /) -> None:
|
def f2(a, b, /) -> None:
|
||||||
return
|
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`
|
A function definition that includes both `*args` and `**kwargs` parameter that are annotated as
|
||||||
parameter that are annotated as `Any` or kept unannotated should be gradual equivalent to a callable
|
`Any` or kept unannotated should be gradual equivalent to a callable with `...` as the parameter
|
||||||
with `...` as the parameter type.
|
type.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def variadic_without_annotation(*args, **kwargs):
|
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:
|
def variadic_with_annotation(*args: Any, **kwargs: Any) -> Any:
|
||||||
return
|
return
|
||||||
|
|
||||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_without_annotation], Callable[..., Any]))
|
def _(
|
||||||
static_assert(is_equivalent_to(CallableTypeOf[variadic_with_annotation], Callable[..., Any]))
|
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
|
Note that `variadic_without_annotation` and `variadic_with_annotation` are *not* considered
|
||||||
callable with `...` as the parameter type.
|
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
|
```py
|
||||||
def variadic_args(*args):
|
def variadic_args(*args):
|
||||||
|
|
@ -520,6 +541,15 @@ def variadic_args(*args):
|
||||||
def variadic_kwargs(**kwargs):
|
def variadic_kwargs(**kwargs):
|
||||||
return
|
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_args], Callable[..., Any]))
|
||||||
static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
static_assert(not is_equivalent_to(CallableTypeOf[variadic_kwargs], Callable[..., Any]))
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1401,7 +1401,7 @@ impl<'db> Type<'db> {
|
||||||
match self {
|
match self {
|
||||||
Type::Callable(_) => Some(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) => {
|
Type::FunctionLiteral(function_literal) => {
|
||||||
Some(Type::Callable(function_literal.into_callable_type(db)))
|
Some(Type::Callable(function_literal.into_callable_type(db)))
|
||||||
|
|
@ -9770,7 +9770,7 @@ impl<'db> BoundMethodType<'db> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|signature| signature.bind_self(db, Some(self_instance))),
|
.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`].
|
/// Convert the `FunctionType` into a [`CallableType`].
|
||||||
pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> {
|
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`].
|
/// Convert the `FunctionType` into a [`BoundMethodType`].
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue