mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] Add tests for protocols with generic method members (#20316)
This commit is contained in:
parent
9cdac2d6fb
commit
bf66178959
1 changed files with 131 additions and 2 deletions
|
@ -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`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue