diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 9bdac93b90..7ca665a983 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 4351f41488..f7b238fd75 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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, 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, 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) => { diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 97f2c56840..bf38e1ae70 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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 + } + } } } }