[ty] Substitute for typing.Self when checking protocol members (#21569)

This patch updates our protocol assignability checks to substitute for
any occurrences of `typing.Self` in method signatures, replacing it with
the class being checked for assignability against the protocol.

This requires a new helper method on signatures, `apply_self`, which
substitutes occurrences of `typing.Self` _without_ binding the `self`
parameter.

We also update the `try_upcast_to_callable` method. Before, it would
return a `Type`, since certain types upcast to a _union_ of callables,
not to a single callable. However, even in that case, we know that every
element of the union is a callable. We now return a vector of
`CallableType`. (Actually a smallvec to handle the most common case of a
single callable; and wrapped in a new type so that we can provide helper
methods.) If there is more than one element in the result, it represents
a union of callables. This lets callers get at the `CallableType`
instances in a more type-safe way. (This makes it easier for our
protocol checking code to call the new `apply_self` helper.) We also
provide an `into_type` method so that callers that really do want a
`Type` can get the original result easily.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager
2025-11-24 14:05:09 -05:00
committed by GitHub
parent 68343e7edf
commit 03fe560164
13 changed files with 317 additions and 128 deletions

View File

@@ -2003,6 +2003,7 @@ python-version = "3.12"
```
```py
from typing import final
from typing_extensions import TypeVar, Self, Protocol
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
@@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
def g(self) -> "NominalReturningSelfNotGeneric":
return self
@final
class Other: ...
class NominalReturningOtherClass:
def g(self) -> Other:
raise NotImplementedError
# TODO: should pass
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2112,8 +2120,7 @@ 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_assignable_to(NominalWithSelf, UsesSelf))
# TODO: should pass
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
# TODO: these should pass
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
# TODO: should pass
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
# These test cases are taken from the typing conformance suite:
class ShapeProtocolImplicitSelf(Protocol):
def set_scale(self, scale: float) -> Self: ...