mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 15:14:42 +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
|
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
|
## 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
|
||||||
|
|
|
@ -3422,13 +3422,31 @@ impl<'db> Type<'db> {
|
||||||
/// Returns an `Err` if the dunder method can't be called,
|
/// Returns an `Err` if the dunder method can't be called,
|
||||||
/// or the given arguments are not valid.
|
/// or the given arguments are not valid.
|
||||||
fn try_call_dunder(
|
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,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
name: &str,
|
name: &str,
|
||||||
mut argument_types: CallArgumentTypes<'_, 'db>,
|
mut argument_types: CallArgumentTypes<'_, 'db>,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
||||||
match self
|
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
|
||||||
{
|
{
|
||||||
Symbol::Type(dunder_callable, boundness) => {
|
Symbol::Type(dunder_callable, boundness) => {
|
||||||
|
|
|
@ -82,7 +82,9 @@ use crate::types::{
|
||||||
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
|
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
|
||||||
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
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::unpack::{Unpack, UnpackPosition};
|
||||||
use crate::util::subscript::{PyIndex, PySlice};
|
use crate::util::subscript::{PyIndex, PySlice};
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
@ -2480,6 +2482,36 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
ensure_assignable_to(instance_attr_ty)
|
ensure_assignable_to(instance_attr_ty)
|
||||||
} else {
|
} else {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {
|
if emit_diagnostics {
|
||||||
self.context.report_lint(
|
self.context.report_lint(
|
||||||
&UNRESOLVED_ATTRIBUTE,
|
&UNRESOLVED_ATTRIBUTE,
|
||||||
|
@ -2497,6 +2529,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
||||||
match object_ty.class_member(db, attribute.into()) {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue