Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions src/argument_markers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@ impl PydanticUndefinedType {
}

#[staticmethod]
pub fn new(py: Python) -> Py<Self> {
UNDEFINED_CELL
.get_or_init(py, || Py::new(py, PydanticUndefinedType {}).unwrap())
.clone_ref(py)
#[pyo3(name = "new")]
pub fn get(py: Python<'_>) -> &Py<Self> {
UNDEFINED_CELL.get_or_init(py, || Py::new(py, PydanticUndefinedType {}).unwrap())
}

fn __repr__(&self) -> &'static str {
Expand Down
6 changes: 3 additions & 3 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,7 +833,7 @@ impl<'py> KeywordArgs<'py> for PyKwargs<'py> {
&self,
key: &'k crate::lookup_key::LookupKey,
) -> ValResult<Option<(&'k crate::lookup_key::LookupPath, Self::Item<'_>)>> {
key.py_get_dict_item(&self.0)
key.py_get_dict_item(&self.0).map_err(Into::into)
}

fn iter(&self) -> impl Iterator<Item = ValResult<(Self::Key<'_>, Self::Item<'_>)>> {
Expand Down Expand Up @@ -864,8 +864,8 @@ impl<'py> ValidatedDict<'py> for GenericPyMapping<'_, 'py> {
key: &'k crate::lookup_key::LookupKey,
) -> ValResult<Option<(&'k crate::lookup_key::LookupPath, Self::Item<'_>)>> {
match self {
Self::Dict(dict) => key.py_get_dict_item(dict),
Self::Mapping(mapping) => key.py_get_mapping_item(mapping),
Self::Dict(dict) => key.py_get_dict_item(dict).map_err(Into::into),
Self::Mapping(mapping) => key.py_get_mapping_item(mapping).map_err(Into::into),
Self::GetAttr(obj, dict) => key.py_get_attr(obj, dict.as_ref()),
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ pub mod _pydantic_core {
m.add("build_profile", env!("PROFILE"))?;
m.add("build_info", build_info())?;
m.add("_recursion_limit", recursion_guard::RECURSION_GUARD_LIMIT)?;
m.add("PydanticUndefined", PydanticUndefinedType::new(m.py()))?;
m.add("PydanticUndefined", PydanticUndefinedType::get(m.py()))?;
Ok(())
}
}
190 changes: 69 additions & 121 deletions src/lookup_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use jiter::{JsonObject, JsonValue};
use crate::build_tools::py_schema_err;
use crate::errors::{py_err_string, ErrorType, LocItem, Location, ToErrorValue, ValError, ValLineError, ValResult};
use crate::input::StringMapping;
use crate::tools::{extract_i64, py_err};
use crate::tools::{extract_i64, mapping_get, py_err};

/// Used for getting items from python dicts, python objects, or JSON objects, in different ways
#[derive(Debug)]
Expand Down Expand Up @@ -89,44 +89,12 @@ impl LookupKey {
pub fn py_get_dict_item<'py, 's>(
&'s self,
dict: &Bound<'py, PyDict>,
) -> ValResult<Option<(&'s LookupPath, Bound<'py, PyAny>)>> {
match self {
Self::Simple(path) => match dict.get_item(&path.first_item.py_key)? {
Some(value) => {
debug_assert!(path.rest.is_empty());
Ok(Some((path, value)))
}
None => Ok(None),
},
Self::Choice { path1, path2, .. } => match dict.get_item(&path1.first_item.py_key)? {
Some(value) => {
debug_assert!(path1.rest.is_empty());
Ok(Some((path1, value)))
}
None => match dict.get_item(&path2.first_item.py_key)? {
Some(value) => {
debug_assert!(path2.rest.is_empty());
Ok(Some((path2, value)))
}
None => Ok(None),
},
},
Self::PathChoices(path_choices) => {
for path in path_choices {
let Some(first_value) = dict.get_item(&path.first_item.py_key)? else {
continue;
};
// iterate over the path and plug each value into the py_any from the last step,
// this could just be a loop but should be somewhat faster with a functional design
if let Some(v) = path.rest.iter().try_fold(first_value, |d, loc| loc.py_get_item(&d)) {
// Successfully found an item, return it
return Ok(Some((path, v)));
}
}
// got to the end of path_choices, without a match, return None
Ok(None)
}
}
) -> PyResult<Option<(&'s LookupPath, Bound<'py, PyAny>)>> {
self.get_impl(
dict,
|dict, path| dict.get_item(&path.py_key),
|d, loc| Ok(loc.py_get_item(&d)),
)
}

pub fn py_get_string_mapping_item<'py, 's>(
Expand All @@ -144,94 +112,23 @@ impl LookupKey {
pub fn py_get_mapping_item<'py, 's>(
&'s self,
dict: &Bound<'py, PyMapping>,
) -> ValResult<Option<(&'s LookupPath, Bound<'py, PyAny>)>> {
match self {
Self::Simple(path) => match dict.get_item(&path.first_item.py_key) {
Ok(value) => {
debug_assert!(path.rest.is_empty());
Ok(Some((path, value)))
}
_ => Ok(None),
},
Self::Choice { path1, path2, .. } => match dict.get_item(&path1.first_item.py_key) {
Ok(value) => {
debug_assert!(path1.rest.is_empty());
Ok(Some((path1, value)))
}
_ => match dict.get_item(&path2.first_item.py_key) {
Ok(value) => {
debug_assert!(path2.rest.is_empty());
Ok(Some((path2, value)))
}
_ => Ok(None),
},
},
Self::PathChoices(path_choices) => {
for path in path_choices {
let Some(first_value) = dict.get_item(&path.first_item.py_key).ok() else {
continue;
};
// iterate over the path and plug each value into the py_any from the last step,
// this could just be a loop but should be somewhat faster with a functional design
if let Some(v) = path.rest.iter().try_fold(first_value, |d, loc| loc.py_get_item(&d)) {
// Successfully found an item, return it
return Ok(Some((path, v)));
}
}
// got to the end of path_choices, without a match, return None
Ok(None)
}
}
) -> PyResult<Option<(&'s LookupPath, Bound<'py, PyAny>)>> {
self.get_impl(
dict,
|dict, path| mapping_get(dict, &path.py_key),
|d, loc| Ok(loc.py_get_item(&d)),
)
}

pub fn simple_py_get_attr<'py, 's>(
&'s self,
obj: &Bound<'py, PyAny>,
) -> PyResult<Option<(&'s LookupPath, Bound<'py, PyAny>)>> {
match self {
Self::Simple(path) => match py_get_attrs(obj, &path.first_item.py_key)? {
Some(value) => {
debug_assert!(path.rest.is_empty());
Ok(Some((path, value)))
}
None => Ok(None),
},
Self::Choice { path1, path2, .. } => match py_get_attrs(obj, &path1.first_item.py_key)? {
Some(value) => {
debug_assert!(path1.rest.is_empty());
Ok(Some((path1, value)))
}
None => match py_get_attrs(obj, &path2.first_item.py_key)? {
Some(value) => {
debug_assert!(path2.rest.is_empty());
Ok(Some((path2, value)))
}
None => Ok(None),
},
},
Self::PathChoices(path_choices) => {
'outer: for path in path_choices {
// similar to above, but using `py_get_attrs`, we can't use try_fold because of the extra Err
// so we have to loop manually
let Some(mut v) = path.first_item.py_get_attrs(obj)? else {
continue;
};
for loc in &path.rest {
v = match loc.py_get_attrs(&v) {
Ok(Some(v)) => v,
Ok(None) => {
continue 'outer;
}
Err(e) => return Err(e),
}
}
// Successfully found an item, return it
return Ok(Some((path, v)));
}
// got to the end of path_choices, without a match, return None
Ok(None)
}
}
self.get_impl(
obj,
|obj, path| py_get_attrs(obj, &path.py_key),
|d, loc| loc.py_get_attrs(&d),
)
}

pub fn py_get_attr<'py, 's>(
Expand Down Expand Up @@ -324,6 +221,57 @@ impl LookupKey {
}
}

fn get_impl<'s, 'a, SourceT, OutputT: 'a>(
&'s self,
source: &'a SourceT,
lookup: impl Fn(&'a SourceT, &'s PathItemString) -> PyResult<Option<OutputT>>,
nested_lookup: impl Fn(OutputT, &'s PathItem) -> PyResult<Option<OutputT>>,
) -> PyResult<Option<(&'s LookupPath, OutputT)>> {
match self {
Self::Simple(path) => match lookup(source, &path.first_item)? {
Some(value) => {
debug_assert!(path.rest.is_empty());
Ok(Some((path, value)))
}
None => Ok(None),
},
Self::Choice { path1, path2, .. } => match lookup(source, &path1.first_item)? {
Some(value) => {
debug_assert!(path1.rest.is_empty());
Ok(Some((path1, value)))
}
None => match lookup(source, &path2.first_item)? {
Some(value) => {
debug_assert!(path2.rest.is_empty());
Ok(Some((path2, value)))
}
None => Ok(None),
},
},
Self::PathChoices(path_choices) => {
'choices: for path in path_choices {
let Some(mut value) = lookup(source, &path.first_item)? else {
continue;
};

// iterate over the path and plug each value into the value from the last step
for loc in &path.rest {
value = match nested_lookup(value, loc) {
Ok(Some(v)) => v,
// this choice did not match, try the next one
Ok(None) => continue 'choices,
Err(e) => return Err(e),
}
}
// Successfully found an item, return it
return Ok(Some((path, value)));
}
// got to the end of path_choices, without a match, return None
Ok(None)
}
}
}

pub fn error(
&self,
error_type: ErrorType,
Expand Down
14 changes: 13 additions & 1 deletion src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use num_bigint::BigInt;

use pyo3::exceptions::PyKeyError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyString};
use pyo3::types::{PyDict, PyMapping, PyString};
use pyo3::{intern, FromPyObject};

use crate::input::Int;
use crate::PydanticUndefinedType;
use jiter::{cached_py_string, StringCacheMode};

pub trait SchemaDict<'py> {
Expand Down Expand Up @@ -190,3 +191,14 @@ pub fn write_truncated_to_limited_bytes<F: fmt::Write>(f: &mut F, val: &str, max
write!(f, "{val}")
}
}

/// Implementation of `mapping.get(key, PydanticUndefined)` which returns `None` if the key is not found
pub fn mapping_get<'py>(
mapping: &Bound<'py, PyMapping>,
key: impl IntoPyObject<'py>,
) -> PyResult<Option<Bound<'py, PyAny>>> {
let undefined = PydanticUndefinedType::get(mapping.py());
mapping
.call_method1(intern!(mapping.py(), "get"), (key, undefined))
.map(|value| if value.is(undefined) { None } else { Some(value) })
}
2 changes: 1 addition & 1 deletion src/validators/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl BuildValidator for ModelValidator {
frozen: schema.get_as(intern!(py, "frozen"))?.unwrap_or(false),
custom_init: schema.get_as(intern!(py, "custom_init"))?.unwrap_or(false),
root_model: schema.get_as(intern!(py, "root_model"))?.unwrap_or(false),
undefined: PydanticUndefinedType::new(py).into_any(),
undefined: PydanticUndefinedType::get(py).clone_ref(schema.py()).into_any(),
// Get the class's `__name__`, not using `class.qualname()`
name,
})
Expand Down
4 changes: 2 additions & 2 deletions src/validators/with_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl BuildValidator for WithDefaultValidator {
validate_default: schema_or_config_same(schema, config, intern!(py, "validate_default"))?.unwrap_or(false),
copy_default,
name,
undefined: PydanticUndefinedType::new(py).into_any(),
undefined: PydanticUndefinedType::get(py).clone_ref(schema.py()).into_any(),
})
.into())
}
Expand Down Expand Up @@ -187,7 +187,7 @@ impl Validator for WithDefaultValidator {
// in an unhelpul error.
let mut err = ValError::new(
ErrorTypeDefaults::DefaultFactoryNotCalled,
PydanticUndefinedType::new(py).into_bound(py).into_any(),
PydanticUndefinedType::get(py).bind(py).clone().into_any(),
);
if let Some(outer_loc) = outer_loc {
err = err.with_outer_location(outer_loc);
Expand Down
27 changes: 27 additions & 0 deletions tests/validators/test_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import sys
from collections import defaultdict
from copy import deepcopy
from decimal import Decimal
from typing import Any, Callable, Union
Expand Down Expand Up @@ -1359,3 +1360,29 @@ class MyModel:
v.validate_assignment(m, 'enum_field', Decimal(1))
v.validate_assignment(m, 'enum_field_2', Decimal(2))
v.validate_assignment(m, 'enum_field_3', IntWrappable(3))


def test_model_from_defaultdict():
# https://github.com/pydantic/pydantic/issues/12376

class MyModel:
def __init__(self, **kwargs: Any) -> None:
self.__dict__.update(kwargs)

schema = core_schema.model_schema(
MyModel, core_schema.model_fields_schema({'field_a': core_schema.model_field(core_schema.int_schema())})
)

v = SchemaValidator(schema)
with pytest.raises(ValidationError) as exc_info:
# the defaultdict should not provide default values for missing fields
v.validate_python(defaultdict(int))

assert exc_info.value.errors(include_url=False) == [
{
'type': 'missing',
'loc': ('field_a',),
'msg': 'Field required',
'input': defaultdict(int),
}
]
Loading