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] ## [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

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 } 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())
} }

View File

@ -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