Skip to content

Commit 4f99c8f

Browse files
committed
feat: Support Python 3.12
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent f0828cb commit 4f99c8f

File tree

10 files changed

+67
-64
lines changed

10 files changed

+67
-64
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ jobs:
137137
fail-fast: false
138138
matrix:
139139
os: [ubuntu-20.04, macos-12, windows-2022]
140-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
140+
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
141141

142142
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
143143
runs-on: ${{ matrix.os }}

bindings/python/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- Support for Python 3.12 [#439](https://github.com/Stranger6667/jsonschema-rs/issues/439)
8+
59
### Changed
610

711
- Expose drafts 2019-09 and 2020-12 to Python

bindings/python/Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/python/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ crate-type = ["cdylib"]
1717

1818
[build-dependencies]
1919
built = { version = "0.7.1", features = ["cargo-lock", "chrono"] }
20+
pyo3-build-config = { version = "0.20.3", features = ["resolve-config"] }
2021

2122
[dependencies.jsonschema]
2223
path = "../../jsonschema"

bindings/python/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
fn main() {
22
built::write_built_file().expect("Failed to acquire build-time information");
3+
pyo3_build_config::use_pyo3_cfgs();
34
}

bindings/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,5 @@ python-source = "python"
5353
strip = true
5454

5555
[build-system]
56-
requires = ["maturin>=0.14.11,<0.15"]
56+
requires = ["maturin>=1.1"]
5757
build-backend = "maturin"

bindings/python/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use jsonschema::{paths::JSONPointer, Draft};
2020
use pyo3::{
2121
exceptions::{self, PyValueError},
22+
ffi::PyUnicode_AsUTF8AndSize,
2223
prelude::*,
2324
types::{PyAny, PyList, PyType},
2425
wrap_pyfunction,
@@ -28,7 +29,6 @@ extern crate pyo3_built;
2829

2930
mod ffi;
3031
mod ser;
31-
mod string;
3232
mod types;
3333

3434
const DRAFT7: u8 = 7;
@@ -385,8 +385,8 @@ impl JSONSchema {
385385
)))
386386
} else {
387387
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
388-
let uni = unsafe { string::read_utf8_from_str(obj_ptr, &mut str_size) };
389-
let slice = unsafe { std::slice::from_raw_parts(uni, str_size as usize) };
388+
let ptr = unsafe { PyUnicode_AsUTF8AndSize(obj_ptr, &mut str_size) };
389+
let slice = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), str_size as usize) };
390390
let raw_schema = serde_json::from_slice(slice)
391391
.map_err(|error| PyValueError::new_err(format!("Invalid string: {}", error)))?;
392392
let options = make_options(draft, with_meta_schemas)?;

bindings/python/src/ser.rs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ use pyo3::{
22
exceptions,
33
ffi::{
44
PyDictObject, PyFloat_AS_DOUBLE, PyList_GET_ITEM, PyList_GET_SIZE, PyLong_AsLongLong,
5-
PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, Py_TYPE,
5+
PyObject_GetAttr, PyTuple_GET_ITEM, PyTuple_GET_SIZE, PyUnicode_AsUTF8AndSize, Py_DECREF,
6+
Py_TYPE,
67
},
78
prelude::*,
89
types::PyAny,
@@ -12,7 +13,7 @@ use serde::{
1213
Serializer,
1314
};
1415

15-
use crate::{ffi, string, types};
16+
use crate::{ffi, types};
1617
use std::ffi::CStr;
1718

1819
pub const RECURSION_LIMIT: u8 = 255;
@@ -114,6 +115,31 @@ pub fn get_object_type(object_type: *mut pyo3::ffi::PyTypeObject) -> ObjectType
114115
}
115116
}
116117

