[ty] Implement the legacy PEP-484 convention for indicating positional-only parameters (#20248)
Some checks are pending
CI / mkdocs (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 / formatter instabilities and black similarity (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
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 / pre-commit (push) Waiting to run
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Alex Waygood 2025-09-05 17:56:06 +01:00 committed by GitHub
parent eb6154f792
commit 5d52902e18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 376 additions and 150 deletions

View file

@ -68,6 +68,78 @@ def _(flag: bool):
reveal_type(foo()) # revealed: int
```
## PEP-484 convention for positional-only parameters
PEP 570, introduced in Python 3.8, added dedicated Python syntax for denoting positional-only
parameters (the `/` in a function signature). However, functions implemented in C were able to have
positional-only parameters prior to Python 3.8 (there was just no syntax for expressing this at the
Python level).
Stub files describing functions implemented in C nonetheless needed a way of expressing that certain
parameters were positional-only. In the absence of dedicated Python syntax, PEP 484 described a
convention that type checkers were expected to understand:
> Some functions are designed to take their arguments only positionally, and expect their callers
> never to use the arguments name to provide that argument by keyword. All arguments with names
> beginning with `__` are assumed to be positional-only, except if their names also end with `__`.
While this convention is now redundant (following the implementation of PEP 570), many projects
still continue to use the old convention, so it is supported by ty as well.
```py
def f(__x: int): ...
f(1)
# error: [missing-argument]
# error: [unknown-argument]
f(__x=1)
```
But not if they follow a non-positional-only parameter:
```py
def g(x: int, __y: str): ...
g(x=1, __y="foo")
```
And also not if they both start and end with `__`:
```py
def h(__x__: str): ...
h(__x__="foo")
```
And if *any* parameters use the new PEP-570 convention, the old convention does not apply:
```py
def i(x: str, /, __y: int): ...
i("foo", __y=42) # fine
```
And `self`/`cls` are implicitly positional-only:
```py
class C:
def method(self, __x: int): ...
@classmethod
def class_method(cls, __x: str): ...
# (the name of the first parameter is irrelevant;
# a staticmethod works the same as a free function in the global scope)
@staticmethod
def static_method(self, __x: int): ...
# error: [missing-argument]
# error: [unknown-argument]
C().method(__x=1)
# error: [missing-argument]
# error: [unknown-argument]
C.class_method(__x="1")
C.static_method("x", __x=42) # fine
```
## Splatted arguments
### Unknown argument length
@ -545,7 +617,7 @@ def _(args: str) -> None:
This is a regression that was highlighted by the ecosystem check, which shows that we might need to
rethink how we perform argument expansion during overload resolution. In particular, we might need
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry
to retry both `match_parameters` *and* `check_types` for each expansion. Currently we only retry
`check_types`.
The issue is that argument expansion might produce a splatted value with a different arity than what

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 typing import SupportsIndex, SupportsAbs, ClassVar, Iterator
# 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` }}
# 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)
# revealed: {"__index__": MethodMember(`(self) -> int`)}
# revealed: {"__index__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(SupportsIndex)
# revealed: {"__abs__": MethodMember(`(self) -> Unknown`)}
# revealed: {"__abs__": MethodMember(`(self, /) -> Unknown`)}
reveal_protocol_interface(SupportsAbs)
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[Unknown]`), "__next__": MethodMember(`(self) -> Unknown`)}
# revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[Unknown]`), "__next__": MethodMember(`(self, /) -> Unknown`)}
reveal_protocol_interface(Iterator)
# 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(Iterator[int])) # revealed: frozenset[str]
# revealed: {"__abs__": MethodMember(`(self) -> int`)}
# revealed: {"__abs__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(SupportsAbs[int])
# revealed: {"__iter__": MethodMember(`(self) -> Iterator[int]`), "__next__": MethodMember(`(self) -> int`)}
# revealed: {"__iter__": MethodMember(`(self, /) -> Iterator[int]`), "__next__": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(Iterator[int])
class BaseProto(Protocol):
@ -450,10 +450,10 @@ class BaseProto(Protocol):
class SubProto(BaseProto, Protocol):
def member(self) -> bool: ...
# revealed: {"member": MethodMember(`(self) -> int`)}
# revealed: {"member": MethodMember(`(self, /) -> int`)}
reveal_protocol_interface(BaseProto)
# revealed: {"member": MethodMember(`(self) -> bool`)}
# revealed: {"member": MethodMember(`(self, /) -> bool`)}
reveal_protocol_interface(SubProto)
class ProtoWithClassVar(Protocol):
@ -1767,7 +1767,7 @@ class Foo(Protocol):
def method(self) -> str: ...
def f(x: Foo):
reveal_type(type(x).method) # revealed: def method(self) -> str
reveal_type(type(x).method) # revealed: def method(self, /) -> str
class Bar:
def __init__(self):
@ -1776,6 +1776,31 @@ class Bar:
f(Bar()) # error: [invalid-argument-type]
```
Some protocols use the old convention (specified in PEP-484) for denoting positional-only
parameters. This is supported by ty:
```py
class HasPosOnlyDunders:
def __invert__(self, /) -> "HasPosOnlyDunders":
return self
def __lt__(self, other, /) -> bool:
return True
class SupportsLessThan(Protocol):
def __lt__(self, __other) -> bool: ...
class Invertable(Protocol):
# `self` and `cls` are always implicitly positional-only for methods defined in `Protocol`
# classes, even if no parameters in the method use the PEP-484 convention.
def __invert__(self) -> object: ...
static_assert(is_assignable_to(HasPosOnlyDunders, SupportsLessThan))
static_assert(is_assignable_to(HasPosOnlyDunders, Invertable))
static_assert(is_assignable_to(str, SupportsLessThan))
static_assert(is_assignable_to(int, Invertable))
```
## Equivalence of protocols with method or property members
Two protocols `P1` and `P2`, both with a method member `x`, are considered equivalent if the