Skip to content
Merged
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
133 changes: 131 additions & 2 deletions crates/ty_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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`
Expand Down