[ty] Add more tests for subtyping/assignability between two protocol types (#20573)
Some checks are pending
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-09-26 12:07:57 +01:00 committed by GitHub
parent 0bae7e613d
commit 2af8c53110
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 225 additions and 8 deletions

View file

@ -228,6 +228,48 @@ def _(flag: bool):
reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B | Result3 | Result4
```
## Union type as iterable where `Iterator[]` is used as the return type of `__iter__`
This test differs from the above tests in that `Iterator` (an abstract type) is used as the return
annotation of the `__iter__` methods, rather than a concrete type being used as the return
annotation.
```py
from typing import Iterator, Literal
class IntIterator:
def __iter__(self) -> Iterator[int]:
return iter(range(42))
class StrIterator:
def __iter__(self) -> Iterator[str]:
return iter("foo")
def f(x: IntIterator | StrIterator):
for a in x:
# TODO: this should be `int | str` (https://github.com/astral-sh/ty/issues/1089)
reveal_type(a) # revealed: int
```
Most real-world iterable types use `Iterator` as the return annotation of their `__iter__` methods:
```py
def g(
a: tuple[int, ...] | tuple[str, ...],
b: list[str] | list[int],
c: Literal["foo", b"bar"],
):
for x in a:
# TODO: should be `int | str` (https://github.com/astral-sh/ty/issues/1089)
reveal_type(x) # revealed: int
for y in b:
# TODO: should be `str | int` (https://github.com/astral-sh/ty/issues/1089)
reveal_type(y) # revealed: str
for z in c:
# TODO: should be `LiteralString | int` (https://github.com/astral-sh/ty/issues/1089)
reveal_type(z) # revealed: LiteralString
```
## Union type as iterable where one union element has no `__iter__` method
<!-- snapshot-diagnostics -->

View file

@ -607,11 +607,22 @@ class HasXY(Protocol):
class Foo:
x: int
class IntSub(int): ...
class HasXIntSub(Protocol):
x: IntSub
static_assert(is_subtype_of(Foo, HasX))
static_assert(is_assignable_to(Foo, HasX))
static_assert(not is_subtype_of(Foo, HasXY))
static_assert(not is_assignable_to(Foo, HasXY))
# TODO: these should pass
static_assert(not is_subtype_of(HasXIntSub, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasXIntSub, HasX)) # error: [static-assert-error]
static_assert(not is_subtype_of(HasX, HasXIntSub)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasX, HasXIntSub)) # error: [static-assert-error]
class FooSub(Foo): ...
static_assert(is_subtype_of(FooSub, HasX))
@ -1546,6 +1557,22 @@ static_assert(is_subtype_of(XImplicitFinal, HasXProperty))
static_assert(is_assignable_to(XImplicitFinal, HasXProperty))
```
But only if it has the correct type:
```py
class XAttrBad:
x: str
class HasStrXProperty(Protocol):
@property
def x(self) -> str: ...
# TODO: these should pass
static_assert(not is_assignable_to(XAttrBad, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasStrXProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasXProperty, HasStrXProperty)) # error: [static-assert-error]
```
A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
example satisfies the `HasXProperty` interface even though the type of the `x` attribute on `XSub`
is a subtype of `int` rather than being exactly `int`.
@ -1558,6 +1585,13 @@ class XSub:
static_assert(is_subtype_of(XSub, HasXProperty))
static_assert(is_assignable_to(XSub, HasXProperty))
class XSubProto(Protocol):
@property
def x(self) -> XSub: ...
static_assert(is_subtype_of(XSubProto, HasXProperty))
static_assert(is_assignable_to(XSubProto, HasXProperty))
```
A read/write property on a protocol, where the getter returns the same type that the setter takes,
@ -1582,8 +1616,8 @@ class XReadProperty:
return 42
# TODO: these should pass
static_assert(not is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_subtype_of(XReadProperty, HasMutableXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XReadProperty, HasMutableXProperty)) # error: [static-assert-error]
class XReadWriteProperty:
@property
@ -1593,18 +1627,19 @@ class XReadWriteProperty:
@x.setter
def x(self, val: int) -> None: ...
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
static_assert(is_subtype_of(XReadWriteProperty, HasMutableXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasMutableXProperty))
class XSub:
x: MyInt
static_assert(not is_subtype_of(XSub, XReadWriteProperty))
static_assert(not is_assignable_to(XSub, XReadWriteProperty))
# TODO: these should pass
static_assert(not is_subtype_of(XSub, HasMutableXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XSub, HasMutableXProperty)) # error: [static-assert-error]
```
A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
attribute `x`. Both are subtypes of a protocol with a read-only prooperty `x`:
attribute `x`. Both are subtypes of a protocol with a read-only property `x`:
```py
from ty_extensions import is_equivalent_to
@ -1618,8 +1653,22 @@ static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error:
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
static_assert(is_subtype_of(HasMutableXAttr, HasMutableXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasMutableXProperty))
static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))
static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))
static_assert(is_subtype_of(HasMutableXProperty, HasMutableXAttr))
static_assert(is_assignable_to(HasMutableXProperty, HasMutableXAttr))
class HasMutableXAttrWrongType(Protocol):
x: str
# TODO: these should pass
static_assert(not is_assignable_to(HasMutableXAttrWrongType, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasMutableXAttrWrongType, HasMutableXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasMutableXProperty, HasMutableXAttrWrongType)) # error: [static-assert-error]
```
A read/write property on a protocol, where the setter accepts a subtype of the type returned by the
@ -2212,6 +2261,129 @@ static_assert(is_equivalent_to(A | B | P1, P2 | B | A))
static_assert(is_equivalent_to(A | B | P3, P4 | B | A)) # error: [static-assert-error]
```
## Subtyping between two protocol types with method members
A protocol `PSub` with a method member can be considered a subtype of a protocol `PSuper` with a
method member if the signature of the member on `PSub` is a subtype of the signature of the member
on `PSuper`:
```py
from typing import Protocol
from ty_extensions import static_assert, is_subtype_of, is_assignable_to
class Super: ...
class Sub(Super): ...
class Unrelated: ...
class MethodPSuper(Protocol):
def f(self) -> Super: ...
class MethodPSub(Protocol):
def f(self) -> Sub: ...
class MethodPUnrelated(Protocol):
def f(self) -> Unrelated: ...
static_assert(is_subtype_of(MethodPSub, MethodPSuper))
# TODO: these should pass
static_assert(not is_assignable_to(MethodPUnrelated, MethodPSuper)) # error: [static-assert-error]
static_assert(not is_assignable_to(MethodPSuper, MethodPUnrelated)) # error: [static-assert-error]
static_assert(not is_assignable_to(MethodPSuper, MethodPSub)) # error: [static-assert-error]
```
## Subtyping between protocols with method members and protocols with non-method members
A protocol with a method member can be considered a subtype of a protocol with a read-only
`@property` member that returns a `Callable` type:
```py
from typing import Protocol, Callable
from ty_extensions import static_assert, is_subtype_of, is_assignable_to
class PropertyInt(Protocol):
@property
def f(self) -> Callable[[], int]: ...
class PropertyBool(Protocol):
@property
def f(self) -> Callable[[], bool]: ...
class PropertyNotReturningCallable(Protocol):
@property
def f(self) -> int: ...
class PropertyWithIncorrectSignature(Protocol):
@property
def f(self) -> Callable[[object], int]: ...
class Method(Protocol):
def f(self) -> bool: ...
static_assert(is_subtype_of(Method, PropertyInt))
static_assert(is_subtype_of(Method, PropertyBool))
# TODO: these should pass
static_assert(not is_assignable_to(Method, PropertyNotReturningCallable)) # error: [static-assert-error]
static_assert(not is_assignable_to(Method, PropertyWithIncorrectSignature)) # error: [static-assert-error]
```
However, a protocol with a method member can never be considered a subtype of a protocol with a
writable property member of the same name, as method members are covariant and immutable:
```py
class ReadWriteProperty(Protocol):
@property
def f(self) -> Callable[[], bool]: ...
@f.setter
def f(self, val: Callable[[], bool]): ...
# TODO: should pass
static_assert(not is_assignable_to(Method, ReadWriteProperty)) # error: [static-assert-error]
```
And for the same reason, they are never assignable to attribute members (which are also mutable):
```py
class Attribute(Protocol):
f: Callable[[], bool]
# TODO: should pass
static_assert(not is_assignable_to(Method, Attribute)) # error: [static-assert-error]
```
Protocols with attribute members, meanwhile, cannot be assigned to protocols with method members,
since a method member is guaranteed to exist on the meta-type as well as the instance type, whereas
this is not true for attribute members. The same principle also applies for protocols with property
members
```py
# TODO: this should pass
static_assert(not is_assignable_to(PropertyBool, Method)) # error: [static-assert-error]
static_assert(not is_assignable_to(Attribute, Method)) # error: [static-assert-error]
```
But an exception to this rule is if an attribute member is marked as `ClassVar`, as this guarantees
that the member will be available on the meta-type as well as the instance type for inhabitants of
the protocol:
```py
from typing import ClassVar
class ClassVarAttribute(Protocol):
f: ClassVar[Callable[[], bool]]
static_assert(is_subtype_of(ClassVarAttribute, Method))
static_assert(is_assignable_to(ClassVarAttribute, Method))
class ClassVarAttributeBad(Protocol):
f: ClassVar[Callable[[], str]]
# TODO: these should pass:
static_assert(not is_subtype_of(ClassVarAttributeBad, Method)) # error: [static-assert-error]
static_assert(not is_assignable_to(ClassVarAttributeBad, Method)) # error: [static-assert-error]
```
## Narrowing of protocols
<!-- snapshot-diagnostics -->
@ -2549,7 +2721,10 @@ class RecursiveOptionalParent(Protocol):
static_assert(is_assignable_to(RecursiveOptionalParent, RecursiveOptionalParent))
static_assert(is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent))
# Due to invariance of mutable attribute members, neither is assignable to the other
#
# TODO: should pass
static_assert(not is_assignable_to(RecursiveNonFullyStatic, RecursiveOptionalParent)) # error: [static-assert-error]
static_assert(not is_assignable_to(RecursiveOptionalParent, RecursiveNonFullyStatic))
class Other(Protocol):