[ty] fix display of top ParamSpec specialization (#22227)

## Summary

I only noticed this in the ecosystem report of
https://github.com/astral-sh/ruff/pull/22213 after merging it. The
change to displaying `Top[]` wrapper around the entire signature instead
of just the parameters had the side effect of not showing it at all when
displaying a top ParamSpec specialization. This PR fixes that.

Marking internal since this is a fixup of a not-released PR.

## Test Plan

Added mdtest that fails without this PR.
This commit is contained in:
Carl Meyer
2025-12-27 11:14:22 -08:00
committed by GitHub
parent 6d5fb09e92
commit 55c8707be6
2 changed files with 38 additions and 1 deletions

View File

@@ -1,5 +1,9 @@
# Display of callable types
## Parenthesizing callables
### Simple
We parenthesize callable types when they appear inside more complex types, to disambiguate:
```py
@@ -9,11 +13,13 @@ def f(x: Callable[[], str] | Callable[[int], str]):
reveal_type(x) # revealed: (() -> str) | ((int, /) -> str)
```
### Overloaded
We don't parenthesize display of an overloaded callable, since it is already wrapped in
`Overload[...]`:
```py
from typing import overload
from typing import overload, Callable
from ty_extensions import CallableTypeOf
@overload
@@ -28,11 +34,36 @@ def _(flag: bool, c: CallableTypeOf[f]):
reveal_type(x) # revealed: Overload[(x: int) -> bool, (x: str) -> str] | Literal[True]
```
### Top
And we don't parenthesize the top callable, since it is wrapped in `Top[...]`:
```py
from typing import Callable
from ty_extensions import Top
def f(x: Top[Callable[..., str]] | Callable[[int], int]):
reveal_type(x) # revealed: Top[(...) -> str] | ((int, /) -> int)
```
## Top ParamSpec
```toml
[environment]
python-version = "3.12"
```
We wrap the signature of a top ParamSpec with `Top[...]`:
```py
from typing import Callable
class C[**P]:
def __init__(self, f: Callable[P, object]) -> None:
self.f = f
def _(x: object):
if callable(x):
c = C(x)
reveal_type(c) # revealed: C[Top[(...)]]
```

View File

@@ -1600,10 +1600,16 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> {
match self.signatures.overloads.as_slice() {
[signature] => {
if matches!(self.kind, CallableTypeKind::ParamSpecValue) {
if signature.parameters().is_top() {
f.write_str("Top[")?;
}
signature
.parameters()
.display_with(self.db, self.settings.clone())
.fmt_detailed(f)?;
if signature.parameters().is_top() {
f.write_str("]")?;
}
} else {
signature
.display_with(self.db, self.settings.clone())