From 829ca19963a796a8f684ccc3864b23ad9e2f4d7c Mon Sep 17 00:00:00 2001 From: Florian Braun Date: Thu, 21 Mar 2024 11:27:31 +0100 Subject: [PATCH] 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. --- bindings/python/CHANGELOG.md | 4 +++ bindings/python/src/ser.rs | 15 +++++++++++ bindings/python/tests-py/test_jsonschema.py | 29 ++++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 04b3282..1a44c9c 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -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 diff --git a/bindings/python/src/ser.rs b/bindings/python/src/ser.rs index fb5e4bd..ccaf975 100644 --- a/bindings/python/src/ser.rs +++ b/bindings/python/src/ser.rs @@ -67,6 +67,19 @@ fn is_enum_subclass(object_type: *mut pyo3::ffi::PyTypeObject) -> bool { unsafe { (*(object_type.cast::())).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()) } diff --git a/bindings/python/tests-py/test_jsonschema.py b/bindings/python/tests-py/test_jsonschema.py index e1e0382..5b4b7e7 100644 --- a/bindings/python/tests-py/test_jsonschema.py +++ b/bindings/python/tests-py/test_jsonschema.py @@ -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