[ty] Remove hack in protocol satisfiability check (#20568)

## Summary

This removes a hack in the protocol satisfiability check that was
previously needed to work around missing assignability-modeling of
inferable type variables. Assignability of type variables is not
implemented fully, but some recent changes allow us to remove that hack
with limited impact on the ecosystem (and the test suite). The change in
the typing conformance test is favorable.

## Test Plan

* Adapted Markdown tests
* Made sure that this change works in combination with
https://github.com/astral-sh/ruff/pull/20517
This commit is contained in:
David Peter 2025-09-25 13:35:47 +02:00 committed by GitHub
parent 9f3cffc65c
commit efbb80f747
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 29 deletions

View file

@ -1928,21 +1928,23 @@ 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: ...
def method(self: Self, input: T) -> None: ...
S = TypeVar("S")
class LegacyClassScoped(Protocol[S]):
def method(self, input: S) -> None: ...
def method(self: Self, input: S) -> None: ...
static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped))
static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int]))
# TODO: these should pass
static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) # error: [static-assert-error]
static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int])) # error: [static-assert-error]
class NominalGeneric[T]:
def method(self, input: T) -> None: ...
def _[T](x: T) -> T:
static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T]))
# TODO: should pass
static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T])) # error: [static-assert-error]
static_assert(is_subtype_of(NominalGeneric[T], NewStyleClassScoped[T]))
static_assert(is_subtype_of(NominalGeneric[T], LegacyClassScoped[T]))
return x
@ -2016,17 +2018,27 @@ class NominalReturningSelfNotGeneric:
# 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(is_assignable_to(NominalNewStyle, NewStyleFunctionScoped))
static_assert(is_assignable_to(NominalNewStyle, LegacyFunctionScoped))
# TODO: should pass
static_assert(is_subtype_of(NominalNewStyle, NewStyleFunctionScoped)) # error: [static-assert-error]
# TODO: should pass
static_assert(is_subtype_of(NominalNewStyle, LegacyFunctionScoped)) # error: [static-assert-error]
static_assert(not is_assignable_to(NominalNewStyle, UsesSelf))
static_assert(is_subtype_of(NominalLegacy, NewStyleFunctionScoped))
static_assert(is_subtype_of(NominalLegacy, LegacyFunctionScoped))
static_assert(is_assignable_to(NominalLegacy, NewStyleFunctionScoped))
static_assert(is_assignable_to(NominalLegacy, LegacyFunctionScoped))
# TODO: should pass
static_assert(is_subtype_of(NominalLegacy, NewStyleFunctionScoped)) # error: [static-assert-error]
# TODO: should pass
static_assert(is_subtype_of(NominalLegacy, LegacyFunctionScoped)) # error: [static-assert-error]
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))
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
# TODO: should pass
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
# TODO: these should pass
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@ -2035,8 +2047,23 @@ 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]
# These test cases are taken from the typing conformance suite:
class ShapeProtocolImplicitSelf(Protocol):
def set_scale(self, scale: float) -> Self: ...
class ShapeProtocolExplicitSelf(Protocol):
def set_scale(self: Self, scale: float) -> Self: ...
class BadReturnType:
def set_scale(self, scale: float) -> int:
return 42
static_assert(not is_assignable_to(BadReturnType, ShapeProtocolImplicitSelf))
static_assert(not is_assignable_to(BadReturnType, ShapeProtocolExplicitSelf))
```
## Subtyping of protocols with `@classmethod` or `@staticmethod` members

View file

@ -23,7 +23,6 @@ use crate::{
diagnostic::report_undeclared_protocol_member,
signatures::{Parameter, Parameters},
todo_type,
visitor::any_over_type,
},
};
@ -571,24 +570,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
attribute_type
};
let proto_member_as_bound_method = method.bind_self(db);
if any_over_type(
db,
proto_member_as_bound_method,
&|t| matches!(t, Type::TypeVar(_)),
true,
) {
// TODO: proper validation for generic methods on protocols
return ConstraintSet::from(true);
}
attribute_type.has_relation_to_impl(
db,
proto_member_as_bound_method,
relation,
visitor,
)
attribute_type.has_relation_to_impl(db, method.bind_self(db), relation, visitor)
}
// TODO: consider the types of the attribute on `other` for property members
ProtocolMemberKind::Property(_) => ConstraintSet::from(matches!(