[ty] Fix further issues in super() inference logic (#20843)

This commit is contained in:
Alex Waygood 2025-10-14 13:48:47 +01:00 committed by GitHub
parent 441ba20876
commit 9090aead0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 212 additions and 142 deletions

View file

@ -25,6 +25,8 @@ python-version = "3.12"
```
```py
from __future__ import annotations
class A:
def a(self): ...
aa: int = 1
@ -116,6 +118,26 @@ def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]):
# revealed: <super: <class 'object'>, dict[Literal["x", "y"], int | bytes]>
reveal_type(super(object, y))
# The first argument to `super()` must be an actual class object;
# instances of `GenericAlias` are not accepted at runtime:
#
# error: [invalid-super-argument]
# revealed: Unknown
reveal_type(super(list[int], []))
```
`super(pivot_class, owner)` can be called from inside methods, just like single-argument `super()`:
```py
class Super:
def method(self) -> int:
return 42
class Sub(Super):
def method(self: Sub) -> int:
# revealed: <super: <class 'Sub'>, Sub>
return reveal_type(super(self.__class__, self)).method()
```
### Implicit Super Object

View file

@ -12,104 +12,121 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
## mdtest_snippet.py
```
1 | class A:
2 | def a(self): ...
3 | aa: int = 1
4 |
5 | class B(A):
6 | def b(self): ...
7 | bb: int = 2
8 |
9 | class C(B):
10 | def c(self): ...
11 | cc: int = 3
12 |
13 | reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>]
14 |
15 | super(C, C()).a
16 | super(C, C()).b
17 | super(C, C()).c # error: [unresolved-attribute]
18 |
19 | super(B, C()).a
20 | super(B, C()).b # error: [unresolved-attribute]
21 | super(B, C()).c # error: [unresolved-attribute]
22 |
23 | super(A, C()).a # error: [unresolved-attribute]
24 | super(A, C()).b # error: [unresolved-attribute]
25 | super(A, C()).c # error: [unresolved-attribute]
26 |
27 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
28 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
29 | reveal_type(super(C, C()).aa) # revealed: int
30 | reveal_type(super(C, C()).bb) # revealed: int
31 | import types
32 | from typing_extensions import Callable, TypeIs, Literal, TypedDict
33 |
34 | def f(): ...
35 |
36 | class Foo[T]:
37 | def method(self): ...
38 | @property
39 | def some_property(self): ...
40 |
41 | type Alias = int
42 |
43 | class SomeTypedDict(TypedDict):
44 | x: int
45 | y: bytes
46 |
47 | # revealed: <super: <class 'object'>, FunctionType>
48 | reveal_type(super(object, f))
49 | # revealed: <super: <class 'object'>, WrapperDescriptorType>
50 | reveal_type(super(object, types.FunctionType.__get__))
51 | # revealed: <super: <class 'object'>, GenericAlias>
52 | reveal_type(super(object, Foo[int]))
53 | # revealed: <super: <class 'object'>, _SpecialForm>
54 | reveal_type(super(object, Literal))
55 | # revealed: <super: <class 'object'>, TypeAliasType>
56 | reveal_type(super(object, Alias))
57 | # revealed: <super: <class 'object'>, MethodType>
58 | reveal_type(super(object, Foo().method))
59 | # revealed: <super: <class 'object'>, property>
60 | reveal_type(super(object, Foo.some_property))
61 |
62 | def g(x: object) -> TypeIs[list[object]]:
63 | return isinstance(x, list)
64 |
65 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]):
66 | if hasattr(x, "bar"):
67 | # revealed: <Protocol with members 'bar'>
68 | reveal_type(x)
69 | # error: [invalid-super-argument]
70 | # revealed: Unknown
71 | reveal_type(super(object, x))
72 |
73 | # error: [invalid-super-argument]
74 | # revealed: Unknown
75 | reveal_type(super(object, z))
76 |
77 | is_list = g(x)
78 | # revealed: TypeIs[list[object] @ x]
79 | reveal_type(is_list)
80 | # revealed: <super: <class 'object'>, bool>
81 | reveal_type(super(object, is_list))
82 |
83 | # revealed: <super: <class 'object'>, dict[Literal["x", "y"], int | bytes]>
84 | reveal_type(super(object, y))
1 | from __future__ import annotations
2 |
3 | class A:
4 | def a(self): ...
5 | aa: int = 1
6 |
7 | class B(A):
8 | def b(self): ...
9 | bb: int = 2
10 |
11 | class C(B):
12 | def c(self): ...
13 | cc: int = 3
14 |
15 | reveal_type(C.__mro__) # revealed: tuple[<class 'C'>, <class 'B'>, <class 'A'>, <class 'object'>]
16 |
17 | super(C, C()).a
18 | super(C, C()).b
19 | super(C, C()).c # error: [unresolved-attribute]
20 |
21 | super(B, C()).a
22 | super(B, C()).b # error: [unresolved-attribute]
23 | super(B, C()).c # error: [unresolved-attribute]
24 |
25 | super(A, C()).a # error: [unresolved-attribute]
26 | super(A, C()).b # error: [unresolved-attribute]
27 | super(A, C()).c # error: [unresolved-attribute]
28 |
29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
30 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
31 | reveal_type(super(C, C()).aa) # revealed: int
32 | reveal_type(super(C, C()).bb) # revealed: int
33 | import types
34 | from typing_extensions import Callable, TypeIs, Literal, TypedDict
35 |
36 | def f(): ...
37 |
38 | class Foo[T]:
39 | def method(self): ...
40 | @property
41 | def some_property(self): ...
42 |
43 | type Alias = int
44 |
45 | class SomeTypedDict(TypedDict):
46 | x: int
47 | y: bytes
48 |
49 | # revealed: <super: <class 'object'>, FunctionType>
50 | reveal_type(super(object, f))
51 | # revealed: <super: <class 'object'>, WrapperDescriptorType>
52 | reveal_type(super(object, types.FunctionType.__get__))
53 | # revealed: <super: <class 'object'>, GenericAlias>
54 | reveal_type(super(object, Foo[int]))
55 | # revealed: <super: <class 'object'>, _SpecialForm>
56 | reveal_type(super(object, Literal))
57 | # revealed: <super: <class 'object'>, TypeAliasType>
58 | reveal_type(super(object, Alias))
59 | # revealed: <super: <class 'object'>, MethodType>
60 | reveal_type(super(object, Foo().method))
61 | # revealed: <super: <class 'object'>, property>
62 | reveal_type(super(object, Foo.some_property))
63 |
64 | def g(x: object) -> TypeIs[list[object]]:
65 | return isinstance(x, list)
66 |
67 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]):
68 | if hasattr(x, "bar"):
69 | # revealed: <Protocol with members 'bar'>
70 | reveal_type(x)
71 | # error: [invalid-super-argument]
72 | # revealed: Unknown
73 | reveal_type(super(object, x))
74 |
75 | # error: [invalid-super-argument]
76 | # revealed: Unknown
77 | reveal_type(super(object, z))
78 |
79 | is_list = g(x)
80 | # revealed: TypeIs[list[object] @ x]
81 | reveal_type(is_list)
82 | # revealed: <super: <class 'object'>, bool>
83 | reveal_type(super(object, is_list))
84 |
85 | # revealed: <super: <class 'object'>, dict[Literal["x", "y"], int | bytes]>
86 | reveal_type(super(object, y))
87 |
88 | # The first argument to `super()` must be an actual class object;
89 | # instances of `GenericAlias` are not accepted at runtime:
90 | #
91 | # error: [invalid-super-argument]
92 | # revealed: Unknown
93 | reveal_type(super(list[int], []))
94 | class Super:
95 | def method(self) -> int:
96 | return 42
97 |
98 | class Sub(Super):
99 | def method(self: Sub) -> int:
100 | # revealed: <super: <class 'Sub'>, Sub>
101 | return reveal_type(super(self.__class__, self)).method()
```
# Diagnostics
```
error[unresolved-attribute]: Type `<super: <class 'C'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:17:1
--> src/mdtest_snippet.py:19:1
|
15 | super(C, C()).a
16 | super(C, C()).b
17 | super(C, C()).c # error: [unresolved-attribute]
17 | super(C, C()).a
18 | super(C, C()).b
19 | super(C, C()).c # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
18 |
19 | super(B, C()).a
20 |
21 | super(B, C()).a
|
info: rule `unresolved-attribute` is enabled by default
@ -117,12 +134,12 @@ info: rule `unresolved-attribute` is enabled by default
```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:20:1
--> src/mdtest_snippet.py:22:1
|
19 | super(B, C()).a
20 | super(B, C()).b # error: [unresolved-attribute]
21 | super(B, C()).a
22 | super(B, C()).b # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
21 | super(B, C()).c # error: [unresolved-attribute]
23 | super(B, C()).c # error: [unresolved-attribute]
|
info: rule `unresolved-attribute` is enabled by default
@ -130,14 +147,14 @@ info: rule `unresolved-attribute` is enabled by default
```
error[unresolved-attribute]: Type `<super: <class 'B'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:21:1
--> src/mdtest_snippet.py:23:1
|
19 | super(B, C()).a
20 | super(B, C()).b # error: [unresolved-attribute]
21 | super(B, C()).c # error: [unresolved-attribute]
21 | super(B, C()).a
22 | super(B, C()).b # error: [unresolved-attribute]
23 | super(B, C()).c # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
22 |
23 | super(A, C()).a # error: [unresolved-attribute]
24 |
25 | super(A, C()).a # error: [unresolved-attribute]
|
info: rule `unresolved-attribute` is enabled by default
@ -145,14 +162,14 @@ info: rule `unresolved-attribute` is enabled by default
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `a`
--> src/mdtest_snippet.py:23:1
--> src/mdtest_snippet.py:25:1
|
21 | super(B, C()).c # error: [unresolved-attribute]
22 |
23 | super(A, C()).a # error: [unresolved-attribute]
23 | super(B, C()).c # error: [unresolved-attribute]
24 |
25 | super(A, C()).a # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
24 | super(A, C()).b # error: [unresolved-attribute]
25 | super(A, C()).c # error: [unresolved-attribute]
26 | super(A, C()).b # error: [unresolved-attribute]
27 | super(A, C()).c # error: [unresolved-attribute]
|
info: rule `unresolved-attribute` is enabled by default
@ -160,12 +177,12 @@ info: rule `unresolved-attribute` is enabled by default
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `b`
--> src/mdtest_snippet.py:24:1
--> src/mdtest_snippet.py:26:1
|
23 | super(A, C()).a # error: [unresolved-attribute]
24 | super(A, C()).b # error: [unresolved-attribute]
25 | super(A, C()).a # error: [unresolved-attribute]
26 | super(A, C()).b # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
25 | super(A, C()).c # error: [unresolved-attribute]
27 | super(A, C()).c # error: [unresolved-attribute]
|
info: rule `unresolved-attribute` is enabled by default
@ -173,14 +190,14 @@ info: rule `unresolved-attribute` is enabled by default
```
error[unresolved-attribute]: Type `<super: <class 'A'>, C>` has no attribute `c`
--> src/mdtest_snippet.py:25:1
--> src/mdtest_snippet.py:27:1
|
23 | super(A, C()).a # error: [unresolved-attribute]
24 | super(A, C()).b # error: [unresolved-attribute]
25 | super(A, C()).c # error: [unresolved-attribute]
25 | super(A, C()).a # error: [unresolved-attribute]
26 | super(A, C()).b # error: [unresolved-attribute]
27 | super(A, C()).c # error: [unresolved-attribute]
| ^^^^^^^^^^^^^^^
26 |
27 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
28 |
29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
info: rule `unresolved-attribute` is enabled by default
@ -188,14 +205,14 @@ info: rule `unresolved-attribute` is enabled by default
```
error[invalid-super-argument]: `<Protocol with members 'bar'>` is an abstract/structural type in `super(<class 'object'>, <Protocol with members 'bar'>)` call
--> src/mdtest_snippet.py:71:21
--> src/mdtest_snippet.py:73:21
|
69 | # error: [invalid-super-argument]
70 | # revealed: Unknown
71 | reveal_type(super(object, x))
71 | # error: [invalid-super-argument]
72 | # revealed: Unknown
73 | reveal_type(super(object, x))
| ^^^^^^^^^^^^^^^^
72 |
73 | # error: [invalid-super-argument]
74 |
75 | # error: [invalid-super-argument]
|
info: rule `invalid-super-argument` is enabled by default
@ -203,14 +220,29 @@ info: rule `invalid-super-argument` is enabled by default
```
error[invalid-super-argument]: `(int, str, /) -> bool` is an abstract/structural type in `super(<class 'object'>, (int, str, /) -> bool)` call
--> src/mdtest_snippet.py:75:17
--> src/mdtest_snippet.py:77:17
|
73 | # error: [invalid-super-argument]
74 | # revealed: Unknown
75 | reveal_type(super(object, z))
75 | # error: [invalid-super-argument]
76 | # revealed: Unknown
77 | reveal_type(super(object, z))
| ^^^^^^^^^^^^^^^^
76 |
77 | is_list = g(x)
78 |
79 | is_list = g(x)
|
info: rule `invalid-super-argument` is enabled by default
```
```
error[invalid-super-argument]: `types.GenericAlias` instance `list[int]` is not a valid class
--> src/mdtest_snippet.py:93:13
|
91 | # error: [invalid-super-argument]
92 | # revealed: Unknown
93 | reveal_type(super(list[int], []))
| ^^^^^^^^^^^^^^^^^^^^
94 | class Super:
95 | def method(self) -> int:
|
info: rule `invalid-super-argument` is enabled by default

View file

@ -162,6 +162,7 @@ error[invalid-super-argument]: `S@method7` is not an instance or subclass of `<c
|
info: Type variable `S` has `object` as its implicit upper bound
info: `object` is not an instance or subclass of `<class 'Foo'>`
help: Consider adding an upper bound to type variable `S`
info: rule `invalid-super-argument` is enabled by default
```

View file

@ -7790,6 +7790,12 @@ pub enum TypeVarKind {
TypingSelf,
}
impl TypeVarKind {
const fn is_self(self) -> bool {
matches!(self, Self::TypingSelf)
}
}
/// The identity of a type variable.
///
/// This represents the core identity of a typevar, independent of its bounds or constraints. Two

View file

@ -5,7 +5,7 @@ use ruff_db::diagnostic::Diagnostic;
use ruff_python_ast::AnyNodeRef;
use crate::{
Db,
Db, DisplaySettings,
place::{Place, PlaceAndQualifiers},
types::{
ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy,
@ -75,10 +75,16 @@ impl<'db> BoundSuperError<'db> {
}
BoundSuperError::InvalidPivotClassType { pivot_class } => {
if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) {
builder.into_diagnostic(format_args!(
"`{pivot_class}` is not a valid class",
pivot_class = pivot_class.display(context.db()),
));
match pivot_class {
Type::GenericAlias(alias) => builder.into_diagnostic(format_args!(
"`types.GenericAlias` instance `{}` is not a valid class",
alias.display_with(context.db(), DisplaySettings::default()),
)),
_ => builder.into_diagnostic(format_args!(
"`{pivot_class}` is not a valid class",
pivot_class = pivot_class.display(context.db()),
)),
};
}
}
BoundSuperError::FailingConditionCheck {
@ -102,6 +108,14 @@ impl<'db> BoundSuperError<'db> {
bound_or_constraints_union.display(context.db()),
pivot_class = pivot_class.display(context.db()),
));
if typevar_context.bound_or_constraints(context.db()).is_none()
&& !typevar_context.kind(context.db()).is_self()
{
diagnostic.help(format_args!(
"Consider adding an upper bound to type variable `{}`",
typevar_context.name(context.db())
));
}
}
}
}
@ -412,15 +426,10 @@ impl<'db> BoundSuperType<'db> {
// but are valid as pivot classes, e.g. unsubscripted `typing.Generic`
let pivot_class = match pivot_class_type {
Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)),
Type::GenericAlias(class) => ClassBase::Class(ClassType::Generic(class)),
Type::SubclassOf(subclass_of) if subclass_of.subclass_of().is_dynamic() => {
ClassBase::Dynamic(
subclass_of
.subclass_of()
.into_dynamic()
.expect("Checked in branch arm"),
)
}
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Class(class) => ClassBase::Class(class),
SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
},
Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol,
Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic,
Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,