Skip to content
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
15 changes: 15 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:16:1
|
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:13:1
--> src/mdtest_snippet.py:19:1
|
11 | class SubclassOfMyProtocol(MyProtocol): ...
12 |
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
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:23:1
|
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:15:5
--> src/mdtest_snippet.py:25:5
|
13 | reveal_type(SubclassOfMyProtocol()) # revealed: SubclassOfMyProtocol
14 | def f(x: type[MyProtocol]):
15 | reveal_type(x()) # revealed: MyProtocol
23 | reveal_type(SubclassOfGenericProtocol[int]()) # revealed: SubclassOfGenericProtocol[int]
24 | def f(x: type[MyProtocol]):
25 | reveal_type(x()) # revealed: MyProtocol
| ^^^^^^^^^^^^^^^^ `MyProtocol`
|

Expand Down
25 changes: 15 additions & 10 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading