Skip to content

Commit 49d77de

Browse files
authored
add json_function to is_instance validator (#293)
1 parent ad12e47 commit 49d77de

File tree

6 files changed

+69
-7
lines changed

6 files changed

+69
-7
lines changed

pydantic_core/core_schema.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,14 +318,22 @@ class IsInstanceSchema(TypedDict, total=False):
318318
type: Required[Literal['is-instance']]
319319
cls: Required[Type[Any]]
320320
json_types: Set[JsonType]
321+
json_function: Callable[[Any], Any]
321322
ref: str
322323
extra: Any
323324

324325

325326
def is_instance_schema(
326-
cls: Type[Any], *, json_types: Set[JsonType] | None = None, ref: str | None = None, extra: Any = None
327+
cls: Type[Any],
328+
*,
329+
json_types: Set[JsonType] | None = None,
330+
json_function: Callable[[Any], Any] | None = None,
331+
ref: str | None = None,
332+
extra: Any = None,
327333
) -> IsInstanceSchema:
328-
return dict_not_none(type='is-instance', cls=cls, json_types=json_types, ref=ref, extra=extra)
334+
return dict_not_none(
335+
type='is-instance', cls=cls, json_types=json_types, json_function=json_function, ref=ref, extra=extra
336+
)
329337

330338

331339
class CallableSchema(TypedDict, total=False):

src/input/input_abstract.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,25 @@ use super::datetime::{EitherDate, EitherDateTime, EitherTime, EitherTimedelta};
99
use super::return_enums::{EitherBytes, EitherString};
1010
use super::{GenericArguments, GenericCollection, GenericIterator, GenericMapping, JsonInput};
1111

12+
pub enum InputType {
13+
Python,
14+
Json,
15+
String,
16+
}
17+
18+
impl InputType {
19+
pub fn is_json(&self) -> bool {
20+
matches!(self, Self::Json)
21+
}
22+
}
23+
1224
/// all types have three methods: `validate_*`, `strict_*`, `lax_*`
1325
/// the convention is to either implement:
1426
/// * `strict_*` & `lax_*` if they have different behavior
1527
/// * or, `validate_*` and `strict_*` to just call `validate_*` if the behavior for strict and lax is the same
1628
pub trait Input<'a>: fmt::Debug + ToPyObject {
29+
fn get_type(&self) -> &'static InputType;
30+
1731
fn as_loc_item(&self) -> LocItem;
1832

1933
fn as_error_value(&'a self) -> InputValue<'a>;

src/input/input_json.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@ use pyo3::prelude::*;
22
use pyo3::types::PyType;
33

44
use crate::errors::{ErrorKind, InputValue, LocItem, ValError, ValResult};
5-
use crate::input::JsonType;
65

76
use super::datetime::{
87
bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, float_as_datetime, float_as_duration,
98
float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime, EitherTime,
109
};
10+
use super::input_abstract::InputType;
1111
use super::parse_json::JsonArray;
1212
use super::shared::{float_as_int, int_as_bool, map_json_err, str_as_bool, str_as_int};
1313
use super::{
1414
EitherBytes, EitherString, EitherTimedelta, GenericArguments, GenericCollection, GenericIterator, GenericMapping,
15-
Input, JsonArgs, JsonInput,
15+
Input, JsonArgs, JsonInput, JsonType,
1616
};
1717

1818
impl<'a> Input<'a> for JsonInput {
19+
fn get_type(&self) -> &'static InputType {
20+
&InputType::Json
21+
}
22+
1923
/// This is required by since JSON object keys are always strings, I don't think it can be called
2024
#[cfg_attr(has_no_coverage, no_coverage)]
2125
fn as_loc_item(&self) -> LocItem {
@@ -299,6 +303,10 @@ impl<'a> Input<'a> for JsonInput {
299303

300304
/// Required for Dict keys so the string can behave like an Input
301305
impl<'a> Input<'a> for String {
306+
fn get_type(&self) -> &'static InputType {
307+
&InputType::String
308+
}
309+
302310
fn as_loc_item(&self) -> LocItem {
303311
self.to_string().into()
304312
}

src/input/input_python.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use super::datetime::{
1919
float_as_duration, float_as_time, int_as_datetime, int_as_duration, int_as_time, EitherDate, EitherDateTime,
2020
EitherTime,
2121
};
22+
use super::input_abstract::InputType;
2223
use super::shared::{float_as_int, int_as_bool, map_json_err, str_as_bool, str_as_int};
2324
use super::{
2425
py_string_str, repr_string, EitherBytes, EitherString, EitherTimedelta, GenericArguments, GenericCollection,
@@ -55,6 +56,10 @@ macro_rules! extract_dict_iter {
5556
}
5657

5758
impl<'a> Input<'a> for PyAny {
59+
fn get_type(&self) -> &'static InputType {
60+
&InputType::Python
61+
}
62+
5863
fn as_loc_item(&self) -> LocItem {
5964
if let Ok(py_str) = self.cast_as::<PyString>() {
6065
py_str.to_string_lossy().as_ref().into()

src/validators/is_instance.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ use crate::errors::{ErrorKind, ValError, ValResult};
77
use crate::input::{Input, JsonType};
88
use crate::recursion_guard::RecursionGuard;
99

10+
use super::function::convert_err;
1011
use super::{BuildContext, BuildValidator, CombinedValidator, Extra, Validator};
1112

1213
#[derive(Debug, Clone)]
1314
pub struct IsInstanceValidator {
1415
class: Py<PyType>,
1516
json_types: u8,
17+
json_function: Option<PyObject>,
1618
class_repr: String,
1719
name: String,
1820
}
@@ -25,16 +27,18 @@ impl BuildValidator for IsInstanceValidator {
2527
_config: Option<&PyDict>,
2628
_build_context: &mut BuildContext,
2729
) -> PyResult<CombinedValidator> {
28-
let class: &PyType = schema.get_as_req(intern!(schema.py(), "cls"))?;
30+
let py = schema.py();
31+
let class: &PyType = schema.get_as_req(intern!(py, "cls"))?;
2932
let class_repr = class.name()?.to_string();
3033
let name = format!("{}[{}]", Self::EXPECTED_TYPE, class_repr);
31-
let json_types = match schema.get_as::<&PySet>(intern!(schema.py(), "json_types"))? {
34+
let json_types = match schema.get_as::<&PySet>(intern!(py, "json_types"))? {
3235
Some(s) => JsonType::combine(s)?,
3336
None => 0,
3437
};
3538
Ok(Self {
3639
class: class.into(),
3740
json_types,
41+
json_function: schema.get_item(intern!(py, "json_function")).map(|f| f.into_py(py)),
3842
class_repr,
3943
name,
4044
}
@@ -52,7 +56,16 @@ impl Validator for IsInstanceValidator {
5256
_recursion_guard: &'s mut RecursionGuard,
5357
) -> ValResult<'data, PyObject> {
5458
match input.is_instance(self.class.as_ref(py), self.json_types)? {
55-
true => Ok(input.to_object(py)),
59+
true => {
60+
if input.get_type().is_json() {
61+
if let Some(ref json_function) = self.json_function {
62+
return json_function
63+
.call1(py, (input.to_object(py),))
64+
.map_err(|e| convert_err(py, e, input));
65+
}
66+
}
67+
Ok(input.to_object(py))
68+
}
5669
false => Err(ValError::new(
5770
ErrorKind::IsInstanceOf {
5871
class: self.class_repr.clone(),

tests/validators/test_is_instance.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from collections import deque
2+
13
import pytest
24

35
from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema
@@ -183,3 +185,15 @@ def test_json_mask():
183185
assert 'json_types:0' in plain_repr(SchemaValidator(core_schema.is_instance_schema(str, json_types=set())))
184186
v = SchemaValidator(core_schema.is_instance_schema(str, json_types={'list', 'dict'}))
185187
assert 'json_types:6' in plain_repr(v) # 2 + 4
188+
189+
190+
def test_json_function():
191+
v = SchemaValidator(core_schema.is_instance_schema(deque, json_types={'list'}, json_function=deque))
192+
output = v.validate_python(deque([1, 2, 3]))
193+
assert output == deque([1, 2, 3])
194+
output = v.validate_json('[1, 2, 3]')
195+
assert output == deque([1, 2, 3])
196+
with pytest.raises(ValidationError, match=r'Input should be an instance of deque \[kind=is_instance_of,'):
197+
v.validate_python([1, 2, 3])
198+
with pytest.raises(ValidationError, match=r'Input should be an instance of deque \[kind=is_instance_of,'):
199+
v.validate_json('{"1": 2}')

0 commit comments

Comments
 (0)