Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ Using `Concatenate` as the first argument to `Callable`:
from typing_extensions import Callable, Concatenate

def _(c: Callable[Concatenate[int, str, ...], int]):
# TODO: Should reveal the correct signature
reveal_type(c) # revealed: (...) -> int
reveal_type(c) # revealed: (int, str, /, *args: Any, **kwargs: Any) -> int
```

And, as one of the parameter types:
Expand Down
4 changes: 3 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/final.md
Original file line number Diff line number Diff line change
Expand Up @@ -1302,7 +1302,9 @@ class Base(ABC):
@abstractproperty # error: [deprecated]
def value(self) -> int:
return 0

# TODO: False positive: `Concatenate` in `classmethod.__init__` signature causes spurious
# invalid-argument-type when the type variables are not fully resolved.
# error: [invalid-argument-type]
@abstractclassmethod # error: [deprecated]
def make(cls) -> "Base":
raise NotImplementedError
Expand Down
124 changes: 124 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/generics/concatenate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# `typing.Concatenate`

`Concatenate` is used with `Callable` and `ParamSpec` to describe higher-order functions that add,
remove, or transform parameters of other callables.

## Basic `Callable[Concatenate[..., ...], ...]` types

### With ellipsis (gradual form)

```py
from typing_extensions import Callable, Concatenate

def _(c: Callable[Concatenate[int, ...], str]):
reveal_type(c) # revealed: (int, /, *args: Any, **kwargs: Any) -> str

def _(c: Callable[Concatenate[int, str, ...], bool]):
reveal_type(c) # revealed: (int, str, /, *args: Any, **kwargs: Any) -> bool
```

### With `ParamSpec`

```toml
[environment]
python-version = "3.12"
```

```py
from typing_extensions import Callable, Concatenate, ParamSpec

P = ParamSpec("P")

def _(c: Callable[Concatenate[int, P], str]):
reveal_type(c) # revealed: (int, /, *args: P@_.args, **kwargs: P@_.kwargs) -> str
```

## Decorator that strips a prefix parameter

A common use case is decorators that strip the first parameter from a callable.

```toml
[environment]
python-version = "3.12"
```

```py
from typing import Callable, reveal_type
from typing_extensions import Concatenate, ParamSpec

P = ParamSpec("P")

def with_request[**P, R](f: Callable[Concatenate[int, P], R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return f(0, *args, **kwargs)
return wrapper

@with_request
def handler(request: int, name: str) -> bool:
return True

# The decorator strips the first `int` parameter
reveal_type(handler) # revealed: (name: str) -> bool

# Calling without the stripped parameter should work
handler("test")
```

## Multiple prefix parameters

```toml
[environment]
python-version = "3.12"
```

```py
from typing import Callable, reveal_type
from typing_extensions import Concatenate, ParamSpec

P = ParamSpec("P")

def add_two_params[**P, R](
f: Callable[Concatenate[int, str, P], R],
) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return f(0, "", *args, **kwargs)
return wrapper

@add_two_params
def process(a: int, b: str, flag: bool) -> None:
pass

reveal_type(process) # revealed: (flag: bool) -> None

process(True)
```

## Assignability of `Concatenate` gradual forms

When both sides of an assignment use `Concatenate[T, ...]`, the prefix parameters must be
compatible. The gradual tail (`...`) still allows assignability for the remaining parameters.

```py
from typing_extensions import Callable, Concatenate

def _(
x: Callable[Concatenate[int, ...], None],
y: Callable[Concatenate[str, ...], None],
same: Callable[Concatenate[int, ...], None],
gradual: Callable[..., None],
multi_self: Callable[Concatenate[int, str, ...], None],
multi_other: Callable[Concatenate[str, int, ...], None],
):
# Same prefix types: assignable
x = same

# Different prefix types: not assignable
x = y # error: [invalid-assignment]

# Swapped multi-prefix types: not assignable
multi_self = multi_other # error: [invalid-assignment]

# Pure gradual is assignable to/from Concatenate gradual
x = gradual
gradual = x
```
Original file line number Diff line number Diff line change
Expand Up @@ -1081,8 +1081,5 @@ class Factory[**P](Protocol):
def call_factory[**P](ctr: Factory[P], *args: P.args, **kwargs: P.kwargs) -> int:
return ctr("", *args, **kwargs)

# TODO: This should be OK - P should be inferred as [] since my_factory only has `arg: str`
# which matches the prefix. Currently this is a false positive.
# error: [invalid-argument-type]
call_factory(my_factory)
```
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,7 @@ from typing_extensions import Callable, Concatenate, TypeAliasType
MyAlias4: TypeAlias = Callable[Concatenate[dict[str, T], ...], list[U]]

def _(c: MyAlias4[int, str]):
# TODO: should be (int, / ...) -> str
reveal_type(c) # revealed: Unknown
reveal_type(c) # revealed: (dict[str, int], /, *args: Any, **kwargs: Any) -> list[str]

T = TypeVar("T")

Expand Down Expand Up @@ -270,8 +269,7 @@ def _(x: ListOrDict[int]):
MyAlias7: TypeAlias = Callable[Concatenate[T, ...], None]

def _(c: MyAlias7[int]):
# TODO: should be (int, / ...) -> None
reveal_type(c) # revealed: Unknown
reveal_type(c) # revealed: (int, /, *args: Any, **kwargs: Any) -> None
```

## Imported
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub(crate) use crate::types::narrow::{
infer_narrowing_constraint,
};
use crate::types::newtype::NewType;
pub(crate) use crate::types::signatures::{Parameter, Parameters};
pub(crate) use crate::types::signatures::{ConcatenateTail, Parameter, Parameters};
use crate::types::signatures::{ParameterForm, walk_signature};
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder};
use crate::types::typed_dict::TypedDictField;
Expand Down
13 changes: 7 additions & 6 deletions crates/ty_python_semantic/src/types/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2084,20 +2084,21 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> {
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
// For `ParamSpec` kind, the parameters still contain `*args` and `**kwargs`, but we
// display them as `**P` instead, so avoid multiline in that case.
// TODO: This might change once we support `Concatenate`
// For `Gradual` kind without prefix params (len <= 2), display as `...`.
let multiline = self.settings.multiline
&& self.parameters.len() > 1
&& !matches!(self.parameters.kind(), ParametersKind::ParamSpec(_))
&& !matches!(
self.parameters.kind(),
ParametersKind::Gradual | ParametersKind::ParamSpec(_)
ParametersKind::Gradual | ParametersKind::Top
);
// Opening parenthesis
f.write_char('(')?;
if multiline {
f.write_str("\n ")?;
}
match self.parameters.kind() {
ParametersKind::Standard => {
ParametersKind::Standard | ParametersKind::Concatenate(_) => {
let mut star_added = false;
let mut needs_slash = false;
let mut first = true;
Expand Down Expand Up @@ -2149,9 +2150,9 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> {
}
}
ParametersKind::Gradual | ParametersKind::Top => {
// We represent gradual form as `...` in the signature, internally the parameters still
// contain `(*args, **kwargs)` parameters. (Top parameters are displayed the same
// as gradual parameters, we just wrap the entire signature in `Top[]`.)
// We represent gradual form as `...` in the signature, internally the parameters
// still contain `(*args, **kwargs)` parameters. (Top parameters are displayed the
// same as gradual parameters, we just wrap the entire signature in `Top[]`.)
f.write_str("...")?;
}
ParametersKind::ParamSpec(typevar) => {
Expand Down
Loading