Skip to content

Commit 9d98a66

Browse files
authored
[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).
1 parent cb60ece commit 9d98a66

File tree

1 file changed

+116
-12
lines changed
  • crates/ty_python_semantic/resources/mdtest/type_qualifiers

1 file changed

+116
-12
lines changed

crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ FINAL_A: Final[int] = 1
1919
FINAL_B: Annotated[Final[int], "the annotation for FINAL_B"] = 1
2020
FINAL_C: Final[Annotated[int, "the annotation for FINAL_C"]] = 1
2121
FINAL_D: "Final[int]" = 1
22-
# Note: Some type checkers do not support a separate declaration and
23-
# assignment for `Final` symbols, but it's possible to support this in
24-
# ty, and is useful for code that declares symbols `Final` inside
25-
# `if TYPE_CHECKING` blocks.
2622
FINAL_F: Final[int]
2723
FINAL_F = 1
2824

@@ -52,7 +48,7 @@ reveal_type(FINAL_D) # revealed: int
5248
reveal_type(FINAL_F) # revealed: int
5349
```
5450

55-
### `Final` without a type
51+
### Bare `Final` without a type
5652

5753
When a symbol is qualified with `Final` but no type is specified, the type is inferred from the
5854
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]
231227
FINAL_LIST[0] = 4
232228
```
233229

234-
## Too many arguments
230+
## Overriding in subclasses
231+
232+
When a symbol is qualified with `Final` in a class, it cannot be overridden in subclasses.
233+
234+
```py
235+
from typing import Final
236+
237+
class Base:
238+
FINAL_A: Final[int] = 1
239+
FINAL_B: Final[int] = 1
240+
FINAL_C: Final = 1
241+
242+
class Derived(Base):
243+
# TODO: This should be an error
244+
FINAL_A = 2
245+
# TODO: This should be an error
246+
FINAL_B: Final[int] = 2
247+
# TODO: This should be an error
248+
FINAL_C = 2
249+
```
250+
251+
## Syntax and usage
252+
253+
### Legal syntactical positions
254+
255+
Final may only be used in assignments or variable annotations. Using it in any other position is an
256+
error.
257+
258+
```py
259+
from typing import Final, ClassVar, Annotated
260+
261+
LEGAL_A: Final[int] = 1
262+
LEGAL_B: Final = 1
263+
LEGAL_C: Final[int]
264+
LEGAL_C = 1
265+
LEGAL_D: Final
266+
LEGAL_D = 1
267+
268+
class C:
269+
LEGAL_E: ClassVar[Final[int]] = 1
270+
LEGAL_F: Final[ClassVar[int]] = 1
271+
LEGAL_G: Annotated[Final[ClassVar[int]], "metadata"] = 1
272+
273+
def __init__(self):
274+
self.LEGAL_H: Final[int] = 1
275+
self.LEGAL_I: Final[int]
276+
self.LEGAL_I = 1
277+
278+
# TODO: This should be an error
279+
def f(ILLEGAL: Final[int]) -> None:
280+
pass
281+
282+
# TODO: This should be an error
283+
def f() -> Final[None]: ...
284+
285+
# TODO: This should be an error
286+
class Foo(Final[tuple[int]]): ...
287+
288+
# TODO: Show `Unknown` instead of `@Todo` type in the MRO; or ignore `Final` and show the MRO as if `Final` was not there
289+
# revealed: tuple[<class 'Foo'>, @Todo(Inference of subscript on special form), <class 'object'>]
290+
reveal_type(Foo.__mro__)
291+
```
292+
293+
### Attribute assignment outside `__init__`
294+
295+
Qualifying an instance attribute with `Final` outside of `__init__` is not allowed. The instance
296+
attribute must be assigned only once, when the instance is created.
297+
298+
```py
299+
from typing import Final
300+
301+
class C:
302+
def some_method(self):
303+
# TODO: This should be an error
304+
self.x: Final[int] = 1
305+
```
306+
307+
### `Final` in loops
308+
309+
Using `Final` in a loop is not allowed.
310+
311+
```py
312+
from typing import Final
313+
314+
for _ in range(10):
315+
# TODO: This should be an error
316+
i: Final[int] = 1
317+
```
318+
319+
### Too many arguments
235320

236321
```py
237322
from typing import Final
@@ -241,39 +326,58 @@ class C:
241326
x: Final[int, str] = 1
242327
```
243328

244-
## Illegal `Final` in type expression
329+
### Illegal `Final` in type expression
245330

246331
```py
247332
from typing import Final
248333

334+
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
335+
x: list[Final[int]] = [] # Error!
336+
249337
class C:
250-
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
338+
# error: [invalid-type-form]
251339
x: Final | int
252340

253-
# error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
341+
# error: [invalid-type-form]
254342
y: int | Final[str]
255343
```
256344

257345
## No assignment
258346

347+
Some type checkers do not support a separate declaration and assignment for `Final` symbols, but
348+
it's possible to support this in ty, and is useful for code that declares symbols `Final` inside
349+
`if TYPE_CHECKING` blocks.
350+
351+
### Basic
352+
259353
```py
260354
from typing import Final
261355

262356
DECLARED_THEN_BOUND: Final[int]
263357
DECLARED_THEN_BOUND = 1
264358
```
265359

266-
## No assignment for bare `Final`
360+
### No assignment
267361

268362
```py
269363
from typing import Final
270364

271365
# TODO: This should be an error
272-
NO_RHS: Final
366+
NO_ASSIGNMENT_A: Final
367+
# TODO: This should be an error
368+
NO_ASSIGNMENT_B: Final[int]
273369

274370
class C:
275371
# TODO: This should be an error
276-
NO_RHS: Final
372+
NO_ASSIGNMENT_A: Final
373+
# TODO: This should be an error
374+
NO_ASSIGNMENT_B: Final[int]
375+
376+
# This is okay. `DEFINED_IN_INIT` is defined in `__init__`.
377+
DEFINED_IN_INIT: Final[int]
378+
379+
def __init__(self):
380+
self.DEFINED_IN_INIT = 1
277381
```
278382

279383
## Full diagnostics

0 commit comments

Comments
 (0)