diff --git a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md index fb1d0a2602..9af250a0a5 100644 --- a/crates/ty_python_semantic/resources/mdtest/expression/boolean.md +++ b/crates/ty_python_semantic/resources/mdtest/expression/boolean.md @@ -93,14 +93,14 @@ class SingleElementTupleSubclass(tuple[int]): ... reveal_type(bool(SingleElementTupleSubclass((0,)))) # revealed: Literal[True] reveal_type(SingleElementTupleSubclass.__bool__) # revealed: (self: tuple[int], /) -> Literal[True] -reveal_type(SingleElementTupleSubclass().__bool__) # revealed: () -> Literal[True] +reveal_type(SingleElementTupleSubclass((1,)).__bool__) # revealed: () -> Literal[True] # Unknown length, but we know the length is guaranteed to be >=2 class MixedTupleSubclass(tuple[int, *tuple[str, ...], bytes]): ... reveal_type(bool(MixedTupleSubclass((1, b"foo")))) # revealed: Literal[True] reveal_type(MixedTupleSubclass.__bool__) # revealed: (self: tuple[int, *tuple[str, ...], bytes], /) -> Literal[True] -reveal_type(MixedTupleSubclass().__bool__) # revealed: () -> Literal[True] +reveal_type(MixedTupleSubclass((1, b"foo")).__bool__) # revealed: () -> Literal[True] # Unknown length with an overridden `__bool__`: class VariadicTupleSubclassWithDunderBoolOverride(tuple[int, ...]): 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 bd2babc0bf..fee5e30fd2 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -19,14 +19,20 @@ def _(p: P, q: Q): ## Instantiating tuples Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a -specialization of `tuple` we (TODO: should) check that the values passed in match the element types -defined in the specialization. +specialization of `tuple` we check that the values passed in match the element types defined in the +specialization. + +```toml +[environment] +python-version = "3.11" +``` ```py from typing_extensions import Iterable, Never reveal_type(tuple()) # revealed: tuple[()] reveal_type(tuple[int]((1,))) # revealed: tuple[int] +reveal_type(tuple[int, *tuple[str, ...]]((1,))) # revealed: tuple[int, *tuple[str, ...]] reveal_type(().__class__()) # revealed: tuple[()] reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]] @@ -56,6 +62,63 @@ reveal_type((1,).__class__()) # revealed: tuple[Literal[1]] reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]] ``` +## Instantiating tuple subclasses + +Tuple subclasses inherit the special-cased constructors from their tuple superclasses: + +```toml +[environment] +python-version = "3.11" +``` + +```py +from typing_extensions import Iterable, Never + +class UnspecializedTupleSubclass(tuple): ... +class EmptyTupleSubclass(tuple[()]): ... +class SingleElementTupleSubclass(tuple[int]): ... +class VariadicTupleSubclass(tuple[int, ...]): ... +class MixedTupleSubclass(tuple[int, *tuple[str, ...]]): ... + +reveal_type(UnspecializedTupleSubclass()) # revealed: UnspecializedTupleSubclass +reveal_type(UnspecializedTupleSubclass(())) # revealed: UnspecializedTupleSubclass +reveal_type(UnspecializedTupleSubclass((1, 2, "foo"))) # revealed: UnspecializedTupleSubclass +reveal_type(UnspecializedTupleSubclass([1, 2, "foo", b"bar"])) # revealed: UnspecializedTupleSubclass + +reveal_type(EmptyTupleSubclass()) # revealed: EmptyTupleSubclass +reveal_type(EmptyTupleSubclass(())) # revealed: EmptyTupleSubclass + +# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[()]`, found `tuple[Literal[1], Literal[2]]`" +reveal_type(EmptyTupleSubclass((1, 2))) # revealed: EmptyTupleSubclass + +reveal_type(SingleElementTupleSubclass((1,))) # revealed: SingleElementTupleSubclass + +# error: [missing-argument] "No argument provided for required parameter `iterable`" +reveal_type(SingleElementTupleSubclass()) # revealed: SingleElementTupleSubclass + +reveal_type(VariadicTupleSubclass()) # revealed: VariadicTupleSubclass +reveal_type(VariadicTupleSubclass(())) # revealed: VariadicTupleSubclass +reveal_type(VariadicTupleSubclass([1, 2, 3])) # revealed: VariadicTupleSubclass +reveal_type(VariadicTupleSubclass((1, 2, 3, 4))) # revealed: VariadicTupleSubclass + +reveal_type(MixedTupleSubclass((1,))) # revealed: MixedTupleSubclass +reveal_type(MixedTupleSubclass((1, "foo"))) # revealed: MixedTupleSubclass + +# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, *tuple[str, ...]]`, found `tuple[Literal[1], Literal[b"foo"]]`" +reveal_type(MixedTupleSubclass((1, b"foo"))) # revealed: MixedTupleSubclass + +# error: [missing-argument] "No argument provided for required parameter `iterable`" +reveal_type(MixedTupleSubclass()) # revealed: MixedTupleSubclass + +def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mixed: MixedTupleSubclass, x: tuple[int, int]): + # error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[()]`, found `tuple[Literal[1], Literal[2]]`" + empty.__class__((1, 2)) + # error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `tuple[Literal[1], Literal[2]]`" + single_element.__class__((1, 2)) + # error: [missing-argument] "No argument provided for required parameter `iterable`" + mixed.__class__() +``` + ## Subtyping relationships The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1` diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 028a411c51..02831c1104 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -916,6 +916,7 @@ c: Callable[[Any], str] = A().g ```py from typing import Any, Callable +from ty_extensions import static_assert, is_assignable_to c: Callable[[object], type] = type c: Callable[[str], Any] = str @@ -936,6 +937,15 @@ class C: def __init__(self, x: int) -> None: ... c: Callable[[int], C] = C + +def f(a: Callable[..., Any], b: Callable[[Any], Any]): ... + +f(tuple, tuple) + +def g(a: Callable[[Any, Any], Any]): ... + +# error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `(Any, Any, /) -> Any`, found ``" +g(tuple) ``` ### Generic class literal types diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 19be47b8ce..76c33a47ea 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4427,31 +4427,14 @@ impl<'db> Type<'db> { .into() } - Type::GenericAlias(alias) => { - let instantiated = Type::instance(db, ClassType::from(alias)); - - let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) { - // ```py - // class tuple: - // @overload - // def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ... - // @overload - // def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ... - // ``` - 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.len().maximum(), Some(0)) { - parameter = parameter.with_default_type(TupleType::empty(db)); - } - Parameters::new([parameter]) - } else { - Parameters::gradual_form() - }; + Type::GenericAlias(_) => { // 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, Some(instantiated))).into() + Binding::single( + self, + Signature::new(Parameters::gradual_form(), self.to_instance(db)), + ) + .into() } Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { @@ -4636,7 +4619,7 @@ impl<'db> Type<'db> { } if let Type::GenericAlias(alias) = self { - if alias.origin(db).is_known(db, KnownClass::Tuple) { + if alias.origin(db).is_tuple(db) { return Ok(todo_type!("*tuple[] annotations")); } } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 1b391029f0..f4f21cfab2 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -575,7 +575,7 @@ impl<'db> ClassType<'db> { pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> { let (class_literal, specialization) = self.class_literal(db); - let synthesize_tuple_method = |return_type| { + let synthesize_simple_tuple_method = |return_type| { let parameters = Parameters::new([Parameter::positional_only(Some(Name::new_static("self"))) .with_annotated_type(Type::instance(db, self))]); @@ -587,22 +587,88 @@ impl<'db> ClassType<'db> { }; match name { - "__len__" if class_literal.is_known(db, KnownClass::Tuple) => { + "__len__" if class_literal.is_tuple(db) => { let return_type = specialization .and_then(|spec| spec.tuple(db).len().into_fixed_length()) .and_then(|len| i64::try_from(len).ok()) .map(Type::IntLiteral) .unwrap_or_else(|| KnownClass::Int.to_instance(db)); - synthesize_tuple_method(return_type) + synthesize_simple_tuple_method(return_type) } - "__bool__" if class_literal.is_known(db, KnownClass::Tuple) => { + + "__bool__" if class_literal.is_tuple(db) => { let return_type = specialization .map(|spec| spec.tuple(db).truthiness().into_type(db)) .unwrap_or_else(|| KnownClass::Bool.to_instance(db)); - synthesize_tuple_method(return_type) + synthesize_simple_tuple_method(return_type) } + + // ```py + // class tuple: + // @overload + // def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ... + // @overload + // def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ... + // ``` + "__new__" if class_literal.is_tuple(db) => { + let mut iterable_parameter = + Parameter::positional_only(Some(Name::new_static("iterable"))); + + match specialization { + Some(spec) => { + let tuple = spec.tuple(db); + let tuple_len = tuple.len(); + + if tuple_len.minimum() == 0 && tuple_len.maximum().is_none() { + // If the tuple has no length restrictions, + // any iterable is allowed as long as the iterable has the correct element type. + let mut tuple_elements = tuple.all_elements(); + iterable_parameter = iterable_parameter.with_annotated_type( + KnownClass::Iterable + .to_specialized_instance(db, [*tuple_elements.next().unwrap()]), + ); + assert_eq!( + tuple_elements.next(), + None, + "Tuple specialization should not have more than one element when it has no length restriction" + ); + } else { + // But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather + // than an iterable, as a tuple is the only kind of iterable for which we can + // specify a fixed length, or that the iterable must be at least a certain length. + iterable_parameter = + iterable_parameter.with_annotated_type(Type::instance(db, self)); + } + } + None => { + // If the tuple isn't specialized at all, we allow any argument as long as it is iterable. + iterable_parameter = iterable_parameter + .with_annotated_type(KnownClass::Iterable.to_instance(db)); + } + } + + // We allow the `iterable` parameter to be omitted for: + // - a zero-length tuple + // - an unspecialized tuple + // - a tuple with no minimum length + if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) { + iterable_parameter = iterable_parameter.with_default_type(TupleType::empty(db)); + } + + let parameters = Parameters::new([ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(SubclassOfType::from(db, self)), + iterable_parameter, + ]); + + let synthesized_dunder = + CallableType::function_like(db, Signature::new(parameters, None)); + + Place::bound(synthesized_dunder).into() + } + _ => class_literal .own_class_member(db, specialization, name) .map_type(|ty| ty.apply_optional_specialization(db, specialization)), @@ -659,38 +725,41 @@ impl<'db> ClassType<'db> { ) .place; - let dunder_new_function = - if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) = - dunder_new_function_symbol - { - // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, - // then we should ignore the `__init__` and just return the `__new__` method. - let returns_non_subclass = - dunder_new_function - .signature(db) - .overloads - .iter() - .any(|signature| { - signature.return_ty.is_some_and(|return_ty| { - !return_ty.is_assignable_to( - db, - self_ty - .to_instance(db) - .expect("ClassType should be instantiable"), - ) - }) - }); + let dunder_new_signature = dunder_new_function_symbol + .ignore_possibly_unbound() + .and_then(|ty| match ty { + Type::FunctionLiteral(function) => Some(function.signature(db)), + Type::Callable(callable) => Some(callable.signatures(db)), + _ => None, + }); - let dunder_new_bound_method = - dunder_new_function.into_bound_method_type(db, self_ty); + let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature { + // Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class, + // then we should ignore the `__init__` and just return the `__new__` method. + let returns_non_subclass = dunder_new_signature.overloads.iter().any(|signature| { + signature.return_ty.is_some_and(|return_ty| { + !return_ty.is_assignable_to( + db, + self_ty + .to_instance(db) + .expect("ClassType should be instantiable"), + ) + }) + }); - if returns_non_subclass { - return dunder_new_bound_method; - } - Some(dunder_new_bound_method) - } else { - None - }; + let dunder_new_bound_method = Type::Callable(CallableType::new( + db, + dunder_new_signature.bind_self(), + true, + )); + + if returns_non_subclass { + return dunder_new_bound_method; + } + Some(dunder_new_bound_method) + } else { + None + }; let dunder_init_function_symbol = self_ty .member_lookup_with_policy( @@ -864,6 +933,10 @@ impl<'db> ClassLiteral<'db> { self.known(db) == Some(known_class) } + pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Tuple) + } + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { // Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code // the knowledge that this class is not generic. diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b99589dee9..68aec9d293 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -511,7 +511,7 @@ impl TupleSpecialization { } fn from_class(db: &dyn Db, class: ClassLiteral) -> Self { - if class.is_known(db, KnownClass::Tuple) { + if class.is_tuple(db) { Self::Yes } else { Self::No diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 9b6aa19bec..6bbfe74f80 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -5637,10 +5637,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::TypeVar | KnownClass::NamedTuple | KnownClass::TypeAliasType - | KnownClass::Tuple | KnownClass::Deprecated ) ) + + // Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`, + // but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.) + // are handled by the default constructor-call logic (we synthesize a `__new__` method for them + // in `ClassType::own_class_member()`). + && (callable_type.is_generic_alias() || !class.is_known(self.db(), KnownClass::Tuple)) + // temporary special-casing for all subclasses of `enum.Enum` // until we support the functional syntax for creating enum classes && KnownClass::Enum @@ -8003,7 +8009,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. if let Type::ClassLiteral(class) = value_ty { - if class.is_known(self.db(), KnownClass::Tuple) { + if class.is_tuple(self.db()) { return self .infer_tuple_type_expression(slice) .to_meta_type(self.db());