Skip to content

Commit

Permalink
[red-knot] Allow type[] to be subscripted (#13667)
Browse files Browse the repository at this point in the history
Fixed a TODO by adding another TODO. It's the red-knot way!

## Summary

`builtins.type` can be subscripted at runtime on Python 3.9+, even
though it has no `__class_getitem__` method and its metaclass (which
is... itself) has no `__getitem__` method. The special case is
[hardcoded directly into `PyObject_GetItem` in
CPython](https://github.com/python/cpython/blob/744caa8ef42ab67c6aa20cd691e078721e72e22a/Objects/abstract.c#L181-L184).
We just have to replicate the special case in our semantic model.

This will fail at runtime on Python <3.9. However, there's a bunch of
outstanding questions (detailed in the TODO comment I added) regarding
how we deal with subscriptions of other generic types on lower Python
versions. Since we want to avoid too many false positives for now, I
haven't tried to address this; I've just made `type` subscriptable on
all Python versions.

## Test Plan

`cargo test -p red_knot_python_semantic --lib`
  • Loading branch information
AlexWaygood authored Oct 7, 2024
1 parent fb90f5a commit 71b52b8
Show file tree
Hide file tree
Showing 2 changed files with 19 additions and 9 deletions.
9 changes: 7 additions & 2 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ pub enum KnownClass {
Set,
Dict,
// Types
GenericAlias,
ModuleType,
FunctionType,
// Typeshed
Expand All @@ -857,6 +858,7 @@ impl<'db> KnownClass {
Self::Dict => "dict",
Self::List => "list",
Self::Type => "type",
Self::GenericAlias => "GenericAlias",
Self::ModuleType => "ModuleType",
Self::FunctionType => "FunctionType",
Self::NoneType => "NoneType",
Expand All @@ -880,7 +882,9 @@ impl<'db> KnownClass {
| Self::Tuple
| Self::Set
| Self::Dict => builtins_symbol_ty(db, self.as_str()),
Self::ModuleType | Self::FunctionType => types_symbol_ty(db, self.as_str()),
Self::GenericAlias | Self::ModuleType | Self::FunctionType => {
types_symbol_ty(db, self.as_str())
}
Self::NoneType => typeshed_symbol_ty(db, self.as_str()),
}
}
Expand Down Expand Up @@ -910,6 +914,7 @@ impl<'db> KnownClass {
"set" => Some(Self::Set),
"dict" => Some(Self::Dict),
"list" => Some(Self::List),
"GenericAlias" => Some(Self::GenericAlias),
"NoneType" => Some(Self::NoneType),
"ModuleType" => Some(Self::ModuleType),
"FunctionType" => Some(Self::FunctionType),
Expand All @@ -934,7 +939,7 @@ impl<'db> KnownClass {
| Self::Tuple
| Self::Set
| Self::Dict => module.name() == "builtins",
Self::ModuleType | Self::FunctionType => module.name() == "types",
Self::GenericAlias | Self::ModuleType | Self::FunctionType => module.name() == "types",
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
}
}
Expand Down
19 changes: 12 additions & 7 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2828,6 +2828,13 @@ impl<'db> TypeInferenceBuilder<'db> {

// Otherwise, if the value is itself a class and defines `__class_getitem__`,
// return its return type.
//
// TODO: lots of classes are only subscriptable at runtime on Python 3.9+,
// *but* we should also allow them to be subscripted in stubs
// (and in annotations if `from __future__ import annotations` is enabled),
// even if the target version is Python 3.8 or lower,
// despite the fact that there will be no corresponding `__class_getitem__`
// method in these `sys.version_info` branches.
if value_ty.is_class(self.db) {
let dunder_class_getitem_method = value_ty.member(self.db, "__class_getitem__");
if !dunder_class_getitem_method.is_unbound() {
Expand All @@ -2848,6 +2855,11 @@ impl<'db> TypeInferenceBuilder<'db> {
});
}

if matches!(value_ty, Type::Class(class) if class.is_known(self.db, KnownClass::Type))
{
return KnownClass::GenericAlias.to_instance(self.db);
}

self.non_subscriptable_diagnostic(
(&**value).into(),
value_ty,
Expand Down Expand Up @@ -6194,13 +6206,6 @@ mod tests {
)?;

let expected_diagnostics = &[
// TODO: these `__class_getitem__` diagnostics are all false positives:
// (`builtins.type` is unique at runtime
// as it can be subscripted even though it has no `__class_getitem__` method)
"Cannot subscript object of type `Literal[type]` with no `__class_getitem__` method",
"Cannot subscript object of type `Literal[type]` with no `__class_getitem__` method",
"Cannot subscript object of type `Literal[type]` with no `__class_getitem__` method",
"Cannot subscript object of type `Literal[type]` with no `__class_getitem__` method",
// Should be `AttributeError`:
"Revealed type is `@Todo`",
// Should be `OSError | RuntimeError`:
Expand Down

0 comments on commit 71b52b8

Please sign in to comment.