#![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::{Cell, RefCell},
hash::{Hash, Hasher},
str::FromStr,
sync::atomic::{AtomicPtr, Ordering},
};
#[cfg(not(Py_GIL_DISABLED))]
use std::sync::atomic::AtomicUsize;
use ahash::{AHashSet, AHasher};
use pyo3::{
exceptions::PyValueError,
ffi, intern,
prelude::*,
sync::PyOnceLock,
types::{PyDict, PyString, PyType},
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,
enum_value: *mut ffi::PyObject,
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"),
enum_value: py
.import("enum")
.and_then(|m| m.getattr("Enum")?.getattr("__dict__")?.get_item("value"))
.map_or(std::ptr::null_mut(), |value| value.as_ptr()),
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 }
}
#[must_use]
pub fn stock_enum_value<'py>(member: Borrowed<'_, 'py, PyAny>) -> Option<Bound<'py, PyAny>> {
let py = member.py();
if !reads_value_stock(&member.get_type()) {
return None;
}
let namespace = unsafe {
Bound::from_owned_ptr_or_err(
py,
ffi::PyObject_GenericGetDict(member.as_ptr(), std::ptr::null_mut()),
)
}
.ok()?;
namespace
.cast_into::<PyDict>()
.ok()?
.get_item(intern!(py, "_value_"))
.ok()
.flatten()
}
const ENUM_CLASSES_LIMIT: usize = 8;
thread_local! {
static ENUM_CLASSES: RefCell<Vec<(Py<PyType>, bool)>> = const { RefCell::new(Vec::new()) };
}
fn reads_value_stock(class: &Bound<'_, PyType>) -> bool {
let cached = ENUM_CLASSES.with(|classes| {
classes
.borrow()
.iter()
.find(|(known, _)| known.as_ptr() == class.as_ptr())
.map(|(_, stock)| *stock)
});
if let Some(stock) = cached {
return stock;
}
let stock = inspect_enum_class(class);
ENUM_CLASSES.with(|classes| {
let mut classes = classes.borrow_mut();
if classes.len() == ENUM_CLASSES_LIMIT {
classes.remove(0);
}
classes.push((class.clone().unbind(), stock));
});
stock
}
fn inspect_enum_class(class: &Bound<'_, PyType>) -> bool {
let py = class.py();
let getattro = unsafe { ffi::PyType_GetSlot(class.as_type_ptr(), ffi::Py_tp_getattro) };
if getattro != ffi::PyObject_GenericGetAttr as *mut std::ffi::c_void {
return false;
}
let value_name = intern!(py, "value");
class
.mro()
.iter()
.find_map(|base| {
let namespace = base.getattr(intern!(py, "__dict__")).ok()?;
if namespace.contains(value_name).ok()? {
namespace.get_item(value_name).ok()
} else {
None
}
})
.is_some_and(|descriptor| descriptor.as_ptr() == types(py).enum_value)
}
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());
let value = match stock_enum_value(current.as_borrowed()) {
Some(value) => Ok(value),
None if holding() => current.getattr("value"),
None => {
NEEDS_HOLDING.with(|needs| needs.set(true));
return inert(current.py());
}
};
match value {
Ok(value) => current = value,
Err(error) => {
record_value_error(&format!("Failed to access enum value: {error}"));
return inert(current.py());
}
}
}
let py = current.py();
if holding() {
unsafe { Borrowed::from_ptr(py, hold(current.into_ptr())) }
} else {
unsafe { Borrowed::from_ptr(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(crate) fn take_pending_error() -> Option<PyErr> {
PENDING_ERROR.with(|slot| slot.borrow_mut().take())
}
pub fn inspect<T>(
instance: Borrowed<'_, '_, PyAny>,
holding: bool,
mut run: impl FnMut() -> T,
) -> PyResult<T> {
let mut holding = holding;
loop {
let _scope = PendingErrorScope::enter(holding);
probe_root(instance);
if let Some(error) = take_pending_error() {
return Err(error);
}
let result = run();
if !holding && NEEDS_HOLDING.with(Cell::get) {
holding = true;
continue;
}
if let Some(error) = take_pending_error() {
return Err(error);
}
return Ok(result);
}
}
thread_local! {
static HELD: RefCell<Vec<*mut ffi::PyObject>> = const { RefCell::new(Vec::new()) };
static HOLDING: Cell<bool> = const { Cell::new(false) };
static NEEDS_HOLDING: Cell<bool> = const { Cell::new(false) };
}
#[cfg(not(Py_GIL_DISABLED))]
static HOLDING_CALLS: AtomicUsize = AtomicUsize::new(0);
#[inline]
fn holding() -> bool {
cfg!(Py_GIL_DISABLED) || HOLDING.with(Cell::get)
}
fn hold(object: *mut ffi::PyObject) -> *mut ffi::PyObject {
HELD.with(|held| held.borrow_mut().push(object));
object
}
#[cfg(not(Py_GIL_DISABLED))]
#[inline]
fn held(object: *mut ffi::PyObject) -> *mut ffi::PyObject {
if object.is_null() || !holds_reads() {
return object;
}
hold_borrowed(object)
}
#[cfg(not(Py_GIL_DISABLED))]
#[inline]
fn holds_reads() -> bool {
HOLDING_CALLS.load(Ordering::Relaxed) != 0
}
#[cfg(not(Py_GIL_DISABLED))]
#[cold]
#[inline(never)]
fn hold_borrowed(object: *mut ffi::PyObject) -> *mut ffi::PyObject {
unsafe { ffi::Py_INCREF(object) };
hold(object)
}
struct PendingErrorScope {
saved: Option<PyErr>,
held: usize,
holding: bool,
needs_holding: bool,
}
impl PendingErrorScope {
fn enter(holding: bool) -> Self {
if holding {
#[cfg(not(Py_GIL_DISABLED))]
HOLDING_CALLS.fetch_add(1, Ordering::Relaxed);
}
Self {
saved: take_pending_error(),
held: HELD.with(|held| held.borrow().len()),
holding: HOLDING.with(|cell| cell.replace(holding)),
needs_holding: NEEDS_HOLDING.with(|cell| cell.replace(false)),
}
}
}
impl Drop for PendingErrorScope {
fn drop(&mut self) {
let saved = self.saved.take();
PENDING_ERROR.with(|slot| *slot.borrow_mut() = saved);
if HOLDING.with(|cell| cell.replace(self.holding)) {
#[cfg(not(Py_GIL_DISABLED))]
HOLDING_CALLS.fetch_sub(1, Ordering::Relaxed);
}
NEEDS_HOLDING.with(|cell| cell.set(self.needs_holding));
let released = HELD.with(|held| held.borrow_mut().split_off(self.held));
for object in released {
unsafe { ffi::Py_DECREF(object) };
}
}
}
#[inline]
fn dict_get(dict: *mut ffi::PyObject, key: *mut ffi::PyObject) -> *mut ffi::PyObject {
#[cfg(Py_GIL_DISABLED)]
{
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
if unsafe { ffi::PyDict_GetItemRef(dict, key, &raw mut value) } > 0 {
hold(value)
} else {
std::ptr::null_mut()
}
}
#[cfg(not(Py_GIL_DISABLED))]
{
held(unsafe { ffi::PyDict_GetItem(dict, key) })
}
}
struct DictCursor<'py> {
dict: Borrowed<'py, 'py, PyDict>,
pos: ffi::Py_ssize_t,
#[cfg(PyPy)]
keys: Option<Bound<'py, PyAny>>,
#[cfg(Py_GIL_DISABLED)]
size: ffi::Py_ssize_t,
#[cfg(not(Py_GIL_DISABLED))]
holding: bool,
}
impl<'py> DictCursor<'py> {
fn new(dict: Borrowed<'py, 'py, PyDict>) -> Self {
Self {
dict,
pos: 0,
#[cfg(PyPy)]
keys: None,
#[cfg(Py_GIL_DISABLED)]
size: unsafe { ffi::PyDict_Size(dict.as_ptr()) },
#[cfg(not(Py_GIL_DISABLED))]
holding: holds_reads(),
}
}
#[allow(clippy::inline_always)]
#[inline(always)]
fn next(&mut self) -> Option<(*mut ffi::PyObject, *mut ffi::PyObject)> {
#[cfg(Py_GIL_DISABLED)]
{
let dict = self.dict.as_ptr();
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
let mut section = unsafe { std::mem::zeroed::<ffi::PyCriticalSection>() };
unsafe { ffi::PyCriticalSection_Begin(&raw mut section, dict) };
let resized = unsafe { ffi::PyDict_Size(dict) } != self.size;
let found = !resized
&& unsafe {
ffi::PyDict_Next(dict, &raw mut self.pos, &raw mut key, &raw mut value)
} != 0;
if found {
unsafe {
ffi::Py_INCREF(key);
ffi::Py_INCREF(value);
}
}
unsafe { ffi::PyCriticalSection_End(&raw mut section) };
if resized {
record_value_error("Dictionary changed size during validation");
}
found.then(|| (hold(key), hold(value)))
}
#[cfg(all(not(PyPy), not(Py_GIL_DISABLED)))]
{
let mut key: *mut ffi::PyObject = std::ptr::null_mut();
let mut value: *mut ffi::PyObject = std::ptr::null_mut();
(unsafe {
ffi::PyDict_Next(
self.dict.as_ptr(),
&raw mut self.pos,
&raw mut key,
&raw mut value,
)
} != 0)
.then(|| {
if self.holding {
(hold_borrowed(key), hold_borrowed(value))
} else {
(key, value)
}
})
}
#[cfg(PyPy)]
{
let py = self.dict.py();
let keys = self.keys.get_or_insert_with(|| unsafe {
Bound::from_owned_ptr(py, ffi::PyDict_Keys(self.dict.as_ptr()))
});
if self.pos >= unsafe { ffi::PyList_Size(keys.as_ptr()) } {
return None;
}
let key = held(unsafe { ffi::PyList_GetItem(keys.as_ptr(), self.pos) });
self.pos += 1;
Some((
key,
held(unsafe { ffi::PyDict_GetItem(self.dict.as_ptr(), key) }),
))
}
}
}
pub struct PyValues<'py>(DictCursor<'py>);
impl<'py> Iterator for PyValues<'py> {
type Item = PyNode<'py>;
#[allow(clippy::inline_always)]
#[inline(always)]
fn next(&mut self) -> Option<PyNode<'py>> {
let (_, value) = self.0.next()?;
Some(unsafe { Borrowed::from_ptr(self.0.dict.py(), value) })
}
}
#[doc(hidden)]
#[inline]
#[must_use]
pub fn object_values<'py>(dict: Borrowed<'py, 'py, PyDict>) -> PyValues<'py> {
PyValues(DictCursor::new(dict))
}
#[inline]
#[doc(hidden)]
#[must_use]
pub fn narrow_array<'py>(node: PyNode<'py>) -> Option<PyArray<'py>> {
let t = types(node.py());
let ty = unsafe { ffi::Py_TYPE(node.as_ptr()) };
if ty == t.list {
return Some(PyArray::new(node, false));
}
if ty == t.tuple {
return Some(PyArray::new(node, true));
}
node.as_array()
}
#[inline]
#[doc(hidden)]
#[must_use]
pub fn narrow_object<'py>(node: PyNode<'py>) -> Option<Borrowed<'py, 'py, PyDict>> {
let t = types(node.py());
let ty = unsafe { ffi::Py_TYPE(node.as_ptr()) };
if ty == t.dict {
return Some(unsafe { node.cast_unchecked::<PyDict>() });
}
node.as_object()
}
pub(crate) 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 cursor = DictCursor::new(unsafe { node.cast_unchecked::<PyDict>() });
while let Some((key, _)) = cursor.next() {
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)
}
ObjType::Float => None,
_ => 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)
}
ObjType::Float => None,
_ => 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)
}
fn is_integer(&self) -> bool {
match self.kind {
ObjType::Int => true,
ObjType::Float => unsafe { ffi::PyFloat_AsDouble(self.node.as_ptr()) }.fract() == 0.0,
_ => crate::types::number_is_integer(&self.to_number()),
}
}
fn is_written_as_integer(&self) -> bool {
match self.kind {
ObjType::Int => true,
ObjType::Float => false,
_ => !self.as_str().contains(['.', 'e', 'E']),
}
}
}
pub struct PyMembers<'py>(DictCursor<'py>);
impl<'py> Iterator for PyMembers<'py> {
type Item = (Cow<'py, str>, PyNode<'py>);
#[allow(clippy::inline_always)]
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let (key, value) = self.0.next()?;
let py = self.0.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 value = dict_get(self.as_ptr(), key.as_ptr());
if value.is_null() {
None
} else {
Some(unsafe { Borrowed::from_ptr(self.py(), value) })
}
}
fn members(&self) -> PyMembers<'py> {
PyMembers(DictCursor::new(*self))
}
}
#[derive(Clone, Copy)]
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,
#[cfg(not(Py_GIL_DISABLED))]
holding: holds_reads(),
}
}
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 kinds = [ObjType::Null; crate::unique::ITEMS_SIZE_THRESHOLD];
for (slot, item) in kinds.iter_mut().zip(&items) {
*slot = object_type(*item);
}
let mut idx = 0;
while idx < size {
let mut inner_idx = idx + 1;
while inner_idx < size {
if items[idx].as_ptr() == items[inner_idx].as_ptr()
|| equal_typed(
items[idx],
kinds[idx],
items[inner_idx],
kinds[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;
}
equal_typed(left, object_type(left), right, object_type(right), depth)
}
fn equal_typed(
left: PyNode<'_>,
left_type: ObjType,
right: PyNode<'_>,
right_type: ObjType,
depth: u8,
) -> bool {
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 cursor = DictCursor::new(unsafe { left.cast_unchecked::<PyDict>() });
while let Some((key, value)) = cursor.next() {
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 = dict_get(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() {
crate::unique::number_bits(number).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,
#[cfg(not(Py_GIL_DISABLED))]
holding: bool,
}
impl<'py> Iterator for PyElements<'py> {
type Item = PyNode<'py>;
#[allow(clippy::inline_always)]
#[inline(always)]
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();
#[cfg(Py_GIL_DISABLED)]
let borrowed = unsafe {
if self.is_tuple {
ffi::PyTuple_GetItem(ptr, index)
} else {
let item = ffi::PyList_GetItemRef(ptr, index);
if item.is_null() {
item
} else {
hold(item)
}
}
};
#[cfg(all(any(Py_LIMITED_API, PyPy, GraalPy), not(Py_GIL_DISABLED)))]
let borrowed = unsafe {
if self.is_tuple {
ffi::PyTuple_GetItem(ptr, index)
} else {
ffi::PyList_GetItem(ptr, index)
}
};
#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy, Py_GIL_DISABLED)))]
let borrowed = unsafe {
if self.is_tuple {
ffi::PyTuple_GET_ITEM(ptr, index)
} else if index < ffi::PyList_GET_SIZE(ptr) {
ffi::PyList_GET_ITEM(ptr, index)
} else {
std::ptr::null_mut()
}
};
if borrowed.is_null() {
unsafe { ffi::PyErr_Clear() };
record_value_error("Sequence changed size during validation");
self.index = self.len;
return None;
}
#[cfg(not(Py_GIL_DISABLED))]
if self.holding && !self.is_tuple {
hold_borrowed(borrowed);
}
Some(unsafe { Borrowed::from_ptr(self.sequence.py(), borrowed) })
}
}
#[cfg(all(test, feature = "pyo3-tests"))]
mod tests {
use super::*;
use serde_json::json;
use std::ffi::CString;
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));
});
}
#[test_case("{'a': 1, 'b': [2], 'c': None}", &json!([1, [2], null]); "every value")]
#[test_case("{}", &json!([]); "empty")]
fn object_values_yields_values(expr: &str, expected: &Value) {
Python::attach(|py| {
let owned = py
.eval(CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval");
let dict = owned.cast::<PyDict>().expect("dict").as_borrowed();
let values: Vec<Value> = object_values(dict)
.map(|value| value.to_value().into_owned())
.collect();
assert_eq!(Value::Array(values), *expected);
});
}
#[test_case("[1, 'a']", false; "list")]
#[test_case("(1, 'a')", true; "tuple")]
#[test_case("type('L', (list,), {})([1, 'a'])", false; "list subclass")]
fn narrow_array_accepts_sequences(expr: &str, is_tuple: bool) {
Python::attach(|py| {
let owned = py
.eval(CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
let array = narrow_array(node).expect("array");
assert_eq!(array.sequence.as_ptr(), node.as_ptr());
assert_eq!(array.is_tuple, is_tuple);
let values: Vec<Value> = array
.elements()
.map(|item| item.to_value().into_owned())
.collect();
assert_eq!(values, [json!(1), json!("a")]);
});
}
#[test_case("{'a': 1}"; "dict")]
#[test_case("type('D', (dict,), {})(a=1)"; "dict subclass")]
fn narrow_object_accepts_dicts(expr: &str) {
Python::attach(|py| {
let owned = py
.eval(CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval");
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
let dict = narrow_object(node).expect("object");
assert_eq!(dict.as_ptr(), node.as_ptr());
let values: Vec<Value> = object_values(dict)
.map(|value| value.to_value().into_owned())
.collect();
assert_eq!(values, [json!(1)]);
});
}
#[test_case("{'a': 1}", "[1]"; "dict and list")]
#[test_case("1", "1"; "int")]
#[test_case("'a'", "'a'"; "string")]
fn narrow_rejects_other_types(not_array: &str, not_object: &str) {
Python::attach(|py| {
let _ = take_pending_error();
let eval = |expr: &str| {
py.eval(CString::new(expr).unwrap().as_c_str(), None, None)
.expect("eval")
};
let owned = eval(not_array);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert!(narrow_array(node).is_none());
let owned = eval(not_object);
let node = unsafe { Borrowed::from_ptr(py, owned.as_ptr()) };
assert!(narrow_object(node).is_none());
assert!(take_pending_error().is_none());
});
}
}