diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md index ce2ae851e1..f52f1b45cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/final.md @@ -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[, @Todo(Inference of subscript on special form), ] +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