[ty] Fix variadic signature display (#27869)

## Summary

Preserve the unpacking `*` in named variadic annotations and avoid
adding a redundant bare `*` before keyword-only parameters when `*args`
is already present. This fixes malformed revealed signatures without
changing signature storage or inference.

## Test plan

- Add a directly declared mixed-signature regression.
- Update TypeVarTuple partial, lambda, and bidirectional reveal
expectations.
- Regenerate existing single-line and multiline signature-display
snapshots.
This commit is contained in:
Dhruv Manilawala authored and GitHub committed 2026-08-19 10:59:10 +05:30
1 parent 49bf50b1c7
commit c876a953dd
6 files changed
+51 -25

No files matched your search

+18 -18
View File
@@ -6018,7 +6018,7 @@ Source with applied edits:
def foo(x: int, *y: bool, z: str | int | list[str]): ...
a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
---------------------------------------------
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:LL:7
@@ -6028,7 +6028,7 @@ Source with applied edits:
info: Source
--> main2.py:LL:16
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^
info[inlay-hint-location]: Inlay Hint Target
@@ -6039,7 +6039,7 @@ Source with applied edits:
info: Source
--> main2.py:LL:25
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
info[inlay-hint-location]: Inlay Hint Target
@@ -6048,10 +6048,10 @@ Source with applied edits:
LL | class str(Sequence[str]):
| ^^^
info: Source
--> main2.py:LL:37
--> main2.py:LL:34
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:LL:7
@@ -6059,10 +6059,10 @@ Source with applied edits:
LL | class int:
| ^^^
info: Source
--> main2.py:LL:43
--> main2.py:LL:40
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:LL:7
@@ -6070,10 +6070,10 @@ Source with applied edits:
LL | class list(MutableSequence[_T]):
| ^^^^
info: Source
--> main2.py:LL:49
--> main2.py:LL:46
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^^
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/builtins.pyi:LL:7
@@ -6081,10 +6081,10 @@ Source with applied edits:
LL | class str(Sequence[str]):
| ^^^
info: Source
--> main2.py:LL:54
--> main2.py:LL:51
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^
info[inlay-hint-location]: Inlay Hint Target
--> stdlib/ty_extensions/_internal.pyi:LL:1
@@ -6092,10 +6092,10 @@ Source with applied edits:
LL | Unknown: _SpecialForm
| ^^^^^^^
info: Source
--> main2.py:LL:63
--> main2.py:LL:60
|
LL | a[: def foo(x: int, *y: bool, *, z: str | int | list[str]) -> Unknown] = foo
| ^^^^^^^
LL | a[: def foo(x: int, *y: bool, z: str | int | list[str]) -> Unknown] = foo
| ^^^^^^^
");
}
@@ -1804,7 +1804,7 @@ reveal_type(f8) # revealed: (int, /) -> None
# An optional keyword-only parameter does not prevent `*args` from accepting the positional
# suffix in a `Callable` annotation.
f9: Callable[[*tuple[int, ...], int], None] = lambda *args, x=1: None
reveal_type(f9) # revealed: (*args, *, x=1) -> None
reveal_type(f9) # revealed: (*args, x=1) -> None
f10: Callable[[str, int, str], tuple[str, int, str]] = lambda x, y, z: reveal_type((x, y, z)) # revealed: tuple[str, int, str]
reveal_type(f10) # revealed: (x: str, y: int, z: str) -> tuple[str, int, str]
@@ -410,7 +410,7 @@ def collect[*Ts](*values: *Ts) -> tuple[*Ts]:
return values
bound = partial(collect)
reveal_type(bound) # revealed: partial[[*Ts](*values: Ts) -> tuple[*Ts]]
reveal_type(bound) # revealed: partial[[*Ts](*values: *Ts) -> tuple[*Ts]]
reveal_type(bound()) # revealed: tuple[()]
reveal_type(bound("x", 1)) # revealed: tuple[Literal["x"], Literal[1]]
```
@@ -432,7 +432,7 @@ def collect[*Ts](prefix: int, *values: *Ts) -> tuple[*Ts]:
return values
bound = partial(collect, 1)
reveal_type(bound) # revealed: partial[[*Ts](*values: Ts) -> tuple[*Ts]]
reveal_type(bound) # revealed: partial[[*Ts](*values: *Ts) -> tuple[*Ts]]
reveal_type(bound()) # revealed: tuple[()]
reveal_type(bound("x", 2)) # revealed: tuple[Literal["x"], Literal[2]]
reveal_type(collect(1)) # revealed: tuple[()]
@@ -455,7 +455,7 @@ def collect[T, *Ts](prefix: T, *values: *Ts) -> tuple[T, *Ts]:
return (prefix, *values)
bound = partial(collect, 1)
reveal_type(bound) # revealed: partial[[*Ts](*values: Ts) -> tuple[Literal[1], *Ts]]
reveal_type(bound) # revealed: partial[[*Ts](*values: *Ts) -> tuple[Literal[1], *Ts]]
reveal_type(bound()) # revealed: tuple[Literal[1]]
reveal_type(bound("x", True)) # revealed: tuple[Literal[1], Literal["x"], Literal[True]]
```
@@ -55,7 +55,7 @@ reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> dict[str, Unknow
Mixing all of them together:
```py
# revealed: (a, b, /, c=True, *args, *, d="default", e=5, **kwargs) -> None
# revealed: (a, b, /, c=True, *args, d="default", e=5, **kwargs) -> None
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
```
@@ -77,6 +77,29 @@ def _(x: object):
reveal_type(c) # revealed: C[Top[(...)]]
```
## Unpacked variadic signatures
Display an unpacked variadic as one parameter, including its fixed prefix and required suffix.
```toml
[environment]
python-version = "3.12"
```
```py
def mixed(
prefix: bytes,
/,
label: str,
*args: *tuple[bool, *tuple[int, ...], bytes, str],
flag: bool = False,
**kwargs: bytes,
) -> None: ...
# revealed: def mixed(prefix: bytes, /, label: str, *args: *tuple[bool, *tuple[int, ...], bytes, str], flag: bool = False, **kwargs: bytes) -> None
reveal_type(mixed)
```
## Type aliases are not expanded unless necessary
```toml
@@ -2610,6 +2610,7 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> {
.fmt_detailed(&mut f.with_detail(TypeDetail::Parameter(param_name)))?;
after_synthetic_unpack |= is_synthetic_unpack;
star_added |= parameter.is_variadic();
first = false;
}
@@ -2744,6 +2745,9 @@ impl<'db> FmtDetailed<'db> for DisplayParameter<'_, 'db> {
if self.param.should_annotation_be_displayed() {
let annotated_type = self.param.annotated_type();
f.write_str(": ")?;
if self.param.is_variadic() && self.param.has_starred_annotation() {
f.write_char('*')?;
}
annotated_type
.display_with(db, self.env, self.settings.clone())
.fmt_detailed(f)?;
@@ -4090,7 +4094,7 @@ mod tests {
],
Some(KnownClass::Bytes.to_instance(db, &env))
),
@"(a, b: int, c=1, d: int = 2, /, e=3, f: int = 4, *args: object, *, g=5, h: int = 6, **kwargs: str) -> bytes"
@"(a, b: int, c=1, d: int = 2, /, e=3, f: int = 4, *args: object, g=5, h: int = 6, **kwargs: str) -> bytes"
);
}
@@ -4256,7 +4260,6 @@ mod tests {
e=3,
f: int = 4,
*args: object,
*,
g=5,
h: int = 6,
**kwargs: str