[ty] Infer type for implicit self parameters in method bodies (#20922)

## Summary

Infer a type of `Self` for unannotated `self` parameters in methods of
classes.

part of https://github.com/astral-sh/ty/issues/159

closes https://github.com/astral-sh/ty/issues/1081

## Conformance tests changes

```diff
+enums_member_values.py:85:9: error[invalid-assignment] Object of type `int` is not assignable to attribute `_value_` of type `str`
```
A true positive ✔️ 

```diff
-generics_self_advanced.py:35:9: error[type-assertion-failure] Argument does not have asserted type `Self@method2`
-generics_self_basic.py:14:9: error[type-assertion-failure] Argument does not have asserted type `Self@set_scale
```

Two false positives going away ✔️ 

```diff
+generics_syntax_infer_variance.py:82:9: error[invalid-assignment] Cannot assign to final attribute `x` on type `Self@__init__`
```

This looks like a true positive to me, even if it's not marked with `#
E` ✔️

```diff
+protocols_explicit.py:56:9: error[invalid-assignment] Object of type `tuple[int, int, str]` is not assignable to attribute `rgb` of type `tuple[int, int, int]`
```

True positive ✔️ 

```
+protocols_explicit.py:85:9: error[invalid-attribute-access] Cannot assign to ClassVar `cm1` from an instance of type `Self@__init__`
```

This looks like a true positive to me, even if it's not marked with `#
E`. But this is consistent with our understanding of `ClassVar`, I
think. ✔️

```py
+qualifiers_final_annotation.py:52:9: error[invalid-assignment] Cannot assign to final attribute `ID4` on type `Self@__init__`
+qualifiers_final_annotation.py:65:9: error[invalid-assignment] Cannot assign to final attribute `ID7` on type `Self@method1`
```

New true positives ✔️ 

```py
+qualifiers_final_annotation.py:52:9: error[invalid-assignment] Cannot assign to final attribute `ID4` on type `Self@__init__`
+qualifiers_final_annotation.py:57:13: error[invalid-assignment] Cannot assign to final attribute `ID6` on type `Self@__init__`
+qualifiers_final_annotation.py:59:13: error[invalid-assignment] Cannot assign to final attribute `ID6` on type `Self@__init__`
```

This is a new false positive, but that's a pre-existing issue on main
(if you annotate with `Self`):
https://play.ty.dev/3ee1c56d-7e13-43bb-811a-7a81e236e6ab  => reported
as https://github.com/astral-sh/ty/issues/1409

## Ecosystem

* There are 5931 new `unresolved-attribute` and 3292 new
`possibly-missing-attribute` attribute errors, way too many to look at
all of them. I randomly sampled 15 of these errors and found:
* 13 instances where there was simply no such attribute that we could
plausibly see. Sometimes [I didn't find it
anywhere](8644d886c6/openlibrary/plugins/openlibrary/tests/test_listapi.py (L33)).
Sometimes it was set externally on the object. Sometimes there was some
[`setattr` dynamicness going
on](a49f6b927d/setuptools/wheel.py (L88-L94)).
I would consider all of them to be true positives.
* 1 instance where [attribute was set on `obj` in
`__new__`](9e87b44fd4/sympy/tensor/array/array_comprehension.py (L45C1-L45C36)),
which we don't support yet
  * 1 instance [where the attribute was defined via `__slots__`

](e250ec0fc8/lib/spack/spack/vendor/pyrsistent/_pdeque.py (L48C5-L48C14))
* I see 44 instances [of the false positive
above](https://github.com/astral-sh/ty/issues/1409) with `Final`
instance attributes being set in `__init__`. I don't think this should
block this PR.

## Test Plan

New Markdown tests.

---------

Co-authored-by: Shaygan Hooshyari <sh.hooshyari@gmail.com>
This commit is contained in:
David Peter 2025-10-23 09:34:39 +02:00 committed by GitHub
parent 76a55314e4
commit 589e8ac0d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 325 additions and 210 deletions

View file

@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
100,
110,
);
bench_project(&benchmark, criterion);
@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
100,
150,
);
bench_project(&benchmark, criterion);

View file

@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
100,
320,
);
static STATIC_FRAME: Benchmark = Benchmark::new(
@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
630,
750,
);
#[track_caller]

View file

@ -1957,14 +1957,34 @@ class Quux:
",
);
// FIXME: This should list completions on `self`, which should
// include, at least, `foo` and `bar`. At time of writing
// (2025-06-04), the type of `self` is inferred as `Unknown` in
// this context. This in turn prevents us from getting a list
// of available attributes.
//
// See: https://github.com/astral-sh/ty/issues/159
assert_snapshot!(test.completions_without_builtins(), @"<No completions found>");
assert_snapshot!(test.completions_without_builtins(), @r"
bar
baz
foo
__annotations__
__class__
__delattr__
__dict__
__dir__
__doc__
__eq__
__format__
__getattribute__
__getstate__
__hash__
__init__
__init_subclass__
__module__
__ne__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
");
}
#[test]

View file

@ -1798,23 +1798,23 @@ class BoundedContainer[T: int, U = str]:
"T" @ 554..555: TypeParameter
"value2" @ 557..563: Parameter
"U" @ 565..566: TypeParameter
"self" @ 577..581: Variable
"self" @ 577..581: TypeParameter
"value1" @ 582..588: Variable
"T" @ 590..591: TypeParameter
"value1" @ 594..600: Parameter
"self" @ 609..613: Variable
"self" @ 609..613: TypeParameter
"value2" @ 614..620: Variable
"U" @ 622..623: TypeParameter
"value2" @ 626..632: Parameter
"get_first" @ 642..651: Method [definition]
"self" @ 652..656: SelfParameter
"T" @ 661..662: TypeParameter
"self" @ 679..683: Variable
"self" @ 679..683: TypeParameter
"value1" @ 684..690: Variable
"get_second" @ 700..710: Method [definition]
"self" @ 711..715: SelfParameter
"U" @ 720..721: TypeParameter
"self" @ 738..742: Variable
"self" @ 738..742: TypeParameter
"value2" @ 743..749: Variable
"BoundedContainer" @ 798..814: Class [definition]
"T" @ 815..816: TypeParameter [definition]

View file

@ -56,7 +56,7 @@ In instance methods, the first parameter (regardless of its name) is assumed to
```toml
[environment]
python-version = "3.11"
python-version = "3.12"
```
```py
@ -64,16 +64,30 @@ from typing import Self
class A:
def implicit_self(self) -> Self:
# TODO: This should be Self@implicit_self
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@implicit_self
return self
def a_method(self) -> int:
def first_arg_is_not_self(a: int) -> int:
def implicit_self_generic[T](self, x: T) -> T:
reveal_type(self) # revealed: Self@implicit_self_generic
return x
def method_a(self) -> None:
def first_param_is_not_self(a: int):
reveal_type(a) # revealed: int
return a
return first_arg_is_not_self(1)
reveal_type(self) # revealed: Self@method_a
def first_param_is_not_self_unannotated(a):
reveal_type(a) # revealed: Unknown
reveal_type(self) # revealed: Self@method_a
def first_param_is_also_not_self(self) -> None:
reveal_type(self) # revealed: Unknown
def first_param_is_explicit_self(this: Self) -> None:
reveal_type(this) # revealed: Self@method_a
reveal_type(self) # revealed: Self@method_a
@classmethod
def a_classmethod(cls) -> Self:
@ -127,19 +141,16 @@ 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
reveal_type(this) # revealed: Self@name_does_not_matter
return this
def positional_only(self, /, x: int) -> Self:
# TODO: Should reveal Self@positional_only
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@positional_only
return self
def keyword_only(self, *, x: int) -> Self:
# TODO: Should reveal Self@keyword_only
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@keyword_only
return self
@property
@ -165,8 +176,7 @@ T = TypeVar("T")
class G(Generic[T]):
def id(self) -> Self:
# TODO: Should reveal Self@id
reveal_type(self) # revealed: Unknown
reveal_type(self) # revealed: Self@id
return self
@ -252,6 +262,20 @@ class LinkedList:
reveal_type(LinkedList().next()) # revealed: LinkedList
```
Attributes can also refer to a generic parameter:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
foo: T
def method(self) -> None:
reveal_type(self) # revealed: Self@method
reveal_type(self.foo) # revealed: T@C
```
## Generic Classes
```py
@ -342,31 +366,28 @@ b: Self
# TODO: "Self" cannot be used in a function with a `self` or `cls` parameter that has a type annotation other than "Self"
class Foo:
# TODO: rejected Self because self has a different type
# TODO: This `self: T` annotation should be rejected because `T` is not `Self`
def has_existing_self_annotation(self: T) -> Self:
return self # error: [invalid-return-type]
def return_concrete_type(self) -> Self:
# TODO: tell user to use "Foo" instead of "Self"
# TODO: We could emit a hint that suggests annotating with `Foo` instead of `Self`
# error: [invalid-return-type]
return Foo()
@staticmethod
# TODO: reject because of staticmethod
# TODO: The usage of `Self` here should be rejected because this is a static method
def make() -> Self:
# error: [invalid-return-type]
return Foo()
class Bar(Generic[T]):
foo: T
def bar(self) -> T:
return self.foo
class Bar(Generic[T]): ...
# error: [invalid-type-form]
class Baz(Bar[Self]): ...
class MyMetaclass(type):
# TODO: rejected
# TODO: reject the Self usage. because self cannot be used within a metaclass.
def __new__(cls) -> Self:
return super().__new__(cls)
```

View file

@ -26,9 +26,7 @@ class C:
c_instance = C(1)
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
# There is no special handling of attributes that are (directly) assigned to a declared parameter,
# which means we union with `Unknown` here, since the attribute itself is not declared. This is
@ -177,8 +175,7 @@ c_instance = C(1)
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
# TODO: Should be `Unknown | Literal[1, "a"]`
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown | Literal[1, "a"]
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
@ -399,9 +396,19 @@ class TupleIterable:
class C:
def __init__(self) -> None:
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
[... for self.a in IntIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
# error: [unresolved-attribute]
[... for (self.b, self.c) in TupleIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
# error: [unresolved-attribute]
[... for self.d in IntIterable() for self.e in IntIterable()]
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
[[... for self.f in IntIterable()] for _ in IntIterable()]
[[... for self.g in IntIterable()] for self in [D()]]
@ -598,6 +605,8 @@ class C:
self.c = c
if False:
def set_e(self, e: str) -> None:
# TODO: Should not emit this diagnostic
# error: [unresolved-attribute]
self.e = e
# TODO: this would ideally be `Unknown | Literal[1]`
@ -685,7 +694,7 @@ class C:
pure_class_variable2: ClassVar = 1
def method(self):
# TODO: this should be an error
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `Self@method`"
self.pure_class_variable1 = "value set through instance"
reveal_type(C.pure_class_variable1) # revealed: str
@ -885,11 +894,9 @@ class Intermediate(Base):
# TODO: This should be an error (violates Liskov)
self.redeclared_in_method_with_wider_type: object = object()
# TODO: This should be an `invalid-assignment` error
self.overwritten_in_subclass_method = None
self.overwritten_in_subclass_method = None # error: [invalid-assignment]
# TODO: This should be an `invalid-assignment` error
self.pure_overwritten_in_subclass_method = None
self.pure_overwritten_in_subclass_method = None # error: [invalid-assignment]
self.pure_undeclared = "intermediate"
@ -1839,6 +1846,7 @@ def external_getattribute(name) -> int:
class ThisFails:
def __init__(self):
# error: [invalid-assignment] "Implicit shadowing of function `__getattribute__`"
self.__getattribute__ = external_getattribute
# error: [unresolved-attribute]

View file

@ -205,7 +205,7 @@ class C:
return str(key)
def f(self):
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
# error: [invalid-assignment] "Implicit shadowing of function `__getitem__`"
self.__getitem__ = None
# This is still fine, and simply calls the `__getitem__` method on the class

View file

@ -163,14 +163,13 @@ class A:
class B(A):
def __init__(self, a: int):
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'B'>, B>
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
super().__init__(a)
@classmethod
def f(cls):
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
# TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
super().f()
@ -358,15 +357,15 @@ from __future__ import annotations
class A:
def test(self):
reveal_type(super()) # revealed: <super: <class 'A'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'A'>, A>
class B:
def test(self):
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'B'>, B>
class C(A.B):
def test(self):
reveal_type(super()) # revealed: <super: <class 'C'>, Unknown>
reveal_type(super()) # revealed: <super: <class 'C'>, C>
def inner(t: C):
reveal_type(super()) # revealed: <super: <class 'B'>, C>
@ -616,7 +615,7 @@ class A:
class B(A):
def __init__(self, a: int):
super().__init__(a)
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
super().a
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"

View file

@ -170,6 +170,7 @@ def f1(flag: bool):
attr = DataDescriptor()
def f(self):
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `attr` on type `Self@f` with custom `__set__` method"
self.attr = "normal"
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]

View file

@ -208,8 +208,7 @@ class SuperUser(User):
def now_called_robert(self):
self.name = "Robert" # fine because overridden with a mutable attribute
# TODO: this should cause us to emit an error as we're assigning to a read-only property
# inherited from the `NamedTuple` superclass (requires https://github.com/astral-sh/ty/issues/159)
# error: 9 [invalid-assignment] "Cannot assign to read-only property `nickname` on object of type `Self@now_called_robert`"
self.nickname = "Bob"
james = SuperUser(0, "James", 42, "Jimmy")

View file

@ -21,144 +21,143 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
7 |
8 | class B(A):
9 | def __init__(self, a: int):
10 | # TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
11 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
12 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
13 | super().__init__(a)
14 |
15 | @classmethod
16 | def f(cls):
17 | # TODO: Once `Self` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
18 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
19 | super().f()
20 |
21 | super(B, B(42)).__init__(42)
22 | super(B, B).f()
23 | import enum
24 | from typing import Any, Self, Never, Protocol, Callable
25 | from ty_extensions import Intersection
26 |
27 | class BuilderMeta(type):
28 | def __new__(
29 | cls: type[Any],
30 | name: str,
31 | bases: tuple[type, ...],
32 | dct: dict[str, Any],
33 | ) -> BuilderMeta:
34 | # revealed: <super: <class 'BuilderMeta'>, Any>
35 | s = reveal_type(super())
36 | # revealed: Any
37 | return reveal_type(s.__new__(cls, name, bases, dct))
38 |
39 | class BuilderMeta2(type):
40 | def __new__(
41 | cls: type[BuilderMeta2],
42 | name: str,
43 | bases: tuple[type, ...],
44 | dct: dict[str, Any],
45 | ) -> BuilderMeta2:
46 | # revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
47 | s = reveal_type(super())
48 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
49 | # revealed: Unknown
50 | return reveal_type(s.__new__(cls, name, bases, dct))
51 |
52 | class Foo[T]:
53 | x: T
54 |
55 | def method(self: Any):
56 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
57 |
58 | if isinstance(self, Foo):
59 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
60 |
61 | def method2(self: Foo[T]):
62 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
63 | reveal_type(super())
64 |
65 | def method3(self: Foo):
66 | # revealed: <super: <class 'Foo'>, Foo[Unknown]>
67 | reveal_type(super())
68 |
69 | def method4(self: Self):
70 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
71 | reveal_type(super())
72 |
73 | def method5[S: Foo[int]](self: S, other: S) -> S:
74 | # revealed: <super: <class 'Foo'>, Foo[int]>
75 | reveal_type(super())
76 | return self
77 |
78 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
79 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
80 | reveal_type(super())
81 | return self
82 |
83 | def method7[S](self: S, other: S) -> S:
84 | # error: [invalid-super-argument]
85 | # revealed: Unknown
86 | reveal_type(super())
87 | return self
88 |
89 | def method8[S: int](self: S, other: S) -> S:
90 | # error: [invalid-super-argument]
91 | # revealed: Unknown
92 | reveal_type(super())
93 | return self
94 |
95 | def method9[S: (int, str)](self: S, other: S) -> S:
96 | # error: [invalid-super-argument]
97 | # revealed: Unknown
98 | reveal_type(super())
99 | return self
100 |
101 | def method10[S: Callable[..., str]](self: S, other: S) -> S:
102 | # error: [invalid-super-argument]
103 | # revealed: Unknown
104 | reveal_type(super())
105 | return self
106 |
107 | type Alias = Bar
108 |
109 | class Bar:
110 | def method(self: Alias):
111 | # revealed: <super: <class 'Bar'>, Bar>
112 | reveal_type(super())
113 |
114 | def pls_dont_call_me(self: Never):
115 | # revealed: <super: <class 'Bar'>, Unknown>
116 | reveal_type(super())
117 |
118 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
119 | # revealed: <super: <class 'Bar'>, Bar>
120 | reveal_type(super())
121 |
122 | class P(Protocol):
123 | def method(self: P):
124 | # revealed: <super: <class 'P'>, P>
125 | reveal_type(super())
126 |
127 | class E(enum.Enum):
128 | X = 1
129 |
130 | def method(self: E):
131 | match self:
132 | case E.X:
133 | # revealed: <super: <class 'E'>, E>
134 | reveal_type(super())
10 | reveal_type(super()) # revealed: <super: <class 'B'>, B>
11 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
12 | super().__init__(a)
13 |
14 | @classmethod
15 | def f(cls):
16 | # TODO: Once `cls` is supported, this should be `<super: <class 'B'>, <class 'B'>>`
17 | reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
18 | super().f()
19 |
20 | super(B, B(42)).__init__(42)
21 | super(B, B).f()
22 | import enum
23 | from typing import Any, Self, Never, Protocol, Callable
24 | from ty_extensions import Intersection
25 |
26 | class BuilderMeta(type):
27 | def __new__(
28 | cls: type[Any],
29 | name: str,
30 | bases: tuple[type, ...],
31 | dct: dict[str, Any],
32 | ) -> BuilderMeta:
33 | # revealed: <super: <class 'BuilderMeta'>, Any>
34 | s = reveal_type(super())
35 | # revealed: Any
36 | return reveal_type(s.__new__(cls, name, bases, dct))
37 |
38 | class BuilderMeta2(type):
39 | def __new__(
40 | cls: type[BuilderMeta2],
41 | name: str,
42 | bases: tuple[type, ...],
43 | dct: dict[str, Any],
44 | ) -> BuilderMeta2:
45 | # revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
46 | s = reveal_type(super())
47 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
48 | # revealed: Unknown
49 | return reveal_type(s.__new__(cls, name, bases, dct))
50 |
51 | class Foo[T]:
52 | x: T
53 |
54 | def method(self: Any):
55 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
56 |
57 | if isinstance(self, Foo):
58 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
59 |
60 | def method2(self: Foo[T]):
61 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
62 | reveal_type(super())
63 |
64 | def method3(self: Foo):
65 | # revealed: <super: <class 'Foo'>, Foo[Unknown]>
66 | reveal_type(super())
67 |
68 | def method4(self: Self):
69 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
70 | reveal_type(super())
71 |
72 | def method5[S: Foo[int]](self: S, other: S) -> S:
73 | # revealed: <super: <class 'Foo'>, Foo[int]>
74 | reveal_type(super())
75 | return self
76 |
77 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
78 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
79 | reveal_type(super())
80 | return self
81 |
82 | def method7[S](self: S, other: S) -> S:
83 | # error: [invalid-super-argument]
84 | # revealed: Unknown
85 | reveal_type(super())
86 | return self
87 |
88 | def method8[S: int](self: S, other: S) -> S:
89 | # error: [invalid-super-argument]
90 | # revealed: Unknown
91 | reveal_type(super())
92 | return self
93 |
94 | def method9[S: (int, str)](self: S, other: S) -> S:
95 | # error: [invalid-super-argument]
96 | # revealed: Unknown
97 | reveal_type(super())
98 | return self
99 |
100 | def method10[S: Callable[..., str]](self: S, other: S) -> S:
101 | # error: [invalid-super-argument]
102 | # revealed: Unknown
103 | reveal_type(super())
104 | return self
105 |
106 | type Alias = Bar
107 |
108 | class Bar:
109 | def method(self: Alias):
110 | # revealed: <super: <class 'Bar'>, Bar>
111 | reveal_type(super())
112 |
113 | def pls_dont_call_me(self: Never):
114 | # revealed: <super: <class 'Bar'>, Unknown>
115 | reveal_type(super())
116 |
117 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
118 | # revealed: <super: <class 'Bar'>, Bar>
119 | reveal_type(super())
120 |
121 | class P(Protocol):
122 | def method(self: P):
123 | # revealed: <super: <class 'P'>, P>
124 | reveal_type(super())
125 |
126 | class E(enum.Enum):
127 | X = 1
128 |
129 | def method(self: E):
130 | match self:
131 | case E.X:
132 | # revealed: <super: <class 'E'>, E>
133 | reveal_type(super())
```
# Diagnostics
```
error[invalid-super-argument]: `S@method7` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method7)` call
--> src/mdtest_snippet.py:86:21
--> src/mdtest_snippet.py:85:21
|
84 | # error: [invalid-super-argument]
85 | # revealed: Unknown
86 | reveal_type(super())
83 | # error: [invalid-super-argument]
84 | # revealed: Unknown
85 | reveal_type(super())
| ^^^^^^^
87 | return self
86 | return self
|
info: Type variable `S` has `object` as its implicit upper bound
info: `object` is not an instance or subclass of `<class 'Foo'>`
@ -169,13 +168,13 @@ info: rule `invalid-super-argument` is enabled by default
```
error[invalid-super-argument]: `S@method8` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method8)` call
--> src/mdtest_snippet.py:92:21
--> src/mdtest_snippet.py:91:21
|
90 | # error: [invalid-super-argument]
91 | # revealed: Unknown
92 | reveal_type(super())
89 | # error: [invalid-super-argument]
90 | # revealed: Unknown
91 | reveal_type(super())
| ^^^^^^^
93 | return self
92 | return self
|
info: Type variable `S` has upper bound `int`
info: `int` is not an instance or subclass of `<class 'Foo'>`
@ -185,13 +184,13 @@ info: rule `invalid-super-argument` is enabled by default
```
error[invalid-super-argument]: `S@method9` is not an instance or subclass of `<class 'Foo'>` in `super(<class 'Foo'>, S@method9)` call
--> src/mdtest_snippet.py:98:21
--> src/mdtest_snippet.py:97:21
|
96 | # error: [invalid-super-argument]
97 | # revealed: Unknown
98 | reveal_type(super())
95 | # error: [invalid-super-argument]
96 | # revealed: Unknown
97 | reveal_type(super())
| ^^^^^^^
99 | return self
98 | return self
|
info: Type variable `S` has constraints `int, str`
info: `int | str` is not an instance or subclass of `<class 'Foo'>`
@ -201,13 +200,13 @@ info: rule `invalid-super-argument` is enabled by default
```
error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(<class 'Foo'>, S@method10)` call
--> src/mdtest_snippet.py:104:21
--> src/mdtest_snippet.py:103:21
|
102 | # error: [invalid-super-argument]
103 | # revealed: Unknown
104 | reveal_type(super())
101 | # error: [invalid-super-argument]
102 | # revealed: Unknown
103 | reveal_type(super())
| ^^^^^^^
105 | return self
104 | return self
|
info: Type variable `S` has upper bound `(...) -> str`
info: rule `invalid-super-argument` is enabled by default

