mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 13:30:49 -05:00
[ty] fix and simplify callable type materializations (#22213)
## Summary A couple things I noticed when taking another look at the callable type materializations. 1) Previously we wrongly ignored the return type when bottom-materializing a callable with gradual signature, and always changed it to `Never`. 2) We weren't correctly handling overloads that included a gradual signature. Rather than separately materializing each overload, we would just mark the entire callable as "top" or replace the entire callable with the bottom signature. Really, "top parameters" is something that belongs on the `Parameters`, not on the entire `CallableType`. Conveniently, we already have `ParametersKind` where we can track this, right next to where we already track `ParametersKind::Gradual`. This saves a bit of memory, fixes the two bugs above, and simplifies the implementation considerably (net removal of 100+ LOC, a bunch of places that shouldn't need to care about topness of a callable no longer need to.) One user-visible change from this is that I now display the "top callable" as `(Top[...]) -> object` instead of `Top[(...) -> object]`. I think this is a (minor) improvement, because it wraps exactly the part in `Top` that needs to be, rather than misleadingly wrapping the entire callable type, including the return type (which has already been separately materialized). I think the prior display would be particularly confusing if the return type also has its own `Top` in it: previously we could have e.g. `Top[(...) -> Top[list[Unknown]]]`, which I think is less clear than the new `(Top[...]) -> Top[list[Unknown]]`. ## Test Plan Added mdtests that failed before this PR and pass after it. ### Ecosystem The changed diagnostics are all either the change to `Top` display, or else known non-deterministic output. The added diagnostics are all true positives: The added diagnostic ataa35ca1965/torchvision/transforms/v2/_utils.py (L149)is a true positive that wasn't caught by the previous version. `str` is not assignable to `Callable[[Any], Any]` (strings are not callable), nor is the top callable (top callable includes callables that do not take a single required positional argument.) The added diagnostic at081535ad9b/starlette/routing.py (L67)is also a (pedantic) true positive. It's the same case as #1567 -- the code assumes that it is impossible for a subclass of `Response` to implement `__await__` (yielding something other than a `Response`). The pytest added diagnostics are also both similar true positives: they make the assumption that an object cannot simultaneously be a `Sequence` and callable, or an `Iterable` and callable.
This commit is contained in:
@@ -45,8 +45,6 @@ def h(x: Callable[..., int] | None):
|
||||
## Narrowing from object
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: object):
|
||||
if callable(x):
|
||||
reveal_type(x) # revealed: Top[(...) -> object]
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Display of callable types
|
||||
|
||||
We parenthesize callable types when they appear inside more complex types, to disambiguate:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable[[], str] | Callable[[int], str]):
|
||||
reveal_type(x) # revealed: (() -> str) | ((int, /) -> str)
|
||||
```
|
||||
|
||||
We don't parenthesize display of an overloaded callable, since it is already wrapped in
|
||||
`Overload[...]`:
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from ty_extensions import CallableTypeOf
|
||||
|
||||
@overload
|
||||
def f(x: int) -> bool: ...
|
||||
@overload
|
||||
def f(x: str) -> str: ...
|
||||
def f(x: int | str) -> bool | str:
|
||||
return bool(x) if isinstance(x, int) else str(x)
|
||||
|
||||
def _(flag: bool, c: CallableTypeOf[f]):
|
||||
x = c if flag else True
|
||||
reveal_type(x) # revealed: Overload[(x: int) -> bool, (x: str) -> str] | Literal[True]
|
||||
```
|
||||
|
||||
And we don't parenthesize the top callable, since it is wrapped in `Top[...]`:
|
||||
|
||||
```py
|
||||
from ty_extensions import Top
|
||||
|
||||
def f(x: Top[Callable[..., str]] | Callable[[int], int]):
|
||||
reveal_type(x) # revealed: Top[(...) -> str] | ((int, /) -> int)
|
||||
```
|
||||
@@ -43,7 +43,7 @@ def _(top_callable: Top[Callable[[Any], None]]):
|
||||
reveal_type(top_callable) # revealed: (Never, /) -> None
|
||||
```
|
||||
|
||||
The invariant position is replaced with an unresolved type variable.
|
||||
The invariant position cannot simplify, and is represented with the `Top` special form.
|
||||
|
||||
```py
|
||||
def _(top_list: Top[list[Any]]):
|
||||
@@ -70,8 +70,13 @@ def _(bottom_callable: Bottom[Callable[[Any, Unknown], None]]):
|
||||
reveal_type(bottom_callable) # revealed: (object, object, /) -> None
|
||||
```
|
||||
|
||||
The invariant position is replaced in the same way as the top materialization, with an unresolved
|
||||
type variable.
|
||||
The invariant position is represented with the `Bottom` special form.
|
||||
|
||||
There is an argument that `Bottom[list[Any]]` should simplify to `Never`, since it is the infinite
|
||||
intersection of all possible materializations of `list[Any]`, and (due to invariance) these
|
||||
materializations are disjoint types. But currently we do not make this simplification: there doesn't
|
||||
seem to be any compelling need for it, and allowing more gradual types to materialize to `Never` has
|
||||
undesirable implications for mutual assignability of seemingly-unrelated gradual types.
|
||||
|
||||
```py
|
||||
def _(bottom_list: Bottom[list[Any]]):
|
||||
@@ -182,8 +187,8 @@ def _(top: Top[C3], bottom: Bottom[C3]) -> None:
|
||||
|
||||
For callables with gradual parameters (the `...` form), the top materialization preserves the
|
||||
gradual form since we cannot know what parameters are required. The bottom materialization
|
||||
simplifies to the bottom callable `(*args: object, **kwargs: object) -> Never` since this is the
|
||||
most specific type that is a subtype of all possible callable materializations.
|
||||
simplifies to the bottom parameters `(*args: object, **kwargs: object)` since this is the most
|
||||
specific type that is a subtype of all possible parameter materializations.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
@@ -220,6 +225,58 @@ static_assert(is_equivalent_to(Bottom[Callable[..., Never]], EquivalentToBottom)
|
||||
static_assert(not is_equivalent_to(Top[Callable[..., object]], Callable[..., object]))
|
||||
```
|
||||
|
||||
Gradual parameters can be top- and bottom-materialized even if the return type is not `Any`:
|
||||
|
||||
```py
|
||||
type GradualParams = Callable[..., int]
|
||||
|
||||
def _(top: Top[GradualParams], bottom: Bottom[GradualParams]) -> None:
|
||||
reveal_type(top) # revealed: Top[(...) -> int]
|
||||
|
||||
reveal_type(bottom) # revealed: (*args: object, **kwargs: object) -> int
|
||||
```
|
||||
|
||||
Materializing an overloaded callable materializes each overload separately.
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from ty_extensions import CallableTypeOf
|
||||
|
||||
@overload
|
||||
def f(x: int) -> Any: ...
|
||||
@overload
|
||||
def f(*args: Any, **kwargs: Any) -> str: ...
|
||||
def f(*args: object, **kwargs: object) -> object:
|
||||
pass
|
||||
|
||||
def _(top: Top[CallableTypeOf[f]], bottom: Bottom[CallableTypeOf[f]]):
|
||||
reveal_type(top) # revealed: Overload[(x: int) -> object, Top[(...) -> str]]
|
||||
reveal_type(bottom) # revealed: Overload[(x: int) -> Never, (*args: object, **kwargs: object) -> str]
|
||||
```
|
||||
|
||||
The top callable can be represented in a `ParamSpec`:
|
||||
|
||||
```py
|
||||
def takes_paramspec[**P](f: Callable[P, None]) -> Callable[P, None]:
|
||||
return f
|
||||
|
||||
def _(top: Top[Callable[..., None]]):
|
||||
revealed = takes_paramspec(top)
|
||||
reveal_type(revealed) # revealed: Top[(...) -> None]
|
||||
```
|
||||
|
||||
The top callable is not a subtype of `(*object, **object) -> object`:
|
||||
|
||||
```py
|
||||
type TopCallable = Top[Callable[..., Any]]
|
||||
|
||||
@staticmethod
|
||||
def takes_objects(*args: object, **kwargs: object) -> object:
|
||||
pass
|
||||
|
||||
static_assert(not is_subtype_of(TopCallable, CallableTypeOf[takes_objects]))
|
||||
```
|
||||
|
||||
## Tuple
|
||||
|
||||
All positions in a tuple are covariant.
|
||||
|
||||
@@ -1903,7 +1903,6 @@ impl<'db> Type<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(method.signatures(db)),
|
||||
CallableTypeKind::Regular,
|
||||
false,
|
||||
))),
|
||||
|
||||
Type::WrapperDescriptor(wrapper_descriptor) => {
|
||||
@@ -1911,7 +1910,6 @@ impl<'db> Type<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(wrapper_descriptor.signatures(db)),
|
||||
CallableTypeKind::Regular,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -12331,7 +12329,6 @@ impl<'db> BoundMethodType<'db> {
|
||||
.map(|signature| signature.bind_self(db, Some(self_instance))),
|
||||
),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12451,12 +12448,6 @@ pub struct CallableType<'db> {
|
||||
pub(crate) signatures: CallableSignature<'db>,
|
||||
|
||||
kind: CallableTypeKind,
|
||||
|
||||
/// Whether this callable is a top materialization (e.g., `Top[Callable[..., object]]`).
|
||||
///
|
||||
/// Bottom materializations of gradual callables are simplified to the bottom callable
|
||||
/// `(*args: object, **kwargs: object) -> Never`, so this is always false for them.
|
||||
is_top_materialization: bool,
|
||||
}
|
||||
|
||||
pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
@@ -12501,7 +12492,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
CallableSignature::single(signature),
|
||||
CallableTypeKind::Regular,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12510,7 +12500,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
CallableSignature::single(signature),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12522,7 +12511,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
CallableSignature::single(Signature::new(parameters, None)),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12552,7 +12540,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
self.signatures(db).bind_self(db, self_type),
|
||||
self.kind(db),
|
||||
self.is_top_materialization(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12561,7 +12548,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
self.signatures(db).apply_self(db, self_type),
|
||||
self.kind(db),
|
||||
self.is_top_materialization(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12570,12 +12556,7 @@ impl<'db> CallableType<'db> {
|
||||
/// Specifically, this represents a callable type with a single signature:
|
||||
/// `(*args: object, **kwargs: object) -> Never`.
|
||||
pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> {
|
||||
Self::new(
|
||||
db,
|
||||
CallableSignature::bottom(),
|
||||
CallableTypeKind::Regular,
|
||||
false,
|
||||
)
|
||||
Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular)
|
||||
}
|
||||
|
||||
/// Return a "normalized" version of this `Callable` type.
|
||||
@@ -12586,7 +12567,6 @@ impl<'db> CallableType<'db> {
|
||||
db,
|
||||
self.signatures(db).normalized_impl(db, visitor),
|
||||
self.kind(db),
|
||||
self.is_top_materialization(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12601,7 +12581,6 @@ impl<'db> CallableType<'db> {
|
||||
self.signatures(db)
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
self.kind(db),
|
||||
self.is_top_materialization(db),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -12612,43 +12591,11 @@ impl<'db> CallableType<'db> {
|
||||
tcx: TypeContext<'db>,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
if let TypeMapping::Materialize(materialization_kind) = type_mapping {
|
||||
// Top materializations are fully static types already,
|
||||
// so materializing them further does nothing.
|
||||
if self.is_top_materialization(db) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// If we're materializing a callable with gradual parameters:
|
||||
// - For Top materialization: wrap in `Top[...]` to preserve the gradual nature
|
||||
// - For Bottom materialization: simplify to the bottom callable since
|
||||
// `Bottom[Callable[..., R]]` is equivalent to `(*args: object, **kwargs: object) -> Bottom[R]`
|
||||
if self.signatures(db).has_gradual_parameters() {
|
||||
match materialization_kind {
|
||||
MaterializationKind::Top => {
|
||||
return CallableType::new(
|
||||
db,
|
||||
self.signatures(db)
|
||||
.materialize_return_types(db, *materialization_kind),
|
||||
self.kind(db),
|
||||
true,
|
||||
);
|
||||
}
|
||||
MaterializationKind::Bottom => {
|
||||
// Bottom materialization of a gradual callable simplifies to the
|
||||
// bottom callable: (*args: object, **kwargs: object) -> Never
|
||||
return CallableType::bottom(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallableType::new(
|
||||
db,
|
||||
self.signatures(db)
|
||||
.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
|
||||
self.kind(db),
|
||||
self.is_top_materialization(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12679,23 +12626,6 @@ impl<'db> CallableType<'db> {
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
|
||||
// Handle top materialization:
|
||||
// - `Top[Callable[..., R]]` is a supertype of all callables with return type subtype of R.
|
||||
//
|
||||
// For Top, we only need to compare return types because Top parameters are a supertype
|
||||
// of all possible parameters. Bottom materializations are simplified to the bottom
|
||||
// callable directly, so they use normal signature comparison.
|
||||
if other.is_top_materialization(db) {
|
||||
return self.signatures(db).return_types_have_relation_to(
|
||||
db,
|
||||
other.signatures(db),
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
);
|
||||
}
|
||||
|
||||
self.signatures(db).has_relation_to_impl(
|
||||
db,
|
||||
other.signatures(db),
|
||||
@@ -12720,11 +12650,6 @@ impl<'db> CallableType<'db> {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
|
||||
// Callables with different top materialization status are not equivalent
|
||||
if self.is_top_materialization(db) != other.is_top_materialization(db) {
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
|
||||
ConstraintSet::from(self.is_function_like(db) == other.is_function_like(db)).and(db, || {
|
||||
self.signatures(db)
|
||||
.is_equivalent_to_impl(db, other.signatures(db), inferable, visitor)
|
||||
|
||||
@@ -1622,21 +1622,6 @@ impl<'db> CallableBinding<'db> {
|
||||
)
|
||||
.entered();
|
||||
|
||||
// If the callable is a top materialization (e.g., `Top[Callable[..., object]]`), any call
|
||||
// should fail because we don't know the actual signature. The type IS callable (it passes
|
||||
// `callable()`), but it represents an infinite union of all possible callable types, so
|
||||
// there's no valid set of arguments.
|
||||
if let Type::Callable(callable) = self.signature_type
|
||||
&& callable.is_top_materialization(db)
|
||||
{
|
||||
for overload in &mut self.overloads {
|
||||
overload
|
||||
.errors
|
||||
.push(BindingError::CalledTopCallable(self.signature_type));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
tracing::trace!(
|
||||
target: "ty_python_semantic::types::call::bind",
|
||||
matching_overload_index = ?self.matching_overload_index(),
|
||||
@@ -3716,6 +3701,11 @@ impl<'db> Binding<'db> {
|
||||
self.return_ty,
|
||||
&mut self.errors,
|
||||
);
|
||||
if self.signature.parameters().is_top() {
|
||||
self.errors
|
||||
.push(BindingError::CalledTopCallable(self.signature_type));
|
||||
return;
|
||||
}
|
||||
|
||||
// If this overload is generic, first see if we can infer a specialization of the function
|
||||
// from the arguments that were passed in.
|
||||
@@ -4741,6 +4731,5 @@ fn asynccontextmanager_return_type<'db>(db: &'db dyn Db, func_ty: Type<'db>) ->
|
||||
db,
|
||||
CallableSignature::single(new_signature),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -1056,7 +1056,6 @@ impl<'db> ClassType<'db> {
|
||||
db,
|
||||
getitem_signature,
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
));
|
||||
Member::definitely_declared(getitem_type)
|
||||
})
|
||||
@@ -1224,7 +1223,6 @@ impl<'db> ClassType<'db> {
|
||||
db,
|
||||
dunder_new_signature.bind_self(db, Some(instance_ty)),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
);
|
||||
|
||||
if returns_non_subclass {
|
||||
@@ -1295,7 +1293,6 @@ impl<'db> ClassType<'db> {
|
||||
db,
|
||||
synthesized_dunder_init_signature,
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
@@ -2128,7 +2125,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
callable_ty.signatures(db),
|
||||
CallableTypeKind::FunctionLike,
|
||||
callable_ty.is_top_materialization(db),
|
||||
)),
|
||||
Type::Union(union) => {
|
||||
union.map(db, |element| into_function_like_callable(db, *element))
|
||||
@@ -2773,7 +2769,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
Some(Type::none(db)),
|
||||
)),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -2800,7 +2795,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__getitem__") => {
|
||||
@@ -2828,7 +2822,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__delitem__") => {
|
||||
@@ -2860,7 +2853,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
Some(Type::none(db)),
|
||||
)),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -2886,7 +2878,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "get") => {
|
||||
@@ -2994,7 +2985,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "pop") => {
|
||||
@@ -3054,7 +3044,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "setdefault") => {
|
||||
@@ -3083,7 +3072,6 @@ impl<'db> ClassLiteral<'db> {
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
CallableTypeKind::FunctionLike,
|
||||
false,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "update") => {
|
||||
|
||||
@@ -1582,7 +1582,6 @@ impl<'db> CallableType<'db> {
|
||||
DisplayCallableType {
|
||||
signatures: self.signatures(db),
|
||||
kind: self.kind(db),
|
||||
is_top_materialization: self.is_top_materialization(db),
|
||||
db,
|
||||
settings,
|
||||
}
|
||||
@@ -1592,20 +1591,12 @@ impl<'db> CallableType<'db> {
|
||||
pub(crate) struct DisplayCallableType<'a, 'db> {
|
||||
signatures: &'a CallableSignature<'db>,
|
||||
kind: CallableTypeKind,
|
||||
is_top_materialization: bool,
|
||||
db: &'db dyn Db,
|
||||
settings: DisplaySettings<'db>,
|
||||
}
|
||||
|
||||
impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> {
|
||||
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
|
||||
// If this callable is a top materialization, wrap it in Top[...]
|
||||
if self.is_top_materialization {
|
||||
f.with_type(Type::SpecialForm(SpecialFormType::Top))
|
||||
.write_str("Top")?;
|
||||
f.write_char('[')?;
|
||||
}
|
||||
|
||||
match self.signatures.overloads.as_slice() {
|
||||
[signature] => {
|
||||
if matches!(self.kind, CallableTypeKind::ParamSpecValue) {
|
||||
@@ -1639,9 +1630,6 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_top_materialization {
|
||||
f.write_char(']')?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1701,6 +1689,10 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
|
||||
// When we exit this function, write a marker signaling we're ending a signature
|
||||
let mut f = f.with_detail(TypeDetail::SignatureEnd);
|
||||
|
||||
if self.parameters.is_top() {
|
||||
f.write_str("Top[")?;
|
||||
}
|
||||
|
||||
// If we're multiline printing and a name hasn't been emitted, try to
|
||||
// remember what the name was by checking if we have a definition
|
||||
if self.settings.multiline
|
||||
@@ -1722,7 +1714,13 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
|
||||
f.write_str(" -> ")?;
|
||||
return_ty
|
||||
.display_with(self.db, self.settings.singleline())
|
||||
.fmt_detailed(&mut f)
|
||||
.fmt_detailed(&mut f)?;
|
||||
|
||||
if self.parameters.is_top() {
|
||||
f.write_str("]")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1831,9 +1829,10 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> {
|
||||
f.write_char('/')?;
|
||||
}
|
||||
}
|
||||
ParametersKind::Gradual => {
|
||||
ParametersKind::Gradual | ParametersKind::Top => {
|
||||
// We represent gradual form as `...` in the signature, internally the parameters still
|
||||
// contain `(*args, **kwargs)` parameters.
|
||||
// contain `(*args, **kwargs)` parameters. (Top parameters are displayed the same
|
||||
// as gradual parameters, we just wrap the entire signature in `Top[]`.)
|
||||
f.write_str("...")?;
|
||||
}
|
||||
ParametersKind::ParamSpec(typevar) => {
|
||||
@@ -2250,9 +2249,12 @@ impl<'db> FmtDetailed<'db> for DisplayMaybeParenthesizedType<'db> {
|
||||
f.write_char(')')
|
||||
};
|
||||
match self.ty {
|
||||
// Callable types with a top materialization are displayed as `Top[(...) -> T]`,
|
||||
// which is already unambiguous and doesn't need additional parentheses.
|
||||
Type::Callable(callable) if !callable.is_top_materialization(self.db) => {
|
||||
Type::Callable(callable)
|
||||
if callable.signatures(self.db).overloads.len() == 1
|
||||
&& !callable.signatures(self.db).overloads[0]
|
||||
.parameters()
|
||||
.is_top() =>
|
||||
{
|
||||
write_parentheses(f)
|
||||
}
|
||||
Type::KnownBoundMethod(_)
|
||||
|
||||
@@ -1094,7 +1094,7 @@ impl<'db> FunctionType<'db> {
|
||||
} else {
|
||||
CallableTypeKind::FunctionLike
|
||||
};
|
||||
CallableType::new(db, self.signature(db), kind, false)
|
||||
CallableType::new(db, self.signature(db), kind)
|
||||
}
|
||||
|
||||
/// Convert the `FunctionType` into a [`BoundMethodType`].
|
||||
|
||||
@@ -2310,7 +2310,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
db,
|
||||
callable.signatures(db),
|
||||
kind,
|
||||
callable.is_top_materialization(db),
|
||||
))),
|
||||
Type::Union(union) => union
|
||||
.try_map(db, |element| propagate_callable_kind(db, *element, kind)),
|
||||
|
||||
@@ -990,6 +990,5 @@ fn protocol_bind_self<'db>(
|
||||
db,
|
||||
callable.signatures(db).bind_self(db, self_type),
|
||||
CallableTypeKind::Regular,
|
||||
callable.is_top_materialization(db),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -189,20 +189,24 @@ impl<'db> CallableSignature<'db> {
|
||||
type_mapping.update_signature_generic_context(db, context)
|
||||
}),
|
||||
definition: signature.definition,
|
||||
parameters: Parameters::new(
|
||||
db,
|
||||
prefix_parameters
|
||||
.iter()
|
||||
.map(|param| {
|
||||
param.apply_type_mapping_impl(
|
||||
db,
|
||||
type_mapping,
|
||||
tcx,
|
||||
visitor,
|
||||
)
|
||||
})
|
||||
.chain(signature.parameters().iter().cloned()),
|
||||
),
|
||||
parameters: if signature.parameters().is_top() {
|
||||
signature.parameters().clone()
|
||||
} else {
|
||||
Parameters::new(
|
||||
db,
|
||||
prefix_parameters
|
||||
.iter()
|
||||
.map(|param| {
|
||||
param.apply_type_mapping_impl(
|
||||
db,
|
||||
type_mapping,
|
||||
tcx,
|
||||
visitor,
|
||||
)
|
||||
})
|
||||
.chain(signature.parameters().iter().cloned()),
|
||||
)
|
||||
},
|
||||
return_ty: self_signature.return_ty.map(|ty| {
|
||||
ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)
|
||||
}),
|
||||
@@ -428,7 +432,6 @@ impl<'db> CallableSignature<'db> {
|
||||
|signature| Signature::new(signature.parameters().clone(), None),
|
||||
)),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
));
|
||||
let param_spec_matches = ConstraintSet::constrain_typevar(
|
||||
db,
|
||||
@@ -462,7 +465,6 @@ impl<'db> CallableSignature<'db> {
|
||||
|signature| Signature::new(signature.parameters().clone(), None),
|
||||
)),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
));
|
||||
let param_spec_matches = ConstraintSet::constrain_typevar(
|
||||
db,
|
||||
@@ -571,56 +573,6 @@ impl<'db> CallableSignature<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if any signature in this callable has gradual parameters (`...`).
|
||||
pub(crate) fn has_gradual_parameters(&self) -> bool {
|
||||
self.overloads.iter().any(|sig| sig.parameters.is_gradual())
|
||||
}
|
||||
|
||||
/// Materialize only the return types of all signatures, preserving parameters as-is.
|
||||
///
|
||||
/// This is used when wrapping gradual callables in `Top[...]`. We want to preserve the gradual
|
||||
/// parameters but materialize the return types (which are in covariant position).
|
||||
pub(crate) fn materialize_return_types(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
materialization_kind: MaterializationKind,
|
||||
) -> Self {
|
||||
Self::from_overloads(
|
||||
self.overloads
|
||||
.iter()
|
||||
.map(|sig| sig.materialize_return_type(db, materialization_kind)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check whether the return types of this callable have the given relation to the return
|
||||
/// types of another callable.
|
||||
pub(crate) fn return_types_have_relation_to(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
other: &Self,
|
||||
inferable: InferableTypeVars<'_, 'db>,
|
||||
relation: TypeRelation<'db>,
|
||||
relation_visitor: &HasRelationToVisitor<'db>,
|
||||
disjointness_visitor: &IsDisjointVisitor<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
// For each overload in self, the return type must have the relation to
|
||||
// the return type of some overload in other.
|
||||
self.overloads.iter().when_all(db, |self_sig| {
|
||||
let self_return_ty = self_sig.return_ty.unwrap_or(Type::unknown());
|
||||
other.overloads.iter().when_any(db, |other_sig| {
|
||||
let other_return_ty = other_sig.return_ty.unwrap_or(Type::unknown());
|
||||
self_return_ty.has_relation_to_impl(
|
||||
db,
|
||||
other_return_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
|
||||
@@ -780,27 +732,7 @@ impl<'db> Signature<'db> {
|
||||
|
||||
/// Return the "bottom" signature, subtype of all other fully-static signatures.
|
||||
pub(crate) fn bottom() -> Self {
|
||||
Self::new(Parameters::object(), Some(Type::Never))
|
||||
}
|
||||
|
||||
/// Materialize only the return type, preserving parameters as-is.
|
||||
pub(crate) fn materialize_return_type(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
materialization_kind: MaterializationKind,
|
||||
) -> Self {
|
||||
Self {
|
||||
generic_context: self.generic_context,
|
||||
definition: self.definition,
|
||||
parameters: self.parameters.clone(),
|
||||
return_ty: self.return_ty.map(|ty| {
|
||||
ty.materialize(
|
||||
db,
|
||||
materialization_kind,
|
||||
&ApplyTypeMappingVisitor::default(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
Self::new(Parameters::bottom(), Some(Type::Never))
|
||||
}
|
||||
|
||||
pub(crate) fn with_inherited_generic_context(
|
||||
@@ -887,12 +819,9 @@ impl<'db> Signature<'db> {
|
||||
.generic_context
|
||||
.map(|context| type_mapping.update_signature_generic_context(db, context)),
|
||||
definition: self.definition,
|
||||
parameters: self.parameters.apply_type_mapping_impl(
|
||||
db,
|
||||
&type_mapping.flip(),
|
||||
tcx,
|
||||
visitor,
|
||||
),
|
||||
parameters: self
|
||||
.parameters
|
||||
.apply_type_mapping_impl(db, type_mapping, tcx, visitor),
|
||||
return_ty: self
|
||||
.return_ty
|
||||
.map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)),
|
||||
@@ -1187,7 +1116,6 @@ impl<'db> Signature<'db> {
|
||||
.map(|signature| Signature::new(signature.parameters().clone(), None)),
|
||||
),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
));
|
||||
let param_spec_matches =
|
||||
ConstraintSet::constrain_typevar(db, self_bound_typevar, Type::Never, upper);
|
||||
@@ -1404,6 +1332,14 @@ impl<'db> Signature<'db> {
|
||||
return ConstraintSet::from(true);
|
||||
}
|
||||
|
||||
// The top signature is supertype of (and assignable from) all other signatures. It is a
|
||||
// subtype of no signature except itself, and assignable only to the gradual signature.
|
||||
if other.parameters.is_top() {
|
||||
return ConstraintSet::from(true);
|
||||
} else if self.parameters.is_top() && !other.parameters.is_gradual() {
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
|
||||
// If either of the parameter lists is gradual (`...`), then it is assignable to and from
|
||||
// any other parameter list, but not a subtype or supertype of any other parameter list.
|
||||
if self.parameters.is_gradual() || other.parameters.is_gradual() {
|
||||
@@ -1439,7 +1375,6 @@ impl<'db> Signature<'db> {
|
||||
db,
|
||||
CallableSignature::single(Signature::new(other.parameters.clone(), None)),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
));
|
||||
let param_spec_matches = ConstraintSet::constrain_typevar(
|
||||
db,
|
||||
@@ -1456,7 +1391,6 @@ impl<'db> Signature<'db> {
|
||||
db,
|
||||
CallableSignature::single(Signature::new(self.parameters.clone(), None)),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
false,
|
||||
));
|
||||
let param_spec_matches = ConstraintSet::constrain_typevar(
|
||||
db,
|
||||
@@ -1815,6 +1749,10 @@ pub(crate) enum ParametersKind<'db> {
|
||||
/// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable
|
||||
Gradual,
|
||||
|
||||
/// Represents the "top" parameters: top materialization of Gradual parameters, or infinite
|
||||
/// union of all possible parameter signatures.
|
||||
Top,
|
||||
|
||||
/// Represents a parameter list containing a `ParamSpec` as the only parameter.
|
||||
///
|
||||
/// Note that this is distinct from a parameter list _containing_ a `ParamSpec` which is
|
||||
@@ -1894,6 +1832,10 @@ impl<'db> Parameters<'db> {
|
||||
matches!(self.kind, ParametersKind::Gradual)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_top(&self) -> bool {
|
||||
matches!(self.kind, ParametersKind::Top)
|
||||
}
|
||||
|
||||
pub(crate) const fn as_paramspec(&self) -> Option<BoundTypeVarInstance<'db>> {
|
||||
match self.kind {
|
||||
ParametersKind::ParamSpec(bound_typevar) => Some(bound_typevar),
|
||||
@@ -1963,8 +1905,9 @@ impl<'db> Parameters<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return parameters that represents `(*args: object, **kwargs: object)`.
|
||||
pub(crate) fn object() -> Self {
|
||||
/// Return parameters that represents `(*args: object, **kwargs: object)`, the bottom signature
|
||||
/// (accepts any call, so subtype of all other signatures.)
|
||||
pub(crate) fn bottom() -> Self {
|
||||
Self {
|
||||
value: vec![
|
||||
Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()),
|
||||
@@ -1975,6 +1918,25 @@ impl<'db> Parameters<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the "top" parameters (infinite union of all possible parameters), which cannot
|
||||
/// accept any call, since there is no possible call that satisfies all possible parameter
|
||||
/// signatures. This is not `(*Never, **Never)`, which is equivalent to no parameters at all
|
||||
/// and still accepts the empty call `()`; it has to be represented instead as a special
|
||||
/// `ParametersKind`.
|
||||
pub(crate) fn top() -> Self {
|
||||
Self {
|
||||
// We always emit `called-top-callable` for any call to the top callable (based on the
|
||||
// `kind` below), so we otherwise give it the most permissive signature`(*object,
|
||||
// **object)`, so that we avoid emitting any other errors about arity mismatches.
|
||||
value: vec![
|
||||
Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::object()),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(Type::object()),
|
||||
],
|
||||
kind: ParametersKind::Top,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the bound `ParamSpec` type variable if the parameters contain a `ParamSpec`.
|
||||
pub(crate) fn find_paramspec_from_args_kwargs<'a>(
|
||||
&'a self,
|
||||
@@ -2131,11 +2093,29 @@ impl<'db> Parameters<'db> {
|
||||
tcx: TypeContext<'db>,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
if let TypeMapping::Materialize(materialization_kind) = type_mapping
|
||||
&& self.kind == ParametersKind::Gradual
|
||||
{
|
||||
match materialization_kind {
|
||||
MaterializationKind::Bottom => {
|
||||
// The bottom materialization of the `...` parameters is `(*object, **object)`,
|
||||
// which accepts any call and is thus a subtype of all other parameters.
|
||||
return Parameters::bottom();
|
||||
}
|
||||
MaterializationKind::Top => {
|
||||
return Parameters::top();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters are in contravariant position, so we need to flip the type mapping.
|
||||
let type_mapping = type_mapping.flip();
|
||||
|
||||
Self {
|
||||
value: self
|
||||
.value
|
||||
.iter()
|
||||
.map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor))
|
||||
.map(|param| param.apply_type_mapping_impl(db, &type_mapping, tcx, visitor))
|
||||
.collect(),
|
||||
kind: self.kind,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user