Skip to content

Commit fb4f204

Browse files
committed
Handle AttributeError subclasses with from_attributes
Backport of: #13096
1 parent ca3ddd1 commit fb4f204

2 files changed

Lines changed: 39 additions & 3 deletions

File tree

‎pydantic-core/src/lookup_key.rs‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::convert::Infallible;
33
use std::fmt;
44

55
use pyo3::IntoPyObjectExt;
6-
use pyo3::exceptions::{PyTypeError, PyValueError};
6+
use pyo3::exceptions::{PyAttributeError, PyTypeError, PyValueError};
77
use pyo3::prelude::*;
88
use pyo3::pybacked::PyBackedStr;
99
use pyo3::types::{PyDict, PyList, PyMapping, PyString};
@@ -111,7 +111,7 @@ impl LookupPath {
111111
}
112112

113113
pub fn simple_py_get_attr<'py>(&self, obj: &Bound<'py, PyAny>) -> PyResult<Option<Bound<'py, PyAny>>> {
114-
self.get_impl(obj, PyAnyMethods::getattr_opt, |d, loc| loc.py_get_attrs(&d))
114+
self.get_impl(obj, py_get_attrs, |d, loc| loc.py_get_attrs(&d))
115115
}
116116

117117
pub fn py_get_attr<'py>(
@@ -366,7 +366,7 @@ impl PathItemString {
366366
// FIXME: should this instance check be for Mapping instead of Dict, and use `mapping_get`?
367367
Ok(obj.get_item(self).ok())
368368
} else {
369-
obj.getattr_opt(self)
369+
py_get_attrs(obj, self)
370370
}
371371
}
372372
}
@@ -454,3 +454,20 @@ impl LookupType {
454454
(self as u8 & other as u8) != 0
455455
}
456456
}
457+
458+
// TODO replace with `PyAnyMethods::getattr_opt` once https://github.com/PyO3/pyo3/pull/5985 is merged:
459+
fn py_get_attrs<'py, N>(obj: &Bound<'py, PyAny>, attr_name: N) -> PyResult<Option<Bound<'py, PyAny>>>
460+
where
461+
N: IntoPyObject<'py, Target = PyString>,
462+
{
463+
match obj.getattr(attr_name) {
464+
Ok(attr) => Ok(Some(attr)),
465+
Err(err) => {
466+
if err.get_type(obj.py()).is_subclass_of::<PyAttributeError>()? {
467+
Ok(None)
468+
} else {
469+
Err(err)
470+
}
471+
}
472+
}
473+
}

‎tests/test_main.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3056,6 +3056,25 @@ class UnrelatedClass:
30563056
assert res == ModelFromAttributesFalse(x=1)
30573057

30583058

3059+
def test_from_attributes_attributeerror_subclass() -> None:
3060+
"""https://github.com/pydantic/pydantic/issues/13092"""
3061+
3062+
class SubAttributeError(AttributeError):
3063+
pass
3064+
3065+
class Model(BaseModel, from_attributes=True):
3066+
field: int | None = None
3067+
3068+
class Obj:
3069+
@property
3070+
def child(self):
3071+
raise SubAttributeError()
3072+
3073+
m = Model.model_validate(Obj())
3074+
3075+
assert m.field is None
3076+
3077+
30593078
@pytest.mark.parametrize(
30603079
'field_type,input_value,expected,raises_match,strict',
30613080
[

0 commit comments

Comments
 (0)