Skip to content

Commit d83d7a0

Browse files
authored
[ty] Fix false-positive diagnostics on super() calls (#20814)
1 parent 565dbf3 commit d83d7a0

File tree

6 files changed

+977
-125
lines changed

6 files changed

+977
-125
lines changed

crates/ty_python_semantic/resources/mdtest/class/super.md

Lines changed: 209 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ common usage.
1414

1515
### Explicit Super Object
1616

17+
<!-- snapshot-diagnostics -->
18+
1719
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
1820
specified pivot class.
1921

22+
```toml
23+
[environment]
24+
python-version = "3.12"
25+
```
26+
2027
```py
2128
class A:
2229
def a(self): ...
@@ -34,34 +41,96 @@ reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, <class 'A'>,
3441

3542
super(C, C()).a
3643
super(C, C()).b
37-
# error: [unresolved-attribute] "Type `<super: <class 'C'>, C>` has no attribute `c`"
38-
super(C, C()).c
44+
super(C, C()).c # error: [unresolved-attribute]
3945

4046
super(B, C()).a
41-
# error: [unresolved-attribute] "Type `<super: <class 'B'>, C>` has no attribute `b`"
42-
super(B, C()).b
43-
# error: [unresolved-attribute] "Type `<super: <class 'B'>, C>` has no attribute `c`"
44-
super(B, C()).c
45-
46-
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `a`"
47-
super(A, C()).a
48-
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `b`"
49-
super(A, C()).b
50-
# error: [unresolved-attribute] "Type `<super: <class 'A'>, C>` has no attribute `c`"
51-
super(A, C()).c
47+
super(B, C()).b # error: [unresolved-attribute]
48+
super(B, C()).c # error: [unresolved-attribute]
49+
50+
super(A, C()).a # error: [unresolved-attribute]
51+
super(A, C()).b # error: [unresolved-attribute]
52+
super(A, C()).c # error: [unresolved-attribute]
5253

5354
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
5455
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
5556
reveal_type(super(C, C()).aa) # revealed: int
5657
reveal_type(super(C, C()).bb) # revealed: int
5758
```
5859

60+
Examples of explicit `super()` with unusual types. We allow almost any type to be passed as the
61+
second argument to `super()` -- the only exceptions are "pure abstract" types such as `Callable` and
62+
synthesized `Protocol`s that cannot be upcast to, or interpreted as, a non-`object` nominal type.
63+
64+
```py
65+
import types
66+
from typing_extensions import Callable, TypeIs, Literal, TypedDict
67+
68+
def f(): ...
69+
70+
class Foo[T]:
71+
def method(self): ...
72+
@property
73+
def some_property(self): ...
74+
75+
type Alias = int
76+
77+
class SomeTypedDict(TypedDict):
78+
x: int
79+
y: bytes
80+
81+
# revealed: <super: <class 'object'>, FunctionType>
82+
reveal_type(super(object, f))
83+
# revealed: <super: <class 'object'>, WrapperDescriptorType>
84+
reveal_type(super(object, types.FunctionType.__get__))
85+
# revealed: <super: <class 'object'>, GenericAlias>
86+
reveal_type(super(object, Foo[int]))
87+
# revealed: <super: <class 'object'>, _SpecialForm>
88+
reveal_type(super(object, Literal))
89+
# revealed: <super: <class 'object'>, TypeAliasType>
90+
reveal_type(super(object, Alias))
91+
# revealed: <super: <class 'object'>, MethodType>
92+
reveal_type(super(object, Foo().method))
93+
# revealed: <super: <class 'object'>, property>
94+
reveal_type(super(object, Foo.some_property))
95+
96+
def g(x: object) -> TypeIs[list[object]]:
97+
return isinstance(x, list)
98+
99+
def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]):
100+
if hasattr(x, "bar"):
101+
# revealed: <Protocol with members 'bar'>
102+
reveal_type(x)
103+
# error: [invalid-super-argument]
104+
# revealed: Unknown
105+
reveal_type(super(object, x))
106+
107+
# error: [invalid-super-argument]
108+
# revealed: Unknown
109+
reveal_type(super(object, z))
110+
111+
is_list = g(x)
112+
# revealed: TypeIs[list[object] @ x]
113+
reveal_type(is_list)
114+
# revealed: <super: <class 'object'>, bool>
115+
reveal_type(super(object, is_list))
116+
117+
# revealed: <super: <class 'object'>, dict[Literal["x", "y"], int | bytes]>
118+
reveal_type(super(object, y))
119+
```
120+
59121
### Implicit Super Object
60122

123+
<!-- snapshot-diagnostics -->
124+
61125
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
62126
to the class that contains the function where `super()` is used. The first argument refers to the
63127
current method’s first parameter (typically `self` or `cls`).
64128

129+
```toml
130+
[environment]
131+
python-version = "3.12"
132+
```
133+
65134
```py
66135
from __future__ import annotations
67136

@@ -74,6 +143,7 @@ class B(A):
74143
def __init__(self, a: int):
75144
# TODO: Once `Self` is supported, this should be `<super: <class 'B'>, B>`
76145
reveal_type(super()) # revealed: <super: <class 'B'>, Unknown>
146+
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
77147
super().__init__(a)
78148

79149
@classmethod
@@ -86,6 +156,123 @@ super(B, B(42)).__init__(42)
86156
super(B, B).f()
87157
```
88158

159+
Some examples with unusual annotations for `self` or `cls`:
160+
161+
```py
162+
import enum
163+
from typing import Any, Self, Never, Protocol, Callable
164+
from ty_extensions import Intersection
165+
166+
class BuilderMeta(type):
167+
def __new__(
168+
cls: type[Any],
169+
name: str,
170+
bases: tuple[type, ...],
171+
dct: dict[str, Any],
172+
) -> BuilderMeta:
173+
# revealed: <super: <class 'BuilderMeta'>, Any>
174+
s = reveal_type(super())
175+
# revealed: Any
176+
return reveal_type(s.__new__(cls, name, bases, dct))
177+
178+
class BuilderMeta2(type):
179+
def __new__(
180+
cls: type[BuilderMeta2],
181+
name: str,
182+
bases: tuple[type, ...],
183+
dct: dict[str, Any],
184+
) -> BuilderMeta2:
185+
# revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
186+
s = reveal_type(super())
187+
# TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
188+
# revealed: Unknown
189+
return reveal_type(s.__new__(cls, name, bases, dct))
190+
191+
class Foo[T]:
192+
x: T
193+
194+
def method(self: Any):
195+
reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
196+
197+
if isinstance(self, Foo):
198+
reveal_type(super()) # revealed: <super: <class 'Foo'>, Any>
199+
200+
def method2(self: Foo[T]):
201+
# revealed: <super: <class 'Foo'>, Foo[T@Foo]>
202+
reveal_type(super())
203+
204+
def method3(self: Foo):
205+
# revealed: <super: <class 'Foo'>, Foo[Unknown]>
206+
reveal_type(super())
207+
208+
def method4(self: Self):
209+
# revealed: <super: <class 'Foo'>, Foo[T@Foo]>
210+
reveal_type(super())
211+
212+
def method5[S: Foo[int]](self: S, other: S) -> S:
213+
# revealed: <super: <class 'Foo'>, Foo[int]>
214+
reveal_type(super())
215+
return self
216+
217+
def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
218+
# revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
219+
reveal_type(super())
220+
return self
221+
222+
def method7[S](self: S, other: S) -> S:
223+
# error: [invalid-super-argument]
224+
# revealed: Unknown
225+
reveal_type(super())
226+
return self
227+
228+
def method8[S: int](self: S, other: S) -> S:
229+
# error: [invalid-super-argument]
230+
# revealed: Unknown
231+
reveal_type(super())
232+
return self
233+
234+
def method9[S: (int, str)](self: S, other: S) -> S:
235+
# error: [invalid-super-argument]
236+
# revealed: Unknown
237+
reveal_type(super())
238+
return self
239+
240+
def method10[S: Callable[..., str]](self: S, other: S) -> S:
241+
# error: [invalid-super-argument]
242+
# revealed: Unknown
243+
reveal_type(super())
244+
return self
245+
246+
type Alias = Bar
247+
248+
class Bar:
249+
def method(self: Alias):
250+
# revealed: <super: <class 'Bar'>, Bar>
251+
reveal_type(super())
252+
253+
def pls_dont_call_me(self: Never):
254+
# revealed: <super: <class 'Bar'>, Unknown>
255+
reveal_type(super())
256+
257+
def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
258+
# revealed: <super: <class 'Bar'>, Bar>
259+
reveal_type(super())
260+
261+
class P(Protocol):
262+
def method(self: P):
263+
# revealed: <super: <class 'P'>, P>
264+
reveal_type(super())
265+
266+
class E(enum.Enum):
267+
X = 1
268+
269+
def method(self: E):
270+
match self:
271+
case E.X:
272+
# revealed: <super: <class 'E'>, E>
273+
reveal_type(super())
274+
```
275+
89276
### Unbound Super Object
90277

91278
Calling `super(cls)` without a second argument returns an _unbound super object_. This is treated as
@@ -167,11 +354,19 @@ class A:
167354
## Built-ins and Literals
168355

169356
```py
357+
from enum import Enum
358+
170359
reveal_type(super(bool, True)) # revealed: <super: <class 'bool'>, bool>
171360
reveal_type(super(bool, bool())) # revealed: <super: <class 'bool'>, bool>
172361
reveal_type(super(int, bool())) # revealed: <super: <class 'int'>, bool>
173362
reveal_type(super(int, 3)) # revealed: <super: <class 'int'>, int>
174363
reveal_type(super(str, "")) # revealed: <super: <class 'str'>, str>
364+
reveal_type(super(bytes, b"")) # revealed: <super: <class 'bytes'>, bytes>
365+
366+
class E(Enum):
367+
X = 42
368+
369+
reveal_type(super(E, E.X)) # revealed: <super: <class 'E'>, E>
175370
```
176371

177372
## Descriptor Behavior with Super
@@ -342,7 +537,7 @@ def f(x: int):
342537
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
343538
super(IntAlias, 0)
344539

345-
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, Literal[""])` call"
540+
# error: [invalid-super-argument] "`str` is not an instance or subclass of `<class 'int'>` in `super(<class 'int'>, str)` call"
346541
# revealed: Unknown
347542
reveal_type(super(int, str()))
348543

0 commit comments

Comments
 (0)