118+
macro_rules! bail_on_integer_conversion_error {
119+
($value:expr) => {
120+
if !$value.is_null() {
121+
let repr = unsafe { pyo3::ffi::PyObject_Str($value) };
122+
let mut size = 0;
123+
let ptr = unsafe { PyUnicode_AsUTF8AndSize(repr, &mut size) };
124+
return if !ptr.is_null() {
125+
let slice = unsafe {
126+
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
127+
ptr.cast::<u8>(),
128+
size as usize,
129+
))
130+
};
131+
let message = String::from(slice);
132+
unsafe { Py_DECREF(repr) };
133+
Err(ser::Error::custom(message))
134+
} else {
135+
Err(ser::Error::custom(
136+
"Internal Error: Failed to convert exception to string",
137+
))
138+
};
139+
}
140+
};
141+
}
142+
117143
/// Convert a Python value to `serde_json::Value`
118144
impl Serialize for SerializePyObject {
119145
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
@@ -123,16 +149,34 @@ impl Serialize for SerializePyObject {
123149
match self.object_type {
124150
ObjectType::Str => {
125151
let mut str_size: pyo3::ffi::Py_ssize_t = 0;
126-
let uni = unsafe { string::read_utf8_from_str(self.object, &mut str_size) };
152+
let ptr = unsafe { PyUnicode_AsUTF8AndSize(self.object, &mut str_size) };
127153
let slice = unsafe {
128154
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
129-
uni,
155+
ptr.cast::<u8>(),
130156
str_size as usize,
131157
))
132158
};
133159
serializer.serialize_str(slice)
134160
}
135-
ObjectType::Int => serializer.serialize_i64(unsafe { PyLong_AsLongLong(self.object) }),
161+
ObjectType::Int => {
162+
let value = unsafe { PyLong_AsLongLong(self.object) };
163+
if value == -1 {
164+
#[cfg(Py_3_12)]
165+
{
166+
let exception = unsafe { pyo3::ffi::PyErr_GetRaisedException() };
167+
bail_on_integer_conversion_error!(exception);
168+
};
169+
#[cfg(not(Py_3_12))]
170+
{
171+
let mut ptype: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
172+
let mut pvalue: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
173+
let mut ptraceback: *mut pyo3::ffi::PyObject = std::ptr::null_mut();
174+
unsafe { pyo3::ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback) };
175+
bail_on_integer_conversion_error!(pvalue);
176+
};
177+
}
178+
serializer.serialize_i64(value)
179+
}
136180
ObjectType::Float => {
137181
serializer.serialize_f64(unsafe { PyFloat_AS_DOUBLE(self.object) })
138182
}
@@ -156,10 +200,10 @@ impl Serialize for SerializePyObject {
156200
pyo3::ffi::PyDict_Next(self.object, &mut pos, &mut key, &mut value);
157201
}
158202
check_type_is_str(key)?;
159-
let uni = unsafe { string::read_utf8_from_str(key, &mut str_size) };
203+
let ptr = unsafe { PyUnicode_AsUTF8AndSize(key, &mut str_size) };
160204
let slice = unsafe {
161205
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
162-
uni,
206+
ptr.cast::<u8>(),
163207
str_size as usize,
164208
))
165209
};

bindings/python/src/string.rs

Lines changed: 0 additions & 48 deletions
This file was deleted.

bindings/python/tests-py/test_jsonschema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,14 @@ def test_initialization_errors(schema, draft, error):
141141

142142
@given(minimum=st.integers().map(abs))
143143
def test_minimum(minimum):
144-
with suppress(SystemError):
144+
with suppress(SystemError, ValueError):
145145
assert is_valid({"minimum": minimum}, minimum)
146146
assert is_valid({"minimum": minimum}, minimum - 1) is False
147147

148148

149149
@given(maximum=st.integers().map(abs))
150150
def test_maximum(maximum):
151-
with suppress(SystemError):
151+
with suppress(SystemError, ValueError):
152152
assert is_valid({"maximum": maximum}, maximum)
153153
assert is_valid({"maximum": maximum}, maximum + 1) is False
154154

0 commit comments

Comments
 (0)