feat: Support Python 3.12
Signed-off-by: Dmitry Dygalo <dmitry@dygalo.dev>
This commit is contained in:
parent
f0828cba01
commit
4f99c8f8be
|
@ -137,7 +137,7 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, macos-12, windows-2022]
|
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 }}
|
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for Python 3.12 [#439](https://github.com/Stranger6667/jsonschema-rs/issues/439)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Expose drafts 2019-09 and 2020-12 to Python
|
- Expose drafts 2019-09 and 2020-12 to Python
|
||||||
|
|
|
@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ahash"
|
name = "ahash"
|
||||||
version = "0.8.10"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
|
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
@ -530,6 +530,7 @@ dependencies = [
|
||||||
"built",
|
"built",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"pyo3",
|
"pyo3",
|
||||||
|
"pyo3-build-config",
|
||||||
"pyo3-built",
|
"pyo3-built",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -17,6 +17,7 @@ crate-type = ["cdylib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
built = { version = "0.7.1", features = ["cargo-lock", "chrono"] }
|
built = { version = "0.7.1", features = ["cargo-lock", "chrono"] }
|
||||||
|
pyo3-build-config = { version = "0.20.3", features = ["resolve-config"] }
|
||||||
|
|
||||||
[dependencies.jsonschema]
|
[dependencies.jsonschema]
|
||||||
path = "../../jsonschema"
|
path = "../../jsonschema"
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
built::write_built_file().expect("Failed to acquire build-time information");
|
built::write_built_file().expect("Failed to acquire build-time information");
|
||||||
|
pyo3_build_config::use_pyo3_cfgs();
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,5 +53,5 @@ python-source = "python"
|
||||||
strip = true
|
strip = true
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["maturin>=0.14.11,<0.15"]
|
requires = ["maturin>=1.1"]
|
||||||
build-backend = "maturin"
|
build-backend = "maturin"
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
use jsonschema::{paths::JSONPointer, Draft};
|
use jsonschema::{paths::JSONPointer, Draft};
|
||||||
use pyo3::{
|
use pyo3::{
|
||||||
exceptions::{self, PyValueError},
|
exceptions::{self, PyValueError},
|
||||||
|
ffi::PyUnicode_AsUTF8AndSize,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
types::{PyAny, PyList, PyType},
|
types::{PyAny, PyList, PyType},
|
||||||
wrap_pyfunction,
|
wrap_pyfunction,
|
||||||
|
@ -28,7 +29,6 @@ extern crate pyo3_built;
|
||||||
|
|
||||||
mod ffi;
|
mod ffi;
|
||||||
mod ser;
|
mod ser;
|
||||||
mod string;
|
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
const DRAFT7: u8 = 7;
|
const DRAFT7: u8 = 7;
|
||||||
|
@ -385,8 +385,8 @@ impl JSONSchema {
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
|
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
|
||||||
let uni = unsafe { string::read_utf8_from_str(obj_ptr, &mut str_size) };
|
let ptr = unsafe { PyUnicode_AsUTF8AndSize(obj_ptr, &mut str_size) };
|
||||||
let slice = unsafe { std::slice::from_raw_parts(uni, str_size as usize) };
|
let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), str_size as usize) };
|
||||||
let raw_schema = serde_json::from_slice(slice)
|
let raw_schema = serde_json::from_slice(slice)
|
||||||
.map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?;
|
.map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?;
|
||||||
let options = make_options(draft, with_meta_schemas)?;
|
let options = make_options(draft, with_meta_schemas)?;
|
||||||
|
|
|
@ -2,7 +2,8 @@ use pyo3::{
|
||||||
exceptions,
|
exceptions,
|
||||||
ffi::{
|
ffi::{
|
||||||
PyDictObject, PyFloat_AS_DOUBLE, PyList_GET_ITEM, PyList_GET_SIZE, PyLong_AsLongLong,
|
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::*,
|
prelude::*,
|
||||||
types::PyAny,
|
types::PyAny,
|
||||||
|
@ -12,7 +13,7 @@ use serde::{
|
||||||
Serializer,
|
Serializer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ffi, string, types};
|
use crate::{ffi, types};
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
|
|
||||||
pub const RECURSION_LIMIT: u8 = 255;
|
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::<u8>(),
|
||||||
|
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`
|
/// Convert a Python value to `serde_json::Value`
|
||||||
impl Serialize for SerializePyObject {
|
impl Serialize for SerializePyObject {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
@ -123,16 +149,34 @@ impl Serialize for SerializePyObject {
|
||||||
match self.object_type {
|
match self.object_type {
|
||||||
ObjectType::Str => {
|
ObjectType::Str => {
|
||||||
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
|
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 {
|
let slice = unsafe {
|
||||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
|
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
|
||||||
uni,
|
ptr.cast::<u8>(),
|
||||||
str_size as usize,
|
str_size as usize,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
serializer.serialize_str(slice)
|
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 => {
|
ObjectType::Float => {
|
||||||
serializer.serialize_f64(unsafe { PyFloat_AS_DOUBLE(self.object) })
|
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);
|
pyo3::ffi::PyDict_Next(self.object, &mut pos, &mut key, &mut value);
|
||||||
}
|
}
|
||||||
check_type_is_str(key)?;
|
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 {
|
let slice = unsafe {
|
||||||
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
|
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
|
||||||
uni,
|
ptr.cast::<u8>(),
|
||||||
str_size as usize,
|
str_size as usize,
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|
|
@ -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::<PyAsciiObject>()).state & STATE_ASCII == STATE_ASCII {
|
|
||||||
*size = (*object_pointer.cast::<PyAsciiObject>()).length;
|
|
||||||
object_pointer.cast::<PyAsciiObject>().offset(1) as *const u8
|
|
||||||
} else if (*object_pointer.cast::<PyAsciiObject>()).state & STATE_COMPACT == STATE_COMPACT
|
|
||||||
&& !(*object_pointer.cast::<PyCompactUnicodeObject>())
|
|
||||||
.utf8
|
|
||||||
.is_null()
|
|
||||||
{
|
|
||||||
*size = (*object_pointer.cast::<PyCompactUnicodeObject>()).utf8_length;
|
|
||||||
(*object_pointer.cast::<PyCompactUnicodeObject>()).utf8 as *const u8
|
|
||||||
} else {
|
|
||||||
PyUnicode_AsUTF8AndSize(object_pointer, size).cast::<u8>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -141,14 +141,14 @@ def test_initialization_errors(schema, draft, error):
|
||||||
|
|
||||||
@given(minimum=st.integers().map(abs))
|
@given(minimum=st.integers().map(abs))
|
||||||
def test_minimum(minimum):
|
def test_minimum(minimum):
|
||||||
with suppress(SystemError):
|
with suppress(SystemError, ValueError):
|
||||||
assert is_valid({"minimum": minimum}, minimum)
|
assert is_valid({"minimum": minimum}, minimum)
|
||||||
assert is_valid({"minimum": minimum}, minimum - 1) is False
|
assert is_valid({"minimum": minimum}, minimum - 1) is False
|
||||||
|
|
||||||
|
|
||||||
@given(maximum=st.integers().map(abs))
|
@given(maximum=st.integers().map(abs))
|
||||||
def test_maximum(maximum):
|
def test_maximum(maximum):
|
||||||
with suppress(SystemError):
|
with suppress(SystemError, ValueError):
|
||||||
assert is_valid({"maximum": maximum}, maximum)
|
assert is_valid({"maximum": maximum}, maximum)
|
||||||
assert is_valid({"maximum": maximum}, maximum + 1) is False
|
assert is_valid({"maximum": maximum}, maximum + 1) is False
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue