Skip to content

[ty] Implement implicit inheritance from Generic[] for PEP-695 generic classes #18283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 26, 2025
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 @@ -37,7 +37,7 @@ class RepeatedTypevar(Generic[T, T]): ...
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).

```py
# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `typing.Generic`"
# error: [invalid-argument-type] "`<class 'int'>` is not a valid argument to `Generic`"
class GenericOfType(Generic[int]): ...
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,41 @@ T = TypeVar("T")

# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables"
class BothGenericSyntaxes[U](Generic[T]): ...

reveal_type(BothGenericSyntaxes.__mro__) # revealed: tuple[<class 'BothGenericSyntaxes[Unknown]'>, Unknown, <class 'object'>]

# error: [invalid-generic-class] "Cannot both inherit from `typing.Generic` and use PEP 695 type variables"
# error: [invalid-base] "Cannot inherit from plain `Generic`"
class DoublyInvalid[T](Generic): ...

reveal_type(DoublyInvalid.__mro__) # revealed: tuple[<class 'DoublyInvalid[Unknown]'>, Unknown, <class 'object'>]
```

Generic classes implicitly inherit from `Generic`:

```py
class Foo[T]: ...

# revealed: tuple[<class 'Foo[Unknown]'>, typing.Generic, <class 'object'>]
reveal_type(Foo.__mro__)
# revealed: tuple[<class 'Foo[int]'>, typing.Generic, <class 'object'>]
reveal_type(Foo[int].__mro__)

class A: ...
class Bar[T](A): ...

# revealed: tuple[<class 'Bar[Unknown]'>, <class 'A'>, typing.Generic, <class 'object'>]
reveal_type(Bar.__mro__)
# revealed: tuple[<class 'Bar[int]'>, <class 'A'>, typing.Generic, <class 'object'>]
reveal_type(Bar[int].__mro__)

class B: ...
class Baz[T](A, B): ...

# revealed: tuple[<class 'Baz[Unknown]'>, <class 'A'>, <class 'B'>, typing.Generic, <class 'object'>]
reveal_type(Baz.__mro__)
# revealed: tuple[<class 'Baz[int]'>, <class 'A'>, <class 'B'>, typing.Generic, <class 'object'>]
reveal_type(Baz[int].__mro__)
```

## Specializing generic classes explicitly
Expand Down
8 changes: 4 additions & 4 deletions crates/ty_python_semantic/resources/mdtest/mro.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,14 +644,14 @@ reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, Unknown, <class 'object'>

class D(D.a):
a: D
#reveal_type(D.__class__) # revealed: <class 'type'>
reveal_type(D.__class__) # revealed: <class 'type'>
reveal_type(D.__mro__) # revealed: tuple[<class 'D'>, Unknown, <class 'object'>]

class E[T](E.a): ...
#reveal_type(E.__class__) # revealed: <class 'type'>
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, <class 'object'>]
reveal_type(E.__class__) # revealed: <class 'type'>
reveal_type(E.__mro__) # revealed: tuple[<class 'E[Unknown]'>, Unknown, typing.Generic, <class 'object'>]

class F[T](F(), F): ... # error: [cyclic-class-definition]
#reveal_type(F.__class__) # revealed: <class 'type'>
reveal_type(F.__class__) # revealed: type[Unknown]
reveal_type(F.__mro__) # revealed: tuple[<class 'F[Unknown]'>, Unknown, <class 'object'>]
```
6 changes: 5 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@ class Bar1(Protocol[T], Generic[T]):
class Bar2[T](Protocol):
x: T

# error: [invalid-generic-class] "Cannot both inherit from subscripted `typing.Protocol` and use PEP 695 type variables"
# error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables"
class Bar3[T](Protocol[T]):
x: T

# Note that this class definition *will* actually succeed at runtime,
# unlike classes that combine PEP-695 type parameters with inheritance from `Generic[]`
reveal_type(Bar3.__mro__) # revealed: tuple[<class 'Bar3[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
```

It's an error to include both bare `Protocol` and subscripted `Protocol[]` in the bases list
Expand Down
11 changes: 9 additions & 2 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,11 @@ impl<'db> ClassType<'db> {
}
}

pub(super) const fn is_generic(self) -> bool {
matches!(self, Self::Generic(_))
pub(super) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
match self {
Self::NonGeneric(class) => class.has_pep_695_type_params(db),
Self::Generic(generic) => generic.origin(db).has_pep_695_type_params(db),
}
}

/// Returns the class literal and specialization for this class. For a non-generic class, this
Expand Down Expand Up @@ -573,6 +576,10 @@ impl<'db> ClassLiteral<'db> {
.or_else(|| self.inherited_legacy_generic_context(db))
}

pub(crate) fn has_pep_695_type_params(self, db: &'db dyn Db) -> bool {
self.pep695_generic_context(db).is_some()
}

#[salsa::tracked(cycle_fn=pep695_generic_context_cycle_recover, cycle_initial=pep695_generic_context_cycle_initial)]
pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
let scope = self.body_scope(db);
Expand Down
54 changes: 9 additions & 45 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ use crate::{Db, FxOrderSet};
pub struct GenericContext<'db> {
#[returns(ref)]
pub(crate) variables: FxOrderSet<TypeVarInstance<'db>>,
pub(crate) origin: GenericContextOrigin,
}

