Merge branch 'main' into dcreager/real-constraint-sets

* main: (21 commits)
  [ty] Literal promotion refactor (#20646)
  [ty] Add tests for nested generic functions (#20631)
  [`cli`] Add conflict between `--add-noqa` and `--diff` options (#20642)
  [ty] Ensure first-party search paths always appear in a sensible order (#20629)
  [ty] Use `typing.Self` for the first parameter of instance methods (#20517)
  [ty] Remove unnecessary `parsed_module()` calls (#20630)
  Remove `TextEmitter` (#20595)
  [ty] Use fully qualified names to distinguish ambiguous protocols in diagnostics (#20627)
  [ty] Ecosystem analyzer: relax timeout thresholds (#20626)
  [ty] Apply type mappings to functions eagerly (#20596)
  [ty] Improve disambiguation of class names in diagnostics (#20603)
  Add the *The Basics* title back to CONTRIBUTING.md (#20624)
  [`playground`] Fix quick fixes for empty ranges in playground (#20599)
  Update dependency ruff to v0.13.2 (#20622)
  [`ruff`] Fix minor typos in doc comments (#20623)
  Update dependency PyYAML to v6.0.3 (#20621)
  Update cargo-bins/cargo-binstall action to v1.15.6 (#20620)
  Fixed documentation for try_consider_else (#20587)
  [ty] Use `Top` materializations for `TypeIs` special form (#20591)
  [ty] Simplify `Any | (Any & T)` to `Any` (#20593)
  ...
This commit is contained in:
Douglas Creager 2025-09-30 08:57:18 -04:00
commit 06aed22426
113 changed files with 2440 additions and 1129 deletions

View file

@ -33,11 +33,6 @@ class Shape:
reveal_type(x) # revealed: Self@nested_func_without_enclosing_binding
inner(self)
def implicit_self(self) -> Self:
# TODO: first argument in a method should be considered as "typing.Self"
reveal_type(self) # revealed: Unknown
return self
reveal_type(Shape().nested_type()) # revealed: list[Shape]
reveal_type(Shape().nested_func()) # revealed: Shape
@ -53,6 +48,150 @@ class Outer:
return self
```
## Type of (unannotated) `self` parameters
In instance methods, the first parameter (regardless of its name) is assumed to have the type
`typing.Self`, unless it has an explicit annotation. This does not apply to `@classmethod` and
`@staticmethod`s.
```toml
[environment]
python-version = "3.11"
```
```py
from typing import Self
class A:
def implicit_self(self) -> Self:
# TODO: This should be Self@implicit_self
reveal_type(self) # revealed: Unknown
return self
def a_method(self) -> int:
def first_arg_is_not_self(a: int) -> int:
reveal_type(a) # revealed: int
return a
return first_arg_is_not_self(1)
@classmethod
def a_classmethod(cls) -> Self:
# TODO: This should be type[Self@bar]
reveal_type(cls) # revealed: Unknown
return cls()
@staticmethod
def a_staticmethod(x: int): ...
a = A()
reveal_type(a.implicit_self()) # revealed: A
reveal_type(a.implicit_self) # revealed: bound method A.implicit_self() -> A
```
Calling an instance method explicitly verifies the first argument:
```py
A.implicit_self(a)
# error: [invalid-argument-type] "Argument to function `implicit_self` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `A` of type variable `Self`"
A.implicit_self(1)
```
Passing `self` implicitly also verifies the type:
```py
from typing import Never
class Strange:
def can_not_be_called(self: Never) -> None: ...
# error: [invalid-argument-type] "Argument to bound method `can_not_be_called` is incorrect: Expected `Never`, found `Strange`"
Strange().can_not_be_called()
```
If the method is a class or static method then first argument is not inferred as `Self`:
```py
A.a_classmethod()
A.a_classmethod(a) # error: [too-many-positional-arguments]
A.a_staticmethod(1)
a.a_staticmethod(1)
A.a_staticmethod(a) # error: [invalid-argument-type]
```
The first parameter of instance methods always has type `Self`, if it is not explicitly annotated.
The name `self` is not special in any way.
```py
class B:
def name_does_not_matter(this) -> Self:
# TODO: Should reveal Self@name_does_not_matter
reveal_type(this) # revealed: Unknown
return this
def positional_only(self, /, x: int) -> Self:
# TODO: Should reveal Self@positional_only
reveal_type(self) # revealed: Unknown
return self
def keyword_only(self, *, x: int) -> Self:
# TODO: Should reveal Self@keyword_only
reveal_type(self) # revealed: Unknown
return self
@property
def a_property(self) -> Self:
# TODO: Should reveal Self@a_property
reveal_type(self) # revealed: Unknown
return self
reveal_type(B().name_does_not_matter()) # revealed: B
reveal_type(B().positional_only(1)) # revealed: B
reveal_type(B().keyword_only(x=1)) # revealed: B
# TODO: this should be B
reveal_type(B().a_property) # revealed: Unknown
```
This also works for generic classes:
```py
from typing import Self, Generic, TypeVar
T = TypeVar("T")
class G(Generic[T]):
def id(self) -> Self:
# TODO: Should reveal Self@id
reveal_type(self) # revealed: Unknown
return self
reveal_type(G[int]().id()) # revealed: G[int]
reveal_type(G[str]().id()) # revealed: G[str]
```
Free functions and nested functions do not use implicit `Self`:
```py
def not_a_method(self):
reveal_type(self) # revealed: Unknown
# error: [invalid-type-form]
def does_not_return_self(self) -> Self:
return self
class C:
def outer(self) -> None:
def inner(self):
reveal_type(self) # revealed: Unknown
reveal_type(not_a_method) # revealed: def not_a_method(self) -> Unknown
```
## typing_extensions
```toml
@ -208,6 +347,47 @@ class MyMetaclass(type):
return super().__new__(cls)
```
## Explicit annotations override implicit `Self`
If the first parameter is explicitly annotated, that annotation takes precedence over the implicit
`Self` type.
```toml
[environment]
python-version = "3.12"
```
```py
from __future__ import annotations
from typing import final
@final
class Disjoint: ...
class Explicit:
# TODO: We could emit a warning if the annotated type of `self` is disjoint from `Explicit`
def bad(self: Disjoint) -> None:
reveal_type(self) # revealed: Disjoint
def forward(self: Explicit) -> None:
reveal_type(self) # revealed: Explicit
# error: [invalid-argument-type] "Argument to bound method `bad` is incorrect: Expected `Disjoint`, found `Explicit`"
Explicit().bad()
Explicit().forward()
class ExplicitGeneric[T]:
def special(self: ExplicitGeneric[int]) -> None:
reveal_type(self) # revealed: ExplicitGeneric[int]
ExplicitGeneric[int]().special()
# TODO: this should be an `invalid-argument-type` error
ExplicitGeneric[str]().special()
```
## Binding a method fixes `Self`
When a method is bound, any instances of `Self` in its signature are "fixed", since we now know the

View file

@ -69,7 +69,9 @@ reveal_type(bound_method(1)) # revealed: str
When we call the function object itself, we need to pass the `instance` explicitly:
```py
C.f(1) # error: [missing-argument]
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `C`, found `Literal[1]`"
# error: [missing-argument]
C.f(1)
reveal_type(C.f(C(), 1)) # revealed: str
```

View file

@ -431,6 +431,8 @@ def _(flag: bool):
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
C7.union_of_metaclass_attributes = 2 if flag else 1
# TODO: https://github.com/astral-sh/ty/issues/1163
# error: [invalid-assignment]
C7.union_of_metaclass_data_descriptor_and_attribute = 2 if flag else 100
C7.union_of_class_attributes = 2 if flag else 1
C7.union_of_class_data_descriptor_and_attribute = 2 if flag else DataDescriptor()

View file

@ -43,8 +43,7 @@ import b
df: a.DataFrame = b.DataFrame() # error: [invalid-assignment] "Object of type `b.DataFrame` is not assignable to `a.DataFrame`"
def _(dfs: list[b.DataFrame]):
# TODO should be"Object of type `list[b.DataFrame]` is not assignable to `list[a.DataFrame]`
# error: [invalid-assignment] "Object of type `list[DataFrame]` is not assignable to `list[DataFrame]`"
# error: [invalid-assignment] "Object of type `list[b.DataFrame]` is not assignable to `list[a.DataFrame]`"
dataframes: list[a.DataFrame] = dfs
```
@ -171,6 +170,36 @@ class Container(Generic[T]):
## Protocols
### Differing members
`bad.py`:
```py
from typing import Protocol, TypeVar
T_co = TypeVar("T_co", covariant=True)
class Iterator(Protocol[T_co]):
def __nexxt__(self) -> T_co: ...
def bad() -> Iterator[str]:
raise NotImplementedError
```
`main.py`:
```py
from typing import Iterator
def f() -> Iterator[str]:
import bad
# error: [invalid-return-type] "Return type does not match returned value: expected `typing.Iterator[str]`, found `bad.Iterator[str]"
return bad.bad()
```
### Same members but with different types
```py
from typing import Protocol
import proto_a
@ -228,3 +257,21 @@ from typing import TypedDict
class Person(TypedDict):
name: bytes
```
## Tuple specializations
`module.py`:
```py
class Model: ...
```
```py
class Model: ...
def get_models_tuple() -> tuple[Model]:
from module import Model
# error: [invalid-return-type] "Return type does not match returned value: expected `tuple[mdtest_snippet.Model]`, found `tuple[module.Model]`"
return (Model(),)
```

View file

@ -562,17 +562,17 @@ class C(Generic[T]):
return u
reveal_type(generic_context(C)) # revealed: tuple[T@C]
reveal_type(generic_context(C.method)) # revealed: None
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: None
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
c: C[int] = C[int]()
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: None
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
```
## Specializations propagate

View file

@ -464,6 +464,7 @@ def f(x: str):
from typing import TypeVar, overload
T = TypeVar("T")
S = TypeVar("S")
def outer(t: T) -> None:
def inner(t: T) -> None: ...
@ -479,6 +480,13 @@ def overloaded_outer(t: T | None = None) -> None:
if t is not None:
inner(t)
def outer(t: T) -> None:
def inner(inner_t: T, s: S) -> tuple[T, S]:
return inner_t, s
reveal_type(inner(t, 1)) # revealed: tuple[T@outer, Literal[1]]
inner("wrong", 1) # error: [invalid-argument-type]
```
## Unpacking a TypeVar

View file

@ -504,17 +504,17 @@ class C[T]:
def cannot_shadow_class_typevar[T](self, t: T): ...
reveal_type(generic_context(C)) # revealed: tuple[T@C]
reveal_type(generic_context(C.method)) # revealed: None
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(C.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: None
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(C[int].method)) # revealed: tuple[Self@method]
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
c: C[int] = C[int]()
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: None
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U@generic_method]
reveal_type(generic_context(c.method)) # revealed: tuple[Self@method]
reveal_type(generic_context(c.generic_method)) # revealed: tuple[Self@generic_method, U@generic_method]
```
## Specializations propagate

View file

@ -474,6 +474,13 @@ def overloaded_outer[T](t: T | None = None) -> None:
if t is not None:
inner(t)
def outer[T](t: T) -> None:
def inner[S](inner_t: T, s: S) -> tuple[T, S]:
return inner_t, s
reveal_type(inner(t, 1)) # revealed: tuple[T@outer, Literal[1]]
inner("wrong", 1) # error: [invalid-argument-type]
```
## Unpacking a TypeVar
@ -534,6 +541,5 @@ class C:
def _(x: int):
reveal_type(C().explicit_self(x)) # revealed: tuple[C, int]
# TODO: this should be `tuple[C, int]` as well, once we support implicit `self`
reveal_type(C().implicit_self(x)) # revealed: tuple[Unknown, int]
reveal_type(C().implicit_self(x)) # revealed: tuple[C, int]
```

View file

@ -117,6 +117,7 @@ reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
reveal_type(C[int]().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Argument type `Literal[1]` does not satisfy upper bound `C[T@C]` of type variable `Self`"
C[int].f(1) # error: [missing-argument]
reveal_type(C[int].f(C[int](), 1)) # revealed: str
@ -154,7 +155,7 @@ from ty_extensions import generic_context
legacy.m("string", None) # error: [invalid-argument-type]
reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m
reveal_type(generic_context(Legacy)) # revealed: tuple[T@Legacy]
reveal_type(generic_context(legacy.m)) # revealed: tuple[S@m]
reveal_type(generic_context(legacy.m)) # revealed: tuple[Self@m, S@m]
```
With PEP 695 syntax, it is clearer that the method uses a separate typevar:

View file

@ -278,8 +278,7 @@ reveal_type(Person._make(("Alice", 42))) # revealed: Unknown
person = Person("Alice", 42)
reveal_type(person._asdict()) # revealed: dict[str, Any]
# TODO: should be `Person` once we support implicit type of `self`
reveal_type(person._replace(name="Bob")) # revealed: Unknown
reveal_type(person._replace(name="Bob")) # revealed: Person
```
When accessing them on child classes of generic `NamedTuple`s, the return type is specialized
@ -296,8 +295,7 @@ class Box(NamedTuple, Generic[T]):
class IntBox(Box[int]):
pass
# TODO: should be `IntBox` once we support the implicit type of `self`
reveal_type(IntBox(1)._replace(content=42)) # revealed: Unknown
reveal_type(IntBox(1)._replace(content=42)) # revealed: IntBox
```
## `collections.namedtuple`

View file

@ -324,8 +324,7 @@ a covariant generic, this is equivalent to using the upper bound of the type par
from typing import Self
class Covariant[T]:
# TODO: remove the explicit `Self` annotation, once we support the implicit type of `self`
def get(self: Self) -> T:
def get(self) -> T:
raise NotImplementedError
def _(x: object):
@ -338,8 +337,7 @@ Similarly, contravariant type parameters use their lower bound of `Never`:
```py
class Contravariant[T]:
# TODO: remove the explicit `Self` annotation, once we support the implicit type of `self`
def push(self: Self, x: T) -> None: ...
def push(self, x: T) -> None: ...
def _(x: object):
if isinstance(x, Contravariant):
@ -354,10 +352,8 @@ the type system, so we represent it with the internal `Top[]` special form.
```py
class Invariant[T]:
# TODO: remove the explicit `Self` annotation, once we support the implicit type of `self`
def push(self: Self, x: T) -> None: ...
# TODO: remove the explicit `Self` annotation, once we support the implicit type of `self`
def get(self: Self) -> T:
def push(self, x: T) -> None: ...
def get(self) -> T:
raise NotImplementedError
def _(x: object):

View file

@ -173,6 +173,11 @@ def _(d: Any):
## Narrowing
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Any
from typing_extensions import TypeGuard, TypeIs
@ -295,6 +300,38 @@ def _(a: Foo):
reveal_type(a) # revealed: Foo & Bar
```
For generics, we transform the argument passed into `TypeIs[]` from `X` to `Top[X]`. This helps
especially when using various functions from typeshed that are annotated as returning
`TypeIs[SomeCovariantGeneric[Any]]` to avoid false positives in other type checkers. For ty's
purposes, it would usually lead to more intuitive results if `object` was used as the specialization
for a covariant generic inside the `TypeIs` special form, but this is mitigated by our implicit
transformation from `TypeIs[SomeCovariantGeneric[Any]]` to `TypeIs[Top[SomeCovariantGeneric[Any]]]`
(which just simplifies to `TypeIs[SomeCovariantGeneric[object]]`).
```py
class Unrelated: ...
class Covariant[T]:
def get(self) -> T:
raise NotImplementedError
def is_instance_of_covariant(arg: object) -> TypeIs[Covariant[Any]]:
return isinstance(arg, Covariant)
def needs_instance_of_unrelated(arg: Unrelated):
pass
def _(x: Unrelated | Covariant[int]):
if is_instance_of_covariant(x):
raise RuntimeError("oh no")
reveal_type(x) # revealed: Unrelated & ~Covariant[object]
# We would emit a false-positive diagnostic here if we didn't implicitly transform
# `TypeIs[Covariant[Any]]` to `TypeIs[Covariant[object]]`
needs_instance_of_unrelated(x)
```
## `TypeGuard` special cases
```py

View file

@ -325,7 +325,7 @@ type A = list[Union["A", str]]
def f(x: A):
reveal_type(x) # revealed: list[A | str]
for item in x:
reveal_type(item) # revealed: list[A | str] | str
reveal_type(item) # revealed: list[Any | str] | str
```
#### With new-style union
@ -336,7 +336,7 @@ type A = list["A" | str]
def f(x: A):
reveal_type(x) # revealed: list[A | str]
for item in x:
reveal_type(item) # revealed: list[A | str] | str
reveal_type(item) # revealed: list[Any | str] | str
```
#### With Optional
@ -349,7 +349,7 @@ type A = list[Optional[Union["A", str]]]
def f(x: A):
reveal_type(x) # revealed: list[A | str | None]
for item in x:
reveal_type(item) # revealed: list[A | str | None] | str | None
reveal_type(item) # revealed: list[Any | str | None] | str | None
```
### Tuple comparison

View file

@ -893,8 +893,10 @@ class LotsOfBindings(Protocol):
match object():
case l: # error: [ambiguous-protocol-member]
...
# error: [ambiguous-protocol-member] "Consider adding an annotation, e.g. `m: int | str = ...`"
m = 1 if 1.2 > 3.4 else "a"
# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"]]
# revealed: frozenset[Literal["Nested", "NestedProtocol", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]]
reveal_type(get_protocol_members(LotsOfBindings))
class Foo(Protocol):
@ -1977,12 +1979,12 @@ from typing_extensions import TypeVar, Self, Protocol
from ty_extensions import is_equivalent_to, static_assert, is_assignable_to, is_subtype_of
class NewStyleClassScoped[T](Protocol):
def method(self: Self, input: T) -> None: ...
def method(self, input: T) -> None: ...
S = TypeVar("S")
class LegacyClassScoped(Protocol[S]):
def method(self: Self, input: S) -> None: ...
def method(self, input: S) -> None: ...
# TODO: these should pass
static_assert(is_equivalent_to(NewStyleClassScoped, LegacyClassScoped)) # error: [static-assert-error]

View file

@ -339,7 +339,7 @@ class A: ...
def f(x: A):
# TODO: no error
# error: [invalid-assignment] "Object of type `A | A` is not assignable to `A`"
# error: [invalid-assignment] "Object of type `mdtest_snippet.A | mdtest_snippet.A` is not assignable to `mdtest_snippet.A`"
x = A()
```

View file

@ -133,6 +133,11 @@ class Single(Enum):
VALUE = 1
static_assert(is_equivalent_to(P | Q | Single, Literal[Single.VALUE] | Q | P))
static_assert(is_equivalent_to(Any, Any | Intersection[Any, str]))
static_assert(is_equivalent_to(Any, Intersection[str, Any] | Any))
static_assert(is_equivalent_to(Any, Any | Intersection[Any, Not[None]]))
static_assert(is_equivalent_to(Any, Intersection[Not[None], Any] | Any))
```
## Tuples

View file

@ -1948,8 +1948,6 @@ static_assert(is_subtype_of(TypeOf[A.g], Callable[[int], int]))
static_assert(not is_subtype_of(TypeOf[a.f], Callable[[float], int]))
static_assert(not is_subtype_of(TypeOf[A.g], Callable[[], int]))
# TODO: This assertion should be true
# error: [static-assert-error] "Static assertion error: argument of type `ty_extensions.ConstraintSet[never]` is statically known to be falsy"
static_assert(is_subtype_of(TypeOf[A.f], Callable[[A, int], int]))
```

View file

@ -657,16 +657,14 @@ alice: Employee = {"name": "Alice", "employee_id": 1}
eve: Employee = {"name": "Eve"}
def combine(p: Person, e: Employee):
# TODO: Should be `Person` once we support the implicit type of self
reveal_type(p.copy()) # revealed: Unknown
# TODO: Should be `Employee` once we support the implicit type of self
reveal_type(e.copy()) # revealed: Unknown
reveal_type(p.copy()) # revealed: Person
reveal_type(e.copy()) # revealed: Employee
reveal_type(p | p) # revealed: Person
reveal_type(e | e) # revealed: Employee
# TODO: Should be `Person` once we support the implicit type of self and subtyping for TypedDicts
reveal_type(p | e) # revealed: Employee
# TODO: Should be `Person` once we support subtyping for TypedDicts
reveal_type(p | e) # revealed: Person | Employee
```
When inheriting from a `TypedDict` with a different `total` setting, inherited fields maintain their

View file

@ -254,8 +254,7 @@ async def long_running_task():
async def main():
async with asyncio.TaskGroup() as tg:
# TODO: should be `TaskGroup`
reveal_type(tg) # revealed: Unknown
reveal_type(tg) # revealed: TaskGroup
tg.create_task(long_running_task())
```