mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Make tuple subclass constructors sound (#19469)
This commit is contained in:
parent
fcdffe4ac9
commit
cb5a9ff8dc
7 changed files with 201 additions and 66 deletions
|
@ -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, ...]):
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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 `<class 'tuple'>`"
|
||||
g(tuple)
|
||||
```
|
||||
|
||||
### Generic class literal types
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,18 +725,18 @@ impl<'db> ClassType<'db> {
|
|||
)
|
||||
.place;
|
||||
|
||||
let dunder_new_function =
|
||||
if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) =
|
||||
dunder_new_function_symbol
|
||||
{
|
||||
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_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_function
|
||||
.signature(db)
|
||||
.overloads
|
||||
.iter()
|
||||
.any(|signature| {
|
||||
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,
|
||||
|
@ -681,8 +747,11 @@ impl<'db> ClassType<'db> {
|
|||
})
|
||||
});
|
||||
|
||||
let dunder_new_bound_method =
|
||||
dunder_new_function.into_bound_method_type(db, self_ty);
|
||||
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;
|
||||
|
@ -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<GenericContext<'db>> {
|
||||
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
|
||||
// the knowledge that this class is not generic.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue