mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[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:
parent
cb60ecef6b
commit
9d98a66f65
1 changed files with 116 additions and 12 deletions
|
@ -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
|
||||
|
||||
class C:
|
||||
# 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]
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue