[ty] Add functions for revealing assignability/subtyping constraints (#20217)

This PR adds two new `ty_extensions` functions,
`reveal_when_assignable_to` and `reveal_when_subtype_of`. These are
closely related to the existing `is_assignable_to` and `is_subtype_of`,
but instead of returning when the property (always) holds, it produces a
diagnostic that describes _when_ the property holds. (This will let us
construct mdtests that print out constraints that are not always true or
always false — though we don't currently have any instances of those.)

I did not replace _every_ occurrence of the `is_property` variants in
the mdtest suite, instead focusing on the generics-related tests where
it will be important to see the full detail of the constraint sets.

As part of this, I also updated the mdtest harness to accept the shorter
`# revealed:` assertion format for more than just `reveal_type`, and
updated the existing uses of `reveal_protocol_interface` to take
advantage of this.
This commit is contained in:
Douglas Creager 2025-09-03 16:44:35 -04:00 committed by GitHub
parent 200349c6e8
commit 77b2cee223
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 309 additions and 199 deletions

View file

@ -129,7 +129,7 @@ specialization. Thus, the typevar is a subtype of itself and of `object`, but no
(including other typevars). (including other typevars).
```py ```py
from ty_extensions import is_assignable_to, is_subtype_of, static_assert from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of
class Super: ... class Super: ...
class Base(Super): ... class Base(Super): ...
@ -137,23 +137,23 @@ class Sub(Base): ...
class Unrelated: ... class Unrelated: ...
def unbounded_unconstrained[T, U](t: T, u: U) -> None: def unbounded_unconstrained[T, U](t: T, u: U) -> None:
static_assert(is_assignable_to(T, T)) reveal_when_assignable_to(T, T) # revealed: always
static_assert(is_assignable_to(T, object)) reveal_when_assignable_to(T, object) # revealed: always
static_assert(not is_assignable_to(T, Super)) reveal_when_assignable_to(T, Super) # revealed: never
static_assert(is_assignable_to(U, U)) reveal_when_assignable_to(U, U) # revealed: always
static_assert(is_assignable_to(U, object)) reveal_when_assignable_to(U, object) # revealed: always
static_assert(not is_assignable_to(U, Super)) reveal_when_assignable_to(U, Super) # revealed: never
static_assert(not is_assignable_to(T, U)) reveal_when_assignable_to(T, U) # revealed: never
static_assert(not is_assignable_to(U, T)) reveal_when_assignable_to(U, T) # revealed: never
static_assert(is_subtype_of(T, T)) reveal_when_subtype_of(T, T) # revealed: always
static_assert(is_subtype_of(T, object)) reveal_when_subtype_of(T, object) # revealed: always
static_assert(not is_subtype_of(T, Super)) reveal_when_subtype_of(T, Super) # revealed: never
static_assert(is_subtype_of(U, U)) reveal_when_subtype_of(U, U) # revealed: always
static_assert(is_subtype_of(U, object)) reveal_when_subtype_of(U, object) # revealed: always
static_assert(not is_subtype_of(U, Super)) reveal_when_subtype_of(U, Super) # revealed: never
static_assert(not is_subtype_of(T, U)) reveal_when_subtype_of(T, U) # revealed: never
static_assert(not is_subtype_of(U, T)) reveal_when_subtype_of(U, T) # revealed: never
``` ```
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
@ -167,40 +167,40 @@ from typing import Any
from typing_extensions import final from typing_extensions import final
def bounded[T: Super](t: T) -> None: def bounded[T: Super](t: T) -> None:
static_assert(is_assignable_to(T, Super)) reveal_when_assignable_to(T, Super) # revealed: always
static_assert(not is_assignable_to(T, Sub)) reveal_when_assignable_to(T, Sub) # revealed: never
static_assert(not is_assignable_to(Super, T)) reveal_when_assignable_to(Super, T) # revealed: never
static_assert(not is_assignable_to(Sub, T)) reveal_when_assignable_to(Sub, T) # revealed: never
static_assert(is_subtype_of(T, Super)) reveal_when_subtype_of(T, Super) # revealed: always
static_assert(not is_subtype_of(T, Sub)) reveal_when_subtype_of(T, Sub) # revealed: never
static_assert(not is_subtype_of(Super, T)) reveal_when_subtype_of(Super, T) # revealed: never
static_assert(not is_subtype_of(Sub, T)) reveal_when_subtype_of(Sub, T) # revealed: never
def bounded_by_gradual[T: Any](t: T) -> None: def bounded_by_gradual[T: Any](t: T) -> None:
static_assert(is_assignable_to(T, Any)) reveal_when_assignable_to(T, Any) # revealed: always
static_assert(is_assignable_to(Any, T)) reveal_when_assignable_to(Any, T) # revealed: always
static_assert(is_assignable_to(T, Super)) reveal_when_assignable_to(T, Super) # revealed: always
static_assert(not is_assignable_to(Super, T)) reveal_when_assignable_to(Super, T) # revealed: never
static_assert(is_assignable_to(T, Sub)) reveal_when_assignable_to(T, Sub) # revealed: always
static_assert(not is_assignable_to(Sub, T)) reveal_when_assignable_to(Sub, T) # revealed: never
static_assert(not is_subtype_of(T, Any)) reveal_when_subtype_of(T, Any) # revealed: never
static_assert(not is_subtype_of(Any, T)) reveal_when_subtype_of(Any, T) # revealed: never
static_assert(not is_subtype_of(T, Super)) reveal_when_subtype_of(T, Super) # revealed: never
static_assert(not is_subtype_of(Super, T)) reveal_when_subtype_of(Super, T) # revealed: never
static_assert(not is_subtype_of(T, Sub)) reveal_when_subtype_of(T, Sub) # revealed: never
static_assert(not is_subtype_of(Sub, T)) reveal_when_subtype_of(Sub, T) # revealed: never
@final @final
class FinalClass: ... class FinalClass: ...
def bounded_final[T: FinalClass](t: T) -> None: def bounded_final[T: FinalClass](t: T) -> None:
static_assert(is_assignable_to(T, FinalClass)) reveal_when_assignable_to(T, FinalClass) # revealed: always
static_assert(not is_assignable_to(FinalClass, T)) reveal_when_assignable_to(FinalClass, T) # revealed: never
static_assert(is_subtype_of(T, FinalClass)) reveal_when_subtype_of(T, FinalClass) # revealed: always
static_assert(not is_subtype_of(FinalClass, T)) reveal_when_subtype_of(FinalClass, T) # revealed: never
``` ```
Two distinct fully static typevars are not subtypes of each other, even if they have the same Two distinct fully static typevars are not subtypes of each other, even if they have the same
@ -210,18 +210,18 @@ typevars to `Never` in addition to that final class.
```py ```py
def two_bounded[T: Super, U: Super](t: T, u: U) -> None: def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U)) reveal_when_assignable_to(T, U) # revealed: never
static_assert(not is_assignable_to(U, T)) reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_subtype_of(T, U)) reveal_when_subtype_of(T, U) # revealed: never
static_assert(not is_subtype_of(U, T)) reveal_when_subtype_of(U, T) # revealed: never
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None: def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U)) reveal_when_assignable_to(T, U) # revealed: never
static_assert(not is_assignable_to(U, T)) reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_subtype_of(T, U)) reveal_when_subtype_of(T, U) # revealed: never
static_assert(not is_subtype_of(U, T)) reveal_when_subtype_of(U, T) # revealed: never
``` ```
A constrained fully static typevar is assignable to the union of its constraints, but not to any of A constrained fully static typevar is assignable to the union of its constraints, but not to any of
@ -232,64 +232,64 @@ intersection of all of its constraints is a subtype of the typevar.
from ty_extensions import Intersection from ty_extensions import Intersection
def constrained[T: (Base, Unrelated)](t: T) -> None: def constrained[T: (Base, Unrelated)](t: T) -> None:
static_assert(not is_assignable_to(T, Super)) reveal_when_assignable_to(T, Super) # revealed: never
static_assert(not is_assignable_to(T, Base)) reveal_when_assignable_to(T, Base) # revealed: never
static_assert(not is_assignable_to(T, Sub)) reveal_when_assignable_to(T, Sub) # revealed: never
static_assert(not is_assignable_to(T, Unrelated)) reveal_when_assignable_to(T, Unrelated) # revealed: never
static_assert(is_assignable_to(T, Super | Unrelated)) reveal_when_assignable_to(T, Super | Unrelated) # revealed: always
static_assert(is_assignable_to(T, Base | Unrelated)) reveal_when_assignable_to(T, Base | Unrelated) # revealed: always
static_assert(not is_assignable_to(T, Sub | Unrelated)) reveal_when_assignable_to(T, Sub | Unrelated) # revealed: never
static_assert(not is_assignable_to(Super, T)) reveal_when_assignable_to(Super, T) # revealed: never
static_assert(not is_assignable_to(Unrelated, T)) reveal_when_assignable_to(Unrelated, T) # revealed: never
static_assert(not is_assignable_to(Super | Unrelated, T)) reveal_when_assignable_to(Super | Unrelated, T) # revealed: never
static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always
static_assert(not is_subtype_of(T, Super)) reveal_when_subtype_of(T, Super) # revealed: never
static_assert(not is_subtype_of(T, Base)) reveal_when_subtype_of(T, Base) # revealed: never
static_assert(not is_subtype_of(T, Sub)) reveal_when_subtype_of(T, Sub) # revealed: never
static_assert(not is_subtype_of(T, Unrelated)) reveal_when_subtype_of(T, Unrelated) # revealed: never
static_assert(is_subtype_of(T, Super | Unrelated)) reveal_when_subtype_of(T, Super | Unrelated) # revealed: always
static_assert(is_subtype_of(T, Base | Unrelated)) reveal_when_subtype_of(T, Base | Unrelated) # revealed: always
static_assert(not is_subtype_of(T, Sub | Unrelated)) reveal_when_subtype_of(T, Sub | Unrelated) # revealed: never
static_assert(not is_subtype_of(Super, T)) reveal_when_subtype_of(Super, T) # revealed: never
static_assert(not is_subtype_of(Unrelated, T)) reveal_when_subtype_of(Unrelated, T) # revealed: never
static_assert(not is_subtype_of(Super | Unrelated, T)) reveal_when_subtype_of(Super | Unrelated, T) # revealed: never
static_assert(is_subtype_of(Intersection[Base, Unrelated], T)) reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: always
def constrained_by_gradual[T: (Base, Any)](t: T) -> None: def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
static_assert(is_assignable_to(T, Super)) reveal_when_assignable_to(T, Super) # revealed: always
static_assert(is_assignable_to(T, Base)) reveal_when_assignable_to(T, Base) # revealed: always
static_assert(not is_assignable_to(T, Sub)) reveal_when_assignable_to(T, Sub) # revealed: never
static_assert(not is_assignable_to(T, Unrelated)) reveal_when_assignable_to(T, Unrelated) # revealed: never
static_assert(is_assignable_to(T, Any)) reveal_when_assignable_to(T, Any) # revealed: always
static_assert(is_assignable_to(T, Super | Any)) reveal_when_assignable_to(T, Super | Any) # revealed: always
static_assert(is_assignable_to(T, Super | Unrelated)) reveal_when_assignable_to(T, Super | Unrelated) # revealed: always
static_assert(not is_assignable_to(Super, T)) reveal_when_assignable_to(Super, T) # revealed: never
static_assert(is_assignable_to(Base, T)) reveal_when_assignable_to(Base, T) # revealed: always
static_assert(not is_assignable_to(Unrelated, T)) reveal_when_assignable_to(Unrelated, T) # revealed: never
static_assert(is_assignable_to(Any, T)) reveal_when_assignable_to(Any, T) # revealed: always
static_assert(not is_assignable_to(Super | Any, T)) reveal_when_assignable_to(Super | Any, T) # revealed: never
static_assert(is_assignable_to(Base | Any, T)) reveal_when_assignable_to(Base | Any, T) # revealed: always
static_assert(not is_assignable_to(Super | Unrelated, T)) reveal_when_assignable_to(Super | Unrelated, T) # revealed: never
static_assert(is_assignable_to(Intersection[Base, Unrelated], T)) reveal_when_assignable_to(Intersection[Base, Unrelated], T) # revealed: always
static_assert(is_assignable_to(Intersection[Base, Any], T)) reveal_when_assignable_to(Intersection[Base, Any], T) # revealed: always
static_assert(not is_subtype_of(T, Super)) reveal_when_subtype_of(T, Super) # revealed: never
static_assert(not is_subtype_of(T, Base)) reveal_when_subtype_of(T, Base) # revealed: never
static_assert(not is_subtype_of(T, Sub)) reveal_when_subtype_of(T, Sub) # revealed: never
static_assert(not is_subtype_of(T, Unrelated)) reveal_when_subtype_of(T, Unrelated) # revealed: never
static_assert(not is_subtype_of(T, Any)) reveal_when_subtype_of(T, Any) # revealed: never
static_assert(not is_subtype_of(T, Super | Any)) reveal_when_subtype_of(T, Super | Any) # revealed: never
static_assert(not is_subtype_of(T, Super | Unrelated)) reveal_when_subtype_of(T, Super | Unrelated) # revealed: never
static_assert(not is_subtype_of(Super, T)) reveal_when_subtype_of(Super, T) # revealed: never
static_assert(not is_subtype_of(Base, T)) reveal_when_subtype_of(Base, T) # revealed: never
static_assert(not is_subtype_of(Unrelated, T)) reveal_when_subtype_of(Unrelated, T) # revealed: never
static_assert(not is_subtype_of(Any, T)) reveal_when_subtype_of(Any, T) # revealed: never
static_assert(not is_subtype_of(Super | Any, T)) reveal_when_subtype_of(Super | Any, T) # revealed: never
static_assert(not is_subtype_of(Base | Any, T)) reveal_when_subtype_of(Base | Any, T) # revealed: never
static_assert(not is_subtype_of(Super | Unrelated, T)) reveal_when_subtype_of(Super | Unrelated, T) # revealed: never
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T)) reveal_when_subtype_of(Intersection[Base, Unrelated], T) # revealed: never
static_assert(not is_subtype_of(Intersection[Base, Any], T)) reveal_when_subtype_of(Intersection[Base, Any], T) # revealed: never
``` ```
Two distinct fully static typevars are not subtypes of each other, even if they have the same Two distinct fully static typevars are not subtypes of each other, even if they have the same
@ -299,58 +299,58 @@ the same type.
```py ```py
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None: def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U)) reveal_when_assignable_to(T, U) # revealed: never
static_assert(not is_assignable_to(U, T)) reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_subtype_of(T, U)) reveal_when_subtype_of(T, U) # revealed: never
static_assert(not is_subtype_of(U, T)) reveal_when_subtype_of(U, T) # revealed: never
@final @final
class AnotherFinalClass: ... class AnotherFinalClass: ...
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None: def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
static_assert(not is_assignable_to(T, U)) reveal_when_assignable_to(T, U) # revealed: never
static_assert(not is_assignable_to(U, T)) reveal_when_assignable_to(U, T) # revealed: never
static_assert(not is_subtype_of(T, U)) reveal_when_subtype_of(T, U) # revealed: never
static_assert(not is_subtype_of(U, T)) reveal_when_subtype_of(U, T) # revealed: never
``` ```
A bound or constrained typevar is a subtype of itself in a union: A bound or constrained typevar is a subtype of itself in a union:
```py ```py
def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: def union[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_assignable_to(T, T | None)) reveal_when_assignable_to(T, T | None) # revealed: always
static_assert(is_assignable_to(U, U | None)) reveal_when_assignable_to(U, U | None) # revealed: always
static_assert(is_subtype_of(T, T | None)) reveal_when_subtype_of(T, T | None) # revealed: always
static_assert(is_subtype_of(U, U | None)) reveal_when_subtype_of(U, U | None) # revealed: always
``` ```
A bound or constrained typevar in a union with a dynamic type is assignable to the typevar: A bound or constrained typevar in a union with a dynamic type is assignable to the typevar:
```py ```py
def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: def union_with_dynamic[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_assignable_to(T | Any, T)) reveal_when_assignable_to(T | Any, T) # revealed: always
static_assert(is_assignable_to(U | Any, U)) reveal_when_assignable_to(U | Any, U) # revealed: always
static_assert(not is_subtype_of(T | Any, T)) reveal_when_subtype_of(T | Any, T) # revealed: never
static_assert(not is_subtype_of(U | Any, U)) reveal_when_subtype_of(U | Any, U) # revealed: never
``` ```
And an intersection of a typevar with another type is always a subtype of the TypeVar: And an intersection of a typevar with another type is always a subtype of the TypeVar:
```py ```py
from ty_extensions import Intersection, Not, is_disjoint_from from ty_extensions import Intersection, Not, is_disjoint_from, static_assert
class A: ... class A: ...
def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None: def inter[T: Base, U: (Base, Unrelated)](t: T, u: U) -> None:
static_assert(is_assignable_to(Intersection[T, Unrelated], T)) reveal_when_assignable_to(Intersection[T, Unrelated], T) # revealed: always
static_assert(is_subtype_of(Intersection[T, Unrelated], T)) reveal_when_subtype_of(Intersection[T, Unrelated], T) # revealed: always
static_assert(is_assignable_to(Intersection[U, A], U)) reveal_when_assignable_to(Intersection[U, A], U) # revealed: always
static_assert(is_subtype_of(Intersection[U, A], U)) reveal_when_subtype_of(Intersection[U, A], U) # revealed: always
static_assert(is_disjoint_from(Not[T], T)) static_assert(is_disjoint_from(Not[T], T))
static_assert(is_disjoint_from(T, Not[T])) static_assert(is_disjoint_from(T, Not[T]))
@ -647,14 +647,14 @@ The intersection of a typevar with any other type is assignable to (and if fully
of) itself. of) itself.
```py ```py
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not from ty_extensions import reveal_when_assignable_to, reveal_when_subtype_of, Not
def intersection_is_assignable[T](t: T) -> None: def intersection_is_assignable[T](t: T) -> None:
static_assert(is_assignable_to(Intersection[T, None], T)) reveal_when_assignable_to(Intersection[T, None], T) # revealed: always
static_assert(is_assignable_to(Intersection[T, Not[None]], T)) reveal_when_assignable_to(Intersection[T, Not[None]], T) # revealed: always
static_assert(is_subtype_of(Intersection[T, None], T)) reveal_when_subtype_of(Intersection[T, None], T) # revealed: always
static_assert(is_subtype_of(Intersection[T, Not[None]], T)) reveal_when_subtype_of(Intersection[T, Not[None]], T) # revealed: always
``` ```
## Narrowing ## Narrowing

