## 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.
4.1 KiB
Cycles
Function signature
Deferred annotations can result in cycles in resolving a function signature:
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
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
[environment]
python-version = "3.12" # typing.TypeAliasType
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
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
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:
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
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)