diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 1267bb27fd..3da3126b16 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -1801,6 +1801,136 @@ static_assert(is_assignable_to(str, SupportsLessThan)) static_assert(is_assignable_to(int, Invertable)) ``` +## Subtyping of protocols with generic method members + +Protocol method members can be generic. They can have generic contexts scoped to the class: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing_extensions import TypeVar, Self, Protocol +from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of + +class NewStyleClassScoped[T](Protocol): + def method(self, input: T) -> None: ... + +S = TypeVar("S") + +class LegacyClassScoped(Protocol[S]): + def method(self, input: S) -> None: ... + +static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) +static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int])) + +class NominalGeneric[T]: + def method(self, input: T) -> None: ... + +def _[T](x: T) -> T: + static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T])) + static_assert(is_subtype_of(NominalGeneric[T], NewStyleClassScoped[T])) + static_assert(is_subtype_of(NominalGeneric[T], LegacyClassScoped[T])) + return x + +class NominalConcrete: + def method(self, input: int) -> None: ... + +static_assert(is_assignable_to(NominalConcrete, NewStyleClassScoped)) +static_assert(is_assignable_to(NominalConcrete, LegacyClassScoped)) +static_assert(is_assignable_to(NominalGeneric[int], NewStyleClassScoped)) +static_assert(is_assignable_to(NominalGeneric[int], LegacyClassScoped)) +static_assert(is_assignable_to(NominalGeneric, NewStyleClassScoped[int])) +static_assert(is_assignable_to(NominalGeneric, LegacyClassScoped[int])) + +# `NewStyleClassScoped` is implicitly `NewStyleClassScoped[Unknown]`, +# and there exist fully static materializations of `NewStyleClassScoped[Unknown]` +# where `Nominal` would not be a subtype of the given materialization, +# hence there is no subtyping relation: +# +# TODO: these should pass +static_assert(not is_subtype_of(NominalConcrete, NewStyleClassScoped)) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalConcrete, LegacyClassScoped)) # error: [static-assert-error] + +# Similarly, `NominalGeneric` is implicitly `NominalGeneric[Unknown`] +# +# TODO: these should pass +static_assert(not is_subtype_of(NominalGeneric, NewStyleClassScoped[int])) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalGeneric, LegacyClassScoped[int])) # error: [static-assert-error] + +static_assert(is_subtype_of(NominalConcrete, NewStyleClassScoped[int])) +static_assert(is_subtype_of(NominalConcrete, LegacyClassScoped[int])) +static_assert(is_subtype_of(NominalGeneric[int], NewStyleClassScoped[int])) +static_assert(is_subtype_of(NominalGeneric[int], LegacyClassScoped[int])) + +# TODO: these should pass +static_assert(not is_assignable_to(NominalConcrete, NewStyleClassScoped[str])) # error: [static-assert-error] +static_assert(not is_assignable_to(NominalConcrete, LegacyClassScoped[str])) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalGeneric[int], NewStyleClassScoped[str])) # error: [static-assert-error] +static_assert(not is_subtype_of(NominalGeneric[int], LegacyClassScoped[str])) # error: [static-assert-error] +``` + +And they can also have generic contexts scoped to the method: + +```py +class NewStyleFunctionScoped(Protocol): + def f[T](self, input: T) -> T: ... + +S = TypeVar("S") + +class LegacyFunctionScoped(Protocol): + def f(self, input: S) -> S: ... + +class UsesSelf(Protocol): + def g(self: Self) -> Self: ... + +class NominalNewStyle: + def f[T](self, input: T) -> T: + return input + +class NominalLegacy: + def f(self, input: S) -> S: + return input + +class NominalWithSelf: + def g(self: Self) -> Self: + return self + +class NominalNotGeneric: + def f(self, input: int) -> int: + return input + +class NominalReturningSelfNotGeneric: + def g(self) -> "NominalReturningSelfNotGeneric": + return self + +# TODO: should pass +static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error] + +static_assert(is_subtype_of(NominalNewStyle, NewStyleFunctionScoped)) +static_assert(is_subtype_of(NominalNewStyle, LegacyFunctionScoped)) +static_assert(not is_assignable_to(NominalNewStyle, UsesSelf)) + +static_assert(is_subtype_of(NominalLegacy, NewStyleFunctionScoped)) +static_assert(is_subtype_of(NominalLegacy, LegacyFunctionScoped)) +static_assert(not is_assignable_to(NominalLegacy, UsesSelf)) + +static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped)) +static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped)) +static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) + +# TODO: these should pass +static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error] +static_assert(not is_assignable_to(NominalNotGeneric, LegacyFunctionScoped)) # error: [static-assert-error] +static_assert(not is_assignable_to(NominalNotGeneric, UsesSelf)) + +static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, NewStyleFunctionScoped)) +static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctionScoped)) +# TODO: should pass +static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error] +``` + ## Equivalence of protocols with method or property members Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the @@ -1919,7 +2049,7 @@ def f(arg1: type, arg2: type): reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers] ``` -## Truthiness of protocol instance +## Truthiness of protocol instances An instance of a protocol type generally has ambiguous truthiness: @@ -2520,7 +2650,6 @@ Add tests for: - `super()` on nominal subtypes (explicit and implicit) of protocol classes - [Recursive protocols][recursive_protocols_spec] - Generic protocols -- Non-generic protocols with function-scoped generic methods - Protocols with instance attributes annotated with `Callable` (can a nominal type with a method satisfy that protocol, and if so in what cases?) - Protocols decorated with `@final`