View file

@ -413,13 +413,13 @@ To see the kinds and types of the protocol members, you can use the debugging ai
from ty_extensions import reveal_protocol_interface from ty_extensions import reveal_protocol_interface
from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator from typing import SupportsIndex, SupportsAbs, ClassVar, Iterator
# error: [revealed-type] "Revealed protocol interface: `{"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}`" # revealed: {"method_member": MethodMember(`(self) -> bytes`), "x": AttributeMember(`int`), "y": PropertyMember { getter: `def y(self) -> str` }, "z": PropertyMember { getter: `def z(self) -> int`, setter: `def z(self, z: int) -> None` }}
reveal_protocol_interface(Foo) reveal_protocol_interface(Foo)
# error: [revealed-type] "Revealed protocol interface: `{"__index__": MethodMember(`(self) -> int`)}`" # revealed: {"__index__": MethodMember(`(self) -> int`)}
reveal_protocol_interface(SupportsIndex) reveal_protocol_interface(SupportsIndex)
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> Unknown`)}`" # revealed: {"__abs__": MethodMember(`(self) -> Unknown`)}
reveal_protocol_interface(SupportsAbs) reveal_protocol_interface(SupportsAbs)
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}`" # revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}
reveal_protocol_interface(Iterator) reveal_protocol_interface(Iterator)
# error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`" # error: [invalid-argument-type] "Invalid argument to `reveal_protocol_interface`: Only protocol classes can be passed to `reveal_protocol_interface`"
@ -439,9 +439,9 @@ do not implement any special handling for generic aliases passed to the function
reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(SupportsAbs[int])) # revealed: frozenset[str]
reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str] reveal_type(get_protocol_members(Iterator[int])) # revealed: frozenset[str]
# error: [revealed-type] "Revealed protocol interface: `{"__abs__": MethodMember(`(self) -> int`)}`" # revealed: {"__abs__": MethodMember(`(self) -> int`)}
reveal_protocol_interface(SupportsAbs[int]) reveal_protocol_interface(SupportsAbs[int])
# error: [revealed-type] "Revealed protocol interface: `{"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}`" # revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}
reveal_protocol_interface(Iterator[int]) reveal_protocol_interface(Iterator[int])
class BaseProto(Protocol): class BaseProto(Protocol):
@ -450,16 +450,16 @@ class BaseProto(Protocol):
class SubProto(BaseProto, Protocol): class SubProto(BaseProto, Protocol):
def member(self) -> bool: ... def member(self) -> bool: ...
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> int`)}`" # revealed: {"member": MethodMember(`(self) -> int`)}
reveal_protocol_interface(BaseProto) reveal_protocol_interface(BaseProto)
# error: [revealed-type] "Revealed protocol interface: `{"member": MethodMember(`(self) -> bool`)}`" # revealed: {"member": MethodMember(`(self) -> bool`)}
reveal_protocol_interface(SubProto) reveal_protocol_interface(SubProto)
class ProtoWithClassVar(Protocol): class ProtoWithClassVar(Protocol):
x: ClassVar[int] x: ClassVar[int]
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`" # revealed: {"x": AttributeMember(`int`; ClassVar)}
reveal_protocol_interface(ProtoWithClassVar) reveal_protocol_interface(ProtoWithClassVar)
class ProtocolWithDefault(Protocol): class ProtocolWithDefault(Protocol):
@ -468,7 +468,7 @@ class ProtocolWithDefault(Protocol):
# We used to incorrectly report this as having an `x: Literal[0]` member; # We used to incorrectly report this as having an `x: Literal[0]` member;
# declared types should take priority over inferred types for protocol interfaces! # declared types should take priority over inferred types for protocol interfaces!
# #
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`)}`" # revealed: {"x": AttributeMember(`int`)}
reveal_protocol_interface(ProtocolWithDefault) reveal_protocol_interface(ProtocolWithDefault)
``` ```
@ -2474,7 +2474,7 @@ class Foo(Protocol):
from stub import Foo from stub import Foo
from ty_extensions import reveal_protocol_interface from ty_extensions import reveal_protocol_interface
# error: [revealed-type] "Revealed protocol interface: `{"x": AttributeMember(`int`; ClassVar)}`" # revealed: {"x": AttributeMember(`int`; ClassVar)}
reveal_protocol_interface(Foo) reveal_protocol_interface(Foo)
``` ```

View file

@ -4168,6 +4168,24 @@ impl<'db> Type<'db> {
) )
.into(), .into(),
Some(
KnownFunction::RevealWhenAssignableTo | KnownFunction::RevealWhenSubtypeOf,
) => Binding::single(
self,
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("a")))
.type_form()
.with_annotated_type(Type::any()),
Parameter::positional_only(Some(Name::new_static("b")))
.type_form()
.with_annotated_type(Type::any()),
]),
Some(KnownClass::NoneType.to_instance(db)),
),
)
.into(),
Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => { Some(KnownFunction::IsSingleton | KnownFunction::IsSingleValued) => {
Binding::single( Binding::single(
self, self,

View file

@ -133,9 +133,6 @@ pub(crate) trait Constraints<'db>: Clone + Sized {
} }
// This is here so that we can easily print constraint sets when debugging. // This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
fn display(&self, db: &'db dyn Db) -> impl Display; fn display(&self, db: &'db dyn Db) -> impl Display;
} }
@ -345,34 +342,6 @@ impl<'db> ConstraintSet<'db> {
} }
} }
} }
// This is here so that we can easily print constraint sets when debugging.
// TODO: Add a ty_extensions function to reveal constraint sets so that this is no longer dead
// code, and so that we verify the contents of our rendering.
#[expect(dead_code)]
pub(crate) fn display(&self, db: &'db dyn Db) -> impl Display {
struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
}
} }
impl<'db> Constraints<'db> for ConstraintSet<'db> { impl<'db> Constraints<'db> for ConstraintSet<'db> {
@ -411,7 +380,27 @@ impl<'db> Constraints<'db> for ConstraintSet<'db> {
} }
fn display(&self, db: &'db dyn Db) -> impl Display { fn display(&self, db: &'db dyn Db) -> impl Display {
self.display(db) struct DisplayConstraintSet<'a, 'db> {
set: &'a ConstraintSet<'db>,
db: &'db dyn Db,
}
impl Display for DisplayConstraintSet<'_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.set.clauses.is_empty() {
return f.write_str("0");
}
for (i, clause) in self.set.clauses.iter().enumerate() {
if i > 0 {
f.write_str(" ")?;
}
clause.display(self.db).fmt(f)?;
}
Ok(())
}
}
DisplayConstraintSet { set: self, db }
} }
} }

View file

@ -65,7 +65,7 @@ use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::ScopeId; use crate::semantic_index::scope::ScopeId;
use crate::semantic_index::semantic_index; use crate::semantic_index::semantic_index;
use crate::types::call::{Binding, CallArguments}; use crate::types::call::{Binding, CallArguments};
use crate::types::constraints::Constraints; use crate::types::constraints::{ConstraintSet, Constraints};
use crate::types::context::InferContext; use crate::types::context::InferContext;
use crate::types::diagnostic::{ use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, INVALID_ARGUMENT_TYPE, REDUNDANT_CAST, STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE,
@ -1188,6 +1188,10 @@ pub enum KnownFunction {
HasMember, HasMember,
/// `ty_extensions.reveal_protocol_interface` /// `ty_extensions.reveal_protocol_interface`
RevealProtocolInterface, RevealProtocolInterface,
/// `ty_extensions.reveal_when_assignable_to`
RevealWhenAssignableTo,
/// `ty_extensions.reveal_when_subtype_of`
RevealWhenSubtypeOf,
} }
impl KnownFunction { impl KnownFunction {
@ -1253,6 +1257,8 @@ impl KnownFunction {
| Self::StaticAssert | Self::StaticAssert
| Self::HasMember | Self::HasMember
| Self::RevealProtocolInterface | Self::RevealProtocolInterface
| Self::RevealWhenAssignableTo
| Self::RevealWhenSubtypeOf
| Self::AllMembers => module.is_ty_extensions(), | Self::AllMembers => module.is_ty_extensions(),
Self::ImportModule => module.is_importlib(), Self::ImportModule => module.is_importlib(),
} }
@ -1548,6 +1554,54 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module)); overload.set_return_type(Type::module_literal(db, file, module));
} }
KnownFunction::RevealWhenAssignableTo => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_assignable_to::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Assignability holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
KnownFunction::RevealWhenSubtypeOf => {
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
return;
};
let constraints = ty_a.when_subtype_of::<ConstraintSet>(db, *ty_b);
let Some(builder) =
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
else {
return;
};
let mut diag = builder.into_diagnostic("Subtyping holds");
let span = context.span(call_expression);
if constraints.is_always_satisfied(db) {
diag.annotate(Annotation::primary(span).message("always"));
} else if constraints.is_never_satisfied(db) {
diag.annotate(Annotation::primary(span).message("never"));
} else {
diag.annotate(
Annotation::primary(span)
.message(format_args!("when {}", constraints.display(db))),
);
}
}
_ => {} _ => {}
} }
} }
@ -1608,6 +1662,8 @@ pub(crate) mod tests {
| KnownFunction::IsEquivalentTo | KnownFunction::IsEquivalentTo
| KnownFunction::HasMember | KnownFunction::HasMember
| KnownFunction::RevealProtocolInterface | KnownFunction::RevealProtocolInterface
| KnownFunction::RevealWhenAssignableTo
| KnownFunction::RevealWhenSubtypeOf
| KnownFunction::AllMembers => KnownModule::TyExtensions, | KnownFunction::AllMembers => KnownModule::TyExtensions,
KnownFunction::ImportModule => KnownModule::ImportLib, KnownFunction::ImportModule => KnownModule::ImportLib,

View file

@ -1,15 +1,19 @@
//! Match [`Diagnostic`]s against assertions and produce test failure //! Match [`Diagnostic`]s against assertions and produce test failure
//! messages for any mismatches. //! messages for any mismatches.
use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::db::Db; use std::borrow::Cow;
use crate::diagnostic::SortedDiagnostics; use std::cmp::Ordering;
use std::ops::Range;
use colored::Colorize; use colored::Colorize;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId}; use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::source::{SourceText, line_index, source_text}; use ruff_db::source::{SourceText, line_index, source_text};
use ruff_source_file::{LineIndex, OneIndexed}; use ruff_source_file::{LineIndex, OneIndexed};
use std::cmp::Ordering;
use std::ops::Range; use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(super) struct FailuresByLine { pub(super) struct FailuresByLine {
@ -194,14 +198,19 @@ impl UnmatchedWithColumn for &Diagnostic {
/// Discard `@Todo`-type metadata from expected types, which is not available /// Discard `@Todo`-type metadata from expected types, which is not available
/// when running in release mode. /// when running in release mode.
fn discard_todo_metadata(ty: &str) -> Cow<'_, str> {
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> { {
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> = static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap()); std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
TODO_METADATA_REGEX.replace_all(ty, "@Todo") TODO_METADATA_REGEX.replace_all(ty, "@Todo")
} }
#[cfg(debug_assertions)]
Cow::Borrowed(ty)
}
struct Matcher { struct Matcher {
line_index: LineIndex, line_index: LineIndex,
source: SourceText, source: SourceText,
@ -297,21 +306,53 @@ impl Matcher {
} }
} }
ParsedAssertion::Revealed(expected_type) => { ParsedAssertion::Revealed(expected_type) => {
#[cfg(not(debug_assertions))] let expected_type = discard_todo_metadata(expected_type);
let expected_type = discard_todo_metadata(&expected_type); let expected_reveal_type_message = format!("`{expected_type}`");
let diagnostic_matches_reveal = |diagnostic: &Diagnostic| {
if diagnostic.id() != DiagnosticId::RevealedType {
return false;
}
let primary_message = diagnostic.primary_message();
let Some(primary_annotation) =
(diagnostic.primary_annotation()).and_then(|a| a.get_message())
else {
return false;
};
// reveal_type
if primary_message == "Revealed type"
&& primary_annotation == expected_reveal_type_message
{
return true;
}
// reveal_protocol_interface
if primary_message == "Revealed protocol interface"
&& primary_annotation == expected_reveal_type_message
{
return true;
}
// reveal_when_assignable_to
if primary_message == "Assignability holds"
&& primary_annotation == expected_type
{
return true;
}
// reveal_when_subtype_of
if primary_message == "Subtyping holds" && primary_annotation == expected_type {
return true;
}
false
};
let mut matched_revealed_type = None; let mut matched_revealed_type = None;
let mut matched_undefined_reveal = None; let mut matched_undefined_reveal = None;
let expected_reveal_type_message = format!("`{expected_type}`");
for (index, diagnostic) in unmatched.iter().enumerate() { for (index, diagnostic) in unmatched.iter().enumerate() {
if matched_revealed_type.is_none() if matched_revealed_type.is_none() && diagnostic_matches_reveal(diagnostic) {
&& diagnostic.id() == DiagnosticId::RevealedType
&& diagnostic
.primary_annotation()
.and_then(|a| a.get_message())
.unwrap_or_default()
== expected_reveal_type_message
{
matched_revealed_type = Some(index); matched_revealed_type = Some(index);
} else if matched_undefined_reveal.is_none() } else if matched_undefined_reveal.is_none()
&& diagnostic.id().is_lint_named("undefined-reveal") && diagnostic.id().is_lint_named("undefined-reveal")

View file

@ -45,15 +45,21 @@ type JustComplex = TypeOf[1.0j]
# Ideally, these would be annotated using `TypeForm`, but that has not been # Ideally, these would be annotated using `TypeForm`, but that has not been
# standardized yet (https://peps.python.org/pep-0747). # standardized yet (https://peps.python.org/pep-0747).
def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ... def is_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_subtype_of(type_derived: Any, type_base: Any) -> bool: ... def is_subtype_of(type_a: Any, type_b: Any) -> bool: ...
def is_assignable_to(type_target: Any, type_source: Any) -> bool: ... def is_assignable_to(type_a: Any, type_b: Any) -> bool: ...
def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ... def is_disjoint_from(type_a: Any, type_b: Any) -> bool: ...
def is_singleton(type: Any) -> bool: ... def is_singleton(ty: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ... def is_single_valued(ty: Any) -> bool: ...
# These are the same as above, but instead of returning _whether_ the property
# holds, it shows a diagnostic that describes under what constraints the
# property holds.
def reveal_when_assignable_to(type_a: Any, type_b: Any) -> None: ...
def reveal_when_subtype_of(type_a: Any, type_b: Any) -> None: ...
# Returns the generic context of a type as a tuple of typevars, or `None` if the # Returns the generic context of a type as a tuple of typevars, or `None` if the
# type is not generic. # type is not generic.
def generic_context(type: Any) -> Any: ... def generic_context(ty: Any) -> Any: ...
# Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if # Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if
# either the module does not have `__all__` or it has invalid elements. # either the module does not have `__all__` or it has invalid elements.