Skip to content

Commit 59c2102

Browse files
committed
[ty] Enforce Final attribute assignment rules for annotated and augmented writes
1 parent 055fb30 commit 59c2102

File tree

5 files changed

+386
-114
lines changed

5 files changed

+386
-114
lines changed

crates/ty_python_semantic/resources/mdtest/attributes.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,43 @@ class C:
256256
reveal_type(C().w) # revealed: Unknown | Weird
257257
```
258258

259+
#### Ecosystem regression: pip SSL context options
260+
261+
```py
262+
from unknown_module import unknown # type: ignore
263+
264+
class SSLContext:
265+
options: int = 0
266+
267+
def create_context() -> None:
268+
context = SSLContext() if unknown else unknown
269+
options = 0
270+
options |= unknown
271+
context.options |= options
272+
```
273+
274+
#### Ecosystem regression: spack Mach-O header size sync
275+
276+
```py
277+
from unknown_module import unknown # type: ignore
278+
279+
class Header:
280+
sizeofcmds: int = 0
281+
282+
class Holder:
283+
def __init__(self) -> None:
284+
self.header = None
285+
self.load()
286+
287+
def load(self) -> None:
288+
self.header = Header() if unknown else unknown
289+
290+
def synchronize_size(self) -> None:
291+
if self.header is None:
292+
return
293+
self.header.sizeofcmds += unknown
294+
```
295+
259296
#### Attributes defined in tuple unpackings
260297

261298
```py

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ c.INSTANCE_FINAL_A = 2
278278
c.INSTANCE_FINAL_B = 2
279279
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_C` on type `C`"
280280
c.INSTANCE_FINAL_C = 2
281+
# error: [invalid-assignment] "Cannot assign to final attribute `INSTANCE_FINAL_A` on type `C`"
282+
c.INSTANCE_FINAL_A += 1
281283
```
282284

283285
## Mutability
@@ -624,7 +626,7 @@ from typing import Final
624626

625627
class C:
626628
def some_method(self):
627-
# TODO: This should be an error
629+
# error: [invalid-assignment]
628630
self.x: Final[int] = 1
629631
```
630632

@@ -889,7 +891,7 @@ python-version = "3.11"
889891
```
890892

891893
```py
892-
from typing import Final, Self
894+
from typing import Final, Generic, Self, TypeVar
893895

894896
class ClassA:
895897
ID4: Final[int] # OK because initialized in __init__
@@ -907,8 +909,17 @@ class ClassB:
907909
def __init__(self): # Without Self annotation
908910
self.ID5 = 1 # Should also be OK
909911

912+
T = TypeVar("T")
913+
914+
class Box(Generic[T]):
915+
value: Final[T]
916+
917+
def __init__(self: Self, value: T):
918+
self.value = value
919+
910920
reveal_type(ClassA().ID4) # revealed: int
911921
reveal_type(ClassB().ID5) # revealed: int
922+
reveal_type(Box(1).value) # revealed: int
912923
```
913924

914925
## Reassignment to Final in `__init__`

crates/ty_python_semantic/src/types/function.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,16 +1452,13 @@ fn is_instance_truthiness<'db>(
14521452
class: ClassLiteral<'db>,
14531453
) -> Truthiness {
14541454
let is_instance = |ty: &Type<'_>| {
1455-
if let Type::NominalInstance(instance) = ty
1456-
&& instance
1455+
ty.as_nominal_instance().is_some_and(|instance| {
1456+
instance
14571457
.class(db)
14581458
.iter_mro(db)
14591459
.filter_map(ClassBase::into_class)
1460-
.any(|c| c.class_literal(db) == class)
1461-
{
1462-
return true;
1463-
}
1464-
false
1460+
.any(|mro_class| mro_class.class_literal(db) == class)
1461+
})
14651462
};
14661463

14671464
let always_true_if = |test: bool| {

0 commit comments

Comments
 (0)