#![allow(unsafe_code)]
#![allow(
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::elidable_lifetime_names
)]
use std::{
borrow::Cow,
cell::RefCell,
hash::{Hash, Hasher},
str::FromStr,
sync::atomic::{AtomicPtr, Ordering},
};
use ahash::{AHashSet, AHasher};
use pyo3::{
exceptions::PyValueError,
ffi,
prelude::*,
sync::PyOnceLock,
types::{PyDict, PyString},
Borrowed,
};
use serde_json::{Map, Number, Value};
use crate::{cmp, types::JsonType, Array, Json, JsonNumber, Node, NodeIdentity, Object};
type PyNode<'py> = Borrowed<'py, 'py, PyAny>;
pub struct Pyo3;
impl Json for Pyo3 {
type Node<'py> = PyNode<'py>;
type PreparedKey = Py<PyString>;
type StringBuffer = ();
fn prepare_key(key: &str) -> Py<PyString> {
Python::attach(|py| PyString::intern(py, key).unbind())
}
fn with_string_node<T>((): &mut (), string: &str, f: impl FnOnce(PyNode<'_>) -> T) -> T {
Python::attach(|py| {
let object = PyString::new(py, string).into_any();
f(object.as_borrowed())
})
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ObjType {
Str,
Int,
Bool,
Null,
Float,
List,
Dict,
Tuple,
Enum,
Decimal,
Unknown,
}
struct TypePtrs {
str_: *mut ffi::PyTypeObject,
int: *mut ffi::PyTypeObject,
bool_: *mut ffi::PyTypeObject,
float: *mut ffi::PyTypeObject,
list: *mut ffi::PyTypeObject,
dict: *mut ffi::PyTypeObject,
tuple: *mut ffi::PyTypeObject,
enum_base: *mut ffi::PyTypeObject,
decimal: *mut ffi::PyTypeObject,
none: *mut ffi::PyObject,
true_: *mut ffi::PyObject,
}
unsafe impl Send for TypePtrs {}
unsafe impl Sync for TypePtrs {}
static TYPES: PyOnceLock<TypePtrs> = PyOnceLock::new();
static TYPES_PTR: AtomicPtr<TypePtrs> = AtomicPtr::new(std::ptr::null_mut());
#[inline]
fn types(py: Python<'_>) -> &'static TypePtrs {
let cached = TYPES_PTR.load(Ordering::Acquire);
if !cached.is_null() {
return unsafe { &*cached };
}
types_init(py)
}
#[cold]
fn types_init(py: Python<'_>) -> &'static TypePtrs {
let t = TYPES.get_or_init(py, || {
let ptr = |import: &str, attribute: &str| {
py.import(import)
.and_then(|m| m.getattr(attribute))
.map_or(std::ptr::null_mut(), |c| {
c.as_ptr().cast::<ffi::PyTypeObject>()
})
};
TypePtrs {
str_: py.get_type::<PyString>().as_type_ptr(),
int: ptr("builtins", "int"),
bool_: ptr("builtins", "bool"),
float: ptr("builtins", "float"),
list: ptr("builtins", "list"),
dict: py.get_type::<PyDict>().as_type_ptr(),
tuple: ptr("builtins", "tuple"),
enum_base: ptr("enum", "Enum"),
decimal: ptr("decimal", "Decimal"),
none: unsafe { ffi::Py_None() },
true_: unsafe { ffi::Py_True() },
}
});
TYPES_PTR.store(std::ptr::from_ref(t).cast_mut(), Ordering::Release);
t
}
fn object_type(node: Borrowed<'_, '_, PyAny>) -> ObjType {
let ptr = node.as_ptr();
let t = types(node.py());
if ptr == t.none {
return ObjType::Null;
}
let ty = unsafe { ffi::Py_TYPE(ptr) };
if ty == t.dict {
ObjType::Dict
} else if ty == t.str_ {
ObjType::Str
} else if ty == t.list {
ObjType::List
} else if ty == t.int {
ObjType::Int
} else if ty == t.bool_ {
ObjType::Bool
} else if ty == t.float {
ObjType::Float
} else if ty == t.tuple {
ObjType::Tuple
} else if !t.decimal.is_null() && ty == t.decimal {
ObjType::Decimal
} else if is_subtype(ty, t.dict) {
ObjType::Dict
} else if is_subtype(ty, t.list) {
ObjType::List
} else if !t.enum_base.is_null() && is_subtype(ty, t.enum_base) {
ObjType::Enum
} else {
ObjType::Unknown
}
}
fn is_subtype(ty: *mut ffi::PyTypeObject, base: *mut ffi::PyTypeObject) -> bool {
!base.is_null() && unsafe { ffi::PyType_IsSubtype(ty, base) != 0 }
}
fn resolved<'py>(node: PyNode<'py>) -> PyNode<'py> {
let mut current = node.to_owned();
let mut seen: Vec<*mut ffi::PyObject> = Vec::new();
while object_type(current.as_borrowed()) == ObjType::Enum {
if seen.contains(¤t.as_ptr()) {
record_value_error("Enum value resolves to itself");
return inert(current.py());
}
seen.push(current.as_ptr());
match current.getattr("value") {
Ok(value) => current = value,
Err(error) => {
record_value_error(&format!("Failed to access enum value: {error}"));
return inert(current.py());
}
}
}
unsafe { Borrowed::from_ptr(current.py(), current.as_ptr()) }
}
fn inert(py: Python<'_>) -> PyNode<'_> {
unsafe { Borrowed::from_ptr(py, ffi::Py_None()) }
}
thread_local! {
static PENDING_ERROR: RefCell<Option<PyErr>> = const { RefCell::new(None) };
}
fn record_error(error: PyErr) {
PENDING_ERROR.with(|slot| {
let mut slot = slot.borrow_mut();
if slot.is_none() {
*slot = Some(error);
}
});
}
fn record_value_error(message: &str) {
record_error(PyValueError::new_err(message.to_owned()));
}
#[must_use]
pub fn take_pending_error() -> Option<PyErr> {
PENDING_ERROR.with(|slot| slot.borrow_mut().take())
}
pub struct PendingErrorScope {
saved: Option<PyErr>,
}
impl PendingErrorScope {
#[must_use]
pub fn enter() -> Self {
Self {
saved: take_pending_error(),
}
}
}
impl Drop for PendingErrorScope {
fn drop(&mut self) {
let saved = self.saved.take();
PENDING_ERROR.with(|slot| *slot.borrow_mut() = saved);
}
}
pub fn probe_root(node: Borrowed<'_, '_, PyAny>) {
match object_type(node) {
ObjType::Str => drop(str_ref(node)),
ObjType::Enum => drop(resolved(node)),
ObjType::Unknown => record_unsupported(node),
ObjType::Dict => {
let mut position: ffi::Py_ssize_t = 0;
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
while unsafe {
ffi::PyDict_Next(
node.as_ptr(),
&raw mut position,
&raw mut key,
&raw mut value,
) != 0
} {
let key = unsafe { Borrowed::from_ptr(node.py(), key) };
if !key_is_usable(key) {
record_unusable_key(key);
break;
}
}
}
_ => {}
}
}
unsafe fn str_ref_ptr<'py>(object: *mut ffi::PyObject) -> Option<&'py str> {
let mut size: ffi::Py_ssize_t = 0;
let ptr = ffi::PyUnicode_AsUTF8AndSize(object, &raw mut size);
if ptr.is_null() {
record_error(PyErr::fetch(Python::assume_attached()));
return None;
}
let slice = std::slice::from_raw_parts(ptr.cast::<u8>(), size as usize);
let text = std::str::from_utf8_unchecked(slice);
Some(std::mem::transmute::<&str, &'py str>(text))
}
fn str_ref<'py>(node: PyNode<'py>) -> Option<&'py str> {
unsafe { str_ref_ptr(node.as_ptr()) }
}
fn obj_str_number(node: Borrowed<'_, '_, PyAny>) -> Option<Number> {
let text = node.str().ok()?;
Number::from_str(text.to_str().ok()?).ok()
}
fn number_of(node: PyNode<'_>) -> Option<Number> {
match object_type(node) {
ObjType::Int => {
let value = unsafe { ffi::PyLong_AsLongLong(node.as_ptr()) };
if value == -1 && unsafe { !ffi::PyErr_Occurred().is_null() } {
unsafe { ffi::PyErr_Clear() };
obj_str_number(node)
} else {
Some(Number::from(value))
}
}
ObjType::Float => {
let value = unsafe { ffi::PyFloat_AsDouble(node.as_ptr()) };
Number::from_f64(value)
}
ObjType::Decimal => obj_str_number(node),
ObjType::Enum => number_of(resolved(node)),
ObjType::Unknown => {
record_unsupported(node);
None
}
_ => None,
}
}
const RECURSION_LIMIT: u8 = 255;
fn record_recursion_limit() {
record_value_error("Recursion limit reached");
}
fn py_to_value(node: PyNode<'_>, depth: u8) -> Value {
match object_type(node) {
ObjType::Null => Value::Null,
ObjType::Unknown => {
record_unsupported(node);
Value::Null
}
ObjType::Bool => Value::Bool(node.as_ptr() == types(node.py()).true_),
ObjType::Int | ObjType::Float | ObjType::Decimal => {
number_of(node).map_or(Value::Null, Value::Number)
}
ObjType::Str => Value::String(str_ref(node).unwrap_or_default().to_owned()),
ty @ (ObjType::List | ObjType::Tuple) => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return Value::Null;
}
let array = PyArray::new(node, ty == ObjType::Tuple);
Value::Array(
array
.elements()
.map(|element| py_to_value(element, depth + 1))
.collect(),
)
}
ObjType::Dict => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return Value::Null;
}
let dict = unsafe { node.cast_unchecked::<PyDict>() };
let mut map = Map::new();
for (key, value) in dict.members() {
map.insert(key.into_owned(), py_to_value(value, depth + 1));
}
Value::Object(map)
}
ObjType::Enum => py_to_value(resolved(node), depth),
}
}
fn key_to_string(key: PyNode<'_>) -> String {
match object_type(key) {
ObjType::Str => str_ref(key).unwrap_or_default().to_owned(),
ObjType::Enum if is_str_instance(key) => {
let value = resolved(key);
if object_type(value) == ObjType::Str {
return str_ref(value).unwrap_or_default().to_owned();
}
record_unusable_key(key);
String::new()
}
_ => {
record_unusable_key(key);
String::new()
}
}
}
fn record_unsupported(node: PyNode<'_>) {
record_value_error(&format!("Unsupported type: '{}'", type_name(node)));
}
fn record_unusable_key(key: PyNode<'_>) {
record_value_error(&format!(
"Dict key must be str or str enum. Got '{}'",
type_name(key)
));
}
fn type_name(node: PyNode<'_>) -> String {
node.get_type()
.name()
.map(|name| name.to_string())
.unwrap_or_default()
}
fn is_str_instance(node: PyNode<'_>) -> bool {
let ty = unsafe { ffi::Py_TYPE(node.as_ptr()) };
ty == types(node.py()).str_ || is_subtype(ty, types(node.py()).str_)
}
fn key_is_usable(key: PyNode<'_>) -> bool {
match object_type(key) {
ObjType::Str => true,
ObjType::Enum => is_str_instance(key),
_ => false,
}
}
impl<'py> Node<'py, Pyo3> for PyNode<'py> {
type Object = Borrowed<'py, 'py, PyDict>;
type Array = PyArray<'py>;
type Number = PyNumber<'py>;
fn as_object(&self) -> Option<Self::Object> {
match object_type(*self) {
ObjType::Dict => Some(unsafe { self.cast_unchecked::<PyDict>() }),
ObjType::Enum => resolved(*self).as_object(),
ObjType::Unknown => {
record_unsupported(*self);
None
}
_ => None,
}
}
fn as_array(&self) -> Option<PyArray<'py>> {
match object_type(*self) {
ObjType::List => Some(PyArray::new(*self, false)),
ObjType::Tuple => Some(PyArray::new(*self, true)),
ObjType::Enum => resolved(*self).as_array(),
ObjType::Unknown => {
record_unsupported(*self);
None
}
_ => None,
}
}
fn as_string(&self) -> Option<Cow<'py, str>> {
match object_type(*self) {
ObjType::Str => str_ref(*self).map(Cow::Borrowed),
ObjType::Enum => resolved(*self).as_string(),
ObjType::Unknown => {
record_unsupported(*self);
None
}
_ => None,
}
}
fn as_number(&self) -> Option<PyNumber<'py>> {
let kind = object_type(*self);
match kind {
ObjType::Int => Some(PyNumber { node: *self, kind }),
ObjType::Float => unsafe { ffi::PyFloat_AsDouble(self.as_ptr()) }
.is_finite()
.then_some(PyNumber { node: *self, kind }),
ObjType::Decimal => number_of(*self)
.is_some()
.then_some(PyNumber { node: *self, kind }),
ObjType::Enum => resolved(*self).as_number(),
ObjType::Unknown => {
record_unsupported(*self);
None
}
_ => None,
}
}
fn is_number(&self) -> bool {
let ptr = self.as_ptr();
let t = types(self.py());
let ty = unsafe { ffi::Py_TYPE(ptr) };
if ty == t.float {
return unsafe { ffi::PyFloat_AsDouble(ptr) }.is_finite();
}
if ty == t.int {
let value = unsafe { ffi::PyLong_AsLongLong(ptr) };
if value == -1 && unsafe { !ffi::PyErr_Occurred().is_null() } {
unsafe { ffi::PyErr_Clear() };
return number_of(*self).is_some();
}
return true;
}
match object_type(*self) {
ObjType::Decimal => number_of(*self).is_some(),
ObjType::Enum => resolved(*self).is_number(),
_ => false,
}
}
fn as_boolean(&self) -> Option<bool> {
match object_type(*self) {
ObjType::Bool => Some(self.as_ptr() == types(self.py()).true_),
ObjType::Enum => resolved(*self).as_boolean(),
ObjType::Unknown => {
record_unsupported(*self);
None
}
_ => None,
}
}
fn is_null(&self) -> bool {
if self.as_ptr() == types(self.py()).none {
return true;
}
match object_type(*self) {
ObjType::Enum => resolved(*self).is_null(),
ObjType::Unknown => {
record_unsupported(*self);
false
}
_ => false,
}
}
fn json_type(&self) -> JsonType {
match object_type(*self) {
ObjType::Null => JsonType::Null,
ObjType::Bool => JsonType::Boolean,
ObjType::Int | ObjType::Float | ObjType::Decimal => JsonType::Number,
ObjType::Str => JsonType::String,
ObjType::List | ObjType::Tuple => JsonType::Array,
ObjType::Dict => JsonType::Object,
ObjType::Unknown => {
record_unsupported(*self);
JsonType::Object
}
ObjType::Enum => resolved(*self).json_type(),
}
}
fn string_length(&self) -> Option<u64> {
match object_type(*self) {
ObjType::Str => Some(unsafe { ffi::PyUnicode_GetLength(self.as_ptr()) }.max(0) as u64),
ObjType::Enum => resolved(*self).string_length(),
_ => None,
}
}
fn equals_value(&self, expected: &Value) -> bool {
match expected {
Value::Null => self.is_null(),
Value::Bool(b) => self.as_boolean() == Some(*b),
Value::Number(n) => self
.as_number()
.is_some_and(|got| cmp::equal_numbers(&got, n)),
Value::String(s) => self
.as_string()
.is_some_and(|got| got.as_ref() == s.as_str()),
Value::Array(items) => self.as_array().is_some_and(|arr| {
arr.len() == items.len()
&& arr
.elements()
.zip(items)
.all(|(element, expected)| element.equals_value(expected))
}),
Value::Object(map) => self.as_object().is_some_and(|object| {
object.len() == map.len()
&& object.members().all(|(key, value)| {
map.get(key.as_ref()).is_some_and(|e| value.equals_value(e))
})
}),
}
}
fn to_value(&self) -> Cow<'py, Value> {
Cow::Owned(py_to_value(*self, 0))
}
fn identity(&self) -> Option<NodeIdentity> {
Some(NodeIdentity::new(self.as_ptr() as usize))
}
}
pub struct PyNumber<'py> {
node: PyNode<'py>,
kind: ObjType,
}
impl JsonNumber for PyNumber<'_> {
fn as_u64(&self) -> Option<u64> {
match self.kind {
ObjType::Int => {
let value = unsafe { ffi::PyLong_AsUnsignedLongLong(self.node.as_ptr()) };
if value == u64::MAX && unsafe { !ffi::PyErr_Occurred().is_null() } {
unsafe { ffi::PyErr_Clear() };
return None;
}
Some(value)
}
_ => self.to_number().as_u64(),
}
}
fn as_i64(&self) -> Option<i64> {
match self.kind {
ObjType::Int => {
let value = unsafe { ffi::PyLong_AsLongLong(self.node.as_ptr()) };
if value == -1 && unsafe { !ffi::PyErr_Occurred().is_null() } {
unsafe { ffi::PyErr_Clear() };
return None;
}
Some(value)
}
_ => self.to_number().as_i64(),
}
}
fn as_f64(&self) -> Option<f64> {
match self.kind {
ObjType::Float => Some(unsafe { ffi::PyFloat_AsDouble(self.node.as_ptr()) }),
_ => self.to_number().as_f64(),
}
}
fn as_str(&self) -> Cow<'_, str> {
self.node
.str()
.ok()
.and_then(|text| text.to_str().map(|text| Cow::Owned(text.to_owned())).ok())
.unwrap_or(Cow::Borrowed(""))
}
fn to_number(&self) -> Cow<'_, Number> {
number_of(self.node).map_or(Cow::Owned(Number::from(0)), Cow::Owned)
}
}
pub struct PyMembers<'py> {
dict: Borrowed<'py, 'py, PyDict>,
pos: ffi::Py_ssize_t,
}
impl<'py> Iterator for PyMembers<'py> {
type Item = (Cow<'py, str>, PyNode<'py>);
fn next(&mut self) -> Option<Self::Item> {
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
if unsafe {
ffi::PyDict_Next(
self.dict.as_ptr(),
&raw mut self.pos,
&raw mut key,
&raw mut value,
)
} == 0
{
return None;
}
let py = self.dict.py();
let name = if unsafe { ffi::Py_TYPE(key) } == types(py).str_ {
Cow::Borrowed(unsafe { str_ref_ptr(key) }.unwrap_or(""))
} else {
Cow::Owned(key_to_string(unsafe { Borrowed::from_ptr(py, key) }))
};
let value = unsafe { Borrowed::from_ptr(py, value) };
Some((name, value))
}
}
impl<'py> Object<'py, Pyo3> for Borrowed<'py, 'py, PyDict> {
type Node = PyNode<'py>;
type MemberName = Cow<'py, str>;
type MembersIter = PyMembers<'py>;
fn len(&self) -> usize {
unsafe { ffi::PyDict_Size(self.as_ptr()) }.max(0) as usize
}
fn get(&self, key: &Py<PyString>) -> Option<PyNode<'py>> {
let py = self.py();
let value = unsafe { ffi::PyDict_GetItem(self.as_ptr(), key.as_ptr()) };
if value.is_null() {
None
} else {
Some(unsafe { Borrowed::from_ptr(py, value) })
}
}
fn members(&self) -> PyMembers<'py> {
PyMembers {
dict: *self,
pos: 0,
}
}
}
pub struct PyArray<'py> {
sequence: PyNode<'py>,
is_tuple: bool,
len: usize,
}
impl<'py> PyArray<'py> {
fn new(sequence: PyNode<'py>, is_tuple: bool) -> Self {
let ptr = sequence.as_ptr();
let len = if is_tuple {
unsafe { ffi::PyTuple_Size(ptr) }
} else {
unsafe { ffi::PyList_Size(ptr) }
};
PyArray {
sequence,
is_tuple,
len: len.max(0) as usize,
}
}
}
impl<'py> Array<'py, Pyo3> for PyArray<'py> {
type Node = PyNode<'py>;
type ElementsIter = PyElements<'py>;
fn len(&self) -> usize {
self.len
}
fn elements(&self) -> PyElements<'py> {
PyElements {
sequence: self.sequence,
is_tuple: self.is_tuple,
index: 0,
len: self.len,
}
}
fn is_unique(&self) -> bool {
let size = self.len;
if size <= 1 {
return true;
}
let items: Vec<PyNode<'py>> = self.elements().collect();
if size <= crate::unique::ITEMS_SIZE_THRESHOLD {
let mut idx = 0;
while idx < size {
let mut inner_idx = idx + 1;
while inner_idx < size {
if equal_nodes(items[idx], items[inner_idx], 0) {
return false;
}
inner_idx += 1;
}
idx += 1;
}
true
} else {
let mut seen = AHashSet::with_capacity(size);
items.into_iter().all(|item| seen.insert(HashedNode(item)))
}
}
}
fn equal_nodes(left: PyNode<'_>, right: PyNode<'_>, depth: u8) -> bool {
if left.as_ptr() == right.as_ptr() {
return true;
}
let (left_type, right_type) = (object_type(left), object_type(right));
match (left_type, right_type) {
(ObjType::Enum, _) => equal_nodes(resolved(left), right, depth),
(_, ObjType::Enum) => equal_nodes(left, resolved(right), depth),
(ObjType::Unknown, _) => {
record_unsupported(left);
false
}
(_, ObjType::Unknown) => {
record_unsupported(right);
false
}
(ObjType::Null, ObjType::Null) => true,
(ObjType::Bool, ObjType::Bool) => left.as_ptr() == right.as_ptr(),
(
ObjType::Int | ObjType::Float | ObjType::Decimal,
ObjType::Int | ObjType::Float | ObjType::Decimal,
) => match (number_of(left), number_of(right)) {
(Some(left), Some(right)) => cmp::equal_numbers(&left, &right),
_ => false,
},
(ObjType::Str, ObjType::Str) => match (str_ref(left), str_ref(right)) {
(Some(left), Some(right)) => left == right,
_ => false,
},
(ObjType::List | ObjType::Tuple, ObjType::List | ObjType::Tuple) => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return false;
}
let left = PyArray::new(left, left_type == ObjType::Tuple);
let right = PyArray::new(right, right_type == ObjType::Tuple);
left.len() == right.len()
&& left
.elements()
.zip(right.elements())
.all(|(left, right)| equal_nodes(left, right, depth + 1))
}
(ObjType::Dict, ObjType::Dict) => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return false;
}
let (left_ptr, right_ptr) = (left.as_ptr(), right.as_ptr());
if unsafe { ffi::PyDict_Size(left_ptr) != ffi::PyDict_Size(right_ptr) } {
return false;
}
let mut position: ffi::Py_ssize_t = 0;
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
while unsafe {
ffi::PyDict_Next(left_ptr, &raw mut position, &raw mut key, &raw mut value) != 0
} {
let key_node = unsafe { Borrowed::from_ptr(left.py(), key) };
if !key_is_usable(key_node) {
record_unusable_key(key_node);
return false;
}
let other = unsafe { ffi::PyDict_GetItem(right_ptr, key) };
if other.is_null() {
return false;
}
let (value, other) = unsafe {
(
Borrowed::from_ptr(left.py(), value),
Borrowed::from_ptr(left.py(), other),
)
};
if !equal_nodes(value, other, depth + 1) {
return false;
}
}
true
}
_ => false,
}
}
fn hash_node<H: Hasher>(node: PyNode<'_>, state: &mut H, depth: u8) {
match object_type(node) {
ObjType::Enum => hash_node(resolved(node), state, depth),
ObjType::Unknown => record_unsupported(node),
ObjType::Null => state.write_u32(3_221_225_473),
ObjType::Bool => (node.as_ptr() == types(node.py()).true_).hash(state),
ObjType::Int | ObjType::Float | ObjType::Decimal => {
if let Some(number) = number_of(node) {
if let Some(number) = number.as_f64() {
number.to_bits().hash(state);
} else if let Some(number) = number.as_u64() {
number.hash(state);
} else if let Some(number) = number.as_i64() {
number.hash(state);
}
}
}
ObjType::Str => str_ref(node).unwrap_or_default().hash(state),
ty @ (ObjType::List | ObjType::Tuple) => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return;
}
let array = PyArray::new(node, ty == ObjType::Tuple);
for item in array.elements() {
hash_node(item, state, depth + 1);
}
}
ObjType::Dict => {
if depth == RECURSION_LIMIT {
record_recursion_limit();
return;
}
let dict = unsafe { node.cast_unchecked::<PyDict>() };
let mut hash = 0;
for (key, value) in dict.members() {
let mut item_hasher = AHasher::default();
key.as_ref().hash(&mut item_hasher);
hash_node(value, &mut item_hasher, depth + 1);
hash ^= item_hasher.finish();
}
state.write_u64(hash);
}
}
}
struct HashedNode<'py>(PyNode<'py>);
impl PartialEq for HashedNode<'_> {
fn eq(&self, other: &Self) -> bool {
equal_nodes(self.0, other.0, 0)
}
}
impl Eq for HashedNode<'_> {}
impl Hash for HashedNode<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
hash_node(self.0, state, 0);
}
}
pub struct PyElements<'py> {
sequence: PyNode<'py>,
is_tuple: bool,
index: usize,
len: usize,
}
impl<'py> Iterator for PyElements<'py> {
type Item = PyNode<'py>;
fn next(&mut self) -> Option<PyNode<'py>> {
if self.index >= self.len {
return None;
}
let index = self.index as ffi::Py_ssize_t;
self.index += 1;
let ptr = self.sequence.as_ptr();
let borrowed = unsafe {
if self.is_tuple {
ffi::PyTuple_GetItem(ptr, index)
} else {
ffi::PyList_GetItem(ptr, index)
}
};
if borrowed.is_null() {
unsafe { ffi::PyErr_Clear() };
record_value_error("Sequence changed size during validation");
self.index = self.len;
return None;
}
Some(unsafe { Borrowed::from_ptr(self.sequence.py(), borrowed) })
}
}
#[cfg(all(test, feature = "pyo3-tests"))]
mod tests {
use super::*;
use serde_json::json;
use test_case::test_case;
fn to_py<'py>(py: Python<'py>, value: &Value) -> Bound<'py, PyAny> {
let json = py.import("json").expect("json module");
let text = serde_json::to_string(value).expect("serialize");
json.call_method1("loads", (text,)).expect("loads")
}
#[cfg(feature = "conformance")]
#[test]
fn conforms_to_contract() {
Python::attach(|py| {
let document = crate::conformance::document();
let owned = to_py(py, &document);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
crate::conformance::assert_conformance::<Pyo3>(&node);
assert_eq!(node.to_value().into_owned(), document);
});
}
#[test]
fn tuple_is_array() {
Python::attach(|py| {
let owned = pyo3::types::PyTuple::new(py, [1, 2, 3])
.expect("tuple")
.into_any();
let tuple = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert_eq!(tuple.json_type(), JsonType::Array);
let values: Vec<Option<u64>> = tuple
.as_array()
.expect("array")
.elements()
.map(|item| item.as_number().and_then(|n| n.as_u64()))
.collect();
assert_eq!(values, [Some(1), Some(2), Some(3)]);
assert_eq!(tuple.to_value().into_owned(), json!([1, 2, 3]));
});
}
#[test]
fn enum_resolves_to_value() {
Python::attach(|py| {
let module = pyo3::types::PyModule::from_code(
py,
std::ffi::CString::new(
"import enum\nclass Color(enum.IntEnum):\n RED = 1\nclass Name(enum.StrEnum):\n A = 'abc'\n",
)
.unwrap()
.as_c_str(),
std::ffi::CString::new("m.py").unwrap().as_c_str(),
std::ffi::CString::new("m").unwrap().as_c_str(),
)
.expect("module");
let color = module.getattr("Color").unwrap().getattr("RED").unwrap();
let color = unsafe { Borrowed::from_ptr(py, color.as_ptr()) };
assert_eq!(color.json_type(), JsonType::Number);
assert_eq!(color.as_number().expect("number").as_u64(), Some(1));
let name = module.getattr("Name").unwrap().getattr("A").unwrap();
let name = unsafe { Borrowed::from_ptr(py, name.as_ptr()) };
assert_eq!(name.json_type(), JsonType::String);
assert_eq!(name.as_string().as_deref(), Some("abc"));
});
}
#[test_case("1", true; "int")]
#[test_case("1.5", true; "float")]
#[test_case("-0.0", true; "negative zero")]
#[test_case("10**40", true; "int beyond i64")]
#[test_case("float('nan')", false; "nan")]
#[test_case("float('inf')", false; "inf")]
#[test_case("float('-inf')", false; "negative inf")]
#[test_case("True", false; "bool")]
#[test_case("'x'", false; "string")]
#[test_case("None", false; "none")]
#[test_case("[1]", false; "list")]
fn is_number_matches_as_number(expr: &str, want: bool) {
Python::attach(|py| {
let owned = py
.eval(std::ffi::CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert_eq!(node.is_number(), want);
assert_eq!(node.is_number(), node.as_number().is_some());
});
}
#[test_case(&json!(42), &json!(42.0), true; "int eq float")]
#[test_case(&json!(42.0), &json!(42), true; "float eq int")]
#[test_case(&json!(1), &json!(2), false; "diff numbers")]
#[test_case(&json!("abc"), &json!("abc"), true; "string eq")]
#[test_case(&json!("abc"), &json!("xyz"), false; "string neq")]
#[test_case(&json!(true), &json!(true), true; "bool eq")]
#[test_case(&json!(true), &json!(1), false; "bool is not number")]
#[test_case(&json!(1), &json!(true), false; "number is not bool")]
#[test_case(&json!(null), &json!(null), true; "null eq")]
#[test_case(&json!(null), &json!(0), false; "null neq number")]
#[test_case(&json!([1, 2]), &json!([1, 2]), true; "array eq")]
#[test_case(&json!([1, 2]), &json!([1, 2, 3]), false; "array len diff")]
#[test_case(&json!([1]), &json!([1.0]), true; "array cross-type number")]
#[test_case(&json!([[1], [2]]), &json!([[1], [2]]), true; "nested array")]
#[test_case(&json!({"a": 1}), &json!({"a": 1}), true; "object eq")]
#[test_case(&json!({"a": 1}), &json!({"a": 1, "b": 2}), false; "expected superset")]
#[test_case(&json!({"a": 1, "b": 2}), &json!({"a": 1}), false; "instance superset")]
#[test_case(&json!({"a": 1}), &json!({"a": 2}), false; "object value diff")]
#[test_case(&json!({"a": 1}), &json!({"b": 1}), false; "object key diff")]
#[test_case(&json!({"a": {"b": 1}}), &json!({"a": {"b": 1}}), true; "nested object")]
#[test_case(&json!({"a": 1}), &json!({"a": 1.0}), true; "object cross-type number")]
#[test_case(&json!("1"), &json!(1), false; "string is not number")]
#[test_case(&json!([]), &json!({}), false; "array is not object")]
fn equals_value_semantics(instance: &Value, expected: &Value, want: bool) {
Python::attach(|py| {
let owned = to_py(py, instance);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert_eq!(node.equals_value(expected), want);
});
}
#[test]
fn equals_value_enum_member() {
Python::attach(|py| {
let module = pyo3::types::PyModule::from_code(
py,
std::ffi::CString::new(
"import enum\nclass Color(enum.IntEnum):\n RED = 1\nclass Name(enum.StrEnum):\n A = 'abc'\n",
)
.unwrap()
.as_c_str(),
std::ffi::CString::new("m.py").unwrap().as_c_str(),
std::ffi::CString::new("m").unwrap().as_c_str(),
)
.expect("module");
let red = module.getattr("Color").unwrap().getattr("RED").unwrap();
let red = unsafe { Borrowed::from_ptr(py, red.as_ptr()) };
assert!(red.equals_value(&json!(1)));
assert!(red.equals_value(&json!(1.0)));
assert!(!red.equals_value(&json!(2)));
let a = module.getattr("Name").unwrap().getattr("A").unwrap();
let a = unsafe { Borrowed::from_ptr(py, a.as_ptr()) };
assert!(a.equals_value(&json!("abc")));
assert!(!a.equals_value(&json!("xyz")));
});
}
#[test_case("[1, 2, 3]", true; "unique ints")]
#[test_case("[1, 1]", false; "duplicate ints")]
#[test_case("[1, 1.0]", false; "int equals float")]
#[test_case("[1, True]", true; "bool is not number")]
#[test_case("['a', 'b']", true; "unique strings")]
#[test_case("['a', 'a']", false; "duplicate strings")]
#[test_case("[None, None]", false; "duplicate nulls")]
#[test_case("[None, False]", true; "null is not bool")]
#[test_case("[[1, 2], [1, 2]]", false; "duplicate arrays")]
#[test_case("[[1, 2], [2, 1]]", true; "arrays differ by order")]
#[test_case("[{'a': 1, 'b': 2}, {'b': 2, 'a': 1}]", false; "objects differ by key order only")]
#[test_case("[{'a': 1}, {'a': 2}]", true; "objects differ by value")]
#[test_case("[{'a': 1}, {'b': 1}]", true; "objects differ by key")]
#[test_case("[{'a': 1}, {'a': 1, 'b': 2}]", true; "object is a subset")]
#[test_case("list(range(20))", true; "large unique")]
#[test_case("list(range(20)) + [0]", false; "large duplicate")]
#[test_case("[{'a': [1, {'b': 2}]}, {'a': [1, {'b': 2}]}]", false; "nested duplicate")]
#[test_case("(1, 2, 1)", false; "tuple duplicate")]
fn array_is_unique(expr: &str, want: bool) {
Python::attach(|py| {
let owned = py
.eval(std::ffi::CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
let array = node.as_array().expect("array");
assert_eq!(array.is_unique(), want);
});
}
#[test]
fn surrogate_string_records_error() {
Python::attach(|py| {
let eval = |src: &str| {
py.eval(std::ffi::CString::new(src).unwrap().as_c_str(), None, None)
.expect("eval")
};
let _ = take_pending_error();
let owned = eval("'\\ud800'");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert!(node.as_string().is_none());
let error = take_pending_error().expect("recorded");
assert_eq!(
error.to_string(),
"UnicodeEncodeError: 'utf-8' codec can't encode character '\\ud800' in position 0: surrogates not allowed"
);
let owned = eval("'abc'");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert_eq!(node.as_string().as_deref(), Some("abc"));
assert!(take_pending_error().is_none());
});
}
fn cyclic_dict(py: Python<'_>) -> Bound<'_, PyAny> {
let dict = PyDict::new(py);
dict.set_item("foo", &dict).expect("set_item");
dict.into_any()
}
fn cyclic_list(py: Python<'_>) -> Bound<'_, PyAny> {
let list = pyo3::types::PyList::empty(py);
list.append(&list).expect("append");
list.into_any()
}
#[test_case(cyclic_dict; "dict")]
#[test_case(cyclic_list; "list")]
fn to_value_bounds_cycles(build: fn(Python<'_>) -> Bound<'_, PyAny>) {
Python::attach(|py| {
let _ = take_pending_error();
let owned = build(py);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
let _ = node.to_value();
assert_eq!(
take_pending_error().map(|e| e.to_string()),
Some("ValueError: Recursion limit reached".to_string())
);
});
}
#[test_case(0; "pairwise")]
#[test_case(crate::unique::ITEMS_SIZE_THRESHOLD; "hashed")]
fn is_unique_bounds_cycles(padding: usize) {
Python::attach(|py| {
let _ = take_pending_error();
let items = pyo3::types::PyList::empty(py);
for index in 0..padding {
items.append(index).expect("append");
}
items.append(cyclic_list(py)).expect("append");
items.append(cyclic_list(py)).expect("append");
let owned = items.into_any();
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
let _ = node.as_array().expect("array").is_unique();
assert_eq!(
take_pending_error().map(|e| e.to_string()),
Some("ValueError: Recursion limit reached".to_string())
);
});
}
#[test]
fn equals_value_terminates_on_cycles() {
Python::attach(|py| {
let _ = take_pending_error();
let owned = cyclic_dict(py);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert!(!node.equals_value(&json!({"foo": {"foo": {}}})));
assert!(take_pending_error().is_none());
});
}
#[test]
fn is_unique_pointer_short_circuit() {
Python::attach(|py| {
let _ = take_pending_error();
let element = cyclic_list(py);
let items = pyo3::types::PyList::empty(py);
items.append(&element).expect("append");
items.append(&element).expect("append");
let owned = items.into_any();
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert!(!node.as_array().expect("array").is_unique());
assert!(take_pending_error().is_none());
});
}
#[test]
fn decimal_is_number() {
Python::attach(|py| {
let owned = py
.import("decimal")
.unwrap()
.getattr("Decimal")
.unwrap()
.call1(("1.5",))
.unwrap();
let decimal = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert_eq!(decimal.json_type(), JsonType::Number);
assert_eq!(decimal.as_number().expect("number").as_f64(), Some(1.5));
});
}
}