impl<'db> GenericContext<'db> {
Expand All @@ -41,7 +40,7 @@ impl<'db> GenericContext<'db> {
.iter()
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
.collect();
Self::new(db, variables, GenericContextOrigin::TypeParameterList)
Self::new(db, variables)
}

fn variable_from_type_param(
Expand Down Expand Up @@ -87,11 +86,7 @@ impl<'db> GenericContext<'db> {
if variables.is_empty() {
return None;
}
Some(Self::new(
db,
variables,
GenericContextOrigin::LegacyGenericFunction,
))
Some(Self::new(db, variables))
}

/// Creates a generic context from the legacy `TypeVar`s that appear in class's base class
Expand All @@ -107,7 +102,7 @@ impl<'db> GenericContext<'db> {
if variables.is_empty() {
return None;
}
Some(Self::new(db, variables, GenericContextOrigin::Inherited))
Some(Self::new(db, variables))
}

pub(crate) fn len(self, db: &'db dyn Db) -> usize {
Expand Down Expand Up @@ -244,46 +239,21 @@ impl<'db> GenericContext<'db> {
.iter()
.map(|ty| ty.normalized(db))
.collect();
Self::new(db, variables, self.origin(db))
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum GenericContextOrigin {
LegacyBase(LegacyGenericBase),
Inherited,
LegacyGenericFunction,
TypeParameterList,
}

impl GenericContextOrigin {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::LegacyBase(base) => base.as_str(),
Self::Inherited => "inherited",
Self::LegacyGenericFunction => "legacy generic function",
Self::TypeParameterList => "type parameter list",
}
Self::new(db, variables)
}
}

impl std::fmt::Display for GenericContextOrigin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum LegacyGenericBase {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(super) enum LegacyGenericBase {
Generic,
Protocol,
}

impl LegacyGenericBase {
pub(crate) const fn as_str(self) -> &'static str {
const fn as_str(self) -> &'static str {
match self {
Self::Generic => "`typing.Generic`",
Self::Protocol => "subscripted `typing.Protocol`",
Self::Generic => "Generic",
Self::Protocol => "Protocol",
}
}
}
Expand All @@ -294,12 +264,6 @@ impl std::fmt::Display for LegacyGenericBase {
}
}

impl From<LegacyGenericBase> for GenericContextOrigin {
fn from(base: LegacyGenericBase) -> Self {
Self::LegacyBase(base)
}
}

/// An assignment of a specific type to each type variable in a generic scope.
///
/// TODO: Handle nested specializations better, with actual parent links to the specialization of
Expand Down
54 changes: 33 additions & 21 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ use super::diagnostic::{
report_runtime_check_against_non_runtime_checkable_protocol, report_slice_step_size_zero,
report_unresolved_reference,
};
use super::generics::{GenericContextOrigin, LegacyGenericBase};
use super::generics::LegacyGenericBase;
use super::slots::check_class_slots;
use super::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION, parse_string_annotation,
Expand Down Expand Up @@ -856,6 +856,25 @@ impl<'db> TypeInferenceBuilder<'db> {
}
continue;
}
// Note that unlike several of the other errors caught in this function,
// this does not lead to the class creation failing at runtime,
// but it is semantically invalid.
Type::KnownInstance(KnownInstanceType::Protocol(Some(_))) => {
if class_node.type_params.is_none() {
continue;
}
let Some(builder) = self
.context
.report_lint(&INVALID_GENERIC_CLASS, &class_node.bases()[i])
else {
continue;
};
builder.into_diagnostic(
"Cannot both inherit from subscripted `Protocol` \
and use PEP 695 type variables",
);
continue;
}
Type::ClassLiteral(class) => class,
// dynamic/unknown bases are never `@final`
_ => continue,
Expand Down Expand Up @@ -917,7 +936,7 @@ impl<'db> TypeInferenceBuilder<'db> {
{
builder.into_diagnostic(format_args!(
"Cannot create a consistent method resolution order (MRO) \
for class `{}` with bases list `[{}]`",
for class `{}` with bases list `[{}]`",
class.name(self.db()),
bases_list
.iter()
Expand All @@ -926,6 +945,16 @@ impl<'db> TypeInferenceBuilder<'db> {
));
}
}
MroErrorKind::Pep695ClassWithGenericInheritance => {
if let Some(builder) =
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(
"Cannot both inherit from `typing.Generic` \
and use PEP 695 type variables",
);
}
}
MroErrorKind::InheritanceCycle => {
if let Some(builder) = self
.context
Expand Down Expand Up @@ -1022,21 +1051,6 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}

// (5) Check that a generic class does not have invalid or conflicting generic
// contexts.
if class.pep695_generic_context(self.db()).is_some() {
if let Some(legacy_context) = class.legacy_generic_context(self.db()) {
if let Some(builder) =
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(format_args!(
"Cannot both inherit from {} and use PEP 695 type variables",
legacy_context.origin(self.db())
));
}
}
}

if let (Some(legacy), Some(inherited)) = (
class.legacy_generic_context(self.db()),
class.inherited_legacy_generic_context(self.db()),
Expand Down Expand Up @@ -7622,17 +7636,15 @@ impl<'db> TypeInferenceBuilder<'db> {
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
{
builder.into_diagnostic(format_args!(
"`{}` is not a valid argument to {origin}",
"`{}` is not a valid argument to `{origin}`",
typevar.display(self.db()),
));
}
None
}
})
.collect();
typevars.map(|typevars| {
GenericContext::new(self.db(), typevars, GenericContextOrigin::from(origin))
})
typevars.map(|typevars| GenericContext::new(self.db(), typevars))
}

fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> {
Expand Down
Loading
Loading