Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,57 @@ class D(C[V, int]):
reveal_type(D(1)) # revealed: D[int]
```

### Generic class inherits `__init__` from generic base class

```py
from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T, U]):
def __init__(self, t: T, u: U) -> None: ...

class D(C[T, U]):
pass

reveal_type(C(1, "str")) # revealed: C[int, str]
reveal_type(D(1, "str")) # revealed: D[int, str]
```

### Generic class inherits `__init__` from `dict`

This is a specific example of the above, since it was reported specifically by a user.

```py
from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class D(dict[T, U]):
pass

reveal_type(D(key=1)) # revealed: D[str, int]
```

### Generic class inherits `__new__` from `tuple`

(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
context. But from the user's point of view, this is another example of the above.)

```py
from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(tuple[T, U]): ...

reveal_type(C((1, 2))) # revealed: C[int, int]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of wish that this was C[Literal[1], Literal[2]] -- I don't think there's a strong reason to upcast the Literal types to int types here, since tuple is covariant. We'd obviously infer tuple[Literal[1], Literal[2]] rather than tuple[int, int] for (1, 2), for similar reasons: the covariance of tuple means that tuple[Literal[1], Literal[2]] is assignable to tuple[int, int], so there's no way that inferring the more precise type can produce false positives.

But that's not for this PR! It's obviously a pre-existing issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep totally!

```

### `__init__` is itself generic

```py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,42 @@ class D[V](C[V, int]):
reveal_type(D(1)) # revealed: D[int]
```

### Generic class inherits `__init__` from generic base class

```py
class C[T, U]:
def __init__(self, t: T, u: U) -> None: ...

class D[T, U](C[T, U]):
pass

reveal_type(C(1, "str")) # revealed: C[int, str]
reveal_type(D(1, "str")) # revealed: D[int, str]
```

### Generic class inherits `__init__` from `dict`

This is a specific example of the above, since it was reported specifically by a user.

```py
class D[T, U](dict[T, U]):
pass

reveal_type(D(key=1)) # revealed: D[str, int]
```

### Generic class inherits `__new__` from `tuple`

(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
context. But from the user's point of view, this is another example of the above.)

```py
class C[T, U](tuple[T, U]): ...

reveal_type(C((1, 2))) # revealed: C[int, int]
```

### `__init__` is itself generic

```py
Expand Down
35 changes: 27 additions & 8 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,10 +571,20 @@ impl<'db> ClassType<'db> {
/// Returns the inferred type of the class member named `name`. Only bound members
/// or those marked as ClassVars are considered.
///
/// You must provide the `inherited_generic_context` that we should use for the `__new__` or
/// `__init__` member. This is inherited from the containing class -­but importantly, from the
/// class that the lookup is being performed on, and not the class containing the (possibly
/// inherited) member.
///
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
/// directly. Use [`ClassType::class_member`] if you require a method that will
/// traverse through the MRO until it finds the member.
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
pub(super) fn own_class_member(
self,
db: &'db dyn Db,
inherited_generic_context: Option<GenericContext<'db>>,
name: &str,
) -> PlaceAndQualifiers<'db> {
fn synthesize_getitem_overload_signature<'db>(
index_annotation: Type<'db>,
return_annotation: Type<'db>,
Expand All @@ -590,7 +600,7 @@ impl<'db> ClassType<'db> {

let fallback_member_lookup = || {
class_literal
.own_class_member(db, specialization, name)
.own_class_member(db, inherited_generic_context, specialization, name)
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
};

Expand Down Expand Up @@ -840,8 +850,11 @@ impl<'db> ClassType<'db> {
iterable_parameter,
]);

let synthesized_dunder =
CallableType::function_like(db, Signature::new(parameters, None));
let synthesized_dunder = CallableType::function_like(
db,
Signature::new(parameters, None)
.with_inherited_generic_context(inherited_generic_context),
);

Place::bound(synthesized_dunder).into()
}
Expand Down Expand Up @@ -1668,7 +1681,10 @@ impl<'db> ClassLiteral<'db> {
}

lookup_result = lookup_result.or_else(|lookup_error| {
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
lookup_error.or_fall_back_to(
db,
class.own_class_member(db, self.generic_context(db), name),
)
});
}
}
Expand Down Expand Up @@ -1716,6 +1732,7 @@ impl<'db> ClassLiteral<'db> {
pub(super) fn own_class_member(
self,
db: &'db dyn Db,
inherited_generic_context: Option<GenericContext<'db>>,
specialization: Option<Specialization<'db>>,
name: &str,
) -> PlaceAndQualifiers<'db> {
Expand Down Expand Up @@ -1744,7 +1761,7 @@ impl<'db> ClassLiteral<'db> {
// to any method with a `@classmethod` decorator. (`__init__` would remain a special
// case, since it's an _instance_ method where we don't yet know the generic class's
// specialization.)
match (self.generic_context(db), ty, specialization, name) {
match (inherited_generic_context, ty, specialization, name) {
(
Some(generic_context),
Type::FunctionLiteral(function),
Expand Down Expand Up @@ -1926,7 +1943,7 @@ impl<'db> ClassLiteral<'db> {
KnownClass::NamedTupleFallback
.to_class_literal(db)
.into_class_literal()?
.own_class_member(db, None, name)
.own_class_member(db, self.generic_context(db), None, name)
.place
.ignore_possibly_unbound()
}
Expand Down Expand Up @@ -4321,7 +4338,9 @@ enum SlotsKind {

impl SlotsKind {
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
let Place::Type(slots_ty, bound) = base
.own_class_member(db, base.generic_context(db), None, "__slots__")
.place
else {
return Self::NotSpecified;
};
Expand Down
8 changes: 8 additions & 0 deletions crates/ty_python_semantic/src/types/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,14 @@ impl<'db> Signature<'db> {
Self::new(Parameters::object(db), Some(Type::Never))
}

pub(crate) fn with_inherited_generic_context(
mut self,
inherited_generic_context: Option<GenericContext<'db>>,
) -> Self {
self.inherited_generic_context = inherited_generic_context;
self
}

fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
Self {
generic_context: self.generic_context,
Expand Down
Loading