mirror of https://github.com/astral-sh/ruff
Treat __setattr__ as fallback-only
This commit is contained in:
parent
c02bd11b93
commit
0ccee860e9
|
|
@ -2002,6 +2002,39 @@ def _(ns: argparse.Namespace):
|
||||||
ns.whatever = 42
|
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
|
## 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
|
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
match setattr_dunder_call_result {
|
||||||
Ok(result) => check_setattr_return_type(result),
|
Ok(result) => check_setattr_return_type(result),
|
||||||
Err(CallDunderError::PossiblyUnbound(result)) => {
|
Err(CallDunderError::PossiblyUnbound(result)) => {
|
||||||
check_setattr_return_type(*result)
|
check_setattr_return_type(*result)
|
||||||
}
|
}
|
||||||
Err(CallDunderError::CallError(..)) => {
|
// When `__setattr__` fails type checking (CallError) or doesn't exist
|
||||||
if emit_diagnostics {
|
// (MethodNotAvailable), check for explicit attributes.
|
||||||
if let Some(builder) =
|
Err(CallDunderError::CallError(..) | CallDunderError::MethodNotAvailable) => {
|
||||||
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) => {
|
|
||||||
match object_ty.class_member(db, attribute.into()) {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
|
meta_attr @ PlaceAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||||
if emit_diagnostics {
|
if emit_diagnostics {
|
||||||
|
|
@ -4785,15 +4779,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
|
|
||||||
ensure_assignable_to(self, value_ty, instance_attr_ty)
|
ensure_assignable_to(self, value_ty, instance_attr_ty)
|
||||||
} else {
|
} else {
|
||||||
|
// No explicit attribute found. If `__setattr__` failed,
|
||||||
|
// report that error; otherwise report unresolved attribute.
|
||||||
if emit_diagnostics {
|
if emit_diagnostics {
|
||||||
if let Some(builder) =
|
if let Some(builder) =
|
||||||
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
|
self.context.report_lint(&UNRESOLVED_ATTRIBUTE, target)
|
||||||
{
|
{
|
||||||
builder.into_diagnostic(format_args!(
|
if setattr_call_failed {
|
||||||
"Unresolved attribute `{}` on type `{}`.",
|
builder.into_diagnostic(format_args!(
|
||||||
attribute,
|
"Cannot assign object of type `{}` to attribute \
|
||||||
object_ty.display(db)
|
`{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)
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue