crates/ty_python_semantic/resources/mdtest/cycle.md
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
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: int
reveal_type(p.y) # revealed: int
[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"]]')
def _(x: JSONValue):
reveal_type(x) # revealed: Sequence[JSONValue] | int | float | None | Mapping[str, JSONValue]
from typing import Generic, TypeVar
B = TypeVar("B", bound="Base")
class Base(Generic[B]):
pass
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.
class C:
def f(self: "C"):
def inner_a(positional=self.a):
return
self.a = inner_a
# revealed: def inner_a(positional=...) -> Unknown
reveal_type(inner_a)
def inner_b(*, kw_only=self.b):
return
self.b = inner_b
# revealed: def inner_b(*, kw_only=...) -> Unknown
reveal_type(inner_b)
def inner_c(positional_only=self.c, /):
return
self.c = inner_c
# revealed: def inner_c(positional_only=..., /) -> Unknown
reveal_type(inner_c)
def inner_d(*, kw_only=self.d):
return
self.d = inner_d
# revealed: def inner_d(*, kw_only=...) -> 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 `(a: int = ...) -> Unknown` is not assignable to annotated parameter type `int`"
def inner_a(a: int = self.a): ...
self.a = inner_a
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 = ...) -> Unknown | ((positional=...) -> Divergent) | ((positional=...) -> Divergent)
reveal_type(self.a)
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Divergent) | ((*, kw_only=...) -> Divergent)
reveal_type(self.b)
# revealed: (positional_only: Unknown = ..., /) -> Unknown | ((positional_only=..., /) -> Divergent) | ((positional_only=..., /) -> Divergent)
reveal_type(self.c)
# revealed: (*, kw_only=...) -> Unknown | ((*, kw_only=...) -> Divergent) | ((*, kw_only=...) -> Divergent)
reveal_type(self.d)
class Cyclic:
def __init__(self, data: str | dict):
self.data = data
def update(self):
if isinstance(self.data, str):
self.data = {"url": self.data}
# revealed: str | dict[Unknown, Unknown] | dict[str, str]
reveal_type(Cyclic("").data)
hasattrThis pattern used to panic with "too many cycle iterations".
class Cached:
def get(self) -> int:
return 0
@property
def metadata(self) -> int:
if not hasattr(self, "_metadata"):
self._metadata = self.get()
return self._metadata
reveal_type(Cached().metadata) # revealed: int
This example was minimized from
a real issue in robotframework.
It created
a complicated cycle with multiple cycle heads,
which also involved
a tricky Salsa behavior that comes up when a query oscillates between being a cycle head and not being one.
entry.py:
from derived import Derived
Derived.decorate
# revealed: bound method <class 'Derived'>.decorate[T](item_class: type[T]) -> type[T]
reveal_type(Derived.decorate)
derived.py:
from ty_extensions import reveal_mro
import bases
class Derived(bases.GenericBase["Foo", "Bar"]): ...
@Derived.decorate
class Foo(bases.Foo): ...
# revealed: <class 'Foo'>
reveal_type(Foo)
# revealed: (<class 'derived.Foo'>, <class 'bases.Foo'>, <class 'object'>)
reveal_mro(Foo)
@Derived.decorate
class Bar(bases.Bar): ...
# revealed: <class 'Bar'>
reveal_type(Bar)
# revealed: (<class 'derived.Bar'>, <class 'bases.Bar'>, <class 'object'>)
reveal_mro(Bar)
bases.py:
from typing import Generic, TypeVar, Type
from ty_extensions import reveal_mro
T = TypeVar("T")
B1 = TypeVar("B1", bound="Foo")
B2 = TypeVar("B2", bound="Bar")
class GenericBase(Generic[B1, B2]):
@classmethod
def decorate(cls, item_class: Type[T]) -> Type[T]:
return item_class
# revealed: <class 'GenericBase'>
reveal_type(GenericBase)
# revealed: (<class 'GenericBase[Unknown, Unknown]'>, typing.Generic, <class 'object'>)
reveal_mro(GenericBase)
# revealed: (<class 'GenericBase[Foo, Bar]'>, typing.Generic, <class 'object'>)
reveal_mro(GenericBase["Foo", "Bar"])
class Foo: ...
class Bar: ...