[ty] Substitute for `typing.Self` when checking protocol members (#21569)

This patch updates our protocol assignability checks to substitute for
any occurrences of `typing.Self` in method signatures, replacing it with
the class being checked for assignability against the protocol.

This requires a new helper method on signatures, `apply_self`, which
substitutes occurrences of `typing.Self` _without_ binding the `self`
parameter.

We also update the `try_upcast_to_callable` method. Before, it would
return a `Type`, since certain types upcast to a _union_ of callables,
not to a single callable. However, even in that case, we know that every
element of the union is a callable. We now return a vector of
`CallableType`. (Actually a smallvec to handle the most common case of a
single callable; and wrapped in a new type so that we can provide helper
methods.) If there is more than one element in the result, it represents
a union of callables. This lets callers get at the `CallableType`
instances in a more type-safe way. (This makes it easier for our
protocol checking code to call the new `apply_self` helper.) We also
provide an `into_type` method so that callers that really do want a
`Type` can get the original result easily.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager 2025-11-24 14:05:09 -05:00 committed by GitHub
parent 68343e7edf
commit 03fe560164
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 317 additions and 128 deletions

View File

@ -2003,6 +2003,7 @@ python-version = "3.12"
```
```py
from typing import final
from typing_extensions import TypeVar, Self, Protocol
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
def g(self) -> "NominalReturningSelfNotGeneric":
return self
@final
class Other: ...
class NominalReturningOtherClass:
def g(self) -> Other:
raise NotImplementedError
# TODO: should pass
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
@ -2112,8 +2120,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
# TODO: should pass
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
# TODO: these should pass
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
# TODO: should pass
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
# These test cases are taken from the typing conformance suite:
class ShapeProtocolImplicitSelf(Protocol):
def set_scale(self, scale: float) -> Self: ...

View File

@ -1376,9 +1376,7 @@ mod implicit_globals {
use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin};
use crate::semantic_index::symbol::Symbol;
use crate::semantic_index::{place_table, use_def_map};
use crate::types::{
CallableType, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
};
use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
use ruff_python_ast::PythonVersion;
use super::{Place, place_from_declarations};
@ -1461,7 +1459,7 @@ mod implicit_globals {
)),
);
Place::Defined(
CallableType::function_like(db, signature),
Type::function_like_callable(db, signature),
TypeOrigin::Inferred,
Definedness::PossiblyUndefined,
)

View File

