[ty] Extend Final test suite (#19476)

## Summary

Restructures and cleans up the `typing.Final` test suite. Also adds a
few more tests with TODOs based on the [typing spec for
`typing.Final`](https://typing.python.org/en/latest/spec/qualifiers.html#uppercase-final).
This commit is contained in:
David Peter 2025-07-22 12:06:47 +02:00 committed by GitHub
parent cb60ecef6b
commit 9d98a66f65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -19,10 +19,6 @@ FINAL_A: Final[int] = 1
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
FINAL_D: "Final[int]" = 1
# Note: Some type checkers do not support a separate declaration and
# assignment for `Final` symbols, but it's possible to support this in
# ty, and is useful for code that declares symbols `Final` inside
# `if TYPE_CHECKING` blocks.
FINAL_F: Final[int]
FINAL_F = 1
@ -52,7 +48,7 @@ reveal_type(FINAL_D) # revealed: int
reveal_type(FINAL_F) # revealed: int
```
### `Final` without a type
### Bare `Final` without a type
When a symbol is qualified with `Final` but no type is specified, the type is inferred from the
right-hand side of the assignment. We do not union the inferred type with `Unknown`, because the
@ -231,7 +227,96 @@ FINAL_LIST: Final[list[int]] = [1, 2, 3]
FINAL_LIST[0] = 4
```
## Too many arguments
## Overriding in subclasses
When a symbol is qualified with `Final` in a class, it cannot be overridden in subclasses.
```py
from typing import Final
class Base:
FINAL_A: Final[int] = 1
FINAL_B: Final[int] = 1
FINAL_C: Final = 1
class Derived(Base):
# TODO: This should be an error
FINAL_A = 2
# TODO: This should be an error
FINAL_B: Final[int] = 2
# TODO: This should be an error
FINAL_C = 2
```
## Syntax and usage
### Legal syntactical positions
Final may only be used in assignments or variable annotations. Using it in any other position is an
error.
```py
from typing import Final, ClassVar, Annotated
LEGAL_A: Final[int] = 1
LEGAL_B: Final = 1
LEGAL_C: Final[int]
LEGAL_C = 1
LEGAL_D: Final
LEGAL_D = 1
class C:
LEGAL_E: ClassVar[Final[int]] = 1
LEGAL_F: Final[ClassVar[int]] = 1
LEGAL_G: Annotated[Final[ClassVar[int]], "metadata"] = 1
def __init__(self):
self.LEGAL_H: Final[int] = 1
self.LEGAL_I: Final[int]
self.LEGAL_I = 1
# TODO: This should be an error
def f(ILLEGAL: Final[int]) -> None:
pass
# TODO: This should be an error
def f() -> Final[None]: ...
# TODO: This should be an error
class Foo(Final[tuple[int]]): ...
# TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `Final` and show the MRO as if `Final` was not there
# revealed: tuple[<class 'Foo'>, @Todo(Inference of subscript on special form), <class 'object'>]
reveal_type(Foo.__mro__)
```
### Attribute assignment outside `__init__`
Qualifying an instance attribute with `Final` outside of `__init__` is not allowed. The instance
attribute must be assigned only once, when the instance is created.
```py
from typing import Final
class C:
def some_method(self):
# TODO: This should be an error
self.x: Final[int] = 1
```
### `Final` in loops
Using `Final` in a loop is not allowed.
```py
from typing import Final
for _ in range(10):
# TODO: This should be an error
i: Final[int] = 1
```
### Too many arguments
```py
from typing import Final
@ -241,21 +326,30 @@ class C:
x: Final[int, str] = 1
```
## Illegal `Final` in type expression
### Illegal `Final` in type expression
```py
from typing import Final
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
x: list[Final[int]] = [] # Error!
class C:
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
# error: [invalid-type-form]
x: Final | int
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
# error: [invalid-type-form]
y: int | Final[str]
```
## No assignment
Some type checkers do not support a separate declaration and assignment for `Final` symbols, but
it's possible to support this in ty, and is useful for code that declares symbols `Final` inside
`if TYPE_CHECKING` blocks.
### Basic
```py
from typing import Final
@ -263,17 +357,27 @@ DECLARED_THEN_BOUND: Final[int]
DECLARED_THEN_BOUND = 1
```
## No assignment for bare `Final`
### No assignment
```py
from typing import Final
# TODO: This should be an error
NO_RHS: Final
NO_ASSIGNMENT_A: Final
# TODO: This should be an error
NO_ASSIGNMENT_B: Final[int]
class C:
# TODO: This should be an error
NO_RHS: Final
NO_ASSIGNMENT_A: Final
# TODO: This should be an error
NO_ASSIGNMENT_B: Final[int]
# This is okay. `DEFINED_IN_INIT` is defined in `__init__`.
DEFINED_IN_INIT: Final[int]
def __init__(self):
self.DEFINED_IN_INIT = 1
```
## Full diagnostics