diff --git a/crates/ty_python_semantic/resources/mdtest/expression/len.md b/crates/ty_python_semantic/resources/mdtest/expression/len.md index 9fe787822b..6dd80b10db 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/len.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/len.md @@ -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] diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index 02d183fd85..bd2babc0bf 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -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]] ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3c2c475d23..d62a76f8eb 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -946,6 +946,13 @@ impl<'db> Type<'db> { matches!(self, Type::ClassLiteral(..)) } + pub(crate) const fn into_tuple(self) -> Option> { + 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() { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 7e2ba6d807..6d0810bab6 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -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); + } + } + _ => {} }, diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 6b7752df6a..2f69f742af 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -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), } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 77fe942ef0..a8301e04f6 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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`