Skip to content

Commit bf66178

Browse files
authored
[ty] Add tests for protocols with generic method members (#20316)
1 parent 9cdac2d commit bf66178

File tree

1 file changed

+131
-2
lines changed

1 file changed

+131
-2
lines changed

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,6 +1801,136 @@ static_assert(is_assignable_to(str, SupportsLessThan))
18011801
static_assert(is_assignable_to(int, Invertable))
18021802
```
18031803

1804+
## Subtyping of protocols with generic method members
1805+
1806+
Protocol method members can be generic. They can have generic contexts scoped to the class:
1807+
1808+
```toml
1809+
[environment]
1810+
python-version = "3.12"
1811+
```
1812+
1813+
```py
1814+
from typing_extensions import TypeVar, Self, Protocol
1815+
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
1816+
1817+
class NewStyleClassScoped[T](Protocol):
1818+
def method(self, input: T) -> None: ...
1819+
1820+
S = TypeVar("S")
1821+
1822+
class LegacyClassScoped(Protocol[S]):
1823+
def method(self, input: S) -> None: ...
1824+
1825+
static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped))
1826+
static_assert(is_equivalent_to(NewStyleClassScoped[int], LegacyClassScoped[int]))
1827+
1828+
class NominalGeneric[T]:
1829+
def method(self, input: T) -> None: ...
1830+
1831+
def _[T](x: T) -> T:
1832+
static_assert(is_equivalent_to(NewStyleClassScoped[T], LegacyClassScoped[T]))
1833+
static_assert(is_subtype_of(NominalGeneric[T], NewStyleClassScoped[T]))
1834+
static_assert(is_subtype_of(NominalGeneric[T], LegacyClassScoped[T]))
1835+
return x
1836+
1837+
class NominalConcrete:
1838+
def method(self, input: int) -> None: ...
1839+
1840+
static_assert(is_assignable_to(NominalConcrete, NewStyleClassScoped))
1841+
static_assert(is_assignable_to(NominalConcrete, LegacyClassScoped))
1842+
static_assert(is_assignable_to(NominalGeneric[int], NewStyleClassScoped))
1843+
static_assert(is_assignable_to(NominalGeneric[int], LegacyClassScoped))
1844+
static_assert(is_assignable_to(NominalGeneric, NewStyleClassScoped[int]))
1845+
static_assert(is_assignable_to(NominalGeneric, LegacyClassScoped[int]))
1846+
1847+
# `NewStyleClassScoped` is implicitly `NewStyleClassScoped[Unknown]`,
1848+
# and there exist fully static materializations of `NewStyleClassScoped[Unknown]`
1849+
# where `Nominal` would not be a subtype of the given materialization,
1850+
# hence there is no subtyping relation:
1851+
#
1852+
# TODO: these should pass
1853+
static_assert(not is_subtype_of(NominalConcrete, NewStyleClassScoped)) # error: [static-assert-error]
1854+
static_assert(not is_subtype_of(NominalConcrete, LegacyClassScoped)) # error: [static-assert-error]
1855+
1856+
# Similarly, `NominalGeneric` is implicitly `NominalGeneric[Unknown`]
1857+
#
1858+
# TODO: these should pass
1859+
static_assert(not is_subtype_of(NominalGeneric, NewStyleClassScoped[int])) # error: [static-assert-error]
1860+
static_assert(not is_subtype_of(NominalGeneric, LegacyClassScoped[int])) # error: [static-assert-error]
1861+
1862+
static_assert(is_subtype_of(NominalConcrete, NewStyleClassScoped[int]))
1863+
static_assert(is_subtype_of(NominalConcrete, LegacyClassScoped[int]))
1864+
static_assert(is_subtype_of(NominalGeneric[int], NewStyleClassScoped[int]))
1865+
static_assert(is_subtype_of(NominalGeneric[int], LegacyClassScoped[int]))
1866+
1867+
# TODO: these should pass
1868+
static_assert(not is_assignable_to(NominalConcrete, NewStyleClassScoped[str])) # error: [static-assert-error]
1869+
static_assert(not is_assignable_to(NominalConcrete, LegacyClassScoped[str])) # error: [static-assert-error]
1870+
static_assert(not is_subtype_of(NominalGeneric[int], NewStyleClassScoped[str])) # error: [static-assert-error]
1871+
static_assert(not is_subtype_of(NominalGeneric[int], LegacyClassScoped[str])) # error: [static-assert-error]
1872+
```
1873+
1874+
And they can also have generic contexts scoped to the method:
1875+
1876+
```py
1877+
class NewStyleFunctionScoped(Protocol):
1878+
def f[T](self, input: T) -> T: ...
1879+
1880+
S = TypeVar("S")
1881+
1882+
class LegacyFunctionScoped(Protocol):
1883+
def f(self, input: S) -> S: ...
1884+
1885+
class UsesSelf(Protocol):
1886+
def g(self: Self) -> Self: ...
1887+
1888+
class NominalNewStyle:
1889+
def f[T](self, input: T) -> T:
1890+
return input
1891+
1892+
class NominalLegacy:
1893+
def f(self, input: S) -> S:
1894+
return input
1895+
1896+
class NominalWithSelf:
1897+
def g(self: Self) -> Self:
1898+
return self
1899+
1900+
class NominalNotGeneric:
1901+
def f(self, input: int) -> int:
1902+
return input
1903+
1904+
class NominalReturningSelfNotGeneric:
1905+
def g(self) -> "NominalReturningSelfNotGeneric":
1906+
return self
1907+
1908+
# TODO: should pass
1909+
static_assert(is_equivalent_to(LegacyFunctionScoped, NewStyleFunctionScoped)) # error: [static-assert-error]
1910+
1911+
static_assert(is_subtype_of(NominalNewStyle, NewStyleFunctionScoped))
1912+
static_assert(is_subtype_of(NominalNewStyle, LegacyFunctionScoped))
1913+
static_assert(not is_assignable_to(NominalNewStyle, UsesSelf))
1914+
1915+
static_assert(is_subtype_of(NominalLegacy, NewStyleFunctionScoped))
1916+
static_assert(is_subtype_of(NominalLegacy, LegacyFunctionScoped))
1917+
static_assert(not is_assignable_to(NominalLegacy, UsesSelf))
1918+
1919+
static_assert(not is_assignable_to(NominalWithSelf, NewStyleFunctionScoped))
1920+
static_assert(not is_assignable_to(NominalWithSelf, LegacyFunctionScoped))
1921+
static_assert(is_subtype_of(NominalWithSelf, UsesSelf))
1922+
1923+
# TODO: these should pass
1924+
static_assert(not is_assignable_to(NominalNotGeneric, NewStyleFunctionScoped)) # error: [static-assert-error]
1925+
static_assert(not is_assignable_to(NominalNotGeneric, LegacyFunctionScoped)) # error: [static-assert-error]
1926+
static_assert(not is_assignable_to(NominalNotGeneric, UsesSelf))
1927+
1928+
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, NewStyleFunctionScoped))
1929+
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, LegacyFunctionScoped))
1930+
# TODO: should pass
1931+
static_assert(not is_assignable_to(NominalReturningSelfNotGeneric, UsesSelf)) # error: [static-assert-error]
1932+
```
1933+
18041934
## Equivalence of protocols with method or property members
18051935

18061936
Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the
@@ -1919,7 +2049,7 @@ def f(arg1: type, arg2: type):
19192049
reveal_type(arg2) # revealed: type & ~type[OnlyMethodMembers]
19202050
```
19212051

1922-
## Truthiness of protocol instance
2052+
## Truthiness of protocol instances
19232053

19242054
An instance of a protocol type generally has ambiguous truthiness:
19252055

@@ -2520,7 +2650,6 @@ Add tests for:
25202650
- `super()` on nominal subtypes (explicit and implicit) of protocol classes
25212651
- [Recursive protocols][recursive_protocols_spec]
25222652
- Generic protocols
2523-
- Non-generic protocols with function-scoped generic methods
25242653
- Protocols with instance attributes annotated with `Callable` (can a nominal type with a method
25252654
satisfy that protocol, and if so in what cases?)
25262655
- Protocols decorated with `@final`

0 commit comments

Comments
 (0)