diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index 568d5331..cb284051 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixed - Set `jsonschema_rs.JSONSchema.__module__` to `jsonschema_rs`. +- Convert tuples into lists for validation to fix `ValueError: Unsupported type: 'tuple'` ### Performance diff --git a/bindings/python/src/ser.rs b/bindings/python/src/ser.rs index ba85e4b4..afb3c806 100644 --- a/bindings/python/src/ser.rs +++ b/bindings/python/src/ser.rs @@ -2,7 +2,7 @@ use pyo3::{ exceptions, ffi::{ PyDictObject, PyFloat_AS_DOUBLE, PyList_GET_ITEM, PyList_GET_SIZE, PyLong_AsLongLong, - Py_TYPE, + PyTuple_GET_ITEM, PyTuple_GET_SIZE, Py_TYPE, }, prelude::*, types::PyAny, @@ -27,6 +27,7 @@ pub enum ObjectType { Float, List, Dict, + Tuple, Unknown(String), } @@ -81,6 +82,8 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType ObjectType::None } else if object_type == unsafe { types::LIST_TYPE } { ObjectType::List + } else if object_type == unsafe { types::TUPLE_TYPE } { + ObjectType::Tuple } else if object_type == unsafe { types::DICT_TYPE } { ObjectType::Dict } else { @@ -150,7 +153,7 @@ impl Serialize for SerializePyObject { if self.recursion_depth == RECURSION_LIMIT { return Err(ser::Error::custom("Recursion limit reached")); } - let length = unsafe { PyList_GET_SIZE(self.object) } as usize; + let length = unsafe { PyList_GET_SIZE(self.object) as usize }; if length == 0 { serializer.serialize_seq(Some(0))?.end() } else { @@ -174,6 +177,34 @@ impl Serialize for SerializePyObject { sequence.end() } } + ObjectType::Tuple => { + if self.recursion_depth == RECURSION_LIMIT { + return Err(ser::Error::custom("Recursion limit reached")); + } + let length = unsafe { PyTuple_GET_SIZE(self.object) as usize }; + if length == 0 { + serializer.serialize_seq(Some(0))?.end() + } else { + let mut type_ptr = std::ptr::null_mut(); + let mut ob_type = ObjectType::Str; + let mut sequence = serializer.serialize_seq(Some(length))?; + for i in 0..length { + let elem = unsafe { PyTuple_GET_ITEM(self.object, i as isize) }; + let current_ob_type = unsafe { Py_TYPE(elem) }; + if current_ob_type != type_ptr { + type_ptr = current_ob_type; + ob_type = get_object_type(current_ob_type); + } + #[allow(clippy::integer_arithmetic)] + sequence.serialize_element(&SerializePyObject::with_obtype( + elem, + ob_type.clone(), + self.recursion_depth + 1, + ))?; + } + sequence.end() + } + } ObjectType::Unknown(ref type_name) => Err(ser::Error::custom(format!( "Unsupported type: '{}'", type_name diff --git a/bindings/python/src/types.rs b/bindings/python/src/types.rs index 6103e8e8..a79a9625 100644 --- a/bindings/python/src/types.rs +++ b/bindings/python/src/types.rs @@ -1,6 +1,6 @@ use pyo3::ffi::{ - PyDict_New, PyFloat_FromDouble, PyList_New, PyLong_FromLongLong, PyTypeObject, PyUnicode_New, - Py_None, Py_TYPE, Py_True, + PyDict_New, PyFloat_FromDouble, PyList_New, PyLong_FromLongLong, PyTuple_New, PyTypeObject, + PyUnicode_New, Py_None, Py_TYPE, Py_True, }; use std::sync::Once; @@ -13,6 +13,7 @@ pub static mut NONE_TYPE: *mut PyTypeObject = 0 as *mut PyTypeObject; pub static mut FLOAT_TYPE: *mut PyTypeObject = 0 as *mut PyTypeObject; pub static mut LIST_TYPE: *mut PyTypeObject = 0 as *mut PyTypeObject; pub static mut DICT_TYPE: *mut PyTypeObject = 0 as *mut PyTypeObject; +pub static mut TUPLE_TYPE: *mut PyTypeObject = 0 as *mut PyTypeObject; static INIT: Once = Once::new(); @@ -24,6 +25,7 @@ pub fn init() { TRUE = Py_True(); STR_TYPE = Py_TYPE(PyUnicode_New(0, 255)); DICT_TYPE = Py_TYPE(PyDict_New()); + TUPLE_TYPE = Py_TYPE(PyTuple_New(0_isize)); LIST_TYPE = Py_TYPE(PyList_New(0_isize)); NONE_TYPE = Py_TYPE(Py_None()); BOOL_TYPE = Py_TYPE(TRUE); diff --git a/bindings/python/tests-py/test_jsonschema.py b/bindings/python/tests-py/test_jsonschema.py index 755c8c31..a363ad9d 100644 --- a/bindings/python/tests-py/test_jsonschema.py +++ b/bindings/python/tests-py/test_jsonschema.py @@ -1,3 +1,4 @@ +from collections import namedtuple from contextlib import suppress from functools import partial @@ -65,6 +66,36 @@ def test_from_str_error(): JSONSchema.from_str(42) +@pytest.mark.parametrize( + "val", + ( + ("A", "B", "C"), + ["A", "B", "C"], + ), +) +def test_array_tuple(val): + schema = {"type": "array", "items": {"type": "string"}} + validate(schema, val) + + +@pytest.mark.parametrize( + "val", + ((1, 2, 3), [1, 2, 3], {"foo": 1}), +) +def test_array_tuple_invalid(val): + schema = {"type": "array", "items": {"type": "string"}} + with pytest.raises(ValueError): + validate(schema, val) + + +def test_named_tuple(): + Person = namedtuple("Person", "first_name last_name") + person_a = Person("Joe", "Smith") + schema = {"type": "array", "items": {"type": "string"}} + with pytest.raises(ValueError): + validate(schema, person_a) + + def test_recursive_dict(): instance = {} instance["foo"] = instance