Skip to content

Commit b773c9d

Browse files
Relax intensity of error for issubclass and isinstance schemas from json (#1479)
1 parent dc4846e commit b773c9d

File tree

7 files changed

+40
-19
lines changed

7 files changed

+40
-19
lines changed

python/pydantic_core/core_schema.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3964,6 +3964,7 @@ def definition_reference_schema(
39643964
'no_such_attribute',
39653965
'json_invalid',
39663966
'json_type',
3967+
'needs_python_object',
39673968
'recursion_loop',
39683969
'missing',
39693970
'frozen_field',

src/errors/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ error_types! {
167167
error: {ctx_type: String, ctx_fn: field_from_context},
168168
},
169169
JsonType {},
170+
NeedsPythonObject { method_name: {ctx_type: String, ctx_fn: field_from_context} },
170171
// ---------------------
171172
// recursion error
172173
RecursionLoop {},
@@ -477,6 +478,7 @@ impl ErrorType {
477478
Self::NoSuchAttribute {..} => "Object has no attribute '{attribute}'",
478479
Self::JsonInvalid {..} => "Invalid JSON: {error}",
479480
Self::JsonType {..} => "JSON input should be string, bytes or bytearray",
481+
Self::NeedsPythonObject {..} => "Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead",
480482
Self::RecursionLoop {..} => "Recursion error - cyclic reference detected",
481483
Self::Missing {..} => "Field required",
482484
Self::FrozenField {..} => "Field is frozen",
@@ -626,6 +628,7 @@ impl ErrorType {
626628
match self {
627629
Self::NoSuchAttribute { attribute, .. } => render!(tmpl, attribute),
628630
Self::JsonInvalid { error, .. } => render!(tmpl, error),
631+
Self::NeedsPythonObject { method_name, .. } => render!(tmpl, method_name),
629632
Self::GetAttributeError { error, .. } => render!(tmpl, error),
630633
Self::ModelType { class_name, .. } => render!(tmpl, class_name),
631634
Self::DataclassType { class_name, .. } => render!(tmpl, class_name),

src/validators/is_instance.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use pyo3::exceptions::PyNotImplementedError;
21
use pyo3::intern;
32
use pyo3::prelude::*;
43
use pyo3::types::{PyDict, PyType};
@@ -56,10 +55,14 @@ impl Validator for IsInstanceValidator {
5655
_state: &mut ValidationState<'_, 'py>,
5756
) -> ValResult<PyObject> {
5857
let Some(obj) = input.as_python() else {
59-
return Err(ValError::InternalErr(PyNotImplementedError::new_err(
60-
"Cannot check isinstance when validating from json, \
61-
use a JsonOrPython validator instead.",
62-
)));
58+
let method_name = "isinstance".to_string();
59+
return Err(ValError::new(
60+
ErrorType::NeedsPythonObject {
61+
context: None,
62+
method_name,
63+
},
64+
input,
65+
));
6366
};
6467
match obj.is_instance(self.class.bind(py))? {
6568
true => Ok(obj.clone().unbind()),

src/validators/is_subclass.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use pyo3::exceptions::PyNotImplementedError;
21
use pyo3::intern;
32
use pyo3::prelude::*;
43
use pyo3::types::{PyDict, PyType};
@@ -51,10 +50,14 @@ impl Validator for IsSubclassValidator {
5150
_state: &mut ValidationState<'_, 'py>,
5251
) -> ValResult<PyObject> {
5352
let Some(obj) = input.as_python() else {
54-
return Err(ValError::InternalErr(PyNotImplementedError::new_err(
55-
"Cannot check issubclass when validating from json, \
56-
use a JsonOrPython validator instead.",
57-
)));
53+
let method_name = "issubclass".to_string();
54+
return Err(ValError::new(
55+
ErrorType::NeedsPythonObject {
56+
context: None,
57+
method_name,
58+
},
59+
input,
60+
));
5861
};
5962
match obj.downcast::<PyType>() {
6063
Ok(py_type) if py_type.is_subclass(self.class.bind(py))? => Ok(obj.clone().unbind()),

tests/test_errors.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ def f(input_value, info):
257257
('no_such_attribute', "Object has no attribute 'wrong_name'", {'attribute': 'wrong_name'}),
258258
('json_invalid', 'Invalid JSON: foobar', {'error': 'foobar'}),
259259
('json_type', 'JSON input should be string, bytes or bytearray', None),
260+
(
261+
'needs_python_object',
262+
'Cannot check `isinstance` when validating from json, use a JsonOrPython validator instead',
263+
{'method_name': 'isinstance'},
264+
),
260265
('recursion_loop', 'Recursion error - cyclic reference detected', None),
261266
('model_type', 'Input should be a valid dictionary or instance of Foobar', {'class_name': 'Foobar'}),
262267
('model_attributes_type', 'Input should be a valid dictionary or object to extract fields from', None),
@@ -506,10 +511,10 @@ def test_all_errors():
506511
'example_context': None,
507512
},
508513
{
509-
'type': 'recursion_loop',
510-
'message_template_python': 'Recursion error - cyclic reference detected',
511-
'example_message_python': 'Recursion error - cyclic reference detected',
512-
'example_context': None,
514+
'type': 'needs_python_object',
515+
'message_template_python': 'Cannot check `{method_name}` when validating from json, use a JsonOrPython validator instead',
516+
'example_message_python': 'Cannot check `` when validating from json, use a JsonOrPython validator instead',
517+
'example_context': {'method_name': ''},
513518
},
514519
]
515520

tests/validators/test_is_instance.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ class Spam:
1919

2020
def test_validate_json() -> None:
2121
v = SchemaValidator({'type': 'is-instance', 'cls': Foo})
22-
with pytest.raises(NotImplementedError, match='use a JsonOrPython validator instead'):
22+
with pytest.raises(ValidationError) as exc_info:
2323
v.validate_json('"foo"')
24+
assert exc_info.value.errors()[0]['type'] == 'needs_python_object'
2425

2526

2627
def test_is_instance():
@@ -175,11 +176,9 @@ def test_is_instance_json_type_before_validator():
175176
schema = core_schema.is_instance_schema(type)
176177
v = SchemaValidator(schema)
177178

178-
with pytest.raises(
179-
NotImplementedError,
180-
match='Cannot check isinstance when validating from json, use a JsonOrPython validator instead.',
181-
):
179+
with pytest.raises(ValidationError) as exc_info:
182180
v.validate_json('null')
181+
assert exc_info.value.errors()[0]['type'] == 'needs_python_object'
183182

184183
# now wrap in a before validator
185184
def set_type_to_int(input: None) -> type:

tests/validators/test_is_subclass.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,10 @@ def test_custom_repr():
7777
'ctx': {'class': 'Spam'},
7878
}
7979
]
80+
81+
82+
def test_is_subclass_json() -> None:
83+
v = SchemaValidator(core_schema.is_subclass_schema(Foo))
84+
with pytest.raises(ValidationError) as exc_info:
85+
v.validate_json("'Foo'")
86+
assert exc_info.value.errors()[0]['type'] == 'needs_python_object'

0 commit comments

Comments
 (0)