Skip to content

Commit 340aae9

Browse files
authored
[ty] Detect invalid attempts to subclass Protocol[] and Generic[] simultaneously (#22948)
1 parent c85ae85 commit 340aae9

5 files changed

Lines changed: 303 additions & 23 deletions

File tree

crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
3737
pass
3838

3939
class VeryBad(
40-
Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
40+
# error: [invalid-generic-class]
41+
# error: [invalid-generic-class]
42+
Protocol[T1, T2, DefaultStrT, T3],
4143
Generic[T1, T2, DefaultStrT, T3],
4244
): ...
4345
```

crates/ty_python_semantic/resources/mdtest/protocols.md

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,15 @@ T = TypeVar("T")
5353
class Bar0(Protocol[T]):
5454
x: T
5555

56+
# Note that this class definition *will* actually succeed at runtime,
57+
# but is banned by the typing spec anyway
58+
# error: [invalid-generic-class] "Cannot both inherit from subscripted `Protocol` and subscripted `Generic`"
5659
class Bar1(Protocol[T], Generic[T]):
5760
x: T
5861

62+
class Bar1Point5(Protocol, Generic[T]):
63+
x: T
64+
5965
class Bar2[T](Protocol):
6066
x: T
6167

@@ -193,8 +199,8 @@ reveal_mro(Fine) # revealed: (<class 'Fine'>, typing.Protocol, typing.Generic,
193199

194200
class StillFine(Protocol, Generic[T], object): ...
195201
class EvenThis[T](Protocol, object): ...
196-
class OrThis(Protocol[T], Generic[T]): ...
197-
class AndThis(Protocol[T], Generic[T], object): ...
202+
class OrThis(Protocol, Generic[T]): ...
203+
class AndThis(Protocol, Generic[T], object): ...
198204
```
199205

200206
And multiple inheritance from a mix of protocol and non-protocol classes is fine as long as
@@ -282,6 +288,50 @@ static_assert(is_subtype_of(TypeOf[Protocol], typing._ProtocolMeta))
282288
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
283289
```
284290

291+
## Diagnostics and autofixes for `Protocol` classes defined in invalid ways
292+
293+
<!-- snapshot-diagnostics -->
294+
295+
```toml
296+
[environment]
297+
python-version = "3.12"
298+
```
299+
300+
```py
301+
from typing import Protocol, Generic, TypeVar
302+
303+
T = TypeVar("T")
304+
305+
class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class]
306+
307+
# fmt: off
308+
309+
# error: [invalid-generic-class]
310+
class Bar(Protocol[
311+
T,
312+
], Generic[T]): ...
313+
314+
class Spam( # docs
315+
# error: [invalid-generic-class]
316+
Protocol[ # some comment
317+
# another comment
318+
T, # just love my comments
319+
# very well documented code
320+
], # important comma!
321+
# and a newline...
322+
Generic[ # look at this
323+
# wow
324+
T, # wow
325+
# wowwwwwww
326+
] # oof
327+
# another newline?
328+
): ...
329+
330+
# fmt: on
331+
332+
class Foo[T](Protocol[T]): ... # error: [invalid-generic-class]
333+
```
334+
285335
## `typing.Protocol` versus `typing_extensions.Protocol`
286336

287337
`typing.Protocol` and its backport in `typing_extensions` should be treated as exactly equivalent.

crates/ty_python_semantic/resources/mdtest/snapshots/invalid_type_paramet…_-_Invalid_Order_of_Leg…_(eaa359e8d6b3031d).snap

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type
4242
27 | pass
4343
28 |
4444
29 | class VeryBad(
45-
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
46-
31 | Generic[T1, T2, DefaultStrT, T3],
47-
32 | ): ...
45+
30 | # error: [invalid-generic-class]
46+
31 | # error: [invalid-generic-class]
47+
32 | Protocol[T1, T2, DefaultStrT, T3],
48+
33 | Generic[T1, T2, DefaultStrT, T3],
49+
34 | ): ...
4850
```
4951