@ -208,7 +208,7 @@ use crate::semantic_index::predicate::{
Predicates, ScopedPredicateId,
};
use crate::types::{
IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
CallableTypes, IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
infer_expression_type, static_expression_truthiness,
};
@ -871,8 +871,10 @@ impl ReachabilityConstraints {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
}
let overloads_iterator =
if let Some(Type::Callable(callable)) = ty.try_upcast_to_callable(db) {
let overloads_iterator = if let Some(callable) = ty
.try_upcast_to_callable(db)
.and_then(CallableTypes::exactly_one)
{
callable.signatures(db).overloads.iter()
} else {
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);

View File

@ -16,6 +16,7 @@ use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_text_size::{Ranged, TextRange};
use smallvec::{SmallVec, smallvec};
use type_ordering::union_or_intersection_elements_ordering;
@ -1533,17 +1534,20 @@ impl<'db> Type<'db> {
}
}
pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option<Type<'db>> {
pub(crate) fn try_upcast_to_callable(self, db: &'db dyn Db) -> Option<CallableTypes<'db>> {
match self {
Type::Callable(_) => Some(self),
Type::Callable(callable) => Some(CallableTypes::one(callable)),
Type::Dynamic(_) => Some(CallableType::function_like(db, Signature::dynamic(self))),
Type::Dynamic(_) => Some(CallableTypes::one(CallableType::function_like(
db,
Signature::dynamic(self),
))),
Type::FunctionLiteral(function_literal) => {
Some(Type::Callable(function_literal.into_callable_type(db)))
Some(CallableTypes::one(function_literal.into_callable_type(db)))
}
Type::BoundMethod(bound_method) => {
Some(Type::Callable(bound_method.into_callable_type(db)))
Some(CallableTypes::one(bound_method.into_callable_type(db)))
}
Type::NominalInstance(_) | Type::ProtocolInstance(_) => {
@ -1574,13 +1578,22 @@ impl<'db> Type<'db> {
// TODO: This is unsound so in future we can consider an opt-in option to disable it.
Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Class(class) => Some(class.into_callable(db)),
SubclassOfInner::Dynamic(dynamic) => Some(CallableType::single(
SubclassOfInner::Dynamic(dynamic) => {
Some(CallableTypes::one(CallableType::single(
db,
Signature::new(Parameters::unknown(), Some(Type::Dynamic(dynamic))),
)),
)))
}
},
Type::Union(union) => union.try_map(db, |element| element.try_upcast_to_callable(db)),
Type::Union(union) => {
let mut callables = SmallVec::new();
for element in union.elements(db) {
let element_callable = element.try_upcast_to_callable(db)?;
callables.extend(element_callable.into_inner());
}
Some(CallableTypes(callables))
}
Type::EnumLiteral(enum_literal) => enum_literal
.enum_class_instance(db)
@ -1588,26 +1601,30 @@ impl<'db> Type<'db> {
Type::TypeAlias(alias) => alias.value_type(db).try_upcast_to_callable(db),
Type::KnownBoundMethod(method) => Some(Type::Callable(CallableType::new(
Type::KnownBoundMethod(method) => Some(CallableTypes::one(CallableType::new(
db,
CallableSignature::from_overloads(method.signatures(db)),
false,
))),
Type::WrapperDescriptor(wrapper_descriptor) => Some(Type::Callable(CallableType::new(
Type::WrapperDescriptor(wrapper_descriptor) => {
Some(CallableTypes::one(CallableType::new(
db,
CallableSignature::from_overloads(wrapper_descriptor.signatures(db)),
false,
))),
)))
}
Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some(CallableType::single(
Type::KnownInstance(KnownInstanceType::NewType(newtype)) => {
Some(CallableTypes::one(CallableType::single(
db,
Signature::new(
Parameters::new([Parameter::positional_only(None)
.with_annotated_type(newtype.base(db).instance_type(db))]),
Some(Type::NewTypeInstance(newtype)),
),
)),
)))
}
Type::Never
| Type::DataclassTransformer(_)
@ -2183,18 +2200,20 @@ impl<'db> Type<'db> {
)
}),
(_, Type::Callable(_)) => relation_visitor.visit((self, target, relation), || {
self.try_upcast_to_callable(db).when_some_and(|callable| {
callable.has_relation_to_impl(
(_, Type::Callable(other_callable)) => {
relation_visitor.visit((self, target, relation), || {
self.try_upcast_to_callable(db).when_some_and(|callables| {
callables.has_relation_to_impl(
db,
target,
other_callable,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}),
})
}
(_, Type::ProtocolInstance(protocol)) => {
relation_visitor.visit((self, target, relation), || {
@ -4093,7 +4112,7 @@ impl<'db> Type<'db> {
Some((self, AttributeKind::NormalOrNonDataDescriptor))
} else {
Some((
Type::Callable(callable.bind_self(db)),
Type::Callable(callable.bind_self(db, None)),
AttributeKind::NormalOrNonDataDescriptor,
))
};
@ -5627,7 +5646,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, getter_signature),
Type::single_callable(db, getter_signature),
Type::none(db),
],
))
@ -5636,7 +5655,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, setter_signature),
Type::single_callable(db, setter_signature),
Type::none(db),
],
))
@ -5645,7 +5664,7 @@ impl<'db> Type<'db> {
.with_annotated_type(UnionType::from_elements(
db,
[
CallableType::single(db, deleter_signature),
Type::single_callable(db, deleter_signature),
Type::none(db),
],
))
@ -7139,6 +7158,15 @@ impl<'db> Type<'db> {
tcx: TypeContext<'db>,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Type<'db> {
// If we are binding `typing.Self`, and this type is what we are binding `Self` to, return
// early. This is not just an optimization, it also prevents us from infinitely expanding
// the type, if it's something that can contain a `Self` reference.
if let TypeMapping::BindSelf(self_type) = type_mapping
&& self == *self_type
{
return self;
}
match self {
Type::TypeVar(bound_typevar) => match type_mapping {
TypeMapping::Specialization(specialization) => {
@ -10957,34 +10985,44 @@ pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
// The Salsa heap is tracked separately.
impl get_size2::GetSize for CallableType<'_> {}
impl<'db> CallableType<'db> {
impl<'db> Type<'db> {
/// Create a callable type with a single non-overloaded signature.
pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::new(
db,
CallableSignature::single(signature),
false,
))
pub(crate) fn single_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::single(db, signature))
}
/// Create a non-overloaded, function-like callable type with a single signature.
///
/// A function-like callable will bind `self` when accessed as an attribute on an instance.
pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::new(
db,
CallableSignature::single(signature),
true,
))
pub(crate) fn function_like_callable(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> {
Type::Callable(CallableType::function_like(db, signature))
}
}
impl<'db> CallableType<'db> {
pub(crate) fn single(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> {
CallableType::new(db, CallableSignature::single(signature), false)
}
pub(crate) fn function_like(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> {
CallableType::new(db, CallableSignature::single(signature), true)
}
/// Create a callable type which accepts any parameters and returns an `Unknown` type.
pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> {
Self::single(db, Signature::unknown())
Type::Callable(Self::single(db, Signature::unknown()))
}
pub(crate) fn bind_self(self, db: &'db dyn Db) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).bind_self(db, None), false)
pub(crate) fn bind_self(
self,
db: &'db dyn Db,
self_type: Option<Type<'db>>,
) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).bind_self(db, self_type), false)
}
pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> {
CallableType::new(db, self.signatures(db).apply_self(db, self_type), false)
}
/// Create a callable type which represents a fully-static "bottom" callable.
@ -11078,6 +11116,72 @@ impl<'db> CallableType<'db> {
}
}
/// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually,
/// when coercing that result to a single type, you'll get a `UnionType`. But this lets you handle
/// that result as a list of `CallableType`s before merging them into a `UnionType` should that be
/// helpful.
///
/// Note that this type is guaranteed to contain at least one callable. If you need to support "no
/// callables" as a possibility, use `Option<CallableTypes>`.
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, salsa::Update)]
pub(crate) struct CallableTypes<'db>(SmallVec<[CallableType<'db>; 1]>);
impl<'db> CallableTypes<'db> {
pub(crate) fn one(callable: CallableType<'db>) -> Self {
CallableTypes(smallvec![callable])
}
pub(crate) fn from_elements(callables: impl IntoIterator<Item = CallableType<'db>>) -> Self {
let callables: SmallVec<_> = callables.into_iter().collect();
assert!(!callables.is_empty(), "CallableTypes should not be empty");
CallableTypes(callables)
}
pub(crate) fn exactly_one(self) -> Option<CallableType<'db>> {
match self.0.as_slice() {
[single] => Some(*single),
_ => None,
}
}
fn into_inner(self) -> SmallVec<[CallableType<'db>; 1]> {
self.0
}
pub(crate) fn into_type(self, db: &'db dyn Db) -> Type<'db> {
match self.0.as_slice() {
[] => unreachable!("CallableTypes should not be empty"),
[single] => Type::Callable(*single),
slice => UnionType::from_elements(db, slice.iter().copied().map(Type::Callable)),
}
}
pub(crate) fn map(self, mut f: impl FnMut(CallableType<'db>) -> CallableType<'db>) -> Self {
Self::from_elements(self.0.iter().map(|element| f(*element)))
}
pub(crate) fn has_relation_to_impl(
self,
db: &'db dyn Db,
other: CallableType<'db>,
inferable: InferableTypeVars<'_, 'db>,
relation: TypeRelation<'db>,
relation_visitor: &HasRelationToVisitor<'db>,
disjointness_visitor: &IsDisjointVisitor<'db>,
) -> ConstraintSet<'db> {
self.0.iter().when_all(db, |element| {
element.has_relation_to_impl(
db,
other,
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
}
/// Represents a specific instance of a bound method type for a builtin class.
///
/// Unlike bound methods of user-defined classes, these are not generally instances

View File

@ -32,7 +32,7 @@ use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::typed_dict::typed_dict_params_from_class_def;
use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard};
use crate::types::{
ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, DATACLASS_FLAGS,
ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, CallableTypes, DATACLASS_FLAGS,
DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType,
ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType,
@ -791,7 +791,7 @@ impl<'db> ClassType<'db> {
.with_annotated_type(Type::instance(db, self))]);
let synthesized_dunder_method =
CallableType::function_like(db, Signature::new(parameters, Some(return_type)));
Type::function_like_callable(db, Signature::new(parameters, Some(return_type)));
Member::definitely_declared(synthesized_dunder_method)
}
@ -1013,7 +1013,7 @@ impl<'db> ClassType<'db> {
iterable_parameter,
]);
let synthesized_dunder = CallableType::function_like(
let synthesized_dunder = Type::function_like_callable(
db,
Signature::new_generic(inherited_generic_context, parameters, None),
);
@ -1052,7 +1052,7 @@ impl<'db> ClassType<'db> {
/// Return a callable type (or union of callable types) that represents the callable
/// constructor signature of this class.
#[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
pub(super) fn into_callable(self, db: &'db dyn Db) -> Type<'db> {
pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> {
let self_ty = Type::from(self);
let metaclass_dunder_call_function_symbol = self_ty
.member_lookup_with_policy(
@ -1070,7 +1070,7 @@ impl<'db> ClassType<'db> {
// https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable
// by always respecting the signature of the metaclass `__call__`, rather than
// using a heuristic which makes unwarranted assumptions to sometimes ignore it.
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
return CallableTypes::one(metaclass_dunder_call_function.into_callable_type(db));
}
let dunder_new_function_symbol = self_ty.lookup_dunder_new(db);
@ -1098,14 +1098,14 @@ impl<'db> ClassType<'db> {
});
let instance_ty = Type::instance(db, self);
let dunder_new_bound_method = Type::Callable(CallableType::new(
let dunder_new_bound_method = CallableType::new(
db,
dunder_new_signature.bind_self(db, Some(instance_ty)),
true,
));
);
if returns_non_subclass {
return dunder_new_bound_method;
return CallableTypes::one(dunder_new_bound_method);
}
Some(dunder_new_bound_method)
} else {
@ -1148,11 +1148,11 @@ impl<'db> ClassType<'db> {
signature.overloads.iter().map(synthesized_signature),
);
Some(Type::Callable(CallableType::new(
Some(CallableType::new(
db,
synthesized_dunder_init_signature,
true,
)))
))
} else {
None
}
@ -1162,12 +1162,14 @@ impl<'db> ClassType<'db> {
match (dunder_new_function, synthesized_dunder_init_callable) {
(Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => {
UnionType::from_elements(
db,
vec![dunder_new_function, synthesized_dunder_init_callable],
)
CallableTypes::from_elements([
dunder_new_function,
synthesized_dunder_init_callable,
])
}
(Some(constructor), None) | (None, Some(constructor)) => {
CallableTypes::one(constructor)
}
(Some(constructor), None) | (None, Some(constructor)) => constructor,
(None, None) => {
// If no `__new__` or `__init__` method is found, then we fall back to looking for
// an `object.__new__` method.
@ -1182,17 +1184,17 @@ impl<'db> ClassType<'db> {
if let Place::Defined(Type::FunctionLiteral(new_function), _, _) =
new_function_symbol
{
Type::Callable(
CallableTypes::one(
new_function
.into_bound_method_type(db, correct_return_type)
.into_callable_type(db),
)
} else {
// Fallback if no `object.__new__` is found.
CallableType::single(
CallableTypes::one(CallableType::single(
db,
Signature::new(Parameters::empty(), Some(correct_return_type)),
)
))
}
}
}
@ -1208,11 +1210,11 @@ impl<'db> ClassType<'db> {
}
fn into_callable_cycle_initial<'db>(
_db: &'db dyn Db,
db: &'db dyn Db,
_id: salsa::Id,
_self: ClassType<'db>,
) -> Type<'db> {
Type::Never
) -> CallableTypes<'db> {
CallableTypes::one(CallableType::bottom(db))
}
impl<'db> From<GenericAlias<'db>> for ClassType<'db> {
@ -2156,7 +2158,7 @@ impl<'db> ClassLiteral<'db> {
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
Some(field.declared_ty),
);
let property_getter = CallableType::single(db, property_getter_signature);
let property_getter = Type::single_callable(db, property_getter_signature);
let property = PropertyInstanceType::new(db, Some(property_getter), None);
return Member::definitely_declared(Type::PropertyInstance(property));
}
@ -2370,7 +2372,7 @@ impl<'db> ClassLiteral<'db> {
),
_ => Signature::new(Parameters::new(parameters), return_ty),
};
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
};
match (field_policy, name) {
@ -2406,7 +2408,7 @@ impl<'db> ClassLiteral<'db> {
Some(KnownClass::Bool.to_instance(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
}
(CodeGeneratorKind::DataclassLike(_), "__hash__") => {
let unsafe_hash = has_dataclass_param(DataclassFlags::UNSAFE_HASH);
@ -2422,7 +2424,7 @@ impl<'db> ClassLiteral<'db> {
Some(KnownClass::Int.to_instance(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
} else if eq && !frozen {
Some(Type::none(db))
} else {
@ -2509,7 +2511,7 @@ impl<'db> ClassLiteral<'db> {
Some(Type::Never),
);
return Some(CallableType::function_like(db, signature));
return Some(Type::function_like_callable(db, signature));
}
None
}
@ -2787,7 +2789,7 @@ impl<'db> ClassLiteral<'db> {
Some(Type::none(db)),
);
Some(CallableType::function_like(db, signature))
Some(Type::function_like_callable(db, signature))
}
_ => None,
}

View File

@ -1790,7 +1790,11 @@ impl<'db> InteriorNode<'db> {
/// Returns a sequent map for this BDD, which records the relationships between the constraints
/// that appear in the BDD.
#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
#[salsa::tracked(
returns(ref),
cycle_initial=sequent_map_cycle_initial,
heap_size=ruff_memory_usage::heap_size,
)]
fn sequent_map(self, db: &'db dyn Db) -> SequentMap<'db> {
let mut map = SequentMap::default();
Node::Interior(self).for_each_constraint(db, &mut |constraint| {
@ -2109,6 +2113,14 @@ impl<'db> InteriorNode<'db> {
}
}
fn sequent_map_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
_self: InteriorNode<'db>,
) -> SequentMap<'db> {
SequentMap::default()
}
/// An assignment of one BDD variable to either `true` or `false`. (When evaluating a BDD, we
/// must provide an assignment for each variable present in the BDD.)
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]

View File

@ -15,7 +15,7 @@ use crate::types::generics::Specialization;
use crate::types::signatures::Signature;
use crate::types::{CallDunderError, UnionType};
use crate::types::{
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
CallableTypes, ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, DisplaySettings, HasType, NameKind, SemanticModel};
@ -876,7 +876,10 @@ pub fn definitions_for_keyword_argument<'db>(
let mut resolved_definitions = Vec::new();
if let Some(Type::Callable(callable_type)) = func_type.try_upcast_to_callable(db) {
if let Some(callable_type) = func_type
.try_upcast_to_callable(db)
.and_then(CallableTypes::exactly_one)
{
let signatures = callable_type.signatures(db);
// For each signature, find the parameter with the matching name
@ -987,7 +990,10 @@ pub fn call_signature_details<'db>(
let func_type = call_expr.func.inferred_type(model);
// Use into_callable to handle all the complex type conversions
if let Some(callable_type) = func_type.try_upcast_to_callable(db) {
if let Some(callable_type) = func_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(db))
{
let call_arguments =
CallArguments::from_arguments(&call_expr.arguments, |_, splatted_value| {
splatted_value.inferred_type(model)
@ -1042,7 +1048,7 @@ pub fn call_type_simplified_by_overloads<'db>(
let func_type = call_expr.func.inferred_type(model);
// Use into_callable to handle all the complex type conversions
let callable_type = func_type.try_upcast_to_callable(db)?;
let callable_type = func_type.try_upcast_to_callable(db)?.into_type(db);
let bindings = callable_type.bindings(db);
// If the callable is trivial this analysis is useless, bail out

View File

@ -101,8 +101,8 @@ use crate::types::typed_dict::{
};
use crate::types::visitor::any_over_type;
use crate::types::{
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
CallDunderError, CallableBinding, CallableType, CallableTypes, ClassLiteral, ClassType,
DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
@ -2291,7 +2291,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let is_input_function_like = inferred_ty
.try_upcast_to_callable(self.db())
.and_then(Type::as_callable)
.and_then(CallableTypes::exactly_one)
.is_some_and(|callable| callable.is_function_like(self.db()));
if is_input_function_like
@ -3284,7 +3284,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// specified in this context.
match default {
ast::Expr::EllipsisLiteral(_) => {
CallableType::single(self.db(), Signature::new(Parameters::gradual_form(), None))
Type::single_callable(self.db(), Signature::new(Parameters::gradual_form(), None))
}
ast::Expr::List(ast::ExprList { elts, .. }) => {
let mut parameter_types = Vec::with_capacity(elts.len());
@ -3312,7 +3312,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}))
};
CallableType::single(self.db(), Signature::new(parameters, None))
Type::single_callable(self.db(), Signature::new(parameters, None))
}
ast::Expr::Name(name) => {
let name_ty = self.infer_name_load(name);
@ -7945,7 +7945,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// TODO: Useful inference of a lambda's return type will require a different approach,
// which does the inference of the body expression based on arguments at each call site,
// rather than eagerly computing a return type without knowing the argument types.
CallableType::function_like(self.db(), Signature::new(parameters, Some(Type::unknown())))
Type::function_like_callable(self.db(), Signature::new(parameters, Some(Type::unknown())))
}
fn infer_call_expression(

View File

@ -1011,7 +1011,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let callable_type = if let (Some(parameters), Some(return_type), true) =
(parameters, return_type, correct_argument_number)
{
CallableType::single(db, Signature::new(parameters, Some(return_type)))
Type::single_callable(db, Signature::new(parameters, Some(return_type)))
} else {
CallableType::unknown(db)
};
@ -1227,7 +1227,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let argument_type = self.infer_expression(&arguments[0], TypeContext::default());
let Some(callable_type) = argument_type.try_upcast_to_callable(db) else {
let Some(callable_type) = argument_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(self.db()))
else {
if let Some(builder) = self
.context
.report_lint(&INVALID_TYPE_FORM, arguments_slice)

View File

@ -126,7 +126,10 @@ fn check_class_declaration<'db>(
break;
};
let Some(superclass_type_as_callable) = superclass_type.try_upcast_to_callable(db) else {
let Some(superclass_type_as_callable) = superclass_type
.try_upcast_to_callable(db)
.map(|callables| callables.into_type(db))
else {
continue;
};

View File

@ -3,8 +3,8 @@ use crate::place::{builtins_symbol, known_module_symbol};
use crate::types::enums::is_single_member_enum;
use crate::types::tuple::TupleType;
use crate::types::{
BoundMethodType, CallableType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter,
Parameters, Signature, SpecialFormType, SubclassOfType, Type, UnionType,
BoundMethodType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter, Parameters,
Signature, SpecialFormType, SubclassOfType, Type, UnionType,
};
use crate::{Db, module_resolver::KnownModule};
use quickcheck::{Arbitrary, Gen};
@ -229,7 +229,7 @@ impl Ty {
create_bound_method(db, function, builtins_class)
}
Ty::Callable { params, returns } => CallableType::single(
Ty::Callable { params, returns } => Type::single_callable(
db,
Signature::new(
params.into_parameters(db),

View File

@ -202,7 +202,7 @@ impl<'db> ProtocolInterface<'db> {
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]),
Some(ty.normalized(db)),
);
let property_getter = CallableType::single(db, property_getter_signature);
let property_getter = Type::single_callable(db, property_getter_signature);
let property = PropertyInstanceType::new(db, Some(property_getter), None);
(
Name::new(name),
@ -300,7 +300,7 @@ impl<'db> ProtocolInterface<'db> {
.and(db, || {
our_type.has_relation_to_impl(
db,
Type::Callable(other_type.bind_self(db)),
Type::Callable(other_type.bind_self(db, None)),
inferable,
relation,
relation_visitor,
@ -311,9 +311,9 @@ impl<'db> ProtocolInterface<'db> {
(
ProtocolMemberKind::Method(our_method),
ProtocolMemberKind::Method(other_method),
) => our_method.bind_self(db).has_relation_to_impl(
) => our_method.bind_self(db, None).has_relation_to_impl(
db,
other_method.bind_self(db),
other_method.bind_self(db, None),
inferable,
relation,
relation_visitor,
@ -676,10 +676,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
// unfortunately not sufficient to obtain the `Callable` supertypes of these types, due to the
// complex interaction between `__new__`, `__init__` and metaclass `__call__`.
let attribute_type = if self.name == "__call__" {
let Some(attribute_type) = other.try_upcast_to_callable(db) else {
return ConstraintSet::from(false);
};
attribute_type
other
} else {
let Place::Defined(attribute_type, _, Definedness::AlwaysDefined) = other
.invoke_descriptor_protocol(
@ -696,14 +693,32 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
attribute_type
};
attribute_type.has_relation_to_impl(
// TODO: Instances of `typing.Self` in the protocol member should specialize to the
// type that we are checking. Without this, we will treat `Self` as an inferable
// typevar, and allow it to match against _any_ type.
//
// It's not very principled, but we also use the literal fallback type, instead of
// `other` directly. This lets us check whether things like `Literal[0]` satisfy a
// protocol that includes methods that have `typing.Self` annotations, without
// overly constraining `Self` to that specific literal.
//
// With the new solver, we should be to replace all of this with an additional
// constraint that enforces what `Self` can specialize to.
let fallback_other = other.literal_fallback_instance(db).unwrap_or(other);
attribute_type
.try_upcast_to_callable(db)
.when_some_and(|callables| {
callables
.map(|callable| callable.apply_self(db, fallback_other))
.has_relation_to_impl(
db,
Type::Callable(method.bind_self(db)),
method.bind_self(db, Some(fallback_other)),
inferable,
relation,
relation_visitor,
disjointness_visitor,
)
})
}
// TODO: consider the types of the attribute on `other` for property members
ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!(

View File

@ -213,6 +213,19 @@ impl<'db> CallableSignature<'db> {
}
}
/// Replaces any occurrences of `typing.Self` in the parameter and return annotations with the
/// given type. (Does not bind the `self` parameter; to do that, use
/// [`bind_self`][Self::bind_self].)
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
Self {
overloads: self
.overloads
.iter()
.map(|signature| signature.apply_self(db, self_type))
.collect(),
}
}
fn is_subtype_of_impl(
&self,
db: &'db dyn Db,
@ -628,6 +641,28 @@ impl<'db> Signature<'db> {
}
}
pub(crate) fn apply_self(&self, db: &'db dyn Db, self_type: Type<'db>) -> Self {
let parameters = self.parameters.apply_type_mapping_impl(
db,
&TypeMapping::BindSelf(self_type),
TypeContext::default(),
&ApplyTypeMappingVisitor::default(),
);
let return_ty = self.return_ty.map(|ty| {
ty.apply_type_mapping(
db,
&TypeMapping::BindSelf(self_type),
TypeContext::default(),
)
});
Self {
generic_context: self.generic_context,
definition: self.definition,
parameters,
return_ty,
}
}
fn inferable_typevars(&self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> {
match self.generic_context {
Some(generic_context) => generic_context.inferable_typevars(db),