Skip to content

Commit d113662

Browse files
thejchapsharkdp
authored andcommitted
Fix __setattr__ call check precedence during attribute assignment
1 parent 8d98c60 commit d113662

File tree

1 file changed

+148
-129
lines changed
  • crates/ty_python_semantic/src/types

1 file changed

+148
-129
lines changed

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 148 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -3115,87 +3115,162 @@ impl<'db> TypeInferenceBuilder<'db> {
31153115
dataclass_params.is_some_and(|params| params.contains(DataclassParams::FROZEN))
31163116
};
31173117

3118-
match object_ty.class_member(db, attribute.into()) {
3119-
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
3118+
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
3119+
// assigning the attributed by the normal mechanism.
3120+
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
3121+
db,
3122+
"__setattr__",
3123+
&mut CallArgumentTypes::positional([
3124+
Type::StringLiteral(StringLiteralType::new(db, Box::from(attribute))),
3125+
value_ty,
3126+
]),
3127+
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
3128+
);
3129+
3130+
match setattr_dunder_call_result {
3131+
Ok(result) => match result.return_type(db) {
3132+
Type::Never => {
3133+
if emit_diagnostics {
3134+
if let Some(builder) =
3135+
self.context.report_lint(&INVALID_ASSIGNMENT, target)
3136+
{
3137+
builder.into_diagnostic(format_args!(
3138+
"Cannot assign to attribute `{attribute}` on type `{}` \
3139+
via `__setattr__` that returns `Never`",
3140+
object_ty.display(db)
3141+
));
3142+
}
3143+
}
3144+
false
3145+
}
3146+
_ => true,
3147+
},
3148+
Err(CallDunderError::CallError(..)) => {
31203149
if emit_diagnostics {
31213150
if let Some(builder) =
3122-
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
3151+
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
31233152
{
31243153
builder.into_diagnostic(format_args!(
3125-
"Cannot assign to ClassVar `{attribute}` \
3126-
from an instance of type `{ty}`",
3127-
ty = object_ty.display(self.db()),
3154+
"Can not assign object of `{}` to attribute \
3155+
`{attribute}` on type `{}` with \
3156+
custom `__setattr__` method.",
3157+
value_ty.display(db),
3158+
object_ty.display(db)
31283159
));
31293160
}
31303161
}
31313162
false
31323163
}
3133-
SymbolAndQualifiers {
3134-
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
3135-
qualifiers: _,
3136-
} => {
3137-
if is_read_only() {
3138-
if emit_diagnostics {
3139-
if let Some(builder) =
3140-
self.context.report_lint(&INVALID_ASSIGNMENT, target)
3141-
{
3142-
builder.into_diagnostic(format_args!(
3164+
Err(CallDunderError::PossiblyUnbound(_)) => true,
3165+
Err(CallDunderError::MethodNotAvailable) => {
3166+
match object_ty.class_member(db, attribute.into()) {
3167+
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
3168+
if emit_diagnostics {
3169+
if let Some(builder) =
3170+
self.context.report_lint(&INVALID_ATTRIBUTE_ACCESS, target)
3171+
{
3172+
builder.into_diagnostic(format_args!(
3173+
"Cannot assign to ClassVar `{attribute}` \
3174+
from an instance of type `{ty}`",
3175+
ty = object_ty.display(self.db()),
3176+
));
3177+
}
3178+
}
3179+
false
3180+
}
3181+
SymbolAndQualifiers {
3182+
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
3183+
qualifiers: _,
3184+
} => {
3185+
if is_read_only() {
3186+
if emit_diagnostics {
3187+
if let Some(builder) =
3188+
self.context.report_lint(&INVALID_ASSIGNMENT, target)
3189+
{
3190+
builder.into_diagnostic(format_args!(
31433191
"Property `{attribute}` defined in `{ty}` is read-only",
31443192
ty = object_ty.display(self.db()),
31453193
));
3146-
}
3147-
}
3148-
false
3149-
} else {
3150-
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
3151-
meta_attr_ty.class_member(db, "__set__".into()).symbol
3152-
{
3153-
let successful_call = meta_dunder_set
3154-
.try_call(
3155-
db,
3156-
&CallArgumentTypes::positional([
3157-
meta_attr_ty,
3158-
object_ty,
3159-
value_ty,
3160-
]),
3161-
)
3162-
.is_ok();
3194+
}
3195+
}
3196+
false
3197+
} else {
3198+
let assignable_to_meta_attr =
3199+
if let Symbol::Type(meta_dunder_set, _) =
3200+
meta_attr_ty.class_member(db, "__set__".into()).symbol
3201+
{
3202+
let successful_call = meta_dunder_set
3203+
.try_call(
3204+
db,
3205+
&CallArgumentTypes::positional([
3206+
meta_attr_ty,
3207+
object_ty,
3208+
value_ty,
3209+
]),
3210+
)
3211+
.is_ok();
31633212

3164-
if !successful_call && emit_diagnostics {
3165-
if let Some(builder) =
3166-
self.context.report_lint(&INVALID_ASSIGNMENT, target)
3167-
{
3168-
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
3169-
builder.into_diagnostic(format_args!(
3213+
if !successful_call && emit_diagnostics {
3214+
if let Some(builder) = self
3215+
.context
3216+
.report_lint(&INVALID_ASSIGNMENT, target)
3217+
{
3218+
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
3219+
builder.into_diagnostic(format_args!(
31703220
"Invalid assignment to data descriptor attribute \
31713221
`{attribute}` on type `{}` with custom `__set__` method",
31723222
object_ty.display(db)
31733223
));
3174-
}
3175-
}
3224+
}
3225+
}
31763226

3177-
successful_call
3178-
} else {
3179-
ensure_assignable_to(meta_attr_ty)
3180-
};
3227+
successful_call
3228+
} else {
3229+
ensure_assignable_to(meta_attr_ty)
3230+
};
31813231

3182-
let assignable_to_instance_attribute =
3183-
if meta_attr_boundness == Boundness::PossiblyUnbound {
3184-
let (assignable, boundness) = if let Symbol::Type(
3185-
instance_attr_ty,
3186-
instance_attr_boundness,
3187-
) =
3188-
object_ty.instance_member(db, attribute).symbol
3189-
{
3190-
(
3191-
ensure_assignable_to(instance_attr_ty),
3192-
instance_attr_boundness,
3193-
)
3194-
} else {
3195-
(true, Boundness::PossiblyUnbound)
3196-
};
3232+
let assignable_to_instance_attribute =
3233+
if meta_attr_boundness == Boundness::PossiblyUnbound {
3234+
let (assignable, boundness) = if let Symbol::Type(
3235+
instance_attr_ty,
3236+
instance_attr_boundness,
3237+
) =
3238+
object_ty.instance_member(db, attribute).symbol
3239+
{
3240+
(
3241+
ensure_assignable_to(instance_attr_ty),
3242+
instance_attr_boundness,
3243+
)
3244+
} else {
3245+
(true, Boundness::PossiblyUnbound)
3246+
};
31973247

3198-
if boundness == Boundness::PossiblyUnbound {
3248+
if boundness == Boundness::PossiblyUnbound {
3249+
report_possibly_unbound_attribute(
3250+
&self.context,
3251+
target,
3252+
attribute,
3253+
object_ty,
3254+
);
3255+
}
3256+
3257+
assignable
3258+
} else {
3259+
true
3260+
};
3261+
3262+
assignable_to_meta_attr && assignable_to_instance_attribute
3263+
}
3264+
}
3265+
3266+
SymbolAndQualifiers {
3267+
symbol: Symbol::Unbound,
3268+
..
3269+
} => {
3270+
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
3271+
object_ty.instance_member(db, attribute).symbol
3272+
{
3273+
if instance_attr_boundness == Boundness::PossiblyUnbound {
31993274
report_possibly_unbound_attribute(
32003275
&self.context,
32013276
target,
@@ -3204,79 +3279,23 @@ impl<'db> TypeInferenceBuilder<'db> {
32043279
);
32053280
}
32063281

3207-
assignable
3208-
} else {
3209-
true
3210-
};
3211-
3212-
assignable_to_meta_attr && assignable_to_instance_attribute
3213-
}
3214-
}
3215-
3216-
SymbolAndQualifiers {
3217-
symbol: Symbol::Unbound,
3218-
..
3219-
} => {
3220-
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
3221-
object_ty.instance_member(db, attribute).symbol
3222-
{
3223-
if instance_attr_boundness == Boundness::PossiblyUnbound {
3224-
report_possibly_unbound_attribute(
3225-
&self.context,
3226-
target,
3227-
attribute,
3228-
object_ty,
3229-
);
3230-
}
3231-
3232-
if is_read_only() {
3233-
if emit_diagnostics {
3234-
if let Some(builder) =
3235-
self.context.report_lint(&INVALID_ASSIGNMENT, target)
3236-
{
3237-
builder.into_diagnostic(format_args!(
3282+
if is_read_only() {
3283+
if emit_diagnostics {
3284+
if let Some(builder) = self
3285+
.context
3286+
.report_lint(&INVALID_ASSIGNMENT, target)
3287+
{
3288+
builder.into_diagnostic(format_args!(
32383289
"Property `{attribute}` defined in `{ty}` is read-only",
32393290
ty = object_ty.display(self.db()),
32403291
));
3241-
}
3242-
}
3243-
false
3244-
} else {
3245-
ensure_assignable_to(instance_attr_ty)
3246-
}
3247-
} else {
3248-
let result = object_ty.try_call_dunder_with_policy(
3249-
db,
3250-
"__setattr__",
3251-
&mut CallArgumentTypes::positional([
3252-
Type::StringLiteral(StringLiteralType::new(
3253-
db,
3254-
Box::from(attribute),
3255-
)),
3256-
value_ty,
3257-
]),
3258-
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
3259-
);
3260-
3261-
match result {
3262-
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
3263-
Err(CallDunderError::CallError(..)) => {
3264-
if emit_diagnostics {
3265-
if let Some(builder) =
3266-
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
3267-
{
3268-
builder.into_diagnostic(format_args!(
3269-
"Can not assign object of `{}` to attribute \
3270-
`{attribute}` on type `{}` with \
3271-
custom `__setattr__` method.",
3272-
value_ty.display(db),
3273-
object_ty.display(db)
3274-
));
3292+
}
32753293
}
3294+
false
3295+
} else {
3296+
ensure_assignable_to(instance_attr_ty)
32763297
}
3277-
false
3278-
}
3279-
Err(CallDunderError::MethodNotAvailable) => {
3298+
} else {
32803299
if emit_diagnostics {
32813300
if let Some(builder) =
32823301
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)

0 commit comments

Comments
 (0)