View file

@ -88,6 +88,8 @@ class C:
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
self.FINAL_E: Final
# TODO: Should not be an error
# error: [invalid-assignment] "Cannot assign to final attribute `FINAL_E` on type `Self@__init__`"
self.FINAL_E = 1
reveal_type(C.FINAL_A) # revealed: int
@ -184,6 +186,7 @@ class C(metaclass=Meta):
self.INSTANCE_FINAL_A: Final[int] = 1
self.INSTANCE_FINAL_B: Final = 1
self.INSTANCE_FINAL_C: Final[int]
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `Self@__init__`"
self.INSTANCE_FINAL_C = 1
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
@ -278,6 +281,8 @@ class C:
def __init__(self):
self.LEGAL_H: Final[int] = 1
self.LEGAL_I: Final[int]
# TODO: Should not be an error
# error: [invalid-assignment]
self.LEGAL_I = 1
# error: [invalid-type-form] "`Final` is not allowed in function parameter annotations"
@ -390,6 +395,8 @@ class C:
DEFINED_IN_INIT: Final[int]
def __init__(self):
# TODO: should not be an error
# error: [invalid-assignment]
self.DEFINED_IN_INIT = 1
```

View file

@ -206,7 +206,7 @@ enum ReduceResult<'db> {
//
// For now (until we solve https://github.com/astral-sh/ty/issues/957), keep this number
// below 200, which is the salsa fixpoint iteration limit.
const MAX_UNION_LITERALS: usize = 199;
const MAX_UNION_LITERALS: usize = 190;
pub(crate) struct UnionBuilder<'db> {
elements: Vec<UnionElement<'db>>,

View file

@ -80,11 +80,11 @@ pub(crate) fn bind_typevar<'db>(
/// Create a `typing.Self` type variable for a given class.
pub(crate) fn typing_self<'db>(
db: &'db dyn Db,
scope_id: ScopeId,
function_scope_id: ScopeId,
typevar_binding_context: Option<Definition<'db>>,
class: ClassLiteral<'db>,
) -> Option<Type<'db>> {
let index = semantic_index(db, scope_id.file(db));
let index = semantic_index(db, function_scope_id.file(db));
let identity = TypeVarIdentity::new(
db,
@ -110,7 +110,7 @@ pub(crate) fn typing_self<'db>(
bind_typevar(
db,
index,
scope_id.file_scope_id(db),
function_scope_id.file_scope_id(db),
typevar_binding_context,
typevar,
)

View file

@ -81,7 +81,7 @@ use crate::types::function::{
};
use crate::types::generics::{
GenericContext, InferableTypeVars, LegacyGenericBase, SpecializationBuilder, bind_typevar,
enclosing_generic_contexts,
enclosing_generic_contexts, typing_self,
};
use crate::types::infer::nearest_enclosing_function;
use crate::types::instance::SliceLiteral;
@ -2495,6 +2495,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} else {
let ty = if let Some(default_ty) = default_ty {
UnionType::from_elements(self.db(), [Type::unknown(), default_ty])
} else if let Some(ty) = self.special_first_method_parameter_type(parameter) {
ty
} else {
Type::unknown()
};
@ -2535,6 +2537,65 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
/// Special case for unannotated `cls` and `self` arguments to class methods and instance methods.
fn special_first_method_parameter_type(
&mut self,
parameter: &ast::Parameter,
) -> Option<Type<'db>> {
let db = self.db();
let file = self.file();
let function_scope_id = self.scope();
let function_scope = function_scope_id.scope(db);
let function = function_scope.node().as_function()?;
let parent_file_scope_id = function_scope.parent()?;
let mut parent_scope_id = parent_file_scope_id.to_scope_id(db, file);
// Skip type parameter scopes, if the method itself is generic.
if parent_scope_id.is_annotation(db) {
let parent_scope = parent_scope_id.scope(db);
parent_scope_id = parent_scope.parent()?.to_scope_id(db, file);
}
// Return early if this is not a method inside a class.
let class = parent_scope_id.scope(db).node().as_class()?;
let method_definition = self.index.expect_single_definition(function);
let DefinitionKind::Function(function_definition) = method_definition.kind(db) else {
return None;
};
if function_definition
.node(self.module())
.parameters
.index(parameter.name())
.is_none_or(|index| index != 0)
{
return None;
}
let method = infer_definition_types(db, method_definition)
.declaration_type(method_definition)
.inner_type()
.as_function_literal()?;
if method.is_classmethod(db) {
// TODO: set the type for `cls` argument
return None;
} else if method.is_staticmethod(db) {
return None;
}
let class_definition = self.index.expect_single_definition(class);
let class_literal = infer_definition_types(db, class_definition)
.declaration_type(class_definition)
.inner_type()
.as_class_literal()?;
typing_self(db, self.scope(), Some(method_definition), class_literal)
}
/// Set initial declared/inferred types for a `*args` variadic positional parameter.
///
/// The annotated type is implicitly wrapped in a string-keyed dictionary.

View file

@ -139,7 +139,7 @@ class FuzzResult:
case Executable.TY:
panic_message = f"The following code triggers a {new}ty panic:"
case _ as unreachable:
assert_never(unreachable) # ty: ignore[type-assertion-failure]
assert_never(unreachable)
print(colored(panic_message, "red"))
print()