[red-knot] Ban direct instantiation of generic protocols as well as non-generic ones (#17741)

This commit is contained in:
Alex Waygood 2025-04-30 17:01:28 +01:00 committed by GitHub
parent 18bac94226
commit b6de01b9a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 106 additions and 27 deletions

View File

@ -304,6 +304,11 @@ reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool
Neither `Protocol`, nor any protocol class, can be directly instantiated:
```toml
[environment]
python-version = "3.12"
```
```py
from typing_extensions import Protocol, reveal_type
@ -315,6 +320,12 @@ class MyProtocol(Protocol):
# error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
reveal_type(MyProtocol()) # revealed: MyProtocol
class GenericProtocol[T](Protocol):
x: T
# error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
```
But a non-protocol class can be instantiated, even if it has `Protocol` in its MRO:
@ -323,6 +334,10 @@ But a non-protocol class can be instantiated, even if it has `Protocol` in its M
class SubclassOfMyProtocol(MyProtocol): ...
reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
```
And as a corollary, `type[MyProtocol]` can also be called:

View File

@ -22,11 +22,21 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/protocols.md
8 |
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
11 |
12 | class GenericProtocol[T](Protocol):
13 | x: T
14 |
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
17 | class SubclassOfMyProtocol(MyProtocol): ...
18 |
19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
20 |
21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
22 |
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
24 | def f(x: type[MyProtocol]):
25 | reveal_type(x()) # revealed: MyProtocol
```
# Diagnostics
@ -64,7 +74,8 @@ error: lint:call-non-callable: Cannot instantiate class `MyProtocol`
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^ This call will raise `TypeError` at runtime
11 | class SubclassOfMyProtocol(MyProtocol): ...
11 |
12 | class GenericProtocol[T](Protocol):
|
info: Protocol classes cannot be instantiated
--> src/mdtest_snippet.py:6:7
@ -85,32 +96,80 @@ info: revealed-type: Revealed type
9 | # error: [call-non-callable] "Cannot instantiate class `MyProtocol`"
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^ `MyProtocol`
11 | class SubclassOfMyProtocol(MyProtocol): ...
11 |
12 | class GenericProtocol[T](Protocol):
|
```
```
error: lint:call-non-callable: Cannot instantiate class `GenericProtocol`
--> src/mdtest_snippet.py:16:13
|
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
| ^^^^^^^^^^^^^^^^^^^^^^ This call will raise `TypeError` at runtime
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
info: Protocol classes cannot be instantiated
--> src/mdtest_snippet.py:12:7
|
10 | reveal_type(MyProtocol()) # revealed: MyProtocol
11 |
12 | class GenericProtocol[T](Protocol):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol` declared as a protocol here
13 | x: T
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:13:1
--> src/mdtest_snippet.py:16:1
|
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
15 | # error: [call-non-callable] "Cannot instantiate class `GenericProtocol`"
16 | reveal_type(GenericProtocol[int]()) # revealed: GenericProtocol[int]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `GenericProtocol[int]`
17 | class SubclassOfMyProtocol(MyProtocol): ...
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:19:1
|
17 | class SubclassOfMyProtocol(MyProtocol): ...
18 |
19 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfMyProtocol`
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
20 |
21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:15:5
--> src/mdtest_snippet.py:23:1
|
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
21 | class SubclassOfGenericProtocol[T](GenericProtocol[T]): ...
22 |
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `SubclassOfGenericProtocol[int]`
24 | def f(x: type[MyProtocol]):
25 | reveal_type(x()) # revealed: MyProtocol
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:25:5
|
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
24 | def f(x: type[MyProtocol]):
25 | reveal_type(x()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|

View File

@ -4555,18 +4555,23 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
// It might look odd here that we emit an error for class-literals but not `type[]` types.
// But it's deliberate! The typing spec explicitly mandates that `type[]` types can be called
// even though class-literals cannot. This is because even though a protocol class `SomeProtocol`
// is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of that protocol
// -- and indeed, according to the spec, type checkers must disallow abstract subclasses of the
// protocol to be passed to parameters that accept `type[SomeProtocol]`.
// It might look odd here that we emit an error for class-literals and generic aliases but not
// `type[]` types. But it's deliberate! The typing spec explicitly mandates that `type[]` types
// can be called even though class-literals cannot. This is because even though a protocol class
// `SomeProtocol` is always an abstract class, `type[SomeProtocol]` can be a concrete subclass of
// that protocol -- and indeed, according to the spec, type checkers must disallow abstract
// subclasses of the protocol to be passed to parameters that accept `type[SomeProtocol]`.
// <https://typing.python.org/en/latest/spec/protocol.html#type-and-class-objects-vs-protocols>.
if let Some(protocol_class) = callable_type
.into_class_literal()
.and_then(|class| class.into_protocol_class(self.db()))
let possible_protocol_class = match callable_type {
Type::ClassLiteral(class) => Some(class),
Type::GenericAlias(generic) => Some(generic.origin(self.db())),
_ => None,
};
if let Some(protocol) =
possible_protocol_class.and_then(|class| class.into_protocol_class(self.db()))
{
report_attempted_protocol_instantiation(&self.context, call_expression, protocol_class);
report_attempted_protocol_instantiation(&self.context, call_expression, protocol);
}
// For class literals we model the entire class instantiation logic, so it is handled