Skip to content

Commit 3a7eb08

Browse files
committed
Add tests for possibly-undefined cases
1 parent 6d40d0e commit 3a7eb08

File tree

4 files changed

+501
-38
lines changed

4 files changed

+501
-38
lines changed

crates/ty_python_semantic/resources/mdtest/final.md

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,31 @@ class ChildOfBad(Bad):
292292

293293
## Edge case: the function is decorated with `@final` but originally defined elsewhere
294294

295-
As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not:
295+
As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and
296+
pyright to emit a diagnostic, the superclass definition decorated with `@final` must be a literal
297+
function definition: an assignment definition where the right-hand side of the assignment is a
298+
`@final-decorated` function is not sufficient for them to consider the superclass definition as
299+
being `@final`.
300+
301+
For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility
302+
with other type checkers. We may decide to change this in the future, however, as it would simplify
303+
our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their
304+
treatment of other type qualifiers such as `Final`. As discussed in
305+
<https://discuss.python.org/t/imported-final-variable/82429>, both type checkers view the `Final`
306+
type qualifier as travelling *across* scopes.
296307

297308
```py
298309
from typing import final
299310

300311
class A:
301312
@final
302-
def method(self): ...
313+
def method(self) -> None: ...
303314

304315
class B:
305316
method = A.method
306317

307318
class C(B):
308-
def method(self): ... # no diagnostic
319+
def method(self) -> None: ... # no diagnostic here (see prose discussion above)
309320
```
310321

311322
## Constructor methods are also checked
@@ -378,3 +389,77 @@ class D(B): # error: [subclass-of-final-class]
378389
# TODO: we should emit a diagnostic here
379390
def method(self): ...
380391
```
392+
393+
## An `@final` method is overridden by an implicit instance attribute
394+
395+
```py
396+
from typing import final, Any
397+
398+
class Parent:
399+
@final
400+
def method(self) -> None: ...
401+
402+
class Child(Parent):
403+
def __init__(self) -> None:
404+
self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here
405+
```
406+
407+
## A possibly-undefined `@final` method is overridden
408+
409+
<!-- snapshot-diagnostics -->
410+
411+
```py
412+
from typing import final
413+
414+
def coinflip() -> bool:
415+
return False
416+
417+
class A:
418+
if coinflip():
419+
@final
420+
def method1(self) -> None: ...
421+
else:
422+
def method1(self) -> None: ...
423+
424+
if coinflip():
425+
def method2(self) -> None: ...
426+
else:
427+
@final
428+
def method2(self) -> None: ...
429+
430+
if coinflip():
431+
@final
432+
def method3(self) -> None: ...
433+
else:
434+
@final
435+
def method3(self) -> None: ...
436+
437+
if coinflip():
438+
def method4(self) -> None: ...
439+
elif coinflip():
440+
@final
441+
def method4(self) -> None: ...
442+
else:
443+
def method4(self) -> None: ...
444+
445+
class B(A):
446+
def method1(self) -> None: ... # error: [override-of-final-method]
447+
def method2(self) -> None: ... # error: [override-of-final-method]
448+
def method3(self) -> None: ... # error: [override-of-final-method]
449+
def method4(self) -> None: ... # error: [override-of-final-method]
450+
451+
# Possible overrides of possibly `@final` methods...
452+
class C(A):
453+
if coinflip():
454+
# TODO: the autofix here introduces invalid syntax because there are now no
455+
# statements inside the `if:` branch
456+
# (but it might still be a useful autofix in an IDE context?)
457+
def method1(self) -> None: ... # error: [override-of-final-method]
458+
else:
459+
pass
460+
461+
if coinflip():
462+
def method2(self) -> None: ... # error: [override-of-final-method]
463+
def method3(self) -> None: ... # error: [override-of-final-method]
464+
def method4(self) -> None: ... # error: [override-of-final-method]
465+
```

0 commit comments

Comments
 (0)