From 16ab8c54b842448f6ac45cda8ef1d184ab9db23b Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 14 May 2026 15:55:31 +0900 Subject: [PATCH 1/3] fix --- pyrefly/lib/alt/call.rs | 7 + pyrefly/lib/alt/class/dataclass.rs | 9 + pyrefly/lib/alt/class/pydantic.rs | 478 +++++++++++++++++++++++++++++ pyrefly/lib/test/pydantic/field.rs | 20 ++ 4 files changed, 514 insertions(+) diff --git a/pyrefly/lib/alt/call.rs b/pyrefly/lib/alt/call.rs index df5857c26b..49dd7fd42e 100644 --- a/pyrefly/lib/alt/call.rs +++ b/pyrefly/lib/alt/call.rs @@ -3112,6 +3112,13 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { "`NotImplemented` is not callable. Did you mean `NotImplementedError`?".to_owned(), ); } + self.check_pydantic_model_validate_constraints( + ty, + &x.arguments.args, + &x.arguments.keywords, + x.arguments.range, + errors, + ); match ty.callee_kind() { Some(CalleeKind::Function(FunctionKind::AssertType)) => self .call_assert_type( diff --git a/pyrefly/lib/alt/class/dataclass.rs b/pyrefly/lib/alt/class/dataclass.rs index a465be1dfa..13dff473bb 100644 --- a/pyrefly/lib/alt/class/dataclass.rs +++ b/pyrefly/lib/alt/class/dataclass.rs @@ -221,6 +221,15 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { { fields.insert(MODEL_CONSTRUCT, model_construct); } + if metadata.is_pydantic_model() { + let root_model_type = self + .get_pydantic_root_model_type_via_mro(cls, metadata) + .map(|(ty, _)| ty); + fields.insert( + Name::new_static("model_validate"), + self.get_pydantic_model_validate(cls, dataclass, root_model_type), + ); + } let dataclass_fields_type = self.stdlib.dict( self.heap.mk_class_type(self.stdlib.str().clone()), self.heap.mk_any_implicit(), diff --git a/pyrefly/lib/alt/class/pydantic.rs b/pyrefly/lib/alt/class/pydantic.rs index ccbb4313cf..3cd2707b8a 100644 --- a/pyrefly/lib/alt/class/pydantic.rs +++ b/pyrefly/lib/alt/class/pydantic.rs @@ -22,7 +22,14 @@ use pyrefly_types::function::FunctionKind; use pyrefly_types::keywords::DataclassFieldKeywords; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::Lit; +use pyrefly_types::types::Union; +use pyrefly_types::typed_dict::AnonymousTypedDictInner; +use pyrefly_types::typed_dict::TypedDict; +use pyrefly_types::typed_dict::TypedDictField; +use pyrefly_types::types::Union; +use ruff_python_ast::DictItem; use ruff_python_ast::Expr; +use ruff_python_ast::Keyword; use ruff_python_ast::name::Name; use ruff_text_size::Ranged; use ruff_text_size::TextRange; @@ -252,6 +259,145 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { ) } + pub fn get_pydantic_model_validate( + &self, + cls: &Class, + dataclass: &DataclassMetadata, + root_model_type: Option, + ) -> ClassSynthesizedField { + let mut seen = SmallSet::new(); + let obj_ty = self.unions(vec![ + self.instantiate(cls), + root_model_type + .map(|ty| self.pydantic_model_validate_type(ty, &mut seen)) + .unwrap_or_else(|| { + self.pydantic_model_validate_dict_type(cls, dataclass, &mut seen) + }), + self.heap.mk_class_type(self.stdlib.dict( + self.heap.mk_class_type(self.stdlib.str().clone()), + self.heap.mk_any_explicit(), + )), + ]); + let bool_or_none = self.union( + self.heap.mk_class_type(self.stdlib.bool().clone()), + Type::None, + ); + let params = vec![ + Param::Pos(Name::new_static("obj"), obj_ty, Required::Required), + Param::KwOnly( + Name::new_static("strict"), + bool_or_none.clone(), + Required::Optional(None), + ), + Param::KwOnly( + Name::new_static("from_attributes"), + bool_or_none.clone(), + Required::Optional(None), + ), + Param::KwOnly( + Name::new_static("context"), + self.heap.mk_any_explicit(), + Required::Optional(None), + ), + Param::KwOnly( + Name::new_static("by_alias"), + bool_or_none.clone(), + Required::Optional(None), + ), + Param::KwOnly( + Name::new_static("by_name"), + bool_or_none, + Required::Optional(None), + ), + ]; + ClassSynthesizedField::new_classvar(self.heap.mk_function(Function { + signature: Callable::list(ParamList::new(params), self.instantiate(cls)), + metadata: FuncMetadata::method(cls, Name::new_static("model_validate")), + })) + } + + fn pydantic_model_validate_type(&self, ty: Type, seen: &mut SmallSet) -> Type { + match ty { + Type::ClassType(cls) => { + let metadata = self.get_metadata_for_class(cls.class_object()); + if metadata.is_pydantic_model() + && let Some(dataclass) = metadata.dataclass_metadata() + && seen.insert(cls.class_object().clone()) + { + let dict_ty = + self.pydantic_model_validate_dict_type(cls.class_object(), dataclass, seen); + seen.shift_remove(cls.class_object()); + self.union(Type::ClassType(cls), dict_ty) + } else if cls.class_object() == self.stdlib.list_object() + && let [elem] = cls.targs().as_slice() + { + self.heap.mk_class_type( + self.stdlib + .list(self.pydantic_model_validate_type(elem.clone(), seen)), + ) + } else if cls.class_object() == self.stdlib.dict_object() + && let [key, value] = cls.targs().as_slice() + { + self.heap.mk_class_type(self.stdlib.dict( + key.clone(), + self.pydantic_model_validate_type(value.clone(), seen), + )) + } else { + Type::ClassType(cls) + } + } + Type::Union(box Union { members, .. }) => self.unions( + members + .into_iter() + .map(|member| self.pydantic_model_validate_type(member, seen)) + .collect(), + ), + Type::Annotated(ty, metadata) => Type::Annotated( + Box::new(self.pydantic_model_validate_type(*ty, seen)), + metadata, + ), + ty => ty, + } + } + + fn pydantic_model_validate_dict_type( + &self, + cls: &Class, + dataclass: &DataclassMetadata, + seen: &mut SmallSet, + ) -> Type { + let kw_only_by_class = self.compute_kw_only_fields_by_class(cls); + let force_optional = matches!( + self.get_metadata_for_class(cls).pydantic_model_kind(), + Some(PydanticModelKind::BaseSettings) + ); + let mut fields = Vec::new(); + for (name, field, field_flags) in self.iter_fields(cls, dataclass, true, &kw_only_by_class) + { + if !field_flags.init { + continue; + } + let has_default = force_optional + || field_flags.default.is_some() + || (field_flags.init_by_name && field_flags.init_by_alias.is_some()); + let field_info = TypedDictField { + ty: self.pydantic_model_validate_type(field.ty(), seen), + required: !has_default, + read_only_reason: None, + }; + if field_flags.init_by_name { + fields.push((name, field_info.clone())); + } + if let Some(alias) = field_flags.init_by_alias { + fields.push((alias, field_info)); + } + } + self.heap + .mk_typed_dict(TypedDict::Anonymous(Box::new(AnonymousTypedDictInner { + fields, + }))) + } + pub fn is_pydantic_strict_metadata(&self, ty: &Type) -> bool { match ty { Type::ClassType(cls) => cls.has_qname(ModuleName::pydantic_types().as_str(), "Strict"), @@ -854,6 +1000,332 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } } + pub fn check_pydantic_model_validate_constraints( + &self, + callee: &Type, + args: &[Expr], + keywords: &[Keyword], + range: TextRange, + errors: &ErrorCollector, + ) { + let Some(cls) = self.pydantic_model_validate_class(callee) else { + return; + }; + let Some(obj) = args.first().or_else(|| { + keywords + .iter() + .find(|kw| kw.arg.as_ref().is_some_and(|arg| arg.id.as_str() == "obj")) + .map(|kw| &kw.value) + }) else { + return; + }; + let metadata = self.get_metadata_for_class(&cls); + let Some(dataclass) = metadata.dataclass_metadata() else { + return; + }; + let infer_errors = self.error_swallower(); + let obj_ty = self.expr_infer(obj, &infer_errors); + let mut seen = SmallSet::new(); + self.check_pydantic_model_validate_expr_constraints( + &cls, dataclass, obj, range, errors, &mut seen, + ); + seen.clear(); + if let Some(initializer) = self.pydantic_name_initializer(obj) { + self.check_pydantic_model_validate_expr_constraints( + &cls, + dataclass, + initializer, + range, + errors, + &mut seen, + ); + seen.clear(); + } + self.check_pydantic_model_validate_type_constraints( + &cls, dataclass, &obj_ty, range, errors, &mut seen, + ); + } + + fn pydantic_model_validate_class(&self, callee: &Type) -> Option { + callee.visit_toplevel_func_metadata(&|meta| { + let FunctionKind::Def(id) = &meta.kind else { + return None; + }; + if id.name.as_str() != "model_validate" { + return None; + } + let cls = id.cls.clone()?; + if self.get_metadata_for_class(&cls).is_pydantic_model() { + Some(cls) + } else { + None + } + }) + } + + fn pydantic_name_initializer<'b>(&'b self, actual: &Expr) -> Option<&'b Expr> { + let Expr::Name(name) = actual else { + return None; + }; + let key = Key::BoundName(ShortIdentifier::expr_name(name)); + let idx = self.bindings().key_to_idx_hashed_opt(Hashed::new(&key))?; + let mut idx = idx; + for _ in 0..20 { + match self.bindings().get(idx) { + Binding::Forward(forward) + | Binding::PromoteForward(forward) + | Binding::ForwardToFirstUse(forward) => idx = *forward, + binding => return Self::pydantic_initializer_from_binding(binding), + } + } + None + } + + fn pydantic_initializer_from_binding(binding: &Binding) -> Option<&Expr> { + match binding { + Binding::Expr(_, expr) => Some(expr), + Binding::NameAssign(assign) => Some(&assign.expr), + Binding::AnnotatedType(_, inner) => Self::pydantic_initializer_from_binding(inner), + _ => None, + } + } + + fn check_pydantic_model_validate_expr_constraints( + &self, + expected_cls: &Class, + dataclass: &DataclassMetadata, + actual: &Expr, + range: TextRange, + errors: &ErrorCollector, + seen: &mut SmallSet, + ) { + if !seen.insert(expected_cls.clone()) { + return; + } + let Expr::Dict(dict) = actual else { + seen.shift_remove(expected_cls); + return; + }; + let actual_fields = self.pydantic_literal_dict_fields(&dict.items); + let kw_only_by_class = self.compute_kw_only_fields_by_class(expected_cls); + for (name, field, field_flags) in + self.iter_fields(expected_cls, dataclass, true, &kw_only_by_class) + { + let actual_field = field_flags + .init_by_name + .then(|| actual_fields.get(&name)) + .flatten() + .or_else(|| { + field_flags + .init_by_alias + .as_ref() + .and_then(|alias| actual_fields.get(alias)) + }); + let Some(actual_field) = actual_field else { + continue; + }; + if let Some(constraints) = PydanticRangeConstraints::from_keywords(&field_flags) { + let infer_errors = self.error_swallower(); + let actual_ty = self.expr_infer(actual_field, &infer_errors); + self.emit_pydantic_argument_constraint( + &actual_ty, + &PydanticParamConstraint { + field_name: name.clone(), + constraints, + }, + actual_field.range(), + errors, + ); + } + self.check_pydantic_field_expr_constraints( + &field.ty(), + actual_field, + range, + errors, + seen, + ); + } + seen.shift_remove(expected_cls); + } + + fn pydantic_literal_dict_fields<'b>(&self, items: &'b [DictItem]) -> SmallMap { + Ast::flatten_dict_items(items) + .into_iter() + .filter_map(|item| { + let key = item.key.as_ref()?.as_string_literal_expr()?; + Some((Name::new(key.value.to_str()), &item.value)) + }) + .collect() + } + + fn check_pydantic_field_expr_constraints( + &self, + expected: &Type, + actual: &Expr, + range: TextRange, + errors: &ErrorCollector, + seen: &mut SmallSet, + ) { + match expected { + Type::ClassType(expected) + if expected.class_object() == self.stdlib.list_object() + && let [expected] = expected.targs().as_slice() + && let Expr::List(actual) = actual => + { + for actual in actual.elts.iter() { + self.check_pydantic_field_expr_constraints( + expected, actual, range, errors, seen, + ); + } + } + Type::ClassType(expected) => { + let metadata = self.get_metadata_for_class(expected.class_object()); + if metadata.is_pydantic_model() + && let Some(dataclass) = metadata.dataclass_metadata() + { + self.check_pydantic_model_validate_expr_constraints( + expected.class_object(), + dataclass, + actual, + range, + errors, + seen, + ); + } + } + Type::Annotated(expected, _) => { + self.check_pydantic_field_expr_constraints(expected, actual, range, errors, seen) + } + Type::Union(union) => { + for expected in union.members.iter() { + self.check_pydantic_field_expr_constraints( + expected, actual, range, errors, seen, + ); + } + } + _ => {} + } + } + + fn check_pydantic_model_validate_type_constraints( + &self, + expected_cls: &Class, + dataclass: &DataclassMetadata, + actual: &Type, + range: TextRange, + errors: &ErrorCollector, + seen: &mut SmallSet, + ) { + if let Type::Union(union) = actual { + for actual in union.members.iter() { + self.check_pydantic_model_validate_type_constraints( + expected_cls, + dataclass, + actual, + range, + errors, + seen, + ); + } + return; + } + if !seen.insert(expected_cls.clone()) { + return; + } + let Type::TypedDict(TypedDict::Anonymous(actual_fields)) = actual else { + seen.shift_remove(expected_cls); + return; + }; + let actual_fields: SmallMap<_, _> = actual_fields.fields.iter().cloned().collect(); + let kw_only_by_class = self.compute_kw_only_fields_by_class(expected_cls); + for (name, field, field_flags) in + self.iter_fields(expected_cls, dataclass, true, &kw_only_by_class) + { + let actual_field = field_flags + .init_by_name + .then(|| actual_fields.get(&name)) + .flatten() + .or_else(|| { + field_flags + .init_by_alias + .as_ref() + .and_then(|alias| actual_fields.get(alias)) + }); + let Some(actual_field) = actual_field else { + continue; + }; + if let Some(constraints) = PydanticRangeConstraints::from_keywords(&field_flags) { + self.emit_pydantic_argument_constraint( + &actual_field.ty, + &PydanticParamConstraint { + field_name: name.clone(), + constraints, + }, + range, + errors, + ); + } + self.check_pydantic_field_type_constraints( + &field.ty(), + &actual_field.ty, + range, + errors, + seen, + ); + } + seen.shift_remove(expected_cls); + } + + fn check_pydantic_field_type_constraints( + &self, + expected: &Type, + actual: &Type, + range: TextRange, + errors: &ErrorCollector, + seen: &mut SmallSet, + ) { + match (expected, actual) { + (Type::Union(expected), _) => { + for expected in expected.members.iter() { + self.check_pydantic_field_type_constraints( + expected, actual, range, errors, seen, + ); + } + } + (_, Type::Union(actual)) => { + for actual in actual.members.iter() { + self.check_pydantic_field_type_constraints( + expected, actual, range, errors, seen, + ); + } + } + (Type::ClassType(expected), Type::ClassType(actual)) + if expected.class_object() == self.stdlib.list_object() + && actual.class_object() == self.stdlib.list_object() + && let ([expected], [actual]) = + (expected.targs().as_slice(), actual.targs().as_slice()) => + { + self.check_pydantic_field_type_constraints(expected, actual, range, errors, seen); + } + (Type::ClassType(expected), Type::TypedDict(TypedDict::Anonymous(_))) => { + let metadata = self.get_metadata_for_class(expected.class_object()); + if metadata.is_pydantic_model() + && let Some(dataclass) = metadata.dataclass_metadata() + { + self.check_pydantic_model_validate_type_constraints( + expected.class_object(), + dataclass, + actual, + range, + errors, + seen, + ); + } + } + _ => {} + } + } + fn collect_pydantic_constraint_params( &self, cls: &Class, @@ -900,6 +1372,12 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { range: TextRange, errors: &ErrorCollector, ) { + if let Type::Union(union) = value_ty { + for value_ty in union.members.iter() { + self.emit_pydantic_argument_constraint(value_ty, info, range, errors); + } + return; + } let Some(value_lit) = int_literal_from_type(value_ty) else { return; }; diff --git a/pyrefly/lib/test/pydantic/field.rs b/pyrefly/lib/test/pydantic/field.rs index b9ad98745a..b945135bb8 100644 --- a/pyrefly/lib/test/pydantic/field.rs +++ b/pyrefly/lib/test/pydantic/field.rs @@ -524,6 +524,26 @@ class Bar(Foo): "#, ); +pydantic_testcase!( + test_model_validate_nested_field_range, + r#" +from pydantic import BaseModel, Field + +class Item(BaseModel): + sku: str + quantity: int = Field(ge=1) + +class Inventory(BaseModel): + items: list[Item] + +Inventory.model_validate({"items": [{"sku": "A-100", "quantity": 5}]}) +Inventory.model_validate({"items": [{"sku": "B-200", "quantity": -3}]}) # E: Argument value `Literal[-3]` violates Pydantic `ge` constraint `Literal[1]` for field `quantity` + +bad = {"items": [{"sku": "B-200", "quantity": -3}]} # E: Argument value `Literal[-3]` violates Pydantic `ge` constraint `Literal[1]` for field `quantity` +Inventory.model_validate(bad) +"#, +); + pydantic_testcase!( bug = "consider erroring on invalid5 and invalid6", test_discriminated_unions, From 87ad0c3dc103b0dacf6e59a8861e09a0890e1753 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 14 May 2026 16:44:09 +0900 Subject: [PATCH 2/3] use Gas fix some regression . --- pyrefly/lib/alt/class/dataclass.rs | 6 ++- pyrefly/lib/alt/class/pydantic.rs | 36 +++++++++----- pyrefly/lib/test/pydantic/field.rs | 77 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 14 deletions(-) diff --git a/pyrefly/lib/alt/class/dataclass.rs b/pyrefly/lib/alt/class/dataclass.rs index 13dff473bb..502dc855b8 100644 --- a/pyrefly/lib/alt/class/dataclass.rs +++ b/pyrefly/lib/alt/class/dataclass.rs @@ -221,7 +221,11 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { { fields.insert(MODEL_CONSTRUCT, model_construct); } - if metadata.is_pydantic_model() { + if metadata.is_pydantic_model() + && !self + .get_class_fields(cls) + .is_some_and(|fields| fields.contains(&Name::new_static("model_validate"))) + { let root_model_type = self .get_pydantic_root_model_type_via_mro(cls, metadata) .map(|(ty, _)| ty); diff --git a/pyrefly/lib/alt/class/pydantic.rs b/pyrefly/lib/alt/class/pydantic.rs index 3cd2707b8a..af7c4d605d 100644 --- a/pyrefly/lib/alt/class/pydantic.rs +++ b/pyrefly/lib/alt/class/pydantic.rs @@ -22,11 +22,10 @@ use pyrefly_types::function::FunctionKind; use pyrefly_types::keywords::DataclassFieldKeywords; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::Lit; -use pyrefly_types::types::Union; use pyrefly_types::typed_dict::AnonymousTypedDictInner; use pyrefly_types::typed_dict::TypedDict; use pyrefly_types::typed_dict::TypedDictField; -use pyrefly_types::types::Union; +use pyrefly_util::gas::Gas; use ruff_python_ast::DictItem; use ruff_python_ast::Expr; use ruff_python_ast::Keyword; @@ -273,6 +272,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { .unwrap_or_else(|| { self.pydantic_model_validate_dict_type(cls, dataclass, &mut seen) }), + self.heap.mk_class_type(self.stdlib.object().clone()), self.heap.mk_class_type(self.stdlib.dict( self.heap.mk_class_type(self.stdlib.str().clone()), self.heap.mk_any_explicit(), @@ -283,6 +283,12 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { Type::None, ); let params = vec![ + Param::PosOnly( + Some(Name::new_static("cls")), + self.heap + .mk_type_of(self.heap.mk_self_type(self.as_class_type_unchecked(cls))), + Required::Required, + ), Param::Pos(Name::new_static("obj"), obj_ty, Required::Required), Param::KwOnly( Name::new_static("strict"), @@ -310,9 +316,14 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { Required::Optional(None), ), ]; - ClassSynthesizedField::new_classvar(self.heap.mk_function(Function { - signature: Callable::list(ParamList::new(params), self.instantiate(cls)), - metadata: FuncMetadata::method(cls, Name::new_static("model_validate")), + let mut metadata = FuncMetadata::method(cls, Name::new_static("model_validate")); + metadata.flags.is_classmethod = true; + ClassSynthesizedField::new(self.heap.mk_function(Function { + signature: Callable::list( + ParamList::new(params), + self.heap.mk_self_type(self.as_class_type_unchecked(cls)), + ), + metadata, })) } @@ -346,8 +357,9 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { Type::ClassType(cls) } } - Type::Union(box Union { members, .. }) => self.unions( - members + Type::Union(union) => self.unions( + union + .members .into_iter() .map(|member| self.pydantic_model_validate_type(member, seen)) .collect(), @@ -1048,13 +1060,10 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { fn pydantic_model_validate_class(&self, callee: &Type) -> Option { callee.visit_toplevel_func_metadata(&|meta| { - let FunctionKind::Def(id) = &meta.kind else { - return None; - }; - if id.name.as_str() != "model_validate" { + if meta.kind.function_name().as_str() != "model_validate" { return None; } - let cls = id.cls.clone()?; + let cls = meta.kind.class()?; if self.get_metadata_for_class(&cls).is_pydantic_model() { Some(cls) } else { @@ -1070,7 +1079,8 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { let key = Key::BoundName(ShortIdentifier::expr_name(name)); let idx = self.bindings().key_to_idx_hashed_opt(Hashed::new(&key))?; let mut idx = idx; - for _ in 0..20 { + let mut gas = Gas::new(100); + while !gas.stop() { match self.bindings().get(idx) { Binding::Forward(forward) | Binding::PromoteForward(forward) diff --git a/pyrefly/lib/test/pydantic/field.rs b/pyrefly/lib/test/pydantic/field.rs index b945135bb8..1577375281 100644 --- a/pyrefly/lib/test/pydantic/field.rs +++ b/pyrefly/lib/test/pydantic/field.rs @@ -544,6 +544,83 @@ Inventory.model_validate(bad) "#, ); +pydantic_testcase!( + test_model_validate_from_attributes_accepts_object, + r#" +from pydantic import BaseModel + +class Model(BaseModel): + x: int + +def validate(obj: object) -> Model: + return Model.model_validate(obj, from_attributes=True) +"#, +); + +pydantic_testcase!( + test_custom_model_validate_is_not_overwritten, + r#" +from typing import Self +from pydantic import BaseModel + +class Model(BaseModel): + x: int + + @classmethod + def model_validate( + cls, + obj: object, + *, + strict: bool | None = None, + from_attributes: bool | None = None, + context: object | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Self: + return cls(x=0) + +def validate(obj: object) -> int: + return Model.model_validate(obj).x +"#, +); + +pydantic_testcase!( + test_super_model_validate_returns_self, + r#" +from typing import Self +from pydantic import BaseModel + +class Parent(BaseModel): + x: int + +class Child(Parent): + y: int + + @classmethod + def model_validate( + cls, + obj: object, + *, + strict: bool | None = None, + from_attributes: bool | None = None, + context: object | None = None, + by_alias: bool | None = None, + by_name: bool | None = None, + ) -> Self: + return super().model_validate( + obj, + strict=strict, + from_attributes=from_attributes, + context=context, + by_alias=by_alias, + by_name=by_name, + ) + +def validate(obj: object) -> int: + return Child.model_validate(obj, from_attributes=True).y +"#, +); + pydantic_testcase!( bug = "consider erroring on invalid5 and invalid6", test_discriminated_unions, From aadd1536bb893f2cf41d376910369711deeea4fd Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Mon, 5 Oct 2026 21:00:30 +0900 Subject: [PATCH 3/3] fix api change --- pyrefly/lib/alt/class/pydantic.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pyrefly/lib/alt/class/pydantic.rs b/pyrefly/lib/alt/class/pydantic.rs index af7c4d605d..4b49ae7f95 100644 --- a/pyrefly/lib/alt/class/pydantic.rs +++ b/pyrefly/lib/alt/class/pydantic.rs @@ -1059,17 +1059,16 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } fn pydantic_model_validate_class(&self, callee: &Type) -> Option { - callee.visit_toplevel_func_metadata(&|meta| { - if meta.kind.function_name().as_str() != "model_validate" { - return None; - } - let cls = meta.kind.class()?; - if self.get_metadata_for_class(&cls).is_pydantic_model() { - Some(cls) - } else { - None - } - }) + let meta = callee.toplevel_func_metadata()?; + if meta.kind.function_name().as_str() != "model_validate" { + return None; + } + let cls = meta.kind.class()?; + if self.get_metadata_for_class(&cls).is_pydantic_model() { + Some(cls) + } else { + None + } } fn pydantic_name_initializer<'b>(&'b self, actual: &Expr) -> Option<&'b Expr> {