[ty] Make tuple instantiations sound (#18987)

## Summary

Ensure that we correctly infer calls such as `tuple((1, 2))`,
`tuple(range(42))`, etc. Ensure that we emit errors on invalid calls
such as `tuple[int, str]()`.

## Test Plan

Mdtests
This commit is contained in:
Alex Waygood 2025-06-27 19:37:16 +01:00 committed by GitHub
parent 6802c4702f
commit a50a993b9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 105 additions and 19 deletions

View file

@ -41,9 +41,7 @@ bar"""
reveal_type(len(())) # revealed: Literal[0] reveal_type(len(())) # revealed: Literal[0]
reveal_type(len((1,))) # revealed: Literal[1] reveal_type(len((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2] reveal_type(len((1, 2))) # revealed: Literal[2]
reveal_type(len(tuple())) # revealed: Literal[0]
# TODO: Handle constructor calls
reveal_type(len(tuple())) # revealed: int
# TODO: Handle star unpacks; Should be: Literal[0] # TODO: Handle star unpacks; Should be: Literal[0]
reveal_type(len((*[],))) # revealed: Literal[1] reveal_type(len((*[],))) # revealed: Literal[1]

View file

@ -23,18 +23,36 @@ specialization of `tuple` we (TODO: should) check that the values passed in matc
defined in the specialization. defined in the specialization.
```py ```py
# TODO: revealed: tuple[()] from typing_extensions import Iterable, Never
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
# TODO: revealed: tuple[Literal[1]]
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
reveal_type(tuple[int]([1])) # revealed: tuple[int]
# TODO: error for invalid arguments
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]
reveal_type(tuple()) # revealed: tuple[()]
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
reveal_type(().__class__()) # revealed: tuple[()] reveal_type(().__class__()) # revealed: tuple[()]
# TODO: error for invalid arguments reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]):
reveal_type(tuple(x)) # revealed: tuple[int, ...]
reveal_type(tuple(y)) # revealed: tuple[str, ...]
reveal_type(tuple(z)) # revealed: tuple[Unknown, ...]
# This is correct as the only inhabitants of `list[Never]` can be empty lists
reveal_type(tuple(aa)) # revealed: tuple[()]
reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
# TODO: should be `tuple[Literal[1], ...]`
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `list[Unknown]`"
reveal_type(tuple[int]([1])) # revealed: tuple[int]
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, str]`, found `tuple[Literal[1]]`"
reveal_type(tuple[int, str]((1,))) # revealed: tuple[int, str]
# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]] reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
# TODO: error for invalid arguments
# error: [missing-argument] "No argument provided for required parameter `iterable`"
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]] reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
``` ```

View file

@ -946,6 +946,13 @@ impl<'db> Type<'db> {
matches!(self, Type::ClassLiteral(..)) matches!(self, Type::ClassLiteral(..))
} }
pub(crate) const fn into_tuple(self) -> Option<TupleType<'db>> {
match self {
Type::Tuple(tuple_type) => Some(tuple_type),
_ => None,
}
}
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`. /// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
/// Since a `ClassType` must be specialized, apply the default specialization to any /// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal. /// unspecialized generic class literal.
@ -4237,6 +4244,27 @@ impl<'db> Type<'db> {
.into() .into()
} }
Some(KnownClass::Tuple) => {
let object = Type::object(db);
CallableBinding::from_overloads(
self,
[
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
Signature::new(
Parameters::new([Parameter::positional_only(Some(
Name::new_static("iterable"),
))
.with_annotated_type(
KnownClass::Iterable.to_specialized_instance(db, [object]),
)]),
Some(TupleType::homogeneous(db, object)),
),
],
)
.into()
}
// Most class literal constructor calls are handled by `try_call_constructor` and // Most class literal constructor calls are handled by `try_call_constructor` and
// not via getting the signature here. This signature can still be used in some // not via getting the signature here. This signature can still be used in some
// cases (e.g. evaluating callable subtyping). TODO improve this definition // cases (e.g. evaluating callable subtyping). TODO improve this definition
@ -4276,14 +4304,24 @@ impl<'db> Type<'db> {
.into() .into()
} }
Type::GenericAlias(_) => { Type::GenericAlias(alias) => {
let instantiated = Type::instance(db, ClassType::from(alias));
let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
let spec = alias.specialization(db).tuple(db);
let mut parameter =
Parameter::positional_only(Some(Name::new_static("iterable")))
.with_annotated_type(instantiated);
if matches!(spec.size_hint().1, Some(0)) {
parameter = parameter.with_default_type(TupleType::empty(db));
}
Parameters::new([parameter])
} else {
Parameters::gradual_form()
};
// TODO annotated return type on `__new__` or metaclass `__call__` // TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__` // TODO check call vs signatures of `__new__` and/or `__init__`
Binding::single( Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
)
.into()
} }
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {

View file

@ -972,6 +972,28 @@ impl<'db> Bindings<'db> {
} }
} }
Some(KnownClass::Tuple) if overload_index == 1 => {
if let [Some(argument)] = overload.parameter_types() {
let overridden_return =
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
// Some awkward special handling is required here because of the fact
// that calling `try_iterate()` on `Never` returns `Never`,
// but `tuple[Never, ...]` eagerly simplifies to `tuple[()]`,
// which will cause us to emit false positives if we index into the tuple
let specialization = if argument.is_never() {
Type::unknown()
} else {
argument.try_iterate(db).expect(
"try_iterate() should not fail on a type \
assignable to `Iterable`",
)
};
TupleType::homogeneous(db, specialization)
});
overload.set_return_type(overridden_return);
}
}
_ => {} _ => {}
}, },

View file

@ -2334,6 +2334,7 @@ pub enum KnownClass {
NamedTuple, NamedTuple,
NewType, NewType,
SupportsIndex, SupportsIndex,
Iterable,
// Collections // Collections
ChainMap, ChainMap,
Counter, Counter,
@ -2426,6 +2427,7 @@ impl KnownClass {
| Self::Float | Self::Float
| Self::Enum | Self::Enum
| Self::ABCMeta | Self::ABCMeta
| KnownClass::Iterable
// Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue // Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue
| Self::NamedTuple | Self::NamedTuple
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9 // Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
@ -2513,6 +2515,7 @@ impl KnownClass {
| Self::DefaultDict | Self::DefaultDict
| Self::OrderedDict | Self::OrderedDict
| Self::NewType | Self::NewType
| Self::Iterable
| Self::BaseExceptionGroup => false, | Self::BaseExceptionGroup => false,
} }
} }
@ -2531,7 +2534,7 @@ impl KnownClass {
/// 2. It's probably more performant. /// 2. It's probably more performant.
const fn is_protocol(self) -> bool { const fn is_protocol(self) -> bool {
match self { match self {
Self::SupportsIndex => true, Self::SupportsIndex | Self::Iterable => true,
Self::Any Self::Any
| Self::Bool | Self::Bool
@ -2648,6 +2651,7 @@ impl KnownClass {
Self::Enum => "Enum", Self::Enum => "Enum",
Self::ABCMeta => "ABCMeta", Self::ABCMeta => "ABCMeta",
Self::Super => "super", Self::Super => "super",
Self::Iterable => "Iterable",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed // For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias", Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed, // This is the name the type of `sys.version_info` has in typeshed,
@ -2882,6 +2886,7 @@ impl KnownClass {
| Self::TypeVar | Self::TypeVar
| Self::NamedTuple | Self::NamedTuple
| Self::StdlibAlias | Self::StdlibAlias
| Self::Iterable
| Self::SupportsIndex => KnownModule::Typing, | Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType Self::TypeAliasType
| Self::TypeVarTuple | Self::TypeVarTuple
@ -2984,6 +2989,7 @@ impl KnownClass {
| Self::NewType | Self::NewType
| Self::Field | Self::Field
| Self::KwOnly | Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false, | Self::NamedTupleFallback => false,
} }
} }
@ -3052,6 +3058,7 @@ impl KnownClass {
| Self::NewType | Self::NewType
| Self::Field | Self::Field
| Self::KwOnly | Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false, | Self::NamedTupleFallback => false,
} }
} }
@ -3101,6 +3108,7 @@ impl KnownClass {
"NewType" => Self::NewType, "NewType" => Self::NewType,
"TypeAliasType" => Self::TypeAliasType, "TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar, "TypeVar" => Self::TypeVar,
"Iterable" => Self::Iterable,
"ParamSpec" => Self::ParamSpec, "ParamSpec" => Self::ParamSpec,
"ParamSpecArgs" => Self::ParamSpecArgs, "ParamSpecArgs" => Self::ParamSpecArgs,
"ParamSpecKwargs" => Self::ParamSpecKwargs, "ParamSpecKwargs" => Self::ParamSpecKwargs,
@ -3197,6 +3205,7 @@ impl KnownClass {
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
| Self::NamedTuple | Self::NamedTuple
| Self::Iterable
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
} }
} }

View file

@ -5343,6 +5343,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownClass::TypeVar | KnownClass::TypeVar
| KnownClass::NamedTuple | KnownClass::NamedTuple
| KnownClass::TypeAliasType | KnownClass::TypeAliasType
| KnownClass::Tuple
) )
) )
// temporary special-casing for all subclasses of `enum.Enum` // temporary special-casing for all subclasses of `enum.Enum`