diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index 65e4a7a1f..c477bae1d 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -4037,6 +4037,7 @@ def definition_reference_schema( 'list_type', 'tuple_type', 'set_type', + 'set_item_not_hashable', 'bool_type', 'bool_parsing', 'int_type', diff --git a/src/errors/types.rs b/src/errors/types.rs index 359f0c3de..0c75e1e24 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -268,6 +268,7 @@ error_types! { // --------------------- // set errors SetType {}, + SetItemNotHashable {}, // --------------------- // bool errors BoolType {}, @@ -513,6 +514,7 @@ impl ErrorType { Self::ListType {..} => "Input should be a valid list", Self::TupleType {..} => "Input should be a valid tuple", Self::SetType {..} => "Input should be a valid set", + Self::SetItemNotHashable {..} => "Set items should be hashable", Self::BoolType {..} => "Input should be a valid boolean", Self::BoolParsing {..} => "Input should be a valid boolean, unable to interpret input", Self::IntType {..} => "Input should be a valid integer", diff --git a/src/input/return_enums.rs b/src/input/return_enums.rs index 64f4f7c0f..6ac382430 100644 --- a/src/input/return_enums.rs +++ b/src/input/return_enums.rs @@ -194,6 +194,25 @@ impl BuildSet for Bound<'_, PyFrozenSet> { } } +fn validate_add<'py>( + py: Python<'py>, + set: &impl BuildSet, + item: impl BorrowInput<'py>, + state: &mut ValidationState<'_, 'py>, + validator: &CombinedValidator, +) -> ValResult<()> { + let validated_item = validator.validate(py, item.borrow_input(), state)?; + match set.build_add(validated_item) { + Ok(()) => Ok(()), + Err(err) => { + if err.matches(py, py.get_type::())? { + return Err(ValError::new(ErrorTypeDefaults::SetItemNotHashable, item)); + } + Err(err)? + } + } +} + #[allow(clippy::too_many_arguments)] pub(crate) fn validate_iter_to_set<'py>( py: Python<'py>, @@ -216,9 +235,8 @@ pub(crate) fn validate_iter_to_set<'py>( false => PartialMode::Off, }; let item = item_result.map_err(|e| any_next_error!(py, e, input, index))?; - match validator.validate(py, item.borrow_input(), state) { - Ok(item) => { - set.build_add(item)?; + match validate_add(py, set, item, state, validator) { + Ok(()) => { if let Some(max_length) = max_length { if set.build_len() > max_length { return Err(ValError::new( diff --git a/tests/test_errors.py b/tests/test_errors.py index 4760baad7..550f94e3d 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -306,6 +306,7 @@ def f(input_value, info): ('iteration_error', 'Error iterating over object, error: foobar', {'error': 'foobar'}), ('list_type', 'Input should be a valid list', None), ('tuple_type', 'Input should be a valid tuple', None), + ('set_item_not_hashable', 'Set items should be hashable', None), ('set_type', 'Input should be a valid set', None), ('bool_type', 'Input should be a valid boolean', None), ('bool_parsing', 'Input should be a valid boolean, unable to interpret input', None), diff --git a/tests/validators/test_set.py b/tests/validators/test_set.py index b5d35abf3..1bda3db70 100644 --- a/tests/validators/test_set.py +++ b/tests/validators/test_set.py @@ -104,6 +104,22 @@ def test_set_multiple_errors(): ] +def test_list_with_unhashable_items(): + v = SchemaValidator({'type': 'set'}) + + class Unhashable: + __hash__ = None + + unhashable = Unhashable() + + with pytest.raises(ValidationError) as exc_info: + v.validate_python([{'a': 'b'}, unhashable]) + assert exc_info.value.errors(include_url=False) == [ + {'type': 'set_item_not_hashable', 'loc': (0,), 'msg': 'Set items should be hashable', 'input': {'a': 'b'}}, + {'type': 'set_item_not_hashable', 'loc': (1,), 'msg': 'Set items should be hashable', 'input': unhashable}, + ] + + def generate_repeats(): for i in 1, 2, 3: yield i