mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[red-knot] Add custom __setattr__
support (#16748)
## Summary Add support for classes with a custom `__setattr__` method. ## Test Plan New Markdown tests, ecosystem checks.
This commit is contained in:
parent
fab7d820bd
commit
2cee86d807
3 changed files with 119 additions and 14 deletions
|
@ -1395,6 +1395,59 @@ def _(ns: argparse.Namespace):
|
|||
reveal_type(ns.whatever) # revealed: Any
|
||||
```
|
||||
|
||||
## Classes with custom `__setattr__` methods
|
||||
|
||||
### Basic
|
||||
|
||||
If a type provides a custom `__setattr__` method, we use the parameter type of that method as the
|
||||
type to validate attribute assignments. Consider the following `CustomSetAttr` class:
|
||||
|
||||
```py
|
||||
class CustomSetAttr:
|
||||
def __setattr__(self, name: str, value: int) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
We can set arbitrary attributes on instances of this class:
|
||||
|
||||
```py
|
||||
c = CustomSetAttr()
|
||||
|
||||
c.whatever = 42
|
||||
```
|
||||
|
||||
### Type of the `name` parameter
|
||||
|
||||
If the `name` parameter of the `__setattr__` method is annotated with a (union of) literal type(s),
|
||||
we only consider the attribute assignment to be valid if the assigned attribute is one of them:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Date:
|
||||
def __setattr__(self, name: Literal["day", "month", "year"], value: int) -> None:
|
||||
pass
|
||||
|
||||
date = Date()
|
||||
date.day = 8
|
||||
date.month = 4
|
||||
date.year = 2025
|
||||
|
||||
# error: [unresolved-attribute] "Can not assign object of `Literal["UTC"]` to attribute `tz` on type `Date` with custom `__setattr__` method."
|
||||
date.tz = "UTC"
|
||||
```
|
||||
|
||||
### `argparse.Namespace`
|
||||
|
||||
A standard library example of a class with a custom `__setattr__` method is `argparse.Namespace`:
|
||||
|
||||
```py
|
||||
import argparse
|
||||
|
||||
def _(ns: argparse.Namespace):
|
||||
ns.whatever = 42
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
|
@ -3422,13 +3422,31 @@ impl<'db> Type<'db> {
|
|||
/// Returns an `Err` if the dunder method can't be called,
|
||||
/// or the given arguments are not valid.
|
||||
fn try_call_dunder(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
argument_types: CallArgumentTypes<'_, 'db>,
|
||||
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
||||
self.try_call_dunder_with_policy(db, name, argument_types, MemberLookupPolicy::empty())
|
||||
}
|
||||
|
||||
/// Same as `try_call_dunder`, but allows specifying a policy for the member lookup. In
|
||||
/// particular, this allows to specify `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK` to avoid
|
||||
/// looking up dunder methods on `object`, which is needed for functions like `__init__`,
|
||||
/// `__new__`, or `__setattr__`.
|
||||
fn try_call_dunder_with_policy(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
mut argument_types: CallArgumentTypes<'_, 'db>,
|
||||
policy: MemberLookupPolicy,
|
||||
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
||||
match self
|
||||
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_INSTANCE_FALLBACK)
|
||||
.member_lookup_with_policy(
|
||||
db,
|
||||
name.into(),
|
||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK | policy,
|
||||
)
|
||||
.symbol
|
||||
{
|
||||
Symbol::Type(dunder_callable, boundness) => {
|
||||
|
|
|
@ -82,7 +82,9 @@ use crate::types::{
|
|||
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
|
||||
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::types::{CallableType, FunctionDecorators, Signature};
|
||||
use crate::types::{
|
||||
CallableType, FunctionDecorators, MemberLookupPolicy, Signature, StringLiteralType,
|
||||
};
|
||||
use crate::unpack::{Unpack, UnpackPosition};
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
@ -2480,19 +2482,51 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
ensure_assignable_to(instance_attr_ty)
|
||||
} else {
|
||||
if emit_diagnostics {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
target,
|
||||
format_args!(
|
||||
"Unresolved attribute `{}` on type `{}`.",
|
||||
attribute,
|
||||
object_ty.display(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
let result = object_ty.try_call_dunder_with_policy(
|
||||
db,
|
||||
"__setattr__",
|
||||
CallArgumentTypes::positional([
|
||||
Type::StringLiteral(StringLiteralType::new(
|
||||
db,
|
||||
Box::from(attribute),
|
||||
)),
|
||||
value_ty,
|
||||
]),
|
||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||
);
|
||||
|
||||
false
|
||||
match result {
|
||||
Ok(_) | Err(CallDunderError::PossiblyUnbound(_)) => true,
|
||||
Err(CallDunderError::CallError(..)) => {
|
||||
if emit_diagnostics {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
target,
|
||||
format_args!(
|
||||
"Can not assign object of `{}` to attribute `{attribute}` on type `{}` with custom `__setattr__` method.",
|
||||
value_ty.display(db),
|
||||
object_ty.display(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
false
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
if emit_diagnostics {
|
||||
self.context.report_lint(
|
||||
&UNRESOLVED_ATTRIBUTE,
|
||||
target,
|
||||
format_args!(
|
||||
"Unresolved attribute `{}` on type `{}`.",
|
||||
attribute,
|
||||
object_ty.display(db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue