Skip to content

Commit b9916af

Browse files
committed
[ty] Implement typing.final for methods
1 parent 2c0c5ff commit b9916af

8 files changed

+1382
-35
lines changed

crates/ty_python_semantic/resources/mdtest/final.md

Lines changed: 253 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tests for the `@typing(_extensions).final` decorator
22

3-
## Cannot subclass
3+
## Cannot subclass a class decorated with `@final`
44

55
Don't do this:
66

@@ -29,3 +29,255 @@ class H(
2929
G,
3030
): ...
3131
```
32+
33+
## Cannot override a method decorated with `@final`
34+
35+
<!-- snapshot-diagnostics -->
36+
37+
```pyi
38+
from typing_extensions import final, Callable, TypeVar
39+
40+
def lossy_decorator(fn: Callable) -> Callable: ...
41+
42+
class Parent:
43+
@final
44+
def foo(self): ...
45+
@final
46+
@property
47+
def my_property1(self) -> int: ...
48+
@property
49+
@final
50+
def my_property2(self) -> int: ...
51+
@final
52+
@classmethod
53+
def class_method1(cls) -> int: ...
54+
@staticmethod
55+
@final
56+
def static_method1() -> int: ...
57+
@final
58+
@classmethod
59+
def class_method2(cls) -> int: ...
60+
@staticmethod
61+
@final
62+
def static_method2() -> int: ...
63+
@lossy_decorator
64+
@final
65+
def decorated_1(self): ...
66+
@final
67+
@lossy_decorator
68+
def decorated_2(self): ...
69+
70+
class Child(Parent):
71+
def foo(self): ... # error: [override-of-final-method]
72+
@property
73+
def my_property1(self) -> int: ... # error: [override-of-final-method]
74+
@property
75+
def my_property2(self) -> int: ... # error: [override-of-final-method]
76+
@classmethod
77+
def class_method1(cls) -> int: ... # error: [override-of-final-method]
78+
@staticmethod
79+
def static_method1() -> int: ... # error: [override-of-final-method]
80+
@classmethod
81+
def class_method2(cls) -> int: ... # error: [override-of-final-method]
82+
@staticmethod
83+
def static_method2() -> int: ... # error: [override-of-final-method]
84+
def decorated_1(self): ... # TODO: should emit [override-of-final-method]
85+
@lossy_decorator
86+
def decorated_2(self): ... # TODO: should emit [override-of-final-method]
87+
88+
class OtherChild(Parent): ...
89+
90+
class Grandchild(OtherChild):
91+
@staticmethod
92+
# TODO: we should emit a Liskov violation here too
93+
# error: [override-of-final-method]
94+
def foo(): ...
95+
@property
96+
# TODO: we should emit a Liskov violation here too
97+
# error: [override-of-final-method]
98+
def my_property1(self) -> str: ...
99+
# TODO: we should emit a Liskov violation here too
100+
# error: [override-of-final-method]
101+
class_method1 = None
102+
103+
# Diagnostic edge case: `final` is very far away from the method definition in the source code:
104+
105+
T = TypeVar("T")
106+
107+
def identity(x: T) -> T: ...
108+
109+
class Foo:
110+
@final
111+
@identity
112+
@identity
113+
@identity
114+
@identity
115+
@identity
116+
@identity
117+
@identity
118+
@identity
119+
@identity
120+
@identity
121+
@identity
122+
@identity
123+
@identity
124+
@identity
125+
@identity
126+
@identity
127+
@identity
128+
@identity
129+
def bar(self): ...
130+
131+
class Baz(Foo):
132+
def bar(self): ... # error: [override-of-final-method]
133+
```
134+
135+
## Diagnostic edge case: superclass with `@final` method has the same name as the subclass
136+
137+
<!-- snapshot-diagnostics -->
138+
139+
`module1.py`:
140+
141+
```py
142+
from typing import final
143+
144+
class Foo:
145+
@final
146+
def f(self): ...
147+
```
148+
149+
`module2.py`:
150+
151+
```py
152+
import module1
153+
154+
class Foo(module1.Foo):
155+
def f(self): ... # error: [override-of-final-method]
156+
```
157+
158+
## Overloaded methods decorated with `@final`
159+
160+
In a stub file, `@final` should be applied to the first overload. In a runtime file, `@final` should
161+
only be applied to the implementation function.
162+
163+
<!-- snapshot-diagnostics -->
164+
165+
`stub.pyi`:
166+
167+
```pyi
168+
from typing import final, overload
169+
170+
class Good:
171+
@overload
172+
@final
173+
def bar(self, x: str) -> str: ...
174+
@overload
175+
def bar(self, x: int) -> int: ...
176+
@final
177+
@overload
178+
def baz(self, x: str) -> str: ...
179+
@overload
180+
def baz(self, x: int) -> int: ...
181+
182+
class ChildOfGood(Good):
183+
@overload
184+
def bar(self, x: str) -> str: ...
185+
@overload
186+
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
187+
@overload
188+
def baz(self, x: str) -> str: ...
189+
@overload
190+
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
191+
192+
class Bad:
193+
@overload
194+
def bar(self, x: str) -> str: ...
195+
@overload
196+
@final
197+
# error: [invalid-overload]
198+
def bar(self, x: int) -> int: ...
199+
@overload
200+
def baz(self, x: str) -> str: ...
201+
@final
202+
@overload
203+
# error: [invalid-overload]
204+
def baz(self, x: int) -> int: ...
205+
206+
class ChildOfBad(Bad):
207+
@overload
208+
def bar(self, x: str) -> str: ...
209+
@overload
210+
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
211+
@overload
212+
def baz(self, x: str) -> str: ...
213+
@overload
214+
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
215+
```
216+
217+
`main.py`:
218+
219+
```py
220+
from typing import overload, final
221+
222+
class Good:
223+
@overload
224+
def f(self, x: str) -> str: ...
225+
@overload
226+
def f(self, x: int) -> int: ...
227+
@final
228+
def f(self, x: int | str) -> int | str:
229+
return x
230+
231+
class ChildOfGood(Good):
232+
@overload
233+
def f(self, x: str) -> str: ...
234+
@overload
235+
def f(self, x: int) -> int: ...
236+
# error: [override-of-final-method]
237+
def f(self, x: int | str) -> int | str:
238+
return x
239+
240+
class Bad:
241+
@overload
242+
@final
243+
def f(self, x: str) -> str: ...
244+
@overload
245+
def f(self, x: int) -> int: ...
246+
# error: [invalid-overload]
247+
def f(self, x: int | str) -> int | str:
248+
return x
249+
250+
@final
251+
@overload
252+
def g(self, x: str) -> str: ...
253+
@overload
254+
def g(self, x: int) -> int: ...
255+
# error: [invalid-overload]
256+
def g(self, x: int | str) -> int | str:
257+
return x
258+
259+
@overload
260+
def h(self, x: str) -> str: ...
261+
@overload
262+
@final
263+
def h(self, x: int) -> int: ...
264+
# error: [invalid-overload]
265+
def h(self, x: int | str) -> int | str:
266+
return x
267+
268+
@overload
269+
def i(self, x: str) -> str: ...
270+
@final
271+
@overload
272+
def i(self, x: int) -> int: ...
273+
# error: [invalid-overload]
274+
def i(self, x: int | str) -> int | str:
275+
return x
276+
277+
class ChildOfBad(Bad):
278+
# TODO: these should all cause us to emit Liskov violations as well
279+
f = None # error: [override-of-final-method]
280+
g = None # error: [override-of-final-method]
281+
h = None # error: [override-of-final-method]
282+
i = None # error: [override-of-final-method]
283+
```

0 commit comments

Comments
 (0)