From 4f99c8f8be379ed739bde6bf7f649c5df5b6a1ff Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sun, 3 Mar 2024 17:58:16 +0100 Subject: [PATCH] feat: Support Python 3.12 Signed-off-by: Dmitry Dygalo --- .github/workflows/build.yml | 2 +- bindings/python/CHANGELOG.md | 4 ++ bindings/python/Cargo.lock | 5 +- bindings/python/Cargo.toml | 1 + bindings/python/build.rs | 1 + bindings/python/pyproject.toml | 2 +- bindings/python/src/lib.rs | 6 +-- bindings/python/src/ser.rs | 58 ++++++++++++++++++--- bindings/python/src/string.rs | 48 ----------------- bindings/python/tests-py/test_jsonschema.py | 4 +- 10 files changed, 67 insertions(+), 64 deletions(-) delete mode 100644 bindings/python/src/string.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 655e425..0090e23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,7 +137,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, macos-12, windows-2022] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] name: Python ${{ matrix.python-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/bindings/python/CHANGELOG.md b/bindings/python/CHANGELOG.md index fc81e7d..54be55a 100644 --- a/bindings/python/CHANGELOG.md +++ b/bindings/python/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Support for Python 3.12 [#439](https://github.com/Stranger6667/jsonschema-rs/issues/439) + ### Changed - Expose drafts 2019-09 and 2020-12 to Python diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index b2b88fd..9f2fc25 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -530,6 +530,7 @@ dependencies = [ "built", "jsonschema", "pyo3", + "pyo3-build-config", "pyo3-built", "serde", "serde_json", diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index a4484d6..e10bf2c 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -17,6 +17,7 @@ crate-type = ["cdylib"] [build-dependencies] built = { version = "0.7.1", features = ["cargo-lock", "chrono"] } +pyo3-build-config = { version = "0.20.3", features = ["resolve-config"] } [dependencies.jsonschema] path = "../../jsonschema" diff --git a/bindings/python/build.rs b/bindings/python/build.rs index d8f91cb..b6004c2 100644 --- a/bindings/python/build.rs +++ b/bindings/python/build.rs @@ -1,3 +1,4 @@ fn main() { built::write_built_file().expect("Failed to acquire build-time information"); + pyo3_build_config::use_pyo3_cfgs(); } diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 6c16ffb..d0608d2 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -53,5 +53,5 @@ python-source = "python" strip = true [build-system] -requires = ["maturin>=0.14.11,<0.15"] +requires = ["maturin>=1.1"] build-backend = "maturin" diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index a568cb0..4f7eaac 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -19,6 +19,7 @@ use jsonschema::{paths::JSONPointer, Draft}; use pyo3::{ exceptions::{self, PyValueError}, + ffi::PyUnicode_AsUTF8AndSize, prelude::*, types::{PyAny, PyList, PyType}, wrap_pyfunction, @@ -28,7 +29,6 @@ extern crate pyo3_built; mod ffi; mod ser; -mod string; mod types; const DRAFT7: u8 = 7; @@ -385,8 +385,8 @@ impl JSONSchema { ))) } else { let mut str_size: pyo3::ffi::Py_ssize_t = 0; - let uni = unsafe { string::read_utf8_from_str(obj_ptr, &mut str_size) }; - let slice = unsafe { std::slice::from_raw_parts(uni, str_size as usize) }; + let ptr = unsafe { PyUnicode_AsUTF8AndSize(obj_ptr, &mut str_size) }; + let slice = unsafe { std::slice::from_raw_parts(ptr.cast::(), str_size as usize) }; let raw_schema = serde_json::from_slice(slice) .map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?; let options = make_options(draft, with_meta_schemas)?; diff --git a/bindings/python/src/ser.rs b/bindings/python/src/ser.rs index d29cd70..fb5e4bd 100644 --- a/bindings/python/src/ser.rs +++ b/bindings/python/src/ser.rs @@ -2,7 +2,8 @@ use pyo3::{ exceptions, ffi::{ PyDictObject, PyFloat_AS_DOUBLE, PyList_GET_ITEM, PyList_GET_SIZE, PyLong_AsLongLong, - PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, Py_TYPE, + PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, PyUnicode_AsUTF8AndSize, Py_DECREF, + Py_TYPE, }, prelude::*, types::PyAny, @@ -12,7 +13,7 @@ use serde::{ Serializer, }; -use crate::{ffi, string, types}; +use crate::{ffi, types}; use std::ffi::CStr; pub const RECURSION_LIMIT: u8 = 255; @@ -114,6 +115,31 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType } } +macro_rules! bail_on_integer_conversion_error { + ($value:expr) => { + if !$value.is_null() { + let repr = unsafe { pyo3::ffi::PyObject_Str($value) }; + let mut size = 0; + let ptr = unsafe { PyUnicode_AsUTF8AndSize(repr, &mut size) }; + return if !ptr.is_null() { + let slice = unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + ptr.cast::(), + size as usize, + )) + }; + let message = String::from(slice); + unsafe { Py_DECREF(repr) }; + Err(ser::Error::custom(message)) + } else { + Err(ser::Error::custom( + "Internal Error: Failed to convert exception to string", + )) + }; + } + }; +} + /// Convert a Python value to `serde_json::Value` impl Serialize for SerializePyObject { fn serialize(&self, serializer: S) -> Result @@ -123,16 +149,34 @@ impl Serialize for SerializePyObject { match self.object_type { ObjectType::Str => { let mut str_size: pyo3::ffi::Py_ssize_t = 0; - let uni = unsafe { string::read_utf8_from_str(self.object, &mut str_size) }; + let ptr = unsafe { PyUnicode_AsUTF8AndSize(self.object, &mut str_size) }; let slice = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts( - uni, + ptr.cast::(), str_size as usize, )) }; serializer.serialize_str(slice) } - ObjectType::Int => serializer.serialize_i64(unsafe { PyLong_AsLongLong(self.object) }), + ObjectType::Int => { + let value = unsafe { PyLong_AsLongLong(self.object) }; + if value == -1 { + #[cfg(Py_3_12)] + { + let exception = unsafe { pyo3::ffi::PyErr_GetRaisedException() }; + bail_on_integer_conversion_error!(exception); + }; + #[cfg(not(Py_3_12))] + { + let mut ptype: *mut pyo3::ffi::PyObject = std::ptr::null_mut(); + let mut pvalue: *mut pyo3::ffi::PyObject = std::ptr::null_mut(); + let mut ptraceback: *mut pyo3::ffi::PyObject = std::ptr::null_mut(); + unsafe { pyo3::ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback) }; + bail_on_integer_conversion_error!(pvalue); + }; + } + serializer.serialize_i64(value) + } ObjectType::Float => { serializer.serialize_f64(unsafe { PyFloat_AS_DOUBLE(self.object) }) } @@ -156,10 +200,10 @@ impl Serialize for SerializePyObject { pyo3::ffi::PyDict_Next(self.object, &mut pos, &mut key, &mut value); } check_type_is_str(key)?; - let uni = unsafe { string::read_utf8_from_str(key, &mut str_size) }; + let ptr = unsafe { PyUnicode_AsUTF8AndSize(key, &mut str_size) }; let slice = unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts( - uni, + ptr.cast::(), str_size as usize, )) }; diff --git a/bindings/python/src/string.rs b/bindings/python/src/string.rs deleted file mode 100644 index c3f01dc..0000000 --- a/bindings/python/src/string.rs +++ /dev/null @@ -1,48 +0,0 @@ -use pyo3::ffi::{PyTypeObject, PyUnicode_AsUTF8AndSize, Py_UNICODE, Py_hash_t, Py_ssize_t}; -use std::os::raw::c_char; - -#[repr(C)] -struct PyAsciiObject { - pub ob_refcnt: Py_ssize_t, - pub ob_type: *mut PyTypeObject, - pub length: Py_ssize_t, - pub hash: Py_hash_t, - pub state: u32, - pub wstr: *mut c_char, -} - -#[repr(C)] -struct PyCompactUnicodeObject { - pub ob_refcnt: Py_ssize_t, - pub ob_type: *mut PyTypeObject, - pub length: Py_ssize_t, - pub hash: Py_hash_t, - pub state: u32, - pub wstr: *mut Py_UNICODE, - pub utf8_length: Py_ssize_t, - pub utf8: *mut c_char, - pub wstr_length: Py_ssize_t, -} - -const STATE_ASCII: u32 = 0b0000_0000_0000_0000_0000_0000_0100_0000; -const STATE_COMPACT: u32 = 0b0000_0000_0000_0000_0000_0000_0010_0000; - -/// Read a UTF-8 string from a pointer and change the given size if needed. -pub unsafe fn read_utf8_from_str( - object_pointer: *mut pyo3::ffi::PyObject, - size: &mut Py_ssize_t, -) -> *const u8 { - if (*object_pointer.cast::()).state & STATE_ASCII == STATE_ASCII { - *size = (*object_pointer.cast::()).length; - object_pointer.cast::().offset(1) as *const u8 - } else if (*object_pointer.cast::()).state & STATE_COMPACT == STATE_COMPACT - && !(*object_pointer.cast::()) - .utf8 - .is_null() - { - *size = (*object_pointer.cast::()).utf8_length; - (*object_pointer.cast::()).utf8 as *const u8 - } else { - PyUnicode_AsUTF8AndSize(object_pointer, size).cast::() - } -} diff --git a/bindings/python/tests-py/test_jsonschema.py b/bindings/python/tests-py/test_jsonschema.py index c15ecc0..e1e0382 100644 --- a/bindings/python/tests-py/test_jsonschema.py +++ b/bindings/python/tests-py/test_jsonschema.py @@ -141,14 +141,14 @@ def test_initialization_errors(schema, draft, error): @given(minimum=st.integers().map(abs)) def test_minimum(minimum): - with suppress(SystemError): + with suppress(SystemError, ValueError): assert is_valid({"minimum": minimum}, minimum) assert is_valid({"minimum": minimum}, minimum - 1) is False @given(maximum=st.integers().map(abs)) def test_maximum(maximum): - with suppress(SystemError): + with suppress(SystemError, ValueError): assert is_valid({"maximum": maximum}, maximum) assert is_valid({"maximum": maximum}, maximum + 1) is False