Skip to content

Commit 9cf212f

Browse files
[ty] Normalize property setter and deleter wrappers (#24509)
## Summary Setters and deleters always return `None`, even if the user returns a different type or annotates it with a different type. We now normalize the return type (while retaining `Never` or `NoReturn`, so we can detect invalid deletions).
1 parent 12a1589 commit 9cf212f

2 files changed

Lines changed: 92 additions & 6 deletions

File tree

crates/ty_python_semantic/resources/mdtest/properties.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,52 @@ c.my_property = 2
136136
c.my_property = "a"
137137
```
138138

139+
Direct `property.__set__` and `property.__delete__` calls return `None` for ordinary accessors, but
140+
preserve `Never`/`NoReturn` for typed non-returning accessors:
141+
142+
```py
143+
from typing import Any, NoReturn, cast
144+
145+
def raw_setter(obj: object, value: object) -> int:
146+
return 1
147+
148+
def raw_deleter(obj: object) -> int:
149+
return 1
150+
151+
prop = property(fset=cast(Any, raw_setter), fdel=cast(Any, raw_deleter))
152+
reveal_type(prop.__set__(object(), object())) # revealed: None
153+
reveal_type(property.__set__(prop, object(), object())) # revealed: None
154+
reveal_type(prop.__delete__(object())) # revealed: None
155+
reveal_type(property.__delete__(prop, object())) # revealed: None
156+
157+
class NoReturnSetterAndDeleter:
158+
@property
159+
def x(self) -> int:
160+
return 1
161+
162+
@x.setter
163+
def x(self, value: int) -> NoReturn:
164+
raise RuntimeError
165+
166+
@x.deleter
167+
def x(self) -> NoReturn:
168+
raise RuntimeError
169+
170+
def direct_set() -> None:
171+
reveal_type(NoReturnSetterAndDeleter.x.__set__(NoReturnSetterAndDeleter(), 1)) # revealed: Never
172+
173+
def direct_set_unbound() -> None:
174+
cls = NoReturnSetterAndDeleter
175+
reveal_type(type(cls.x).__set__(cls.x, cls(), 1)) # revealed: Never
176+
177+
def direct_delete() -> None:
178+
reveal_type(NoReturnSetterAndDeleter.x.__delete__(NoReturnSetterAndDeleter())) # revealed: Never
179+
180+
def direct_delete_unbound() -> None:
181+
cls = NoReturnSetterAndDeleter
182+
reveal_type(type(cls.x).__delete__(cls.x, cls())) # revealed: Never
183+
```
184+
139185
## Conditional redefinition in class body
140186

141187
Distinct property definitions in statically unknown class-body branches should remain distinct, the

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,12 +1251,22 @@ impl<'db> Bindings<'db> {
12511251
] = overload.parameter_types()
12521252
{
12531253
if let Some(setter) = property.setter(db) {
1254-
if let Err(_call_error) = setter
1254+
if let Ok(return_ty) = setter
12551255
.try_call(db, &CallArguments::positional([*instance, *value]))
1256+
.map(|binding| binding.return_type(db))
12561257
{
1258+
// `property.__set__` returns `None` for ordinary setters, but
1259+
// preserving `Never` keeps non-returning setters divergent.
1260+
overload.set_return_type(if return_ty.is_never() {
1261+
return_ty
1262+
} else {
1263+
Type::none(db)
1264+
});
1265+
} else {
12571266
overload.errors.push(BindingError::InternalCallError(
12581267
"calling the setter failed",
12591268
));
1269+
overload.set_return_type(Type::unknown());
12601270
}
12611271
} else {
12621272
overload
@@ -1271,12 +1281,22 @@ impl<'db> Bindings<'db> {
12711281
overload.parameter_types()
12721282
{
12731283
if let Some(deleter) = property.deleter(db) {
1274-
if let Err(_call_error) =
1275-
deleter.try_call(db, &CallArguments::positional([*instance]))
1284+
if let Ok(return_ty) = deleter
1285+
.try_call(db, &CallArguments::positional([*instance]))
1286+
.map(|binding| binding.return_type(db))
12761287
{
1288+
// `property.__delete__` returns `None` for ordinary deleters,
1289+
// but preserving `Never` keeps non-returning deleters divergent.
1290+
overload.set_return_type(if return_ty.is_never() {
1291+
return_ty
1292+
} else {
1293+
Type::none(db)
1294+
});
1295+
} else {
12771296
overload.errors.push(BindingError::InternalCallError(
12781297
"calling the deleter failed",
12791298
));
1299+
overload.set_return_type(Type::unknown());
12801300
}
12811301
} else {
12821302
overload
@@ -1289,12 +1309,22 @@ impl<'db> Bindings<'db> {
12891309
Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(property)) => {
12901310
if let [Some(instance), Some(value), ..] = overload.parameter_types() {
12911311
if let Some(setter) = property.setter(db) {
1292-
if let Err(_call_error) = setter
1312+
if let Ok(return_ty) = setter
12931313
.try_call(db, &CallArguments::positional([*instance, *value]))
1314+
.map(|binding| binding.return_type(db))
12941315
{
1316+
// `property.__set__` returns `None` for ordinary setters, but
1317+
// preserving `Never` keeps non-returning setters divergent.
1318+
overload.set_return_type(if return_ty.is_never() {
1319+
return_ty
1320+
} else {
1321+
Type::none(db)
1322+
});
1323+
} else {
12951324
overload.errors.push(BindingError::InternalCallError(
12961325
"calling the setter failed",
12971326
));
1327+
overload.set_return_type(Type::unknown());
12981328
}
12991329
} else {
13001330
overload
@@ -1309,12 +1339,22 @@ impl<'db> Bindings<'db> {
13091339
)) => {
13101340
if let [Some(instance), ..] = overload.parameter_types() {
13111341
if let Some(deleter) = property.deleter(db) {
1312-
if let Err(_call_error) =
1313-
deleter.try_call(db, &CallArguments::positional([*instance]))
1342+
if let Ok(return_ty) = deleter
1343+
.try_call(db, &CallArguments::positional([*instance]))
1344+
.map(|binding| binding.return_type(db))
13141345
{
1346+
// `property.__delete__` returns `None` for ordinary deleters,
1347+
// but preserving `Never` keeps non-returning deleters divergent.
1348+
overload.set_return_type(if return_ty.is_never() {
1349+
return_ty
1350+
} else {
1351+
Type::none(db)
1352+
});
1353+
} else {
13151354
overload.errors.push(BindingError::InternalCallError(
13161355
"calling the deleter failed",
13171356
));
1357+
overload.set_return_type(Type::unknown());
13181358
}
13191359
} else {
13201360
overload

0 commit comments

Comments
 (0)