[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((1,))) # revealed: Literal[1]
reveal_type(len((1, 2))) # revealed: Literal[2]
# TODO: Handle constructor calls
reveal_type(len(tuple())) # revealed: int
reveal_type(len(tuple())) # revealed: Literal[0]
# TODO: Handle star unpacks; Should be: Literal[0]
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.
```py
# TODO: revealed: tuple[()]
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]
from typing_extensions import Iterable, Never
reveal_type(tuple()) # revealed: tuple[()]
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
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]]
# 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]]
```

View file

@ -946,6 +946,13 @@ impl<'db> Type<'db> {
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`.
/// Since a `ClassType` must be specialized, apply the default specialization to any
/// unspecialized generic class literal.
@ -4237,6 +4244,27 @@ impl<'db> Type<'db> {
.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
// not via getting the signature here. This signature can still be used in some
// cases (e.g. evaluating callable subtyping). TODO improve this definition
@ -4276,14 +4304,24 @@ impl<'db> Type<'db> {
.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 check call vs signatures of `__new__` and/or `__init__`
Binding::single(
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
)
.into()
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
}
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,
NewType,
SupportsIndex,
Iterable,
// Collections
ChainMap,
Counter,
@ -2426,6 +2427,7 @@ impl KnownClass {
| Self::Float
| Self::Enum
| Self::ABCMeta
| KnownClass::Iterable
// Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue
| Self::NamedTuple
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
@ -2513,6 +2515,7 @@ impl KnownClass {
| Self::DefaultDict
| Self::OrderedDict
| Self::NewType
| Self::Iterable
| Self::BaseExceptionGroup => false,
}
}
@ -2531,7 +2534,7 @@ impl KnownClass {
/// 2. It's probably more performant.
const fn is_protocol(self) -> bool {
match self {
Self::SupportsIndex => true,
Self::SupportsIndex | Self::Iterable => true,
Self::Any
| Self::Bool
@ -2648,6 +2651,7 @@ impl KnownClass {
Self::Enum => "Enum",
Self::ABCMeta => "ABCMeta",
Self::Super => "super",
Self::Iterable => "Iterable",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
Self::StdlibAlias => "_Alias",
// This is the name the type of `sys.version_info` has in typeshed,
@ -2882,6 +2886,7 @@ impl KnownClass {
| Self::TypeVar
| Self::NamedTuple
| Self::StdlibAlias
| Self::Iterable
| Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType
| Self::TypeVarTuple
@ -2984,6 +2989,7 @@ impl KnownClass {
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false,
}
}
@ -3052,6 +3058,7 @@ impl KnownClass {
| Self::NewType
| Self::Field
| Self::KwOnly
| Self::Iterable
| Self::NamedTupleFallback => false,
}
}
@ -3101,6 +3108,7 @@ impl KnownClass {
"NewType" => Self::NewType,
"TypeAliasType" => Self::TypeAliasType,
"TypeVar" => Self::TypeVar,
"Iterable" => Self::Iterable,
"ParamSpec" => Self::ParamSpec,
"ParamSpecArgs" => Self::ParamSpecArgs,
"ParamSpecKwargs" => Self::ParamSpecKwargs,
@ -3197,6 +3205,7 @@ impl KnownClass {
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::NamedTuple
| Self::Iterable
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
}
}

View file

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