mirror of https://github.com/astral-sh/ruff
[red-knot] Ban direct instantiation of generic protocols as well as non-generic ones (#17741)
This commit is contained in:
parent
18bac94226
commit
b6de01b9a5
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue