diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 5f124e0549..30afd66e7c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap index 9f5d4e2781..a223adb681 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/protocols.md_-_Protocols_-_Calls_to_protocol_classes.snap @@ -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` | diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 69c20c33eb..22b821bdd3 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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]`. // . - 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