5052
# Diagnostics
@@ -163,17 +165,42 @@ info: rule `invalid-generic-class` is enabled by default
163165
```
164166

165167
```
166-
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
167-
--> src/mdtest_snippet.py:30:14
168+
error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` and subscripted `Generic`
169+
--> src/mdtest_snippet.py:32:5
170+
|
171+
30 | # error: [invalid-generic-class]
172+
31 | # error: [invalid-generic-class]
173+
32 | Protocol[T1, T2, DefaultStrT, T3],
174+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
175+
33 | Generic[T1, T2, DefaultStrT, T3],
176+
34 | ): ...
168177
|
178+
help: Remove the type parameters from the `Protocol` base
179+
info: rule `invalid-generic-class` is enabled by default
169180
29 | class VeryBad(
170-
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
181+
30 | # error: [invalid-generic-class]
182+
31 | # error: [invalid-generic-class]
183+
- Protocol[T1, T2, DefaultStrT, T3],
184+
32 + Protocol,
185+
33 | Generic[T1, T2, DefaultStrT, T3],
186+
34 | ): ...
187+
note: This is an unsafe fix and may change runtime behavior
188+
189+
```
190+
191+
```
192+
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
193+
--> src/mdtest_snippet.py:32:14
194+
|
195+
30 | # error: [invalid-generic-class]
196+
31 | # error: [invalid-generic-class]
197+
32 | Protocol[T1, T2, DefaultStrT, T3],
171198
| ^^^^^^^^^^^^^^^^^^^^^^^
172199
| |
173200
| Type variables `T2` and `T3` do not have defaults
174201
| Earlier TypeVar `T1` does
175-
31 | Generic[T1, T2, DefaultStrT, T3],
176-
32 | ): ...
202+
33 | Generic[T1, T2, DefaultStrT, T3],
203+
34 | ): ...
177204
|
178205
::: src/mdtest_snippet.py:3:1
179206
|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: protocols.md - Protocols - Diagnostics and autofixes for `Protocol` classes defined in invalid ways
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/protocols.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | from typing import Protocol, Generic, TypeVar
17+
2 |
18+
3 | T = TypeVar("T")
19+
4 |
20+
5 | class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class]
21+
6 |
22+
7 | # fmt: off
23+
8 |
24+
9 | # error: [invalid-generic-class]
25+
10 | class Bar(Protocol[
26+
11 | T,
27+
12 | ], Generic[T]): ...
28+
13 |
29+
14 | class Spam( # docs
30+
15 | # error: [invalid-generic-class]
31+
16 | Protocol[ # some comment
32+
17 | # another comment
33+
18 | T, # just love my comments
34+
19 | # very well documented code
35+
20 | ], # important comma!
36+
21 | # and a newline...
37+
22 | Generic[ # look at this
38+
23 | # wow
39+
24 | T, # wow
40+
25 | # wowwwwwww
41+
26 | ] # oof
42+
27 | # another newline?
43+
28 | ): ...
44+
29 |
45+
30 | # fmt: on
46+
31 |
47+
32 | class Foo[T](Protocol[T]): ... # error: [invalid-generic-class]
48+
```
49+
50+
# Diagnostics
51+
52+
```
53+
error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` and subscripted `Generic`
54+
--> src/mdtest_snippet.py:5:11
55+
|
56+
3 | T = TypeVar("T")
57+
4 |
58+
5 | class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class]
59+
| ^^^^^^^^^^^
60+
6 |
61+
7 | # fmt: off
62+
|
63+
help: Remove the type parameters from the `Protocol` base
64+
info: rule `invalid-generic-class` is enabled by default
65+
2 |
66+
3 | T = TypeVar("T")
67+
4 |
68+
- class Foo(Protocol[T], Generic[T]): ... # error: [invalid-generic-class]
69+
5 + class Foo(Protocol, Generic[T]): ... # error: [invalid-generic-class]
70+
6 |
71+
7 | # fmt: off
72+
8 |
73+
note: This is an unsafe fix and may change runtime behavior
74+
75+
```
76+
77+
```
78+
error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` and subscripted `Generic`
79+
--> src/mdtest_snippet.py:10:11
80+
|
81+
9 | # error: [invalid-generic-class]
82+
10 | class Bar(Protocol[
83+
| ___________^
84+
11 | | T,
85+
12 | | ], Generic[T]): ...
86+
| |_^
87+
13 |
88+
14 | class Spam( # docs
89+
|
90+
help: Remove the type parameters from the `Protocol` base
91+
info: rule `invalid-generic-class` is enabled by default
92+
7 | # fmt: off
93+
8 |
94+
9 | # error: [invalid-generic-class]
95+
- class Bar(Protocol[
96+
- T,
97+
- ], Generic[T]): ...
98+
10 + class Bar(Protocol, Generic[T]): ...
99+
11 |
100+
12 | class Spam( # docs
101+
13 | # error: [invalid-generic-class]
102+
note: This is an unsafe fix and may change runtime behavior
103+
104+
```
105+
106+
```
107+
error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` and subscripted `Generic`
108+
--> src/mdtest_snippet.py:16:3
109+
|
110+
14 | class Spam( # docs
111+
15 | # error: [invalid-generic-class]
112+
16 | / Protocol[ # some comment
113+
17 | | # another comment
114+
18 | | T, # just love my comments
115+
19 | | # very well documented code
116+
20 | | ], # important comma!
117+
| |_^
118+
21 | # and a newline...
119+
22 | Generic[ # look at this
120+
|
121+
help: Remove the type parameters from the `Protocol` base
122+
info: rule `invalid-generic-class` is enabled by default
123+
13 |
124+
14 | class Spam( # docs
125+
15 | # error: [invalid-generic-class]
126+
- Protocol[ # some comment
127+
- # another comment
128+
- T, # just love my comments
129+
- # very well documented code
130+
- ], # important comma!
131+
16 + Protocol, # important comma!
132+
17 | # and a newline...
133+
18 | Generic[ # look at this
134+
19 | # wow
135+
note: This is an unsafe fix and may change runtime behavior
136+
137+
```
138+
139+
```
140+
error[invalid-generic-class]: Cannot both inherit from subscripted `Protocol` and use PEP 695 type variables
141+
--> src/mdtest_snippet.py:32:14
142+
|
143+
30 | # fmt: on
144+
31 |
145+
32 | class Foo[T](Protocol[T]): ... # error: [invalid-generic-class]
146+
| ^^^^^^^^^^^
147+
|
148+
help: Remove the type parameters from the `Protocol` base
149+
info: rule `invalid-generic-class` is enabled by default
150+
29 |
151+
30 | # fmt: on
152+
31 |
153+
- class Foo[T](Protocol[T]): ... # error: [invalid-generic-class]
154+
32 + class Foo[T](Protocol): ... # error: [invalid-generic-class]
155+
note: This is an unsafe fix and may change runtime behavior
156+
157+
```

0 commit comments

Comments
 (0)