diff --git a/python/pydantic_core/core_schema.py b/python/pydantic_core/core_schema.py index b1c4c13b5..cd1525b6a 100644 --- a/python/pydantic_core/core_schema.py +++ b/python/pydantic_core/core_schema.py @@ -3964,6 +3964,7 @@ def definition_reference_schema( 'no_such_attribute', 'json_invalid', 'json_type', + 'needs_python_object', 'recursion_loop', 'missing', 'frozen_field', diff --git a/src/errors/types.rs b/src/errors/types.rs index ec129a63a..07186003d 100644 --- a/src/errors/types.rs +++ b/src/errors/types.rs @@ -167,6 +167,7 @@ error_types! { error: {ctx_type: String, ctx_fn: field_from_context}, }, JsonType {}, + NeedsPythonObject { method_name: {ctx_type: String, ctx_fn: field_from_context} }, // --------------------- // recursion error RecursionLoop {}, @@ -477,6 +478,7 @@ impl ErrorType { Self::NoSuchAttribute {..} => "Object has no attribute '{attribute}'", Self::JsonInvalid {..} => "Invalid JSON: {error}", Self::JsonType {..} => "JSON input should be string, bytes or bytearray", + Self::NeedsPythonObject {..} => "Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead", Self::RecursionLoop {..} => "Recursion error - cyclic reference detected", Self::Missing {..} => "Field required", Self::FrozenField {..} => "Field is frozen", @@ -626,6 +628,7 @@ impl ErrorType { match self { Self::NoSuchAttribute { attribute, .. } => render!(tmpl, attribute), Self::JsonInvalid { error, .. } => render!(tmpl, error), + Self::NeedsPythonObject { method_name, .. } => render!(tmpl, method_name), Self::GetAttributeError { error, .. } => render!(tmpl, error), Self::ModelType { class_name, .. } => render!(tmpl, class_name), Self::DataclassType { class_name, .. } => render!(tmpl, class_name), diff --git a/src/validators/is_instance.rs b/src/validators/is_instance.rs index d9190f33c..e62301587 100644 --- a/src/validators/is_instance.rs +++ b/src/validators/is_instance.rs @@ -1,4 +1,3 @@ -use pyo3::exceptions::PyNotImplementedError; use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; @@ -56,10 +55,14 @@ impl Validator for IsInstanceValidator { _state: &mut ValidationState<'_, 'py>, ) -> ValResult { let Some(obj) = input.as_python() else { - return Err(ValError::InternalErr(PyNotImplementedError::new_err( - "Cannot check isinstance when validating from json, \ - use a JsonOrPython validator instead.", - ))); + let method_name = "isinstance".to_string(); + return Err(ValError::new( + ErrorType::NeedsPythonObject { + context: None, + method_name, + }, + input, + )); }; match obj.is_instance(self.class.bind(py))? { true => Ok(obj.clone().unbind()), diff --git a/src/validators/is_subclass.rs b/src/validators/is_subclass.rs index fdfea5b75..ed587a50c 100644 --- a/src/validators/is_subclass.rs +++ b/src/validators/is_subclass.rs @@ -1,4 +1,3 @@ -use pyo3::exceptions::PyNotImplementedError; use pyo3::intern; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; @@ -51,10 +50,14 @@ impl Validator for IsSubclassValidator { _state: &mut ValidationState<'_, 'py>, ) -> ValResult { let Some(obj) = input.as_python() else { - return Err(ValError::InternalErr(PyNotImplementedError::new_err( - "Cannot check issubclass when validating from json, \ - use a JsonOrPython validator instead.", - ))); + let method_name = "issubclass".to_string(); + return Err(ValError::new( + ErrorType::NeedsPythonObject { + context: None, + method_name, + }, + input, + )); }; match obj.downcast::() { Ok(py_type) if py_type.is_subclass(self.class.bind(py))? => Ok(obj.clone().unbind()), diff --git a/tests/test_errors.py b/tests/test_errors.py index b8265f04e..9ac993031 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -257,6 +257,11 @@ def f(input_value, info): ('no_such_attribute', "Object has no attribute 'wrong_name'", {'attribute': 'wrong_name'}), ('json_invalid', 'Invalid JSON: foobar', {'error': 'foobar'}), ('json_type', 'JSON input should be string, bytes or bytearray', None), + ( + 'needs_python_object', + 'Cannot check `isinstance` when validating from json, use a JsonOrPython validator instead', + {'method_name': 'isinstance'}, + ), ('recursion_loop', 'Recursion error - cyclic reference detected', None), ('model_type', 'Input should be a valid dictionary or instance of Foobar', {'class_name': 'Foobar'}), ('model_attributes_type', 'Input should be a valid dictionary or object to extract fields from', None), @@ -506,10 +511,10 @@ def test_all_errors(): 'example_context': None, }, { - 'type': 'recursion_loop', - 'message_template_python': 'Recursion error - cyclic reference detected', - 'example_message_python': 'Recursion error - cyclic reference detected', - 'example_context': None, + 'type': 'needs_python_object', + 'message_template_python': 'Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead', + 'example_message_python': 'Cannot check `` when validating from json, use a JsonOrPython validator instead', + 'example_context': {'method_name': ''}, }, ] diff --git a/tests/validators/test_is_instance.py b/tests/validators/test_is_instance.py index 7a5351554..9bce323a7 100644 --- a/tests/validators/test_is_instance.py +++ b/tests/validators/test_is_instance.py @@ -19,8 +19,9 @@ class Spam: def test_validate_json() -> None: v = SchemaValidator({'type': 'is-instance', 'cls': Foo}) - with pytest.raises(NotImplementedError, match='use a JsonOrPython validator instead'): + with pytest.raises(ValidationError) as exc_info: v.validate_json('"foo"') + assert exc_info.value.errors()[0]['type'] == 'needs_python_object' def test_is_instance(): @@ -175,11 +176,9 @@ def test_is_instance_json_type_before_validator(): schema = core_schema.is_instance_schema(type) v = SchemaValidator(schema) - with pytest.raises( - NotImplementedError, - match='Cannot check isinstance when validating from json, use a JsonOrPython validator instead.', - ): + with pytest.raises(ValidationError) as exc_info: v.validate_json('null') + assert exc_info.value.errors()[0]['type'] == 'needs_python_object' # now wrap in a before validator def set_type_to_int(input: None) -> type: diff --git a/tests/validators/test_is_subclass.py b/tests/validators/test_is_subclass.py index cb940ab34..43e65b6b1 100644 --- a/tests/validators/test_is_subclass.py +++ b/tests/validators/test_is_subclass.py @@ -77,3 +77,10 @@ def test_custom_repr(): 'ctx': {'class': 'Spam'}, } ] + + +def test_is_subclass_json() -> None: + v = SchemaValidator(core_schema.is_subclass_schema(Foo)) + with pytest.raises(ValidationError) as exc_info: + v.validate_json("'Foo'") + assert exc_info.value.errors()[0]['type'] == 'needs_python_object'