Skip to content
Merged
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 @@ -580,7 +580,7 @@ def _():
reveal_type(x4) # revealed: X
```

## Prefer the declared type of generic classes
## Prefer the declared type of generic classes and callables

```toml
[environment]
Expand Down Expand Up @@ -682,6 +682,38 @@ x1: X[int | None] = X()
reveal_type(x1) # revealed: X[None]
```

We also prefer the declared type of `Callable` parameters, which are in contravariant position:

```py
from typing import Callable

type AnyToBool = Callable[[Any], bool]

def wrap[**P, T](f: Callable[P, T]) -> Callable[P, T]:
return f

def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError

def maybe_make_callable[T](x: T) -> Callable[[T], bool] | None:
raise NotImplementedError

x1: Callable[[Any], bool] = make_callable(0)
reveal_type(x1) # revealed: (Any, /) -> bool

x2: AnyToBool = make_callable(0)
reveal_type(x2) # revealed: (Any, /) -> bool

x3: Callable[[list[Any]], bool] = make_callable([0])
reveal_type(x3) # revealed: (list[Any], /) -> bool

x4: Callable[[Any], bool] = wrap(make_callable(0))
reveal_type(x4) # revealed: (Any, /) -> bool

x5: Callable[[Any], bool] | None = maybe_make_callable(0)
reveal_type(x5) # revealed: ((Any, /) -> bool) | None
```

## Declared type preference sees through subtyping

```toml
Expand Down Expand Up @@ -775,33 +807,48 @@ python-version = "3.12"
```

```py
from typing import reveal_type, TypedDict
from typing import reveal_type, Any, Callable, TypedDict

def identity[T](x: T) -> T:
return x

def _(narrow: dict[str, str], target: list[str] | dict[str, str] | None):
type Target = Any | list[str] | dict[str, str] | Callable[[str], None] | None

