[ty] Implicit instance attributes declared Final (#19462)

## Summary

Adds proper type inference for implicit instance attributes that are
declared with a "bare" `Final` and adds `invalid-assignment` diagnostics
for all implicit instance attributes that are declared `Final` or
`Final[…]`.

## Test Plan

New and updated MD tests.

## Ecosystem analysis

```diff
pytest (https://github.com/pytest-dev/pytest)
+ error[invalid-return-type] src/_pytest/fixtures.py:1662:24: Return type does not match returned value: expected `Scope`, found `Scope | (Unknown & ~None & ~((...) -> object) & ~str) | (((str, Config, /) -> Unknown) & ~((...) -> object) & ~str) | (Unknown & ~str)
```

The definition of the `scope` attribute is [here](

5f99385635/src/_pytest/fixtures.py (L1020-L1028)).
Looks like this is a new false positive due to missing `TypeAlias`
support that is surfaced here because we now infer a more precise type
for `FixtureDef._scope`.
This commit is contained in:
David Peter 2025-07-21 20:01:07 +02:00 committed by GitHub
parent dc66019fbc
commit b8dec79182
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 111 additions and 66 deletions

View file

@ -19,6 +19,10 @@ 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
@ -87,6 +91,8 @@ class C:
def __init__(self):
self.FINAL_C: Final[int] = 1
self.FINAL_D: Final = 1
self.FINAL_E: Final
self.FINAL_E = 1
reveal_type(C.FINAL_A) # revealed: int
reveal_type(C.FINAL_B) # revealed: Literal[1]
@ -94,8 +100,8 @@ reveal_type(C.FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_A) # revealed: int
reveal_type(C().FINAL_B) # revealed: Literal[1]
reveal_type(C().FINAL_C) # revealed: int
# TODO: this should be `Literal[1]`
reveal_type(C().FINAL_D) # revealed: Unknown
reveal_type(C().FINAL_D) # revealed: Literal[1]
reveal_type(C().FINAL_E) # revealed: Literal[1]
```
## Not modifiable
@ -181,6 +187,8 @@ class C(metaclass=Meta):
def __init__(self):
self.INSTANCE_FINAL_A: Final[int] = 1
self.INSTANCE_FINAL_B: Final = 1
self.INSTANCE_FINAL_C: Final[int]
self.INSTANCE_FINAL_C = 1
# error: [invalid-assignment] "Cannot assign to final attribute `META_FINAL_A` on type `<class 'C'>`"
C.META_FINAL_A = 2
@ -197,10 +205,12 @@ c = C()
c.CLASS_FINAL_A = 2
# error: [invalid-assignment] "Cannot assign to final attribute `CLASS_FINAL_B` on type `C`"
c.CLASS_FINAL_B = 2
# TODO: this should be an error
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_A` on type `C`"
c.INSTANCE_FINAL_A = 2
# TODO: this should be an error
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_B` on type `C`"
c.INSTANCE_FINAL_B = 2
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `C`"
c.INSTANCE_FINAL_C = 2
```
## Mutability