Compare commits
3 Commits
david/allo
...
cjm/nomrof
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59dedb5d3c | ||
|
|
46a1fd3b3e | ||
|
|
327b913d68 |
@@ -275,14 +275,16 @@ c: C[int] = C[int]()
|
||||
reveal_type(c.method("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Cyclic class definition
|
||||
## Cyclic class definitions
|
||||
|
||||
### F-bounded quantification
|
||||
|
||||
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
|
||||
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
|
||||
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
#### In a stub file
|
||||
|
||||
`stub.pyi`:
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
|
||||
```pyi
|
||||
class Base[T]: ...
|
||||
@@ -291,9 +293,9 @@ class Sub(Base[Sub]): ...
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
A similar case can work in a non-stub file, if forward references are stringified:
|
||||
#### With string forward references
|
||||
|
||||
`string_annotation.py`:
|
||||
A similar case can work in a non-stub file, if forward references are stringified:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
@@ -302,9 +304,9 @@ class Sub(Base["Sub"]): ...
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
In a non-stub file, without stringified forward references, this raises a `NameError`:
|
||||
#### Without string forward references
|
||||
|
||||
`bare_annotation.py`:
|
||||
In a non-stub file, without stringified forward references, this raises a `NameError`:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
@@ -313,11 +315,23 @@ class Base[T]: ...
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
## Another cyclic case
|
||||
### Cyclic inheritance as a generic parameter
|
||||
|
||||
```pyi
|
||||
class Derived[T](list[Derived[T]]): ...
|
||||
```
|
||||
|
||||
### Direct cyclic inheritance
|
||||
|
||||
Inheritance that would result in a cyclic MRO is detected as an error.
|
||||
|
||||
```py
|
||||
# error: [cyclic-class-definition]
|
||||
class C[T](C): ...
|
||||
|
||||
# error: [cyclic-class-definition]
|
||||
class D[T](D[int]): ...
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
||||
|
||||
@@ -53,6 +53,25 @@ class B(A): ...
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Linear inheritance with PEP 695 generic class
|
||||
|
||||
The same is true if the base with the metaclass is a generic class.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T](metaclass=M): ...
|
||||
class B(A): ...
|
||||
class C(A[int]): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
|
||||
@@ -688,20 +688,21 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn into_class_type(self) -> Option<ClassType<'db>> {
|
||||
/// 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.
|
||||
pub fn to_class_type(self, db: &'db dyn Db) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Type::ClassLiteral(ClassLiteralType::NonGeneric(non_generic)) => {
|
||||
Some(ClassType::NonGeneric(non_generic))
|
||||
}
|
||||
Type::ClassLiteral(class_literal) => Some(class_literal.default_specialization(db)),
|
||||
Type::GenericAlias(alias) => Some(ClassType::Generic(alias)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_class_type(self) -> ClassType<'db> {
|
||||
self.into_class_type()
|
||||
.expect("Expected a Type::GenericAlias or non-generic Type::ClassLiteral variant")
|
||||
pub fn expect_class_type(self, db: &'db dyn Db) -> ClassType<'db> {
|
||||
self.to_class_type(db)
|
||||
.expect("Expected a Type::GenericAlias or Type::ClassLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn is_class_type(&self) -> bool {
|
||||
|
||||
@@ -62,28 +62,6 @@ fn explicit_bases_cycle_initial<'db>(
|
||||
Box::default()
|
||||
}
|
||||
|
||||
fn try_mro_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<Mro<'db>, MroError<'db>>,
|
||||
_count: u32,
|
||||
_self: ClassLiteralType<'db>,
|
||||
_specialization: Option<Specialization<'db>>,
|
||||
) -> salsa::CycleRecoveryAction<Result<Mro<'db>, MroError<'db>>> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn try_mro_cycle_initial<'db>(
|
||||
db: &'db dyn Db,
|
||||
self_: ClassLiteralType<'db>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
) -> Result<Mro<'db>, MroError<'db>> {
|
||||
Ok(Mro::from_error(
|
||||
db,
|
||||
self_.apply_optional_specialization(db, specialization),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
|
||||
fn inheritance_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
@@ -174,6 +152,10 @@ impl<'db> GenericAlias<'db> {
|
||||
pub(crate) fn definition(self, db: &'db dyn Db) -> Definition<'db> {
|
||||
self.origin(db).class(db).definition(db)
|
||||
}
|
||||
|
||||
pub(crate) fn class_literal(self, db: &'db dyn Db) -> ClassLiteralType<'db> {
|
||||
ClassLiteralType::Generic(self.origin(db))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<GenericAlias<'db>> for Type<'db> {
|
||||
@@ -573,12 +555,13 @@ impl<'db> ClassLiteralType<'db> {
|
||||
self.explicit_bases_query(db)
|
||||
}
|
||||
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class objects.
|
||||
/// Iterate over this class's explicit bases, filtering out any bases that are not class
|
||||
/// objects, and applying default specialization to any unspecialized generic class literals.
|
||||
fn fully_static_explicit_bases(self, db: &'db dyn Db) -> impl Iterator<Item = ClassType<'db>> {
|
||||
self.explicit_bases(db)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(Type::into_class_type)
|
||||
.filter_map(|ty| ty.to_class_type(db))
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref, cycle_fn=explicit_bases_cycle_recover, cycle_initial=explicit_bases_cycle_initial)]
|
||||
@@ -656,7 +639,7 @@ impl<'db> ClassLiteralType<'db> {
|
||||
/// attribute on a class at runtime.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
#[salsa::tracked(return_ref, cycle_fn=try_mro_cycle_recover, cycle_initial=try_mro_cycle_initial)]
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(super) fn try_mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -763,7 +746,7 @@ impl<'db> ClassLiteralType<'db> {
|
||||
(KnownClass::Type.to_class_literal(db), self)
|
||||
};
|
||||
|
||||
let mut candidate = if let Some(metaclass_ty) = metaclass.into_class_type() {
|
||||
let mut candidate = if let Some(metaclass_ty) = metaclass.to_class_type(db) {
|
||||
MetaclassCandidate {
|
||||
metaclass: metaclass_ty,
|
||||
explicit_metaclass_of: class_metaclass_was_from,
|
||||
@@ -805,7 +788,7 @@ impl<'db> ClassLiteralType<'db> {
|
||||
// - https://github.com/python/cpython/blob/83ba8c2bba834c0b92de669cac16fcda17485e0e/Objects/typeobject.c#L3629-L3663
|
||||
for base_class in base_classes {
|
||||
let metaclass = base_class.metaclass(db);
|
||||
let Some(metaclass) = metaclass.into_class_type() else {
|
||||
let Some(metaclass) = metaclass.to_class_type(db) else {
|
||||
continue;
|
||||
};
|
||||
if metaclass.is_subclass_of(db, candidate.metaclass) {
|
||||
@@ -1690,8 +1673,12 @@ impl<'db> ClassLiteralType<'db> {
|
||||
visited_classes: &mut IndexSet<ClassLiteralType<'db>>,
|
||||
) -> bool {
|
||||
let mut result = false;
|
||||
for explicit_base_class in class.fully_static_explicit_bases(db) {
|
||||
let (explicit_base_class_literal, _) = explicit_base_class.class_literal(db);
|
||||
for explicit_base in class.explicit_bases(db) {
|
||||
let explicit_base_class_literal = match explicit_base {
|
||||
Type::ClassLiteral(class_literal) => *class_literal,
|
||||
Type::GenericAlias(generic_alias) => generic_alias.class_literal(db),
|
||||
_ => continue,
|
||||
};
|
||||
if !classes_on_stack.insert(explicit_base_class_literal) {
|
||||
return true;
|
||||
}
|
||||
@@ -1705,7 +1692,6 @@ impl<'db> ClassLiteralType<'db> {
|
||||
visited_classes,
|
||||
);
|
||||
}
|
||||
|
||||
classes_on_stack.pop();
|
||||
}
|
||||
result
|
||||
@@ -2157,7 +2143,7 @@ impl<'db> KnownClass {
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.to_class_literal(db)
|
||||
.into_class_type()
|
||||
.to_class_type(db)
|
||||
.map(Type::instance)
|
||||
.unwrap_or_else(Type::unknown)
|
||||
}
|
||||
@@ -2224,7 +2210,7 @@ impl<'db> KnownClass {
|
||||
/// If the class cannot be found in typeshed, a debug-level log message will be emitted stating this.
|
||||
pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.to_class_literal(db)
|
||||
.into_class_type()
|
||||
.to_class_type(db)
|
||||
.map(|class| SubclassOfType::from(db, class))
|
||||
.unwrap_or_else(SubclassOfType::subclass_of_unknown)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl<'db> ClassBase<'db> {
|
||||
pub(super) fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
.to_class_literal(db)
|
||||
.into_class_type()
|
||||
.to_class_type(db)
|
||||
.map_or(Self::unknown(), Self::Class)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user