[ty] Add tests for protocols with generic method members (#20316)

This commit is contained in:
Alex Waygood 2025-09-09 17:44:00 +01:00 committed by GitHub
parent 9cdac2d6fb
commit bf66178959
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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`