diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 3c28431d58..6cbcf321c7 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2002,6 +2002,39 @@ def _(ns: argparse.Namespace): ns.whatever = 42 ``` +### `__setattr__` is a fallback for explicitly defined attributes + +When a class has both a custom `__setattr__` method and explicitly defined attributes, the +`__setattr__` method is treated as a fallback. The type of the explicit attribute takes precedence +over the `__setattr__` parameter type. + +This matches the behavior of other type checkers and reflects the common pattern in libraries like +PyTorch, where `__setattr__` may have a narrow type signature but forwards to +`super().__setattr__()` for attributes that don't match. + +```py +from typing import Union + +class Tensor: ... + +class Module: + def __setattr__(self, name: str, value: Union[Tensor, "Module"]) -> None: + super().__setattr__(name, value) + +class MyModule(Module): + some_param: int # Explicit attribute with type `int` + +def use_module(m: MyModule, param: int) -> None: + # This is allowed because `some_param` is explicitly defined with type `int`, + # even though `__setattr__` only accepts `Union[Tensor, Module]`. + m.some_param = param + + # But assigning to an attribute that's not explicitly defined will still + # use `__setattr__` for validation. + # error: [unresolved-attribute] "Cannot assign object of type `int` to attribute `undefined_param` on type `MyModule` with custom `__setattr__` method." + m.undefined_param = param +``` + ## Objects of all types have a `__class__` method The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c93269ade2..8679aeccc7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4629,28 +4629,22 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } }; + // Determine whether `__setattr__` was called but failed type checking. + // In this case, we treat `__setattr__` as a fallback: if there's an explicit + // attribute defined, we validate against that instead. + let setattr_call_failed = matches!( + setattr_dunder_call_result, + Err(CallDunderError::CallError(..)) + ); + match setattr_dunder_call_result { Ok(result) => check_setattr_return_type(result), Err(CallDunderError::PossiblyUnbound(result)) => { check_setattr_return_type(*result) } - Err(CallDunderError::CallError(..)) => { - if emit_diagnostics { - if let Some(builder) = - self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) - { - builder.into_diagnostic(format_args!( - "Cannot assign object of type `{}` to attribute \ - `{attribute}` on type `{}` with \ - custom `__setattr__` method.", - value_ty.display(db), - object_ty.display(db) - )); - } - } - false - } - Err(CallDunderError::MethodNotAvailable) => { + // When `__setattr__` fails type checking (CallError) or doesn't exist + // (MethodNotAvailable), check for explicit attributes. + Err(CallDunderError::CallError(..) | CallDunderError::MethodNotAvailable) => { match object_ty.class_member(db, attribute.into()) { meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => { if emit_diagnostics { @@ -4785,15 +4779,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ensure_assignable_to(self, value_ty, instance_attr_ty) } else { + // No explicit attribute found. If `__setattr__` failed, + // report that error; otherwise report unresolved attribute. if emit_diagnostics { if let Some(builder) = self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target) { - builder.into_diagnostic(format_args!( - "Unresolved attribute `{}` on type `{}`.", - attribute, - object_ty.display(db) - )); + if setattr_call_failed { + builder.into_diagnostic(format_args!( + "Cannot assign object of type `{}` to attribute \ + `{attribute}` on type `{}` with \ + custom `__setattr__` method.", + value_ty.display(db), + object_ty.display(db) + )); + } else { + builder.into_diagnostic(format_args!( + "Unresolved attribute `{}` on type `{}`.", + attribute, + object_ty.display(db) + )); + } } }