feat(python): support validation of dict subclasses
Currently, attemping to validate an instance of a dict subclass will raise a `ValueError`. This should not be happening, since the instance is still dict. Achieve compatibility by checking the subclasses' inheritance tree, and treat the instance like a dict if that check passes.
This commit is contained in:
parent
9771bc227c
commit
829ca19963
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support subclasses of Python `dict`s [#427](https://github.com/Stranger6667/jsonschema-rs/issues/427)
|
||||||
|
|
||||||
## [0.17.2] - 2024-03-03
|
## [0.17.2] - 2024-03-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -67,6 +67,19 @@ fn is_enum_subclass(object_type: *mut pyo3::ffi::PyTypeObject) -> bool {
|
||||||
unsafe { (*(object_type.cast::<ffi::PyTypeObject>())).ob_type == types::ENUM_TYPE }
|
unsafe { (*(object_type.cast::<ffi::PyTypeObject>())).ob_type == types::ENUM_TYPE }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_dict_subclass(object_type: *mut pyo3::ffi::PyTypeObject) -> bool {
|
||||||
|
// traverse the object's inheritance chain to check if it's a dict subclass
|
||||||
|
let mut current_type = object_type;
|
||||||
|
while !current_type.is_null() {
|
||||||
|
if current_type == unsafe { types::DICT_TYPE } {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current_type = unsafe { (*current_type).tp_base }
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn get_object_type_from_object(object: *mut pyo3::ffi::PyObject) -> ObjectType {
|
fn get_object_type_from_object(object: *mut pyo3::ffi::PyObject) -> ObjectType {
|
||||||
unsafe {
|
unsafe {
|
||||||
let object_type = Py_TYPE(object);
|
let object_type = Py_TYPE(object);
|
||||||
|
@ -110,6 +123,8 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType
|
||||||
ObjectType::Dict
|
ObjectType::Dict
|
||||||
} else if is_enum_subclass(object_type) {
|
} else if is_enum_subclass(object_type) {
|
||||||
ObjectType::Enum
|
ObjectType::Enum
|
||||||
|
} else if is_dict_subclass(object_type) {
|
||||||
|
ObjectType::Dict
|
||||||
} else {
|
} else {
|
||||||
ObjectType::Unknown(get_type_name(object_type).to_string())
|
ObjectType::Unknown(get_type_name(object_type).to_string())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from collections import namedtuple
|
from collections import namedtuple, OrderedDict
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
@ -248,3 +248,30 @@ def test_dict_with_non_str_keys():
|
||||||
with pytest.raises(ValueError) as exec_info:
|
with pytest.raises(ValueError) as exec_info:
|
||||||
validate(schema, instance)
|
validate(schema, instance)
|
||||||
assert exec_info.value.args[0] == "Dict key must be str. Got 'UUID'"
|
assert exec_info.value.args[0] == "Dict key must be str. Got 'UUID'"
|
||||||
|
|
||||||
|
|
||||||
|
class MyDict(dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MyDict2(MyDict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"type_, value, expected",
|
||||||
|
(
|
||||||
|
(dict, 1, True),
|
||||||
|
(dict, "bar", False),
|
||||||
|
(OrderedDict, 1, True),
|
||||||
|
(OrderedDict, "bar", False),
|
||||||
|
(MyDict, 1, True),
|
||||||
|
(MyDict, "bar", False),
|
||||||
|
(MyDict2, 1, True),
|
||||||
|
(MyDict2, "bar", False),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_dict_subclasses(type_, value, expected):
|
||||||
|
schema = {"type": "object", "properties": {"foo": {"type": "integer"}}}
|
||||||
|
document = type_({"foo": value})
|
||||||
|
assert is_valid(schema, document) is expected
|
||||||
|
|
Loading…
Reference in New Issue