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:
Florian Braun 2024-03-21 11:27:31 +01:00 committed by Dmitry Dygalo
parent 9771bc227c
commit 829ca19963
3 changed files with 47 additions and 1 deletions

View File

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Support subclasses of Python `dict`s [#427](https://github.com/Stranger6667/jsonschema-rs/issues/427)
## [0.17.2] - 2024-03-03
### Added

View File

@ -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 }
}
#[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 {
unsafe {
let object_type = Py_TYPE(object);
@ -110,6 +123,8 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType
ObjectType::Dict
} else if is_enum_subclass(object_type) {
ObjectType::Enum
} else if is_dict_subclass(object_type) {
ObjectType::Dict
} else {
ObjectType::Unknown(get_type_name(object_type).to_string())
}

View File

@ -1,6 +1,6 @@
import sys
import uuid
from collections import namedtuple
from collections import namedtuple, OrderedDict
from contextlib import suppress
from enum import Enum
from functools import partial
@ -248,3 +248,30 @@ def test_dict_with_non_str_keys():
with pytest.raises(ValueError) as exec_info:
validate(schema, instance)
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