Skip to content

Commit 03fe560

Browse files
[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>
1 parent 68343e7 commit 03fe560

File tree

13 files changed

+317
-128
lines changed

13 files changed

+317
-128
lines changed

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2003,6 +2003,7 @@ python-version = "3.12"
20032003
```
20042004

20052005
```py
2006+
from typing import final
20062007
from typing_extensions import TypeVar, Self, Protocol
20072008
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
20082009

@@ -2094,6 +2095,13 @@ class NominalReturningSelfNotGeneric:
20942095
def g(self) -> "NominalReturningSelfNotGeneric":
20952096
return self
20962097

2098+
@final
2099+
class Other: ...
2100+
2101+
class NominalReturningOtherClass:
2102+
def g(self) -> Other:
2103+
raise NotImplementedError
2104+
20972105
# TODO: should pass
20982106
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
20992107

@@ -2112,8 +2120,7 @@ static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
21122120
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
21132121
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
21142122
static_assert(is_assignable_to(NominalWithSelf, UsesSelf))
2115-
# TODO: should pass
2116-
static_assert(is_subtype_of(NominalWithSelf, UsesSelf)) # error: [static-assert-error]
2123+
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
21172124

21182125
# TODO: these should pass
21192126
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
@@ -2126,6 +2133,8 @@ static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctio
21262133
# TODO: should pass
21272134
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
21282135

2136+
static_assert(not is_assignable_to(NominalReturningOtherClass, UsesSelf))
2137+
21292138
# These test cases are taken from the typing conformance suite:
21302139
class ShapeProtocolImplicitSelf(Protocol):
21312140
def set_scale(self, scale: float) -> Self: ...

crates/ty_python_semantic/src/place.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,9 +1376,7 @@ mod implicit_globals {
13761376
use crate::place::{Definedness, PlaceAndQualifiers, TypeOrigin};
13771377
use crate::semantic_index::symbol::Symbol;
13781378
use crate::semantic_index::{place_table, use_def_map};
1379-
use crate::types::{
1380-
CallableType, KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type,
1381-
};
1379+
use crate::types::{KnownClass, MemberLookupPolicy, Parameter, Parameters, Signature, Type};
13821380
use ruff_python_ast::PythonVersion;
13831381

13841382
use super::{Place, place_from_declarations};
@@ -1461,7 +1459,7 @@ mod implicit_globals {
14611459
)),
14621460
);
14631461
Place::Defined(
1464-
CallableType::function_like(db, signature),
1462+
Type::function_like_callable(db, signature),
14651463
TypeOrigin::Inferred,
14661464
Definedness::PossiblyUndefined,
14671465
)

crates/ty_python_semantic/src/semantic_index/reachability_constraints.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ use crate::semantic_index::predicate::{
208208
Predicates, ScopedPredicateId,
209209
};
210210
use crate::types::{
211-
IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
211+
CallableTypes, IntersectionBuilder, Truthiness, Type, TypeContext, UnionBuilder, UnionType,
212212
infer_expression_type, static_expression_truthiness,
213213
};
214214

@@ -871,12 +871,14 @@ impl ReachabilityConstraints {
871871
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
872872
}
873873

874-
let overloads_iterator =
875-
if let Some(Type::Callable(callable)) = ty.try_upcast_to_callable(db) {
876-
callable.signatures(db).overloads.iter()
877-
} else {
878-
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
879-
};
874+
let overloads_iterator = if let Some(callable) = ty
875+
.try_upcast_to_callable(db)
876+
.and_then(CallableTypes::exactly_one)
877+
{
878+
callable.signatures(db).overloads.iter()
879+
} else {
880+
return Truthiness::AlwaysFalse.negate_if(!predicate.is_positive);
881+
};
880882

881883
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
882884
.fold((true, true), |(none, all), overload| {

0 commit comments

Comments
 (0)