def _(narrow: dict[str, str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: dict[str, str]

def _(narrow: list[str], target: list[str] | dict[str, str] | None):
def _(narrow: list[str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: list[str]

def _(narrow: list[str] | dict[str, str], target: list[str] | dict[str, str] | None):
def _(narrow: Callable[[str], None], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: (str, /) -> None

def _(narrow: list[str] | dict[str, str], target: Target):
target = identity(narrow)
reveal_type(target) # revealed: list[str] | dict[str, str]

class TD(TypedDict):
x: int

def _(target: list[TD] | dict[str, TD] | None):
type TargetWithTD = Any | list[TD] | dict[str, TD] | Callable[[TD], None] | None

def _(target: TargetWithTD):
target = identity([{"x": 1}])
reveal_type(target) # revealed: list[TD]

def _(target: list[TD] | dict[str, TD] | None):
def _(target: TargetWithTD):
target = identity({"x": {"x": 1}})
reveal_type(target) # revealed: dict[str, TD]

def _(target: TargetWithTD):
def make_callable[T](x: T) -> Callable[[T], None]:
raise NotImplementedError

target = identity(make_callable({"x": 1}))
reveal_type(target) # revealed: (TD, /) -> None
```

## Prefer the inferred type of non-generic classes
Expand Down Expand Up @@ -886,7 +933,7 @@ def _(a: int, b: str, c: int | str):
reveal_type(x10) # revealed: int | str | None
```

## Assignability diagnostics ignore declared type of generic classes
## Assignability diagnostics ignore declared type

```toml
[environment]
Expand All @@ -912,19 +959,27 @@ class A(TypedDict):
x2: list[A | bool] = [{"bar": 1}, 1]
```

However, the declared type of generic classes should be ignored if the specialization is not
solvable:
However, the declared type should be ignored if the specialization is not solvable:

```py
from typing import Any, Callable

def g[T](x: list[T]) -> T:
return x[0]

def _(a: int | None):
# error: [invalid-assignment] "Object of type `list[int | None]` is not assignable to `list[str]`"
y1: list[str] = f(a)
x1: list[str] = f(a)

# error: [invalid-assignment] "Object of type `int | None` is not assignable to `str`"
y2: str = g(f(a))
x2: str = g(f(a))

def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError

def _(a: int | None):
# error: [invalid-assignment] "Object of type `(int | None, /) -> bool` is not assignable to `(str, /) -> bool`"
x1: Callable[[str], bool] = make_callable(a)
```

## Forward annotation with unclosed string literal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def outside_callable(t: T) -> Callable[[T], T]:
# revealed: ty_extensions.GenericContext[T@outside_callable]
reveal_type(generic_context(outside_callable))

# revealed: (Literal[1], /) -> Literal[1]
# revealed: (int, /) -> int
reveal_type(outside_callable(1))
# revealed: None
reveal_type(generic_context(outside_callable(1)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def outside_callable[T](t: T) -> Callable[[T], T]:
# revealed: ty_extensions.GenericContext[T@outside_callable]
reveal_type(generic_context(outside_callable))

# revealed: (Literal[1], /) -> Literal[1]
# revealed: (int, /) -> int
reveal_type(outside_callable(1))
# revealed: None
reveal_type(generic_context(outside_callable(1)))
Expand Down
32 changes: 31 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ We promote in non-covariant position in the return type of a generic function, o
generic class:

```py
from typing import Callable, Literal

class Bivariant[T]:
def __init__(self, value: T): ...

Expand Down Expand Up @@ -124,6 +126,8 @@ def f8[T](x: T) -> Invariant[T] | Covariant[T] | None: ...
def f9[T](x: T) -> tuple[Invariant[T], Invariant[T]] | None: ...
def f10[T, U](x: T, y: U) -> tuple[Invariant[T], Covariant[U]] | None: ...
def f11[T, U](x: T, y: U) -> tuple[Invariant[Covariant[T] | None], Covariant[U]] | None: ...
def f12[T](x: T) -> Callable[[T], bool] | None: ...
def f13[T](x: T) -> Callable[[bool], Invariant[T]] | None: ...

reveal_type(Bivariant(1)) # revealed: Bivariant[Literal[1]]
reveal_type(Covariant(1)) # revealed: Covariant[Literal[1]]
Expand All @@ -144,6 +148,9 @@ reveal_type(f9(1)) # revealed: tuple[Invariant[int], Invariant[int]] | None

reveal_type(f10(1, 1)) # revealed: tuple[Invariant[int], Covariant[Literal[1]]] | None
reveal_type(f11(1, 1)) # revealed: tuple[Invariant[Covariant[int] | None], Covariant[Literal[1]]] | None

reveal_type(f12(1)) # revealed: ((int, /) -> bool) | None
reveal_type(f13(1)) # revealed: ((bool, /) -> Invariant[int]) | None
```

## Promotion is recursive
Expand Down Expand Up @@ -190,6 +197,7 @@ declared in a promotable position:
```py
from enum import Enum
from typing import Sequence, Literal, LiteralString
from typing import Callable

class Color(Enum):
RED = "red"
Expand Down Expand Up @@ -274,6 +282,18 @@ reveal_type(x21) # revealed: X[Literal[1]]

x22: X[Literal[1]] | None = x([1])
reveal_type(x22) # revealed: X[Literal[1]]

def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError

def maybe_make_callable[T](x: T) -> Callable[[T], bool] | None:
raise NotImplementedError

x23: Callable[[Literal[1]], bool] = make_callable(1)
reveal_type(x23) # revealed: (Literal[1], /) -> bool

x24: Callable[[Literal[1]], bool] | None = maybe_make_callable(1)
reveal_type(x24) # revealed: ((Literal[1], /) -> bool) | None
```

## Literal annotations see through subtyping
Expand Down Expand Up @@ -403,7 +423,7 @@ later used in a promotable position:

```py
from enum import Enum
from typing import Literal
from typing import Callable, Literal

def promote[T](x: T) -> list[T]:
return [x]
Expand Down Expand Up @@ -449,6 +469,16 @@ class MyEnum(Enum):
def _(x: Literal[MyEnum.A, MyEnum.B]):
reveal_type(x) # revealed: Literal[MyEnum.A, MyEnum.B]
reveal_type([x]) # revealed: list[Literal[MyEnum.A, MyEnum.B]]

def make_callable[T](x: T) -> Callable[[T], bool]:
raise NotImplementedError

def maybe_make_callable[T](x: T) -> Callable[[T], bool] | None:
raise NotImplementedError

def _(x: Literal[1]):
reveal_type(make_callable(x)) # revealed: (Literal[1], /) -> bool
reveal_type(maybe_make_callable(x)) # revealed: ((Literal[1], /) -> bool) | None
```

Literal promotability is respected by unions:
Expand Down
Loading
Loading