Skip to content

Commit a21e1d1

Browse files
committed
[red-knot] Add custom __setattr__ support
1 parent c755eec commit a21e1d1

File tree

2 files changed

+68
-13
lines changed

2 files changed

+68
-13
lines changed

crates/red_knot_python_semantic/resources/mdtest/attributes.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,27 @@ def _(ns: argparse.Namespace):
13941394
reveal_type(ns.whatever) # revealed: Any
13951395
```
13961396

1397+
## Classes with custom `__setattr__` methods
1398+
1399+
### Basic
1400+
1401+
If a type provides a custom `__setattr__` method, we use the parameter type of that method as the
1402+
type to validate attribute assignments. Consider the following `CustomSetAttr` class:
1403+
1404+
```py
1405+
class CustomSetAttr:
1406+
def __setattr__(self, name: str, value: int) -> None:
1407+
pass
1408+
```
1409+
1410+
We can set arbitrary attributes on instances of this class:
1411+
1412+
```py
1413+
c = CustomSetAttr()
1414+
1415+
c.whatever = 42
1416+
```
1417+
13971418
## Objects of all types have a `__class__` method
13981419

13991420
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ use crate::types::{
8484
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
8585
UnionType,
8686
};
87-
use crate::types::{CallableType, GeneralCallableType, ParameterKind, Signature};
87+
use crate::types::{
88+
CallableType, GeneralCallableType, ParameterKind, Signature, StringLiteralType,
89+
};
8890
use crate::unpack::Unpack;
8991
use crate::util::subscript::{PyIndex, PySlice};
9092
use crate::Db;
@@ -2346,19 +2348,51 @@ impl<'db> TypeInferenceBuilder<'db> {
23462348

23472349
ensure_assignable_to(instance_attr_ty)
23482350
} else {
2349-
if emit_diagnostics {
2350-
self.context.report_lint(
2351-
&UNRESOLVED_ATTRIBUTE,
2352-
target,
2353-
format_args!(
2354-
"Unresolved attribute `{}` on type `{}`.",
2355-
attribute,
2356-
object_ty.display(db)
2357-
),
2358-
);
2359-
}
2351+
// TODO: we need to avoid looking up object.__setattr__
2352+
let result = object_ty.try_call_dunder(
2353+
db,
2354+
"__setattr__",
2355+
&CallArguments::positional([
2356+
Type::StringLiteral(StringLiteralType::new(
2357+
db,
2358+
Box::from(attribute),
2359+
)),
2360+
value_ty,
2361+
]),
2362+
);
2363+
2364+
match result {
2365+
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
2366+
Err(CallDunderError::Call(_)) => {
2367+
if emit_diagnostics {
2368+
self.context.report_lint(
2369+
&UNRESOLVED_ATTRIBUTE,
2370+
target,
2371+
format_args!(
2372+
"Can not assign object of `{}` to attribute `{attribute}` on type `{}` with custom `__setattr__` method.",
2373+
value_ty.display(db),
2374+
object_ty.display(db)
2375+
),
2376+
);
2377+
}
2378+
false
2379+
}
2380+
Err(CallDunderError::MethodNotAvailable) => {
2381+
if emit_diagnostics {
2382+
self.context.report_lint(
2383+
&UNRESOLVED_ATTRIBUTE,
2384+
target,
2385+
format_args!(
2386+
"Unresolved attribute `{}` on type `{}`.",
2387+
attribute,
2388+
object_ty.display(db)
2389+
),
2390+
);
2391+
}
23602392

2361-
false
2393+
false
2394+
}
2395+
}
23622396
}
23632397
}
23642398
},

0 commit comments

Comments
 (0)