-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[ty] Extend Final test suite
#19476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ty] Extend Final test suite
#19476
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Comment on lines
+295
to
+296
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how does from typing import Final
class Foo:
def __new__(cls):
self = object.__new__(cls)
self.x: Final = 42
return selfit's basically the pattern that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec says (emphasis mine):
In your code snippet in particular, any declaration would be illegal in that position. You can't annotate attribute assignments if they don't refer to the first parameter in a method (ty does not yet enforce this, see astral-sh/ty#509).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, I think there might be value in special-casing |
||
|
|
||
| ```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,39 +326,58 @@ 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 | ||
|
|
||
| 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.