## Summary Previously if an explicit specialization failed (e.g. wrong number of type arguments or violates an upper bound) we just inferred `Unknown` for the entire type. This actually caused us to panic on an a case of a recursive upper bound with invalid specialization; the upper bound would oscillate indefinitely in fixpoint iteration between `Unknown` and the given specialization. This could be fixed with a cycle recovery function, but in this case there's a simpler fix: if we infer `C[Unknown]` instead of `Unknown` for an invalid attempt to specialize `C`, that allows fixpoint iteration to quickly converge, as well as giving a more precise type inference. Other type checkers actually just go with the attempted specialization even if it's invalid. So if `C` has a type parameter with upper bound `int`, and you say `C[str]`, they'll emit a diagnostic but just go with `C[str]`. Even weirder, if `C` has a single type parameter and you say `C[str, bytes]`, they'll just go with `C[str]` as the type. I'm not convinced by this approach; it seems odd to have specializations floating around that explicitly violate the declared upper bound, or in the latter case aren't even the specialization the annotation requested. I prefer `C[Unknown]` for this case. Fixing this revealed an issue with `collections.namedtuple`, which returns `type[tuple[Any, ...]]`. Due to https://github.com/astral-sh/ty/issues/1649 we consider that to be an invalid specialization. So previously we returned `Unknown`; after this PR it would be `type[tuple[Unknown]]`, leading to more false positives from our lack of functional namedtuple support. To avoid that I added an explicit Todo type for functional namedtuples for now. ## Test Plan Added and updated mdtests. The conformance suite changes have to do with `ParamSpec`, so no meaningful signal there. The ecosystem changes appear to be the expected effects of having more precise type information (including occurrences of known issues such as https://github.com/astral-sh/ty/issues/1495 ). Most effects are just changes to types in diagnostics.
140 lines
4.1 KiB
Markdown
140 lines
4.1 KiB
Markdown
# Cycles
|
|
|
|
## Function signature
|
|
|
|
Deferred annotations can result in cycles in resolving a function signature:
|
|
|
|
```py
|
|
from __future__ import annotations
|
|
|
|
# error: [invalid-type-form]
|
|
def f(x: f):
|
|
pass
|
|
|
|
reveal_type(f) # revealed: def f(x: Unknown) -> Unknown
|
|
```
|
|
|
|
## Unpacking
|
|
|
|
See: <https://github.com/astral-sh/ty/issues/364>
|
|
|
|
```py
|
|
class Point:
|
|
def __init__(self, x: int = 0, y: int = 0) -> None:
|
|
self.x = x
|
|
self.y = y
|
|
|
|
def replace_with(self, other: "Point") -> None:
|
|
self.x, self.y = other.x, other.y
|
|
|
|
p = Point()
|
|
reveal_type(p.x) # revealed: Unknown | int
|
|
reveal_type(p.y) # revealed: Unknown | int
|
|
```
|
|
|
|
## Self-referential bare type alias
|
|
|
|
```toml
|
|
[environment]
|
|
python-version = "3.12" # typing.TypeAliasType
|
|
```
|
|
|
|
```py
|
|
from typing import Union, TypeAliasType, Sequence, Mapping
|
|
|
|
A = list["A" | None]
|
|
|
|
def f(x: A):
|
|
# TODO: should be `list[A | None]`?
|
|
reveal_type(x) # revealed: list[Divergent]
|
|
# TODO: should be `A | None`?
|
|
reveal_type(x[0]) # revealed: Divergent
|
|
|
|
JSONPrimitive = Union[str, int, float, bool, None]
|
|
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
|
|
```
|
|
|
|
## Self-referential legacy type variables
|
|
|
|
```py
|
|
from typing import Generic, TypeVar
|
|
|
|
B = TypeVar("B", bound="Base")
|
|
|
|
class Base(Generic[B]):
|
|
pass
|
|
```
|
|
|
|
## Parameter default values
|
|
|
|
This is a regression test for <https://github.com/astral-sh/ty/issues/1402>. When a parameter has a
|
|
default value that references the callable itself, we currently prevent infinite recursion by simply
|
|
falling back to `Unknown` for the type of the default value, which does not have any practical
|
|
impact except for the displayed type. We could also consider inferring `Divergent` when we encounter
|
|
too many layers of nesting (instead of just one), but that would require a type traversal which
|
|
could have performance implications. So for now, we mainly make sure not to panic or stack overflow
|
|
for these seeminly rare cases.
|
|
|
|
### Functions
|
|
|
|
```py
|
|
class C:
|
|
def f(self: "C"):
|
|
def inner_a(positional=self.a):
|
|
return
|
|
self.a = inner_a
|
|
# revealed: def inner_a(positional=Unknown | (def inner_a(positional=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(inner_a)
|
|
|
|
def inner_b(*, kw_only=self.b):
|
|
return
|
|
self.b = inner_b
|
|
# revealed: def inner_b(*, kw_only=Unknown | (def inner_b(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(inner_b)
|
|
|
|
def inner_c(positional_only=self.c, /):
|
|
return
|
|
self.c = inner_c
|
|
# revealed: def inner_c(positional_only=Unknown | (def inner_c(positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
|
reveal_type(inner_c)
|
|
|
|
def inner_d(*, kw_only=self.d):
|
|
return
|
|
self.d = inner_d
|
|
# revealed: def inner_d(*, kw_only=Unknown | (def inner_d(*, kw_only=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(inner_d)
|
|
```
|
|
|
|
We do, however, still check assignability of the default value to the parameter type:
|
|
|
|
```py
|
|
class D:
|
|
def f(self: "D"):
|
|
# error: [invalid-parameter-default] "Default value of type `Unknown | (def inner_a(a: int = Unknown | (def inner_a(a: int = Unknown) -> Unknown)) -> Unknown)` is not assignable to annotated parameter type `int`"
|
|
def inner_a(a: int = self.a): ...
|
|
self.a = inner_a
|
|
```
|
|
|
|
### Lambdas
|
|
|
|
```py
|
|
class C:
|
|
def f(self: "C"):
|
|
self.a = lambda positional=self.a: positional
|
|
self.b = lambda *, kw_only=self.b: kw_only
|
|
self.c = lambda positional_only=self.c, /: positional_only
|
|
self.d = lambda *, kw_only=self.d: kw_only
|
|
|
|
# revealed: (positional=Unknown | ((positional=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(self.a)
|
|
|
|
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(self.b)
|
|
|
|
# revealed: (positional_only=Unknown | ((positional_only=Unknown, /) -> Unknown), /) -> Unknown
|
|
reveal_type(self.c)
|
|
|
|
# revealed: (*, kw_only=Unknown | ((*, kw_only=Unknown) -> Unknown)) -> Unknown
|
|
reveal_type(self.d)
|
|
```
|