Vendor deps.
This commit is contained in:
1 parent
294527085e
commit
91e18fde3a
1714 files changed
+556667
-7
No files matched your search
Vendored
+1083
File diff suppressed because it is too large.
Load diff
Vendored
+281
@@ -0,0 +1,281 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::PyTypeInfo;
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use crate::{
|
||||
err::error_on_minusone,
|
||||
ffi::{
|
||||
self,
|
||||
compat::{
|
||||
PyBytesWriter_Create, PyBytesWriter_Discard, PyBytesWriter_Finish,
|
||||
PyBytesWriter_GetData, PyBytesWriter_GetSize, PyBytesWriter_Resize,
|
||||
},
|
||||
},
|
||||
ffi_ptr_ext::FfiPtrExt,
|
||||
py_result_ext::PyResultExt,
|
||||
};
|
||||
use crate::{types::PyBytes, Bound, IntoPyObject, PyErr, PyResult, Python};
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use core::{
|
||||
mem::ManuallyDrop,
|
||||
ptr::{self, NonNull},
|
||||
};
|
||||
use std::io::IoSlice;
|
||||
|
||||
pub struct PyBytesWriter<'py> {
|
||||
python: Python<'py>,
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
writer: NonNull<ffi::compat::PyBytesWriter>,
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'py> PyBytesWriter<'py> {
|
||||
/// Create a new `PyBytesWriter` with a default initial capacity.
|
||||
#[inline]
|
||||
pub fn new(py: Python<'py>) -> PyResult<Self> {
|
||||
Self::with_capacity(py, 0)
|
||||
}
|
||||
|
||||
/// Create a new `PyBytesWriter` with the specified initial capacity.
|
||||
#[inline]
|
||||
#[cfg_attr(Py_LIMITED_API, allow(clippy::unnecessary_wraps))]
|
||||
pub fn with_capacity(py: Python<'py>, capacity: usize) -> PyResult<Self> {
|
||||
cfg_select! {
|
||||
not(Py_LIMITED_API) => NonNull::new(unsafe { PyBytesWriter_Create(capacity as _) }).map_or_else(
|
||||
|| Err(PyErr::fetch(py)),
|
||||
|writer| {
|
||||
let mut writer = PyBytesWriter { python: py, writer };
|
||||
if capacity > 0 {
|
||||
// SAFETY: By setting the length to 0, we ensure no bytes are considered uninitialized.
|
||||
unsafe { writer.set_len(0)? };
|
||||
}
|
||||
Ok(writer)
|
||||
},
|
||||
),
|
||||
Py_LIMITED_API => Ok(PyBytesWriter {
|
||||
python: py,
|
||||
buffer: Vec::with_capacity(capacity),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current length of the internal buffer.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
cfg_select! {
|
||||
not(Py_LIMITED_API) => unsafe {
|
||||
PyBytesWriter_GetSize(self.writer.as_ptr()) as _
|
||||
},
|
||||
Py_LIMITED_API => self.buffer.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
fn as_mut_ptr(&mut self) -> *mut u8 {
|
||||
unsafe { PyBytesWriter_GetData(self.writer.as_ptr()) as _ }
|
||||
}
|
||||
|
||||
/// Set the length of the internal buffer to `new_len`. The new bytes are uninitialized.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller must ensure the new bytes are initialized. This will also make all pointers
|
||||
/// returned by `as_mut_ptr` invalid, so the caller must not hold any references to the buffer
|
||||
/// across this call.
|
||||
#[inline]
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
unsafe fn set_len(&mut self, new_len: usize) -> PyResult<()> {
|
||||
unsafe {
|
||||
error_on_minusone(
|
||||
self.python,
|
||||
PyBytesWriter_Resize(self.writer.as_ptr(), new_len as _),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> TryFrom<PyBytesWriter<'py>> for Bound<'py, PyBytes> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: PyBytesWriter<'py>) -> Result<Self, Self::Error> {
|
||||
let py = value.python;
|
||||
cfg_select! {
|
||||
// SAFETY:
|
||||
// - we no longer use `value` after this call
|
||||
// - `PyBytesWriter_Finish` will return a new reference to a bytes object on success
|
||||
not(Py_LIMITED_API) => unsafe {
|
||||
PyBytesWriter_Finish(ManuallyDrop::new(value).writer.as_ptr())
|
||||
.assume_owned_or_err(py)
|
||||
.cast_into_unchecked()
|
||||
},
|
||||
Py_LIMITED_API => Ok(PyBytes::new(py, &value.buffer))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for PyBytesWriter<'py> {
|
||||
type Target = PyBytes;
|
||||
type Output = Bound<'py, PyBytes>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
impl<'py> Drop for PyBytesWriter<'py> {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
unsafe { PyBytesWriter_Discard(self.writer.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
impl std::io::Write for PyBytesWriter<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.write_all(buf)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> std::io::Result<usize> {
|
||||
let len = bufs.iter().map(|b| b.len()).sum();
|
||||
let pos = self.len();
|
||||
|
||||
// SAFETY: We write the new uninitialized bytes below.
|
||||
unsafe { self.set_len(self.len() + len)? }
|
||||
|
||||
// SAFETY: We ensured enough capacity above and the ptr will be valid because we will not be
|
||||
// resizing the buffer until we have written all the data.
|
||||
let mut ptr = unsafe { self.as_mut_ptr().add(pos) };
|
||||
|
||||
for buf in bufs {
|
||||
// SAFETY: We have ensured enough capacity above.
|
||||
unsafe { ptr::copy_nonoverlapping(buf.as_ptr(), ptr, buf.len()) };
|
||||
|
||||
// SAFETY: We just wrote buf.len() bytes
|
||||
ptr = unsafe { ptr.add(buf.len()) };
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
|
||||
let len = buf.len();
|
||||
let pos = self.len();
|
||||
|
||||
// SAFETY: We write the new uninitialized bytes below.
|
||||
unsafe { self.set_len(pos + len)? }
|
||||
|
||||
// SAFETY: We have ensured enough capacity above.
|
||||
unsafe { ptr::copy_nonoverlapping(buf.as_ptr(), self.as_mut_ptr().add(pos), len) };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
impl std::io::Write for PyBytesWriter<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> std::io::Result<usize> {
|
||||
self.buffer.write_vectored(bufs)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
|
||||
self.buffer.write_all(buf)
|
||||
}
|
||||
|
||||
fn write_fmt(&mut self, args: core::fmt::Arguments<'_>) -> std::io::Result<()> {
|
||||
self.buffer.write_fmt(args)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::PyBytesMethods;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_io_write() {
|
||||
Python::attach(|py| {
|
||||
let buf = b"hallo world";
|
||||
let mut writer = PyBytesWriter::new(py).unwrap();
|
||||
assert_eq!(writer.write(buf).unwrap(), 11);
|
||||
let bytes: Bound<'_, PyBytes> = writer.try_into().unwrap();
|
||||
assert_eq!(bytes.as_bytes(), buf);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pre_allocated() {
|
||||
Python::attach(|py| {
|
||||
let buf = b"hallo world";
|
||||
let mut writer = PyBytesWriter::with_capacity(py, buf.len()).unwrap();
|
||||
assert_eq!(writer.len(), 0, "Writer position should be zero");
|
||||
assert_eq!(writer.write(buf).unwrap(), 11);
|
||||
let bytes: Bound<'_, PyBytes> = writer.try_into().unwrap();
|
||||
assert_eq!(bytes.as_bytes(), buf);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_write_vectored() {
|
||||
Python::attach(|py| {
|
||||
let bufs = [IoSlice::new(b"hallo "), IoSlice::new(b"world")];
|
||||
let mut writer = PyBytesWriter::new(py).unwrap();
|
||||
assert_eq!(writer.write_vectored(&bufs).unwrap(), 11);
|
||||
let bytes: Bound<'_, PyBytes> = writer.try_into().unwrap();
|
||||
assert_eq!(bytes.as_bytes(), b"hallo world");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_io_write_vectored_large() {
|
||||
Python::attach(|py| {
|
||||
let large_data = vec![b'\n'; 1024]; // 1 KB
|
||||
let bufs = [
|
||||
IoSlice::new(b"hallo"),
|
||||
IoSlice::new(&large_data),
|
||||
IoSlice::new(b"world"),
|
||||
];
|
||||
let mut writer = PyBytesWriter::new(py).unwrap();
|
||||
assert_eq!(writer.write_vectored(&bufs).unwrap(), 1034);
|
||||
let bytes: Bound<'_, PyBytes> = writer.try_into().unwrap();
|
||||
assert!(bytes.as_bytes().starts_with(b"hallo\n"));
|
||||
assert!(bytes.as_bytes().ends_with(b"world"));
|
||||
assert_eq!(bytes.as_bytes().len(), 1034);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_data() {
|
||||
Python::attach(|py| {
|
||||
let mut writer = PyBytesWriter::new(py).unwrap();
|
||||
let large_data = vec![0; 1024]; // 1 KB
|
||||
writer.write_all(&large_data).unwrap();
|
||||
let bytes: Bound<'_, PyBytes> = writer.try_into().unwrap();
|
||||
assert_eq!(bytes.as_bytes(), large_data.as_slice());
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+315
@@ -0,0 +1,315 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Defines how Python calls are dispatched, see [`PyCallArgs`].for more information.
|
||||
|
||||
use crate::ffi_ptr_ext::FfiPtrExt as _;
|
||||
use crate::types::{PyAnyMethods as _, PyDict, PyString, PyTuple};
|
||||
use crate::{ffi, Borrowed, Bound, IntoPyObjectExt as _, Py, PyAny, PyResult};
|
||||
|
||||
pub(crate) mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for () {}
|
||||
impl Sealed for Bound<'_, PyTuple> {}
|
||||
impl Sealed for &'_ Bound<'_, PyTuple> {}
|
||||
impl Sealed for Py<PyTuple> {}
|
||||
impl Sealed for &'_ Py<PyTuple> {}
|
||||
impl Sealed for Borrowed<'_, '_, PyTuple> {}
|
||||
pub struct Token;
|
||||
}
|
||||
|
||||
/// This trait marks types that can be used as arguments to Python function
|
||||
/// calls.
|
||||
///
|
||||
/// This trait is currently implemented for Rust tuple (up to a size of 12),
|
||||
/// [`Bound<'py, PyTuple>`] and [`Py<PyTuple>`]. Custom types that are
|
||||
/// convertible to `PyTuple` via `IntoPyObject` need to do so before passing it
|
||||
/// to `call`.
|
||||
///
|
||||
/// This trait is not intended to used by downstream crates directly. As such it
|
||||
/// has no publicly available methods and cannot be implemented outside of
|
||||
/// `pyo3`. The corresponding public API is available through [`call`]
|
||||
/// ([`call0`], [`call1`] and friends) on [`PyAnyMethods`].
|
||||
///
|
||||
/// # What is `PyCallArgs` used for?
|
||||
/// `PyCallArgs` is used internally in `pyo3` to dispatch the Python calls in
|
||||
/// the most optimal way for the current build configuration. Certain types,
|
||||
/// such as Rust tuples, do allow the usage of a faster calling convention of
|
||||
/// the Python interpreter (if available). More types that may take advantage
|
||||
/// from this may be added in the future.
|
||||
///
|
||||
/// [`call0`]: crate::types::PyAnyMethods::call0
|
||||
/// [`call1`]: crate::types::PyAnyMethods::call1
|
||||
/// [`call`]: crate::types::PyAnyMethods::call
|
||||
/// [`PyAnyMethods`]: crate::types::PyAnyMethods
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` cannot used as a Python `call` argument",
|
||||
note = "`PyCallArgs` is implemented for Rust tuples, `Bound<'py, PyTuple>` and `Py<PyTuple>`",
|
||||
note = "if your type is convertible to `PyTuple` via `IntoPyObject`, call `<arg>.into_pyobject(py)` manually",
|
||||
note = "if you meant to pass the type as a single argument, wrap it in a 1-tuple, `(<arg>,)`"
|
||||
)]
|
||||
pub trait PyCallArgs<'py>: Sized + private::Sealed {
|
||||
#[doc(hidden)]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn call_method_positional(
|
||||
self,
|
||||
object: Borrowed<'_, 'py, PyAny>,
|
||||
method_name: Borrowed<'_, 'py, PyString>,
|
||||
_: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
object
|
||||
.getattr(method_name)
|
||||
.and_then(|method| method.call1(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for () {
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let args = self.into_pyobject_or_pyerr(function.py())?;
|
||||
args.call(function, kwargs, token)
|
||||
}
|
||||
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let args = self.into_pyobject_or_pyerr(function.py())?;
|
||||
args.call_positional(function, token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for Bound<'py, PyTuple> {
|
||||
#[inline]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.as_borrowed().call(function, kwargs, token)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.as_borrowed().call_positional(function, token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for &'_ Bound<'py, PyTuple> {
|
||||
#[inline]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.as_borrowed().call(function, kwargs, token)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.as_borrowed().call_positional(function, token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for Py<PyTuple> {
|
||||
#[inline]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.bind_borrowed(function.py())
|
||||
.call(function, kwargs, token)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.bind_borrowed(function.py())
|
||||
.call_positional(function, token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for &'_ Py<PyTuple> {
|
||||
#[inline]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.bind_borrowed(function.py())
|
||||
.call(function, kwargs, token)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
token: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
self.bind_borrowed(function.py())
|
||||
.call_positional(function, token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> PyCallArgs<'py> for Borrowed<'_, 'py, PyTuple> {
|
||||
#[inline]
|
||||
fn call(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
kwargs: Borrowed<'_, 'py, PyDict>,
|
||||
_: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
unsafe {
|
||||
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr())
|
||||
.assume_owned_or_err(function.py())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call_positional(
|
||||
self,
|
||||
function: Borrowed<'_, 'py, PyAny>,
|
||||
_: private::Token,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
unsafe {
|
||||
ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), core::ptr::null_mut())
|
||||
.assume_owned_or_err(function.py())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "macros")]
|
||||
mod tests {
|
||||
use crate::{
|
||||
pyfunction,
|
||||
types::{PyDict, PyTuple},
|
||||
Py,
|
||||
};
|
||||
|
||||
#[pyfunction(signature = (*args, **kwargs), crate = "crate")]
|
||||
fn args_kwargs(
|
||||
args: Py<PyTuple>,
|
||||
kwargs: Option<Py<PyDict>>,
|
||||
) -> (Py<PyTuple>, Option<Py<PyDict>>) {
|
||||
(args, kwargs)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call() {
|
||||
use crate::{
|
||||
types::{IntoPyDict, PyAnyMethods, PyDict, PyTuple},
|
||||
wrap_pyfunction, Py, Python,
|
||||
};
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(args_kwargs, py).unwrap();
|
||||
|
||||
let args = PyTuple::new(py, [1, 2, 3]).unwrap();
|
||||
let kwargs = &[("foo", 1), ("bar", 2)].into_py_dict(py).unwrap();
|
||||
|
||||
macro_rules! check_call {
|
||||
($args:expr, $kwargs:expr) => {
|
||||
let (a, k): (Py<PyTuple>, Py<PyDict>) = f
|
||||
.call(args.clone(), Some(kwargs))
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert!(a.is(&args));
|
||||
assert!(k.is(kwargs));
|
||||
};
|
||||
}
|
||||
|
||||
// Bound<'py, PyTuple>
|
||||
check_call!(args.clone(), kwargs);
|
||||
|
||||
// &Bound<'py, PyTuple>
|
||||
check_call!(&args, kwargs);
|
||||
|
||||
// Py<PyTuple>
|
||||
check_call!(args.clone().unbind(), kwargs);
|
||||
|
||||
// &Py<PyTuple>
|
||||
check_call!(&args.as_unbound(), kwargs);
|
||||
|
||||
// Borrowed<'_, '_, PyTuple>
|
||||
check_call!(args.as_borrowed(), kwargs);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_positional() {
|
||||
use crate::{
|
||||
types::{PyAnyMethods, PyNone, PyTuple},
|
||||
wrap_pyfunction, Py, Python,
|
||||
};
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(args_kwargs, py).unwrap();
|
||||
|
||||
let args = PyTuple::new(py, [1, 2, 3]).unwrap();
|
||||
|
||||
macro_rules! check_call {
|
||||
($args:expr, $kwargs:expr) => {
|
||||
let (a, k): (Py<PyTuple>, Py<PyNone>) =
|
||||
f.call1(args.clone()).unwrap().extract().unwrap();
|
||||
assert!(a.is(&args));
|
||||
assert!(k.is_none(py));
|
||||
};
|
||||
}
|
||||
|
||||
// Bound<'py, PyTuple>
|
||||
check_call!(args.clone(), kwargs);
|
||||
|
||||
// &Bound<'py, PyTuple>
|
||||
check_call!(&args, kwargs);
|
||||
|
||||
// Py<PyTuple>
|
||||
check_call!(args.clone().unbind(), kwargs);
|
||||
|
||||
// &Py<PyTuple>
|
||||
check_call!(args.as_unbound(), kwargs);
|
||||
|
||||
// Borrowed<'_, '_, PyTuple>
|
||||
check_call!(args.as_borrowed(), kwargs);
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+646
@@ -0,0 +1,646 @@
|
||||
//! Defines conversions between Rust and Python types.
|
||||
use crate::err::PyResult;
|
||||
use crate::impl_::pyclass::ExtractPyClassWithClone;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_identifier, type_hint_subscript, PyStaticExpr};
|
||||
use crate::pyclass::boolean_struct::False;
|
||||
use crate::pyclass::{PyClassGuardError, PyClassGuardMutError};
|
||||
use crate::types::PyList;
|
||||
use crate::types::PyTuple;
|
||||
use crate::{
|
||||
Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut,
|
||||
PyTypeCheck, Python,
|
||||
};
|
||||
use core::convert::Infallible;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Defines a conversion from a Rust type to a Python object, which may fail.
|
||||
///
|
||||
/// This trait has `#[derive(IntoPyObject)]` to automatically implement it for simple types and
|
||||
/// `#[derive(IntoPyObjectRef)]` to implement the same for references.
|
||||
///
|
||||
/// It functions similarly to std's [`TryInto`] trait, but requires a [`Python<'py>`] token
|
||||
/// as an argument.
|
||||
///
|
||||
/// The [`into_pyobject`][IntoPyObject::into_pyobject] method is designed for maximum flexibility and efficiency; it
|
||||
/// - allows for a concrete Python type to be returned (the [`Target`][IntoPyObject::Target] associated type)
|
||||
/// - allows for the smart pointer containing the Python object to be either `Bound<'py, Self::Target>` or `Borrowed<'a, 'py, Self::Target>`
|
||||
/// to avoid unnecessary reference counting overhead
|
||||
/// - allows for a custom error type to be returned in the event of a conversion error to avoid
|
||||
/// unnecessarily creating a Python exception
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - The [`IntoPyObjectExt`] trait, which provides convenience methods for common usages of
|
||||
/// `IntoPyObject` which erase type information and convert errors to `PyErr`.
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` cannot be converted to a Python object",
|
||||
note = "`IntoPyObject` is automatically implemented by the `#[pyclass]` macro",
|
||||
note = "if you do not wish to have a corresponding Python type, implement it manually",
|
||||
note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`"
|
||||
)]
|
||||
pub trait IntoPyObject<'py>: Sized {
|
||||
/// The Python output type
|
||||
type Target;
|
||||
/// The smart pointer type to use.
|
||||
///
|
||||
/// This will usually be [`Bound<'py, Target>`], but in special cases [`Borrowed<'a, 'py, Target>`] can be
|
||||
/// used to minimize reference counting overhead.
|
||||
type Output: BoundObject<'py, Self::Target>;
|
||||
/// The type returned in the event of a conversion error.
|
||||
type Error: Into<PyErr>;
|
||||
|
||||
/// Extracts the type hint information for this type when it appears as a return value.
|
||||
///
|
||||
/// For example, `Vec<u32>` would return `List[int]`.
|
||||
/// The default implementation returns `Any`, which is correct for any type.
|
||||
///
|
||||
/// For most types, the return value for this method will be identical to that of [`FromPyObject::INPUT_TYPE`].
|
||||
/// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument.
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete");
|
||||
|
||||
/// Performs the conversion.
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error>;
|
||||
|
||||
/// Converts sequence of Self into a Python object. Used to specialize `Vec<u8>`, `[u8; N]`
|
||||
/// and `SmallVec<[u8; N]>` as a sequence of bytes into a `bytes` object.
|
||||
#[doc(hidden)]
|
||||
fn owned_sequence_into_pyobject<I>(
|
||||
iter: I,
|
||||
py: Python<'py>,
|
||||
_: private::Token,
|
||||
) -> Result<Bound<'py, PyAny>, PyErr>
|
||||
where
|
||||
I: IntoIterator<Item = Self> + AsRef<[Self]>,
|
||||
I::IntoIter: ExactSizeIterator<Item = Self>,
|
||||
{
|
||||
Ok(PyList::new(py, iter)?.into_any())
|
||||
}
|
||||
|
||||
/// Converts sequence of Self into a Python object. Used to specialize `&[u8]` and `Cow<[u8]>`
|
||||
/// as a sequence of bytes into a `bytes` object.
|
||||
#[doc(hidden)]
|
||||
fn borrowed_sequence_into_pyobject<I>(
|
||||
iter: I,
|
||||
py: Python<'py>,
|
||||
_: private::Token,
|
||||
) -> Result<Bound<'py, PyAny>, PyErr>
|
||||
where
|
||||
Self: private::Reference,
|
||||
I: IntoIterator<Item = Self> + AsRef<[<Self as private::Reference>::BaseType]>,
|
||||
I::IntoIter: ExactSizeIterator<Item = Self>,
|
||||
{
|
||||
Ok(PyList::new(py, iter)?.into_any())
|
||||
}
|
||||
|
||||
/// The output type of [`IntoPyObject::owned_sequence_into_pyobject`] and [`IntoPyObject::borrowed_sequence_into_pyobject`]
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
#[doc(hidden)]
|
||||
const SEQUENCE_OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyList::TYPE_HINT, Self::OUTPUT_TYPE);
|
||||
}
|
||||
|
||||
pub(crate) mod private {
|
||||
pub struct Token;
|
||||
|
||||
pub trait Reference {
|
||||
type BaseType;
|
||||
}
|
||||
|
||||
impl<T> Reference for &'_ T {
|
||||
type BaseType = T;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyTypeCheck> IntoPyObject<'py> for Bound<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &'a Bound<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.as_borrowed())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for Borrowed<'a, 'py, T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &Borrowed<'a, 'py, T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(*self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyTypeCheck> IntoPyObject<'py> for Py<T> {
|
||||
type Target = T;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.into_bound(py))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyTypeCheck> IntoPyObject<'py> for &'a Py<T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.bind_borrowed(py))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> IntoPyObject<'py> for &&'a T
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = <&'a T as IntoPyObject<'py>>::Target;
|
||||
type Output = <&'a T as IntoPyObject<'py>>::Output;
|
||||
type Error = <&'a T as IntoPyObject<'py>>::Error;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
mod into_pyobject_ext {
|
||||
pub trait Sealed {}
|
||||
impl<'py, T> Sealed for T where T: super::IntoPyObject<'py> {}
|
||||
}
|
||||
|
||||
/// Convenience methods for common usages of [`IntoPyObject`]. Every type that implements
|
||||
/// [`IntoPyObject`] also implements this trait.
|
||||
///
|
||||
/// These methods:
|
||||
/// - Drop type information from the output, returning a `PyAny` object.
|
||||
/// - Always convert the `Error` type to `PyErr`, which may incur a performance penalty but it
|
||||
/// more convenient in contexts where the `?` operator would produce a `PyErr` anyway.
|
||||
pub trait IntoPyObjectExt<'py>: IntoPyObject<'py> + into_pyobject_ext::Sealed {
|
||||
/// Converts `self` into an owned Python object, dropping type information.
|
||||
#[inline]
|
||||
fn into_bound_py_any(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
|
||||
match self.into_pyobject(py) {
|
||||
Ok(obj) => Ok(obj.into_any().into_bound()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `self` into an owned Python object, dropping type information and unbinding it
|
||||
/// from the `'py` lifetime.
|
||||
#[inline]
|
||||
fn into_py_any(self, py: Python<'py>) -> PyResult<Py<PyAny>> {
|
||||
match self.into_pyobject(py) {
|
||||
Ok(obj) => Ok(obj.into_any().unbind()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts `self` into a Python object.
|
||||
///
|
||||
/// This is equivalent to calling [`into_pyobject`][IntoPyObject::into_pyobject] followed
|
||||
/// with `.map_err(Into::into)` to convert the error type to [`PyErr`]. This is helpful
|
||||
/// for generic code which wants to make use of the `?` operator.
|
||||
#[inline]
|
||||
fn into_pyobject_or_pyerr(self, py: Python<'py>) -> PyResult<Self::Output> {
|
||||
match self.into_pyobject(py) {
|
||||
Ok(obj) => Ok(obj),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {}
|
||||
|
||||
/// Extract a type from a Python object.
|
||||
///
|
||||
///
|
||||
/// Normal usage is through the `extract` methods on [`Bound`], [`Borrowed`] and [`Py`], which
|
||||
/// forward to this trait.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use pyo3::prelude::*;
|
||||
/// use pyo3::types::PyString;
|
||||
///
|
||||
/// # fn main() -> PyResult<()> {
|
||||
/// Python::attach(|py| {
|
||||
/// // Calling `.extract()` on a `Bound` smart pointer
|
||||
/// let obj: Bound<'_, PyString> = PyString::new(py, "blah");
|
||||
/// let s: String = obj.extract()?;
|
||||
/// # assert_eq!(s, "blah");
|
||||
///
|
||||
/// // Calling `.extract(py)` on a `Py` smart pointer
|
||||
/// let obj: Py<PyString> = obj.unbind();
|
||||
/// let s: String = obj.extract(py)?;
|
||||
/// # assert_eq!(s, "blah");
|
||||
/// # Ok(())
|
||||
/// })
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Note: Depending on the Python version and implementation, some [`FromPyObject`] implementations
|
||||
/// may produce a result that borrows into the Python type. This is described by the input lifetime
|
||||
/// `'a` of `obj`.
|
||||
///
|
||||
/// Types that must not borrow from the input can use [`FromPyObjectOwned`] as a restriction. This
|
||||
/// is most often the case for collection types. See its documentation for more details.
|
||||
///
|
||||
/// # How to implement [`FromPyObject`]?
|
||||
/// ## `#[derive(FromPyObject)]`
|
||||
/// The simplest way to implement [`FromPyObject`] for a custom type is to make use of our derive
|
||||
/// macro.
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(dead_code)]
|
||||
/// use pyo3::prelude::*;
|
||||
///
|
||||
/// #[derive(FromPyObject)]
|
||||
/// struct MyObject {
|
||||
/// msg: String,
|
||||
/// list: Vec<u32>
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
/// By default this will try to extract each field from the Python object by attribute access, but
|
||||
/// this can be customized. For more information about the derive macro, its configuration as well
|
||||
/// as its working principle for other types, take a look at the [guide].
|
||||
///
|
||||
/// In case the derive macro is not sufficient or can not be used for some other reason,
|
||||
/// [`FromPyObject`] can be implemented manually. In the following types without lifetime parameters
|
||||
/// are handled first, because they are a little bit simpler. Types with lifetime parameters are
|
||||
/// explained below.
|
||||
///
|
||||
/// ## Manual implementation for types without lifetime
|
||||
/// Types that do not contain lifetime parameters are unable to borrow from the Python object, so
|
||||
/// the lifetimes of [`FromPyObject`] can be elided:
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(dead_code)]
|
||||
/// use pyo3::prelude::*;
|
||||
///
|
||||
/// struct MyObject {
|
||||
/// msg: String,
|
||||
/// list: Vec<u32>
|
||||
/// }
|
||||
///
|
||||
/// impl FromPyObject<'_, '_> for MyObject {
|
||||
/// type Error = PyErr;
|
||||
///
|
||||
/// fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
/// Ok(MyObject {
|
||||
/// msg: obj.getattr("msg")?.extract()?,
|
||||
/// list: obj.getattr("list")?.extract()?,
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
/// This is basically what the derive macro above expands to.
|
||||
///
|
||||
/// ## Manual implementation for types with lifetime parameters
|
||||
/// For types that contain lifetimes, these lifetimes need to be bound to the corresponding
|
||||
/// [`FromPyObject`] lifetime. This is roughly how the extraction of a typed [`Bound`] is
|
||||
/// implemented within PyO3.
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # #![allow(dead_code)]
|
||||
/// use pyo3::prelude::*;
|
||||
/// use pyo3::types::PyString;
|
||||
///
|
||||
/// struct MyObject<'py>(Bound<'py, PyString>);
|
||||
///
|
||||
/// impl<'py> FromPyObject<'_, 'py> for MyObject<'py> {
|
||||
/// type Error = PyErr;
|
||||
///
|
||||
/// fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
/// Ok(MyObject(obj.cast()?.to_owned()))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// # Details
|
||||
/// [`Cow<'a, str>`] is an example of an output type that may or may not borrow from the input
|
||||
/// lifetime `'a`. Which variant will be produced depends on the runtime type of the Python object.
|
||||
/// For a Python byte string, the existing string data can be borrowed for `'a` into a
|
||||
/// [`Cow::Borrowed`]. For a Python Unicode string, the data may have to be reencoded to UTF-8, and
|
||||
/// copied into a [`Cow::Owned`]. It does _not_ depend on the Python lifetime `'py`.
|
||||
///
|
||||
/// The output type may also depend on the Python lifetime `'py`. This allows the output type to
|
||||
/// keep interacting with the Python interpreter. See also [`Bound<'py, T>`].
|
||||
///
|
||||
/// [`Cow<'a, str>`]: alloc::borrow::Cow
|
||||
/// [`Cow::Borrowed`]: alloc::borrow::Cow::Borrowed
|
||||
/// [`Cow::Owned`]: alloc::borrow::Cow::Owned
|
||||
/// [guide]: https://pyo3.rs/latest/conversions/traits.html#deriving-frompyobject
|
||||
pub trait FromPyObject<'a, 'py>: Sized {
|
||||
/// The type returned in the event of a conversion error.
|
||||
///
|
||||
/// For most use cases defaulting to [PyErr] here is perfectly acceptable. Using a custom error
|
||||
/// type can be used to avoid having to create a Python exception object in the case where that
|
||||
/// exception never reaches Python. This may lead to slightly better performance under certain
|
||||
/// conditions.
|
||||
///
|
||||
/// # Note
|
||||
/// Unfortunately `Try` and thus `?` is based on [`From`], not [`Into`], so implementations may
|
||||
/// need to use `.map_err(Into::into)` sometimes to convert a generic `Error` into a [`PyErr`].
|
||||
type Error: Into<PyErr>;
|
||||
|
||||
/// Provides the type hint information for this type when it appears as an argument.
|
||||
///
|
||||
/// For example, `Vec<u32>` would be `collections.abc.Sequence[int]`.
|
||||
/// The default value is `typing.Any`, which is correct for any type.
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("_typeshed", "Incomplete");
|
||||
|
||||
/// Extracts `Self` from the bound smart pointer `obj`.
|
||||
///
|
||||
/// Users are advised against calling this method directly: instead, use this via
|
||||
/// [`Bound<'_, PyAny>::extract`](crate::types::any::PyAnyMethods::extract) or [`Py::extract`].
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error>;
|
||||
|
||||
/// Specialization hook for extracting sequences for types like `Vec<u8>` and `[u8; N]`,
|
||||
/// where the bytes can be directly copied from some python objects without going through
|
||||
/// iteration.
|
||||
#[doc(hidden)]
|
||||
#[inline(always)]
|
||||
fn sequence_extractor(
|
||||
_obj: Borrowed<'_, 'py, PyAny>,
|
||||
_: private::Token,
|
||||
) -> Option<impl FromPyObjectSequence<Target = Self>> {
|
||||
struct NeverASequence<T>(PhantomData<T>);
|
||||
|
||||
impl<T> FromPyObjectSequence for NeverASequence<T> {
|
||||
type Target = T;
|
||||
|
||||
fn to_vec(&self) -> Vec<Self::Target> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn to_array<const N: usize>(&self) -> PyResult<[Self::Target; N]> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
Option::<NeverASequence<Self>>::None
|
||||
}
|
||||
|
||||
/// Helper used to make a specialized path in extracting `DateTime<Tz>` where `Tz` is
|
||||
/// `chrono::Local`, which will accept "naive" datetime objects as being in the local timezone.
|
||||
#[cfg(feature = "chrono-local")]
|
||||
#[inline]
|
||||
fn as_local_tz(_: private::Token) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
mod from_py_object_sequence {
|
||||
use crate::PyResult;
|
||||
|
||||
/// Private trait for implementing specialized sequence extraction for `Vec<u8>` and `[u8; N]`
|
||||
#[doc(hidden)]
|
||||
pub trait FromPyObjectSequence {
|
||||
type Target;
|
||||
|
||||
fn to_vec(&self) -> Vec<Self::Target>;
|
||||
|
||||
fn to_array<const N: usize>(&self) -> PyResult<[Self::Target; N]>;
|
||||
}
|
||||
}
|
||||
|
||||
// Only reachable / implementable inside PyO3 itself.
|
||||
pub(crate) use from_py_object_sequence::FromPyObjectSequence;
|
||||
|
||||
/// A data structure that can be extracted without borrowing any data from the input.
|
||||
///
|
||||
/// This is primarily useful for trait bounds. For example a [`FromPyObject`] implementation of a
|
||||
/// wrapper type may be able to borrow data from the input, but a [`FromPyObject`] implementation of
|
||||
/// a collection type may only extract owned data.
|
||||
///
|
||||
/// For example [`PyList`] will not hand out references tied to its own lifetime, but "owned"
|
||||
/// references independent of it. (Similar to [`Vec<Arc<T>>`] where you clone the [`Arc<T>`] out).
|
||||
/// This makes it impossible to collect borrowed types in a collection, since they would not borrow
|
||||
/// from the original [`PyList`], but the much shorter lived element reference. See the example
|
||||
/// below.
|
||||
///
|
||||
/// ```,no_run
|
||||
/// # use pyo3::prelude::*;
|
||||
/// # #[allow(dead_code)]
|
||||
/// pub struct MyWrapper<T>(T);
|
||||
///
|
||||
/// impl<'a, 'py, T> FromPyObject<'a, 'py> for MyWrapper<T>
|
||||
/// where
|
||||
/// T: FromPyObject<'a, 'py>
|
||||
/// {
|
||||
/// type Error = T::Error;
|
||||
///
|
||||
/// fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
/// obj.extract().map(MyWrapper)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # #[allow(dead_code)]
|
||||
/// pub struct MyVec<T>(Vec<T>);
|
||||
///
|
||||
/// impl<'py, T> FromPyObject<'_, 'py> for MyVec<T>
|
||||
/// where
|
||||
/// T: FromPyObjectOwned<'py> // 👈 can only extract owned values, because each `item` below
|
||||
/// // is a temporary short lived owned reference
|
||||
/// {
|
||||
/// type Error = PyErr;
|
||||
///
|
||||
/// fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
/// let mut v = MyVec(Vec::new());
|
||||
/// for item in obj.try_iter()? {
|
||||
/// v.0.push(item?.extract::<T>().map_err(Into::into)?);
|
||||
/// }
|
||||
/// Ok(v)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
/// [`Arc<T>`]: alloc::sync::Arc
|
||||
pub trait FromPyObjectOwned<'py>: for<'a> FromPyObject<'a, 'py> {}
|
||||
impl<'py, T> FromPyObjectOwned<'py> for T where T: for<'a> FromPyObject<'a, 'py> {}
|
||||
|
||||
impl<'a, 'py, T> FromPyObject<'a, 'py> for T
|
||||
where
|
||||
T: PyClass + Clone + ExtractPyClassWithClone,
|
||||
{
|
||||
type Error = PyClassGuardError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = <T as crate::PyTypeInfo>::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
Ok(obj.extract::<PyClassGuard<'_, T>>()?.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> FromPyObject<'a, 'py> for PyRef<'py, T>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
type Error = PyClassGuardError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = <T as crate::PyTypeInfo>::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
obj.cast::<T>()
|
||||
.map_err(|e| PyClassGuardError(Some(e)))?
|
||||
.try_borrow()
|
||||
.map_err(|_| PyClassGuardError(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> FromPyObject<'a, 'py> for PyRefMut<'py, T>
|
||||
where
|
||||
T: PyClass<Frozen = False>,
|
||||
{
|
||||
type Error = PyClassGuardMutError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = <T as crate::PyTypeInfo>::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
obj.cast::<T>()
|
||||
.map_err(|e| PyClassGuardMutError(Some(e)))?
|
||||
.try_borrow_mut()
|
||||
.map_err(|_| PyClassGuardMutError(None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for () {
|
||||
type Target = PyTuple;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyTuple::TYPE_HINT, PyStaticExpr::Tuple { elts: &[] });
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyTuple::empty(py))
|
||||
}
|
||||
}
|
||||
|
||||
/// ```rust,compile_fail
|
||||
/// use pyo3::prelude::*;
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// struct TestClass {
|
||||
/// num: u32,
|
||||
/// }
|
||||
///
|
||||
/// let t = TestClass { num: 10 };
|
||||
///
|
||||
/// Python::attach(|py| {
|
||||
/// let pyvalue = Py::new(py, t).unwrap().to_object(py);
|
||||
/// let t: TestClass = pyvalue.extract(py).unwrap();
|
||||
/// })
|
||||
/// ```
|
||||
mod test_no_clone {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_pyclass_skip_from_py_object() {
|
||||
use crate::{types::PyAnyMethods, FromPyObject, IntoPyObject, PyErr, Python};
|
||||
|
||||
#[crate::pyclass(crate = "crate", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct Foo(i32);
|
||||
|
||||
impl<'py> FromPyObject<'_, 'py> for Foo {
|
||||
type Error = PyErr;
|
||||
|
||||
fn extract(obj: crate::Borrowed<'_, 'py, crate::PyAny>) -> Result<Self, Self::Error> {
|
||||
if let Ok(obj) = obj.cast::<Self>() {
|
||||
Ok(obj.borrow().clone())
|
||||
} else {
|
||||
obj.extract::<i32>().map(Self)
|
||||
}
|
||||
}
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let foo1 = 42i32.into_pyobject(py)?;
|
||||
assert_eq!(foo1.extract::<Foo>()?.0, 42);
|
||||
|
||||
let foo2 = Foo(0).into_pyobject(py)?;
|
||||
assert_eq!(foo2.extract::<Foo>()?.0, 0);
|
||||
|
||||
Ok::<_, PyErr>(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_pyclass_from_py_object() {
|
||||
use crate::{types::PyAnyMethods, IntoPyObject, PyErr, Python};
|
||||
|
||||
#[crate::pyclass(crate = "crate", from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct Foo(i32);
|
||||
|
||||
Python::attach(|py| {
|
||||
let foo1 = 42i32.into_pyobject(py)?;
|
||||
assert!(foo1.extract::<Foo>().is_err());
|
||||
|
||||
let foo2 = Foo(0).into_pyobject(py)?;
|
||||
assert_eq!(foo2.extract::<Foo>()?.0, 0);
|
||||
|
||||
Ok::<_, PyErr>(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
#![cfg(feature = "anyhow")]
|
||||
|
||||
//! A conversion from [anyhow]’s [`Error`][anyhow-error] type to [`PyErr`].
|
||||
//!
|
||||
//! Use of an error handling library like [anyhow] is common in application code and when you just
|
||||
//! want error handling to be easy. If you are writing a library or you need more control over your
|
||||
//! errors you might want to design your own error type instead.
|
||||
//!
|
||||
//! When the inner error is a [`PyErr`] without source, it will be extracted out.
|
||||
//! Otherwise a Python [`RuntimeError`] will be created.
|
||||
//! You might find that you need to map the error from your Rust code into another Python exception.
|
||||
//! See [`PyErr::new`] for more information about that.
|
||||
//!
|
||||
//! For information about error handling in general, see the [Error handling] chapter of the Rust
|
||||
//! book.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! ## change * to the version you want to use, ideally the latest.
|
||||
//! anyhow = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"anyhow\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of anyhow and PyO3.
|
||||
//! The required anyhow version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example: Propagating a `PyErr` into [`anyhow::Error`]
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use std::path::PathBuf;
|
||||
//!
|
||||
//! // A wrapper around a Rust function.
|
||||
//! // The pyfunction macro performs the conversion to a PyErr
|
||||
//! #[pyfunction]
|
||||
//! fn py_open(filename: PathBuf) -> anyhow::Result<Vec<u8>> {
|
||||
//! let data = std::fs::read(filename)?;
|
||||
//! Ok(data)
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let error = Python::attach(|py| -> PyResult<Vec<u8>> {
|
||||
//! let fun = wrap_pyfunction!(py_open, py)?;
|
||||
//! let text = fun.call1(("foo.txt",))?.extract::<Vec<u8>>()?;
|
||||
//! Ok(text)
|
||||
//! }).unwrap_err();
|
||||
//!
|
||||
//! println!("{}", error);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Example: Using `anyhow` in general
|
||||
//!
|
||||
//! Note that you don't need this feature to convert a [`PyErr`] into an [`anyhow::Error`], because
|
||||
//! it can already convert anything that implements [`Error`](core::error::Error):
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use pyo3::types::PyBytes;
|
||||
//!
|
||||
//! // An example function that must handle multiple error types.
|
||||
//! //
|
||||
//! // To do this you usually need to design your own error type or use
|
||||
//! // `Box<dyn Error>`. `anyhow` is a convenient alternative for this.
|
||||
//! pub fn decompress(bytes: &[u8]) -> anyhow::Result<String> {
|
||||
//! // An arbitrary example of a Python api you
|
||||
//! // could call inside an application...
|
||||
//! // This might return a `PyErr`.
|
||||
//! let res = Python::attach(|py| {
|
||||
//! let zlib = PyModule::import(py, "zlib")?;
|
||||
//! let decompress = zlib.getattr("decompress")?;
|
||||
//! let bytes = PyBytes::new(py, bytes);
|
||||
//! let value = decompress.call1((bytes,))?;
|
||||
//! value.extract::<Vec<u8>>()
|
||||
//! })?;
|
||||
//!
|
||||
//! // This might be a `FromUtf8Error`.
|
||||
//! let text = String::from_utf8(res)?;
|
||||
//!
|
||||
//! Ok(text)
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> anyhow::Result<()> {
|
||||
//! let bytes: &[u8] = b"x\x9c\x8b\xcc/U(\xce\xc8/\xcdIQ((\xcaOJL\xca\xa9T\
|
||||
//! (-NU(\xc9HU\xc8\xc9LJ\xcbI,IUH.\x02\x91\x99y\xc5%\
|
||||
//! \xa9\x89)z\x00\xf2\x15\x12\xfe";
|
||||
//! let text = decompress(bytes)?;
|
||||
//!
|
||||
//! println!("The text is \"{}\"", text);
|
||||
//! # assert_eq!(text, "You should probably use the libflate crate instead.");
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [anyhow]: https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications."
|
||||
//! [anyhow-error]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type"
|
||||
//! [`RuntimeError`]: https://docs.python.org/3/library/exceptions.html#RuntimeError "Built-in Exceptions — Python documentation"
|
||||
//! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language"
|
||||
|
||||
use crate::exceptions::PyRuntimeError;
|
||||
use crate::PyErr;
|
||||
|
||||
impl From<anyhow::Error> for PyErr {
|
||||
fn from(mut error: anyhow::Error) -> Self {
|
||||
// Errors containing a PyErr without chain or context are returned as the underlying error
|
||||
if error.source().is_none() {
|
||||
error = match error.downcast::<Self>() {
|
||||
Ok(py_err) => return py_err,
|
||||
Err(error) => error,
|
||||
};
|
||||
}
|
||||
PyRuntimeError::new_err(format!("{error:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_anyhow {
|
||||
use crate::exceptions::{PyRuntimeError, PyValueError};
|
||||
use crate::prelude::*;
|
||||
use crate::types::IntoPyDict;
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
|
||||
fn f() -> Result<()> {
|
||||
use std::io;
|
||||
bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!"));
|
||||
}
|
||||
|
||||
fn g() -> Result<()> {
|
||||
f().context("f failed")
|
||||
}
|
||||
|
||||
fn h() -> Result<()> {
|
||||
g().context("g failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_exception_contents() {
|
||||
let err = h().unwrap_err();
|
||||
let expected_contents = format!("{err:?}");
|
||||
let pyerr = PyErr::from(err);
|
||||
|
||||
Python::attach(|py| {
|
||||
let locals = [("err", pyerr)].into_py_dict(py).unwrap();
|
||||
let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err();
|
||||
assert_eq!(pyerr.value(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
|
||||
fn k() -> Result<()> {
|
||||
Err(anyhow!("Some sort of error"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_exception_contents2() {
|
||||
let err = k().unwrap_err();
|
||||
let expected_contents = format!("{err:?}");
|
||||
let pyerr = PyErr::from(err);
|
||||
|
||||
Python::attach(|py| {
|
||||
let locals = [("err", pyerr)].into_py_dict(py).unwrap();
|
||||
let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err();
|
||||
assert_eq!(pyerr.value(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_unwrap_simple_err() {
|
||||
let origin_exc = PyValueError::new_err("Value Error");
|
||||
let err: anyhow::Error = origin_exc.into();
|
||||
let converted: PyErr = err.into();
|
||||
assert!(Python::attach(
|
||||
|py| converted.is_instance_of::<PyValueError>(py)
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_pyo3_unwrap_complex_err() {
|
||||
let origin_exc = PyValueError::new_err("Value Error");
|
||||
let mut err: anyhow::Error = origin_exc.into();
|
||||
err = err.context("Context");
|
||||
let converted: PyErr = err.into();
|
||||
assert!(Python::attach(
|
||||
|py| converted.is_instance_of::<PyRuntimeError>(py)
|
||||
))
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
#![cfg(feature = "bigdecimal")]
|
||||
//! Conversions to and from [bigdecimal](https://docs.rs/bigdecimal)'s [`BigDecimal`] type.
|
||||
//!
|
||||
//! This is useful for converting Python's decimal.Decimal into and from a native Rust type.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"bigdecimal\"] }")]
|
||||
//! bigdecimal = "0.4"
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use a compatible version of bigdecimal and PyO3.
|
||||
//! The required bigdecimal version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create a function that adds one to a BigDecimal
|
||||
//!
|
||||
//! ```rust
|
||||
//! use bigdecimal::BigDecimal;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_one(d: BigDecimal) -> BigDecimal {
|
||||
//! d + 1
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_one, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality
|
||||
//!
|
||||
//!
|
||||
//! ```python
|
||||
//! from my_module import add_one
|
||||
//! from decimal import Decimal
|
||||
//!
|
||||
//! d = Decimal("2")
|
||||
//! value = add_one(d)
|
||||
//!
|
||||
//! assert d + 1 == value
|
||||
//! ```
|
||||
|
||||
use core::str::FromStr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::types::PyTuple;
|
||||
use crate::{
|
||||
exceptions::PyValueError,
|
||||
sync::PyOnceLock,
|
||||
types::{PyAnyMethods, PyStringMethods, PyType},
|
||||
Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python,
|
||||
};
|
||||
use bigdecimal::BigDecimal;
|
||||
use num_bigint::Sign;
|
||||
|
||||
fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
static DECIMAL_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
DECIMAL_CLS.import(py, "decimal", "Decimal")
|
||||
}
|
||||
|
||||
fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
static INVALID_OPERATION_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation")
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for BigDecimal {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
|
||||
let py_str = &obj.str()?;
|
||||
let rs_str = &py_str.to_cow()?;
|
||||
BigDecimal::from_str(rs_str).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for BigDecimal {
|
||||
type Target = PyAny;
|
||||
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let cls = get_decimal_cls(py)?;
|
||||
let (bigint, scale) = self.into_bigint_and_scale();
|
||||
if scale == 0 {
|
||||
return cls.call1((bigint,));
|
||||
}
|
||||
let exponent = scale.checked_neg().ok_or_else(|| {
|
||||
get_invalid_operation_error_cls(py)
|
||||
.map_or_else(|err| err, |cls| PyErr::from_type(cls.clone(), ()))
|
||||
})?;
|
||||
let (sign, digits) = bigint.to_radix_be(10);
|
||||
let signed = matches!(sign, Sign::Minus).into_pyobject(py)?;
|
||||
let digits = PyTuple::new(py, digits)?;
|
||||
|
||||
cls.call1(((signed, digits, exponent),))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_bigdecimal {
|
||||
use super::*;
|
||||
use crate::types::dict::PyDictMethods;
|
||||
use crate::types::PyDict;
|
||||
use alloc::ffi::CString;
|
||||
|
||||
use bigdecimal::{One, Zero};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use proptest::prelude::*;
|
||||
|
||||
macro_rules! convert_constants {
|
||||
($name:ident, $rs:expr, $py:literal) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
Python::attach(|py| {
|
||||
let rs_orig = $rs;
|
||||
let rs_dec = rs_orig.clone().into_pyobject(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("rs_dec", &rs_dec).unwrap();
|
||||
// Checks if BigDecimal -> Python Decimal conversion is correct
|
||||
py.run(
|
||||
&CString::new(format!(
|
||||
"import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec",
|
||||
$py
|
||||
))
|
||||
.unwrap(),
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
// Checks if Python Decimal -> BigDecimal conversion is correct
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let py_result: BigDecimal = py_dec.extract().unwrap();
|
||||
assert_eq!(rs_orig, py_result);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
convert_constants!(convert_zero, BigDecimal::zero(), "0");
|
||||
convert_constants!(convert_one, BigDecimal::one(), "1");
|
||||
convert_constants!(convert_neg_one, -BigDecimal::one(), "-1");
|
||||
convert_constants!(convert_two, BigDecimal::from(2), "2");
|
||||
convert_constants!(convert_ten, BigDecimal::from_str("10").unwrap(), "10");
|
||||
convert_constants!(
|
||||
convert_one_hundred_point_one,
|
||||
BigDecimal::from_str("100.1").unwrap(),
|
||||
"100.1"
|
||||
);
|
||||
convert_constants!(
|
||||
convert_one_thousand,
|
||||
BigDecimal::from_str("1000").unwrap(),
|
||||
"1000"
|
||||
);
|
||||
convert_constants!(
|
||||
convert_scientific,
|
||||
BigDecimal::from_str("1e10").unwrap(),
|
||||
"1e10"
|
||||
);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_roundtrip(
|
||||
number in 0..28u32
|
||||
) {
|
||||
let num = BigDecimal::from(number);
|
||||
Python::attach(|py| {
|
||||
let rs_dec = num.clone().into_pyobject(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("rs_dec", &rs_dec).unwrap();
|
||||
py.run(
|
||||
&CString::new(format!(
|
||||
"import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(),
|
||||
None, Some(&locals)).unwrap();
|
||||
let roundtripped: BigDecimal = rs_dec.extract().unwrap();
|
||||
assert_eq!(num, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integers(num in any::<i64>()) {
|
||||
Python::attach(|py| {
|
||||
let py_num = num.into_pyobject(py).unwrap();
|
||||
let roundtripped: BigDecimal = py_num.extract().unwrap();
|
||||
let rs_dec = BigDecimal::from(num);
|
||||
assert_eq!(rs_dec, roundtripped);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nan() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import decimal\npy_dec = decimal.Decimal(\"NaN\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let roundtripped: Result<BigDecimal, PyErr> = py_dec.extract();
|
||||
assert!(roundtripped.is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infinity() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import decimal\npy_dec = decimal.Decimal(\"Infinity\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let roundtripped: Result<BigDecimal, PyErr> = py_dec.extract();
|
||||
assert!(roundtripped.is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_precision_loss() {
|
||||
Python::attach(|py| {
|
||||
let src = "1e4";
|
||||
let expected = get_decimal_cls(py)
|
||||
.unwrap()
|
||||
.call1((src,))
|
||||
.unwrap()
|
||||
.call_method0("as_tuple")
|
||||
.unwrap();
|
||||
let actual = src
|
||||
.parse::<BigDecimal>()
|
||||
.unwrap()
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.call_method0("as_tuple")
|
||||
.unwrap();
|
||||
|
||||
assert!(actual.eq(expected).unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+147
@@ -0,0 +1,147 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "bytes")]
|
||||
|
||||
//! Conversions to and from [bytes](https://docs.rs/bytes/latest/bytes/)'s [`Bytes`].
|
||||
//!
|
||||
//! This is useful for efficiently converting Python's `bytes` types efficiently.
|
||||
//! While `bytes` will be directly borrowed, converting from `bytearray` will result in a copy.
|
||||
//!
|
||||
//! When converting `Bytes` back into Python, this will do a copy, just like `&[u8]` and `Vec<u8>`.
|
||||
//!
|
||||
//! # When to use `Bytes`
|
||||
//!
|
||||
//! Unless you specifically need [`Bytes`] for ref-counted ownership and sharing,
|
||||
//! you may find that using `&[u8]`, `Vec<u8>`, [`Bound<PyBytes>`], or [`PyBackedBytes`]
|
||||
//! is simpler for most use cases.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add in your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! bytes = "1.10"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"bytes\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of bytes and PyO3.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create functions which return `Bytes` or take `Bytes` as arguments:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pyo3::prelude::*;
|
||||
//! use bytes::Bytes;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn get_message_bytes() -> Bytes {
|
||||
//! Bytes::from_static(b"Hello Python!")
|
||||
//! }
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn num_bytes(bytes: Bytes) -> usize {
|
||||
//! bytes.len()
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(get_message_bytes, m)?)?;
|
||||
//! m.add_function(wrap_pyfunction!(num_bytes, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that calls these functions:
|
||||
//!
|
||||
//! ```python
|
||||
//! from my_module import get_message_bytes, num_bytes
|
||||
//!
|
||||
//! message = get_message_bytes()
|
||||
//! assert message == b"Hello Python!"
|
||||
//!
|
||||
//! size = num_bytes(message)
|
||||
//! assert size == 13
|
||||
//! ```
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::instance::Bound;
|
||||
use crate::pybacked::PyBackedBytes;
|
||||
use crate::types::PyBytes;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::PyTypeInfo;
|
||||
use crate::{Borrowed, CastError, FromPyObject, PyAny, PyErr, Python};
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for Bytes {
|
||||
type Error = CastError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyBackedBytes::INPUT_TYPE;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
Ok(Bytes::from_owner(obj.extract::<PyBackedBytes>()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Bytes {
|
||||
type Target = PyBytes;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyBytes::new(py, &self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Bytes {
|
||||
type Target = PyBytes;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyBytes::new(py, self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes};
|
||||
use crate::Python;
|
||||
|
||||
#[test]
|
||||
fn test_bytes() {
|
||||
Python::attach(|py| {
|
||||
let py_bytes = PyBytes::new(py, b"foobar");
|
||||
let bytes: Bytes = py_bytes.extract().unwrap();
|
||||
assert_eq!(&*bytes, b"foobar");
|
||||
|
||||
let bytes = Bytes::from_static(b"foobar").into_pyobject(py).unwrap();
|
||||
assert!(bytes.is_instance_of::<PyBytes>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytearray() {
|
||||
Python::attach(|py| {
|
||||
let py_bytearray = PyByteArray::new(py, b"foobar");
|
||||
let bytes: Bytes = py_bytearray.extract().unwrap();
|
||||
assert_eq!(&*bytes, b"foobar");
|
||||
|
||||
// Editing the bytearray should not change extracted Bytes
|
||||
unsafe { py_bytearray.as_bytes_mut()[0] = b'x' };
|
||||
assert_eq!(&bytes, "foobar");
|
||||
assert_eq!(&py_bytearray.extract::<Vec<u8>>().unwrap(), b"xoobar");
|
||||
});
|
||||
}
|
||||
}
|
||||
+1525
File diff suppressed because it is too large.
Load diff
+229
@@ -0,0 +1,229 @@
|
||||
#![cfg(all(Py_3_9, feature = "chrono-tz"))]
|
||||
|
||||
//! Conversions to and from [chrono-tz](https://docs.rs/chrono-tz/)’s `Tz`.
|
||||
//!
|
||||
//! This feature requires at least Python 3.9.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! chrono-tz = "0.8"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"chrono-tz\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of chrono, chrono-tz and PyO3.
|
||||
//! The required chrono version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example: Convert a `zoneinfo.ZoneInfo` to chrono-tz's `Tz`
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use chrono_tz::Tz;
|
||||
//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
|
||||
//!
|
||||
//! fn main() -> PyResult<()> {
|
||||
//! Python::initialize();
|
||||
//! Python::attach(|py| {
|
||||
//! // Convert to Python
|
||||
//! let py_tzinfo = Tz::Europe__Paris.into_pyobject(py)?;
|
||||
//! // Convert back to Rust
|
||||
//! assert_eq!(py_tzinfo.extract::<Tz>()?, Tz::Europe__Paris);
|
||||
//! Ok(())
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::PyValueError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::types::{any::PyAnyMethods, PyTzInfo};
|
||||
#[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
|
||||
use crate::PyTypeInfo;
|
||||
use crate::{intern, Borrowed, Bound, FromPyObject, PyAny, PyErr, Python};
|
||||
use alloc::borrow::Cow;
|
||||
use chrono_tz::Tz;
|
||||
use core::str::FromStr;
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Tz {
|
||||
type Target = PyTzInfo;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(all(feature = "experimental-inspect", Py_3_9))]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
|
||||
|
||||
#[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PyTzInfo::timezone(py, self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Tz {
|
||||
type Target = PyTzInfo;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Tz::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for Tz {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(all(feature = "experimental-inspect", Py_3_9))]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("zoneinfo", "ZoneInfo");
|
||||
|
||||
#[cfg(all(feature = "experimental-inspect", not(Py_3_9)))]
|
||||
const INPUT_TYPE: PyStaticExpr = PyTzInfo::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
Tz::from_str(
|
||||
&ob.getattr(intern!(ob.py(), "key"))?
|
||||
.extract::<Cow<'_, str>>()?,
|
||||
)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::PyAnyMethods;
|
||||
use crate::types::IntoPyDict;
|
||||
use crate::types::PyTzInfo;
|
||||
use crate::Bound;
|
||||
use crate::Python;
|
||||
use chrono::offset::LocalResult;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
|
||||
#[test]
|
||||
fn test_frompyobject() {
|
||||
Python::attach(|py| {
|
||||
assert_eq!(
|
||||
new_zoneinfo(py, "Europe/Paris").extract::<Tz>().unwrap(),
|
||||
Tz::Europe__Paris
|
||||
);
|
||||
assert_eq!(new_zoneinfo(py, "UTC").extract::<Tz>().unwrap(), Tz::UTC);
|
||||
assert_eq!(
|
||||
new_zoneinfo(py, "Etc/GMT-5").extract::<Tz>().unwrap(),
|
||||
Tz::Etc__GMTMinus5
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ambiguous_datetime_to_pyobject() {
|
||||
let dates = [
|
||||
DateTime::<Utc>::from_str("2020-10-24 23:00:00 UTC").unwrap(),
|
||||
DateTime::<Utc>::from_str("2020-10-25 00:00:00 UTC").unwrap(),
|
||||
DateTime::<Utc>::from_str("2020-10-25 01:00:00 UTC").unwrap(),
|
||||
];
|
||||
|
||||
let dates = dates.map(|dt| dt.with_timezone(&Tz::Europe__London));
|
||||
|
||||
assert_eq!(
|
||||
dates.map(|dt| dt.to_string()),
|
||||
[
|
||||
"2020-10-25 00:00:00 BST",
|
||||
"2020-10-25 01:00:00 BST",
|
||||
"2020-10-25 01:00:00 GMT"
|
||||
]
|
||||
);
|
||||
|
||||
let dates = Python::attach(|py| {
|
||||
let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
|
||||
assert_eq!(
|
||||
pydates
|
||||
.clone()
|
||||
.map(|dt| dt.getattr("hour").unwrap().extract::<usize>().unwrap()),
|
||||
[0, 1, 1]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
pydates
|
||||
.clone()
|
||||
.map(|dt| dt.getattr("fold").unwrap().extract::<usize>().unwrap() > 0),
|
||||
[false, false, true]
|
||||
);
|
||||
|
||||
pydates.map(|dt| dt.extract::<DateTime<Tz>>().unwrap())
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
dates.map(|dt| dt.to_string()),
|
||||
[
|
||||
"2020-10-25 00:00:00 BST",
|
||||
"2020-10-25 01:00:00 BST",
|
||||
"2020-10-25 01:00:00 GMT"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonexistent_datetime_from_pyobject() {
|
||||
// Pacific_Apia skipped the 30th of December 2011 entirely
|
||||
|
||||
let naive_dt = NaiveDate::from_ymd_opt(2011, 12, 30)
|
||||
.unwrap()
|
||||
.and_hms_opt(2, 0, 0)
|
||||
.unwrap();
|
||||
let tz = Tz::Pacific__Apia;
|
||||
|
||||
// sanity check
|
||||
assert_eq!(naive_dt.and_local_timezone(tz), LocalResult::None);
|
||||
|
||||
Python::attach(|py| {
|
||||
// create as a Python object manually
|
||||
let py_tz = tz.into_pyobject(py).unwrap();
|
||||
let py_dt_naive = naive_dt.into_pyobject(py).unwrap();
|
||||
let py_dt = py_dt_naive
|
||||
.call_method(
|
||||
"replace",
|
||||
(),
|
||||
Some(&[("tzinfo", py_tz)].into_py_dict(py).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now try to extract
|
||||
let err = py_dt.extract::<DateTime<Tz>>().unwrap_err();
|
||||
assert_eq!(err.to_string(), "ValueError: The datetime datetime.datetime(2011, 12, 30, 2, 0, tzinfo=zoneinfo.ZoneInfo(key='Pacific/Apia')) contains an incompatible timezone");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(Py_GIL_DISABLED))] // https://github.com/python/cpython/issues/116738#issuecomment-2404360445
|
||||
fn test_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let assert_eq = |l: Bound<'_, PyTzInfo>, r: Bound<'_, PyTzInfo>| {
|
||||
assert!(l.eq(&r).unwrap(), "{l:?} != {r:?}");
|
||||
};
|
||||
|
||||
assert_eq(
|
||||
Tz::Europe__Paris.into_pyobject(py).unwrap(),
|
||||
new_zoneinfo(py, "Europe/Paris"),
|
||||
);
|
||||
assert_eq(Tz::UTC.into_pyobject(py).unwrap(), new_zoneinfo(py, "UTC"));
|
||||
assert_eq(
|
||||
Tz::Etc__GMTMinus5.into_pyobject(py).unwrap(),
|
||||
new_zoneinfo(py, "Etc/GMT-5"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn new_zoneinfo<'py>(py: Python<'py>, name: &str) -> Bound<'py, PyTzInfo> {
|
||||
PyTzInfo::timezone(py, name).unwrap()
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
#![cfg(feature = "either")]
|
||||
|
||||
//! Conversion to/from
|
||||
//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
|
||||
//! [`Either`] type to a union of two Python types.
|
||||
//!
|
||||
//! Use of a generic sum type like [either] is common when you want to either accept one of two possible
|
||||
//! types as an argument or return one of two possible types from a function, without having to define
|
||||
//! a helper type manually yourself.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! ## change * to the version you want to use, ideally the latest.
|
||||
//! either = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"either\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of either and PyO3.
|
||||
//! The required either version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example: Convert a `int | str` to `Either<i32, String>`.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use either::Either;
|
||||
//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods};
|
||||
//!
|
||||
//! fn main() -> PyResult<()> {
|
||||
//! Python::initialize();
|
||||
//! Python::attach(|py| {
|
||||
//! // Create a string and an int in Python.
|
||||
//! let py_str = "crab".into_pyobject(py)?;
|
||||
//! let py_int = 42i32.into_pyobject(py)?;
|
||||
//! // Now convert it to an Either<i32, String>.
|
||||
//! let either_str: Either<i32, String> = py_str.extract()?;
|
||||
//! let either_int: Either<i32, String> = py_int.extract()?;
|
||||
//! Ok(())
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [either](https://docs.rs/either/ "A library for easy idiomatic error handling and reporting in Rust applications")’s
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_union;
|
||||
use crate::{
|
||||
exceptions::PyTypeError, Borrowed, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny,
|
||||
PyErr, Python,
|
||||
};
|
||||
use either::Either;
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
|
||||
impl<'py, L, R> IntoPyObject<'py> for Either<L, R>
|
||||
where
|
||||
L: IntoPyObject<'py>,
|
||||
R: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(L::OUTPUT_TYPE, R::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
Either::Left(l) => l.into_bound_py_any(py),
|
||||
Either::Right(r) => r.into_bound_py_any(py),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
|
||||
impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either<L, R>
|
||||
where
|
||||
&'a L: IntoPyObject<'py>,
|
||||
&'a R: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(<&L>::OUTPUT_TYPE, <&R>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
Either::Left(l) => l.into_bound_py_any(py),
|
||||
Either::Right(r) => r.into_bound_py_any(py),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
|
||||
impl<'a, 'py, L, R> FromPyObject<'a, 'py> for Either<L, R>
|
||||
where
|
||||
L: FromPyObject<'a, 'py>,
|
||||
R: FromPyObject<'a, 'py>,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(L::INPUT_TYPE, R::INPUT_TYPE);
|
||||
|
||||
#[inline]
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
if let Ok(l) = obj.extract::<L>() {
|
||||
Ok(Either::Left(l))
|
||||
} else if let Ok(r) = obj.extract::<R>() {
|
||||
Ok(Either::Right(r))
|
||||
} else {
|
||||
// TODO: it might be nice to use the `type_input()` name here once `type_input`
|
||||
// is not experimental, rather than the Rust type names.
|
||||
let err_msg = format!(
|
||||
"failed to convert the value to 'Union[{}, {}]'",
|
||||
core::any::type_name::<L>(),
|
||||
core::any::type_name::<R>()
|
||||
);
|
||||
Err(PyTypeError::new_err(err_msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
use crate::exceptions::PyTypeError;
|
||||
use crate::{IntoPyObject, Python};
|
||||
|
||||
use crate::types::PyAnyMethods;
|
||||
use either::Either;
|
||||
|
||||
#[test]
|
||||
fn test_either_conversion() {
|
||||
type E = Either<i32, String>;
|
||||
type E1 = Either<i32, f32>;
|
||||
type E2 = Either<f32, i32>;
|
||||
|
||||
Python::attach(|py| {
|
||||
let l = E::Left(42);
|
||||
let obj_l = (&l).into_pyobject(py).unwrap();
|
||||
assert_eq!(obj_l.extract::<i32>().unwrap(), 42);
|
||||
assert_eq!(obj_l.extract::<E>().unwrap(), l);
|
||||
|
||||
let r = E::Right("foo".to_owned());
|
||||
let obj_r = (&r).into_pyobject(py).unwrap();
|
||||
assert_eq!(obj_r.extract::<Cow<'_, str>>().unwrap(), "foo");
|
||||
assert_eq!(obj_r.extract::<E>().unwrap(), r);
|
||||
|
||||
let obj_s = "foo".into_pyobject(py).unwrap();
|
||||
let err = obj_s.extract::<E1>().unwrap_err();
|
||||
assert!(err.is_instance_of::<PyTypeError>(py));
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"TypeError: failed to convert the value to 'Union[i32, f32]'"
|
||||
);
|
||||
|
||||
let obj_i = 42i32.into_pyobject(py).unwrap();
|
||||
assert_eq!(obj_i.extract::<E1>().unwrap(), E1::Left(42));
|
||||
assert_eq!(obj_i.extract::<E2>().unwrap(), E2::Left(42.0));
|
||||
|
||||
let obj_f = 42.0f64.into_pyobject(py).unwrap();
|
||||
assert_eq!(obj_f.extract::<E1>().unwrap(), E1::Right(42.0));
|
||||
assert_eq!(obj_f.extract::<E2>().unwrap(), E2::Left(42.0));
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+196
@@ -0,0 +1,196 @@
|
||||
#![cfg(feature = "eyre")]
|
||||
|
||||
//! A conversion from
|
||||
//! [eyre](https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications.")’s
|
||||
//! [`Report`] type to [`PyErr`].
|
||||
//!
|
||||
//! Use of an error handling library like [eyre] is common in application code and when you just
|
||||
//! want error handling to be easy. If you are writing a library or you need more control over your
|
||||
//! errors you might want to design your own error type instead.
|
||||
//!
|
||||
//! When the inner error is a [`PyErr`] without source, it will be extracted out.
|
||||
//! Otherwise a Python [`RuntimeError`] will be created.
|
||||
//! You might find that you need to map the error from your Rust code into another Python exception.
|
||||
//! See [`PyErr::new`] for more information about that.
|
||||
//!
|
||||
//! For information about error handling in general, see the [Error handling] chapter of the Rust
|
||||
//! book.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! ## change * to the version you want to use, ideally the latest.
|
||||
//! eyre = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"eyre\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of eyre and PyO3.
|
||||
//! The required eyre version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example: Propagating a `PyErr` into [`eyre::Report`]
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use std::path::PathBuf;
|
||||
//!
|
||||
//! // A wrapper around a Rust function.
|
||||
//! // The pyfunction macro performs the conversion to a PyErr
|
||||
//! #[pyfunction]
|
||||
//! fn py_open(filename: PathBuf) -> eyre::Result<Vec<u8>> {
|
||||
//! let data = std::fs::read(filename)?;
|
||||
//! Ok(data)
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let error = Python::attach(|py| -> PyResult<Vec<u8>> {
|
||||
//! let fun = wrap_pyfunction!(py_open, py)?;
|
||||
//! let text = fun.call1(("foo.txt",))?.extract::<Vec<u8>>()?;
|
||||
//! Ok(text)
|
||||
//! }).unwrap_err();
|
||||
//!
|
||||
//! println!("{}", error);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Example: Using `eyre` in general
|
||||
//!
|
||||
//! Note that you don't need this feature to convert a [`PyErr`] into an [`eyre::Report`], because
|
||||
//! it can already convert anything that implements [`Error`](core::error::Error):
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use pyo3::types::PyBytes;
|
||||
//!
|
||||
//! // An example function that must handle multiple error types.
|
||||
//! //
|
||||
//! // To do this you usually need to design your own error type or use
|
||||
//! // `Box<dyn Error>`. `eyre` is a convenient alternative for this.
|
||||
//! pub fn decompress(bytes: &[u8]) -> eyre::Result<String> {
|
||||
//! // An arbitrary example of a Python api you
|
||||
//! // could call inside an application...
|
||||
//! // This might return a `PyErr`.
|
||||
//! let res = Python::attach(|py| {
|
||||
//! let zlib = PyModule::import(py, "zlib")?;
|
||||
//! let decompress = zlib.getattr("decompress")?;
|
||||
//! let bytes = PyBytes::new(py, bytes);
|
||||
//! let value = decompress.call1((bytes,))?;
|
||||
//! value.extract::<Vec<u8>>()
|
||||
//! })?;
|
||||
//!
|
||||
//! // This might be a `FromUtf8Error`.
|
||||
//! let text = String::from_utf8(res)?;
|
||||
//!
|
||||
//! Ok(text)
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> eyre::Result<()> {
|
||||
//! let bytes: &[u8] = b"x\x9c\x8b\xcc/U(\xce\xc8/\xcdIQ((\xcaOJL\xca\xa9T\
|
||||
//! (-NU(\xc9HU\xc8\xc9LJ\xcbI,IUH.\x02\x91\x99y\xc5%\
|
||||
//! \xa9\x89)z\x00\xf2\x15\x12\xfe";
|
||||
//! let text = decompress(bytes)?;
|
||||
//!
|
||||
//! println!("The text is \"{}\"", text);
|
||||
//! # assert_eq!(text, "You should probably use the libflate crate instead.");
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications."
|
||||
//! [`RuntimeError`]: https://docs.python.org/3/library/exceptions.html#RuntimeError "Built-in Exceptions — Python documentation"
|
||||
//! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language"
|
||||
|
||||
use crate::exceptions::PyRuntimeError;
|
||||
use crate::PyErr;
|
||||
use eyre::Report;
|
||||
|
||||
/// Converts [`eyre::Report`] to a [`PyErr`] containing a [`PyRuntimeError`].
|
||||
///
|
||||
/// If you want to raise a different Python exception you will have to do so manually. See
|
||||
/// [`PyErr::new`] for more information about that.
|
||||
impl From<eyre::Report> for PyErr {
|
||||
fn from(mut error: Report) -> Self {
|
||||
// Errors containing a PyErr without chain or context are returned as the underlying error
|
||||
if error.source().is_none() {
|
||||
error = match error.downcast::<Self>() {
|
||||
Ok(py_err) => return py_err,
|
||||
Err(error) => error,
|
||||
};
|
||||
}
|
||||
PyRuntimeError::new_err(format!("{error:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::exceptions::{PyRuntimeError, PyValueError};
|
||||
use crate::prelude::*;
|
||||
use crate::types::IntoPyDict;
|
||||
|
||||
use eyre::{bail, eyre, Report, Result, WrapErr};
|
||||
|
||||
fn f() -> Result<()> {
|
||||
use std::io;
|
||||
bail!(io::Error::new(io::ErrorKind::PermissionDenied, "oh no!"));
|
||||
}
|
||||
|
||||
fn g() -> Result<()> {
|
||||
f().wrap_err("f failed")
|
||||
}
|
||||
|
||||
fn h() -> Result<()> {
|
||||
g().wrap_err("g failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_exception_contents() {
|
||||
let err = h().unwrap_err();
|
||||
let expected_contents = format!("{err:?}");
|
||||
let pyerr = PyErr::from(err);
|
||||
|
||||
Python::attach(|py| {
|
||||
let locals = [("err", pyerr)].into_py_dict(py).unwrap();
|
||||
let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err();
|
||||
assert_eq!(pyerr.value(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
|
||||
fn k() -> Result<()> {
|
||||
Err(eyre!("Some sort of error"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_exception_contents2() {
|
||||
let err = k().unwrap_err();
|
||||
let expected_contents = format!("{err:?}");
|
||||
let pyerr = PyErr::from(err);
|
||||
|
||||
Python::attach(|py| {
|
||||
let locals = [("err", pyerr)].into_py_dict(py).unwrap();
|
||||
let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err();
|
||||
assert_eq!(pyerr.value(py).to_string(), expected_contents);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyo3_unwrap_simple_err() {
|
||||
let origin_exc = PyValueError::new_err("Value Error");
|
||||
let report: Report = origin_exc.into();
|
||||
let converted: PyErr = report.into();
|
||||
assert!(Python::attach(
|
||||
|py| converted.is_instance_of::<PyValueError>(py)
|
||||
))
|
||||
}
|
||||
#[test]
|
||||
fn test_pyo3_unwrap_complex_err() {
|
||||
let origin_exc = PyValueError::new_err("Value Error");
|
||||
let mut report: Report = origin_exc.into();
|
||||
report = report.wrap_err("Wrapped");
|
||||
let converted: PyErr = report.into();
|
||||
assert!(Python::attach(
|
||||
|py| converted.is_instance_of::<PyRuntimeError>(py)
|
||||
))
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
#![cfg(feature = "hashbrown")]
|
||||
|
||||
//! Conversions to and from [hashbrown](https://docs.rs/hashbrown/)’s
|
||||
//! `HashMap` and `HashSet`.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # change * to the latest versions
|
||||
//! hashbrown = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"hashbrown\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of hashbrown and PyO3.
|
||||
//! The required hashbrown version may vary based on the version of PyO3.
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::{
|
||||
conversion::{FromPyObjectOwned, IntoPyObject},
|
||||
types::{
|
||||
any::PyAnyMethods, dict::PyDictMethods, frozenset::PyFrozenSetMethods, set::PySetMethods,
|
||||
PyDict, PyFrozenSet, PySet,
|
||||
},
|
||||
Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python,
|
||||
};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::{type_hint_subscript, type_hint_union, PyTypeInfo};
|
||||
use core::hash;
|
||||
|
||||
impl<'py, K, V, H> IntoPyObject<'py> for hashbrown::HashMap<K, V, H>
|
||||
where
|
||||
K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::OUTPUT_TYPE, V::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a hashbrown::HashMap<K, V, H>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
&'a V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, <&K>::OUTPUT_TYPE, <&V>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, V, S> FromPyObject<'_, 'py> for hashbrown::HashMap<K, V, S>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + Eq + hash::Hash,
|
||||
V: FromPyObjectOwned<'py>,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::INPUT_TYPE, V::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, PyErr> {
|
||||
let dict = ob.cast::<PyDict>()?;
|
||||
let mut ret = hashbrown::HashMap::with_capacity_and_hasher(dict.len(), S::default());
|
||||
for (k, v) in dict.iter() {
|
||||
ret.insert(
|
||||
k.extract().map_err(Into::into)?,
|
||||
v.extract().map_err(Into::into)?,
|
||||
);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, H> IntoPyObject<'py> for hashbrown::HashSet<K, H>
|
||||
where
|
||||
K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, K::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, H> IntoPyObject<'py> for &'a hashbrown::HashSet<K, H>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, <&K>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, S> FromPyObject<'_, 'py> for hashbrown::HashSet<K, S>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + Eq + hash::Hash,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(
|
||||
type_hint_subscript!(PySet::TYPE_HINT, K::INPUT_TYPE),
|
||||
type_hint_subscript!(PyFrozenSet::TYPE_HINT, K::INPUT_TYPE)
|
||||
);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
|
||||
match ob.cast::<PySet>() {
|
||||
Ok(set) => set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
if let Ok(frozen_set) = ob.cast::<PyFrozenSet>() {
|
||||
frozen_set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect()
|
||||
} else {
|
||||
Err(PyErr::from(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::IntoPyDict;
|
||||
use std::collections::hash_map::RandomState;
|
||||
|
||||
#[test]
|
||||
fn test_hashbrown_hashmap_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let mut map =
|
||||
hashbrown::HashMap::<i32, i32, RandomState>::with_hasher(RandomState::new());
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = (&map).into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
assert_eq!(map, py_map.extract().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashbrown_hashmap_into_dict() {
|
||||
Python::attach(|py| {
|
||||
let mut map =
|
||||
hashbrown::HashMap::<i32, i32, RandomState>::with_hasher(RandomState::new());
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = map.into_py_dict(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_hashbrown_hashset() {
|
||||
Python::attach(|py| {
|
||||
let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: hashbrown::HashSet<usize, RandomState> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
|
||||
let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: hashbrown::HashSet<usize, RandomState> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashbrown_hashset_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let hs: hashbrown::HashSet<u64, RandomState> =
|
||||
[1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
|
||||
let hso = hs.clone().into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(hs, hso.extract().unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
#![cfg(feature = "indexmap")]
|
||||
|
||||
//! Conversions to and from [indexmap](https://docs.rs/indexmap/)’s
|
||||
//! `IndexMap`.
|
||||
//!
|
||||
//! [`indexmap::IndexMap`] is a hash table that is closely compatible with the standard [`std::collections::HashMap`],
|
||||
//! with the difference that it preserves the insertion order when iterating over keys. It was inspired
|
||||
//! by Python's 3.6+ dict implementation.
|
||||
//!
|
||||
//! Dictionary order is guaranteed to be insertion order in Python, hence IndexMap is a good candidate
|
||||
//! for maintaining an equivalent behaviour in Rust.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # change * to the latest versions
|
||||
//! indexmap = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"indexmap\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of indexmap and PyO3.
|
||||
//! The required indexmap version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Using [indexmap](https://docs.rs/indexmap) to return a dictionary with some statistics
|
||||
//! about a list of numbers. Because of the insertion order guarantees, the Python code will
|
||||
//! always print the same result, matching users' expectations about Python's dict.
|
||||
//! ```rust
|
||||
//! use indexmap::{indexmap, IndexMap};
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! fn median(data: &Vec<i32>) -> f32 {
|
||||
//! let sorted_data = data.clone().sort();
|
||||
//! let mid = data.len() / 2;
|
||||
//! if data.len() % 2 == 0 {
|
||||
//! data[mid] as f32
|
||||
//! }
|
||||
//! else {
|
||||
//! (data[mid] + data[mid - 1]) as f32 / 2.0
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn mean(data: &Vec<i32>) -> f32 {
|
||||
//! data.iter().sum::<i32>() as f32 / data.len() as f32
|
||||
//! }
|
||||
//! fn mode(data: &Vec<i32>) -> f32 {
|
||||
//! let mut frequency = IndexMap::new(); // we can use IndexMap as any hash table
|
||||
//!
|
||||
//! for &element in data {
|
||||
//! *frequency.entry(element).or_insert(0) += 1;
|
||||
//! }
|
||||
//!
|
||||
//! frequency
|
||||
//! .iter()
|
||||
//! .max_by(|a, b| a.1.cmp(&b.1))
|
||||
//! .map(|(k, _v)| *k)
|
||||
//! .unwrap() as f32
|
||||
//! }
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn calculate_statistics(data: Vec<i32>) -> IndexMap<&'static str, f32> {
|
||||
//! indexmap! {
|
||||
//! "median" => median(&data),
|
||||
//! "mean" => mean(&data),
|
||||
//! "mode" => mode(&data),
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code:
|
||||
//! ```python
|
||||
//! from my_module import calculate_statistics
|
||||
//!
|
||||
//! data = [1, 1, 1, 3, 4, 5]
|
||||
//! print(calculate_statistics(data))
|
||||
//! # always prints {"median": 2.0, "mean": 2.5, "mode": 1.0} in the same order
|
||||
//! # if another hash table was used, the order could be random
|
||||
//! ```
|
||||
|
||||
use crate::conversion::{FromPyObjectOwned, IntoPyObject};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::types::*;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::{type_hint_subscript, PyTypeInfo};
|
||||
use crate::{Borrowed, Bound, FromPyObject, PyErr, Python};
|
||||
use core::hash;
|
||||
|
||||
impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap<K, V, H>
|
||||
where
|
||||
K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::OUTPUT_TYPE, V::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a indexmap::IndexMap<K, V, H>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
&'a V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, <&K>::OUTPUT_TYPE, <&V>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, V, S> FromPyObject<'_, 'py> for indexmap::IndexMap<K, V, S>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + Eq + hash::Hash,
|
||||
V: FromPyObjectOwned<'py>,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::INPUT_TYPE, V::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
let dict = ob.cast::<PyDict>()?;
|
||||
let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default());
|
||||
for (k, v) in dict.iter() {
|
||||
ret.insert(
|
||||
k.extract().map_err(Into::into)?,
|
||||
v.extract().map_err(Into::into)?,
|
||||
);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_indexmap {
|
||||
|
||||
use crate::types::*;
|
||||
use crate::{IntoPyObject, Python};
|
||||
|
||||
#[test]
|
||||
fn test_indexmap_indexmap_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let mut map = indexmap::IndexMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = (&map).into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
assert_eq!(
|
||||
map,
|
||||
py_map.extract::<indexmap::IndexMap::<i32, i32>>().unwrap()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexmap_indexmap_into_dict() {
|
||||
Python::attach(|py| {
|
||||
let mut map = indexmap::IndexMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = map.into_py_dict(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indexmap_indexmap_insertion_order_round_trip() {
|
||||
Python::attach(|py| {
|
||||
let n = 20;
|
||||
let mut map = indexmap::IndexMap::<i32, i32>::new();
|
||||
|
||||
for i in 1..=n {
|
||||
if i % 2 == 1 {
|
||||
map.insert(i, i);
|
||||
} else {
|
||||
map.insert(n - i, i);
|
||||
}
|
||||
}
|
||||
|
||||
let py_map = (&map).into_py_dict(py).unwrap();
|
||||
|
||||
let trip_map = py_map.extract::<indexmap::IndexMap<i32, i32>>().unwrap();
|
||||
|
||||
for (((k1, v1), (k2, v2)), (k3, v3)) in
|
||||
map.iter().zip(py_map.iter()).zip(trip_map.iter())
|
||||
{
|
||||
let k2 = k2.extract::<i32>().unwrap();
|
||||
let v2 = v2.extract::<i32>().unwrap();
|
||||
assert_eq!((k1, v1), (&k2, &v2));
|
||||
assert_eq!((k1, v1), (k3, v3));
|
||||
assert_eq!((&k2, &v2), (k3, v3));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+1318
File diff suppressed because it is too large.
Load diff
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
//! This module contains conversions between various Rust object and their representation in Python.
|
||||
|
||||
pub mod anyhow;
|
||||
pub mod bigdecimal;
|
||||
pub mod bytes;
|
||||
pub mod chrono;
|
||||
pub mod chrono_tz;
|
||||
pub mod either;
|
||||
pub mod eyre;
|
||||
pub mod hashbrown;
|
||||
pub mod indexmap;
|
||||
pub mod jiff;
|
||||
pub mod num_bigint;
|
||||
pub mod num_complex;
|
||||
pub mod num_rational;
|
||||
pub mod ordered_float;
|
||||
pub mod rust_decimal;
|
||||
pub mod serde;
|
||||
pub mod smallvec;
|
||||
pub(crate) mod std;
|
||||
pub mod time;
|
||||
pub mod uuid;
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "num-bigint")]
|
||||
//! Conversions to and from [num-bigint](https://docs.rs/num-bigint)’s [`BigInt`] and [`BigUint`] types.
|
||||
//!
|
||||
//! This is useful for converting Python integers when they may not fit in Rust's built-in integer types.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! num-bigint = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-bigint\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of num-bigint and PyO3.
|
||||
//! The required num-bigint version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! ## Examples
|
||||
//!
|
||||
//! Using [`BigInt`] to correctly increment an arbitrary precision integer.
|
||||
//! This is not possible with Rust's native integers if the Python integer is too large,
|
||||
//! in which case it will fail its conversion and raise `OverflowError`.
|
||||
//! ```rust,no_run
|
||||
//! use num_bigint::BigInt;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_one(n: BigInt) -> BigInt {
|
||||
//! n + 1
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_one, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code:
|
||||
//! ```python
|
||||
//! from my_module import add_one
|
||||
//!
|
||||
//! n = 1 << 1337
|
||||
//! value = add_one(n)
|
||||
//!
|
||||
//! assert n + 1 == value
|
||||
//! ```
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
use crate::types::{bytes::PyBytesMethods, PyBytes};
|
||||
use crate::{
|
||||
conversion::IntoPyObject, std::num::nb_index, types::PyInt, Borrowed, Bound, FromPyObject,
|
||||
PyAny, PyErr, PyResult, Python,
|
||||
};
|
||||
|
||||
use num_bigint::{BigInt, BigUint};
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::PyTypeInfo;
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use num_bigint::Sign;
|
||||
|
||||
// for identical functionality between BigInt and BigUint
|
||||
macro_rules! bigint_conversion {
|
||||
($rust_ty: ty, $is_signed: literal) => {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))]
|
||||
impl<'py> IntoPyObject<'py> for $rust_ty {
|
||||
type Target = PyInt;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&$rust_ty>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))]
|
||||
impl<'py> IntoPyObject<'py> for &$rust_ty {
|
||||
type Target = PyInt;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyInt::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
use num_traits::ToBytes;
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), Py_3_13))]
|
||||
{
|
||||
use crate::conversions::std::num::int_from_ne_bytes;
|
||||
let bytes = self.to_ne_bytes();
|
||||
Ok(int_from_ne_bytes::<{ $is_signed }>(py, &bytes))
|
||||
}
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_13)))]
|
||||
{
|
||||
use crate::conversions::std::num::int_from_le_bytes;
|
||||
let bytes = self.to_le_bytes();
|
||||
Ok(int_from_le_bytes::<{ $is_signed }>(py, &bytes))
|
||||
}
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
use $crate::py_result_ext::PyResultExt;
|
||||
use $crate::types::any::PyAnyMethods;
|
||||
let bytes = self.to_le_bytes();
|
||||
let bytes_obj = PyBytes::new(py, &bytes);
|
||||
let kwargs = if $is_signed {
|
||||
let kwargs = crate::types::PyDict::new(py);
|
||||
kwargs.set_item(crate::intern!(py, "signed"), true)?;
|
||||
Some(kwargs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
unsafe {
|
||||
py.get_type::<PyInt>()
|
||||
.call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref())
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bigint_conversion!(BigUint, false);
|
||||
bigint_conversion!(BigInt, true);
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))]
|
||||
impl<'py> FromPyObject<'_, 'py> for BigInt {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyInt::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<BigInt, Self::Error> {
|
||||
// fast path - checking for subclass of `int` just checks a bit in the type object
|
||||
let num_owned: Bound<'_, PyInt>;
|
||||
let num = if let Ok(long) = ob.cast::<PyInt>() {
|
||||
long
|
||||
} else {
|
||||
num_owned = nb_index(&ob)?;
|
||||
num_owned.as_borrowed()
|
||||
};
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
{
|
||||
let mut buffer = int_to_u32_vec::<true>(&num)?;
|
||||
let sign = if buffer.last().copied().is_some_and(|last| last >> 31 != 0) {
|
||||
// BigInt::new takes an unsigned array, so need to convert from two's complement
|
||||
// flip all bits, 'subtract' 1 (by adding one to the unsigned array)
|
||||
let mut elements = buffer.iter_mut();
|
||||
for element in elements.by_ref() {
|
||||
*element = (!*element).wrapping_add(1);
|
||||
if *element != 0 {
|
||||
// if the element didn't wrap over, no need to keep adding further ...
|
||||
break;
|
||||
}
|
||||
}
|
||||
// ... so just two's complement the rest
|
||||
for element in elements {
|
||||
*element = !*element;
|
||||
}
|
||||
Sign::Minus
|
||||
} else {
|
||||
Sign::Plus
|
||||
};
|
||||
Ok(BigInt::new(sign, buffer))
|
||||
}
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
let n_bits = int_n_bits(&num)?;
|
||||
if n_bits == 0 {
|
||||
return Ok(BigInt::from(0isize));
|
||||
}
|
||||
let bytes = int_to_py_bytes(&num, (n_bits + 8) / 8, true)?;
|
||||
Ok(BigInt::from_signed_bytes_le(bytes.as_bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))]
|
||||
impl<'py> FromPyObject<'_, 'py> for BigUint {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyInt::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<BigUint, Self::Error> {
|
||||
// fast path - checking for subclass of `int` just checks a bit in the type object
|
||||
let num_owned: Bound<'_, PyInt>;
|
||||
let num = if let Ok(long) = ob.cast::<PyInt>() {
|
||||
long
|
||||
} else {
|
||||
num_owned = nb_index(&ob)?;
|
||||
num_owned.as_borrowed()
|
||||
};
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
{
|
||||
let buffer = int_to_u32_vec::<false>(&num)?;
|
||||
Ok(BigUint::new(buffer))
|
||||
}
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
let n_bits = int_n_bits(&num)?;
|
||||
if n_bits == 0 {
|
||||
return Ok(BigUint::from(0usize));
|
||||
}
|
||||
let bytes = int_to_py_bytes(&num, n_bits.div_ceil(8), false)?;
|
||||
Ok(BigUint::from_bytes_le(bytes.as_bytes()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_LIMITED_API, Py_3_13)))]
|
||||
#[inline]
|
||||
fn int_to_u32_vec<const SIGNED: bool>(long: &Bound<'_, PyInt>) -> PyResult<Vec<u32>> {
|
||||
use crate::ffi;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let n_bits = int_n_bits(long)?;
|
||||
if n_bits == 0 {
|
||||
return Ok(buffer);
|
||||
}
|
||||
let n_digits = if SIGNED {
|
||||
(n_bits + 32) / 32
|
||||
} else {
|
||||
n_bits.div_ceil(32)
|
||||
};
|
||||
buffer.reserve_exact(n_digits);
|
||||
unsafe {
|
||||
crate::err::error_on_minusone(
|
||||
long.py(),
|
||||
ffi::_PyLong_AsByteArray(
|
||||
long.as_ptr().cast(),
|
||||
buffer.as_mut_ptr() as *mut u8,
|
||||
n_digits * 4,
|
||||
1,
|
||||
SIGNED.into(),
|
||||
),
|
||||
)?;
|
||||
buffer.set_len(n_digits)
|
||||
};
|
||||
buffer
|
||||
.iter_mut()
|
||||
.for_each(|chunk| *chunk = u32::from_le(*chunk));
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), Py_3_13))]
|
||||
#[inline]
|
||||
fn int_to_u32_vec<const SIGNED: bool>(long: &Bound<'_, PyInt>) -> PyResult<Vec<u32>> {
|
||||
use crate::ffi;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
let mut flags = ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN;
|
||||
if !SIGNED {
|
||||
flags |= ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER | ffi::Py_ASNATIVEBYTES_REJECT_NEGATIVE;
|
||||
}
|
||||
let n_bytes =
|
||||
unsafe { ffi::PyLong_AsNativeBytes(long.as_ptr().cast(), core::ptr::null_mut(), 0, flags) };
|
||||
let n_bytes_unsigned: usize = n_bytes
|
||||
.try_into()
|
||||
.map_err(|_| crate::PyErr::fetch(long.py()))?;
|
||||
if n_bytes == 0 {
|
||||
return Ok(buffer);
|
||||
}
|
||||
let n_digits = n_bytes_unsigned.div_ceil(4);
|
||||
buffer.reserve_exact(n_digits);
|
||||
unsafe {
|
||||
ffi::PyLong_AsNativeBytes(
|
||||
long.as_ptr().cast(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
(n_digits * 4).try_into().unwrap(),
|
||||
flags,
|
||||
);
|
||||
buffer.set_len(n_digits);
|
||||
};
|
||||
buffer
|
||||
.iter_mut()
|
||||
.for_each(|chunk| *chunk = u32::from_le(*chunk));
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
fn int_to_py_bytes<'py>(
|
||||
long: &Bound<'py, PyInt>,
|
||||
n_bytes: usize,
|
||||
is_signed: bool,
|
||||
) -> PyResult<Bound<'py, PyBytes>> {
|
||||
use crate::intern;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
let py = long.py();
|
||||
let kwargs = if is_signed {
|
||||
let kwargs = crate::types::PyDict::new(py);
|
||||
kwargs.set_item(intern!(py, "signed"), true)?;
|
||||
Some(kwargs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let bytes = long.call_method(
|
||||
intern!(py, "to_bytes"),
|
||||
(n_bytes, intern!(py, "little")),
|
||||
kwargs.as_ref(),
|
||||
)?;
|
||||
Ok(bytes.cast_into()?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(any(not(Py_3_13), Py_LIMITED_API))]
|
||||
fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult<usize> {
|
||||
use crate::types::PyAnyMethods;
|
||||
long.call_method0(crate::intern!(long.py(), "bit_length"))
|
||||
.and_then(|any| any.extract())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::exceptions::PyTypeError;
|
||||
use crate::test_utils::generate_unique_module_name;
|
||||
use crate::types::{PyAnyMethods as _, PyDict, PyModule};
|
||||
use pyo3_ffi::c_str;
|
||||
|
||||
fn rust_fib<T>() -> impl Iterator<Item = T>
|
||||
where
|
||||
T: From<u16>,
|
||||
for<'a> &'a T: core::ops::Add<Output = T>,
|
||||
{
|
||||
let mut f0: T = T::from(1);
|
||||
let mut f1: T = T::from(1);
|
||||
core::iter::from_fn(move || {
|
||||
let f2 = &f0 + &f1;
|
||||
Some(core::mem::replace(&mut f0, core::mem::replace(&mut f1, f2)))
|
||||
})
|
||||
}
|
||||
|
||||
fn python_fib(py: Python<'_>) -> impl Iterator<Item = Bound<'_, PyAny>> + '_ {
|
||||
let mut f0 = 1i32.into_pyobject(py).unwrap().into_any();
|
||||
let mut f1 = 1i32.into_pyobject(py).unwrap().into_any();
|
||||
core::iter::from_fn(move || {
|
||||
let f2 = f0.call_method1("__add__", (&f1,)).unwrap();
|
||||
Some(core::mem::replace(&mut f0, core::mem::replace(&mut f1, f2)))
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_biguint() {
|
||||
Python::attach(|py| {
|
||||
// check the first 2000 numbers in the fibonacci sequence
|
||||
for (py_result, rs_result) in python_fib(py).zip(rust_fib::<BigUint>()).take(2000) {
|
||||
// Python -> Rust
|
||||
assert_eq!(py_result.extract::<BigUint>().unwrap(), rs_result);
|
||||
// Rust -> Python
|
||||
assert!(py_result.eq(rs_result).unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_bigint() {
|
||||
Python::attach(|py| {
|
||||
// check the first 2000 numbers in the fibonacci sequence
|
||||
for (py_result, rs_result) in python_fib(py).zip(rust_fib::<BigInt>()).take(2000) {
|
||||
// Python -> Rust
|
||||
assert_eq!(py_result.extract::<BigInt>().unwrap(), rs_result);
|
||||
// Rust -> Python
|
||||
assert!(py_result.eq(&rs_result).unwrap());
|
||||
|
||||
// negate
|
||||
|
||||
let rs_result = rs_result * -1;
|
||||
let py_result = py_result.call_method0("__neg__").unwrap();
|
||||
|
||||
// Python -> Rust
|
||||
assert_eq!(py_result.extract::<BigInt>().unwrap(), rs_result);
|
||||
// Rust -> Python
|
||||
assert!(py_result.eq(rs_result).unwrap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn python_index_class(py: Python<'_>) -> Bound<'_, PyModule> {
|
||||
let index_code = c_str!(
|
||||
r#"
|
||||
class C:
|
||||
def __init__(self, x):
|
||||
self.x = x
|
||||
def __index__(self):
|
||||
return self.x
|
||||
"#
|
||||
);
|
||||
PyModule::from_code(
|
||||
py,
|
||||
index_code,
|
||||
c"index.py",
|
||||
&generate_unique_module_name("index"),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_index_class() {
|
||||
Python::attach(|py| {
|
||||
let index = python_index_class(py);
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("index", index).unwrap();
|
||||
let ob = py.eval(c"index.C(10)", None, Some(&locals)).unwrap();
|
||||
let _: BigInt = ob.extract().unwrap();
|
||||
let _: BigUint = ob.extract().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_zero() {
|
||||
Python::attach(|py| {
|
||||
let zero: BigInt = 0i32.into_pyobject(py).unwrap().extract().unwrap();
|
||||
assert_eq!(zero, BigInt::from(0));
|
||||
})
|
||||
}
|
||||
|
||||
/// `OverflowError` on converting Python int to BigInt, see issue #629
|
||||
#[test]
|
||||
fn check_overflow() {
|
||||
Python::attach(|py| {
|
||||
macro_rules! test {
|
||||
($T:ty, $value:expr, $py:expr) => {
|
||||
let value = $value;
|
||||
println!("{}: {}", stringify!($T), value);
|
||||
let python_value = value.clone().into_pyobject(py).unwrap();
|
||||
let roundtrip_value = python_value.extract::<$T>().unwrap();
|
||||
assert_eq!(value, roundtrip_value);
|
||||
};
|
||||
}
|
||||
|
||||
for i in 0..=256usize {
|
||||
// test a lot of values to help catch other bugs too
|
||||
test!(BigInt, BigInt::from(i), py);
|
||||
test!(BigUint, BigUint::from(i), py);
|
||||
test!(BigInt, -BigInt::from(i), py);
|
||||
test!(BigInt, BigInt::from(1) << i, py);
|
||||
test!(BigUint, BigUint::from(1u32) << i, py);
|
||||
test!(BigInt, -BigInt::from(1) << i, py);
|
||||
test!(BigInt, (BigInt::from(1) << i) + 1u32, py);
|
||||
test!(BigUint, (BigUint::from(1u32) << i) + 1u32, py);
|
||||
test!(BigInt, (-BigInt::from(1) << i) + 1u32, py);
|
||||
test!(BigInt, (BigInt::from(1) << i) - 1u32, py);
|
||||
test!(BigUint, (BigUint::from(1u32) << i) - 1u32, py);
|
||||
test!(BigInt, (-BigInt::from(1) << i) - 1u32, py);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_py_float_type_error() {
|
||||
Python::attach(|py| {
|
||||
let obj = (12.3f64).into_pyobject(py).unwrap();
|
||||
let err = obj.extract::<BigInt>().unwrap_err();
|
||||
assert!(err.is_instance_of::<PyTypeError>(py));
|
||||
|
||||
let err = obj.extract::<BigUint>().unwrap_err();
|
||||
assert!(err.is_instance_of::<PyTypeError>(py));
|
||||
});
|
||||
}
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "num-complex")]
|
||||
|
||||
//! Conversions to and from [num-complex](https://docs.rs/num-complex)’
|
||||
//! [`Complex`]`<`[`f32`]`>` and [`Complex`]`<`[`f64`]`>`.
|
||||
//!
|
||||
//! num-complex’ [`Complex`] supports more operations than PyO3's [`PyComplex`]
|
||||
//! and can be used with the rest of the Rust ecosystem.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # change * to the latest versions
|
||||
//! num-complex = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-complex\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of num-complex and PyO3.
|
||||
//! The required num-complex version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! Using [num-complex](https://docs.rs/num-complex) and [nalgebra](https://docs.rs/nalgebra)
|
||||
//! to create a pyfunction that calculates the eigenvalues of a 2x2 matrix.
|
||||
//! ```ignore
|
||||
//! # // not tested because nalgebra isn't supported on msrv
|
||||
//! # // please file an issue if it breaks!
|
||||
//! use nalgebra::base::{dimension::Const, Matrix};
|
||||
//! use num_complex::Complex;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! type T = Complex<f64>;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn get_eigenvalues(m11: T, m12: T, m21: T, m22: T) -> Vec<T> {
|
||||
//! let mat = Matrix::<T, Const<2>, Const<2>, _>::new(m11, m12, m21, m22);
|
||||
//!
|
||||
//! match mat.eigenvalues() {
|
||||
//! Some(e) => e.data.as_slice().to_vec(),
|
||||
//! None => vec![],
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(get_eigenvalues, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! # // test
|
||||
//! # use assert_approx_eq::assert_approx_eq;
|
||||
//! # use nalgebra::ComplexField;
|
||||
//! # use pyo3::types::PyComplex;
|
||||
//! #
|
||||
//! # fn main() -> PyResult<()> {
|
||||
//! # Python::attach(|py| -> PyResult<()> {
|
||||
//! # let module = PyModule::new(py, "my_module")?;
|
||||
//! #
|
||||
//! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module)?)?;
|
||||
//! #
|
||||
//! # let m11 = PyComplex::from_doubles(py, 0_f64, -1_f64);
|
||||
//! # let m12 = PyComplex::from_doubles(py, 1_f64, 0_f64);
|
||||
//! # let m21 = PyComplex::from_doubles(py, 2_f64, -1_f64);
|
||||
//! # let m22 = PyComplex::from_doubles(py, -1_f64, 0_f64);
|
||||
//! #
|
||||
//! # let result = module
|
||||
//! # .getattr("get_eigenvalues")?
|
||||
//! # .call1((m11, m12, m21, m22))?;
|
||||
//! # println!("eigenvalues: {:?}", result);
|
||||
//! #
|
||||
//! # let result = result.extract::<Vec<T>>()?;
|
||||
//! # let e0 = result[0];
|
||||
//! # let e1 = result[1];
|
||||
//! #
|
||||
//! # assert_approx_eq!(e0, Complex::new(1_f64, -1_f64));
|
||||
//! # assert_approx_eq!(e1, Complex::new(-2_f64, 0_f64));
|
||||
//! #
|
||||
//! # Ok(())
|
||||
//! # })
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code:
|
||||
//! ```python
|
||||
//! from my_module import get_eigenvalues
|
||||
//!
|
||||
//! m11 = complex(0,-1)
|
||||
//! m12 = complex(1,0)
|
||||
//! m21 = complex(2,-1)
|
||||
//! m22 = complex(-1,0)
|
||||
//!
|
||||
//! result = get_eigenvalues(m11,m12,m21,m22)
|
||||
//! assert result == [complex(1,-1), complex(-2,0)]
|
||||
//! ```
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::{
|
||||
ffi, ffi_ptr_ext::FfiPtrExt, types::PyComplex, Borrowed, Bound, FromPyObject, PyAny, PyErr,
|
||||
Python,
|
||||
};
|
||||
use core::ffi::c_double;
|
||||
use num_complex::Complex;
|
||||
|
||||
impl PyComplex {
|
||||
/// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`].
|
||||
pub fn from_complex_bound<F: Into<c_double>>(
|
||||
py: Python<'_>,
|
||||
complex: Complex<F>,
|
||||
) -> Bound<'_, PyComplex> {
|
||||
unsafe {
|
||||
ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into())
|
||||
.assume_owned(py)
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! complex_conversion {
|
||||
($float: ty) => {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))]
|
||||
impl<'py> crate::conversion::IntoPyObject<'py> for Complex<$float> {
|
||||
type Target = PyComplex;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("builtins", "complex");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
unsafe {
|
||||
Ok(
|
||||
ffi::PyComplex_FromDoubles(self.re as c_double, self.im as c_double)
|
||||
.assume_owned(py)
|
||||
.cast_into_unchecked(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))]
|
||||
impl<'py> crate::conversion::IntoPyObject<'py> for &Complex<$float> {
|
||||
type Target = PyComplex;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <Complex<$float>>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))]
|
||||
impl FromPyObject<'_, '_> for Complex<$float> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("builtins", "complex");
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Complex<$float>, Self::Error> {
|
||||
#[cfg(not(any(Py_LIMITED_API, PyPy)))]
|
||||
unsafe {
|
||||
let val = ffi::PyComplex_AsCComplex(obj.as_ptr());
|
||||
if val.real == -1.0 {
|
||||
if let Some(err) = PyErr::take(obj.py()) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(Complex::new(val.real as $float, val.imag as $float))
|
||||
}
|
||||
|
||||
#[cfg(any(Py_LIMITED_API, PyPy))]
|
||||
unsafe {
|
||||
use $crate::types::any::PyAnyMethods;
|
||||
let complex;
|
||||
let obj = if obj.is_instance_of::<PyComplex>() {
|
||||
obj
|
||||
} else if let Some(method) =
|
||||
obj.lookup_special(crate::intern!(obj.py(), "__complex__"))?
|
||||
{
|
||||
complex = method.call0()?;
|
||||
complex.as_borrowed()
|
||||
} else {
|
||||
// `obj` might still implement `__float__` or `__index__`, which will be
|
||||
// handled by `PyComplex_{Real,Imag}AsDouble`, including propagating any
|
||||
// errors if those methods don't exist / raise exceptions.
|
||||
obj
|
||||
};
|
||||
let ptr = obj.as_ptr();
|
||||
let real = ffi::PyComplex_RealAsDouble(ptr);
|
||||
if real == -1.0 {
|
||||
if let Some(err) = PyErr::take(obj.py()) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
let imag = ffi::PyComplex_ImagAsDouble(ptr);
|
||||
Ok(Complex::new(real as $float, imag as $float))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
complex_conversion!(f32);
|
||||
complex_conversion!(f64);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::generate_unique_module_name;
|
||||
use crate::types::PyAnyMethods as _;
|
||||
use crate::types::{complex::PyComplexMethods, PyModule};
|
||||
use crate::IntoPyObject;
|
||||
|
||||
#[test]
|
||||
fn from_complex() {
|
||||
Python::attach(|py| {
|
||||
let complex = Complex::new(3.0, 1.2);
|
||||
let py_c = PyComplex::from_complex_bound(py, complex);
|
||||
assert_eq!(py_c.real(), 3.0);
|
||||
assert_eq!(py_c.imag(), 1.2);
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn to_from_complex() {
|
||||
Python::attach(|py| {
|
||||
let val = Complex::new(3.0f64, 1.2);
|
||||
let obj = val.into_pyobject(py).unwrap();
|
||||
assert_eq!(obj.extract::<Complex<f64>>().unwrap(), val);
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn from_complex_err() {
|
||||
Python::attach(|py| {
|
||||
let obj = vec![1i32].into_pyobject(py).unwrap();
|
||||
assert!(obj.extract::<Complex<f64>>().is_err());
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn from_python_magic() {
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::from_code(
|
||||
py,
|
||||
cr#"
|
||||
class A:
|
||||
def __complex__(self): return 3.0+1.2j
|
||||
class B:
|
||||
def __float__(self): return 3.0
|
||||
class C:
|
||||
def __index__(self): return 3
|
||||
"#,
|
||||
c"test.py",
|
||||
&generate_unique_module_name("test"),
|
||||
)
|
||||
.unwrap();
|
||||
let from_complex = module.getattr("A").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_complex.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 1.2)
|
||||
);
|
||||
let from_float = module.getattr("B").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_float.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 0.0)
|
||||
);
|
||||
let from_index = module.getattr("C").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_index.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 0.0)
|
||||
);
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn from_python_inherited_magic() {
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::from_code(
|
||||
py,
|
||||
cr#"
|
||||
class First: pass
|
||||
class ComplexMixin:
|
||||
def __complex__(self): return 3.0+1.2j
|
||||
class FloatMixin:
|
||||
def __float__(self): return 3.0
|
||||
class IndexMixin:
|
||||
def __index__(self): return 3
|
||||
class A(First, ComplexMixin): pass
|
||||
class B(First, FloatMixin): pass
|
||||
class C(First, IndexMixin): pass
|
||||
"#,
|
||||
c"test.py",
|
||||
&generate_unique_module_name("test"),
|
||||
)
|
||||
.unwrap();
|
||||
let from_complex = module.getattr("A").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_complex.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 1.2)
|
||||
);
|
||||
let from_float = module.getattr("B").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_float.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 0.0)
|
||||
);
|
||||
let from_index = module.getattr("C").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
from_index.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 0.0)
|
||||
);
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn from_python_noncallable_descriptor_magic() {
|
||||
// Functions and lambdas implement the descriptor protocol in a way that makes
|
||||
// `type(inst).attr(inst)` equivalent to `inst.attr()` for methods, but this isn't the only
|
||||
// way the descriptor protocol might be implemented.
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::from_code(
|
||||
py,
|
||||
cr#"
|
||||
class A:
|
||||
@property
|
||||
def __complex__(self):
|
||||
return lambda: 3.0+1.2j
|
||||
"#,
|
||||
c"test.py",
|
||||
&generate_unique_module_name("test"),
|
||||
)
|
||||
.unwrap();
|
||||
let obj = module.getattr("A").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
obj.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 1.2)
|
||||
);
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn from_python_nondescriptor_magic() {
|
||||
// Magic methods don't need to implement the descriptor protocol, if they're callable.
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::from_code(
|
||||
py,
|
||||
cr#"
|
||||
class MyComplex:
|
||||
def __call__(self): return 3.0+1.2j
|
||||
class A:
|
||||
__complex__ = MyComplex()
|
||||
"#,
|
||||
c"test.py",
|
||||
&generate_unique_module_name("test"),
|
||||
)
|
||||
.unwrap();
|
||||
let obj = module.getattr("A").unwrap().call0().unwrap();
|
||||
assert_eq!(
|
||||
obj.extract::<Complex<f64>>().unwrap(),
|
||||
Complex::new(3.0, 1.2)
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "num-rational")]
|
||||
//! Conversions to and from [num-rational](https://docs.rs/num-rational) types.
|
||||
//!
|
||||
//! This is useful for converting between Python's [fractions.Fraction](https://docs.python.org/3/library/fractions.html) into and from a native Rust
|
||||
//! type.
|
||||
//!
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-rational\"] }")]
|
||||
//! num-rational = "0.4.1"
|
||||
//! ```
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create a function that adds five to a fraction:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use num_rational::Ratio;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_five_to_fraction(fraction: Ratio<i32>) -> Ratio<i32> {
|
||||
//! fraction + Ratio::new(5, 1)
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_five_to_fraction, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality:
|
||||
//! ```python
|
||||
//! from my_module import add_five_to_fraction
|
||||
//! from fractions import Fraction
|
||||
//!
|
||||
//! fraction = Fraction(2,1)
|
||||
//! fraction_plus_five = add_five_to_fraction(f)
|
||||
//! assert fraction + 5 == fraction_plus_five
|
||||
//! ```
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::ffi;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::sync::PyOnceLock;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::PyType;
|
||||
use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
|
||||
#[cfg(feature = "num-bigint")]
|
||||
use num_bigint::BigInt;
|
||||
use num_rational::Ratio;
|
||||
|
||||
static FRACTION_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
|
||||
fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
FRACTION_CLS.import(py, "fractions", "Fraction")
|
||||
}
|
||||
|
||||
macro_rules! rational_conversion {
|
||||
($int: ty) => {
|
||||
impl<'py> FromPyObject<'_, 'py> for Ratio<$int> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("fractions", "Fraction");
|
||||
|
||||
fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
let py = obj.py();
|
||||
let py_numerator_obj = obj.getattr(crate::intern!(py, "numerator"))?;
|
||||
let py_denominator_obj = obj.getattr(crate::intern!(py, "denominator"))?;
|
||||
let numerator_owned = unsafe {
|
||||
Bound::from_owned_ptr_or_err(py, ffi::PyNumber_Long(py_numerator_obj.as_ptr()))?
|
||||
};
|
||||
let denominator_owned = unsafe {
|
||||
Bound::from_owned_ptr_or_err(
|
||||
py,
|
||||
ffi::PyNumber_Long(py_denominator_obj.as_ptr()),
|
||||
)?
|
||||
};
|
||||
let rs_numerator: $int = numerator_owned.extract()?;
|
||||
let rs_denominator: $int = denominator_owned.extract()?;
|
||||
Ok(Ratio::new(rs_numerator, rs_denominator))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Ratio<$int> {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Ratio<$int>>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Ratio<$int> {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("fractions", "Fraction");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
get_fraction_cls(py)?.call1((self.numer().clone(), self.denom().clone()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
rational_conversion!(i8);
|
||||
rational_conversion!(i16);
|
||||
rational_conversion!(i32);
|
||||
rational_conversion!(isize);
|
||||
rational_conversion!(i64);
|
||||
#[cfg(feature = "num-bigint")]
|
||||
rational_conversion!(BigInt);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::dict::PyDictMethods;
|
||||
use crate::types::PyDict;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use proptest::prelude::*;
|
||||
#[test]
|
||||
fn test_negative_fraction() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import fractions\npy_frac = fractions.Fraction(-0.125)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(-1, 8);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
#[test]
|
||||
fn test_obj_with_incorrect_atts() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"not_fraction = \"contains_incorrect_atts\"",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("not_fraction").unwrap().unwrap();
|
||||
assert!(py_frac.extract::<Ratio<i32>>().is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_fraction_type() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(10, 1);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_decimal() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(11, 10);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction_with_num_den() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import fractions\npy_frac = fractions.Fraction(10,5)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_frac = locals.get_item("py_frac").unwrap().unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
let rs_frac = Ratio::new(10, 5);
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[test]
|
||||
fn test_int_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
let rs_frac = Ratio::new(1i32, 2);
|
||||
let py_frac = rs_frac.into_pyobject(py).unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
// float conversion
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[test]
|
||||
fn test_big_int_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
let rs_frac = Ratio::from_float(5.5).unwrap();
|
||||
let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
|
||||
let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_int_roundtrip(num in any::<i32>(), den in any::<i32>()) {
|
||||
Python::attach(|py| {
|
||||
let rs_frac = Ratio::new(num, den);
|
||||
let py_frac = rs_frac.into_pyobject(py).unwrap();
|
||||
let roundtripped: Ratio<i32> = py_frac.extract().unwrap();
|
||||
assert_eq!(rs_frac, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "num-bigint")]
|
||||
fn test_big_int_roundtrip(num in any::<f32>()) {
|
||||
Python::attach(|py| {
|
||||
let rs_frac = Ratio::from_float(num).unwrap();
|
||||
let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
|
||||
let roundtripped: Ratio<BigInt> = py_frac.extract().unwrap();
|
||||
assert_eq!(roundtripped, rs_frac);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infinity() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
let py_bound = py.run(
|
||||
c"import fractions\npy_frac = fractions.Fraction(\"Infinity\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
);
|
||||
assert!(py_bound.is_err());
|
||||
})
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
#![cfg(feature = "ordered-float")]
|
||||
//! Conversions to and from [ordered-float](https://docs.rs/ordered-float) types.
|
||||
//! [`NotNan`]`<`[`f32`]`>` and [`NotNan`]`<`[`f64`]`>`.
|
||||
//! [`OrderedFloat`]`<`[`f32`]`>` and [`OrderedFloat`]`<`[`f64`]`>`.
|
||||
//!
|
||||
//! This is useful for converting between Python's float into and from a native Rust type.
|
||||
//!
|
||||
//! Take care when comparing sorted collections of float types between Python and Rust.
|
||||
//! They will likely differ due to the ambiguous sort order of NaNs in Python.
|
||||
//
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"ordered-float\"] }")]
|
||||
//! ordered-float = "5.0.0"
|
||||
//! ```
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create functions that add ordered floats:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ordered_float::{NotNan, OrderedFloat};
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_not_nans(a: NotNan<f64>, b: NotNan<f64>) -> NotNan<f64> {
|
||||
//! a + b
|
||||
//! }
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_ordered_floats(a: OrderedFloat<f64>, b: OrderedFloat<f64>) -> OrderedFloat<f64> {
|
||||
//! a + b
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_not_nans, m)?)?;
|
||||
//! m.add_function(wrap_pyfunction!(add_ordered_floats, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality:
|
||||
//! ```python
|
||||
//! from my_module import add_not_nans, add_ordered_floats
|
||||
//!
|
||||
//! assert add_not_nans(1.0,2.0) == 3.0
|
||||
//! assert add_ordered_floats(1.0,2.0) == 3.0
|
||||
//! ```
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::PyValueError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::types::PyFloat;
|
||||
use crate::{Borrowed, Bound, FromPyObject, PyAny, Python};
|
||||
use core::convert::Infallible;
|
||||
use ordered_float::{NotNan, OrderedFloat};
|
||||
|
||||
macro_rules! float_conversions {
|
||||
($wrapper:ident, $float_type:ty, $constructor:expr) => {
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for $wrapper<$float_type> {
|
||||
type Error = <$float_type as FromPyObject<'a, 'py>>::Error;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = <$float_type>::INPUT_TYPE;
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
let val: $float_type = obj.extract()?;
|
||||
$constructor(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for $wrapper<$float_type> {
|
||||
type Target = PyFloat;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <$float_type>::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.into_inner().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &$wrapper<$float_type> {
|
||||
type Target = PyFloat;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <$wrapper<$float_type>>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
float_conversions!(OrderedFloat, f32, |val| Ok(OrderedFloat(val)));
|
||||
float_conversions!(OrderedFloat, f64, |val| Ok(OrderedFloat(val)));
|
||||
float_conversions!(NotNan, f32, |val| NotNan::new(val)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string())));
|
||||
float_conversions!(NotNan, f64, |val| NotNan::new(val)
|
||||
.map_err(|e| PyValueError::new_err(e.to_string())));
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ordered_float {
|
||||
use super::*;
|
||||
use crate::types::dict::IntoPyDict;
|
||||
use crate::types::PyAnyMethods;
|
||||
use alloc::ffi::CString;
|
||||
use core::ffi::CStr;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn py_run<'py>(py: Python<'py>, script: &CStr, locals: impl IntoPyDict<'py>) {
|
||||
py.run(script, None, Some(&locals.into_py_dict(py).unwrap()))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
macro_rules! float_roundtrip_tests {
|
||||
($wrapper:ident, $float_type:ty, $constructor:expr, $standard_test:ident, $wasm_test:ident, $infinity_test:ident, $zero_test:ident) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn $standard_test(inner_f: $float_type) {
|
||||
let f = $constructor(inner_f);
|
||||
|
||||
Python::attach(|py| {
|
||||
let f_py: Bound<'_, PyFloat> = f.into_pyobject(py).unwrap();
|
||||
|
||||
py_run(py, &CString::new(format!(
|
||||
"import math\nassert math.isclose(f_py, {})",
|
||||
inner_f as f64 // Always interpret the literal rs float value as f64
|
||||
// so that it's comparable with the python float
|
||||
)).unwrap(), [("f_py", &f_py)]);
|
||||
|
||||
let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap();
|
||||
|
||||
assert_eq!(f, roundtripped_f);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[test]
|
||||
fn $wasm_test() {
|
||||
let inner_f = 10.0;
|
||||
let f = $constructor(inner_f);
|
||||
|
||||
Python::attach(|py| {
|
||||
let f_py: Bound<'_, PyFloat> = f.into_pyobject(py).unwrap();
|
||||
|
||||
py_run(
|
||||
py,
|
||||
&CString::new(format!(
|
||||
"import math\nassert math.isclose(f_py, {})",
|
||||
inner_f as f64 // Always interpret the literal rs float value as f64
|
||||
// so that it's comparable with the python float
|
||||
))
|
||||
.unwrap(),
|
||||
[("f_py", &f_py)],
|
||||
);
|
||||
|
||||
let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap();
|
||||
|
||||
assert_eq!(f, roundtripped_f);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn $infinity_test() {
|
||||
let inner_pinf = <$float_type>::INFINITY;
|
||||
let pinf = $constructor(inner_pinf);
|
||||
|
||||
let inner_ninf = <$float_type>::NEG_INFINITY;
|
||||
let ninf = $constructor(inner_ninf);
|
||||
|
||||
Python::attach(|py| {
|
||||
let pinf_py: Bound<'_, PyFloat> = pinf.into_pyobject(py).unwrap();
|
||||
let ninf_py: Bound<'_, PyFloat> = ninf.into_pyobject(py).unwrap();
|
||||
|
||||
py_run(
|
||||
py,
|
||||
c"\
|
||||
assert pinf_py == float('inf')\n\
|
||||
assert ninf_py == float('-inf')",
|
||||
[("pinf_py", &pinf_py), ("ninf_py", &ninf_py)],
|
||||
);
|
||||
|
||||
let roundtripped_pinf: $wrapper<$float_type> = pinf_py.extract().unwrap();
|
||||
let roundtripped_ninf: $wrapper<$float_type> = ninf_py.extract().unwrap();
|
||||
|
||||
assert_eq!(pinf, roundtripped_pinf);
|
||||
assert_eq!(ninf, roundtripped_ninf);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn $zero_test() {
|
||||
let inner_pzero: $float_type = 0.0;
|
||||
let pzero = $constructor(inner_pzero);
|
||||
|
||||
let inner_nzero: $float_type = -0.0;
|
||||
let nzero = $constructor(inner_nzero);
|
||||
|
||||
Python::attach(|py| {
|
||||
let pzero_py: Bound<'_, PyFloat> = pzero.into_pyobject(py).unwrap();
|
||||
let nzero_py: Bound<'_, PyFloat> = nzero.into_pyobject(py).unwrap();
|
||||
|
||||
// This python script verifies that the values are 0.0 in magnitude
|
||||
// and that the signs are correct(+0.0 vs -0.0)
|
||||
py_run(
|
||||
py,
|
||||
c"\
|
||||
import math\n\
|
||||
assert pzero_py == 0.0\n\
|
||||
assert math.copysign(1.0, pzero_py) > 0.0\n\
|
||||
assert nzero_py == 0.0\n\
|
||||
assert math.copysign(1.0, nzero_py) < 0.0",
|
||||
[("pzero_py", &pzero_py), ("nzero_py", &nzero_py)],
|
||||
);
|
||||
|
||||
let roundtripped_pzero: $wrapper<$float_type> = pzero_py.extract().unwrap();
|
||||
let roundtripped_nzero: $wrapper<$float_type> = nzero_py.extract().unwrap();
|
||||
|
||||
assert_eq!(pzero, roundtripped_pzero);
|
||||
assert_eq!(roundtripped_pzero.signum(), 1.0);
|
||||
assert_eq!(nzero, roundtripped_nzero);
|
||||
assert_eq!(roundtripped_nzero.signum(), -1.0);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
float_roundtrip_tests!(
|
||||
OrderedFloat,
|
||||
f32,
|
||||
OrderedFloat,
|
||||
ordered_float_f32_standard,
|
||||
ordered_float_f32_wasm,
|
||||
ordered_float_f32_infinity,
|
||||
ordered_float_f32_zero
|
||||
);
|
||||
float_roundtrip_tests!(
|
||||
OrderedFloat,
|
||||
f64,
|
||||
OrderedFloat,
|
||||
ordered_float_f64_standard,
|
||||
ordered_float_f64_wasm,
|
||||
ordered_float_f64_infinity,
|
||||
ordered_float_f64_zero
|
||||
);
|
||||
float_roundtrip_tests!(
|
||||
NotNan,
|
||||
f32,
|
||||
|val| NotNan::new(val).unwrap(),
|
||||
not_nan_f32_standard,
|
||||
not_nan_f32_wasm,
|
||||
not_nan_f32_infinity,
|
||||
not_nan_f32_zero
|
||||
);
|
||||
float_roundtrip_tests!(
|
||||
NotNan,
|
||||
f64,
|
||||
|val| NotNan::new(val).unwrap(),
|
||||
not_nan_f64_standard,
|
||||
not_nan_f64_wasm,
|
||||
not_nan_f64_infinity,
|
||||
not_nan_f64_zero
|
||||
);
|
||||
|
||||
macro_rules! ordered_float_pynan_tests {
|
||||
($test_name:ident, $float_type:ty) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
let inner_nan: $float_type = <$float_type>::NAN;
|
||||
let nan = OrderedFloat(inner_nan);
|
||||
|
||||
Python::attach(|py| {
|
||||
let nan_py: Bound<'_, PyFloat> = nan.into_pyobject(py).unwrap();
|
||||
|
||||
py_run(
|
||||
py,
|
||||
c"import math\nassert math.isnan(nan_py)",
|
||||
[("nan_py", &nan_py)],
|
||||
);
|
||||
|
||||
let roundtripped_nan: OrderedFloat<$float_type> = nan_py.extract().unwrap();
|
||||
|
||||
assert_eq!(nan, roundtripped_nan);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
ordered_float_pynan_tests!(test_ordered_float_pynan_f32, f32);
|
||||
ordered_float_pynan_tests!(test_ordered_float_pynan_f64, f64);
|
||||
|
||||
macro_rules! not_nan_pynan_tests {
|
||||
($test_name:ident, $float_type:ty) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
Python::attach(|py| {
|
||||
let nan_py = py.eval(c"float('nan')", None, None).unwrap();
|
||||
|
||||
let nan_rs: Result<NotNan<$float_type>, _> = nan_py.extract();
|
||||
|
||||
assert!(nan_rs.is_err());
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
not_nan_pynan_tests!(test_not_nan_pynan_f32, f32);
|
||||
not_nan_pynan_tests!(test_not_nan_pynan_f64, f64);
|
||||
|
||||
macro_rules! py64_rs32 {
|
||||
($test_name:ident, $wrapper:ident, $float_type:ty) => {
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
Python::attach(|py| {
|
||||
let py_64 = py
|
||||
.import("sys")
|
||||
.unwrap()
|
||||
.getattr("float_info")
|
||||
.unwrap()
|
||||
.getattr("max")
|
||||
.unwrap();
|
||||
let rs_32 = py_64.extract::<$wrapper<f32>>().unwrap();
|
||||
// The python f64 is not representable in a rust f32
|
||||
assert!(rs_32.is_infinite());
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
py64_rs32!(ordered_float_f32, OrderedFloat, f32);
|
||||
py64_rs32!(ordered_float_f64, OrderedFloat, f64);
|
||||
py64_rs32!(not_nan_f32, NotNan, f32);
|
||||
py64_rs32!(not_nan_f64, NotNan, f64);
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
#![cfg(feature = "rust_decimal")]
|
||||
//! Conversions to and from [rust_decimal](https://docs.rs/rust_decimal)'s [`Decimal`] type.
|
||||
//!
|
||||
//! This is useful for converting Python's decimal.Decimal into and from a native Rust type.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"rust_decimal\"] }")]
|
||||
//! rust_decimal = "1.0"
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use a compatible version of rust_decimal and PyO3.
|
||||
//! The required rust_decimal version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create a function that adds one to a Decimal
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use rust_decimal::Decimal;
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyfunction]
|
||||
//! fn add_one(d: Decimal) -> Decimal {
|
||||
//! d + Decimal::ONE
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(add_one, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality
|
||||
//!
|
||||
//!
|
||||
//! ```python
|
||||
//! from my_module import add_one
|
||||
//! from decimal import Decimal
|
||||
//!
|
||||
//! d = Decimal("2")
|
||||
//! value = add_one(d)
|
||||
//!
|
||||
//! assert d + 1 == value
|
||||
//! ```
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::PyValueError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::sync::PyOnceLock;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::string::PyStringMethods;
|
||||
use crate::types::PyType;
|
||||
use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
|
||||
use core::str::FromStr;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
impl FromPyObject<'_, '_> for Decimal {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
// use the string representation to not be lossy
|
||||
if let Ok(val) = obj.extract() {
|
||||
Ok(Decimal::new(val, 0))
|
||||
} else {
|
||||
let py_str = &obj.str()?;
|
||||
let rs_str = &py_str.to_cow()?;
|
||||
Decimal::from_str(rs_str).or_else(|_| {
|
||||
Decimal::from_scientific(rs_str).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static DECIMAL_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
|
||||
fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
DECIMAL_CLS.import(py, "decimal", "Decimal")
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Decimal {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("decimal", "Decimal");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dec_cls = get_decimal_cls(py)?;
|
||||
// now call the constructor with the Rust Decimal string-ified
|
||||
// to not be lossy
|
||||
dec_cls.call1((self.to_string(),))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Decimal {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Decimal::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_rust_decimal {
|
||||
use super::*;
|
||||
use crate::types::dict::PyDictMethods;
|
||||
use crate::types::PyDict;
|
||||
use alloc::ffi::CString;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use proptest::prelude::*;
|
||||
|
||||
macro_rules! convert_constants {
|
||||
($name:ident, $rs:expr, $py:literal) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
Python::attach(|py| {
|
||||
let rs_orig = $rs;
|
||||
let rs_dec = rs_orig.into_pyobject(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("rs_dec", &rs_dec).unwrap();
|
||||
// Checks if Rust Decimal -> Python Decimal conversion is correct
|
||||
py.run(
|
||||
&CString::new(format!(
|
||||
"import decimal\npy_dec = decimal.Decimal({})\nassert py_dec == rs_dec",
|
||||
$py
|
||||
))
|
||||
.unwrap(),
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
// Checks if Python Decimal -> Rust Decimal conversion is correct
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let py_result: Decimal = py_dec.extract().unwrap();
|
||||
assert_eq!(rs_orig, py_result);
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
convert_constants!(convert_zero, Decimal::ZERO, "0");
|
||||
convert_constants!(convert_one, Decimal::ONE, "1");
|
||||
convert_constants!(convert_neg_one, Decimal::NEGATIVE_ONE, "-1");
|
||||
convert_constants!(convert_two, Decimal::TWO, "2");
|
||||
convert_constants!(convert_ten, Decimal::TEN, "10");
|
||||
convert_constants!(convert_one_hundred, Decimal::ONE_HUNDRED, "100");
|
||||
convert_constants!(convert_one_thousand, Decimal::ONE_THOUSAND, "1000");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_roundtrip(
|
||||
lo in any::<u32>(),
|
||||
mid in any::<u32>(),
|
||||
high in any::<u32>(),
|
||||
negative in any::<bool>(),
|
||||
scale in 0..28u32
|
||||
) {
|
||||
let num = Decimal::from_parts(lo, mid, high, negative, scale);
|
||||
Python::attach(|py| {
|
||||
let rs_dec = num.into_pyobject(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("rs_dec", &rs_dec).unwrap();
|
||||
py.run(
|
||||
&CString::new(format!(
|
||||
"import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(),
|
||||
None, Some(&locals)).unwrap();
|
||||
let roundtripped: Decimal = rs_dec.extract().unwrap();
|
||||
assert_eq!(num, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integers(num in any::<i64>()) {
|
||||
Python::attach(|py| {
|
||||
let py_num = num.into_pyobject(py).unwrap();
|
||||
let roundtripped: Decimal = py_num.extract().unwrap();
|
||||
let rs_dec = Decimal::new(num, 0);
|
||||
assert_eq!(rs_dec, roundtripped);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nan() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import decimal\npy_dec = decimal.Decimal(\"NaN\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let roundtripped: Result<Decimal, PyErr> = py_dec.extract();
|
||||
assert!(roundtripped.is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific_notation() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import decimal\npy_dec = decimal.Decimal(\"1e3\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let roundtripped: Decimal = py_dec.extract().unwrap();
|
||||
let rs_dec = Decimal::from_scientific("1e3").unwrap();
|
||||
assert_eq!(rs_dec, roundtripped);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_infinity() {
|
||||
Python::attach(|py| {
|
||||
let locals = PyDict::new(py);
|
||||
py.run(
|
||||
c"import decimal\npy_dec = decimal.Decimal(\"Infinity\")",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
let py_dec = locals.get_item("py_dec").unwrap().unwrap();
|
||||
let roundtripped: Result<Decimal, PyErr> = py_dec.extract();
|
||||
assert!(roundtripped.is_err());
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
#![cfg(feature = "serde")]
|
||||
|
||||
//! Enables (de)serialization of [`Py`]`<T>` objects via [serde](https://docs.rs/serde).
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"serde\"] }")]
|
||||
//! serde = "1.0"
|
||||
//! ```
|
||||
|
||||
use crate::{Py, PyAny, PyClass, Python};
|
||||
use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
impl<T> Serialize for Py<T>
|
||||
where
|
||||
T: Serialize + PyClass,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
Python::attach(|py| {
|
||||
self.try_borrow(py)
|
||||
.map_err(|e| ser::Error::custom(e.to_string()))?
|
||||
.serialize(serializer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Deserialize<'de> for Py<T>
|
||||
where
|
||||
T: PyClass<BaseType = PyAny> + Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Py<T>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let deserialized = T::deserialize(deserializer)?;
|
||||
|
||||
Python::attach(|py| Py::new(py, deserialized).map_err(|e| de::Error::custom(e.to_string())))
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "smallvec")]
|
||||
|
||||
//! Conversions to and from [smallvec](https://docs.rs/smallvec/).
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add this to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! # change * to the latest versions
|
||||
//! smallvec = "*"
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"smallvec\"] }")]
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use compatible versions of smallvec and PyO3.
|
||||
//! The required smallvec version may vary based on the version of PyO3.
|
||||
use crate::conversion::{FromPyObjectOwned, IntoPyObject};
|
||||
use crate::exceptions::PyTypeError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_subscript;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::{PySequence, PyString};
|
||||
use crate::{
|
||||
err::CastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, PyTypeInfo, Python,
|
||||
};
|
||||
use smallvec::{Array, SmallVec};
|
||||
|
||||
impl<'py, A> IntoPyObject<'py> for SmallVec<A>
|
||||
where
|
||||
A: Array,
|
||||
A::Item: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = A::Item::SEQUENCE_OUTPUT_TYPE;
|
||||
|
||||
/// Turns [`SmallVec<u8>`] into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
|
||||
///
|
||||
/// [`PyBytes`]: crate::types::PyBytes
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
<A::Item>::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, A> IntoPyObject<'py> for &'a SmallVec<A>
|
||||
where
|
||||
A: Array,
|
||||
&'a A::Item: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&[A::Item]>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.as_slice().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, A> FromPyObject<'_, 'py> for SmallVec<A>
|
||||
where
|
||||
A: Array,
|
||||
A::Item: FromPyObjectOwned<'py>,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PySequence::TYPE_HINT, A::Item::INPUT_TYPE);
|
||||
|
||||
fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
if obj.is_instance_of::<PyString>() {
|
||||
return Err(PyTypeError::new_err("Can't extract `str` to `SmallVec`"));
|
||||
}
|
||||
extract_sequence(obj)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_sequence<'py, A>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<SmallVec<A>>
|
||||
where
|
||||
A: Array,
|
||||
A::Item: FromPyObjectOwned<'py>,
|
||||
{
|
||||
// Types that pass `PySequence_Check` usually implement enough of the sequence protocol
|
||||
// to support this function and if not, we will only fail extraction safely.
|
||||
if unsafe { ffi::PySequence_Check(obj.as_ptr()) } == 0 {
|
||||
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
|
||||
}
|
||||
|
||||
let mut sv = SmallVec::with_capacity(obj.len().unwrap_or(0));
|
||||
for item in obj.try_iter()? {
|
||||
sv.push(item?.extract::<A::Item>().map_err(Into::into)?);
|
||||
}
|
||||
Ok(sv)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{PyBytes, PyBytesMethods, PyDict, PyList};
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_from_py_object() {
|
||||
Python::attach(|py| {
|
||||
let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let sv: SmallVec<[u64; 8]> = l.extract().unwrap();
|
||||
assert_eq!(sv.as_slice(), [1, 2, 3, 4, 5]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_from_py_object_fails() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
let sv: PyResult<SmallVec<[u64; 8]>> = dict.extract();
|
||||
assert_eq!(
|
||||
sv.unwrap_err().to_string(),
|
||||
"TypeError: 'dict' object is not an instance of 'Sequence'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
let hso = sv.into_pyobject(py).unwrap();
|
||||
let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
assert!(l.eq(hso).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smallvec_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes: SmallVec<[u8; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
let obj = bytes.clone().into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &*bytes);
|
||||
|
||||
let nums: SmallVec<[u16; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
let obj = nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::conversion::{FromPyObjectOwned, FromPyObjectSequence, IntoPyObject};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_subscript, PyStaticExpr};
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::PySequence;
|
||||
use crate::{err::CastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python};
|
||||
use crate::{exceptions, Borrowed, Bound, PyErr};
|
||||
|
||||
impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N]
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::SEQUENCE_OUTPUT_TYPE;
|
||||
|
||||
/// Turns [`[u8; N]`](core::array) into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
|
||||
///
|
||||
/// [`PyBytes`]: crate::types::PyBytes
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T, const N: usize> IntoPyObject<'py> for &'a [T; N]
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&[T]>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.as_slice().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T, const N: usize> FromPyObject<'_, 'py> for [T; N]
|
||||
where
|
||||
T: FromPyObjectOwned<'py>,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySequence::TYPE_HINT, T::INPUT_TYPE);
|
||||
|
||||
fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
|
||||
if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) {
|
||||
return extractor.to_array();
|
||||
}
|
||||
|
||||
create_array_from_obj(obj)
|
||||
}
|
||||
}
|
||||
|
||||
fn create_array_from_obj<'py, T, const N: usize>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<[T; N]>
|
||||
where
|
||||
T: FromPyObjectOwned<'py>,
|
||||
{
|
||||
// Types that pass `PySequence_Check` usually implement enough of the sequence protocol
|
||||
// to support this function and if not, we will only fail extraction safely.
|
||||
if unsafe { ffi::PySequence_Check(obj.as_ptr()) } == 0 {
|
||||
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
|
||||
}
|
||||
|
||||
let seq_len = obj.len()?;
|
||||
if seq_len != N {
|
||||
return Err(invalid_sequence_length(N, seq_len));
|
||||
}
|
||||
array_try_from_fn(|idx| {
|
||||
obj.get_item(idx)
|
||||
.and_then(|any| any.extract().map_err(Into::into))
|
||||
})
|
||||
}
|
||||
|
||||
// TODO use core::array::try_from_fn, if that stabilises:
|
||||
// (https://github.com/rust-lang/rust/issues/89379)
|
||||
fn array_try_from_fn<E, F, T, const N: usize>(mut cb: F) -> Result<[T; N], E>
|
||||
where
|
||||
F: FnMut(usize) -> Result<T, E>,
|
||||
{
|
||||
// Helper to safely create arrays since the standard library doesn't
|
||||
// provide one yet. Shouldn't be necessary in the future.
|
||||
struct ArrayGuard<T, const N: usize> {
|
||||
dst: *mut T,
|
||||
initialized: usize,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Drop for ArrayGuard<T, N> {
|
||||
fn drop(&mut self) {
|
||||
debug_assert!(self.initialized <= N);
|
||||
let initialized_part = core::ptr::slice_from_raw_parts_mut(self.dst, self.initialized);
|
||||
unsafe {
|
||||
core::ptr::drop_in_place(initialized_part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [MaybeUninit<T>; N] would be "nicer" but is actually difficult to create - there are nightly
|
||||
// APIs which would make this easier.
|
||||
let mut array: core::mem::MaybeUninit<[T; N]> = core::mem::MaybeUninit::uninit();
|
||||
let mut guard: ArrayGuard<T, N> = ArrayGuard {
|
||||
dst: array.as_mut_ptr() as _,
|
||||
initialized: 0,
|
||||
};
|
||||
unsafe {
|
||||
let mut value_ptr = array.as_mut_ptr() as *mut T;
|
||||
for i in 0..N {
|
||||
core::ptr::write(value_ptr, cb(i)?);
|
||||
value_ptr = value_ptr.offset(1);
|
||||
guard.initialized += 1;
|
||||
}
|
||||
core::mem::forget(guard);
|
||||
Ok(array.assume_init())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr {
|
||||
exceptions::PyValueError::new_err(format!(
|
||||
"expected a sequence of length {expected} (got {actual})"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(panic = "unwind")]
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
#[cfg(panic = "unwind")]
|
||||
use std::panic;
|
||||
|
||||
use crate::{
|
||||
conversion::IntoPyObject,
|
||||
types::{any::PyAnyMethods, PyBytes, PyBytesMethods},
|
||||
};
|
||||
use crate::{types::PyList, PyResult, Python};
|
||||
|
||||
#[test]
|
||||
#[cfg(panic = "unwind")]
|
||||
fn array_try_from_fn() {
|
||||
static DROP_COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
struct CountDrop;
|
||||
impl Drop for CountDrop {
|
||||
fn drop(&mut self) {
|
||||
DROP_COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
let _ = catch_unwind_silent(move || {
|
||||
let _: Result<[CountDrop; 4], ()> = super::array_try_from_fn(|idx| {
|
||||
#[expect(clippy::manual_assert, reason = "testing panic during array creation")]
|
||||
if idx == 2 {
|
||||
panic!("peek a boo");
|
||||
}
|
||||
Ok(CountDrop)
|
||||
});
|
||||
});
|
||||
assert_eq!(DROP_COUNTER.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytes_to_array() {
|
||||
Python::attach(|py| {
|
||||
let v: [u8; 33] = py
|
||||
.eval(c"b'abcabcabcabcabcabcabcabcabcabcabc'", None, None)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytes_wrong_length() {
|
||||
Python::attach(|py| {
|
||||
let v: PyResult<[u8; 3]> = py.eval(c"b'abcdefg'", None, None).unwrap().extract();
|
||||
assert_eq!(
|
||||
v.unwrap_err().to_string(),
|
||||
"ValueError: expected a sequence of length 3 (got 7)"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytearray_to_array() {
|
||||
Python::attach(|py| {
|
||||
let v: [u8; 33] = py
|
||||
.eval(
|
||||
c"bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_small_bytearray_to_array() {
|
||||
Python::attach(|py| {
|
||||
let v: [u8; 3] = py
|
||||
.eval(c"bytearray(b'abc')", None, None)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(&v, b"abc");
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn test_into_pyobject_array_conversion() {
|
||||
Python::attach(|py| {
|
||||
let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
|
||||
let pyobject = array.into_pyobject(py).unwrap();
|
||||
let pylist = pyobject.cast::<PyList>().unwrap();
|
||||
assert_eq!(pylist.get_item(0).unwrap().extract::<f32>().unwrap(), 0.0);
|
||||
assert_eq!(pylist.get_item(1).unwrap().extract::<f32>().unwrap(), -16.0);
|
||||
assert_eq!(pylist.get_item(2).unwrap().extract::<f32>().unwrap(), 16.0);
|
||||
assert_eq!(pylist.get_item(3).unwrap().extract::<f32>().unwrap(), 42.0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_invalid_sequence_length() {
|
||||
Python::attach(|py| {
|
||||
let v: PyResult<[u8; 3]> = py
|
||||
.eval(c"bytearray(b'abcdefg')", None, None)
|
||||
.unwrap()
|
||||
.extract();
|
||||
assert_eq!(
|
||||
v.unwrap_err().to_string(),
|
||||
"ValueError: expected a sequence of length 3 (got 7)"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intopyobject_array_conversion() {
|
||||
Python::attach(|py| {
|
||||
let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
|
||||
let pylist = array
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.cast_into::<PyList>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(pylist.get_item(0).unwrap().extract::<f32>().unwrap(), 0.0);
|
||||
assert_eq!(pylist.get_item(1).unwrap().extract::<f32>().unwrap(), -16.0);
|
||||
assert_eq!(pylist.get_item(2).unwrap().extract::<f32>().unwrap(), 16.0);
|
||||
assert_eq!(pylist.get_item(3).unwrap().extract::<f32>().unwrap(), 42.0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_array_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes: [u8; 6] = *b"foobar";
|
||||
let obj = bytes.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &bytes);
|
||||
|
||||
let nums: [u16; 4] = [0, 1, 2, 3];
|
||||
let obj = nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_non_iterable_to_array() {
|
||||
Python::attach(|py| {
|
||||
let v = py.eval(c"42", None, None).unwrap();
|
||||
v.extract::<i32>().unwrap();
|
||||
v.extract::<[i32; 1]>().unwrap_err();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
#[test]
|
||||
fn test_pyclass_intopy_array_conversion() {
|
||||
#[crate::pyclass(crate = "crate")]
|
||||
struct Foo;
|
||||
|
||||
Python::attach(|py| {
|
||||
let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo];
|
||||
let list = array
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.cast_into::<PyList>()
|
||||
.unwrap();
|
||||
let _bound = list.get_item(4).unwrap().cast::<Foo>().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/59211505
|
||||
#[cfg(panic = "unwind")]
|
||||
fn catch_unwind_silent<F, R>(f: F) -> std::thread::Result<R>
|
||||
where
|
||||
F: FnOnce() -> R + panic::UnwindSafe,
|
||||
{
|
||||
let prev_hook = panic::take_hook();
|
||||
panic::set_hook(Box::new(|_| {}));
|
||||
let result = panic::catch_unwind(f);
|
||||
panic::set_hook(prev_hook);
|
||||
result
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
use core::cell::Cell;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::{conversion::IntoPyObject, Borrowed, FromPyObject, PyAny, Python};
|
||||
|
||||
impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell<T> {
|
||||
type Target = T::Target;
|
||||
type Output = T::Output;
|
||||
type Error = T::Error;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.get().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell<T> {
|
||||
type Target = T::Target;
|
||||
type Output = T::Output;
|
||||
type Error = T::Error;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.get().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: FromPyObject<'a, 'py>> FromPyObject<'a, 'py> for Cell<T> {
|
||||
type Error = T::Error;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = T::INPUT_TYPE;
|
||||
|
||||
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
ob.extract().map(Cell::new)
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
use crate::exceptions::PyUnicodeDecodeError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::types::PyString;
|
||||
use crate::{Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, Python};
|
||||
use alloc::borrow::Cow;
|
||||
use core::ffi::CStr;
|
||||
use std::ffi::CString;
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
use {
|
||||
crate::{exceptions::PyValueError, ffi},
|
||||
core::slice,
|
||||
};
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &CStr {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&str>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.to_str()
|
||||
.map_err(|e| PyUnicodeDecodeError::new_err_from_utf8(py, self.to_bytes(), e))?
|
||||
.into_pyobject(py)
|
||||
.map_err(|err| match err {})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for CString {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&CStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &CString {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&CStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Cow<'_, CStr> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&CStr>::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Cow<'_, CStr> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&CStr>::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
impl<'a> FromPyObject<'a, '_> for &'a CStr {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let obj = obj.cast::<PyString>()?;
|
||||
let mut size = 0;
|
||||
// SAFETY: obj is a PyString so we can safely call PyUnicode_AsUTF8AndSize
|
||||
let ptr = unsafe { ffi::PyUnicode_AsUTF8AndSize(obj.as_ptr(), &mut size) };
|
||||
|
||||
if ptr.is_null() {
|
||||
return Err(PyErr::fetch(obj.py()));
|
||||
}
|
||||
|
||||
// SAFETY: PyUnicode_AsUTF8AndSize always returns a NUL-terminated string but size does not
|
||||
// include the NUL terminator. So we add 1 to the size to include it.
|
||||
let slice = unsafe { slice::from_raw_parts(ptr.cast(), size as usize + 1) };
|
||||
|
||||
CStr::from_bytes_with_nul(slice).map_err(|err| PyValueError::new_err(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromPyObject<'a, '_> for Cow<'a, CStr> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
Ok(Cow::Borrowed(obj.extract::<&CStr>()?))
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
{
|
||||
Ok(Cow::Owned(obj.extract::<CString>()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromPyObject<'_, '_> for CString {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
Ok(obj.extract::<&CStr>()?.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
{
|
||||
CString::new(&*obj.cast::<PyString>()?.to_cow()?).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::string::PyStringMethods;
|
||||
use crate::types::PyAnyMethods;
|
||||
use crate::Python;
|
||||
|
||||
#[test]
|
||||
fn test_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello, Python!";
|
||||
let cstr = CString::new(s).unwrap();
|
||||
|
||||
let py_string = cstr.as_c_str().into_pyobject(py).unwrap();
|
||||
assert_eq!(py_string.to_cow().unwrap(), s);
|
||||
|
||||
let py_string = cstr.into_pyobject(py).unwrap();
|
||||
assert_eq!(py_string.to_cow().unwrap(), s);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_with_nul_error() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello\0Python";
|
||||
let py_string = s.into_pyobject(py).unwrap();
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
let err = py_string.extract::<&CStr>();
|
||||
assert!(err.is_err());
|
||||
}
|
||||
|
||||
let err = py_string.extract::<CString>();
|
||||
assert!(err.is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_cstr_and_cstring() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello, world!";
|
||||
let cstr = CString::new(s).unwrap();
|
||||
let py_string = cstr.as_c_str().into_pyobject(py).unwrap();
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
let extracted_cstr: &CStr = py_string.extract().unwrap();
|
||||
assert_eq!(extracted_cstr.to_str().unwrap(), s);
|
||||
}
|
||||
|
||||
let extracted_cstring: CString = py_string.extract().unwrap();
|
||||
assert_eq!(extracted_cstring.to_str().unwrap(), s);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cow_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello, world!";
|
||||
let cstr = CString::new(s).unwrap();
|
||||
let cow: Cow<'_, CStr> = Cow::Borrowed(cstr.as_c_str());
|
||||
|
||||
let py_string = cow.into_pyobject(py).unwrap();
|
||||
assert_eq!(py_string.to_cow().unwrap(), s);
|
||||
|
||||
let roundtripped: Cow<'_, CStr> = py_string.extract().unwrap();
|
||||
assert_eq!(roundtripped.as_ref(), cstr.as_c_str());
|
||||
})
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::PyValueError;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_identifier, type_hint_union, PyStaticExpr};
|
||||
use crate::sync::PyOnceLock;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::string::PyStringMethods;
|
||||
use crate::types::PyType;
|
||||
use crate::{intern, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python};
|
||||
use core::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
impl FromPyObject<'_, '_> for IpAddr {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(
|
||||
type_hint_identifier!("ipaddress", "IPv4Address"),
|
||||
type_hint_identifier!("ipaddress", "IPv6Address")
|
||||
);
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
match obj.getattr(intern!(obj.py(), "packed")) {
|
||||
Ok(packed) => {
|
||||
if let Ok(packed) = packed.extract::<[u8; 4]>() {
|
||||
Ok(IpAddr::V4(Ipv4Addr::from(packed)))
|
||||
} else if let Ok(packed) = packed.extract::<[u8; 16]>() {
|
||||
Ok(IpAddr::V6(Ipv6Addr::from(packed)))
|
||||
} else {
|
||||
Err(PyValueError::new_err("invalid packed length"))
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// We don't have a .packed attribute, so we try to construct an IP from str().
|
||||
obj.str()?.to_cow()?.parse().map_err(PyValueError::new_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Ipv4Addr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("ipaddress", "IPv4Address");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
static IPV4_ADDRESS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
IPV4_ADDRESS
|
||||
.import(py, "ipaddress", "IPv4Address")?
|
||||
.call1((u32::from_be_bytes(self.octets()),))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Ipv4Addr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Ipv4Addr::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Ipv6Addr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("ipaddress", "IPv6Address");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
static IPV6_ADDRESS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
IPV6_ADDRESS
|
||||
.import(py, "ipaddress", "IPv6Address")?
|
||||
.call1((u128::from_be_bytes(self.octets()),))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Ipv6Addr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Ipv6Addr::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for IpAddr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_union!(&Ipv4Addr::OUTPUT_TYPE, &Ipv6Addr::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self {
|
||||
IpAddr::V4(ip) => ip.into_pyobject(py),
|
||||
IpAddr::V6(ip) => ip.into_pyobject(py),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &IpAddr {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = IpAddr::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ipaddr {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::types::PyString;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
fn roundtrip(py: Python<'_>, ip: &str) {
|
||||
let ip = IpAddr::from_str(ip).unwrap();
|
||||
let py_cls = if ip.is_ipv4() {
|
||||
"IPv4Address"
|
||||
} else {
|
||||
"IPv6Address"
|
||||
};
|
||||
|
||||
let pyobj = ip.into_pyobject(py).unwrap();
|
||||
let repr = pyobj.repr().unwrap();
|
||||
let repr = repr.to_string_lossy();
|
||||
assert_eq!(repr, format!("{py_cls}('{ip}')"));
|
||||
|
||||
let ip2: IpAddr = pyobj.extract().unwrap();
|
||||
assert_eq!(ip, ip2);
|
||||
}
|
||||
roundtrip(py, "127.0.0.1");
|
||||
roundtrip(py, "::1");
|
||||
roundtrip(py, "0.0.0.0");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_pystring() {
|
||||
Python::attach(|py| {
|
||||
let py_str = PyString::new(py, "0:0:0:0:0:0:0:1");
|
||||
let ip: IpAddr = py_str.extract().unwrap();
|
||||
assert_eq!(ip, IpAddr::from_str("::1").unwrap());
|
||||
|
||||
let py_str = PyString::new(py, "invalid");
|
||||
assert!(py_str.extract::<IpAddr>().is_err());
|
||||
});
|
||||
}
|
||||
}
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_subscript, PyStaticExpr};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::{
|
||||
conversion::{FromPyObjectOwned, IntoPyObject},
|
||||
instance::Bound,
|
||||
types::{any::PyAnyMethods, dict::PyDictMethods, PyDict},
|
||||
Borrowed, FromPyObject, PyAny, PyErr, Python,
|
||||
};
|
||||
use core::{cmp, hash};
|
||||
use std::collections;
|
||||
|
||||
impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap<K, V, H>
|
||||
where
|
||||
K: IntoPyObject<'py> + cmp::Eq + hash::Hash,
|
||||
V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::OUTPUT_TYPE, V::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a collections::HashMap<K, V, H>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash,
|
||||
&'a V: IntoPyObject<'py>,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, <&K>::OUTPUT_TYPE, <&V>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap<K, V>
|
||||
where
|
||||
K: IntoPyObject<'py> + cmp::Eq,
|
||||
V: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::OUTPUT_TYPE, V::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, V> IntoPyObject<'py> for &'a collections::BTreeMap<K, V>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + cmp::Eq,
|
||||
&'a V: IntoPyObject<'py>,
|
||||
K: 'a,
|
||||
V: 'a,
|
||||
{
|
||||
type Target = PyDict;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, <&K>::OUTPUT_TYPE, <&V>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let dict = PyDict::new(py);
|
||||
for (k, v) in self {
|
||||
dict.set_item(k, v)?;
|
||||
}
|
||||
Ok(dict)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, V, S> FromPyObject<'_, 'py> for collections::HashMap<K, V, S>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash,
|
||||
V: FromPyObjectOwned<'py>,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(&PyDict::TYPE_HINT, K::INPUT_TYPE, V::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
let dict = ob.cast::<PyDict>()?;
|
||||
let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default());
|
||||
for (k, v) in dict.iter() {
|
||||
ret.insert(
|
||||
k.extract().map_err(Into::into)?,
|
||||
v.extract().map_err(Into::into)?,
|
||||
);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, V> FromPyObject<'_, 'py> for collections::BTreeMap<K, V>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + cmp::Ord,
|
||||
V: FromPyObjectOwned<'py>,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr =
|
||||
type_hint_subscript!(PyDict::TYPE_HINT, K::INPUT_TYPE, V::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, PyErr> {
|
||||
let dict = ob.cast::<PyDict>()?;
|
||||
let mut ret = collections::BTreeMap::new();
|
||||
for (k, v) in dict.iter() {
|
||||
ret.insert(
|
||||
k.extract().map_err(Into::into)?,
|
||||
v.extract().map_err(Into::into)?,
|
||||
);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_hashmap_to_python() {
|
||||
Python::attach(|py| {
|
||||
let mut map = HashMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = (&map).into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
assert_eq!(map, py_map.extract().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_btreemap_to_python() {
|
||||
Python::attach(|py| {
|
||||
let mut map = BTreeMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = (&map).into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
assert_eq!(map, py_map.extract().unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hashmap_into_python() {
|
||||
Python::attach(|py| {
|
||||
let mut map = HashMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = map.into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_btreemap_into_py() {
|
||||
Python::attach(|py| {
|
||||
let mut map = BTreeMap::<i32, i32>::new();
|
||||
map.insert(1, 1);
|
||||
|
||||
let py_map = map.into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(py_map.len(), 1);
|
||||
assert!(
|
||||
py_map
|
||||
.get_item(1)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap()
|
||||
== 1
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
mod array;
|
||||
mod cell;
|
||||
mod cstring;
|
||||
mod ipaddr;
|
||||
mod map;
|
||||
pub(crate) mod num;
|
||||
mod option;
|
||||
mod osstr;
|
||||
mod path;
|
||||
mod set;
|
||||
mod slice;
|
||||
mod string;
|
||||
mod time;
|
||||
mod vec;
|
||||
+1242
File diff suppressed because it is too large.
Load diff
+65
@@ -0,0 +1,65 @@
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_union, PyStaticExpr};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::types::PyNone;
|
||||
use crate::{
|
||||
conversion::IntoPyObject, types::any::PyAnyMethods, BoundObject, FromPyObject, PyAny, Python,
|
||||
};
|
||||
use crate::{Borrowed, Bound};
|
||||
|
||||
impl<'py, T> IntoPyObject<'py> for Option<T>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = T::Error;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_union!(&T::OUTPUT_TYPE, PyNone::TYPE_HINT);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.map_or_else(
|
||||
|| Ok(py.None().into_bound(py)),
|
||||
|val| {
|
||||
val.into_pyobject(py)
|
||||
.map(BoundObject::into_any)
|
||||
.map(BoundObject::into_bound)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> IntoPyObject<'py> for &'a Option<T>
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = <&'a T as IntoPyObject<'py>>::Error;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <Option<&T>>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.as_ref().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> FromPyObject<'a, 'py> for Option<T>
|
||||
where
|
||||
T: FromPyObject<'a, 'py>,
|
||||
{
|
||||
type Error = T::Error;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(T::INPUT_TYPE, PyNone::TYPE_HINT);
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
if obj.is_none() {
|
||||
Ok(None)
|
||||
} else {
|
||||
obj.extract().map(Some)
|
||||
}
|
||||
}
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
use crate::conversion::IntoPyObject;
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
use crate::ffi;
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::instance::Bound;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::types::PyString;
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
use crate::types::{PyBytes, PyBytesMethods};
|
||||
use crate::{Borrowed, FromPyObject, PyAny, PyErr, Python};
|
||||
use alloc::borrow::Cow;
|
||||
use core::convert::Infallible;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &OsStr {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
// If the string is UTF-8, take the quick and easy shortcut
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
if let Some(valid_utf8_path) = self.to_str() {
|
||||
return valid_utf8_path.into_pyobject(py);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "wasi")]
|
||||
{
|
||||
self.to_str()
|
||||
.expect("wasi strings are UTF8")
|
||||
.into_pyobject(py)
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
{
|
||||
let bytes = self.as_bytes();
|
||||
let ptr = bytes.as_ptr().cast();
|
||||
let len = bytes.len() as ffi::Py_ssize_t;
|
||||
// SAFETY: passing valid pointer to python API
|
||||
unsafe {
|
||||
// DecodeFSDefault automatically chooses an appropriate decoding mechanism to
|
||||
// parse os strings losslessly (i.e. surrogateescape most of the time)
|
||||
Ok(ffi::PyUnicode_DecodeFSDefaultAndSize(ptr, len)
|
||||
.assume_owned(py)
|
||||
.cast_into_unchecked())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let wstr: Vec<u16> = self.encode_wide().collect();
|
||||
// SAFETY: passing valid pointer to python API
|
||||
unsafe {
|
||||
// This will not panic because the data from encode_wide is well-formed Windows
|
||||
// string data
|
||||
|
||||
Ok(
|
||||
ffi::PyUnicode_FromWideChar(wstr.as_ptr(), wstr.len() as ffi::Py_ssize_t)
|
||||
.assume_owned(py)
|
||||
.cast_into_unchecked(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &&OsStr {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&OsStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for OsString {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let pystring = ob.cast::<PyString>()?;
|
||||
|
||||
#[cfg(target_os = "wasi")]
|
||||
{
|
||||
Ok(pystring.to_cow()?.into_owned().into())
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
{
|
||||
// Decode from Python's lossless bytes string representation back into raw bytes
|
||||
// SAFETY: PyUnicode_EncodeFSDefault returns a new reference or null on error, known to
|
||||
// be a `bytes` object, thread is attached to the interpreter
|
||||
let fs_encoded_bytes = unsafe {
|
||||
ffi::PyUnicode_EncodeFSDefault(pystring.as_ptr())
|
||||
.assume_owned_or_err(ob.py())?
|
||||
.cast_into_unchecked::<PyBytes>()
|
||||
};
|
||||
|
||||
// Create an OsStr view into the raw bytes from Python
|
||||
let os_str: &OsStr = OsStrExt::from_bytes(fs_encoded_bytes.as_bytes());
|
||||
|
||||
Ok(os_str.to_os_string())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Take the quick and easy shortcut if UTF-8
|
||||
if let Ok(utf8_string) = pystring.to_cow() {
|
||||
return Ok(utf8_string.into_owned().into());
|
||||
}
|
||||
|
||||
// Get an owned allocated wide char buffer from PyString, which we have to deallocate
|
||||
// ourselves
|
||||
// SAFETY: passing valid pointer to python API
|
||||
let size =
|
||||
unsafe { ffi::PyUnicode_AsWideChar(pystring.as_ptr(), core::ptr::null_mut(), 0) };
|
||||
crate::err::error_on_minusone(ob.py(), size)?;
|
||||
|
||||
debug_assert!(
|
||||
size > 0,
|
||||
"PyUnicode_AsWideChar should return at least 1 for null terminator"
|
||||
);
|
||||
let size = size - 1; // exclude null terminator
|
||||
|
||||
let mut buffer = vec![0; size as usize];
|
||||
// SAFETY: passing valid pointer to python API
|
||||
let bytes_read =
|
||||
unsafe { ffi::PyUnicode_AsWideChar(pystring.as_ptr(), buffer.as_mut_ptr(), size) };
|
||||
assert_eq!(bytes_read, size);
|
||||
|
||||
// Copy wide char buffer into OsString
|
||||
let os_string = std::os::windows::ffi::OsStringExt::from_wide(&buffer);
|
||||
|
||||
Ok(os_string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Cow<'_, OsStr> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&OsStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&OsStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromPyObject<'a, '_> for Cow<'a, OsStr> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = OsString::INPUT_TYPE;
|
||||
|
||||
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
if let Ok(s) = obj.extract::<&str>() {
|
||||
return Ok(Cow::Borrowed(s.as_ref()));
|
||||
}
|
||||
|
||||
obj.extract::<OsString>().map(Cow::Owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for OsString {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&OsStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.as_os_str().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &OsString {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&OsStr>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
self.as_os_str().into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(target_os = "wasi")]
|
||||
use crate::exceptions::PyFileNotFoundError;
|
||||
use crate::types::{PyAnyMethods, PyString, PyStringMethods};
|
||||
use crate::{Bound, BoundObject, IntoPyObject, Python};
|
||||
use alloc::borrow::Cow;
|
||||
use core::fmt::Debug;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
|
||||
#[test]
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
fn test_non_utf8_conversion() {
|
||||
Python::attach(|py| {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
// this is not valid UTF-8
|
||||
let payload = &[250, 251, 252, 253, 254, 255, 0, 255];
|
||||
let os_str = OsStr::from_bytes(payload);
|
||||
|
||||
// do a roundtrip into Pythonland and back and compare
|
||||
let py_str = os_str.into_pyobject(py).unwrap();
|
||||
let os_str_2: OsString = py_str.extract().unwrap();
|
||||
assert_eq!(os_str, os_str_2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "wasi")]
|
||||
fn test_extract_non_utf8_wasi_should_error() {
|
||||
Python::attach(|py| {
|
||||
// Non utf-8 strings are not valid wasi paths
|
||||
let open_result = py.run(c"open('\\udcff', 'rb')", None, None).unwrap_err();
|
||||
assert!(
|
||||
!open_result.is_instance_of::<PyFileNotFoundError>(py),
|
||||
"Opening invalid utf8 will error with OSError, not FileNotFoundError"
|
||||
);
|
||||
|
||||
// Create a Python string with not valid UTF-8: &[255]
|
||||
let py_str = py.eval(c"'\\udcff'", None, None).unwrap();
|
||||
assert!(
|
||||
py_str.extract::<OsString>().is_err(),
|
||||
"Extracting invalid UTF-8 as OsString should error"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intopyobject_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
fn test_roundtrip<'py, T>(py: Python<'py>, obj: T)
|
||||
where
|
||||
T: IntoPyObject<'py> + AsRef<OsStr> + Debug + Clone,
|
||||
T::Error: Debug,
|
||||
{
|
||||
let pyobject = obj.clone().into_pyobject(py).unwrap().into_any();
|
||||
let pystring = pyobject.as_borrowed().cast::<PyString>().unwrap();
|
||||
assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy());
|
||||
let roundtripped_obj: OsString = pystring.extract().unwrap();
|
||||
assert_eq!(obj.as_ref(), roundtripped_obj.as_os_str());
|
||||
}
|
||||
let os_str = OsStr::new("Hello\0\n🐍");
|
||||
test_roundtrip::<&OsStr>(py, os_str);
|
||||
test_roundtrip::<Cow<'_, OsStr>>(py, Cow::Borrowed(os_str));
|
||||
test_roundtrip::<Cow<'_, OsStr>>(py, Cow::Owned(os_str.to_os_string()));
|
||||
test_roundtrip::<OsString>(py, os_str.to_os_string());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn test_windows_non_utf8_osstring_roundtrip() {
|
||||
use std::os::windows::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
Python::attach(|py| {
|
||||
// Example: Unpaired surrogate (0xD800) is not valid UTF-8, but valid in Windows OsString
|
||||
let wide: &[u16] = &['A' as u16, 0xD800, 'B' as u16]; // 'A', unpaired surrogate, 'B'
|
||||
let os_str = OsString::from_wide(wide);
|
||||
|
||||
assert_eq!(os_str.to_string_lossy(), "A�B");
|
||||
|
||||
// This cannot be represented as UTF-8, so .to_str() would return None
|
||||
assert!(os_str.to_str().is_none());
|
||||
|
||||
// Convert to Python and back
|
||||
let py_str = os_str.as_os_str().into_pyobject(py).unwrap();
|
||||
let os_str_2 = py_str.extract::<OsString>().unwrap();
|
||||
|
||||
// The roundtrip should preserve the original wide data
|
||||
assert_eq!(os_str, os_str_2);
|
||||
|
||||
// Show that encode_wide is necessary: direct UTF-8 conversion would lose information
|
||||
let encoded: Vec<u16> = os_str.encode_wide().collect();
|
||||
assert_eq!(encoded, wide);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_cow() {
|
||||
Python::attach(|py| {
|
||||
fn test_extract<'py, T>(py: Python<'py>, input: &T, is_borrowed: bool)
|
||||
where
|
||||
for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>,
|
||||
for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug,
|
||||
T: AsRef<OsStr> + ?Sized,
|
||||
{
|
||||
let pystring = input.into_pyobject(py).unwrap();
|
||||
let cow: Cow<'_, OsStr> = pystring.extract().unwrap();
|
||||
assert_eq!(cow, input.as_ref());
|
||||
assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_)));
|
||||
}
|
||||
|
||||
// On Python 3.10+ or when not using the limited API, we can borrow strings from python
|
||||
let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
|
||||
// This can be borrowed because it is valid UTF-8
|
||||
test_extract::<str>(py, "Hello\0\n🐍", can_borrow_str);
|
||||
test_extract::<str>(py, "Hello, world!", can_borrow_str);
|
||||
|
||||
#[cfg(windows)]
|
||||
let os_str = {
|
||||
// 'A', unpaired surrogate, 'B'
|
||||
OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16])
|
||||
};
|
||||
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) };
|
||||
|
||||
// This cannot be borrowed because it is not valid UTF-8
|
||||
#[cfg(any(windows, unix, target_os = "emscripten"))]
|
||||
test_extract::<OsStr>(py, &os_str, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_identifier, type_hint_subscript, type_hint_union, PyStaticExpr};
|
||||
use crate::sync::PyOnceLock;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::{ffi, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python};
|
||||
use alloc::borrow::Cow;
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
impl FromPyObject<'_, '_> for PathBuf {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(
|
||||
OsString::INPUT_TYPE,
|
||||
type_hint_subscript!(
|
||||
type_hint_identifier!("os", "PathLike"),
|
||||
OsString::INPUT_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
// We use os.fspath to get the underlying path as bytes or str
|
||||
let path = unsafe { ffi::PyOS_FSPath(ob.as_ptr()).assume_owned_or_err(ob.py())? };
|
||||
Ok(path.extract::<OsString>()?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Path {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("pathlib", "Path");
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
static PY_PATH: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
|
||||
PY_PATH
|
||||
.import(py, "pathlib", "Path")?
|
||||
.call((self.as_os_str(),), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &&Path {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Cow<'_, Path> {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Cow<'_, Path> {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromPyObject<'a, '_> for Cow<'a, Path> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PathBuf::INPUT_TYPE;
|
||||
|
||||
fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
if let Ok(s) = obj.extract::<&str>() {
|
||||
return Ok(Cow::Borrowed(s.as_ref()));
|
||||
}
|
||||
|
||||
obj.extract::<PathBuf>().map(Cow::Owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for PathBuf {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &PathBuf {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&Path>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
types::{PyAnyMethods, PyString},
|
||||
IntoPyObjectExt,
|
||||
};
|
||||
use core::fmt::Debug;
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
|
||||
#[test]
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
fn test_non_utf8_conversion() {
|
||||
Python::attach(|py| {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
// this is not valid UTF-8
|
||||
let payload = &[250, 251, 252, 253, 254, 255, 0, 255];
|
||||
let path = Path::new(OsStr::from_bytes(payload));
|
||||
|
||||
// do a roundtrip into Pythonland and back and compare
|
||||
let py_str = path.into_pyobject(py).unwrap();
|
||||
let path_2: PathBuf = py_str.extract().unwrap();
|
||||
assert_eq!(path, path_2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intopyobject_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
fn test_roundtrip<'py, T>(py: Python<'py>, obj: T)
|
||||
where
|
||||
T: IntoPyObject<'py> + AsRef<Path> + Debug + Clone,
|
||||
T::Error: Debug,
|
||||
{
|
||||
let pyobject = obj.clone().into_bound_py_any(py).unwrap();
|
||||
let roundtripped_obj: PathBuf = pyobject.extract().unwrap();
|
||||
assert_eq!(obj.as_ref(), roundtripped_obj.as_path());
|
||||
}
|
||||
let path = Path::new("Hello\0\n🐍");
|
||||
test_roundtrip::<&Path>(py, path);
|
||||
test_roundtrip::<Cow<'_, Path>>(py, Cow::Borrowed(path));
|
||||
test_roundtrip::<Cow<'_, Path>>(py, Cow::Owned(path.to_path_buf()));
|
||||
test_roundtrip::<PathBuf>(py, path.to_path_buf());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_pystring() {
|
||||
Python::attach(|py| {
|
||||
let path = "Hello\0\n🐍";
|
||||
let pystring = PyString::new(py, path);
|
||||
let roundtrip: PathBuf = pystring.extract().unwrap();
|
||||
assert_eq!(roundtrip, Path::new(path));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_cow() {
|
||||
Python::attach(|py| {
|
||||
fn test_extract<'py, T>(py: Python<'py>, path: &T, is_borrowed: bool)
|
||||
where
|
||||
for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>,
|
||||
for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug,
|
||||
T: AsRef<Path> + ?Sized,
|
||||
{
|
||||
let pystring = path.into_pyobject(py).unwrap();
|
||||
let cow: Cow<'_, Path> = pystring.extract().unwrap();
|
||||
assert_eq!(cow, path.as_ref());
|
||||
assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_)));
|
||||
}
|
||||
|
||||
// On Python 3.10+ or when not using the limited API, we can borrow strings from python
|
||||
let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API)));
|
||||
// This can be borrowed because it is valid UTF-8
|
||||
test_extract::<str>(py, "Hello\0\n🐍", can_borrow_str);
|
||||
test_extract::<str>(py, "Hello, world!", can_borrow_str);
|
||||
|
||||
#[cfg(windows)]
|
||||
let os_str = {
|
||||
// 'A', unpaired surrogate, 'B'
|
||||
OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16])
|
||||
};
|
||||
|
||||
#[cfg(any(unix, target_os = "emscripten"))]
|
||||
let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) };
|
||||
|
||||
// This cannot be borrowed because it is not valid UTF-8
|
||||
#[cfg(any(unix, windows, target_os = "emscripten"))]
|
||||
test_extract::<OsStr>(py, &os_str, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
use core::{cmp, hash};
|
||||
use std::collections;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_subscript, PyStaticExpr};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::{
|
||||
conversion::{FromPyObjectOwned, IntoPyObject},
|
||||
types::{
|
||||
any::PyAnyMethods, frozenset::PyFrozenSetMethods, set::PySetMethods, PyFrozenSet, PySet,
|
||||
},
|
||||
Borrowed, Bound, FromPyObject, PyAny, PyErr, Python,
|
||||
};
|
||||
|
||||
impl<'py, K, S> IntoPyObject<'py> for collections::HashSet<K, S>
|
||||
where
|
||||
K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, K::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K, H> IntoPyObject<'py> for &'a collections::HashSet<K, H>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + Eq + hash::Hash,
|
||||
H: hash::BuildHasher,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, <&K>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K, S> FromPyObject<'_, 'py> for collections::HashSet<K, S>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash,
|
||||
S: hash::BuildHasher + Default,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, K::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
match ob.cast::<PySet>() {
|
||||
Ok(set) => set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
if let Ok(frozen_set) = ob.cast::<PyFrozenSet>() {
|
||||
frozen_set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect()
|
||||
} else {
|
||||
Err(PyErr::from(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K> IntoPyObject<'py> for collections::BTreeSet<K>
|
||||
where
|
||||
K: IntoPyObject<'py> + cmp::Ord,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, K::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, K> IntoPyObject<'py> for &'a collections::BTreeSet<K>
|
||||
where
|
||||
&'a K: IntoPyObject<'py> + cmp::Ord,
|
||||
K: 'a,
|
||||
{
|
||||
type Target = PySet;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, <&K>::OUTPUT_TYPE);
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
PySet::new(py, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, K> FromPyObject<'_, 'py> for collections::BTreeSet<K>
|
||||
where
|
||||
K: FromPyObjectOwned<'py> + cmp::Ord,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySet::TYPE_HINT, K::INPUT_TYPE);
|
||||
|
||||
fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
match ob.cast::<PySet>() {
|
||||
Ok(set) => set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
if let Ok(frozen_set) = ob.cast::<PyFrozenSet>() {
|
||||
frozen_set
|
||||
.iter()
|
||||
.map(|any| any.extract().map_err(Into::into))
|
||||
.collect()
|
||||
} else {
|
||||
Err(PyErr::from(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::types::{any::PyAnyMethods, PyFrozenSet, PySet};
|
||||
use crate::{IntoPyObject, Python};
|
||||
use alloc::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_extract_hashset() {
|
||||
Python::attach(|py| {
|
||||
let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: HashSet<usize> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
|
||||
let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: HashSet<usize> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_btreeset() {
|
||||
Python::attach(|py| {
|
||||
let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: BTreeSet<usize> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
|
||||
let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap();
|
||||
let hash_set: BTreeSet<usize> = set.extract().unwrap();
|
||||
assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let bt: BTreeSet<u64> = [1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
let hs: HashSet<u64> = [1, 2, 3, 4, 5].iter().cloned().collect();
|
||||
|
||||
let bto = (&bt).into_pyobject(py).unwrap();
|
||||
let hso = (&hs).into_pyobject(py).unwrap();
|
||||
|
||||
assert_eq!(bt, bto.extract().unwrap());
|
||||
assert_eq!(hs, hso.extract().unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::{
|
||||
conversion::IntoPyObject, types::PyBytes, Bound, CastError, PyAny, PyErr, PyResult, Python,
|
||||
};
|
||||
|
||||
impl<'a, 'py, T> IntoPyObject<'py> for &'a [T]
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&T>::SEQUENCE_OUTPUT_TYPE;
|
||||
|
||||
/// Turns [`&[u8]`](core::slice) into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
|
||||
///
|
||||
/// [`PyBytes`]: crate::types::PyBytes
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
<&T>::borrowed_sequence_into_pyobject(self, py, crate::conversion::private::Token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for &'a [u8] {
|
||||
type Error = CastError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
fn extract(obj: crate::Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
Ok(obj.cast::<PyBytes>()?.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
/// Special-purpose trait impl to efficiently handle both `bytes` and `bytearray`
|
||||
///
|
||||
/// If the source object is a `bytes` object, the `Cow` will be borrowed and
|
||||
/// pointing into the source object, and no copying or heap allocations will happen.
|
||||
/// If it is a `bytearray`, its contents will be copied to an owned `Cow`.
|
||||
impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for Cow<'a, [u8]> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = Vec::<u8>::INPUT_TYPE;
|
||||
|
||||
fn extract(ob: crate::Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
|
||||
Ok(if let Ok(bytes) = ob.cast::<PyBytes>() {
|
||||
Cow::Borrowed(bytes.as_bytes()) // It's immutable, we can take a slice
|
||||
} else {
|
||||
Cow::Owned(Vec::extract(ob)?) // Not possible to take a slice, we have to build a Vec<u8>
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> IntoPyObject<'py> for Cow<'_, [T]>
|
||||
where
|
||||
T: Clone,
|
||||
for<'a> &'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&T>::SEQUENCE_OUTPUT_TYPE;
|
||||
|
||||
/// Turns `Cow<[u8]>` into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
|
||||
///
|
||||
/// [`PyBytes`]: crate::types::PyBytes
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
<&T>::borrowed_sequence_into_pyobject(self.as_ref(), py, crate::conversion::private::Token)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
conversion::IntoPyObject,
|
||||
types::{any::PyAnyMethods, PyBytes, PyBytesMethods, PyList},
|
||||
Python,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytes() {
|
||||
Python::attach(|py| {
|
||||
let py_bytes = py.eval(c"b'Hello Python'", None, None).unwrap();
|
||||
let bytes: &[u8] = py_bytes.extract().unwrap();
|
||||
assert_eq!(bytes, b"Hello Python");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cow_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes = py.eval(cr#"b"foobar""#, None, None).unwrap();
|
||||
let cow = bytes.extract::<Cow<'_, [u8]>>().unwrap();
|
||||
assert_eq!(cow, Cow::<[u8]>::Borrowed(b"foobar"));
|
||||
|
||||
let byte_array = py.eval(cr#"bytearray(b"foobar")"#, None, None).unwrap();
|
||||
let cow = byte_array.extract::<Cow<'_, [u8]>>().unwrap();
|
||||
assert_eq!(cow, Cow::<[u8]>::Owned(b"foobar".to_vec()));
|
||||
|
||||
let something_else_entirely = py.eval(c"42", None, None).unwrap();
|
||||
something_else_entirely
|
||||
.extract::<Cow<'_, [u8]>>()
|
||||
.unwrap_err();
|
||||
|
||||
let cow = Cow::<[u8]>::Borrowed(b"foobar").into_pyobject(py).unwrap();
|
||||
assert!(cow.is_instance_of::<PyBytes>());
|
||||
|
||||
let cow = Cow::<[u8]>::Owned(b"foobar".to_vec())
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
assert!(cow.is_instance_of::<PyBytes>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slice_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes: &[u8] = b"foobar";
|
||||
let obj = bytes.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), bytes);
|
||||
|
||||
let nums: &[u16] = &[0, 1, 2, 3];
|
||||
let obj = nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cow_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let borrowed_bytes = Cow::<[u8]>::Borrowed(b"foobar");
|
||||
let obj = borrowed_bytes.clone().into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &*borrowed_bytes);
|
||||
|
||||
let owned_bytes = Cow::<[u8]>::Owned(b"foobar".to_vec());
|
||||
let obj = owned_bytes.clone().into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &*owned_bytes);
|
||||
|
||||
let borrowed_nums = Cow::<[u16]>::Borrowed(&[0, 1, 2, 3]);
|
||||
let obj = borrowed_nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
|
||||
let owned_nums = Cow::<[u16]>::Owned(vec![0, 1, 2, 3]);
|
||||
let obj = owned_nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::{
|
||||
conversion::IntoPyObject, instance::Bound, types::PyString, Borrowed, FromPyObject, PyAny,
|
||||
PyErr, Python,
|
||||
};
|
||||
use alloc::borrow::Cow;
|
||||
use core::convert::Infallible;
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &str {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &&str {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Cow<'_, str> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Cow<'_, str> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(&**self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for char {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let mut bytes = [0u8; 4];
|
||||
Ok(PyString::new(py, self.encode_utf8(&mut bytes)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &char {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for String {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, &self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &String {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = String::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
impl<'a> FromPyObject<'a, '_> for &'a str {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
ob.cast::<PyString>()?.to_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromPyObject<'a, '_> for Cow<'a, str> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(ob: Borrowed<'a, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
ob.cast::<PyString>()?.to_cow()
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows extracting strings from Python objects.
|
||||
/// Accepts Python `str` and `unicode` objects.
|
||||
impl FromPyObject<'_, '_> for String {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
obj.cast::<PyString>()?.to_cow().map(Cow::into_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for char {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let s = obj.cast::<PyString>()?.to_cow()?;
|
||||
let mut iter = s.chars();
|
||||
if let (Some(ch), None) = (iter.next(), iter.next()) {
|
||||
Ok(ch)
|
||||
} else {
|
||||
Err(crate::exceptions::PyValueError::new_err(
|
||||
"expected a string of length 1",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::{IntoPyObject, Python};
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
#[test]
|
||||
fn test_cow_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello Python";
|
||||
let py_string = Cow::Borrowed(s).into_pyobject(py).unwrap();
|
||||
assert_eq!(s, py_string.extract::<Cow<'_, str>>().unwrap());
|
||||
let py_string = Cow::<str>::Owned(s.into()).into_pyobject(py).unwrap();
|
||||
assert_eq!(s, py_string.extract::<Cow<'_, str>>().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_bmp() {
|
||||
Python::attach(|py| {
|
||||
let s = "\u{1F30F}";
|
||||
let py_string = s.into_pyobject(py).unwrap();
|
||||
assert_eq!(s, py_string.extract::<String>().unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_str() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello Python";
|
||||
let py_string = s.into_pyobject(py).unwrap();
|
||||
|
||||
let s2: Cow<'_, str> = py_string.extract().unwrap();
|
||||
assert_eq!(s, s2);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_char() {
|
||||
Python::attach(|py| {
|
||||
let ch = '😃';
|
||||
let py_string = ch.into_pyobject(py).unwrap();
|
||||
let ch2: char = py_string.extract().unwrap();
|
||||
assert_eq!(ch, ch2);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_char_err() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello Python";
|
||||
let py_string = s.into_pyobject(py).unwrap();
|
||||
let err: crate::PyResult<char> = py_string.extract();
|
||||
assert!(err
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("expected a string of length 1"));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let s = "Hello Python";
|
||||
let s2 = s.to_owned();
|
||||
let s3 = &s2;
|
||||
assert_eq!(
|
||||
s,
|
||||
s3.into_pyobject(py)
|
||||
.unwrap()
|
||||
.extract::<Cow<'_, str>>()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
s,
|
||||
s2.into_pyobject(py)
|
||||
.unwrap()
|
||||
.extract::<Cow<'_, str>>()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
s,
|
||||
s.into_pyobject(py)
|
||||
.unwrap()
|
||||
.extract::<Cow<'_, str>>()
|
||||
.unwrap()
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::{PyOverflowError, PyValueError};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
use crate::intern;
|
||||
use crate::sync::PyOnceLock;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_object::PyTypeInfo;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use crate::types::PyDeltaAccess;
|
||||
use crate::types::{PyDateTime, PyDelta, PyTzInfo};
|
||||
use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
|
||||
use core::time::Duration;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
|
||||
|
||||
impl FromPyObject<'_, '_> for Duration {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let delta = obj.cast::<PyDelta>()?;
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
let (days, seconds, microseconds) = {
|
||||
(
|
||||
delta.get_days(),
|
||||
delta.get_seconds(),
|
||||
delta.get_microseconds(),
|
||||
)
|
||||
};
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
let (days, seconds, microseconds): (i32, i32, i32) = {
|
||||
let py = delta.py();
|
||||
(
|
||||
delta.getattr(intern!(py, "days"))?.extract()?,
|
||||
delta.getattr(intern!(py, "seconds"))?.extract()?,
|
||||
delta.getattr(intern!(py, "microseconds"))?.extract()?,
|
||||
)
|
||||
};
|
||||
|
||||
// We cast
|
||||
let days = u64::try_from(days).map_err(|_| {
|
||||
PyValueError::new_err(
|
||||
"It is not possible to convert a negative timedelta to a Rust Duration",
|
||||
)
|
||||
})?;
|
||||
let seconds = u64::try_from(seconds).unwrap(); // 0 <= seconds < 3600*24
|
||||
let microseconds = u32::try_from(microseconds).unwrap(); // 0 <= microseconds < 1000000
|
||||
|
||||
// We convert
|
||||
let total_seconds = days * SECONDS_PER_DAY + seconds; // We casted from i32, this can't overflow
|
||||
let nanoseconds = microseconds.checked_mul(1_000).unwrap(); // 0 <= microseconds < 1000000
|
||||
|
||||
Ok(Duration::new(total_seconds, nanoseconds))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Duration {
|
||||
type Target = PyDelta;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyDelta::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let days = self.as_secs() / SECONDS_PER_DAY;
|
||||
let seconds = self.as_secs() % SECONDS_PER_DAY;
|
||||
let microseconds = self.subsec_micros();
|
||||
|
||||
PyDelta::new(
|
||||
py,
|
||||
days.try_into()?,
|
||||
seconds.try_into()?,
|
||||
microseconds.try_into()?,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Duration {
|
||||
type Target = PyDelta;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Duration::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
// Conversions between SystemTime and datetime do not rely on the floating point timestamp of the
|
||||
// timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the
|
||||
// timedelta/core::time::Duration types by taking for reference point the UNIX epoch.
|
||||
//
|
||||
// TODO: it might be nice to investigate using timestamps anyway, at least when the datetime is a safe range.
|
||||
|
||||
impl FromPyObject<'_, '_> for SystemTime {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let duration_since_unix_epoch: Duration = obj.sub(unix_epoch_py(obj.py())?)?.extract()?;
|
||||
UNIX_EPOCH
|
||||
.checked_add(duration_since_unix_epoch)
|
||||
.ok_or_else(|| {
|
||||
PyOverflowError::new_err("Overflow error when converting the time to Rust")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for SystemTime {
|
||||
type Target = PyDateTime;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyDateTime::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let duration_since_unix_epoch =
|
||||
self.duration_since(UNIX_EPOCH).unwrap().into_pyobject(py)?;
|
||||
unix_epoch_py(py)?
|
||||
.add(duration_since_unix_epoch)?
|
||||
.cast_into()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &SystemTime {
|
||||
type Target = PyDateTime;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = SystemTime::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_epoch_py(py: Python<'_>) -> PyResult<Borrowed<'_, '_, PyDateTime>> {
|
||||
static UNIX_EPOCH: PyOnceLock<Py<PyDateTime>> = PyOnceLock::new();
|
||||
Ok(UNIX_EPOCH
|
||||
.get_or_try_init(py, || {
|
||||
let utc = PyTzInfo::utc(py)?;
|
||||
Ok::<_, PyErr>(PyDateTime::new(py, 1970, 1, 1, 0, 0, 0, 0, Some(&utc))?.into())
|
||||
})?
|
||||
.bind_borrowed(py))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::PyDict;
|
||||
|
||||
#[test]
|
||||
fn test_duration_frompyobject() {
|
||||
Python::attach(|py| {
|
||||
assert_eq!(
|
||||
new_timedelta(py, 0, 0, 0).extract::<Duration>().unwrap(),
|
||||
Duration::new(0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
new_timedelta(py, 1, 0, 0).extract::<Duration>().unwrap(),
|
||||
Duration::new(86400, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
new_timedelta(py, 0, 1, 0).extract::<Duration>().unwrap(),
|
||||
Duration::new(1, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
new_timedelta(py, 0, 0, 1).extract::<Duration>().unwrap(),
|
||||
Duration::new(0, 1_000)
|
||||
);
|
||||
assert_eq!(
|
||||
new_timedelta(py, 1, 1, 1).extract::<Duration>().unwrap(),
|
||||
Duration::new(86401, 1_000)
|
||||
);
|
||||
assert_eq!(
|
||||
timedelta_class(py)
|
||||
.getattr("max")
|
||||
.unwrap()
|
||||
.extract::<Duration>()
|
||||
.unwrap(),
|
||||
Duration::new(86399999999999, 999999000)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_frompyobject_negative() {
|
||||
Python::attach(|py| {
|
||||
assert_eq!(
|
||||
new_timedelta(py, 0, -1, 0)
|
||||
.extract::<Duration>()
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| {
|
||||
assert!(l.eq(r).unwrap());
|
||||
};
|
||||
|
||||
assert_eq(
|
||||
Duration::new(0, 0).into_pyobject(py).unwrap().into_any(),
|
||||
new_timedelta(py, 0, 0, 0),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(86400, 0)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any(),
|
||||
new_timedelta(py, 1, 0, 0),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(1, 0).into_pyobject(py).unwrap().into_any(),
|
||||
new_timedelta(py, 0, 1, 0),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(0, 1_000)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any(),
|
||||
new_timedelta(py, 0, 0, 1),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(0, 1).into_pyobject(py).unwrap().into_any(),
|
||||
new_timedelta(py, 0, 0, 0),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(86401, 1_000)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any(),
|
||||
new_timedelta(py, 1, 1, 1),
|
||||
);
|
||||
assert_eq(
|
||||
Duration::new(86399999999999, 999999000)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any(),
|
||||
timedelta_class(py).getattr("max").unwrap(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_into_pyobject_overflow() {
|
||||
Python::attach(|py| {
|
||||
assert!(Duration::MAX.into_pyobject(py).is_err());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_frompyobject() {
|
||||
Python::attach(|py| {
|
||||
assert_eq!(
|
||||
new_datetime(py, 1970, 1, 1, 0, 0, 0, 0)
|
||||
.extract::<SystemTime>()
|
||||
.unwrap(),
|
||||
UNIX_EPOCH
|
||||
);
|
||||
assert_eq!(
|
||||
new_datetime(py, 2020, 2, 3, 4, 5, 6, 7)
|
||||
.extract::<SystemTime>()
|
||||
.unwrap(),
|
||||
UNIX_EPOCH
|
||||
.checked_add(Duration::new(1580702706, 7000))
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
max_datetime(py).extract::<SystemTime>().unwrap(),
|
||||
UNIX_EPOCH
|
||||
.checked_add(Duration::new(253402300799, 999999000))
|
||||
.unwrap()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_frompyobject_before_epoch() {
|
||||
Python::attach(|py| {
|
||||
assert_eq!(
|
||||
new_datetime(py, 1950, 1, 1, 0, 0, 0, 0)
|
||||
.extract::<SystemTime>()
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"ValueError: It is not possible to convert a negative timedelta to a Rust Duration"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_intopyobject() {
|
||||
Python::attach(|py| {
|
||||
let assert_eq = |l: Bound<'_, PyDateTime>, r: Bound<'_, PyDateTime>| {
|
||||
assert!(l.eq(r).unwrap());
|
||||
};
|
||||
|
||||
assert_eq(
|
||||
UNIX_EPOCH
|
||||
.checked_add(Duration::new(1580702706, 7123))
|
||||
.unwrap()
|
||||
.into_pyobject(py)
|
||||
.unwrap(),
|
||||
new_datetime(py, 2020, 2, 3, 4, 5, 6, 7),
|
||||
);
|
||||
assert_eq(
|
||||
UNIX_EPOCH
|
||||
.checked_add(Duration::new(253402300799, 999999000))
|
||||
.unwrap()
|
||||
.into_pyobject(py)
|
||||
.unwrap(),
|
||||
max_datetime(py),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn new_datetime(
|
||||
py: Python<'_>,
|
||||
year: i32,
|
||||
month: u8,
|
||||
day: u8,
|
||||
hour: u8,
|
||||
minute: u8,
|
||||
second: u8,
|
||||
microsecond: u32,
|
||||
) -> Bound<'_, PyDateTime> {
|
||||
let utc = PyTzInfo::utc(py).unwrap();
|
||||
PyDateTime::new(
|
||||
py,
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond,
|
||||
Some(&utc),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn max_datetime(py: Python<'_>) -> Bound<'_, PyDateTime> {
|
||||
let naive_max = datetime_class(py).getattr("max").unwrap();
|
||||
let kargs = PyDict::new(py);
|
||||
kargs
|
||||
.set_item("tzinfo", PyTzInfo::utc(py).unwrap())
|
||||
.unwrap();
|
||||
naive_max
|
||||
.call_method("replace", (), Some(&kargs))
|
||||
.unwrap()
|
||||
.cast_into()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_intopyobject_overflow() {
|
||||
let big_system_time = UNIX_EPOCH
|
||||
.checked_add(Duration::new(300000000000, 0))
|
||||
.unwrap();
|
||||
Python::attach(|py| {
|
||||
assert!(big_system_time.into_pyobject(py).is_err());
|
||||
})
|
||||
}
|
||||
|
||||
fn new_timedelta(
|
||||
py: Python<'_>,
|
||||
days: i32,
|
||||
seconds: i32,
|
||||
microseconds: i32,
|
||||
) -> Bound<'_, PyAny> {
|
||||
timedelta_class(py)
|
||||
.call1((days, seconds, microseconds))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn datetime_class(py: Python<'_>) -> Bound<'_, PyAny> {
|
||||
py.import("datetime").unwrap().getattr("datetime").unwrap()
|
||||
}
|
||||
|
||||
fn timedelta_class(py: Python<'_>) -> Bound<'_, PyAny> {
|
||||
py.import("datetime").unwrap().getattr("timedelta").unwrap()
|
||||
}
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::{type_hint_subscript, PyStaticExpr};
|
||||
use crate::{
|
||||
conversion::{FromPyObject, FromPyObjectOwned, FromPyObjectSequence, IntoPyObject},
|
||||
exceptions::PyTypeError,
|
||||
ffi,
|
||||
types::{PyAnyMethods, PySequence, PyString},
|
||||
Borrowed, CastError, PyResult, PyTypeInfo,
|
||||
};
|
||||
use crate::{Bound, PyAny, PyErr, Python};
|
||||
|
||||
impl<'py, T> IntoPyObject<'py> for Vec<T>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::SEQUENCE_OUTPUT_TYPE;
|
||||
|
||||
/// Turns [`Vec<u8>`] into [`PyBytes`], all other `T`s will be turned into a [`PyList`]
|
||||
///
|
||||
/// [`PyBytes`]: crate::types::PyBytes
|
||||
/// [`PyList`]: crate::types::PyList
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T> IntoPyObject<'py> for &'a Vec<T>
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&[T]>::OUTPUT_TYPE;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
// NB: we could actually not cast to `PyAny`, which would be nice for
|
||||
// `&Vec<u8>`, but that'd be inconsistent with the `IntoPyObject` impl
|
||||
// above which always returns a `PyAny` for `Vec<T>`.
|
||||
self.as_slice().into_pyobject(py).map(Bound::into_any)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> FromPyObject<'_, 'py> for Vec<T>
|
||||
where
|
||||
T: FromPyObjectOwned<'py>,
|
||||
{
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_subscript!(PySequence::TYPE_HINT, T::INPUT_TYPE);
|
||||
|
||||
fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Self> {
|
||||
if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) {
|
||||
return Ok(extractor.to_vec());
|
||||
}
|
||||
|
||||
if obj.is_instance_of::<PyString>() {
|
||||
return Err(PyTypeError::new_err("Can't extract `str` to `Vec`"));
|
||||
}
|
||||
|
||||
extract_sequence(obj)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_sequence<'py, T>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<Vec<T>>
|
||||
where
|
||||
T: FromPyObjectOwned<'py>,
|
||||
{
|
||||
// Types that pass `PySequence_Check` usually implement enough of the sequence protocol
|
||||
// to support this function and if not, we will only fail extraction safely.
|
||||
if unsafe { ffi::PySequence_Check(obj.as_ptr()) } == 0 {
|
||||
return Err(CastError::new(obj, PySequence::type_object(obj.py()).into_any()).into());
|
||||
}
|
||||
|
||||
let mut v = Vec::with_capacity(obj.len().unwrap_or(0));
|
||||
for item in obj.try_iter()? {
|
||||
v.push(item?.extract::<T>().map_err(Into::into)?);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::types::{PyAnyMethods, PyBytes, PyBytesMethods, PyList};
|
||||
use crate::Python;
|
||||
|
||||
#[test]
|
||||
fn test_vec_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes: Vec<u8> = b"foobar".to_vec();
|
||||
let obj = bytes.clone().into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &bytes);
|
||||
|
||||
let nums: Vec<u16> = vec![0, 1, 2, 3];
|
||||
let obj = nums.into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vec_reference_intopyobject_impl() {
|
||||
Python::attach(|py| {
|
||||
let bytes: Vec<u8> = b"foobar".to_vec();
|
||||
let obj = (&bytes).into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyBytes>());
|
||||
let obj = obj.cast_into::<PyBytes>().unwrap();
|
||||
assert_eq!(obj.as_bytes(), &bytes);
|
||||
|
||||
let nums: Vec<u16> = vec![0, 1, 2, 3];
|
||||
let obj = (&nums).into_pyobject(py).unwrap();
|
||||
assert!(obj.is_instance_of::<PyList>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strings_cannot_be_extracted_to_vec() {
|
||||
Python::attach(|py| {
|
||||
let v = "London Calling";
|
||||
let ob = v.into_pyobject(py).unwrap();
|
||||
|
||||
assert!(ob.extract::<Vec<String>>().is_err());
|
||||
assert!(ob.extract::<Vec<char>>().is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytes_to_vec() {
|
||||
Python::attach(|py| {
|
||||
let v: Vec<u8> = PyBytes::new(py, b"abc").extract().unwrap();
|
||||
assert_eq!(v, b"abc");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tuple_to_vec() {
|
||||
Python::attach(|py| {
|
||||
let v: Vec<i32> = py.eval(c"(1, 2)", None, None).unwrap().extract().unwrap();
|
||||
assert_eq!(v, [1, 2]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_range_to_vec() {
|
||||
Python::attach(|py| {
|
||||
let v: Vec<i32> = py
|
||||
.eval(c"range(1, 5)", None, None)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(v, [1, 2, 3, 4]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_bytearray_to_vec() {
|
||||
Python::attach(|py| {
|
||||
let v: Vec<u8> = py
|
||||
.eval(c"bytearray(b'abc')", None, None)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_eq!(v, b"abc");
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+1574
File diff suppressed because it is too large.
Load diff
Vendored
+252
@@ -0,0 +1,252 @@
|
||||
#![cfg(feature = "uuid")]
|
||||
|
||||
//! Conversions to and from [uuid](https://docs.rs/uuid/latest/uuid/)'s [`Uuid`] and [`NonNilUuid`] types.
|
||||
//!
|
||||
//! This is useful for converting Python's uuid.UUID into and from a native Rust type.
|
||||
//!
|
||||
//! # Setup
|
||||
//!
|
||||
//! To use this feature, add to your **`Cargo.toml`**:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"uuid\"] }")]
|
||||
//! uuid = "1.11.0"
|
||||
//! ```
|
||||
//!
|
||||
//! Note that you must use a compatible version of uuid and PyO3.
|
||||
//! The required uuid version may vary based on the version of PyO3.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Rust code to create a function that parses a UUID string and returns it as a `Uuid`:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pyo3::prelude::*;
|
||||
//! use pyo3::exceptions::PyValueError;
|
||||
//! use uuid::Uuid;
|
||||
//!
|
||||
//! /// Parse a UUID from a string.
|
||||
//! #[pyfunction]
|
||||
//! fn get_uuid_from_str(s: &str) -> PyResult<Uuid> {
|
||||
//! Uuid::parse_str(s).map_err(|e| PyValueError::new_err(e.to_string()))
|
||||
//! }
|
||||
//!
|
||||
//! /// Passing a Python uuid.UUID directly to Rust.
|
||||
//! #[pyfunction]
|
||||
//! fn get_uuid(u: Uuid) -> Uuid {
|
||||
//! u
|
||||
//! }
|
||||
//!
|
||||
//! #[pymodule]
|
||||
//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(get_uuid_from_str, m)?)?;
|
||||
//! m.add_function(wrap_pyfunction!(get_uuid, m)?)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Python code that validates the functionality
|
||||
//!
|
||||
//!
|
||||
//! ```python
|
||||
//! from my_module import get_uuid_from_str, get_uuid
|
||||
//! import uuid
|
||||
//!
|
||||
//! py_uuid = uuid.uuid4()
|
||||
//!
|
||||
//! # Convert string to Rust Uuid
|
||||
//! rust_uuid = get_uuid_from_str(str(py_uuid))
|
||||
//! assert py_uuid == rust_uuid
|
||||
//!
|
||||
//! # Pass Python UUID directly to Rust
|
||||
//! returned_uuid = get_uuid(py_uuid)
|
||||
//! assert py_uuid == returned_uuid
|
||||
//! ```
|
||||
use uuid::{NonNilUuid, Uuid};
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::{PyTypeError, PyValueError};
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use crate::instance::Bound;
|
||||
use crate::sync::PyOnceLock;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_identifier;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::PyType;
|
||||
use crate::{intern, Borrowed, FromPyObject, Py, PyAny, PyErr, PyResult, Python};
|
||||
|
||||
fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> {
|
||||
static UUID_CLS: PyOnceLock<Py<PyType>> = PyOnceLock::new();
|
||||
UUID_CLS.import(py, "uuid", "UUID")
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for Uuid {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_identifier!("uuid", "UUID");
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
|
||||
let py = obj.py();
|
||||
let uuid_cls = get_uuid_cls(py)?;
|
||||
|
||||
if obj.is_instance(uuid_cls)? {
|
||||
let uuid_int: u128 = obj.getattr(intern!(py, "int"))?.extract()?;
|
||||
Ok(Uuid::from_u128(uuid_int))
|
||||
} else {
|
||||
Err(PyTypeError::new_err("Expected a `uuid.UUID` instance."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for Uuid {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = type_hint_identifier!("uuid", "UUID");
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
let uuid_cls = get_uuid_cls(py)?;
|
||||
|
||||
uuid_cls.call1((py.None(), py.None(), py.None(), py.None(), self.as_u128()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &Uuid {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Uuid::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for NonNilUuid {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = Uuid::INPUT_TYPE;
|
||||
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
|
||||
let uuid: Uuid = obj.extract()?;
|
||||
NonNilUuid::new(uuid).ok_or_else(|| PyValueError::new_err("UUID is nil"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for NonNilUuid {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = Uuid::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Uuid::from(self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &NonNilUuid {
|
||||
type Target = PyAny;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = NonNilUuid::OUTPUT_TYPE;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
(*self).into_pyobject(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::dict::PyDictMethods;
|
||||
use crate::types::PyDict;
|
||||
use alloc::ffi::CString;
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! convert_constants {
|
||||
($name:ident, $rs:expr, $py:literal) => {
|
||||
#[test]
|
||||
fn $name() -> PyResult<()> {
|
||||
Python::attach(|py| {
|
||||
let rs_orig = $rs;
|
||||
let rs_uuid = rs_orig.into_pyobject(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("rs_uuid", &rs_uuid).unwrap();
|
||||
|
||||
py.run(
|
||||
&CString::new(format!(
|
||||
"import uuid\npy_uuid = uuid.UUID('{}')\nassert py_uuid == rs_uuid",
|
||||
$py
|
||||
))
|
||||
.unwrap(),
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let py_uuid = locals.get_item("py_uuid").unwrap().unwrap();
|
||||
let py_result: Uuid = py_uuid.extract().unwrap();
|
||||
assert_eq!(rs_orig, py_result);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
convert_constants!(
|
||||
convert_nil,
|
||||
Uuid::nil(),
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
);
|
||||
convert_constants!(
|
||||
convert_max,
|
||||
Uuid::max(),
|
||||
"ffffffff-ffff-ffff-ffff-ffffffffffff"
|
||||
);
|
||||
|
||||
convert_constants!(
|
||||
convert_uuid_v4,
|
||||
Uuid::parse_str("a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00").unwrap(),
|
||||
"a4f6d1b9-1898-418f-b11d-ecc6fe1e1f00"
|
||||
);
|
||||
|
||||
convert_constants!(
|
||||
convert_uuid_v3,
|
||||
Uuid::parse_str("6fa459ea-ee8a-3ca4-894e-db77e160355e").unwrap(),
|
||||
"6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
||||
);
|
||||
|
||||
convert_constants!(
|
||||
convert_uuid_v1,
|
||||
Uuid::parse_str("a6cc5730-2261-11ee-9c43-2eb5a363657c").unwrap(),
|
||||
"a6cc5730-2261-11ee-9c43-2eb5a363657c"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_non_nil_uuid() {
|
||||
Python::attach(|py| {
|
||||
let rs_uuid = NonNilUuid::new(Uuid::max()).unwrap();
|
||||
let py_uuid = rs_uuid.into_pyobject(py).unwrap();
|
||||
|
||||
let extract_uuid: NonNilUuid = py_uuid.extract().unwrap();
|
||||
assert_eq!(extract_uuid, rs_uuid);
|
||||
|
||||
let nil_uuid = Uuid::nil().into_pyobject(py).unwrap();
|
||||
let extract_nil: PyResult<NonNilUuid> = nil_uuid.extract();
|
||||
assert!(extract_nil.is_err());
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
//! Python coroutine implementation, used notably when wrapping `async fn`
|
||||
//! with `#[pyfunction]`/`#[pymethods]`.
|
||||
use alloc::sync::Arc;
|
||||
use core::{
|
||||
future::Future,
|
||||
panic,
|
||||
pin::Pin,
|
||||
task::{Context, Poll, Waker},
|
||||
};
|
||||
|
||||
use pyo3_macros::{pyclass, pymethods};
|
||||
|
||||
use crate::{
|
||||
coroutine::{cancel::ThrowCallback, waker::AsyncioWaker},
|
||||
exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration},
|
||||
panic::PanicException,
|
||||
types::{string::PyStringMethods, PyIterator, PyString},
|
||||
Bound, Py, PyAny, PyErr, PyResult, Python,
|
||||
};
|
||||
|
||||
pub(crate) mod cancel;
|
||||
mod waker;
|
||||
|
||||
pub use cancel::CancelHandle;
|
||||
|
||||
const COROUTINE_REUSED_ERROR: &str = "cannot reuse already awaited coroutine";
|
||||
|
||||
/// Python coroutine wrapping a [`Future`].
|
||||
#[pyclass(crate = "crate")]
|
||||
pub struct Coroutine {
|
||||
name: Option<Py<PyString>>,
|
||||
qualname_prefix: Option<&'static str>,
|
||||
throw_callback: Option<ThrowCallback>,
|
||||
#[expect(clippy::type_complexity)]
|
||||
future: Option<Pin<Box<dyn Future<Output = PyResult<Py<PyAny>>> + Send>>>,
|
||||
waker: Option<Arc<AsyncioWaker>>,
|
||||
}
|
||||
|
||||
// Safety: `Coroutine` is allowed to be `Sync` even though the future is not,
|
||||
// because the future is polled with `&mut self` receiver
|
||||
unsafe impl Sync for Coroutine {}
|
||||
|
||||
impl Coroutine {
|
||||
/// Wrap a future into a Python coroutine.
|
||||
///
|
||||
/// Coroutine `send` polls the wrapped future, ignoring the value passed
|
||||
/// (should always be `None` anyway).
|
||||
///
|
||||
/// `Coroutine `throw` drop the wrapped future and reraise the exception passed
|
||||
pub(crate) fn new<'py, F>(
|
||||
name: Option<Bound<'py, PyString>>,
|
||||
qualname_prefix: Option<&'static str>,
|
||||
throw_callback: Option<ThrowCallback>,
|
||||
future: F,
|
||||
) -> Self
|
||||
where
|
||||
F: Future<Output = Result<Py<PyAny>, PyErr>> + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
name: name.map(Bound::unbind),
|
||||
qualname_prefix,
|
||||
throw_callback,
|
||||
future: Some(Box::pin(future)),
|
||||
waker: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self, py: Python<'_>, throw: Option<Py<PyAny>>) -> PyResult<Py<PyAny>> {
|
||||
// raise if the coroutine has already been run to completion
|
||||
let future_rs = match self.future {
|
||||
Some(ref mut fut) => fut,
|
||||
None => return Err(PyRuntimeError::new_err(COROUTINE_REUSED_ERROR)),
|
||||
};
|
||||
// reraise thrown exception it
|
||||
match (throw, &self.throw_callback) {
|
||||
(Some(exc), Some(cb)) => cb.throw(exc),
|
||||
(Some(exc), None) => {
|
||||
self.close();
|
||||
return Err(PyErr::from_value(exc.into_bound(py)));
|
||||
}
|
||||
(None, _) => {}
|
||||
}
|
||||
// create a new waker, or try to reset it in place
|
||||
if let Some(waker) = self.waker.as_mut().and_then(Arc::get_mut) {
|
||||
waker.reset();
|
||||
} else {
|
||||
self.waker = Some(Arc::new(AsyncioWaker::new()));
|
||||
}
|
||||
let waker = Waker::from(self.waker.clone().unwrap());
|
||||
// poll the Rust future and forward its results if ready
|
||||
// polling is UnwindSafe because the future is dropped in case of panic
|
||||
let poll = || future_rs.as_mut().poll(&mut Context::from_waker(&waker));
|
||||
match std::panic::catch_unwind(panic::AssertUnwindSafe(poll)) {
|
||||
Ok(Poll::Ready(res)) => {
|
||||
self.close();
|
||||
return Err(PyStopIteration::new_err((res?,)));
|
||||
}
|
||||
Err(err) => {
|
||||
self.close();
|
||||
return Err(PanicException::from_panic_payload(err));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// otherwise, initialize the waker `asyncio.Future`
|
||||
if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? {
|
||||
// `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__`
|
||||
// and will yield itself if its result has not been set in polling above
|
||||
if let Some(future) = PyIterator::from_object(future).unwrap().next() {
|
||||
// future has not been leaked into Python for now, and Rust code can only call
|
||||
// `set_result(None)` in `Wake` implementation, so it's safe to unwrap
|
||||
return Ok(future.unwrap().into());
|
||||
}
|
||||
}
|
||||
// if waker has been waken during future polling, this is roughly equivalent to
|
||||
// `await asyncio.sleep(0)`, so just yield `None`.
|
||||
Ok(py.None())
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods(crate = "crate")]
|
||||
impl Coroutine {
|
||||
#[getter]
|
||||
fn __name__(&self, py: Python<'_>) -> PyResult<Py<PyString>> {
|
||||
match &self.name {
|
||||
Some(name) => Ok(name.clone_ref(py)),
|
||||
None => Err(PyAttributeError::new_err("__name__")),
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn __qualname__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
|
||||
match (&self.name, &self.qualname_prefix) {
|
||||
(Some(name), Some(prefix)) => Ok(PyString::new(
|
||||
py,
|
||||
&format!("{}.{}", prefix, name.bind(py).to_cow()?),
|
||||
)),
|
||||
(Some(name), None) => Ok(name.bind(py).clone()),
|
||||
(None, _) => Err(PyAttributeError::new_err("__qualname__")),
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&mut self, py: Python<'_>, _value: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
|
||||
self.poll(py, None)
|
||||
}
|
||||
|
||||
fn throw(&mut self, py: Python<'_>, exc: Py<PyAny>) -> PyResult<Py<PyAny>> {
|
||||
self.poll(py, Some(exc))
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
// the Rust future is dropped, and the field set to `None`
|
||||
// to indicate the coroutine has been run to completion
|
||||
drop(self.future.take());
|
||||
}
|
||||
|
||||
fn __await__(self_: Py<Self>) -> Py<Self> {
|
||||
self_
|
||||
}
|
||||
|
||||
fn __next__(&mut self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self.poll(py, None)
|
||||
}
|
||||
}
|
||||
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
use crate::{Py, PyAny};
|
||||
use alloc::sync::Arc;
|
||||
use core::future::poll_fn;
|
||||
use core::task::{Context, Poll, Waker};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
exception: Option<Py<PyAny>>,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
/// Helper used to wait and retrieve exception thrown in [`Coroutine`](super::Coroutine).
|
||||
///
|
||||
/// Only the last exception thrown can be retrieved.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CancelHandle(Arc<Mutex<Inner>>);
|
||||
|
||||
impl CancelHandle {
|
||||
/// Create a new `CoroutineCancel`.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Returns whether the associated coroutine has been cancelled.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.0.lock().unwrap().exception.is_some()
|
||||
}
|
||||
|
||||
/// Poll to retrieve the exception thrown in the associated coroutine.
|
||||
pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll<Py<PyAny>> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
if let Some(exc) = inner.exception.take() {
|
||||
return Poll::Ready(exc);
|
||||
}
|
||||
if let Some(ref waker) = inner.waker {
|
||||
if cx.waker().will_wake(waker) {
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
inner.waker = Some(cx.waker().clone());
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
/// Retrieve the exception thrown in the associated coroutine.
|
||||
pub async fn cancelled(&mut self) -> Py<PyAny> {
|
||||
poll_fn(|cx| self.poll_cancelled(cx)).await
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn throw_callback(&self) -> ThrowCallback {
|
||||
ThrowCallback(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct ThrowCallback(Arc<Mutex<Inner>>);
|
||||
|
||||
impl ThrowCallback {
|
||||
pub(super) fn throw(&self, exc: Py<PyAny>) {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.exception = Some(exc);
|
||||
if let Some(waker) = inner.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+106
@@ -0,0 +1,106 @@
|
||||
use crate::sync::PyOnceLock;
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::types::PyCFunction;
|
||||
use crate::{intern, wrap_pyfunction, Bound, Py, PyAny, PyResult, Python};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::task::Wake;
|
||||
use pyo3_macros::pyfunction;
|
||||
|
||||
/// Lazy `asyncio.Future` wrapper, implementing [`Wake`] by calling `Future.set_result`.
|
||||
///
|
||||
/// asyncio future is let uninitialized until [`initialize_future`][1] is called.
|
||||
/// If [`wake`][2] is called before future initialization (during Rust future polling),
|
||||
/// [`initialize_future`][1] will return `None` (it is roughly equivalent to `asyncio.sleep(0)`)
|
||||
///
|
||||
/// [1]: AsyncioWaker::initialize_future
|
||||
/// [2]: AsyncioWaker::wake
|
||||
pub struct AsyncioWaker(PyOnceLock<Option<LoopAndFuture>>);
|
||||
|
||||
impl AsyncioWaker {
|
||||
pub(super) fn new() -> Self {
|
||||
Self(PyOnceLock::new())
|
||||
}
|
||||
|
||||
pub(super) fn reset(&mut self) {
|
||||
self.0.take();
|
||||
}
|
||||
|
||||
pub(super) fn initialize_future<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<Option<&Bound<'py, PyAny>>> {
|
||||
let init = || LoopAndFuture::new(py).map(Some);
|
||||
let loop_and_future = self.0.get_or_try_init(py, init)?.as_ref();
|
||||
Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.bind(py)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Wake for AsyncioWaker {
|
||||
fn wake(self: Arc<Self>) {
|
||||
self.wake_by_ref()
|
||||
}
|
||||
|
||||
fn wake_by_ref(self: &Arc<Self>) {
|
||||
Python::attach(|py| {
|
||||
if let Some(loop_and_future) = self.0.get_or_init(py, || None) {
|
||||
loop_and_future
|
||||
.set_result(py)
|
||||
.expect("unexpected error in coroutine waker");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct LoopAndFuture {
|
||||
event_loop: Py<PyAny>,
|
||||
future: Py<PyAny>,
|
||||
}
|
||||
|
||||
impl LoopAndFuture {
|
||||
fn new(py: Python<'_>) -> PyResult<Self> {
|
||||
static GET_RUNNING_LOOP: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
|
||||
let import = || -> PyResult<_> {
|
||||
let module = py.import("asyncio")?;
|
||||
Ok(module.getattr("get_running_loop")?.into())
|
||||
};
|
||||
let event_loop = GET_RUNNING_LOOP.get_or_try_init(py, import)?.call0(py)?;
|
||||
let future = event_loop.call_method0(py, "create_future")?;
|
||||
Ok(Self { event_loop, future })
|
||||
}
|
||||
|
||||
fn set_result(&self, py: Python<'_>) -> PyResult<()> {
|
||||
static RELEASE_WAITER: PyOnceLock<Py<PyCFunction>> = PyOnceLock::new();
|
||||
let release_waiter = RELEASE_WAITER.get_or_try_init(py, || {
|
||||
wrap_pyfunction!(release_waiter, py).map(Bound::unbind)
|
||||
})?;
|
||||
// `Future.set_result` must be called in event loop thread,
|
||||
// so it requires `call_soon_threadsafe`
|
||||
let call_soon_threadsafe = self.event_loop.call_method1(
|
||||
py,
|
||||
intern!(py, "call_soon_threadsafe"),
|
||||
(release_waiter, self.future.bind(py)),
|
||||
);
|
||||
if let Err(err) = call_soon_threadsafe {
|
||||
// `call_soon_threadsafe` will raise if the event loop is closed;
|
||||
// instead of catching an unspecific `RuntimeError`, check directly if it's closed.
|
||||
let is_closed = self.event_loop.call_method0(py, "is_closed")?;
|
||||
if !is_closed.extract(py)? {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Call `future.set_result` if the future is not done.
|
||||
///
|
||||
/// Future can be cancelled by the event loop before being waken.
|
||||
/// See <https://github.com/python/cpython/blob/main/Lib/asyncio/tasks.py#L452C5-L452C5>
|
||||
#[pyfunction(crate = "crate")]
|
||||
fn release_waiter(future: &Bound<'_, PyAny>) -> PyResult<()> {
|
||||
let done = future.call_method0(intern!(future.py(), "done"))?;
|
||||
if !done.extract::<bool>()? {
|
||||
future.call_method1(intern!(future.py(), "set_result"), (future.py().None(),))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+265
@@ -0,0 +1,265 @@
|
||||
use alloc::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
exceptions,
|
||||
types::{
|
||||
PyAnyMethods, PyNone, PyStringMethods, PyTuple, PyTupleMethods, PyType, PyTypeMethods,
|
||||
},
|
||||
Borrowed, Bound, IntoPyObjectExt, Py, PyAny, PyErr, PyErrArguments, PyTypeInfo, Python,
|
||||
};
|
||||
|
||||
/// Error that indicates an object was not an instance of a given target type.
|
||||
#[derive(Debug)]
|
||||
pub struct CastError<'a, 'py> {
|
||||
/// The original object that failed the `isinstance` check.
|
||||
from: Borrowed<'a, 'py, PyAny>,
|
||||
/// The type we tried (and failed) to convert to.
|
||||
/// (see `PyTypeCheck::classinfo_object`)
|
||||
classinfo: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
impl<'a, 'py> CastError<'a, 'py> {
|
||||
/// Create a new `CastError` representing a failure to interpret a smart pointer to
|
||||
/// `from` as a type from the given `classinfo`.
|
||||
///
|
||||
/// As with [`PyTypeCheck::classinfo_object`][crate::PyTypeCheck::classinfo_object],
|
||||
/// valid `classinfo` values are those which can be used with `isinstance`, such as `type`
|
||||
/// objects, tuples of `type` objects, or `typing.Union` instances.
|
||||
#[inline]
|
||||
pub fn new(from: Borrowed<'a, 'py, PyAny>, classinfo: Bound<'py, PyAny>) -> Self {
|
||||
Self { from, classinfo }
|
||||
}
|
||||
}
|
||||
|
||||
/// Equivalent to [`CastError`] for operations where the smart pointer cast transfers ownership
|
||||
/// of the original object.
|
||||
///
|
||||
/// The original object can be retrieved using [`into_inner`][CastIntoError::into_inner].
|
||||
#[derive(Debug)]
|
||||
pub struct CastIntoError<'py> {
|
||||
from: Bound<'py, PyAny>,
|
||||
classinfo: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
impl<'py> CastIntoError<'py> {
|
||||
/// Create a new `CastError` representing a failure to interpret a smart pointer to
|
||||
/// `from` as a type from the given `classinfo`.
|
||||
///
|
||||
/// Equivalent to [`CastError::new`] for owned objects.
|
||||
#[inline]
|
||||
pub fn new(from: Bound<'py, PyAny>, classinfo: Bound<'py, PyAny>) -> Self {
|
||||
Self { from, classinfo }
|
||||
}
|
||||
|
||||
/// Consumes this `CastIntoError` and returns the original object, allowing continued
|
||||
/// use of it after a failed conversion.
|
||||
///
|
||||
/// See [`cast_into`][Bound::cast_into] for an example.
|
||||
pub fn into_inner(self) -> Bound<'py, PyAny> {
|
||||
self.from
|
||||
}
|
||||
}
|
||||
|
||||
struct CastErrorArguments {
|
||||
from: Py<PyAny>,
|
||||
classinfo: Py<PyAny>,
|
||||
}
|
||||
|
||||
impl PyErrArguments for CastErrorArguments {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
format!(
|
||||
"{}",
|
||||
DisplayCastError {
|
||||
from: &self.from.into_bound(py),
|
||||
classinfo: &self.classinfo.into_bound(py),
|
||||
}
|
||||
)
|
||||
.into_py_any(py)
|
||||
.expect("failed to create Python string")
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `CastError` to Python `TypeError`.
|
||||
impl core::convert::From<CastError<'_, '_>> for PyErr {
|
||||
fn from(err: CastError<'_, '_>) -> PyErr {
|
||||
let args = CastErrorArguments {
|
||||
from: err.from.to_owned().unbind(),
|
||||
classinfo: err.classinfo.unbind(),
|
||||
};
|
||||
|
||||
exceptions::PyTypeError::new_err(args)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for CastError<'_, '_> {}
|
||||
|
||||
impl core::fmt::Display for CastError<'_, '_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||
DisplayCastError {
|
||||
from: &self.from,
|
||||
classinfo: &self.classinfo,
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `CastIntoError` to Python `TypeError`.
|
||||
impl core::convert::From<CastIntoError<'_>> for PyErr {
|
||||
fn from(err: CastIntoError<'_>) -> PyErr {
|
||||
let args = CastErrorArguments {
|
||||
from: err.from.to_owned().unbind(),
|
||||
classinfo: err.classinfo.unbind(),
|
||||
};
|
||||
|
||||
exceptions::PyTypeError::new_err(args)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for CastIntoError<'_> {}
|
||||
|
||||
impl core::fmt::Display for CastIntoError<'_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
||||
DisplayCastError {
|
||||
from: &self.from.to_owned(),
|
||||
classinfo: &self.classinfo,
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayCastError<'a, 'py> {
|
||||
from: &'a Bound<'py, PyAny>,
|
||||
classinfo: &'a Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for DisplayCastError<'_, '_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let to = DisplayClassInfo(self.classinfo);
|
||||
if self.from.is_none() {
|
||||
write!(f, "'None' is not an instance of '{to}'")
|
||||
} else {
|
||||
let from = self.from.get_type().qualname();
|
||||
let from = from
|
||||
.as_ref()
|
||||
.map(|name| name.to_string_lossy())
|
||||
.unwrap_or(Cow::Borrowed("<failed to extract type name>"));
|
||||
write!(f, "'{from}' object is not an instance of '{to}'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayClassInfo<'a, 'py>(&'a Bound<'py, PyAny>);
|
||||
|
||||
impl core::fmt::Display for DisplayClassInfo<'_, '_> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
if let Ok(t) = self.0.cast::<PyType>() {
|
||||
if t.is(PyNone::type_object(t.py())) {
|
||||
f.write_str("None")
|
||||
} else {
|
||||
t.qualname()
|
||||
.map_err(|_| core::fmt::Error)?
|
||||
.to_string_lossy()
|
||||
.fmt(f)
|
||||
}
|
||||
} else if let Ok(t) = self.0.cast::<PyTuple>() {
|
||||
for (i, t) in t.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(" | ")?;
|
||||
}
|
||||
write!(f, "{}", DisplayClassInfo(&t))?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
types::{PyBool, PyString},
|
||||
PyTypeInfo,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_display_cast_error() {
|
||||
Python::attach(|py| {
|
||||
let obj = PyBool::new(py, true).to_any();
|
||||
let classinfo = py.get_type::<PyString>().into_any();
|
||||
let err = CastError::new(obj, classinfo);
|
||||
assert_eq!(err.to_string(), "'bool' object is not an instance of 'str'");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_cast_error_with_none() {
|
||||
Python::attach(|py| {
|
||||
let obj = py.None().into_bound(py);
|
||||
let classinfo = py.get_type::<PyString>().into_any();
|
||||
let err = CastError::new(obj.as_borrowed(), classinfo);
|
||||
assert_eq!(err.to_string(), "'None' is not an instance of 'str'");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_cast_error_with_tuple() {
|
||||
Python::attach(|py| {
|
||||
let obj = PyBool::new(py, true).to_any();
|
||||
let classinfo = PyTuple::new(
|
||||
py,
|
||||
&[
|
||||
py.get_type::<PyString>().into_any(),
|
||||
crate::types::PyNone::type_object(py).into_any(),
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
.into_any();
|
||||
let err = CastError::new(obj, classinfo);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"'bool' object is not an instance of 'str | None'"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_cast_into_error() {
|
||||
Python::attach(|py| {
|
||||
let obj = PyBool::new(py, true).to_any();
|
||||
let classinfo = py.get_type::<PyString>().into_any();
|
||||
let err = CastIntoError::new(obj.to_owned(), classinfo);
|
||||
assert_eq!(err.to_string(), "'bool' object is not an instance of 'str'");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyerr_from_cast_error() {
|
||||
Python::attach(|py| {
|
||||
let obj = PyBool::new(py, true).to_any();
|
||||
let classinfo = py.get_type::<PyString>().into_any();
|
||||
let err = CastError::new(obj, classinfo);
|
||||
let py_err: PyErr = err.into();
|
||||
assert_eq!(
|
||||
py_err.to_string(),
|
||||
"TypeError: 'bool' object is not an instance of 'str'"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyerr_from_cast_into_error() {
|
||||
Python::attach(|py| {
|
||||
let obj = PyBool::new(py, true).to_any();
|
||||
let classinfo = py.get_type::<PyString>().into_any();
|
||||
let err = CastIntoError::new(obj.to_owned(), classinfo);
|
||||
let py_err: PyErr = err.into();
|
||||
assert_eq!(
|
||||
py_err.to_string(),
|
||||
"TypeError: 'bool' object is not an instance of 'str'"
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+484
@@ -0,0 +1,484 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use std::{
|
||||
sync::{Mutex, Once},
|
||||
thread::ThreadId,
|
||||
};
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
use crate::sync::MutexExt;
|
||||
use crate::{
|
||||
exceptions::{PyBaseException, PyTypeError},
|
||||
ffi,
|
||||
ffi_ptr_ext::FfiPtrExt,
|
||||
types::{PyAnyMethods, PyTraceback, PyType},
|
||||
Bound, Py, PyAny, PyErrArguments, PyTypeInfo, Python,
|
||||
};
|
||||
|
||||
pub(crate) struct PyErrState {
|
||||
// Safety: can only hand out references when in the "normalized" state. Will never change
|
||||
// after normalization.
|
||||
normalized: Once,
|
||||
// Guard against re-entrancy when normalizing the exception state.
|
||||
normalizing_thread: Mutex<Option<ThreadId>>,
|
||||
inner: UnsafeCell<Option<PyErrStateInner>>,
|
||||
}
|
||||
|
||||
// Safety: The inner value is protected by locking to ensure that only the normalized state is
|
||||
// handed out as a reference.
|
||||
unsafe impl Send for PyErrState {}
|
||||
unsafe impl Sync for PyErrState {}
|
||||
#[cfg(feature = "nightly")]
|
||||
unsafe impl crate::marker::Ungil for PyErrState {}
|
||||
|
||||
impl PyErrState {
|
||||
pub(crate) fn lazy(f: Box<PyErrStateLazyFn>) -> Self {
|
||||
Self::from_inner(PyErrStateInner::Lazy(f))
|
||||
}
|
||||
|
||||
pub(crate) fn lazy_arguments(ptype: Py<PyAny>, args: impl PyErrArguments + 'static) -> Self {
|
||||
Self::from_inner(PyErrStateInner::Lazy(Box::new(move |py| {
|
||||
PyErrStateLazyFnOutput {
|
||||
ptype,
|
||||
pvalue: args.arguments(py),
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self {
|
||||
let state = Self::from_inner(PyErrStateInner::Normalized(normalized));
|
||||
// This state is already normalized, by completing the Once immediately we avoid
|
||||
// reaching the `py.detach` in `make_normalized` which is less efficient
|
||||
// and introduces a GIL switch which could deadlock.
|
||||
// See https://github.com/PyO3/pyo3/issues/4764
|
||||
state.normalized.call_once(|| {});
|
||||
state
|
||||
}
|
||||
|
||||
pub(crate) fn restore(self, py: Python<'_>) {
|
||||
self.inner
|
||||
.into_inner()
|
||||
.expect("PyErr state should never be invalid outside of normalization")
|
||||
.restore(py)
|
||||
}
|
||||
|
||||
fn from_inner(inner: PyErrStateInner) -> Self {
|
||||
Self {
|
||||
normalized: Once::new(),
|
||||
normalizing_thread: Mutex::new(None),
|
||||
inner: UnsafeCell::new(Some(inner)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn as_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized {
|
||||
if self.normalized.is_completed() {
|
||||
match unsafe {
|
||||
// Safety: self.inner will never be written again once normalized.
|
||||
&*self.inner.get()
|
||||
} {
|
||||
Some(PyErrStateInner::Normalized(n)) => return n,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
self.make_normalized(py)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn make_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized {
|
||||
// This process is safe because:
|
||||
// - Write happens only once, and then never will change again.
|
||||
// - The `Once` ensure that only one thread will do the write.
|
||||
|
||||
// Guard against re-entrant normalization, because `Once` does not provide
|
||||
// re-entrancy guarantees.
|
||||
if let Some(thread) = self.normalizing_thread.lock().unwrap().as_ref() {
|
||||
assert!(
|
||||
!(*thread == std::thread::current().id()),
|
||||
"Re-entrant normalization of PyErrState detected"
|
||||
);
|
||||
}
|
||||
|
||||
// avoid deadlock of `.call_once` with the GIL
|
||||
py.detach(|| {
|
||||
self.normalized.call_once(|| {
|
||||
self.normalizing_thread
|
||||
.lock()
|
||||
.unwrap()
|
||||
.replace(std::thread::current().id());
|
||||
|
||||
// Safety: no other thread can access the inner value while we are normalizing it.
|
||||
let state = unsafe {
|
||||
(*self.inner.get())
|
||||
.take()
|
||||
.expect("Cannot normalize a PyErr while already normalizing it.")
|
||||
};
|
||||
|
||||
let normalized_state =
|
||||
Python::attach(|py| PyErrStateInner::Normalized(state.normalize(py)));
|
||||
|
||||
// Safety: no other thread can access the inner value while we are normalizing it.
|
||||
unsafe {
|
||||
*self.inner.get() = Some(normalized_state);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
match unsafe {
|
||||
// Safety: self.inner will never be written again once normalized.
|
||||
&*self.inner.get()
|
||||
} {
|
||||
Some(PyErrStateInner::Normalized(n)) => n,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PyErrStateNormalized {
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptype: Py<PyType>,
|
||||
pub pvalue: Py<PyBaseException>,
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptraceback: std::sync::Mutex<Option<Py<PyTraceback>>>,
|
||||
}
|
||||
|
||||
impl PyErrStateNormalized {
|
||||
pub(crate) fn new(pvalue: Bound<'_, PyBaseException>) -> Self {
|
||||
Self {
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptype: pvalue.get_type().into(),
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptraceback: unsafe {
|
||||
Mutex::new(
|
||||
ffi::PyException_GetTraceback(pvalue.as_ptr())
|
||||
.assume_owned_or_opt(pvalue.py())
|
||||
.map(|b| b.cast_into_unchecked().unbind()),
|
||||
)
|
||||
},
|
||||
pvalue: pvalue.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> {
|
||||
self.ptype.bind(py).clone()
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> {
|
||||
self.pvalue.bind(py).get_type()
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
pub(crate) fn ptraceback<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyTraceback>> {
|
||||
self.ptraceback
|
||||
.lock_py_attached(py)
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|traceback| traceback.bind(py).clone())
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
pub(crate) fn ptraceback<'py>(&self, py: Python<'py>) -> Option<Bound<'py, PyTraceback>> {
|
||||
unsafe {
|
||||
ffi::PyException_GetTraceback(self.pvalue.as_ptr())
|
||||
.assume_owned_or_opt(py)
|
||||
.map(|b| b.cast_into_unchecked())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
pub(crate) fn set_ptraceback<'py>(&self, py: Python<'py>, tb: Option<Bound<'py, PyTraceback>>) {
|
||||
*self.ptraceback.lock_py_attached(py).unwrap() = tb.map(Bound::unbind);
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
pub(crate) fn set_ptraceback<'py>(&self, py: Python<'py>, tb: Option<Bound<'py, PyTraceback>>) {
|
||||
let tb = tb
|
||||
.as_ref()
|
||||
.map(Bound::as_ptr)
|
||||
.unwrap_or_else(|| crate::types::PyNone::get(py).as_ptr());
|
||||
|
||||
unsafe { ffi::PyException_SetTraceback(self.pvalue.as_ptr(), tb) };
|
||||
}
|
||||
|
||||
pub(crate) fn take(py: Python<'_>) -> Option<PyErrStateNormalized> {
|
||||
#[cfg(Py_3_12)]
|
||||
{
|
||||
// Safety: PyErr_GetRaisedException can be called when attached to Python and
|
||||
// returns either NULL or an owned reference.
|
||||
unsafe { ffi::PyErr_GetRaisedException().assume_owned_or_opt(py) }.map(|pvalue| {
|
||||
PyErrStateNormalized {
|
||||
// Safety: PyErr_GetRaisedException returns a valid exception type.
|
||||
pvalue: unsafe { pvalue.cast_into_unchecked() }.unbind(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
{
|
||||
let (ptype, pvalue, ptraceback) = unsafe {
|
||||
let mut ptype: *mut ffi::PyObject = core::ptr::null_mut();
|
||||
let mut pvalue: *mut ffi::PyObject = core::ptr::null_mut();
|
||||
let mut ptraceback: *mut ffi::PyObject = core::ptr::null_mut();
|
||||
|
||||
ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback);
|
||||
|
||||
// Ensure that the exception coming from the interpreter is normalized.
|
||||
if !ptype.is_null() {
|
||||
ffi::PyErr_NormalizeException(&mut ptype, &mut pvalue, &mut ptraceback);
|
||||
}
|
||||
|
||||
// Safety: PyErr_NormalizeException will have produced up to three owned
|
||||
// references of the correct types.
|
||||
(
|
||||
ptype
|
||||
.assume_owned_or_opt(py)
|
||||
.map(|b| b.cast_into_unchecked()),
|
||||
pvalue
|
||||
.assume_owned_or_opt(py)
|
||||
.map(|b| b.cast_into_unchecked()),
|
||||
ptraceback
|
||||
.assume_owned_or_opt(py)
|
||||
.map(|b| b.cast_into_unchecked()),
|
||||
)
|
||||
};
|
||||
|
||||
ptype.map(|ptype| PyErrStateNormalized {
|
||||
ptype: ptype.unbind(),
|
||||
pvalue: pvalue.expect("normalized exception value missing").unbind(),
|
||||
ptraceback: std::sync::Mutex::new(ptraceback.map(Bound::unbind)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
unsafe fn from_normalized_ffi_tuple(
|
||||
py: Python<'_>,
|
||||
ptype: *mut ffi::PyObject,
|
||||
pvalue: *mut ffi::PyObject,
|
||||
ptraceback: *mut ffi::PyObject,
|
||||
) -> Self {
|
||||
PyErrStateNormalized {
|
||||
ptype: unsafe {
|
||||
ptype
|
||||
.assume_owned_or_opt(py)
|
||||
.expect("Exception type missing")
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
.unbind(),
|
||||
pvalue: unsafe {
|
||||
pvalue
|
||||
.assume_owned_or_opt(py)
|
||||
.expect("Exception value missing")
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
.unbind(),
|
||||
ptraceback: Mutex::new(
|
||||
unsafe { ptraceback.assume_owned_or_opt(py) }
|
||||
.map(|b| unsafe { b.cast_into_unchecked() }.unbind()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_ref(&self, py: Python<'_>) -> Self {
|
||||
Self {
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptype: self.ptype.clone_ref(py),
|
||||
pvalue: self.pvalue.clone_ref(py),
|
||||
#[cfg(not(Py_3_12))]
|
||||
ptraceback: std::sync::Mutex::new(
|
||||
self.ptraceback
|
||||
.lock_py_attached(py)
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|ptraceback| ptraceback.clone_ref(py)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PyErrStateLazyFnOutput {
|
||||
pub(crate) ptype: Py<PyAny>,
|
||||
pub(crate) pvalue: Py<PyAny>,
|
||||
}
|
||||
|
||||
pub(crate) type PyErrStateLazyFn =
|
||||
dyn for<'py> FnOnce(Python<'py>) -> PyErrStateLazyFnOutput + Send + Sync;
|
||||
|
||||
enum PyErrStateInner {
|
||||
Lazy(Box<PyErrStateLazyFn>),
|
||||
Normalized(PyErrStateNormalized),
|
||||
}
|
||||
|
||||
impl PyErrStateInner {
|
||||
fn normalize(self, py: Python<'_>) -> PyErrStateNormalized {
|
||||
match self {
|
||||
#[cfg(not(Py_3_12))]
|
||||
PyErrStateInner::Lazy(lazy) => {
|
||||
let (ptype, pvalue, ptraceback) = lazy_into_normalized_ffi_tuple(py, lazy);
|
||||
unsafe {
|
||||
PyErrStateNormalized::from_normalized_ffi_tuple(py, ptype, pvalue, ptraceback)
|
||||
}
|
||||
}
|
||||
#[cfg(Py_3_12)]
|
||||
PyErrStateInner::Lazy(lazy) => {
|
||||
// To keep the implementation simple, just write the exception into the interpreter,
|
||||
// which will cause it to be normalized
|
||||
raise_lazy(py, lazy);
|
||||
PyErrStateNormalized::take(py)
|
||||
.expect("exception missing after writing to the interpreter")
|
||||
}
|
||||
PyErrStateInner::Normalized(normalized) => normalized,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
fn restore(self, py: Python<'_>) {
|
||||
let (ptype, pvalue, ptraceback) = match self {
|
||||
PyErrStateInner::Lazy(lazy) => lazy_into_normalized_ffi_tuple(py, lazy),
|
||||
PyErrStateInner::Normalized(PyErrStateNormalized {
|
||||
ptype,
|
||||
pvalue,
|
||||
ptraceback,
|
||||
}) => (
|
||||
ptype.into_ptr(),
|
||||
pvalue.into_ptr(),
|
||||
ptraceback
|
||||
.into_inner()
|
||||
.unwrap()
|
||||
.map_or(core::ptr::null_mut(), Py::into_ptr),
|
||||
),
|
||||
};
|
||||
unsafe { ffi::PyErr_Restore(ptype, pvalue, ptraceback) }
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
fn restore(self, py: Python<'_>) {
|
||||
match self {
|
||||
PyErrStateInner::Lazy(lazy) => raise_lazy(py, lazy),
|
||||
PyErrStateInner::Normalized(PyErrStateNormalized { pvalue }) => unsafe {
|
||||
ffi::PyErr_SetRaisedException(pvalue.into_ptr())
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
fn lazy_into_normalized_ffi_tuple(
|
||||
py: Python<'_>,
|
||||
lazy: Box<PyErrStateLazyFn>,
|
||||
) -> (*mut ffi::PyObject, *mut ffi::PyObject, *mut ffi::PyObject) {
|
||||
// To be consistent with 3.12 logic, go via raise_lazy, but also then normalize
|
||||
// the resulting exception
|
||||
raise_lazy(py, lazy);
|
||||
let mut ptype = core::ptr::null_mut();
|
||||
let mut pvalue = core::ptr::null_mut();
|
||||
let mut ptraceback = core::ptr::null_mut();
|
||||
unsafe {
|
||||
ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback);
|
||||
ffi::PyErr_NormalizeException(&mut ptype, &mut pvalue, &mut ptraceback);
|
||||
}
|
||||
(ptype, pvalue, ptraceback)
|
||||
}
|
||||
|
||||
/// Raises a "lazy" exception state into the Python interpreter.
|
||||
///
|
||||
/// In principle this could be split in two; first a function to create an exception
|
||||
/// in a normalized state, and then a call to `PyErr_SetRaisedException` to raise it.
|
||||
///
|
||||
/// This would require either moving some logic from C to Rust, or requesting a new
|
||||
/// API in CPython.
|
||||
fn raise_lazy(py: Python<'_>, lazy: Box<PyErrStateLazyFn>) {
|
||||
let PyErrStateLazyFnOutput { ptype, pvalue } = lazy(py);
|
||||
unsafe {
|
||||
if ffi::PyExceptionClass_Check(ptype.as_ptr()) == 0 {
|
||||
ffi::PyErr_SetString(
|
||||
PyTypeError::type_object_raw(py).cast(),
|
||||
c"exceptions must derive from BaseException".as_ptr(),
|
||||
)
|
||||
} else {
|
||||
ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
exceptions::PyValueError, sync::PyOnceLock, Py, PyAny, PyErr, PyErrArguments, Python,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Re-entrant normalization of PyErrState detected")]
|
||||
fn test_reentrant_normalization() {
|
||||
static ERR: PyOnceLock<PyErr> = PyOnceLock::new();
|
||||
|
||||
struct RecursiveArgs;
|
||||
|
||||
impl PyErrArguments for RecursiveArgs {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
// .value(py) triggers normalization
|
||||
ERR.get(py)
|
||||
.expect("is set just below")
|
||||
.value(py)
|
||||
.clone()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
ERR.set(py, PyValueError::new_err(RecursiveArgs)).unwrap();
|
||||
ERR.get(py).expect("is set just above").value(py);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
|
||||
fn test_no_deadlock_thread_switch() {
|
||||
static ERR: PyOnceLock<PyErr> = PyOnceLock::new();
|
||||
|
||||
struct GILSwitchArgs;
|
||||
|
||||
impl PyErrArguments for GILSwitchArgs {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
// releasing the GIL potentially allows for other threads to deadlock
|
||||
// with the normalization going on here
|
||||
py.detach(|| {
|
||||
std::thread::sleep(core::time::Duration::from_millis(10));
|
||||
});
|
||||
py.None()
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| ERR.set(py, PyValueError::new_err(GILSwitchArgs)).unwrap());
|
||||
|
||||
// Let many threads attempt to read the normalized value at the same time
|
||||
let handles = (0..10)
|
||||
.map(|_| {
|
||||
std::thread::spawn(|| {
|
||||
Python::attach(|py| {
|
||||
ERR.get(py).expect("is set just above").value(py);
|
||||
});
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
// We should never have deadlocked, and should be able to run
|
||||
// this assertion
|
||||
Python::attach(|py| {
|
||||
assert!(ERR
|
||||
.get(py)
|
||||
.expect("is set above")
|
||||
.is_instance_of::<PyValueError>(py))
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+348
@@ -0,0 +1,348 @@
|
||||
use crate::{err::PyErrArguments, exceptions, types, PyErr, Python};
|
||||
use crate::{IntoPyObject, Py, PyAny};
|
||||
use std::io;
|
||||
|
||||
/// Convert `PyErr` to `io::Error`
|
||||
impl From<PyErr> for io::Error {
|
||||
fn from(err: PyErr) -> Self {
|
||||
let kind = Python::attach(|py| {
|
||||
if err.is_instance_of::<exceptions::PyBrokenPipeError>(py) {
|
||||
io::ErrorKind::BrokenPipe
|
||||
} else if err.is_instance_of::<exceptions::PyConnectionRefusedError>(py) {
|
||||
io::ErrorKind::ConnectionRefused
|
||||
} else if err.is_instance_of::<exceptions::PyConnectionAbortedError>(py) {
|
||||
io::ErrorKind::ConnectionAborted
|
||||
} else if err.is_instance_of::<exceptions::PyConnectionResetError>(py) {
|
||||
io::ErrorKind::ConnectionReset
|
||||
} else if err.is_instance_of::<exceptions::PyInterruptedError>(py) {
|
||||
io::ErrorKind::Interrupted
|
||||
} else if err.is_instance_of::<exceptions::PyFileNotFoundError>(py) {
|
||||
io::ErrorKind::NotFound
|
||||
} else if err.is_instance_of::<exceptions::PyPermissionError>(py) {
|
||||
io::ErrorKind::PermissionDenied
|
||||
} else if err.is_instance_of::<exceptions::PyFileExistsError>(py) {
|
||||
io::ErrorKind::AlreadyExists
|
||||
} else if err.is_instance_of::<exceptions::PyBlockingIOError>(py) {
|
||||
io::ErrorKind::WouldBlock
|
||||
} else if err.is_instance_of::<exceptions::PyTimeoutError>(py) {
|
||||
io::ErrorKind::TimedOut
|
||||
} else if err.is_instance_of::<exceptions::PyMemoryError>(py) {
|
||||
io::ErrorKind::OutOfMemory
|
||||
} else if err.is_instance_of::<exceptions::PyIsADirectoryError>(py) {
|
||||
io::ErrorKind::IsADirectory
|
||||
} else if err.is_instance_of::<exceptions::PyNotADirectoryError>(py) {
|
||||
io::ErrorKind::NotADirectory
|
||||
} else {
|
||||
io::ErrorKind::Other
|
||||
}
|
||||
});
|
||||
io::Error::new(kind, err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `PyErr` from `io::Error`
|
||||
/// (`OSError` except if the `io::Error` is wrapping a Python exception,
|
||||
/// in this case the exception is returned)
|
||||
impl From<io::Error> for PyErr {
|
||||
fn from(err: io::Error) -> PyErr {
|
||||
// If the error wraps a Python error we return it
|
||||
if err.get_ref().is_some_and(|e| e.is::<PyErr>()) {
|
||||
return *err.into_inner().unwrap().downcast().unwrap();
|
||||
}
|
||||
match err.kind() {
|
||||
io::ErrorKind::BrokenPipe => exceptions::PyBrokenPipeError::new_err(err),
|
||||
io::ErrorKind::ConnectionRefused => exceptions::PyConnectionRefusedError::new_err(err),
|
||||
io::ErrorKind::ConnectionAborted => exceptions::PyConnectionAbortedError::new_err(err),
|
||||
io::ErrorKind::ConnectionReset => exceptions::PyConnectionResetError::new_err(err),
|
||||
io::ErrorKind::Interrupted => exceptions::PyInterruptedError::new_err(err),
|
||||
io::ErrorKind::NotFound => exceptions::PyFileNotFoundError::new_err(err),
|
||||
io::ErrorKind::PermissionDenied => exceptions::PyPermissionError::new_err(err),
|
||||
io::ErrorKind::AlreadyExists => exceptions::PyFileExistsError::new_err(err),
|
||||
io::ErrorKind::WouldBlock => exceptions::PyBlockingIOError::new_err(err),
|
||||
io::ErrorKind::TimedOut => exceptions::PyTimeoutError::new_err(err),
|
||||
io::ErrorKind::OutOfMemory => exceptions::PyMemoryError::new_err(err),
|
||||
io::ErrorKind::IsADirectory => exceptions::PyIsADirectoryError::new_err(err),
|
||||
io::ErrorKind::NotADirectory => exceptions::PyNotADirectoryError::new_err(err),
|
||||
_ => exceptions::PyOSError::new_err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PyErrArguments for io::Error {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
//FIXME(icxolu) remove unwrap
|
||||
self.to_string()
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> From<io::IntoInnerError<W>> for PyErr {
|
||||
fn from(err: io::IntoInnerError<W>) -> PyErr {
|
||||
err.into_error().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Send + Sync> PyErrArguments for io::IntoInnerError<W> {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
self.into_error().arguments(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::convert::Infallible> for PyErr {
|
||||
fn from(_: core::convert::Infallible) -> PyErr {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_to_pyerr {
|
||||
($err: ty, $pyexc: ty) => {
|
||||
impl PyErrArguments for $err {
|
||||
fn arguments(self, py: Python<'_>) -> $crate::Py<$crate::PyAny> {
|
||||
// FIXME(icxolu) remove unwrap
|
||||
self.to_string()
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::convert::From<$err> for PyErr {
|
||||
fn from(err: $err) -> PyErr {
|
||||
<$pyexc>::new_err(err)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct Utf8ErrorWithBytes {
|
||||
err: core::str::Utf8Error,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PyErrArguments for Utf8ErrorWithBytes {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
let Self { err, bytes } = self;
|
||||
let start = err.valid_up_to();
|
||||
let end = err.error_len().map_or(bytes.len(), |l| start + l);
|
||||
|
||||
let encoding = types::PyString::new(py, "utf-8").into_any();
|
||||
let bytes = types::PyBytes::new(py, &bytes).into_any();
|
||||
let start = types::PyInt::new(py, start).into_any();
|
||||
let end = types::PyInt::new(py, end).into_any();
|
||||
let reason = types::PyString::new(py, "invalid utf-8").into_any();
|
||||
|
||||
// FIXME(icxolu) remove unwrap
|
||||
types::PyTuple::new(py, &[encoding, bytes, start, end, reason])
|
||||
.unwrap()
|
||||
.into_any()
|
||||
.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
impl PyErrArguments for alloc::string::FromUtf8Error {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
Utf8ErrorWithBytes {
|
||||
err: self.utf8_error(),
|
||||
bytes: self.into_bytes(),
|
||||
}
|
||||
.arguments(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::convert::From<alloc::string::FromUtf8Error> for PyErr {
|
||||
fn from(err: alloc::string::FromUtf8Error) -> PyErr {
|
||||
exceptions::PyUnicodeDecodeError::new_err(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl PyErrArguments for alloc::ffi::IntoStringError {
|
||||
fn arguments(self, py: Python<'_>) -> Py<PyAny> {
|
||||
Utf8ErrorWithBytes {
|
||||
err: self.utf8_error(),
|
||||
bytes: self.into_cstring().into_bytes(),
|
||||
}
|
||||
.arguments(py)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::convert::From<alloc::ffi::IntoStringError> for PyErr {
|
||||
fn from(err: alloc::ffi::IntoStringError) -> PyErr {
|
||||
exceptions::PyUnicodeDecodeError::new_err(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl_to_pyerr!(core::array::TryFromSliceError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::num::ParseIntError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::num::ParseFloatError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::num::TryFromIntError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::str::ParseBoolError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(alloc::ffi::NulError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::net::AddrParseError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::time::TryFromFloatSecsError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(std::time::SystemTimeError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(std::path::StripPrefixError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(std::env::JoinPathsError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::char::ParseCharError, exceptions::PyValueError);
|
||||
impl_to_pyerr!(core::char::CharTryFromError, exceptions::PyValueError);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::exceptions::PyUnicodeDecodeError;
|
||||
use crate::types::PyAnyMethods;
|
||||
use crate::{IntoPyObjectExt as _, PyErr, Python};
|
||||
use std::io;
|
||||
|
||||
#[test]
|
||||
fn io_errors() {
|
||||
use crate::types::any::PyAnyMethods;
|
||||
|
||||
let check_err = |kind, expected_ty| {
|
||||
Python::attach(|py| {
|
||||
let rust_err = io::Error::new(kind, "some error msg");
|
||||
|
||||
let py_err: PyErr = rust_err.into();
|
||||
let py_err_msg = format!("{expected_ty}: some error msg");
|
||||
assert_eq!(py_err.to_string(), py_err_msg);
|
||||
let py_error_clone = py_err.clone_ref(py);
|
||||
|
||||
let rust_err_from_py_err: io::Error = py_err.into();
|
||||
assert_eq!(rust_err_from_py_err.to_string(), py_err_msg);
|
||||
assert_eq!(rust_err_from_py_err.kind(), kind);
|
||||
|
||||
let py_err_recovered_from_rust_err: PyErr = rust_err_from_py_err.into();
|
||||
assert!(py_err_recovered_from_rust_err
|
||||
.value(py)
|
||||
.is(py_error_clone.value(py))); // It should be the same exception
|
||||
})
|
||||
};
|
||||
|
||||
check_err(io::ErrorKind::BrokenPipe, "BrokenPipeError");
|
||||
check_err(io::ErrorKind::ConnectionRefused, "ConnectionRefusedError");
|
||||
check_err(io::ErrorKind::ConnectionAborted, "ConnectionAbortedError");
|
||||
check_err(io::ErrorKind::ConnectionReset, "ConnectionResetError");
|
||||
check_err(io::ErrorKind::Interrupted, "InterruptedError");
|
||||
check_err(io::ErrorKind::NotFound, "FileNotFoundError");
|
||||
check_err(io::ErrorKind::PermissionDenied, "PermissionError");
|
||||
check_err(io::ErrorKind::AlreadyExists, "FileExistsError");
|
||||
check_err(io::ErrorKind::WouldBlock, "BlockingIOError");
|
||||
check_err(io::ErrorKind::TimedOut, "TimeoutError");
|
||||
check_err(io::ErrorKind::IsADirectory, "IsADirectoryError");
|
||||
check_err(io::ErrorKind::NotADirectory, "NotADirectoryError");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(invalid_from_utf8)]
|
||||
fn utf8_errors() {
|
||||
let bytes = b"abc\xffdef".to_vec();
|
||||
|
||||
let check_err = |py_err: PyErr| {
|
||||
Python::attach(|py| {
|
||||
let py_err = py_err.into_bound_py_any(py).unwrap();
|
||||
|
||||
assert!(py_err.is_instance_of::<exceptions::PyUnicodeDecodeError>());
|
||||
assert_eq!(
|
||||
py_err
|
||||
.getattr("encoding")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"utf-8"
|
||||
);
|
||||
assert_eq!(
|
||||
py_err
|
||||
.getattr("object")
|
||||
.unwrap()
|
||||
.extract::<Vec<u8>>()
|
||||
.unwrap(),
|
||||
&*bytes
|
||||
);
|
||||
assert_eq!(
|
||||
py_err.getattr("start").unwrap().extract::<usize>().unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
py_err.getattr("end").unwrap().extract::<usize>().unwrap(),
|
||||
4
|
||||
);
|
||||
assert_eq!(
|
||||
py_err
|
||||
.getattr("reason")
|
||||
.unwrap()
|
||||
.extract::<String>()
|
||||
.unwrap(),
|
||||
"invalid utf-8"
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
let utf8_err_with_bytes = PyUnicodeDecodeError::new_err(Utf8ErrorWithBytes {
|
||||
err: core::str::from_utf8(&bytes).expect_err("\\xff is invalid utf-8"),
|
||||
bytes: bytes.clone(),
|
||||
});
|
||||
check_err(utf8_err_with_bytes);
|
||||
|
||||
let from_utf8_err = String::from_utf8(bytes.clone())
|
||||
.expect_err("\\xff is invalid utf-8")
|
||||
.into();
|
||||
check_err(from_utf8_err);
|
||||
|
||||
let from_utf8_err = alloc::ffi::CString::new(bytes.clone())
|
||||
.unwrap()
|
||||
.into_string()
|
||||
.expect_err("\\xff is invalid utf-8")
|
||||
.into();
|
||||
check_err(from_utf8_err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn std_error_conversions() {
|
||||
Python::attach(|py| {
|
||||
let check_err = |err: PyErr, expected_msg: &str| {
|
||||
let py_err = err.into_bound_py_any(py).unwrap();
|
||||
assert!(py_err.is_instance_of::<exceptions::PyValueError>());
|
||||
let msg = py_err.str().unwrap().to_string();
|
||||
assert_eq!(msg, expected_msg);
|
||||
};
|
||||
|
||||
// TryFromFloatSecsError
|
||||
let float_secs_err = core::time::Duration::try_from_secs_f32(-1.0).unwrap_err();
|
||||
let expected = float_secs_err.to_string();
|
||||
check_err(float_secs_err.into(), &expected);
|
||||
|
||||
// SystemTimeError
|
||||
let sys_time_err = std::time::SystemTime::now()
|
||||
.duration_since(std::time::SystemTime::now() + core::time::Duration::from_secs(1))
|
||||
.unwrap_err();
|
||||
let expected = sys_time_err.to_string();
|
||||
check_err(sys_time_err.into(), &expected);
|
||||
|
||||
// StripPrefixError
|
||||
let strip_prefix_err = std::path::Path::new("/a/b/c")
|
||||
.strip_prefix("/x/y/z")
|
||||
.unwrap_err();
|
||||
let expected = strip_prefix_err.to_string();
|
||||
check_err(strip_prefix_err.into(), &expected);
|
||||
|
||||
// JoinPathsError
|
||||
let join_paths_err = std::env::join_paths(["a:b", "a;b", "a\"b"].iter()).unwrap_err();
|
||||
let expected = join_paths_err.to_string();
|
||||
check_err(join_paths_err.into(), &expected);
|
||||
|
||||
// ParseCharError
|
||||
let parse_char_err = "abc".parse::<char>().unwrap_err();
|
||||
let expected = parse_char_err.to_string();
|
||||
check_err(parse_char_err.into(), &expected);
|
||||
|
||||
// CharTryFromError
|
||||
let char_try_from_err = char::try_from(0xD800_u32).unwrap_err();
|
||||
let expected = char_try_from_err.to_string();
|
||||
check_err(char_try_from_err.into(), &expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+1081
File diff suppressed because it is too large.
Load diff
Vendored
+1304
File diff suppressed because it is too large.
Load diff
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
//! Raw FFI declarations for Python's C API.
|
||||
//!
|
||||
//! This module provides low level bindings to the Python interpreter.
|
||||
//! It is meant for advanced users only - regular PyO3 users shouldn't
|
||||
//! need to interact with this module at all.
|
||||
//!
|
||||
//! The contents of this module are not documented here, as it would entail
|
||||
//! basically copying the documentation from CPython. Consult the [Python/C API Reference
|
||||
//! Manual][capi] for up-to-date documentation.
|
||||
//!
|
||||
//! # Safety
|
||||
//!
|
||||
//! The functions in this module lack individual safety documentation, but
|
||||
//! generally the following apply:
|
||||
//! - Pointer arguments have to point to a valid Python object of the correct type,
|
||||
//! although null pointers are sometimes valid input.
|
||||
//! - The vast majority can only be used safely while the GIL is held.
|
||||
//! - Some functions have additional safety requirements, consult the
|
||||
//! [Python/C API Reference Manual][capi] for more information.
|
||||
//!
|
||||
//! [capi]: https://docs.python.org/3/c-api/index.html
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// reexport raw bindings exposed in pyo3_ffi
|
||||
pub use pyo3_ffi::*;
|
||||
Vendored
+340
@@ -0,0 +1,340 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::ffi::*;
|
||||
use crate::Python;
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use crate::types::any::PyAnyMethods;
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), any(not(any(PyPy, GraalPy)), feature = "macros")))]
|
||||
use crate::types::PyString;
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use crate::{types::PyDict, Bound, PyAny};
|
||||
#[cfg(not(any(Py_3_12, Py_LIMITED_API, GraalPy)))]
|
||||
use libc::wchar_t;
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
#[test]
|
||||
fn test_datetime_fromtimestamp() {
|
||||
use crate::IntoPyObject;
|
||||
Python::attach(|py| {
|
||||
let args = (100,).into_pyobject(py).unwrap();
|
||||
let dt = unsafe {
|
||||
PyDateTime_IMPORT();
|
||||
Bound::from_owned_ptr(py, PyDateTime_FromTimestamp(args.as_ptr()))
|
||||
};
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("dt", dt).unwrap();
|
||||
py.run(
|
||||
c"import datetime; assert dt == datetime.datetime.fromtimestamp(100)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
#[test]
|
||||
fn test_date_fromtimestamp() {
|
||||
use crate::IntoPyObject;
|
||||
Python::attach(|py| {
|
||||
let args = (100,).into_pyobject(py).unwrap();
|
||||
let dt = unsafe {
|
||||
PyDateTime_IMPORT();
|
||||
Bound::from_owned_ptr(py, PyDate_FromTimestamp(args.as_ptr()))
|
||||
};
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("dt", dt).unwrap();
|
||||
py.run(
|
||||
c"import datetime; assert dt == datetime.date.fromtimestamp(100)",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
#[test]
|
||||
fn test_utc_timezone() {
|
||||
Python::attach(|py| {
|
||||
let utc_timezone: Bound<'_, PyAny> = unsafe {
|
||||
PyDateTime_IMPORT();
|
||||
Bound::from_borrowed_ptr(py, PyDateTime_TimeZone_UTC())
|
||||
};
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("utc_timezone", utc_timezone).unwrap();
|
||||
py.run(
|
||||
c"import datetime; assert utc_timezone is datetime.timezone.utc",
|
||||
None,
|
||||
Some(&locals),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg(feature = "macros")]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
fn test_timezone_from_offset() {
|
||||
use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta};
|
||||
|
||||
Python::attach(|py| {
|
||||
let delta = PyDelta::new(py, 0, 100, 0, false).unwrap();
|
||||
let tz = unsafe { PyTimeZone_FromOffset(delta.as_ptr()).assume_owned(py) };
|
||||
crate::py_run!(
|
||||
py,
|
||||
tz,
|
||||
"import datetime; assert tz == datetime.timezone(datetime.timedelta(seconds=100))"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg(feature = "macros")]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
fn test_timezone_from_offset_and_name() {
|
||||
use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta};
|
||||
|
||||
Python::attach(|py| {
|
||||
let delta = PyDelta::new(py, 0, 100, 0, false).unwrap();
|
||||
let tzname = PyString::new(py, "testtz");
|
||||
let tz = unsafe {
|
||||
PyTimeZone_FromOffsetAndName(delta.as_ptr(), tzname.as_ptr()).assume_owned(py)
|
||||
};
|
||||
crate::py_run!(
|
||||
py,
|
||||
tz,
|
||||
"import datetime; assert tz == datetime.timezone(datetime.timedelta(seconds=100), 'testtz')"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(Py_LIMITED_API, GraalPy)))]
|
||||
fn ascii_object_bitfield() {
|
||||
let ob_base: PyObject = unsafe { core::mem::zeroed() };
|
||||
|
||||
#[cfg_attr(Py_3_14, allow(unused_mut, unused_variables))]
|
||||
let mut o = PyASCIIObject {
|
||||
ob_base,
|
||||
length: 0,
|
||||
#[cfg(any(Py_3_11, not(PyPy)))]
|
||||
hash: 0,
|
||||
state: 0u32,
|
||||
#[cfg(not(Py_3_12))]
|
||||
wstr: core::ptr::null_mut() as *mut wchar_t,
|
||||
};
|
||||
|
||||
#[cfg(not(Py_3_14))]
|
||||
unsafe {
|
||||
assert_eq!(o.interned(), 0);
|
||||
assert_eq!(o.kind(), 0);
|
||||
assert_eq!(o.compact(), 0);
|
||||
assert_eq!(o.ascii(), 0);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(o.ready(), 0);
|
||||
|
||||
let interned_count = if cfg!(Py_3_12) { 2 } else { 4 };
|
||||
|
||||
for i in 0..interned_count {
|
||||
o.set_interned(i);
|
||||
assert_eq!(o.interned(), i);
|
||||
}
|
||||
|
||||
for i in 0..8 {
|
||||
o.set_kind(i);
|
||||
assert_eq!(o.kind(), i);
|
||||
}
|
||||
|
||||
o.set_compact(1);
|
||||
assert_eq!(o.compact(), 1);
|
||||
|
||||
o.set_ascii(1);
|
||||
assert_eq!(o.ascii(), 1);
|
||||
|
||||
#[cfg(not(Py_3_12))]
|
||||
o.set_ready(1);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(o.ready(), 1);
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
o.set_statically_allocated(1);
|
||||
#[cfg(Py_3_12)]
|
||||
assert_eq!(o.statically_allocated(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
|
||||
fn ascii() {
|
||||
Python::attach(|py| {
|
||||
// This test relies on implementation details of PyString.
|
||||
let s = PyString::new(py, "hello, world");
|
||||
let ptr = s.as_ptr();
|
||||
|
||||
unsafe {
|
||||
#[cfg(not(Py_3_14))]
|
||||
{
|
||||
let ascii_ptr = ptr as *mut PyASCIIObject;
|
||||
let ascii = ascii_ptr.as_ref().unwrap();
|
||||
|
||||
assert_eq!(ascii.interned(), 0);
|
||||
assert_eq!(ascii.kind(), PyUnicode_1BYTE_KIND);
|
||||
assert_eq!(ascii.compact(), 1);
|
||||
assert_eq!(ascii.ascii(), 1);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(ascii.ready(), 1);
|
||||
|
||||
assert_eq!(PyUnicode_IS_ASCII(ptr), 1);
|
||||
assert_eq!(PyUnicode_IS_COMPACT(ptr), 1);
|
||||
assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 1);
|
||||
}
|
||||
|
||||
assert!(!PyUnicode_1BYTE_DATA(ptr).is_null());
|
||||
// 2 and 4 byte macros return nonsense for this string instance.
|
||||
assert_eq!(PyUnicode_KIND(ptr), PyUnicode_1BYTE_KIND);
|
||||
|
||||
assert!(!PyUnicode_DATA(ptr).is_null());
|
||||
|
||||
assert_eq!(PyUnicode_GET_LENGTH(ptr), s.len().unwrap() as Py_ssize_t);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(PyUnicode_IS_READY(ptr), 1);
|
||||
|
||||
// This has potential to mutate object. But it should be a no-op since
|
||||
// we're already ready.
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(PyUnicode_READY(ptr), 0);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
|
||||
fn ucs4() {
|
||||
Python::attach(|py| {
|
||||
let s = "哈哈🐈";
|
||||
let py_string = PyString::new(py, s);
|
||||
let ptr = py_string.as_ptr();
|
||||
|
||||
unsafe {
|
||||
#[cfg(not(Py_3_14))]
|
||||
{
|
||||
let ascii_ptr = ptr as *mut PyASCIIObject;
|
||||
let ascii = ascii_ptr.as_ref().unwrap();
|
||||
|
||||
assert_eq!(ascii.interned(), 0);
|
||||
assert_eq!(ascii.kind(), PyUnicode_4BYTE_KIND);
|
||||
assert_eq!(ascii.compact(), 1);
|
||||
assert_eq!(ascii.ascii(), 0);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(ascii.ready(), 1);
|
||||
|
||||
assert_eq!(PyUnicode_IS_ASCII(ptr), 0);
|
||||
assert_eq!(PyUnicode_IS_COMPACT(ptr), 1);
|
||||
assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 0);
|
||||
}
|
||||
assert!(!PyUnicode_4BYTE_DATA(ptr).is_null());
|
||||
assert_eq!(PyUnicode_KIND(ptr), PyUnicode_4BYTE_KIND);
|
||||
|
||||
assert!(!PyUnicode_DATA(ptr).is_null());
|
||||
|
||||
assert_eq!(
|
||||
PyUnicode_GET_LENGTH(ptr),
|
||||
py_string.len().unwrap() as Py_ssize_t
|
||||
);
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(PyUnicode_IS_READY(ptr), 1);
|
||||
|
||||
// This has potential to mutate object. But it should be a no-op since
|
||||
// we're already ready.
|
||||
#[cfg(not(Py_3_12))]
|
||||
assert_eq!(PyUnicode_READY(ptr), 0);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
|
||||
fn test_get_tzinfo() {
|
||||
use crate::types::PyTzInfo;
|
||||
|
||||
crate::Python::attach(|py| {
|
||||
use crate::types::{PyDateTime, PyTime};
|
||||
|
||||
let utc: &Bound<'_, _> = &PyTzInfo::utc(py).unwrap();
|
||||
|
||||
let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap();
|
||||
|
||||
assert!(
|
||||
unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) }
|
||||
.is(utc)
|
||||
);
|
||||
|
||||
let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap();
|
||||
|
||||
assert!(
|
||||
unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) }
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap();
|
||||
|
||||
assert!(
|
||||
unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }.is(utc)
|
||||
);
|
||||
|
||||
let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap();
|
||||
|
||||
assert!(
|
||||
unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }
|
||||
.is_none()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inc_dec_ref() {
|
||||
Python::attach(|py| {
|
||||
let obj = py.eval(c"object()", None, None).unwrap();
|
||||
|
||||
let ref_count = obj._get_refcnt();
|
||||
let ptr = obj.as_ptr();
|
||||
|
||||
unsafe { Py_INCREF(ptr) };
|
||||
|
||||
assert_eq!(obj._get_refcnt(), ref_count + 1);
|
||||
|
||||
unsafe { Py_DECREF(ptr) };
|
||||
|
||||
assert_eq!(obj._get_refcnt(), ref_count);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(Py_3_12)]
|
||||
fn test_inc_dec_ref_immortal() {
|
||||
Python::attach(|py| {
|
||||
let obj = py.None();
|
||||
|
||||
let ref_count = obj._get_refcnt(py);
|
||||
let ptr = obj.as_ptr();
|
||||
|
||||
unsafe { Py_INCREF(ptr) };
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), ref_count);
|
||||
|
||||
unsafe { Py_DECREF(ptr) };
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), ref_count);
|
||||
})
|
||||
}
|
||||
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
use crate::sealed::Sealed;
|
||||
use crate::{
|
||||
ffi,
|
||||
instance::{Borrowed, Bound},
|
||||
PyAny, PyResult, Python,
|
||||
};
|
||||
|
||||
pub(crate) trait FfiPtrExt: Sealed {
|
||||
/// Assumes this pointer carries a Python reference which needs to be decref'd.
|
||||
///
|
||||
/// If the pointer is NULL, this function will fetch an error.
|
||||
unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
|
||||
|
||||
/// Same as `assume_owned_or_err`, but doesn't fetch an error on NULL.
|
||||
unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option<Bound<'_, PyAny>>;
|
||||
|
||||
/// Same as `assume_owned_or_err`, but panics on NULL.
|
||||
unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny>;
|
||||
|
||||
/// Same as `assume_owned_or_err`, but does not check for NULL.
|
||||
unsafe fn assume_owned_unchecked(self, py: Python<'_>) -> Bound<'_, PyAny>;
|
||||
|
||||
/// Assumes this pointer is borrowed from a parent object.
|
||||
///
|
||||
/// Warning: the lifetime `'a` is not bounded by the function arguments; the caller is
|
||||
/// responsible to ensure this is tied to some appropriate lifetime.
|
||||
unsafe fn assume_borrowed_or_err<'a>(self, py: Python<'_>)
|
||||
-> PyResult<Borrowed<'a, '_, PyAny>>;
|
||||
|
||||
/// Same as `assume_borrowed_or_err`, but doesn't fetch an error on NULL.
|
||||
unsafe fn assume_borrowed_or_opt<'a>(self, py: Python<'_>) -> Option<Borrowed<'a, '_, PyAny>>;
|
||||
|
||||
/// Same as `assume_borrowed_or_err`, but panics on NULL.
|
||||
unsafe fn assume_borrowed<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny>;
|
||||
|
||||
/// Same as `assume_borrowed_or_err`, but does not check for NULL.
|
||||
unsafe fn assume_borrowed_unchecked<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny>;
|
||||
}
|
||||
|
||||
impl FfiPtrExt for *mut ffi::PyObject {
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Bound::from_owned_ptr_or_err`]
|
||||
#[inline]
|
||||
unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Bound::from_owned_ptr_or_err(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Bound::from_owned_ptr_or_opt`]
|
||||
#[inline]
|
||||
unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option<Bound<'_, PyAny>> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Bound::from_owned_ptr_or_opt(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Bound::from_owned_ptr`]
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Bound::from_owned_ptr(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Bound::from_owned_ptr_unchecked`]
|
||||
#[inline]
|
||||
unsafe fn assume_owned_unchecked(self, py: Python<'_>) -> Bound<'_, PyAny> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Bound::from_owned_ptr_unchecked(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Borrowed::from_ptr_or_err`]
|
||||
#[inline]
|
||||
unsafe fn assume_borrowed_or_err<'a>(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Borrowed<'a, '_, PyAny>> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Borrowed::from_ptr_or_err(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Borrowed::from_ptr_or_opt`]
|
||||
#[inline]
|
||||
unsafe fn assume_borrowed_or_opt<'a>(self, py: Python<'_>) -> Option<Borrowed<'a, '_, PyAny>> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Borrowed::from_ptr_or_opt(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Borrowed::from_ptr`]
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
unsafe fn assume_borrowed<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Borrowed::from_ptr(py, self) }
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// see requirements for [`Borrowed::from_ptr_unchecked`]
|
||||
#[inline]
|
||||
unsafe fn assume_borrowed_unchecked<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { Borrowed::from_ptr_unchecked(py, self) }
|
||||
}
|
||||
}
|
||||
Vendored
+228
@@ -0,0 +1,228 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(any(doc, all(Py_3_14, not(Py_LIMITED_API))))]
|
||||
use crate::{types::PyString, Python};
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
use {
|
||||
crate::ffi::{
|
||||
PyUnicodeWriter_Create, PyUnicodeWriter_Discard, PyUnicodeWriter_Finish,
|
||||
PyUnicodeWriter_WriteChar, PyUnicodeWriter_WriteUTF8,
|
||||
},
|
||||
crate::ffi_ptr_ext::FfiPtrExt,
|
||||
crate::py_result_ext::PyResultExt,
|
||||
crate::IntoPyObject,
|
||||
crate::{ffi, Bound, PyErr, PyResult},
|
||||
core::fmt,
|
||||
core::mem::ManuallyDrop,
|
||||
core::ptr::NonNull,
|
||||
};
|
||||
|
||||
/// This macro is analogous to Rust's [`format!`] macro, but returns a [`PyString`] instead of a [`String`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// The arguments are exactly like [`format!`], but with `py` (a [`Python`] token) as the first argument:
|
||||
///
|
||||
/// # Interning Advantage
|
||||
///
|
||||
/// If the format string is a static string and all arguments are constant at compile time,
|
||||
/// this macro will intern the string in Python, offering better performance and memory usage
|
||||
/// compared to [`PyString::from_fmt`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pyo3::{py_format, Python, types::PyString, Bound};
|
||||
/// Python::attach(|py| {
|
||||
/// let py_string: Bound<'_, PyString> = py_format!(py, "{} {}", "hello", "world").unwrap();
|
||||
/// assert_eq!(py_string.to_string(), "hello world");
|
||||
/// });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! py_format {
|
||||
($py: expr, $($arg:tt)*) => {{
|
||||
if let Some(static_string) = format_args!($($arg)*).as_str() {
|
||||
static INTERNED: $crate::sync::PyOnceLock<$crate::Py<$crate::types::PyString>> = $crate::sync::PyOnceLock::new();
|
||||
Ok(
|
||||
INTERNED
|
||||
.get_or_init($py, || $crate::types::PyString::intern($py, static_string).unbind())
|
||||
.bind($py)
|
||||
.to_owned()
|
||||
)
|
||||
} else {
|
||||
$crate::types::PyString::from_fmt($py, format_args!($($arg)*))
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
/// The `PyUnicodeWriter` is a utility for efficiently constructing Python strings
|
||||
pub(crate) struct PyUnicodeWriter<'py> {
|
||||
python: Python<'py>,
|
||||
writer: NonNull<ffi::PyUnicodeWriter>,
|
||||
last_error: Option<PyErr>,
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
impl<'py> PyUnicodeWriter<'py> {
|
||||
/// Creates a new `PyUnicodeWriter`.
|
||||
pub fn new(py: Python<'py>) -> PyResult<Self> {
|
||||
Self::with_capacity(py, 0)
|
||||
}
|
||||
|
||||
/// Creates a new `PyUnicodeWriter` with the specified initial capacity.
|
||||
#[inline]
|
||||
pub fn with_capacity(py: Python<'py>, capacity: usize) -> PyResult<Self> {
|
||||
match NonNull::new(unsafe { PyUnicodeWriter_Create(capacity.try_into()?) }) {
|
||||
Some(ptr) => Ok(PyUnicodeWriter {
|
||||
python: py,
|
||||
writer: ptr,
|
||||
last_error: None,
|
||||
}),
|
||||
None => Err(PyErr::fetch(py)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes the `PyUnicodeWriter` and returns a `Bound<PyString>` containing the constructed string.
|
||||
#[inline]
|
||||
pub fn into_py_string(mut self) -> PyResult<Bound<'py, PyString>> {
|
||||
let py = self.python;
|
||||
if let Some(error) = self.take_error() {
|
||||
Err(error)
|
||||
} else {
|
||||
unsafe {
|
||||
PyUnicodeWriter_Finish(ManuallyDrop::new(self).as_ptr())
|
||||
.assume_owned_or_err(py)
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When fmt::Write returned an error, this function can be used to retrieve the last error that occurred.
|
||||
#[inline]
|
||||
pub fn take_error(&mut self) -> Option<PyErr> {
|
||||
self.last_error.take()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_ptr(&self) -> *mut ffi::PyUnicodeWriter {
|
||||
self.writer.as_ptr()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_error(&mut self) {
|
||||
self.last_error = Some(PyErr::fetch(self.python));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
impl fmt::Write for PyUnicodeWriter<'_> {
|
||||
#[inline]
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
let result = unsafe {
|
||||
PyUnicodeWriter_WriteUTF8(self.as_ptr(), s.as_ptr().cast(), s.len() as isize)
|
||||
};
|
||||
if result < 0 {
|
||||
self.set_error();
|
||||
Err(fmt::Error)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write_char(&mut self, c: char) -> fmt::Result {
|
||||
let result = unsafe { PyUnicodeWriter_WriteChar(self.as_ptr(), c.into()) };
|
||||
if result < 0 {
|
||||
self.set_error();
|
||||
Err(fmt::Error)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
impl Drop for PyUnicodeWriter<'_> {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
PyUnicodeWriter_Discard(self.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
impl<'py> IntoPyObject<'py> for PyUnicodeWriter<'py> {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = PyErr;
|
||||
|
||||
#[inline]
|
||||
fn into_pyobject(self, _py: Python<'py>) -> PyResult<Bound<'py, PyString>> {
|
||||
self.into_py_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
impl<'py> TryInto<Bound<'py, PyString>> for PyUnicodeWriter<'py> {
|
||||
type Error = PyErr;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> PyResult<Bound<'py, PyString>> {
|
||||
self.into_py_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
use super::*;
|
||||
use crate::types::PyStringMethods;
|
||||
use crate::{IntoPyObject, Python};
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::write_literal)]
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
fn unicode_writer_test() {
|
||||
use core::fmt::Write;
|
||||
Python::attach(|py| {
|
||||
let mut writer = PyUnicodeWriter::new(py).unwrap();
|
||||
write!(writer, "Hello {}!", "world").unwrap();
|
||||
writer.write_char('😎').unwrap();
|
||||
let result = writer.into_py_string().unwrap();
|
||||
assert_eq!(result.to_string(), "Hello world!😎");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::write_literal)]
|
||||
#[cfg(all(Py_3_14, not(Py_LIMITED_API)))]
|
||||
fn unicode_writer_with_capacity() {
|
||||
use core::fmt::Write;
|
||||
Python::attach(|py| {
|
||||
let mut writer = PyUnicodeWriter::with_capacity(py, 10).unwrap();
|
||||
write!(writer, "Hello {}!", "world").unwrap();
|
||||
writer.write_char('😎').unwrap();
|
||||
let result = writer.into_py_string().unwrap();
|
||||
assert_eq!(result.to_string(), "Hello world!😎");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pystring_from_fmt() {
|
||||
Python::attach(|py| {
|
||||
py_format!(py, "Hello {}!", "world").unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_format() {
|
||||
Python::attach(|py| {
|
||||
let complex_value = (42, "foo", [0; 0]).into_pyobject(py).unwrap();
|
||||
let py_string = py_format!(py, "This is some complex value: {complex_value}").unwrap();
|
||||
let actual = py_string.to_cow().unwrap();
|
||||
let expected = "This is some complex value: (42, 'foo', [])";
|
||||
assert_eq!(actual, expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
//! Internals of PyO3 which are accessed by code expanded from PyO3's procedural macros.
|
||||
//!
|
||||
//! Usage of any of these APIs in downstream code is implicitly acknowledging that these
|
||||
//! APIs may may change at any time without documentation in the CHANGELOG and without
|
||||
//! breaking semver guarantees.
|
||||
|
||||
pub mod callback;
|
||||
pub mod concat;
|
||||
#[cfg(feature = "experimental-async")]
|
||||
pub mod coroutine;
|
||||
pub mod deprecated;
|
||||
pub mod exceptions;
|
||||
pub mod extract_argument;
|
||||
pub mod freelist;
|
||||
pub mod frompyobject;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
pub mod introspection;
|
||||
pub mod panic;
|
||||
pub mod pycell;
|
||||
pub mod pyclass;
|
||||
pub mod pyclass_init;
|
||||
pub mod pyfunction;
|
||||
pub mod pymethods;
|
||||
pub mod pymodule;
|
||||
#[doc(hidden)]
|
||||
pub mod trampoline;
|
||||
pub mod unindent;
|
||||
pub mod wrap;
|
||||
Vendored
+225
@@ -0,0 +1,225 @@
|
||||
//! Utilities for a Python callable object that invokes a Rust function.
|
||||
|
||||
use crate::err::{PyErr, PyResult};
|
||||
use crate::exceptions::PyOverflowError;
|
||||
use crate::ffi::{self, Py_hash_t};
|
||||
use crate::{BoundObject, IntoPyObject, Py, PyAny, Python};
|
||||
use core::ffi::c_int;
|
||||
|
||||
/// A type which can be the return type of a python C-API callback
|
||||
pub trait PyCallbackOutput: Copy + py_callback_output::Sealed {
|
||||
/// The error value to return to python if the callback raised an exception
|
||||
const ERR_VALUE: Self;
|
||||
}
|
||||
|
||||
/// Seals `PyCallbackOutput` so that types outside PyO3 cannot implement it.
|
||||
mod py_callback_output {
|
||||
use core::ffi::c_int;
|
||||
|
||||
use pyo3_ffi::Py_ssize_t;
|
||||
|
||||
use crate::ffi::PyObject;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl Sealed for *mut PyObject {}
|
||||
impl Sealed for c_int {}
|
||||
impl Sealed for Py_ssize_t {}
|
||||
}
|
||||
|
||||
impl PyCallbackOutput for *mut ffi::PyObject {
|
||||
const ERR_VALUE: Self = core::ptr::null_mut();
|
||||
}
|
||||
|
||||
impl PyCallbackOutput for core::ffi::c_int {
|
||||
const ERR_VALUE: Self = -1;
|
||||
}
|
||||
|
||||
impl PyCallbackOutput for ffi::Py_ssize_t {
|
||||
const ERR_VALUE: Self = -1;
|
||||
}
|
||||
|
||||
/// Convert the result of callback function into the appropriate return value.
|
||||
pub trait IntoPyCallbackOutput<'py, Target>: into_py_callback_output::Sealed<'py, Target> {
|
||||
fn convert(self, py: Python<'py>) -> PyResult<Target>;
|
||||
}
|
||||
|
||||
/// Seals `IntoPyCallbackOutput` so that types outside PyO3 cannot implement it.
|
||||
mod into_py_callback_output {
|
||||
use pyo3_ffi::Py_hash_t;
|
||||
|
||||
use crate::{
|
||||
ffi,
|
||||
impl_::callback::{HashCallbackOutput, IntoPyCallbackOutput, WrappingCastTo},
|
||||
IntoPyObject, Py, PyAny, PyErr,
|
||||
};
|
||||
|
||||
pub trait Sealed<'py, Target> {}
|
||||
|
||||
impl<'py, T: IntoPyObject<'py>> Sealed<'py, *mut ffi::PyObject> for T {}
|
||||
impl<'py, T: IntoPyCallbackOutput<'py, U>, E: Into<PyErr>, U> Sealed<'py, U> for Result<T, E> {}
|
||||
impl Sealed<'_, Self> for *mut ffi::PyObject {}
|
||||
impl Sealed<'_, core::ffi::c_int> for () {}
|
||||
impl Sealed<'_, core::ffi::c_int> for bool {}
|
||||
impl Sealed<'_, ()> for () {}
|
||||
impl Sealed<'_, ffi::Py_ssize_t> for usize {}
|
||||
impl Sealed<'_, bool> for bool {}
|
||||
impl Sealed<'_, usize> for usize {}
|
||||
impl<'py, T: IntoPyObject<'py>> Sealed<'py, Py<PyAny>> for T {}
|
||||
impl Sealed<'_, Py_hash_t> for HashCallbackOutput {}
|
||||
impl<T: WrappingCastTo<Py_hash_t>> Sealed<'_, HashCallbackOutput> for T {}
|
||||
}
|
||||
|
||||
impl<'py, T, E, U> IntoPyCallbackOutput<'py, U> for Result<T, E>
|
||||
where
|
||||
T: IntoPyCallbackOutput<'py, U>,
|
||||
E: Into<PyErr>,
|
||||
{
|
||||
#[inline]
|
||||
fn convert(self, py: Python<'py>) -> PyResult<U> {
|
||||
match self {
|
||||
Ok(v) => v.convert(py),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> IntoPyCallbackOutput<'py, *mut ffi::PyObject> for T
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
#[inline]
|
||||
fn convert(self, py: Python<'py>) -> PyResult<*mut ffi::PyObject> {
|
||||
self.into_pyobject(py)
|
||||
.map(BoundObject::into_ptr)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, Self> for *mut ffi::PyObject {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<Self> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, core::ffi::c_int> for () {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<core::ffi::c_int> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, core::ffi::c_int> for bool {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<core::ffi::c_int> {
|
||||
Ok(self as c_int)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, ()> for () {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, ffi::Py_ssize_t> for usize {
|
||||
#[inline]
|
||||
fn convert(self, _py: Python<'_>) -> PyResult<ffi::Py_ssize_t> {
|
||||
self.try_into().map_err(|_err| PyOverflowError::new_err(()))
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion traits needed by pyo3's macros
|
||||
|
||||
impl IntoPyCallbackOutput<'_, bool> for bool {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<bool> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPyCallbackOutput<'_, usize> for usize {
|
||||
#[inline]
|
||||
fn convert(self, _: Python<'_>) -> PyResult<usize> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> IntoPyCallbackOutput<'py, Py<PyAny>> for T
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
#[inline]
|
||||
fn convert(self, py: Python<'py>) -> PyResult<Py<PyAny>> {
|
||||
self.into_pyobject(py)
|
||||
.map(BoundObject::into_any)
|
||||
.map(BoundObject::unbind)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WrappingCastTo<T>: wrapping_cast_to::Sealed<T> {
|
||||
fn wrapping_cast(self) -> T;
|
||||
}
|
||||
|
||||
/// Seals `WrappingCastTo` so that types outside PyO3 cannot implement it.
|
||||
mod wrapping_cast_to {
|
||||
pub trait Sealed<T> {}
|
||||
}
|
||||
|
||||
macro_rules! wrapping_cast {
|
||||
($from:ty, $to:ty) => {
|
||||
impl WrappingCastTo<$to> for $from {
|
||||
#[inline]
|
||||
fn wrapping_cast(self) -> $to {
|
||||
self as $to
|
||||
}
|
||||
}
|
||||
impl wrapping_cast_to::Sealed<$to> for $from {}
|
||||
};
|
||||
}
|
||||
wrapping_cast!(u8, Py_hash_t);
|
||||
wrapping_cast!(u16, Py_hash_t);
|
||||
wrapping_cast!(u32, Py_hash_t);
|
||||
wrapping_cast!(usize, Py_hash_t);
|
||||
wrapping_cast!(u64, Py_hash_t);
|
||||
wrapping_cast!(i8, Py_hash_t);
|
||||
wrapping_cast!(i16, Py_hash_t);
|
||||
wrapping_cast!(i32, Py_hash_t);
|
||||
wrapping_cast!(isize, Py_hash_t);
|
||||
wrapping_cast!(i64, Py_hash_t);
|
||||
|
||||
pub struct HashCallbackOutput(Py_hash_t);
|
||||
|
||||
impl IntoPyCallbackOutput<'_, Py_hash_t> for HashCallbackOutput {
|
||||
#[inline]
|
||||
fn convert(self, _py: Python<'_>) -> PyResult<Py_hash_t> {
|
||||
let hash = self.0;
|
||||
if hash == -1 {
|
||||
Ok(-2)
|
||||
} else {
|
||||
Ok(hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoPyCallbackOutput<'_, HashCallbackOutput> for T
|
||||
where
|
||||
T: WrappingCastTo<Py_hash_t>,
|
||||
{
|
||||
#[inline]
|
||||
fn convert(self, _py: Python<'_>) -> PyResult<HashCallbackOutput> {
|
||||
Ok(HashCallbackOutput(self.wrapping_cast()))
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
pub fn convert<'py, T, U>(py: Python<'py>, value: T) -> PyResult<U>
|
||||
where
|
||||
T: IntoPyCallbackOutput<'py, U>,
|
||||
{
|
||||
value.convert(py)
|
||||
}
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
/// Calculates the total byte length of all byte pieces in the array.
|
||||
///
|
||||
/// This is a useful utility in order to determine the size needed for the constant
|
||||
/// `combine` function.
|
||||
pub const fn combined_len(pieces: &[&[u8]]) -> usize {
|
||||
let mut len = 0;
|
||||
let mut pieces_idx = 0;
|
||||
while pieces_idx < pieces.len() {
|
||||
len += pieces[pieces_idx].len();
|
||||
pieces_idx += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
/// Combines all bytes pieces into a single byte array.
|
||||
///
|
||||
/// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic.
|
||||
const fn combine(pieces: &[&[u8]], mut out: &mut [u8]) {
|
||||
let mut pieces_idx = 0;
|
||||
while pieces_idx < pieces.len() {
|
||||
let piece = pieces[pieces_idx];
|
||||
slice_copy_from_slice(out, piece);
|
||||
// using split_at_mut because range indexing not yet supported in const fn
|
||||
out = out.split_at_mut(piece.len()).1;
|
||||
pieces_idx += 1;
|
||||
}
|
||||
// should be no trailing buffer
|
||||
assert!(out.is_empty(), "output buffer too large");
|
||||
}
|
||||
|
||||
/// Wrapper around `combine` which has a const generic parameter, this is going to be more codegen
|
||||
/// at compile time (?)
|
||||
pub const fn combine_to_array<const LEN: usize>(pieces: &[&[u8]]) -> [u8; LEN] {
|
||||
let mut out: [u8; LEN] = [0u8; LEN];
|
||||
combine(pieces, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Replacement for `slice::copy_from_slice`, which is const from 1.87
|
||||
pub(crate) const fn slice_copy_from_slice(out: &mut [u8], src: &[u8]) {
|
||||
let mut i = 0;
|
||||
while i < src.len() {
|
||||
out[i] = src[i];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_combined_len() {
|
||||
let pieces: [&[u8]; 3] = [b"foo", b"bar", b"baz"];
|
||||
assert_eq!(combined_len(&pieces), 9);
|
||||
let empty: [&[u8]; 0] = [];
|
||||
assert_eq!(combined_len(&empty), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combine_to_array() {
|
||||
let pieces: [&[u8]; 2] = [b"foo", b"bar"];
|
||||
let combined = combine_to_array::<6>(&pieces);
|
||||
assert_eq!(&combined, b"foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "index out of bounds")]
|
||||
fn test_combine_to_array_buffer_too_small() {
|
||||
let pieces: [&[u8]; 2] = [b"foo", b"bar"];
|
||||
// Intentionally wrong length
|
||||
let _ = combine_to_array::<5>(&pieces);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "output buffer too large")]
|
||||
fn test_combine_to_array_buffer_too_big() {
|
||||
let pieces: [&[u8]; 2] = [b"foo", b"bar"];
|
||||
// Intentionally wrong length
|
||||
let _ = combine_to_array::<10>(&pieces);
|
||||
}
|
||||
}
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
use core::future::Future;
|
||||
|
||||
use crate::{
|
||||
coroutine::{cancel::ThrowCallback, Coroutine},
|
||||
instance::Bound,
|
||||
types::PyString,
|
||||
Py, PyAny, PyResult, Python,
|
||||
};
|
||||
|
||||
pub fn new_coroutine<'py, F>(
|
||||
name: &Bound<'py, PyString>,
|
||||
qualname_prefix: Option<&'static str>,
|
||||
throw_callback: Option<ThrowCallback>,
|
||||
future: F,
|
||||
) -> Coroutine
|
||||
where
|
||||
F: Future<Output = PyResult<Py<PyAny>>> + Send + 'static,
|
||||
{
|
||||
Coroutine::new(Some(name.clone()), qualname_prefix, throw_callback, future)
|
||||
}
|
||||
|
||||
/// Handle which assumes that the coroutine is attached to the thread. Unlike `Python<'_>`, this is `Send`.
|
||||
pub struct AssumeAttachedInCoroutine(());
|
||||
|
||||
impl AssumeAttachedInCoroutine {
|
||||
/// Safety: this should only be used inside a future passed to `new_coroutine`, where the coroutine is
|
||||
/// guaranteed to be attached to the thread when polled.
|
||||
pub unsafe fn new() -> Self {
|
||||
Self(())
|
||||
}
|
||||
|
||||
pub fn py(&self) -> Python<'_> {
|
||||
// Safety: this type holds the invariant that the thread is attached
|
||||
unsafe { Python::assume_attached() }
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
pub struct HasAutomaticFromPyObject<const IS_CLONE: bool> {}
|
||||
|
||||
impl HasAutomaticFromPyObject<true> {
|
||||
#[deprecated(
|
||||
since = "0.28.0",
|
||||
note = "The `FromPyObject` implementation for `#[pyclass]` types which implement `Clone` is changing to an opt-in option. Use `#[pyclass(from_py_object)]` to opt-in to the `FromPyObject` derive now, or `#[pyclass(skip_from_py_object)]` to skip the `FromPyObject` implementation."
|
||||
)]
|
||||
pub const MSG: () = ();
|
||||
}
|
||||
|
||||
impl HasAutomaticFromPyObject<false> {
|
||||
pub const MSG: () = ();
|
||||
}
|
||||
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
use crate::{sync::PyOnceLock, types::PyType, Bound, Py, Python};
|
||||
|
||||
pub struct ImportedExceptionTypeObject {
|
||||
imported_value: PyOnceLock<Py<PyType>>,
|
||||
module: &'static str,
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl ImportedExceptionTypeObject {
|
||||
pub const fn new(module: &'static str, name: &'static str) -> Self {
|
||||
Self {
|
||||
imported_value: PyOnceLock::new(),
|
||||
module,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> {
|
||||
self.imported_value
|
||||
.import(py, self.module, self.name)
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"failed to import exception {}.{}: {}",
|
||||
self.module, self.name, e
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
+1155
File diff suppressed because it is too large.
Load diff
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
//! Support for [free allocation lists][1].
|
||||
//!
|
||||
//! This can improve performance for types that are often created and deleted in quick succession.
|
||||
//!
|
||||
//! Rather than implementing this manually,
|
||||
//! implement it by annotating a struct with `#[pyclass(freelist = N)]`,
|
||||
//! where `N` is the size of the freelist.
|
||||
//!
|
||||
//! [1]: https://en.wikipedia.org/wiki/Free_list
|
||||
|
||||
use crate::ffi;
|
||||
use core::ptr::NonNull;
|
||||
|
||||
/// A free allocation list for PyObject ffi pointers.
|
||||
///
|
||||
/// See [the parent module](crate::impl_::freelist) for more details.
|
||||
pub struct PyObjectFreeList {
|
||||
entries: Box<[Option<NonNull<ffi::PyObject>>]>,
|
||||
split: usize,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
// safety: the pointers are never used internally and they are cleared when they are given out
|
||||
unsafe impl Send for PyObjectFreeList {}
|
||||
|
||||
impl PyObjectFreeList {
|
||||
/// Creates a new `PyObjectFreeList` instance with specified capacity.
|
||||
pub fn with_capacity(capacity: usize) -> PyObjectFreeList {
|
||||
let entries = vec![None; capacity].into_boxed_slice();
|
||||
|
||||
PyObjectFreeList {
|
||||
entries,
|
||||
split: 0,
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pops the first non empty item.
|
||||
pub fn pop(&mut self) -> Option<NonNull<ffi::PyObject>> {
|
||||
let idx = self.split;
|
||||
if idx == 0 {
|
||||
None
|
||||
} else {
|
||||
let val = self.entries[idx - 1]
|
||||
.take()
|
||||
.expect("PyObjectFreeList is corrupt");
|
||||
self.split = idx - 1;
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a value into the list. Returns `Some(val)` if the `PyObjectFreeList` is full.
|
||||
pub fn insert(&mut self, val: NonNull<ffi::PyObject>) -> Option<NonNull<ffi::PyObject>> {
|
||||
let next = self.split + 1;
|
||||
if next < self.capacity {
|
||||
self.entries[self.split] = Some(val);
|
||||
self.split = next;
|
||||
None
|
||||
} else {
|
||||
Some(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use crate::types::any::PyAnyMethods;
|
||||
use crate::Bound;
|
||||
use crate::{exceptions::PyTypeError, FromPyObject, PyAny, PyErr, PyResult, Python};
|
||||
|
||||
#[cold]
|
||||
pub fn failed_to_extract_enum(
|
||||
py: Python<'_>,
|
||||
type_name: &str,
|
||||
variant_names: &[&str],
|
||||
error_names: &[&str],
|
||||
errors: &[PyErr],
|
||||
) -> PyErr {
|
||||
// TODO maybe use ExceptionGroup on Python 3.11+ ?
|
||||
let mut err_msg = format!(
|
||||
"failed to extract enum {} ('{}')",
|
||||
type_name,
|
||||
error_names.join(" | ")
|
||||
);
|
||||
for ((variant_name, error_name), error) in variant_names.iter().zip(error_names).zip(errors) {
|
||||
use core::fmt::Write;
|
||||
write!(
|
||||
&mut err_msg,
|
||||
"\n- variant {variant_name} ({error_name}): {error_msg}",
|
||||
variant_name = variant_name,
|
||||
error_name = error_name,
|
||||
error_msg = extract_traceback(py, error.clone_ref(py)),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
PyTypeError::new_err(err_msg)
|
||||
}
|
||||
|
||||
/// Flattens a chain of errors into a single string.
|
||||
fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String {
|
||||
use core::fmt::Write;
|
||||
|
||||
let mut error_msg = error.to_string();
|
||||
while let Some(cause) = error.cause(py) {
|
||||
write!(&mut error_msg, ", caused by {cause}").unwrap();
|
||||
error = cause
|
||||
}
|
||||
error_msg
|
||||
}
|
||||
|
||||
pub fn extract_struct_field<'a, 'py, T>(
|
||||
obj: &'a Bound<'py, PyAny>,
|
||||
struct_name: &str,
|
||||
field_name: &str,
|
||||
) -> PyResult<T>
|
||||
where
|
||||
T: FromPyObject<'a, 'py>,
|
||||
{
|
||||
match obj.extract() {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(failed_to_extract_struct_field(
|
||||
obj.py(),
|
||||
err.into(),
|
||||
struct_name,
|
||||
field_name,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_struct_field_with<'a, 'py, T>(
|
||||
extractor: fn(&'a Bound<'py, PyAny>) -> PyResult<T>,
|
||||
obj: &'a Bound<'py, PyAny>,
|
||||
struct_name: &str,
|
||||
field_name: &str,
|
||||
) -> PyResult<T> {
|
||||
match extractor(obj) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(failed_to_extract_struct_field(
|
||||
obj.py(),
|
||||
err,
|
||||
struct_name,
|
||||
field_name,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn failed_to_extract_struct_field(
|
||||
py: Python<'_>,
|
||||
inner_err: PyErr,
|
||||
struct_name: &str,
|
||||
field_name: &str,
|
||||
) -> PyErr {
|
||||
let new_err = PyTypeError::new_err(format!(
|
||||
"failed to extract field {struct_name}.{field_name}"
|
||||
));
|
||||
new_err.set_cause(py, ::core::option::Option::Some(inner_err));
|
||||
new_err
|
||||
}
|
||||
|
||||
pub fn extract_tuple_struct_field<'a, 'py, T>(
|
||||
obj: &'a Bound<'py, PyAny>,
|
||||
struct_name: &str,
|
||||
index: usize,
|
||||
) -> PyResult<T>
|
||||
where
|
||||
T: FromPyObject<'a, 'py>,
|
||||
{
|
||||
match obj.extract() {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(failed_to_extract_tuple_struct_field(
|
||||
obj.py(),
|
||||
err.into(),
|
||||
struct_name,
|
||||
index,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_tuple_struct_field_with<'a, 'py, T>(
|
||||
extractor: fn(&'a Bound<'py, PyAny>) -> PyResult<T>,
|
||||
obj: &'a Bound<'py, PyAny>,
|
||||
struct_name: &str,
|
||||
index: usize,
|
||||
) -> PyResult<T> {
|
||||
match extractor(obj) {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(failed_to_extract_tuple_struct_field(
|
||||
obj.py(),
|
||||
err,
|
||||
struct_name,
|
||||
index,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn failed_to_extract_tuple_struct_field(
|
||||
py: Python<'_>,
|
||||
inner_err: PyErr,
|
||||
struct_name: &str,
|
||||
index: usize,
|
||||
) -> PyErr {
|
||||
let new_err = PyTypeError::new_err(format!("failed to extract field {struct_name}.{index}"));
|
||||
new_err.set_cause(py, ::core::option::Option::Some(inner_err));
|
||||
new_err
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::inspect::PyStaticExpr;
|
||||
|
||||
/// Seals `PyReturnType` so that types outside PyO3 cannot implement it.
|
||||
mod return_type {
|
||||
use crate::{impl_::introspection::PyReturnType, IntoPyObject};
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<'a, T: IntoPyObject<'a>> Sealed for T {}
|
||||
impl<T: PyReturnType, E> Sealed for Result<T, E> {}
|
||||
}
|
||||
|
||||
/// Trait to guess a function Python return type
|
||||
///
|
||||
/// It is useful to properly get the return type `T` when the Rust implementation returns e.g. `PyResult<T>`
|
||||
pub trait PyReturnType: return_type::Sealed {
|
||||
/// The function return type
|
||||
const OUTPUT_TYPE: PyStaticExpr;
|
||||
}
|
||||
|
||||
impl<'a, T: IntoPyObject<'a>> PyReturnType for T {
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
|
||||
}
|
||||
|
||||
impl<T: PyReturnType, E> PyReturnType for Result<T, E> {
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::OUTPUT_TYPE;
|
||||
}
|
||||
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` cannot be converted to a Python object",
|
||||
label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`",
|
||||
note = "implement `IntoPyObject` for `&{Self}` or `IntoPyObject + Clone` for `{Self}` to define the conversion"
|
||||
)]
|
||||
pub trait PyIntoPyObjectMaybeRefType<const REF_IMPL_EXISTS: bool> {
|
||||
const OUTPUT_TYPE: PyStaticExpr;
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: 'a> PyIntoPyObjectMaybeRefType<true> for T
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
const OUTPUT_TYPE: PyStaticExpr = <&T as IntoPyObject<'_>>::OUTPUT_TYPE;
|
||||
}
|
||||
|
||||
impl<'py, T: IntoPyObject<'py>> PyIntoPyObjectMaybeRefType<false> for T {
|
||||
const OUTPUT_TYPE: PyStaticExpr = <T as IntoPyObject<'_>>::OUTPUT_TYPE;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SerializedIntrospectionFragment<const LEN: usize> {
|
||||
pub length: u32,
|
||||
pub fragment: [u8; LEN],
|
||||
}
|
||||
|
||||
/// Escapes a string to be valid JSON. Does not add quotes around it
|
||||
///
|
||||
/// Returns the number of written bytes
|
||||
pub const fn escape_json_string(input: &str, output: &mut [u8]) -> usize {
|
||||
let input = input.as_bytes();
|
||||
let mut input_i = 0;
|
||||
let mut output_i = 0;
|
||||
while input_i < input.len() {
|
||||
match input[input_i] {
|
||||
b'\\' => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
}
|
||||
b'"' => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'"';
|
||||
output_i += 1;
|
||||
}
|
||||
0x08 => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'b';
|
||||
output_i += 1;
|
||||
}
|
||||
0x0C => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'f';
|
||||
output_i += 1;
|
||||
}
|
||||
b'\n' => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'n';
|
||||
output_i += 1;
|
||||
}
|
||||
b'\r' => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'r';
|
||||
output_i += 1;
|
||||
}
|
||||
b'\t' => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b't';
|
||||
output_i += 1;
|
||||
}
|
||||
c @ 0..32 => {
|
||||
output[output_i] = b'\\';
|
||||
output_i += 1;
|
||||
output[output_i] = b'u';
|
||||
output_i += 1;
|
||||
output[output_i] = b'0';
|
||||
output_i += 1;
|
||||
output[output_i] = b'0';
|
||||
output_i += 1;
|
||||
output[output_i] = b'0' + (c / 16);
|
||||
output_i += 1;
|
||||
let remainder = c % 16;
|
||||
output[output_i] = if remainder >= 10 {
|
||||
b'a' + remainder - 10
|
||||
} else {
|
||||
b'0' + remainder
|
||||
};
|
||||
output_i += 1;
|
||||
}
|
||||
c => {
|
||||
output[output_i] = c;
|
||||
output_i += 1;
|
||||
}
|
||||
}
|
||||
input_i += 1;
|
||||
}
|
||||
output_i
|
||||
}
|
||||
|
||||
/// Number of bytes written by [`escape_json_string`]
|
||||
pub const fn escaped_json_string_len(input: &str) -> usize {
|
||||
let input = input.as_bytes();
|
||||
let mut len = 0;
|
||||
let mut i = 0;
|
||||
while i < input.len() {
|
||||
len += match input[i] {
|
||||
b'\\' | b'"' | 0x08 | 0x0C | b'\n' | b'\r' | b'\t' => 2,
|
||||
0..32 => 6,
|
||||
_ => 1,
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
/// Type which will panic if dropped.
|
||||
///
|
||||
/// If this is dropped during a panic, this will cause an abort.
|
||||
///
|
||||
/// Use this to avoid letting unwinds cross through the FFI boundary, which is UB.
|
||||
pub struct PanicTrap {
|
||||
msg: &'static str,
|
||||
}
|
||||
|
||||
impl PanicTrap {
|
||||
#[inline]
|
||||
pub const fn new(msg: &'static str) -> Self {
|
||||
Self { msg }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn disarm(self) {
|
||||
core::mem::forget(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PanicTrap {
|
||||
fn drop(&mut self) {
|
||||
// Panic here will abort the process, assuming in an unwind.
|
||||
panic!("{}", self.msg)
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
//! Externally-accessible implementation of pycell
|
||||
pub use crate::pycell::impl_::{
|
||||
GetBorrowChecker, PyClassMutability, PyClassObjectBase, PyClassObjectBaseLayout,
|
||||
PyStaticClassObject, PyVariableClassObject, PyVariableClassObjectBase,
|
||||
};
|
||||
Vendored
+1673
File diff suppressed because it is too large.
Load diff
+46
@@ -0,0 +1,46 @@
|
||||
/// Helper function that can be used at compile time to emit a diagnostic if
|
||||
/// the type does not implement `Send` or `Sync` when it should; the mere act
|
||||
/// of invoking this function will cause the diagnostic to be emitted if needed.
|
||||
pub const fn assert_pyclass_send_sync<T>()
|
||||
where
|
||||
T: Send + Sync,
|
||||
{
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[allow(clippy::assertions_on_constants, reason = "invoked by a proc macro")]
|
||||
pub const fn assert_dict_supported() {
|
||||
assert!(
|
||||
cfg!(any(not(Py_LIMITED_API), Py_3_9)),
|
||||
"`dict` requires Python >= 3.9 when using the `abi3` feature"
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[allow(clippy::assertions_on_constants, reason = "invoked by a proc macro")]
|
||||
pub const fn assert_weakref_supported() {
|
||||
assert!(
|
||||
cfg!(any(not(Py_LIMITED_API), Py_3_9)),
|
||||
"`weakref` requires Python >= 3.9 when using the `abi3` feature"
|
||||
)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[allow(clippy::assertions_on_constants, reason = "invoked by a proc macro")]
|
||||
pub const fn assert_immutable_type_supported() {
|
||||
assert!(
|
||||
cfg!(any(all(Py_3_10, not(Py_LIMITED_API)), Py_3_14)),
|
||||
"`immutable_type` requires Python >= 3.10 (or >= 3.14 when using the `abi3` feature)"
|
||||
);
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#[cfg(feature = "macros")]
|
||||
#[test]
|
||||
fn test_assert_pyclass_send_sync() {
|
||||
#[crate::pyclass(crate = "crate")]
|
||||
struct MyClass {}
|
||||
|
||||
super::assert_pyclass_send_sync::<MyClass>();
|
||||
}
|
||||
}
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
use core::{ffi::CStr, marker::PhantomData};
|
||||
|
||||
use crate::{impl_::pyclass::PyClassImpl, PyClass};
|
||||
|
||||
/// Trait implemented by classes with a known text signature for instantiation.
|
||||
///
|
||||
/// This is implemented by the `#[pymethods]` macro when handling expansion for a
|
||||
/// `#[new]` method.
|
||||
pub trait PyClassNewTextSignature {
|
||||
const TEXT_SIGNATURE: &'static str;
|
||||
}
|
||||
|
||||
/// Type which uses specialization on impl blocks to facilitate generating the documentation for a
|
||||
/// `#[pyclass]` type.
|
||||
///
|
||||
/// At the moment, this is only used to help lift the `TEXT_SIGNATURE` constant to compile time
|
||||
/// providing a base case and a specialized implementation when the signature is known at compile time.
|
||||
///
|
||||
/// In the future when const eval is more advanced, it will probably be possible to format the whole
|
||||
/// class docstring at compile time as part of this type instead of in macro expansion.
|
||||
pub struct PyClassDocGenerator<
|
||||
ClassT: PyClass,
|
||||
// switch to determine if a signature for class instantiation is known
|
||||
const HAS_NEW_TEXT_SIGNATURE: bool,
|
||||
>(PhantomData<ClassT>);
|
||||
|
||||
impl<ClassT: PyClass + PyClassNewTextSignature> PyClassDocGenerator<ClassT, true> {
|
||||
pub const DOC_PIECES: &'static [&'static [u8]] = &[
|
||||
<ClassT as PyClass>::NAME.as_bytes(),
|
||||
ClassT::TEXT_SIGNATURE.as_bytes(),
|
||||
b"\n--\n\n",
|
||||
<ClassT as PyClassImpl>::RAW_DOC.to_bytes_with_nul(),
|
||||
];
|
||||
}
|
||||
|
||||
impl<ClassT: PyClass> PyClassDocGenerator<ClassT, false> {
|
||||
pub const DOC_PIECES: &'static [&'static [u8]] =
|
||||
&[<ClassT as PyClassImpl>::RAW_DOC.to_bytes_with_nul()];
|
||||
}
|
||||
|
||||
/// Casts bytes to a CStr, ensuring they are valid.
|
||||
pub const fn doc_bytes_as_cstr(bytes: &'static [u8]) -> &'static ::core::ffi::CStr {
|
||||
match CStr::from_bytes_with_nul(bytes) {
|
||||
Ok(cstr) => cstr,
|
||||
#[cfg(not(from_bytes_with_nul_error))] // MSRV 1.86
|
||||
Err(_) => panic!("invalid pyclass doc"),
|
||||
#[cfg(from_bytes_with_nul_error)]
|
||||
// This case may happen if the user provides an invalid docstring
|
||||
Err(core::ffi::FromBytesWithNulError::InteriorNul { .. }) => {
|
||||
panic!("pyclass doc contains nul bytes")
|
||||
}
|
||||
// This case shouldn't happen using the macro machinery as long as `PyClassDocGenerator`
|
||||
// uses the RAW_DOC as the final piece, which is nul terminated.
|
||||
#[cfg(from_bytes_with_nul_error)]
|
||||
Err(core::ffi::FromBytesWithNulError::NotNulTerminated) => {
|
||||
panic!("pyclass doc expected to be nul terminated")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "macros")]
|
||||
fn test_doc_generator() {
|
||||
use crate::impl_::concat::{combine_to_array, combined_len};
|
||||
|
||||
/// A dummy class with signature.
|
||||
#[crate::pyclass(crate = "crate")]
|
||||
struct MyClass;
|
||||
|
||||
#[crate::pymethods(crate = "crate")]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new(x: i32, y: i32) -> Self {
|
||||
let _ = (x, y); // suppress unused variable warnings
|
||||
MyClass
|
||||
}
|
||||
}
|
||||
|
||||
// simulate what the macro is doing
|
||||
const PIECES: &[&[u8]] = PyClassDocGenerator::<MyClass, true>::DOC_PIECES;
|
||||
assert_eq!(
|
||||
&combine_to_array::<{ combined_len(PIECES) }>(PIECES),
|
||||
b"MyClass(x, y)\n--\n\nA dummy class with signature.\0"
|
||||
);
|
||||
|
||||
// simulate if the macro detected no text signature
|
||||
const PIECES_WITHOUT_SIGNATURE: &[&[u8]] =
|
||||
PyClassDocGenerator::<MyClass, false>::DOC_PIECES;
|
||||
assert_eq!(
|
||||
&combine_to_array::<{ combined_len(PIECES_WITHOUT_SIGNATURE) }>(
|
||||
PIECES_WITHOUT_SIGNATURE
|
||||
),
|
||||
b"A dummy class with signature.\0"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_doc_bytes_as_cstr() {
|
||||
let cstr = doc_bytes_as_cstr(b"MyClass\0");
|
||||
assert_eq!(cstr, c"MyClass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(from_bytes_with_nul_error)]
|
||||
#[should_panic(expected = "pyclass doc contains nul bytes")]
|
||||
fn test_doc_bytes_as_cstr_central_nul() {
|
||||
doc_bytes_as_cstr(b"MyClass\0Foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(from_bytes_with_nul_error)]
|
||||
#[should_panic(expected = "pyclass doc expected to be nul terminated")]
|
||||
fn test_doc_bytes_as_cstr_not_nul_terminated() {
|
||||
doc_bytes_as_cstr(b"MyClass");
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::{ffi::CStr, marker::PhantomData};
|
||||
use std::thread::{self, ThreadId};
|
||||
|
||||
#[cfg(Py_3_14)]
|
||||
use crate::err::error_on_minusone;
|
||||
#[allow(deprecated)]
|
||||
use crate::sync::GILOnceCell;
|
||||
#[cfg(Py_3_14)]
|
||||
use crate::types::PyTypeMethods;
|
||||
use crate::{
|
||||
exceptions::PyRuntimeError,
|
||||
ffi,
|
||||
impl_::pymethods::PyMethodDefType,
|
||||
pyclass::{create_type_object, PyClassTypeObject},
|
||||
types::PyType,
|
||||
Bound, Py, PyAny, PyClass, PyErr, PyResult, Python,
|
||||
};
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::PyClassItemsIter;
|
||||
|
||||
/// Lazy type object for PyClass.
|
||||
#[doc(hidden)]
|
||||
pub struct LazyTypeObject<T>(LazyTypeObjectInner, PhantomData<T>);
|
||||
|
||||
// Non-generic inner of LazyTypeObject to keep code size down
|
||||
struct LazyTypeObjectInner {
|
||||
#[allow(deprecated)]
|
||||
value: GILOnceCell<PyClassTypeObject>,
|
||||
// Threads which have begun initialization of the `tp_dict`. Used for
|
||||
// reentrant initialization detection.
|
||||
initializing_threads: Mutex<Vec<ThreadId>>,
|
||||
#[allow(deprecated)]
|
||||
fully_initialized_type: GILOnceCell<Py<PyType>>,
|
||||
}
|
||||
|
||||
impl<T> LazyTypeObject<T> {
|
||||
/// Creates an uninitialized `LazyTypeObject`.
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
LazyTypeObject(
|
||||
LazyTypeObjectInner {
|
||||
#[allow(deprecated)]
|
||||
value: GILOnceCell::new(),
|
||||
initializing_threads: Mutex::new(Vec::new()),
|
||||
#[allow(deprecated)]
|
||||
fully_initialized_type: GILOnceCell::new(),
|
||||
},
|
||||
PhantomData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass> LazyTypeObject<T> {
|
||||
/// Gets the type object contained by this `LazyTypeObject`, initializing it if needed.
|
||||
#[inline]
|
||||
pub fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> {
|
||||
if let Some(type_object) = self.0.fully_initialized_type.get(py) {
|
||||
// Fast path
|
||||
return Ok(type_object.bind(py));
|
||||
}
|
||||
|
||||
self.try_init(py)
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> {
|
||||
self.0.get_or_try_init(
|
||||
py,
|
||||
create_type_object::<T>,
|
||||
<T as PyClass>::NAME,
|
||||
T::items_iter(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl LazyTypeObjectInner {
|
||||
// Uses dynamically dispatched fn(Python<'py>) -> PyResult<Py<PyType>
|
||||
// so that this code is only instantiated once, instead of for every T
|
||||
// like the generic LazyTypeObject<T> methods above.
|
||||
fn get_or_try_init<'py>(
|
||||
&self,
|
||||
py: Python<'py>,
|
||||
init: fn(Python<'py>) -> PyResult<PyClassTypeObject>,
|
||||
name: &str,
|
||||
items_iter: PyClassItemsIter,
|
||||
) -> PyResult<&Bound<'py, PyType>> {
|
||||
(|| -> PyResult<_> {
|
||||
let PyClassTypeObject {
|
||||
type_object,
|
||||
is_immutable_type,
|
||||
..
|
||||
} = self.value.get_or_try_init(py, || init(py))?;
|
||||
let type_object = type_object.bind(py);
|
||||
self.ensure_init(type_object, *is_immutable_type, name, items_iter)?;
|
||||
Ok(type_object)
|
||||
})()
|
||||
.map_err(|err| {
|
||||
wrap_in_runtime_error(
|
||||
py,
|
||||
err,
|
||||
format!("An error occurred while initializing class {name}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_init(
|
||||
&self,
|
||||
type_object: &Bound<'_, PyType>,
|
||||
#[allow(unused_variables)] is_immutable_type: bool,
|
||||
name: &str,
|
||||
items_iter: PyClassItemsIter,
|
||||
) -> PyResult<()> {
|
||||
let py = type_object.py();
|
||||
|
||||
// We might want to fill the `tp_dict` with python instances of `T`
|
||||
// itself. In order to do so, we must first initialize the type object
|
||||
// with an empty `tp_dict`: now we can create instances of `T`.
|
||||
//
|
||||
// Then we fill the `tp_dict`. Multiple threads may try to fill it at
|
||||
// the same time, but only one of them will succeed.
|
||||
//
|
||||
// More importantly, if a thread is performing initialization of the
|
||||
// `tp_dict`, it can still request the type object through `get_or_init`,
|
||||
// but the `tp_dict` may appear empty of course.
|
||||
|
||||
if self.fully_initialized_type.get(py).is_some() {
|
||||
// `tp_dict` is already filled: ok.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let thread_id = thread::current().id();
|
||||
{
|
||||
let mut threads = self.initializing_threads.lock().unwrap();
|
||||
if threads.contains(&thread_id) {
|
||||
// Reentrant call: just return the type object, even if the
|
||||
// `tp_dict` is not filled yet.
|
||||
return Ok(());
|
||||
}
|
||||
threads.push(thread_id);
|
||||
}
|
||||
|
||||
struct InitializationGuard<'a> {
|
||||
initializing_threads: &'a Mutex<Vec<ThreadId>>,
|
||||
thread_id: ThreadId,
|
||||
}
|
||||
impl Drop for InitializationGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut threads = self.initializing_threads.lock().unwrap();
|
||||
threads.retain(|id| *id != self.thread_id);
|
||||
}
|
||||
}
|
||||
|
||||
let guard = InitializationGuard {
|
||||
initializing_threads: &self.initializing_threads,
|
||||
thread_id,
|
||||
};
|
||||
|
||||
// Pre-compute the class attribute objects: this can temporarily
|
||||
// release the GIL since we're calling into arbitrary user code. It
|
||||
// means that another thread can continue the initialization in the
|
||||
// meantime: at worst, we'll just make a useless computation.
|
||||
let mut items = vec![];
|
||||
for class_items in items_iter {
|
||||
for method in class_items.methods {
|
||||
if let PyMethodDefType::ClassAttribute(attr) = method {
|
||||
match (attr.meth)(py) {
|
||||
Ok(val) => items.push((attr.name, val)),
|
||||
Err(err) => {
|
||||
return Err(wrap_in_runtime_error(
|
||||
py,
|
||||
err,
|
||||
format!(
|
||||
"An error occurred while initializing `{}.{}`",
|
||||
name,
|
||||
attr.name.to_str().unwrap()
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we hold the GIL and we can assume it won't be released until we
|
||||
// return from the function.
|
||||
let result = self.fully_initialized_type.get_or_try_init(py, move || {
|
||||
initialize_tp_dict(py, type_object.as_ptr(), items)?;
|
||||
#[cfg(Py_3_14)]
|
||||
if is_immutable_type {
|
||||
// freeze immutable types after __dict__ is initialized
|
||||
let res = unsafe { ffi::PyType_Freeze(type_object.as_type_ptr()) };
|
||||
error_on_minusone(py, res)?;
|
||||
}
|
||||
#[cfg(all(Py_3_10, not(Py_LIMITED_API), not(Py_3_14)))]
|
||||
if is_immutable_type {
|
||||
use crate::types::PyTypeMethods as _;
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
unsafe {
|
||||
(*type_object.as_type_ptr()).tp_flags |= ffi::Py_TPFLAGS_IMMUTABLETYPE
|
||||
};
|
||||
#[cfg(Py_GIL_DISABLED)]
|
||||
unsafe {
|
||||
(*type_object.as_type_ptr()).tp_flags.fetch_or(
|
||||
ffi::Py_TPFLAGS_IMMUTABLETYPE,
|
||||
core::sync::atomic::Ordering::Relaxed,
|
||||
)
|
||||
};
|
||||
unsafe { ffi::PyType_Modified(type_object.as_type_ptr()) };
|
||||
}
|
||||
|
||||
// Initialization successfully complete, can clear the thread list.
|
||||
// (No further calls to get_or_init() will try to init, on any thread.)
|
||||
let mut threads = {
|
||||
drop(guard);
|
||||
self.initializing_threads.lock().unwrap()
|
||||
};
|
||||
threads.clear();
|
||||
Ok(type_object.clone().unbind())
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
return Err(wrap_in_runtime_error(
|
||||
py,
|
||||
err,
|
||||
format!("An error occurred while initializing `{name}.__dict__`"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_tp_dict(
|
||||
py: Python<'_>,
|
||||
type_object: *mut ffi::PyObject,
|
||||
items: Vec<(&'static CStr, Py<PyAny>)>,
|
||||
) -> PyResult<()> {
|
||||
// We hold the GIL: the dictionary update can be considered atomic from
|
||||
// the POV of other threads.
|
||||
for (key, val) in items {
|
||||
crate::err::error_on_minusone(py, unsafe {
|
||||
ffi::PyObject_SetAttrString(type_object, key.as_ptr(), val.as_ptr())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// This is necessary for making static `LazyTypeObject`s
|
||||
unsafe impl<T> Sync for LazyTypeObject<T> {}
|
||||
|
||||
/// Used in the macro-expanded implementation of `type_object_raw` for `#[pyclass]` types
|
||||
#[cold]
|
||||
pub fn type_object_init_failed(py: Python<'_>, err: PyErr, type_name: &str) -> ! {
|
||||
err.write_unraisable(py, None);
|
||||
panic!("failed to create type object for `{type_name}`")
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn wrap_in_runtime_error(py: Python<'_>, err: PyErr, message: String) -> PyErr {
|
||||
let runtime_err = PyRuntimeError::new_err(message);
|
||||
runtime_err.set_cause(py, Some(err));
|
||||
runtime_err
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::{FromPyObject, Py};
|
||||
|
||||
/// Trait used to combine with zero-sized types to calculate at compile time
|
||||
/// some property of a type.
|
||||
///
|
||||
/// The trick uses the fact that an associated constant has higher priority
|
||||
/// than a trait constant, so we can use the trait to define the false case.
|
||||
///
|
||||
/// The true case is defined in the zero-sized type's impl block, which is
|
||||
/// gated on some property like trait bound or only being implemented
|
||||
/// for fixed concrete types.
|
||||
pub trait Probe: probe::Sealed {
|
||||
const VALUE: bool = false;
|
||||
}
|
||||
|
||||
/// Seals `Probe` so that types outside PyO3 cannot implement it.
|
||||
mod probe {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
macro_rules! probe {
|
||||
($name:ident) => {
|
||||
pub struct $name<T>(PhantomData<T>);
|
||||
impl<T> Probe for $name<T> {}
|
||||
impl<T> probe::Sealed for $name<T> {}
|
||||
};
|
||||
}
|
||||
|
||||
probe!(IsPyT);
|
||||
|
||||
impl<T> IsPyT<Py<T>> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsIntoPyObjectRef);
|
||||
|
||||
impl<'a, 'py, T: 'a> IsIntoPyObjectRef<T>
|
||||
where
|
||||
&'a T: IntoPyObject<'py>,
|
||||
{
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsSend);
|
||||
|
||||
impl<T: Send> IsSend<T> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsSync);
|
||||
|
||||
impl<T: Sync> IsSync<T> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsFromPyObject);
|
||||
|
||||
impl<'a, 'py, T> IsFromPyObject<T>
|
||||
where
|
||||
T: FromPyObject<'a, 'py>,
|
||||
{
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(HasNewTextSignature);
|
||||
|
||||
impl<T: super::doc::PyClassNewTextSignature> HasNewTextSignature<T> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsClone);
|
||||
|
||||
impl<T: Clone> IsClone<T> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
probe!(IsReturningEmptyTuple);
|
||||
|
||||
impl IsReturningEmptyTuple<()> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
impl<E> IsReturningEmptyTuple<Result<(), E>> {
|
||||
pub const VALUE: bool = true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! value_of {
|
||||
($probe:ident, $ty:ty) => {{
|
||||
#[allow(unused_imports)] // probe trait not used if VALUE is true
|
||||
use crate::impl_::pyclass::Probe as _;
|
||||
$probe::<$ty>::VALUE
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use value_of;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
/// Re-exported so that macros can name this internal type.
|
||||
pub use crate::internal::pyclass_init::PyNativeTypeInitializer;
|
||||
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
|
||||
use crate::{
|
||||
ffi,
|
||||
ffi_ptr_ext::FfiPtrExt,
|
||||
py_result_ext::PyResultExt,
|
||||
types::{PyCFunction, PyModule, PyModuleMethods},
|
||||
Borrowed, Bound, PyResult, Python,
|
||||
};
|
||||
|
||||
pub use crate::impl_::pymethods::PyMethodDef;
|
||||
|
||||
/// Wrapper around `ffi::PyMethodDef` suitable to use as a static variable for `#[pyfunction]` values.
|
||||
///
|
||||
/// The `UnsafeCell` is used because the Python interpreter consumes these as `*mut ffi::PyMethodDef`.
|
||||
pub struct PyFunctionDef(UnsafeCell<ffi::PyMethodDef>);
|
||||
|
||||
// Safety: contents are only ever used by the Python interpreter, which uses global statics in this way.
|
||||
unsafe impl Sync for PyFunctionDef {}
|
||||
|
||||
impl PyFunctionDef {
|
||||
pub const fn new(def: ffi::PyMethodDef) -> Self {
|
||||
Self(UnsafeCell::new(def))
|
||||
}
|
||||
|
||||
pub const fn from_method_def(def: PyMethodDef) -> Self {
|
||||
Self::new(def.into_raw())
|
||||
}
|
||||
|
||||
pub(crate) fn create_py_c_function<'py>(
|
||||
&'static self,
|
||||
py: Python<'py>,
|
||||
module: Option<&Bound<'py, PyModule>>,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
// Safety: self is static
|
||||
unsafe { create_py_c_function(py, self.0.get(), module) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to enable the use of `wrap_pyfunction` with both `Python` and `PyModule`,
|
||||
/// and also to infer the return type of either `&'py PyCFunction` or `Bound<'py, PyCFunction>`.
|
||||
pub trait WrapPyFunctionArg<'py, T>: wrap_pyfunctionarg::Sealed {
|
||||
fn wrap_pyfunction(self, function_def: &'static PyFunctionDef) -> PyResult<T>;
|
||||
}
|
||||
|
||||
/// Seals `WrapPyFunctionArg` so that types outside PyO3 cannot implement it.
|
||||
mod wrap_pyfunctionarg {
|
||||
use crate::{types::PyModule, Borrowed, Bound, Python};
|
||||
|
||||
pub trait Sealed {}
|
||||
impl<'py> Sealed for Bound<'py, PyModule> {}
|
||||
impl<'py> Sealed for &Bound<'py, PyModule> {}
|
||||
impl<'a, 'py> Sealed for Borrowed<'a, 'py, PyModule> {}
|
||||
impl<'a, 'py> Sealed for &Borrowed<'a, 'py, PyModule> {}
|
||||
impl<'py> Sealed for Python<'py> {}
|
||||
}
|
||||
|
||||
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Bound<'py, PyModule> {
|
||||
fn wrap_pyfunction(
|
||||
self,
|
||||
function_def: &'static PyFunctionDef,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
function_def.create_py_c_function(self.py(), Some(&self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Bound<'py, PyModule> {
|
||||
fn wrap_pyfunction(
|
||||
self,
|
||||
function_def: &'static PyFunctionDef,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
function_def.create_py_c_function(self.py(), Some(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Borrowed<'_, 'py, PyModule> {
|
||||
fn wrap_pyfunction(
|
||||
self,
|
||||
function_def: &'static PyFunctionDef,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
function_def.create_py_c_function(self.py(), Some(&self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, 'py, PyModule> {
|
||||
fn wrap_pyfunction(
|
||||
self,
|
||||
function_def: &'static PyFunctionDef,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
function_def.create_py_c_function(self.py(), Some(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Python<'py> {
|
||||
fn wrap_pyfunction(
|
||||
self,
|
||||
function_def: &'static PyFunctionDef,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
function_def.create_py_c_function(self, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `PyCFunction` object from a `PyMethodDef`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `method_def` pointer must be valid for the lifetime of the returned `PyCFunction`
|
||||
/// (effectively, it must be a static variable).
|
||||
pub unsafe fn create_py_c_function<'py>(
|
||||
py: Python<'py>,
|
||||
method_def: *mut ffi::PyMethodDef,
|
||||
module: Option<&Bound<'py, PyModule>>,
|
||||
) -> PyResult<Bound<'py, PyCFunction>> {
|
||||
let (mod_ptr, module_name) = if let Some(m) = module {
|
||||
let mod_ptr = m.as_ptr();
|
||||
(mod_ptr, Some(m.name()?))
|
||||
} else {
|
||||
(core::ptr::null_mut(), None)
|
||||
};
|
||||
|
||||
let module_name_ptr = module_name
|
||||
.as_ref()
|
||||
.map_or(core::ptr::null_mut(), Bound::as_ptr);
|
||||
|
||||
unsafe {
|
||||
ffi::PyCFunction_NewEx(method_def, mod_ptr, module_name_ptr)
|
||||
.assume_owned_or_err(py)
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "macros")]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_wrap_pyfunction_forms() {
|
||||
use crate::types::{PyAnyMethods, PyModule};
|
||||
use crate::{wrap_pyfunction, Python};
|
||||
|
||||
#[crate::pyfunction(crate = "crate")]
|
||||
fn f() {}
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::new(py, "test_wrap_pyfunction_forms").unwrap();
|
||||
|
||||
let func = wrap_pyfunction!(f, module.clone()).unwrap();
|
||||
func.call0().unwrap();
|
||||
|
||||
let func = wrap_pyfunction!(f, &module).unwrap();
|
||||
func.call0().unwrap();
|
||||
|
||||
let module_borrowed = module.as_borrowed();
|
||||
|
||||
let func = wrap_pyfunction!(f, module_borrowed).unwrap();
|
||||
func.call0().unwrap();
|
||||
|
||||
let func = wrap_pyfunction!(f, &module_borrowed).unwrap();
|
||||
func.call0().unwrap();
|
||||
|
||||
let func = wrap_pyfunction!(f, py).unwrap();
|
||||
func.call0().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+834
@@ -0,0 +1,834 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::exceptions::PyStopAsyncIteration;
|
||||
use crate::impl_::callback::IntoPyCallbackOutput;
|
||||
use crate::impl_::panic::PanicTrap;
|
||||
use crate::impl_::pycell::PyClassObjectBaseLayout;
|
||||
use crate::impl_::pyclass::PyClassDict as _;
|
||||
use crate::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE};
|
||||
use crate::internal::pyclass_init::PyClassInit;
|
||||
use crate::internal::state::ForbidAttaching;
|
||||
use crate::pycell::impl_::{PyClassBorrowChecker as _, PyClassObjectLayout};
|
||||
use crate::types::PyType;
|
||||
use crate::{
|
||||
ffi, Borrowed, Bound, Py, PyAny, PyClass, PyErr, PyResult, PyTraverseError, PyVisit, Python,
|
||||
};
|
||||
use core::ffi::CStr;
|
||||
use core::ffi::{c_int, c_void};
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
use core::panic::AssertUnwindSafe;
|
||||
use core::ptr::{null_mut, NonNull};
|
||||
use std::panic::catch_unwind;
|
||||
|
||||
use super::pyclass::PyClassImpl;
|
||||
use super::trampoline;
|
||||
use crate::internal_tricks::{clear_eq, traverse_eq};
|
||||
|
||||
/// `PyMethodDefType` represents different types of Python callable objects.
|
||||
/// It is used by the `#[pymethods]` attribute.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum PyMethodDefType {
|
||||
/// Represents a class method (might be `classmethod` or `staticmethod`, depends on `ml_flags`)
|
||||
Method(PyMethodDef),
|
||||
/// Represents class attribute, used by `#[attribute]`
|
||||
ClassAttribute(PyClassAttributeDef),
|
||||
/// Represents getter descriptor, used by `#[getter]`
|
||||
Getter(PyGetterDef),
|
||||
/// Represents setter descriptor, used by `#[setter]` and `#[deleter]`
|
||||
Setter(PySetterDef),
|
||||
/// Represents deleter descriptor, used by `#[deleter]`
|
||||
Deleter(PyDeleterDef),
|
||||
/// Represents a struct member
|
||||
StructMember(ffi::PyMemberDef),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum PyMethodType {
|
||||
PyCFunction(ffi::PyCFunction),
|
||||
PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords),
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
PyCFunctionFastWithKeywords(ffi::PyCFunctionFastWithKeywords),
|
||||
}
|
||||
|
||||
pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult<Py<PyAny>>;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PyMethodDef {
|
||||
pub(crate) ml_name: &'static CStr,
|
||||
pub(crate) ml_meth: PyMethodType,
|
||||
pub(crate) ml_flags: c_int,
|
||||
pub(crate) ml_doc: &'static CStr,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PyClassAttributeDef {
|
||||
pub(crate) name: &'static CStr,
|
||||
pub(crate) meth: PyClassAttributeFactory,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PyGetterDef {
|
||||
pub(crate) name: &'static CStr,
|
||||
pub(crate) meth: Getter,
|
||||
pub(crate) doc: Option<&'static CStr>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PySetterDef {
|
||||
pub(crate) name: &'static CStr,
|
||||
pub(crate) meth: Setter,
|
||||
pub(crate) doc: Option<&'static CStr>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PyDeleterDef {
|
||||
pub(crate) name: &'static CStr,
|
||||
pub(crate) meth: Deleter,
|
||||
pub(crate) doc: Option<&'static CStr>,
|
||||
}
|
||||
|
||||
/// Abstraction around fastcall calling convention, which is only available in Python 3.10 and up,
|
||||
/// can inline this directly into the proc macro when Python 3.10 support dropped
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! maybe_define_fastcall_function_with_keywords {
|
||||
($name:ident, $py:ident, $slf:ident, $args:ident, $nargs:ident, $kwargs:ident, $body:block) => {
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn $name<'py>(
|
||||
$py: $crate::Python<'py>,
|
||||
$slf: *mut $crate::ffi::PyObject,
|
||||
$args: *const *mut $crate::ffi::PyObject,
|
||||
$nargs: $crate::ffi::Py_ssize_t,
|
||||
$kwargs: *mut $crate::ffi::PyObject
|
||||
) -> $crate::PyResult<*mut $crate::ffi::PyObject> $body
|
||||
};
|
||||
}
|
||||
|
||||
/// On older abi3 versions, required to use varargs calling convention
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! maybe_define_fastcall_function_with_keywords {
|
||||
($name:ident, $py:ident, $slf:ident, $args:ident, $nargs:ident, $kwargs:ident, $body:block) => {
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn $name<'py>(
|
||||
$py: $crate::Python<'py>,
|
||||
$slf: *mut $crate::ffi::PyObject,
|
||||
$args: *mut $crate::ffi::PyObject,
|
||||
$kwargs: *mut $crate::ffi::PyObject
|
||||
) -> $crate::PyResult<*mut $crate::ffi::PyObject> $body
|
||||
};
|
||||
}
|
||||
|
||||
pub use crate::maybe_define_fastcall_function_with_keywords;
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! maybe_extract_arguments_fastcall {
|
||||
($description:ident, $py:ident, $args:ident, $nargs:ident, $kwargs:ident, $args_array:ident, $args_handler:ty, $kwargs_handler:ty) => {
|
||||
// SAFETY: guaranteed by the proc macro that all args to extract_arguments_fastcall are valid
|
||||
unsafe {
|
||||
$description.extract_arguments_fastcall::<$args_handler, $kwargs_handler>(
|
||||
$py,
|
||||
$args,
|
||||
$nargs,
|
||||
$kwargs,
|
||||
&mut $args_array,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! maybe_extract_arguments_fastcall {
|
||||
($description:ident, $py:ident, $args:ident, $nargs:ident, $kwargs:ident, $args_array:ident, $args_handler:ty, $kwargs_handler:ty) => {
|
||||
// SAFETY: guaranteed by the proc macro that all args to extract_arguments_tuple_dict are valid
|
||||
unsafe {
|
||||
$description.extract_arguments_tuple_dict::<$args_handler, $kwargs_handler>(
|
||||
$py,
|
||||
$args,
|
||||
$kwargs,
|
||||
&mut $args_array,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub use crate::maybe_extract_arguments_fastcall;
|
||||
|
||||
impl PyMethodDef {
|
||||
/// Define a function that takes no arguments.
|
||||
pub const fn noargs(
|
||||
ml_name: &'static CStr,
|
||||
cfunction: ffi::PyCFunction,
|
||||
ml_doc: &'static CStr,
|
||||
) -> Self {
|
||||
Self {
|
||||
ml_name,
|
||||
ml_meth: PyMethodType::PyCFunction(cfunction),
|
||||
ml_flags: ffi::METH_NOARGS,
|
||||
ml_doc,
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a function that takes arbitrary arguments as a tuple and dict.
|
||||
pub const fn cfunction_with_keywords(
|
||||
ml_name: &'static CStr,
|
||||
cfunction: ffi::PyCFunctionWithKeywords,
|
||||
ml_doc: &'static CStr,
|
||||
) -> Self {
|
||||
Self {
|
||||
ml_name,
|
||||
ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction),
|
||||
ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS,
|
||||
ml_doc,
|
||||
}
|
||||
}
|
||||
|
||||
/// Define a function that takes arbitrary arguments as a C-style array and tuple of keyword arguments.
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
pub const fn fastcall_cfunction_with_keywords(
|
||||
ml_name: &'static CStr,
|
||||
cfunction: ffi::PyCFunctionFastWithKeywords,
|
||||
ml_doc: &'static CStr,
|
||||
) -> Self {
|
||||
Self {
|
||||
ml_name,
|
||||
ml_meth: PyMethodType::PyCFunctionFastWithKeywords(cfunction),
|
||||
ml_flags: ffi::METH_FASTCALL | ffi::METH_KEYWORDS,
|
||||
ml_doc,
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction over fastcall to fall back to varargs on older Python versions.
|
||||
pub const fn maybe_fastcall_cfunction_with_keywords(
|
||||
ml_name: &'static CStr,
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] cfunction: ffi::PyCFunctionFastWithKeywords,
|
||||
// on older abi3 versions, Fastcall not supported
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] cfunction: ffi::PyCFunctionWithKeywords,
|
||||
ml_doc: &'static CStr,
|
||||
) -> Self {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
Self::fastcall_cfunction_with_keywords(ml_name, cfunction, ml_doc)
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
{
|
||||
Self::cfunction_with_keywords(ml_name, cfunction, ml_doc)
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn flags(mut self, flags: c_int) -> Self {
|
||||
self.ml_flags |= flags;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn into_raw(self) -> ffi::PyMethodDef {
|
||||
let meth = match self.ml_meth {
|
||||
PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth },
|
||||
PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer {
|
||||
PyCFunctionWithKeywords: meth,
|
||||
},
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer {
|
||||
PyCFunctionFastWithKeywords: meth,
|
||||
},
|
||||
};
|
||||
|
||||
ffi::PyMethodDef {
|
||||
ml_name: self.ml_name.as_ptr(),
|
||||
ml_meth: meth,
|
||||
ml_flags: self.ml_flags,
|
||||
ml_doc: self.ml_doc.as_ptr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PyClassAttributeDef {
|
||||
/// Define a class attribute.
|
||||
pub const fn new(name: &'static CStr, meth: PyClassAttributeFactory) -> Self {
|
||||
Self { name, meth }
|
||||
}
|
||||
}
|
||||
|
||||
// Manual implementation because `Python<'_>` does not implement `Debug` and
|
||||
// trait bounds on `fn` compiler-generated derive impls are too restrictive.
|
||||
impl fmt::Debug for PyClassAttributeDef {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PyClassAttributeDef")
|
||||
.field("name", &self.name)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Class getter / setters
|
||||
pub(crate) type Getter =
|
||||
for<'py> unsafe fn(Python<'py>, NonNull<ffi::PyObject>) -> PyResult<*mut ffi::PyObject>;
|
||||
pub(crate) type Setter = for<'py> unsafe fn(
|
||||
Python<'py>,
|
||||
NonNull<ffi::PyObject>,
|
||||
NonNull<ffi::PyObject>,
|
||||
) -> PyResult<c_int>;
|
||||
pub(crate) type Deleter =
|
||||
for<'py> unsafe fn(Python<'py>, NonNull<ffi::PyObject>) -> PyResult<c_int>;
|
||||
|
||||
impl PyGetterDef {
|
||||
/// Define a getter.
|
||||
pub const fn new(name: &'static CStr, getter: Getter, doc: Option<&'static CStr>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
meth: getter,
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PySetterDef {
|
||||
/// Define a setter.
|
||||
pub const fn new(name: &'static CStr, setter: Setter, doc: Option<&'static CStr>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
meth: setter,
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PyDeleterDef {
|
||||
/// Define a deleter.
|
||||
pub const fn new(name: &'static CStr, deleter: Deleter, doc: Option<&'static CStr>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
meth: deleter,
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls an implementation of __traverse__ for tp_traverse
|
||||
///
|
||||
/// NB cannot accept `'static` visitor, this is a sanity check below:
|
||||
///
|
||||
/// ```rust,compile_fail
|
||||
/// use pyo3::prelude::*;
|
||||
/// use pyo3::pyclass::{PyTraverseError, PyVisit};
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// struct Foo;
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Foo {
|
||||
/// fn __traverse__(&self, _visit: PyVisit<'static>) -> Result<(), PyTraverseError> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Elided lifetime should compile ok:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pyo3::prelude::*;
|
||||
/// use pyo3::pyclass::{PyTraverseError, PyVisit};
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// struct Foo;
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Foo {
|
||||
/// fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub unsafe fn _call_traverse<T>(
|
||||
slf: *mut ffi::PyObject,
|
||||
impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>,
|
||||
visit: ffi::visitproc,
|
||||
arg: *mut c_void,
|
||||
current_traverse: ffi::traverseproc,
|
||||
) -> c_int
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
// It is important the implementation of `__traverse__` cannot safely access the interpreter,
|
||||
// c.f. https://github.com/PyO3/pyo3/issues/3165, and hence we do not expose our Python
|
||||
// token to the user code and forbid safe methods for attaching.
|
||||
// (This includes enforcing the `&self` method receiver as e.g. `PyRef<Self>` could
|
||||
// reconstruct a Python token via `PyRef::py`.)
|
||||
//
|
||||
// The traversal lives in `traverse_impl` so that it can return early: `trap` is armed until
|
||||
// `disarm` below, and dropping it armed panics, which aborts out of `tp_traverse`.
|
||||
let trap = PanicTrap::new("uncaught panic inside __traverse__ handler");
|
||||
let lock = ForbidAttaching::during_traverse();
|
||||
|
||||
let retval = unsafe { traverse_impl(slf, impl_, visit, arg, current_traverse) };
|
||||
|
||||
// Drop lock before trap just in case dropping lock panics
|
||||
drop(lock);
|
||||
trap.disarm();
|
||||
retval
|
||||
}
|
||||
|
||||
/// Visits the base type, the instance `__dict__` and the pyclass's own data, stopping as soon as
|
||||
/// one of them returns non-zero.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `slf` must be a valid pointer to an instance of `T`.
|
||||
/// - Must only be called from `_call_traverse`, which holds the `PanicTrap` and `ForbidAttaching`
|
||||
/// lock this relies on.
|
||||
unsafe fn traverse_impl<T>(
|
||||
slf: *mut ffi::PyObject,
|
||||
impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>,
|
||||
visit: ffi::visitproc,
|
||||
arg: *mut c_void,
|
||||
current_traverse: ffi::traverseproc,
|
||||
) -> c_int
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
// A non-zero return means a `visitproc` has asked us to stop the traversal.
|
||||
let super_retval = unsafe { call_super_traverse(slf, visit, arg, current_traverse) };
|
||||
if super_retval != 0 {
|
||||
return super_retval;
|
||||
}
|
||||
|
||||
// SAFETY: `slf` is a valid Python object pointer to a class object of type T, and
|
||||
// traversal is running so no mutations can occur.
|
||||
let class_object: &<T as PyClassImpl>::Layout = unsafe { &*slf.cast() };
|
||||
|
||||
// The `__dict__` is not Rust data, so it is visited without the thread and borrow checks
|
||||
// below: it must stay reachable to the GC even when the pyclass data cannot be traversed.
|
||||
let dict_retval = unsafe { class_object.contents().dict.traverse_dict(visit, arg) };
|
||||
if dict_retval != 0 {
|
||||
return dict_retval;
|
||||
}
|
||||
|
||||
// `#[pyclass(unsendable)]` types can only be deallocated by their own thread, so do not
|
||||
// traverse them if not on their owning thread :(
|
||||
// ... and we cannot traverse a type which might be being mutated by a Rust thread.
|
||||
if class_object.check_threadsafe().is_err()
|
||||
|| class_object.borrow_checker().try_borrow().is_err()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct TraverseGuard<'a, T: PyClassImpl>(&'a T::Layout);
|
||||
impl<T: PyClassImpl> Drop for TraverseGuard<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.0.borrow_checker().release_borrow()
|
||||
}
|
||||
}
|
||||
|
||||
// `.try_borrow()` above created a borrow, we need to release it when we're done
|
||||
// traversing the object. This allows us to read `instance` safely.
|
||||
let _guard = TraverseGuard::<T>(class_object);
|
||||
let instance = unsafe { &*class_object.contents().value.get() };
|
||||
|
||||
let visit = PyVisit {
|
||||
visit,
|
||||
arg,
|
||||
_guard: PhantomData,
|
||||
};
|
||||
|
||||
match catch_unwind(AssertUnwindSafe(move || impl_(instance, visit))) {
|
||||
Ok(Ok(())) => 0,
|
||||
Ok(Err(traverse_error)) => traverse_error.into_inner(),
|
||||
Err(_err) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Call super-type traverse method, if necessary.
|
||||
///
|
||||
/// Adapted from <https://github.com/cython/cython/blob/7acfb375fb54a033f021b0982a3cd40c34fb22ac/Cython/Utility/ExtensionTypes.c#L386>
|
||||
///
|
||||
/// TODO: There are possible optimizations over looking up the base type in this way
|
||||
/// - if the base type is known in this module, can potentially look it up directly in module state
|
||||
/// (when we have it)
|
||||
/// - if the base type is a Python builtin, can jut call the C function directly
|
||||
/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to
|
||||
/// tp_alloc where we solve this at compile time
|
||||
unsafe fn call_super_traverse(
|
||||
obj: *mut ffi::PyObject,
|
||||
visit: ffi::visitproc,
|
||||
arg: *mut c_void,
|
||||
current_traverse: ffi::traverseproc,
|
||||
) -> c_int {
|
||||
// SAFETY: in this function here it's ok to work with raw type objects `ffi::Py_TYPE`
|
||||
// because the GC is running and so
|
||||
// - (a) we cannot do refcounting and
|
||||
// - (b) the type of the object cannot change.
|
||||
let mut ty = unsafe { ffi::Py_TYPE(obj) };
|
||||
let mut traverse: Option<ffi::traverseproc>;
|
||||
|
||||
// First find the current type by the current_traverse function
|
||||
loop {
|
||||
traverse = unsafe { get_slot(ty, TP_TRAVERSE) };
|
||||
if traverse_eq(traverse, current_traverse) {
|
||||
break;
|
||||
}
|
||||
ty = unsafe { get_slot(ty, TP_BASE) };
|
||||
if ty.is_null() {
|
||||
// FIXME: return an error if current type not in the MRO? Should be impossible.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Get first base which has a different traverse function
|
||||
while traverse_eq(traverse, current_traverse) {
|
||||
ty = unsafe { get_slot(ty, TP_BASE) };
|
||||
if ty.is_null() {
|
||||
break;
|
||||
}
|
||||
traverse = unsafe { get_slot(ty, TP_TRAVERSE) };
|
||||
}
|
||||
|
||||
// If we found a type with a different traverse function, call it
|
||||
if let Some(traverse) = traverse {
|
||||
return unsafe { traverse(obj, visit, arg) };
|
||||
}
|
||||
|
||||
// FIXME same question as cython: what if the current type is not in the MRO?
|
||||
0
|
||||
}
|
||||
|
||||
/// Calls an implementation of __clear__ for tp_clear
|
||||
pub unsafe fn _call_clear<T>(
|
||||
slf: *mut ffi::PyObject,
|
||||
impl_: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<()>,
|
||||
current_clear: ffi::inquiry,
|
||||
) -> c_int
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
unsafe {
|
||||
trampoline::trampoline(move |py| {
|
||||
let super_retval = call_super_clear(py, slf, current_clear);
|
||||
if super_retval != 0 {
|
||||
return Err(PyErr::fetch(py));
|
||||
}
|
||||
impl_(py, slf)?;
|
||||
|
||||
// Clear the `__dict__`, breaking any reference cycle through the instance's
|
||||
// attributes.
|
||||
//
|
||||
// SAFETY: `slf` is a valid instance of `T`. A shared reference suffices: clearing
|
||||
// the `__dict__` never touches the pyclass data, so needs no borrow check.
|
||||
let class_object: &<T as PyClassImpl>::Layout = &*slf.cast();
|
||||
class_object.contents().dict.clear_dict(py);
|
||||
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `tp_traverse` for a `#[pyclass]` which defines no `__traverse__` of its own: visits the
|
||||
/// base type and the instance `__dict__` (if it is a `#[pyclass(dict)]`).
|
||||
pub unsafe extern "C" fn synthesized_traverse<T>(
|
||||
slf: *mut ffi::PyObject,
|
||||
visit: ffi::visitproc,
|
||||
arg: *mut c_void,
|
||||
) -> c_int
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
let super_retval = unsafe { call_super_traverse(slf, visit, arg, synthesized_traverse::<T>) };
|
||||
if super_retval != 0 {
|
||||
return super_retval;
|
||||
}
|
||||
|
||||
// SAFETY: `slf` is a valid pointer to an instance of `T`, and traversal is running so no
|
||||
// mutations can occur. The `__dict__` is not Rust data, so needs no thread or borrow check.
|
||||
let class_object: &<T as PyClassImpl>::Layout = unsafe { &*slf.cast() };
|
||||
unsafe { class_object.contents().dict.traverse_dict(visit, arg) }
|
||||
}
|
||||
|
||||
/// `tp_clear` for a `#[pyclass]` which defines no `__clear__` of its own: calls the base type
|
||||
/// and clears the instance `__dict__` (if it is a `#[pyclass(dict)]`).
|
||||
pub unsafe extern "C" fn synthesized_clear<T>(slf: *mut ffi::PyObject) -> c_int
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
unsafe { _call_clear::<T>(slf, |_, _| Ok(()), synthesized_clear::<T>) }
|
||||
}
|
||||
|
||||
/// Call super-type traverse method, if necessary.
|
||||
///
|
||||
/// Adapted from <https://github.com/cython/cython/blob/7acfb375fb54a033f021b0982a3cd40c34fb22ac/Cython/Utility/ExtensionTypes.c#L386>
|
||||
///
|
||||
/// TODO: There are possible optimizations over looking up the base type in this way
|
||||
/// - if the base type is known in this module, can potentially look it up directly in module state
|
||||
/// (when we have it)
|
||||
/// - if the base type is a Python builtin, can jut call the C function directly
|
||||
/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to
|
||||
/// tp_alloc where we solve this at compile time
|
||||
unsafe fn call_super_clear(
|
||||
py: Python<'_>,
|
||||
obj: *mut ffi::PyObject,
|
||||
current_clear: ffi::inquiry,
|
||||
) -> c_int {
|
||||
let mut ty = unsafe { PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj)) };
|
||||
let mut clear: Option<ffi::inquiry>;
|
||||
|
||||
// First find the current type by the current_clear function
|
||||
loop {
|
||||
clear = ty.get_slot(TP_CLEAR);
|
||||
if clear_eq(clear, current_clear) {
|
||||
break;
|
||||
}
|
||||
let base = ty.get_slot(TP_BASE);
|
||||
if base.is_null() {
|
||||
// FIXME: return an error if current type not in the MRO? Should be impossible.
|
||||
return 0;
|
||||
}
|
||||
ty = unsafe { PyType::from_borrowed_type_ptr(py, base) };
|
||||
}
|
||||
|
||||
// Get first base which has a different clear function
|
||||
while clear_eq(clear, current_clear) {
|
||||
let base = ty.get_slot(TP_BASE);
|
||||
if base.is_null() {
|
||||
break;
|
||||
}
|
||||
ty = unsafe { PyType::from_borrowed_type_ptr(py, base) };
|
||||
clear = ty.get_slot(TP_CLEAR);
|
||||
}
|
||||
|
||||
// If we found a type with a different clear function, call it
|
||||
if let Some(clear) = clear {
|
||||
return unsafe { clear(obj) };
|
||||
}
|
||||
|
||||
// FIXME same question as cython: what if the current type is not in the MRO?
|
||||
0
|
||||
}
|
||||
|
||||
// Autoref-based specialization for handling `__next__` returning `Option`
|
||||
|
||||
pub struct IterBaseTag;
|
||||
|
||||
impl IterBaseTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value, Target>(self, py: Python<'py>, value: Value) -> PyResult<Target>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, Target>,
|
||||
{
|
||||
value.convert(py)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IterBaseKind {
|
||||
#[inline]
|
||||
fn iter_tag(&self) -> IterBaseTag {
|
||||
IterBaseTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value> IterBaseKind for &Value {}
|
||||
|
||||
pub struct IterOptionTag;
|
||||
|
||||
impl IterOptionTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value>(
|
||||
self,
|
||||
py: Python<'py>,
|
||||
value: Option<Value>,
|
||||
) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
|
||||
{
|
||||
match value {
|
||||
Some(value) => value.convert(py),
|
||||
None => Ok(null_mut()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IterOptionKind {
|
||||
#[inline]
|
||||
fn iter_tag(&self) -> IterOptionTag {
|
||||
IterOptionTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value> IterOptionKind for Option<Value> {}
|
||||
|
||||
pub struct IterResultOptionTag;
|
||||
|
||||
impl IterResultOptionTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value, Error>(
|
||||
self,
|
||||
py: Python<'py>,
|
||||
value: Result<Option<Value>, Error>,
|
||||
) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
|
||||
Error: Into<PyErr>,
|
||||
{
|
||||
match value {
|
||||
Ok(Some(value)) => value.convert(py),
|
||||
Ok(None) => Ok(null_mut()),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IterResultOptionKind {
|
||||
#[inline]
|
||||
fn iter_tag(&self) -> IterResultOptionTag {
|
||||
IterResultOptionTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> IterResultOptionKind for Result<Option<Value>, Error> {}
|
||||
|
||||
// Autoref-based specialization for handling `__anext__` returning `Option`
|
||||
|
||||
pub struct AsyncIterBaseTag;
|
||||
|
||||
impl AsyncIterBaseTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value, Target>(self, py: Python<'py>, value: Value) -> PyResult<Target>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, Target>,
|
||||
{
|
||||
value.convert(py)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsyncIterBaseKind {
|
||||
#[inline]
|
||||
fn async_iter_tag(&self) -> AsyncIterBaseTag {
|
||||
AsyncIterBaseTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value> AsyncIterBaseKind for &Value {}
|
||||
|
||||
pub struct AsyncIterOptionTag;
|
||||
|
||||
impl AsyncIterOptionTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value>(
|
||||
self,
|
||||
py: Python<'py>,
|
||||
value: Option<Value>,
|
||||
) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
|
||||
{
|
||||
match value {
|
||||
Some(value) => value.convert(py),
|
||||
None => Err(PyStopAsyncIteration::new_err(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsyncIterOptionKind {
|
||||
#[inline]
|
||||
fn async_iter_tag(&self) -> AsyncIterOptionTag {
|
||||
AsyncIterOptionTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value> AsyncIterOptionKind for Option<Value> {}
|
||||
|
||||
pub struct AsyncIterResultOptionTag;
|
||||
|
||||
impl AsyncIterResultOptionTag {
|
||||
#[inline]
|
||||
pub fn convert<'py, Value, Error>(
|
||||
self,
|
||||
py: Python<'py>,
|
||||
value: Result<Option<Value>, Error>,
|
||||
) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>,
|
||||
Error: Into<PyErr>,
|
||||
{
|
||||
match value {
|
||||
Ok(Some(value)) => value.convert(py),
|
||||
Ok(None) => Err(PyStopAsyncIteration::new_err(())),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsyncIterResultOptionKind {
|
||||
#[inline]
|
||||
fn async_iter_tag(&self) -> AsyncIterResultOptionTag {
|
||||
AsyncIterResultOptionTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<Value, Error> AsyncIterResultOptionKind for Result<Option<Value>, Error> {}
|
||||
|
||||
/// Re-exported so that `#[new]` generated code can resolve the type tag for `tp_new_impl`
|
||||
pub use crate::internal::pyclass_init::tp_new_resolver;
|
||||
|
||||
#[expect(
|
||||
private_bounds,
|
||||
reason = "`PyClassInit` is not a public trait, bound exist for diagnostics"
|
||||
)]
|
||||
/// # SAFETY
|
||||
/// - `cls` must be the type object for `ClassT` (or a subclass)
|
||||
pub unsafe fn tp_new_impl<'py, InitializerT, ClassT>(
|
||||
py: Python<'py>,
|
||||
initializer: InitializerT,
|
||||
cls: *mut ffi::PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
InitializerT: PyClassInit<'py, ClassT>,
|
||||
{
|
||||
// SAFETY: caller has guaranteed `cls` is the correct object
|
||||
unsafe { initializer.init(Borrowed::from_ptr_unchecked(py, cls.cast()).cast_unchecked()) }
|
||||
.map(Bound::into_ptr)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
fn test_fastcall_function_with_keywords() {
|
||||
use super::PyMethodDef;
|
||||
use crate::impl_::pyfunction::PyFunctionDef;
|
||||
use crate::types::PyAnyMethods;
|
||||
use crate::{ffi, Python};
|
||||
|
||||
Python::attach(|py| {
|
||||
let def =
|
||||
PyFunctionDef::from_method_def(PyMethodDef::fastcall_cfunction_with_keywords(
|
||||
c"test",
|
||||
accepts_no_arguments,
|
||||
c"doc",
|
||||
));
|
||||
// leak to make it 'static
|
||||
// deliberately done at runtime to have coverage of `PyFunctionDef::from_method_def`
|
||||
let def = Box::leak(Box::new(def));
|
||||
|
||||
unsafe extern "C" fn accepts_no_arguments(
|
||||
_slf: *mut ffi::PyObject,
|
||||
_args: *const *mut ffi::PyObject,
|
||||
nargs: ffi::Py_ssize_t,
|
||||
kwargs: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject {
|
||||
assert_eq!(nargs, 0);
|
||||
assert!(kwargs.is_null());
|
||||
unsafe { Python::assume_attached().None().into_ptr() }
|
||||
}
|
||||
|
||||
let f = def.create_py_c_function(py, None).unwrap();
|
||||
|
||||
f.call0().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+742
@@ -0,0 +1,742 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code.
|
||||
use core::{
|
||||
cell::UnsafeCell,
|
||||
ffi::CStr,
|
||||
ffi::{c_int, c_void},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
#[cfg(all(
|
||||
not(any(PyPy, GraalPy)),
|
||||
Py_3_9,
|
||||
not(all(windows, Py_LIMITED_API, not(Py_3_10))),
|
||||
))]
|
||||
use core::sync::atomic::Ordering;
|
||||
|
||||
#[cfg(all(
|
||||
not(any(PyPy, GraalPy)),
|
||||
Py_3_9,
|
||||
not(all(windows, Py_LIMITED_API, not(Py_3_10))),
|
||||
target_has_atomic = "64",
|
||||
))]
|
||||
use core::sync::atomic::AtomicI64;
|
||||
#[cfg(all(
|
||||
not(any(PyPy, GraalPy)),
|
||||
Py_3_9,
|
||||
not(all(windows, Py_LIMITED_API, not(Py_3_10))),
|
||||
not(target_has_atomic = "64"),
|
||||
))]
|
||||
use portable_atomic::AtomicI64;
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
use crate::exceptions::PyImportError;
|
||||
#[cfg(any(not(all(Py_LIMITED_API, Py_GIL_DISABLED)), Py_3_15))]
|
||||
use crate::internal_tricks::array_ptr_as_mut;
|
||||
use crate::prelude::PyTypeMethods;
|
||||
use crate::{
|
||||
ffi,
|
||||
impl_::pyfunction::PyFunctionDef,
|
||||
types::{PyModule, PyModuleMethods},
|
||||
Bound, PyClass, PyResult, PyTypeInfo,
|
||||
};
|
||||
use crate::{ffi_ptr_ext::FfiPtrExt, PyErr};
|
||||
use crate::{
|
||||
sync::PyOnceLock,
|
||||
types::{any::PyAnyMethods, dict::PyDictMethods, PyDict},
|
||||
Py, PyAny, Python,
|
||||
};
|
||||
|
||||
/// `Sync` wrapper of `ffi::PyModuleDef`.
|
||||
pub struct ModuleDef {
|
||||
// wrapped in UnsafeCell so that Rust compiler treats this as interior mutability
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
ffi_def: UnsafeCell<ffi::PyModuleDef>,
|
||||
#[cfg(Py_3_15)]
|
||||
name: &'static CStr,
|
||||
#[cfg(Py_3_15)]
|
||||
doc: &'static CStr,
|
||||
#[cfg(Py_3_15)]
|
||||
slots: &'static PyModuleSlots,
|
||||
/// Interpreter ID where module was initialized (not applicable on PyPy).
|
||||
#[cfg(all(
|
||||
not(any(PyPy, GraalPy)),
|
||||
Py_3_9,
|
||||
not(all(windows, Py_LIMITED_API, not(Py_3_10)))
|
||||
))]
|
||||
interpreter: AtomicI64,
|
||||
/// Initialized module object, cached to avoid reinitialization.
|
||||
module: PyOnceLock<Py<PyModule>>,
|
||||
}
|
||||
|
||||
unsafe impl Sync for ModuleDef {}
|
||||
|
||||
impl ModuleDef {
|
||||
/// Make new module definition with given module name.
|
||||
pub const fn new(
|
||||
name: &'static CStr,
|
||||
doc: &'static CStr,
|
||||
slots: &'static PrimaryModuleSlots,
|
||||
secondary_slots: &'static SecondaryModuleSlots,
|
||||
) -> Self {
|
||||
// This is only used in PyO3 for append_to_inittab on Python 3.15 and newer.
|
||||
// There could also be other tools that need the legacy init hook.
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const INIT: ffi::PyModuleDef = ffi::PyModuleDef {
|
||||
m_base: ffi::PyModuleDef_HEAD_INIT,
|
||||
m_name: core::ptr::null(),
|
||||
m_doc: core::ptr::null(),
|
||||
m_size: 0,
|
||||
m_methods: core::ptr::null_mut(),
|
||||
m_slots: core::ptr::null_mut(),
|
||||
m_traverse: None,
|
||||
m_clear: None,
|
||||
m_free: None,
|
||||
};
|
||||
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
let ffi_def = UnsafeCell::new(ffi::PyModuleDef {
|
||||
m_name: name.as_ptr(),
|
||||
m_doc: doc.as_ptr(),
|
||||
m_slots: array_ptr_as_mut({
|
||||
cfg_select! {
|
||||
Py_3_15 => secondary_slots.0.get(),
|
||||
_ => slots.0.get(),
|
||||
}
|
||||
}),
|
||||
..INIT
|
||||
});
|
||||
|
||||
#[cfg(any(not(Py_3_15), all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
let _ = secondary_slots;
|
||||
|
||||
ModuleDef {
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
ffi_def,
|
||||
#[cfg(Py_3_15)]
|
||||
name,
|
||||
#[cfg(Py_3_15)]
|
||||
doc,
|
||||
#[cfg(Py_3_15)]
|
||||
slots,
|
||||
// -1 is never expected to be a valid interpreter ID
|
||||
#[cfg(all(
|
||||
not(any(PyPy, GraalPy)),
|
||||
Py_3_9,
|
||||
not(all(windows, Py_LIMITED_API, not(Py_3_10)))
|
||||
))]
|
||||
interpreter: AtomicI64::new(-1),
|
||||
module: PyOnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject {
|
||||
unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) }
|
||||
}
|
||||
|
||||
/// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules.
|
||||
pub fn make_module(&'static self, py: Python<'_>) -> PyResult<Py<PyModule>> {
|
||||
// Check the interpreter ID has not changed, since we currently have no way to guarantee
|
||||
// that static data is not reused across interpreters.
|
||||
//
|
||||
// PyPy does not have subinterpreters, so no need to check interpreter ID.
|
||||
//
|
||||
// TODO: it should be possible to use the Py_mod_multiple_interpreters slot on sufficiently
|
||||
// new Python versions to remove the need for this custom logic
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
{
|
||||
// PyInterpreterState_Get is only available on 3.9 and later, but is missing
|
||||
// from python3.dll for Windows stable API on 3.9
|
||||
#[cfg(all(Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
|
||||
{
|
||||
let current_interpreter =
|
||||
unsafe { ffi::PyInterpreterState_GetID(ffi::PyInterpreterState_Get()) };
|
||||
crate::err::error_on_minusone(py, current_interpreter)?;
|
||||
if let Err(initialized_interpreter) = self.interpreter.compare_exchange(
|
||||
-1,
|
||||
current_interpreter,
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
if initialized_interpreter != current_interpreter {
|
||||
return Err(PyImportError::new_err(
|
||||
"PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(all(Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10))))))]
|
||||
{
|
||||
// CPython before 3.9 does not have APIs to check the interpreter ID, so best that can be
|
||||
// done to guard against subinterpreters is fail if the module is initialized twice
|
||||
if self.module.get(py).is_some() {
|
||||
return Err(PyImportError::new_err(
|
||||
"PyO3 modules compiled for CPython 3.8 or older may only be initialized once per interpreter process"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make a dummy spec, needs a `name` attribute and that seems to be sufficient
|
||||
// for the loader system
|
||||
|
||||
static SIMPLE_NAMESPACE: PyOnceLock<Py<PyAny>> = PyOnceLock::new();
|
||||
let simple_ns = SIMPLE_NAMESPACE.import(py, "types", "SimpleNamespace")?;
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
let ffi_def = self.ffi_def.get();
|
||||
|
||||
let m_name = unsafe { CStr::from_ptr((*ffi_def).m_name) };
|
||||
let name = m_name
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
crate::exceptions::PyUnicodeDecodeError::new_err_from_utf8(
|
||||
py,
|
||||
m_name.to_bytes(),
|
||||
e,
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
let kwargs = PyDict::new(py);
|
||||
kwargs.set_item("name", name)?;
|
||||
let spec = simple_ns.call((), Some(&kwargs))?;
|
||||
|
||||
self.module
|
||||
.get_or_try_init(py, || {
|
||||
let def = self.ffi_def.get();
|
||||
let module = unsafe {
|
||||
ffi::PyModule_FromDefAndSpec(def, spec.as_ptr()).assume_owned_or_err(py)?
|
||||
}
|
||||
.cast_into()?;
|
||||
if unsafe { ffi::PyModule_ExecDef(module.as_ptr(), def) } != 0 {
|
||||
return Err(PyErr::fetch(py));
|
||||
}
|
||||
Ok(module.unbind())
|
||||
})
|
||||
.map(|py_module| py_module.clone_ref(py))
|
||||
}
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
let name = self.name;
|
||||
let doc = self.doc;
|
||||
let kwargs = PyDict::new(py);
|
||||
kwargs.set_item("name", name)?;
|
||||
let spec = simple_ns.call((), Some(&kwargs))?;
|
||||
|
||||
self.module
|
||||
.get_or_try_init(py, || {
|
||||
let slots = self.get_slots();
|
||||
let module = unsafe {
|
||||
ffi::PyModule_FromSlotsAndSpec(slots, spec.as_ptr())
|
||||
.assume_owned_or_err(py)?
|
||||
}
|
||||
.cast_into()?;
|
||||
if unsafe { ffi::PyModule_SetDocString(module.as_ptr(), doc.as_ptr()) } != 0 {
|
||||
return Err(PyErr::fetch(py));
|
||||
}
|
||||
if unsafe { ffi::PyModule_Exec(module.as_ptr()) } != 0 {
|
||||
return Err(PyErr::fetch(py));
|
||||
}
|
||||
Ok(module.unbind())
|
||||
})
|
||||
.map(|py_module| py_module.clone_ref(py))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
pub fn get_slots(&'static self) -> *mut ffi::PySlot {
|
||||
array_ptr_as_mut(self.slots.0.get())
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the `PyModExport_<name>` entry point used by Python 3.15 and newer.
|
||||
///
|
||||
/// This is wrapped in a `macro_rules!` so the proc-macro backend can emit a single
|
||||
/// version-agnostic invocation; the body only expands on Python 3.15+, where
|
||||
/// `ffi::PySlot` is defined.
|
||||
#[cfg(Py_3_15)]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __pyo3_pymodexport {
|
||||
($symbol:literal, $def:path) => {
|
||||
#[doc(hidden)]
|
||||
#[export_name = $symbol]
|
||||
pub unsafe extern "C" fn __pyo3_export() -> *mut $crate::ffi::PySlot {
|
||||
$def.get_slots()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __pyo3_pymodexport {
|
||||
($symbol:literal, $def:path) => {};
|
||||
}
|
||||
|
||||
/// Defines the `PyInit_<name>` entry point used by Python 3.14 and older.
|
||||
///
|
||||
/// This is wrapped in a `macro_rules!` so the proc-macro backend can emit a single
|
||||
/// version-agnostic invocation; the body only expands on Python 3.14 and older
|
||||
#[cfg(not(all(Py_3_15, Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __pyo3_pyinit {
|
||||
($symbol:literal, $def:path) => {
|
||||
#[doc(hidden)]
|
||||
#[export_name = $symbol]
|
||||
pub unsafe extern "C" fn __pyo3_init() -> *mut $crate::ffi::PyObject {
|
||||
$def.init_multi_phase()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_15, Py_LIMITED_API, Py_GIL_DISABLED))]
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! __pyo3_pyinit {
|
||||
($symbol:literal, $def:path) => {};
|
||||
}
|
||||
|
||||
/// Type of the exec slot used to initialise module contents
|
||||
pub type ModuleExecSlot = unsafe extern "C" fn(*mut ffi::PyObject) -> c_int;
|
||||
|
||||
const MAX_SLOTS: usize =
|
||||
// Py_mod_exec
|
||||
1 +
|
||||
// Py_mod_gil
|
||||
cfg!(Py_3_13) as usize +
|
||||
// Py_mod_name, Py_mod_doc, and Py_mod_abi
|
||||
3 * (cfg!(Py_3_15) as usize);
|
||||
const MAX_SLOTS_WITH_TRAILING_NULL: usize = MAX_SLOTS + 1;
|
||||
|
||||
/// On Python 3.15+ we use `PySlot` system and `PyModule_FromSlotsAndSpec`
|
||||
#[cfg(Py_3_15)]
|
||||
pub type PrimaryModuleSlots = PyModuleSlots;
|
||||
#[cfg(all(Py_3_15, not(all(Py_LIMITED_API, Py_GIL_DISABLED))))]
|
||||
pub type SecondaryModuleSlots = PyModuleDefSlots;
|
||||
|
||||
/// On Python 3.14 and older the primary system is `ffi::PyModuleDef`.
|
||||
#[cfg(not(Py_3_15))]
|
||||
pub type PrimaryModuleSlots = PyModuleDefSlots;
|
||||
#[cfg(not(all(Py_3_15, not(all(Py_LIMITED_API, Py_GIL_DISABLED)))))]
|
||||
pub type SecondaryModuleSlots = ();
|
||||
|
||||
pub const fn secondary_slots(slots: &'static PrimaryModuleSlots) -> SecondaryModuleSlots {
|
||||
cfg_select! {
|
||||
// On Python 3.15+ we populate `PyModuleDefSlots` to point at primary slots
|
||||
// (as long as not using abi3t where `PyModuleDef` is opaque and we cannot know the layout)
|
||||
all(Py_3_15, not(all(Py_LIMITED_API, Py_GIL_DISABLED))) => PyModuleDefSlots(UnsafeCell::new([
|
||||
ffi::PyModuleDef_Slot {
|
||||
slot: ffi::Py_slot_subslots,
|
||||
value: slots.0.get().cast(),
|
||||
},
|
||||
// SAFETY: terminator of C-style array
|
||||
unsafe { core::mem::zeroed() },
|
||||
])),
|
||||
// Older versions have no secondary slots
|
||||
_ => { let _ = slots; }
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder to create module slots. The size of the number of slots desired must
|
||||
/// be known up front, and N needs to be at least one greater than the number of
|
||||
/// actual slots pushed due to the need to have a zeroed element on the end.
|
||||
pub struct PyModuleSlotsBuilder {
|
||||
// values (initially all zeroed)
|
||||
slots: PrimaryModuleSlots,
|
||||
// current length
|
||||
len: usize,
|
||||
}
|
||||
|
||||
// note that macros cannot use conditional compilation,
|
||||
// so all implementations below must be available in all
|
||||
// Python versions
|
||||
// By handling it here we can avoid conditional
|
||||
// compilation within the macros; they can always emit
|
||||
// e.g. a `.with_gil_used()` call.
|
||||
impl PyModuleSlotsBuilder {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
slots: cfg_select! {
|
||||
Py_3_15 => PyModuleSlots(UnsafeCell::new(
|
||||
// SAFETY: `PySlot` is legal to be zeroed (terminates C-style array)
|
||||
[unsafe { core::mem::zeroed::<ffi::PySlot>() }; MAX_SLOTS_WITH_TRAILING_NULL],
|
||||
)),
|
||||
_ => PyModuleDefSlots(UnsafeCell::new(
|
||||
// SAFETY: `PyModuleDef_Slot` is legal to be zeroed (terminates C-style array)
|
||||
[unsafe { core::mem::zeroed::<ffi::PyModuleDef_Slot>() }; MAX_SLOTS_WITH_TRAILING_NULL],
|
||||
))
|
||||
},
|
||||
len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_mod_exec(self, exec: ModuleExecSlot) -> Self {
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
self.push(ffi::Py_mod_exec, exec as *mut c_void)
|
||||
}
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
// safety: exce is not NULL
|
||||
self.push_value(unsafe { ffi::PySlot_FUNC(ffi::Py_mod_exec, exec as *mut c_void) })
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_gil_used(self, gil_used: bool) -> Self {
|
||||
#[cfg(all(Py_3_13, not(Py_3_15)))]
|
||||
{
|
||||
self.push(
|
||||
ffi::Py_mod_gil,
|
||||
if gil_used {
|
||||
ffi::Py_MOD_GIL_USED
|
||||
} else {
|
||||
ffi::Py_MOD_GIL_NOT_USED
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
self.push_value(ffi::PySlot_DATA(
|
||||
ffi::Py_mod_gil,
|
||||
if gil_used {
|
||||
ffi::Py_MOD_GIL_USED
|
||||
} else {
|
||||
ffi::Py_MOD_GIL_NOT_USED
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_13))]
|
||||
{
|
||||
// Silence unused variable warning
|
||||
let _ = gil_used;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_name(self, name: &'static CStr) -> Self {
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
self.push_value(ffi::PySlot_STATIC_DATA(
|
||||
ffi::Py_mod_name,
|
||||
name.as_ptr() as *mut c_void,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
// Silence unused variable warning
|
||||
let _ = name;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_abi_info(self) -> Self {
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
ffi::PyABIInfo_VAR!(ABI_INFO);
|
||||
self.push_value(ffi::PySlot_STATIC_DATA(
|
||||
ffi::Py_mod_abi,
|
||||
(&raw mut ABI_INFO).cast(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn with_doc(self, doc: &'static CStr) -> Self {
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
self.push_value(ffi::PySlot_STATIC_DATA(
|
||||
ffi::Py_mod_doc,
|
||||
doc.as_ptr() as *mut c_void,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
// Silence unused variable warning
|
||||
let _ = doc;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn build(self) -> PrimaryModuleSlots {
|
||||
self.slots
|
||||
}
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
const fn push(mut self, slot: c_int, value: *mut c_void) -> Self {
|
||||
// Required to guarantee there's still a zeroed element
|
||||
// at the end
|
||||
assert!(
|
||||
self.len < MAX_SLOTS,
|
||||
"Cannot add more than MAX_SLOTS slots to a PyModuleSlots",
|
||||
);
|
||||
self.slots.0.get_mut()[self.len] = ffi::PyModuleDef_Slot { slot, value };
|
||||
self.len += 1;
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
const fn push_value(mut self, value: ffi::PySlot) -> Self {
|
||||
assert!(
|
||||
self.len < MAX_SLOTS,
|
||||
"Cannot add more than MAX_SLOTS slots to a PyModuleSlots",
|
||||
);
|
||||
self.slots.0.get_mut()[self.len] = value;
|
||||
self.len += 1;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper to safely store module slots, to be used in a `ModuleDef`.
|
||||
pub struct PyModuleSlots(
|
||||
// necessarily empty before Python 3.15; PySlot doesn't exist
|
||||
#[cfg(Py_3_15)] UnsafeCell<[ffi::PySlot; MAX_SLOTS_WITH_TRAILING_NULL]>,
|
||||
);
|
||||
|
||||
/// Slots to populate a `PyModuleDef`
|
||||
/// Cannot create a `PyModuleDef` on abi3t due to lack of knowledge of object layout
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
pub struct PyModuleDefSlots(
|
||||
UnsafeCell<
|
||||
[ffi::PyModuleDef_Slot; cfg_select! {
|
||||
// on Python 3.15+ only one slot for pointing at the primary slots, plus trailing null
|
||||
Py_3_15 => 2,
|
||||
_ => MAX_SLOTS_WITH_TRAILING_NULL
|
||||
}],
|
||||
>,
|
||||
);
|
||||
|
||||
// It might be possible to avoid this with SyncUnsafeCell in the future
|
||||
//
|
||||
// SAFETY: the inner values are only accessed within a `ModuleDef`,
|
||||
// used to call `PyModule_FromSlotsAndSpec`
|
||||
unsafe impl Sync for PyModuleSlots {}
|
||||
// SAFETY: the inner values are only accessed within a `ModuleDef`,
|
||||
// which only uses them to build the `ffi::ModuleDef`.
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
unsafe impl Sync for PyModuleDefSlots {}
|
||||
|
||||
/// Trait to add an element (class, function...) to a module.
|
||||
///
|
||||
/// Currently only implemented for classes.
|
||||
pub trait PyAddToModule: crate::sealed::Sealed {
|
||||
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()>;
|
||||
}
|
||||
|
||||
/// For adding native types (non-pyclass) to a module.
|
||||
pub struct AddTypeToModule<T>(PhantomData<T>);
|
||||
|
||||
impl<T> AddTypeToModule<T> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
AddTypeToModule(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyTypeInfo> PyAddToModule for AddTypeToModule<T> {
|
||||
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
let object = T::type_object(module.py());
|
||||
module.add(object.name()?, object)
|
||||
}
|
||||
}
|
||||
|
||||
/// For adding a class to a module.
|
||||
pub struct AddClassToModule<T>(PhantomData<T>);
|
||||
|
||||
impl<T> AddClassToModule<T> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
AddClassToModule(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass> PyAddToModule for AddClassToModule<T> {
|
||||
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_class::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
/// For adding a function to a module.
|
||||
impl PyAddToModule for PyFunctionDef {
|
||||
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// safety: self is static
|
||||
module.add_function(self.create_py_c_function(module.py(), Some(module))?)
|
||||
}
|
||||
}
|
||||
|
||||
/// For adding a module to a module.
|
||||
impl PyAddToModule for ModuleDef {
|
||||
fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_submodule(self.make_module(module.py())?.bind(module.py()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::borrow::Cow;
|
||||
use core::{ffi::c_int, ffi::CStr};
|
||||
|
||||
use crate::impl_::trampoline;
|
||||
|
||||
use super::*;
|
||||
|
||||
unsafe extern "C" fn module_exec(_module: *mut ffi::PyObject) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_init() {
|
||||
unsafe extern "C" fn module_exec(module: *mut ffi::PyObject) -> c_int {
|
||||
unsafe {
|
||||
trampoline::module_exec(module, |m| {
|
||||
m.add("SOME_CONSTANT", 42)?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static NAME: &CStr = c"test_module";
|
||||
static DOC: &CStr = c"some doc";
|
||||
|
||||
static SLOTS: PrimaryModuleSlots = PyModuleSlotsBuilder::new()
|
||||
.with_mod_exec(module_exec)
|
||||
.with_gil_used(false)
|
||||
.with_abi_info()
|
||||
.with_name(NAME)
|
||||
.with_doc(DOC)
|
||||
.build();
|
||||
|
||||
static SECONDARY_SLOTS: SecondaryModuleSlots = secondary_slots(&SLOTS);
|
||||
|
||||
static MODULE_DEF: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SECONDARY_SLOTS);
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = MODULE_DEF.make_module(py).unwrap().into_bound(py);
|
||||
assert_eq!(
|
||||
module
|
||||
.getattr("__name__")
|
||||
.unwrap()
|
||||
.extract::<Cow<'_, str>>()
|
||||
.unwrap(),
|
||||
"test_module",
|
||||
);
|
||||
assert_eq!(
|
||||
module
|
||||
.getattr("__doc__")
|
||||
.unwrap()
|
||||
.extract::<Cow<'_, str>>()
|
||||
.unwrap(),
|
||||
"some doc",
|
||||
);
|
||||
assert_eq!(
|
||||
module
|
||||
.getattr("SOME_CONSTANT")
|
||||
.unwrap()
|
||||
.extract::<u8>()
|
||||
.unwrap(),
|
||||
42,
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_def_new() {
|
||||
// To get coverage for ModuleDef::new() need to create a non-static ModuleDef, however init
|
||||
// etc require static ModuleDef, so this test needs to be separated out.
|
||||
static NAME: &CStr = c"test_module";
|
||||
static DOC: &CStr = c"some doc";
|
||||
|
||||
static SLOTS: PrimaryModuleSlots = PyModuleSlotsBuilder::new().build();
|
||||
static SECONDARY_SLOTS: SecondaryModuleSlots = secondary_slots(&SLOTS);
|
||||
|
||||
let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS, &SECONDARY_SLOTS);
|
||||
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
unsafe {
|
||||
let expected_slots = cfg_select! {
|
||||
Py_3_15 => SECONDARY_SLOTS.0.get().cast(),
|
||||
_ => SLOTS.0.get().cast(),
|
||||
};
|
||||
assert_eq!((*module_def.ffi_def.get()).m_slots, expected_slots);
|
||||
}
|
||||
#[cfg(all(Py_3_15, not(all(Py_LIMITED_API, Py_GIL_DISABLED))))]
|
||||
unsafe {
|
||||
let secondary_slots = &*SECONDARY_SLOTS.0.get();
|
||||
assert_eq!(secondary_slots[0].slot, ffi::Py_slot_subslots);
|
||||
assert_eq!(secondary_slots[0].value, SLOTS.0.get().cast());
|
||||
assert!(secondary_slots[1] == ffi::PyModuleDef_Slot::default());
|
||||
}
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
assert_eq!(module_def.name, NAME);
|
||||
assert_eq!(module_def.doc, DOC);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(panic = "unwind")]
|
||||
fn test_build_maximal_slots() {
|
||||
let mut builder = PyModuleSlotsBuilder::new()
|
||||
.with_mod_exec(module_exec)
|
||||
.with_name(c"test_module")
|
||||
.with_doc(c"some doc")
|
||||
.with_gil_used(false)
|
||||
.with_abi_info();
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
{
|
||||
let second_last = builder.slots.0.get_mut()[builder.len - 1];
|
||||
let last = builder.slots.0.get_mut()[builder.len];
|
||||
let zeroed = unsafe { core::mem::zeroed() };
|
||||
fn raw_bytes(inst: &ffi::PySlot) -> &[u8] {
|
||||
unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
inst as *const ffi::PySlot as *const u8,
|
||||
core::mem::size_of::<ffi::PySlot>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
let zeroed_bytes = raw_bytes(&zeroed);
|
||||
assert_eq!(raw_bytes(&last), zeroed_bytes);
|
||||
assert_ne!(raw_bytes(&second_last), zeroed_bytes);
|
||||
}
|
||||
#[cfg(not(Py_3_15))]
|
||||
{
|
||||
let second_last = builder.slots.0.get_mut()[builder.len - 1];
|
||||
let last = builder.slots.0.get_mut()[builder.len];
|
||||
let zeroed = ffi::PyModuleDef_Slot::default();
|
||||
assert!(last == zeroed);
|
||||
assert!(second_last != zeroed);
|
||||
}
|
||||
assert!(builder.len == MAX_SLOTS);
|
||||
|
||||
let result = std::panic::catch_unwind(|| builder.with_mod_exec(module_exec).build());
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_module_slots_builder_overflow() {
|
||||
let mut builder = PyModuleSlotsBuilder::new();
|
||||
for _ in 0..MAX_SLOTS + 1 {
|
||||
builder = builder.with_mod_exec(module_exec);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+355
@@ -0,0 +1,355 @@
|
||||
#![warn(clippy::undocumented_unsafe_blocks, clippy::missing_safety_doc)]
|
||||
|
||||
//! Trampolines for various pyfunction and pymethod implementations.
|
||||
//!
|
||||
//! They exist to monomorphise std::panic::catch_unwind once into PyO3, rather than inline in every
|
||||
//! function, thus saving a huge amount of compile-time complexity.
|
||||
|
||||
use core::{any::Any, ffi::c_int, panic::UnwindSafe};
|
||||
|
||||
use crate::internal::state::AttachGuard;
|
||||
use crate::{
|
||||
ffi, ffi_ptr_ext::FfiPtrExt, impl_::callback::PyCallbackOutput, impl_::panic::PanicTrap,
|
||||
panic::PanicException, types::PyModule, Bound, PyResult, Python,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub unsafe fn module_exec(
|
||||
module: *mut ffi::PyObject,
|
||||
f: for<'a, 'py> fn(&'a Bound<'py, PyModule>) -> PyResult<()>,
|
||||
) -> c_int {
|
||||
// SAFETY: f accepts a Bound object so Python is attached
|
||||
unsafe {
|
||||
trampoline(|py| {
|
||||
let module = module.assume_borrowed_or_err(py)?.cast::<PyModule>()?;
|
||||
f(&module)?;
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A workaround for Rust not allowing function pointers as const generics: define a trait which
|
||||
/// has a constant function pointer.
|
||||
pub trait MethodDef<T> {
|
||||
const METH: T;
|
||||
}
|
||||
|
||||
/// Generates an implementation of `MethodDef` and then returns the trampoline function
|
||||
/// specialized to call the provided method.
|
||||
///
|
||||
/// Note that the functions returned by this macro are instantiations of generic functions. Code
|
||||
/// should not depend on these function pointers being stable (e.g. across compilation units);
|
||||
/// the intended purpose of these is to create function pointers which can be passed to the Python
|
||||
/// C-API to correctly wrap Rust functions.
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! get_trampoline_function {
|
||||
($trampoline:ident, $f:path) => {{
|
||||
struct Def;
|
||||
impl $crate::impl_::trampoline::MethodDef<$crate::impl_::trampoline::$trampoline::Func> for Def {
|
||||
const METH: $crate::impl_::trampoline::$trampoline::Func = $f;
|
||||
}
|
||||
$crate::impl_::trampoline::$trampoline::<Def>
|
||||
}};
|
||||
}
|
||||
|
||||
pub use get_trampoline_function;
|
||||
|
||||
/// Macro to define a trampoline function for a given function signature.
|
||||
///
|
||||
/// This macro generates:
|
||||
/// 1. An external "C" function that serves as the trampoline, generic on a specific function pointer.
|
||||
/// 2. A companion module containing a non-generic inner function, and the function pointer type.
|
||||
macro_rules! trampoline {
|
||||
(pub fn $name:ident($($arg_names:ident: $arg_types:ty),* $(,)?) -> $ret:ty;) => {
|
||||
/// External symbol called by Python, which calls the provided Rust function.
|
||||
///
|
||||
/// The Rust function is supplied via the generic parameter `Meth`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The interpreter must be attached
|
||||
pub unsafe extern "C" fn $name<Meth: MethodDef<$name::Func>>(
|
||||
$($arg_names: $arg_types,)*
|
||||
) -> $ret {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { $name::inner($($arg_names),*, Meth::METH) }
|
||||
}
|
||||
|
||||
/// Companion module contains the function pointer type.
|
||||
pub mod $name {
|
||||
use super::*;
|
||||
|
||||
/// Non-generic inner function to ensure only one trampoline instantiated
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The interpreter must be attached
|
||||
#[inline]
|
||||
pub(crate) unsafe fn inner($($arg_names: $arg_types),*, f: $name::Func) -> $ret {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { trampoline(|py| f(py, $($arg_names,)*)) }
|
||||
}
|
||||
|
||||
/// The type of the function pointer for this trampoline.
|
||||
pub type Func = for<'py> unsafe fn (Python<'py>, $($arg_types),*) -> PyResult<$ret>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Noargs is a special case where the `_args` parameter is unused and not passed to the inner `Func`.
|
||||
/// # Safety
|
||||
///
|
||||
/// Interpreter must be attached
|
||||
pub unsafe extern "C" fn noargs<Meth: MethodDef<noargs::Func>>(
|
||||
slf: *mut ffi::PyObject,
|
||||
_args: *mut ffi::PyObject, // unused and value not defined
|
||||
) -> *mut ffi::PyObject {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { noargs::inner(slf, Meth::METH) }
|
||||
}
|
||||
|
||||
pub mod noargs {
|
||||
use super::*;
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// Interpreter must be attached
|
||||
#[inline]
|
||||
pub(crate) unsafe fn inner(slf: *mut ffi::PyObject, f: Func) -> *mut ffi::PyObject {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { trampoline(|py| f(py, slf)) }
|
||||
}
|
||||
|
||||
pub type Func = unsafe fn(Python<'_>, *mut ffi::PyObject) -> PyResult<*mut ffi::PyObject>;
|
||||
}
|
||||
|
||||
macro_rules! trampolines {
|
||||
($(pub fn $name:ident($($arg_names:ident: $arg_types:ty),* $(,)?) -> $ret:ty);* ;) => {
|
||||
$(trampoline!(pub fn $name($($arg_names: $arg_types),*) -> $ret;));*;
|
||||
}
|
||||
}
|
||||
|
||||
// Trampolines used by `PyMethodDef` constructors
|
||||
trampolines!(
|
||||
pub fn fastcall_cfunction_with_keywords(
|
||||
slf: *mut ffi::PyObject,
|
||||
args: *const *mut ffi::PyObject,
|
||||
nargs: ffi::Py_ssize_t,
|
||||
kwnames: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn cfunction_with_keywords(
|
||||
slf: *mut ffi::PyObject,
|
||||
args: *mut ffi::PyObject,
|
||||
kwargs: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject;
|
||||
);
|
||||
|
||||
/// "fastcall" method calls only available on abi3 in Python 3.10 and up, otherwise fall back to the older call convention.
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
pub use self::fastcall_cfunction_with_keywords as maybe_fastcall_cfunction_with_keywords;
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
pub use self::cfunction_with_keywords as maybe_fastcall_cfunction_with_keywords;
|
||||
|
||||
// Trampolines used by slot methods
|
||||
trampolines!(
|
||||
pub fn getattrofunc(slf: *mut ffi::PyObject, attr: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn setattrofunc(
|
||||
slf: *mut ffi::PyObject,
|
||||
attr: *mut ffi::PyObject,
|
||||
value: *mut ffi::PyObject,
|
||||
) -> c_int;
|
||||
|
||||
pub fn binaryfunc(slf: *mut ffi::PyObject, arg1: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn descrgetfunc(
|
||||
slf: *mut ffi::PyObject,
|
||||
arg1: *mut ffi::PyObject,
|
||||
arg2: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn getiterfunc(slf: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn hashfunc(slf: *mut ffi::PyObject) -> ffi::Py_hash_t;
|
||||
|
||||
pub fn inquiry(slf: *mut ffi::PyObject) -> c_int;
|
||||
|
||||
pub fn iternextfunc(slf: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn lenfunc(slf: *mut ffi::PyObject) -> ffi::Py_ssize_t;
|
||||
|
||||
pub fn newfunc(
|
||||
subtype: *mut ffi::PyTypeObject,
|
||||
args: *mut ffi::PyObject,
|
||||
kwargs: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn initproc(
|
||||
slf: *mut ffi::PyObject,
|
||||
args: *mut ffi::PyObject,
|
||||
kwargs: *mut ffi::PyObject,
|
||||
) -> c_int;
|
||||
|
||||
pub fn objobjproc(slf: *mut ffi::PyObject, arg1: *mut ffi::PyObject) -> c_int;
|
||||
|
||||
pub fn reprfunc(slf: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn richcmpfunc(
|
||||
slf: *mut ffi::PyObject,
|
||||
other: *mut ffi::PyObject,
|
||||
op: c_int,
|
||||
) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn ssizeargfunc(arg1: *mut ffi::PyObject, arg2: ffi::Py_ssize_t) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn ternaryfunc(
|
||||
slf: *mut ffi::PyObject,
|
||||
arg1: *mut ffi::PyObject,
|
||||
arg2: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject;
|
||||
|
||||
pub fn unaryfunc(slf: *mut ffi::PyObject) -> *mut ffi::PyObject;
|
||||
);
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
trampoline! {
|
||||
pub fn getbufferproc(slf: *mut ffi::PyObject, buf: *mut ffi::Py_buffer, flags: c_int) -> c_int;
|
||||
}
|
||||
|
||||
/// Releasebufferproc is a special case where the function cannot return an error,
|
||||
/// so we use trampoline_unraisable.
|
||||
/// # Safety
|
||||
///
|
||||
/// - It must be sound to call the method with slf and buf
|
||||
/// - Interpreter must be attached
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
pub unsafe extern "C" fn releasebufferproc<Meth: MethodDef<releasebufferproc::Func>>(
|
||||
slf: *mut ffi::PyObject,
|
||||
buf: *mut ffi::Py_buffer,
|
||||
) {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { releasebufferproc::inner(slf, buf, Meth::METH) }
|
||||
}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
pub mod releasebufferproc {
|
||||
use super::*;
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// - It must be sound to call f with slf and buf
|
||||
/// - Interpreter must be attached
|
||||
#[inline]
|
||||
pub(crate) unsafe fn inner(slf: *mut ffi::PyObject, buf: *mut ffi::Py_buffer, f: Func) {
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe { trampoline_unraisable(|py| f(py, slf, buf), slf) }
|
||||
}
|
||||
|
||||
pub type Func = unsafe fn(Python<'_>, *mut ffi::PyObject, *mut ffi::Py_buffer) -> PyResult<()>;
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// - slf must be either a valid ffi::PyObject or NULL
|
||||
/// - if slf isn't NULL it must not be used again
|
||||
/// - The thread must be attached to the interpreter when this is called.
|
||||
#[inline]
|
||||
pub(crate) unsafe fn dealloc(
|
||||
slf: *mut ffi::PyObject,
|
||||
f: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> (),
|
||||
) {
|
||||
// After calling tp_dealloc the object is no longer valid,
|
||||
// so pass null_mut() to the context.
|
||||
//
|
||||
// (Note that we don't allow the implementation `f` to fail.)
|
||||
//
|
||||
// SAFETY: caller upholds requirements
|
||||
unsafe {
|
||||
trampoline_unraisable(
|
||||
|py| {
|
||||
f(py, slf);
|
||||
Ok(())
|
||||
},
|
||||
core::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of trampoline functions, which sets up an AttachGuard and calls F.
|
||||
///
|
||||
/// Panics during execution are trapped so that they don't propagate through any
|
||||
/// outer FFI boundary.
|
||||
///
|
||||
/// # Safety
|
||||
/// The thread must already be attached to the interpreter when this is called.
|
||||
#[inline]
|
||||
pub(crate) unsafe fn trampoline<F, R>(body: F) -> R
|
||||
where
|
||||
F: for<'py> FnOnce(Python<'py>) -> PyResult<R> + UnwindSafe,
|
||||
R: PyCallbackOutput,
|
||||
{
|
||||
let trap = PanicTrap::new("uncaught panic at ffi boundary");
|
||||
|
||||
// SAFETY: This function requires the thread to already be attached.
|
||||
let guard = unsafe { AttachGuard::assume() };
|
||||
let py = guard.python();
|
||||
let out = panic_result_into_callback_output(
|
||||
py,
|
||||
std::panic::catch_unwind(move || -> PyResult<_> { body(py) }),
|
||||
);
|
||||
trap.disarm();
|
||||
out
|
||||
}
|
||||
|
||||
/// Converts the output of std::panic::catch_unwind into a Python function output, either by raising a Python
|
||||
/// exception or by unwrapping the contained success output.
|
||||
#[inline]
|
||||
fn panic_result_into_callback_output<R>(
|
||||
py: Python<'_>,
|
||||
panic_result: Result<PyResult<R>, Box<dyn Any + Send + 'static>>,
|
||||
) -> R
|
||||
where
|
||||
R: PyCallbackOutput,
|
||||
{
|
||||
let py_err = match panic_result {
|
||||
Ok(Ok(value)) => return value,
|
||||
Ok(Err(py_err)) => py_err,
|
||||
Err(payload) => PanicException::from_panic_payload(payload),
|
||||
};
|
||||
py_err.restore(py);
|
||||
R::ERR_VALUE
|
||||
}
|
||||
|
||||
/// Implementation of trampoline for functions which can't return an error.
|
||||
///
|
||||
/// Panics during execution are trapped so that they don't propagate through any
|
||||
/// outer FFI boundary.
|
||||
///
|
||||
/// Exceptions produced are sent to `sys.unraisablehook`.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - ctx must be either a valid ffi::PyObject or NULL
|
||||
/// - The thread must be attached to the interpreter when this is called.
|
||||
#[inline]
|
||||
unsafe fn trampoline_unraisable<F>(body: F, ctx: *mut ffi::PyObject)
|
||||
where
|
||||
F: for<'py> FnOnce(Python<'py>) -> PyResult<()> + UnwindSafe,
|
||||
{
|
||||
let trap = PanicTrap::new("uncaught panic at ffi boundary");
|
||||
|
||||
// SAFETY: Thread is known to be attached.
|
||||
let guard = unsafe { AttachGuard::assume() };
|
||||
let py = guard.python();
|
||||
|
||||
if let Err(py_err) = std::panic::catch_unwind(move || body(py))
|
||||
.unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload)))
|
||||
{
|
||||
// SAFETY: caller upholds requirements
|
||||
py_err.write_unraisable(py, unsafe { ctx.assume_borrowed_or_opt(py) }.as_deref());
|
||||
}
|
||||
trap.disarm();
|
||||
}
|
||||
Vendored
+252
@@ -0,0 +1,252 @@
|
||||
#![warn(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::impl_::concat::slice_copy_from_slice;
|
||||
|
||||
/// This is a reimplementation of the `indoc` crate's unindent functionality:
|
||||
///
|
||||
/// 1. Count the leading spaces of each line, ignoring the first line and any lines that are empty or contain spaces only.
|
||||
/// 2. Take the minimum.
|
||||
/// 3. If the first line is empty i.e. the string begins with a newline, remove the first line.
|
||||
/// 4. Remove the computed number of spaces from the beginning of each line.
|
||||
const fn unindent_bytes(bytes: &mut [u8]) -> usize {
|
||||
// (1) + (2) - count leading spaces, take the minimum
|
||||
let Some(to_unindent) = get_minimum_leading_spaces(bytes) else {
|
||||
// all lines were empty, nothing to unindent
|
||||
return bytes.len();
|
||||
};
|
||||
|
||||
// now copy from the original buffer, bringing values forward as needed
|
||||
let mut read_idx = 0;
|
||||
let mut write_idx = 0;
|
||||
|
||||
// (3) - remove first line if it is empty
|
||||
match consume_eol(bytes, read_idx) {
|
||||
// skip empty first line
|
||||
Some(eol) => read_idx = eol,
|
||||
// copy non-empty first line as-is
|
||||
None => {
|
||||
(read_idx, write_idx) = copy_forward_until_eol(bytes, read_idx, write_idx);
|
||||
}
|
||||
};
|
||||
|
||||
// (4) - unindent remaining lines
|
||||
while read_idx < bytes.len() {
|
||||
let leading_spaces = count_spaces(bytes, read_idx);
|
||||
|
||||
if leading_spaces < to_unindent {
|
||||
read_idx += leading_spaces;
|
||||
assert!(
|
||||
consume_eol(bytes, read_idx).is_some(),
|
||||
"removed fewer spaces than expected on non-empty line"
|
||||
);
|
||||
} else {
|
||||
// leading_spaces may be equal to or larger than to_unindent, only need to unindent
|
||||
// the required amount, additional indentation is meaningful
|
||||
read_idx += to_unindent;
|
||||
}
|
||||
|
||||
// copy remainder of line
|
||||
(read_idx, write_idx) = copy_forward_until_eol(bytes, read_idx, write_idx);
|
||||
}
|
||||
|
||||
write_idx
|
||||
}
|
||||
|
||||
/// Counts the minimum leading spaces of all non-empty lines except the first line.
|
||||
///
|
||||
/// Returns `None` if there are no non-empty lines except the first line.
|
||||
const fn get_minimum_leading_spaces(bytes: &[u8]) -> Option<usize> {
|
||||
// scan for leading spaces (ignoring first line and empty lines)
|
||||
let mut i = 0;
|
||||
|
||||
// skip first line
|
||||
i = advance_to_next_line(bytes, i);
|
||||
|
||||
let mut to_unindent = None;
|
||||
|
||||
// for remaining lines, count leading spaces
|
||||
while i < bytes.len() {
|
||||
let line_leading_spaces = count_spaces(bytes, i);
|
||||
i += line_leading_spaces;
|
||||
|
||||
// line only had spaces, ignore for the count
|
||||
if let Some(eol) = consume_eol(bytes, i) {
|
||||
i = eol;
|
||||
continue;
|
||||
}
|
||||
|
||||
// this line has content, consider its leading spaces
|
||||
if let Some(current) = to_unindent {
|
||||
// .unwrap_or(usize::MAX) not available in const fn
|
||||
if line_leading_spaces < current {
|
||||
to_unindent = Some(line_leading_spaces);
|
||||
}
|
||||
} else {
|
||||
to_unindent = Some(line_leading_spaces);
|
||||
}
|
||||
|
||||
i = advance_to_next_line(bytes, i);
|
||||
}
|
||||
|
||||
to_unindent
|
||||
}
|
||||
|
||||
const fn advance_to_next_line(bytes: &[u8], mut i: usize) -> usize {
|
||||
while i < bytes.len() {
|
||||
if let Some(eol) = consume_eol(bytes, i) {
|
||||
return eol;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Brings elements in `bytes` forward until `\n` (inclusive) or end of `source`.
|
||||
///
|
||||
/// `read_idx` must be greater than or equal to `write_idx`.
|
||||
const fn copy_forward_until_eol(
|
||||
bytes: &mut [u8],
|
||||
mut read_idx: usize,
|
||||
mut write_idx: usize,
|
||||
) -> (usize, usize) {
|
||||
assert!(read_idx >= write_idx);
|
||||
while read_idx < bytes.len() {
|
||||
let value = bytes[read_idx];
|
||||
bytes[write_idx] = value;
|
||||
read_idx += 1;
|
||||
write_idx += 1;
|
||||
if value == b'\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(read_idx, write_idx)
|
||||
}
|
||||
|
||||
const fn count_spaces(bytes: &[u8], mut i: usize) -> usize {
|
||||
let mut count = 0;
|
||||
while i < bytes.len() && bytes[i] == b' ' {
|
||||
count += 1;
|
||||
i += 1;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
const fn consume_eol(bytes: &[u8], i: usize) -> Option<usize> {
|
||||
if bytes.len() == i {
|
||||
// special case: treat end of buffer as EOL without consuming anything
|
||||
Some(i)
|
||||
} else if bytes.len() > i && bytes[i] == b'\n' {
|
||||
Some(i + 1)
|
||||
} else if bytes[i] == b'\r' && bytes.len() > i + 1 && bytes[i + 1] == b'\n' {
|
||||
Some(i + 2)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn unindent_sized<const N: usize>(src: &[u8]) -> ([u8; N], usize) {
|
||||
let mut out: [u8; N] = [0; N];
|
||||
slice_copy_from_slice(&mut out, src);
|
||||
let new_len = unindent_bytes(&mut out);
|
||||
(out, new_len)
|
||||
}
|
||||
|
||||
/// Helper for `py_run!` macro which unindents a string at compile time.
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! unindent {
|
||||
($value:expr) => {{
|
||||
const RAW: &str = $value;
|
||||
const LEN: usize = RAW.len();
|
||||
const UNINDENTED: ([u8; LEN], usize) =
|
||||
$crate::impl_::unindent::unindent_sized::<LEN>(RAW.as_bytes());
|
||||
// SAFETY: this removes only spaces and preserves all other contents
|
||||
unsafe { ::core::str::from_utf8_unchecked(UNINDENTED.0.split_at(UNINDENTED.1).0) }
|
||||
}};
|
||||
}
|
||||
|
||||
pub use crate::unindent;
|
||||
|
||||
/// Equivalent of the `unindent!` macro, but works at runtime.
|
||||
pub fn unindent(s: &str) -> String {
|
||||
let mut bytes = s.as_bytes().to_owned();
|
||||
let unindented_size = unindent_bytes(&mut bytes);
|
||||
bytes.resize(unindented_size, 0);
|
||||
String::from_utf8(bytes).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SAMPLE_1_WITH_FIRST_LINE: &str = " first line
|
||||
line one
|
||||
|
||||
line two
|
||||
";
|
||||
|
||||
const UNINDENTED_1: &str = " first line\nline one\n\n line two\n";
|
||||
|
||||
const SAMPLE_2_EMPTY_FIRST_LINE: &str = "
|
||||
line one
|
||||
|
||||
line two
|
||||
";
|
||||
const UNINDENTED_2: &str = "line one\n\n line two\n";
|
||||
|
||||
const SAMPLE_3_NO_INDENT: &str = "
|
||||
no indent
|
||||
here";
|
||||
|
||||
const UNINDENTED_3: &str = "no indent\n here";
|
||||
|
||||
const SAMPLE_4_NOOP: &str = "no indent\nhere\n but here";
|
||||
|
||||
const SAMPLE_5_EMPTY: &str = " \n \n";
|
||||
|
||||
const ALL_CASES: &[(&str, &str)] = &[
|
||||
(SAMPLE_1_WITH_FIRST_LINE, UNINDENTED_1),
|
||||
(SAMPLE_2_EMPTY_FIRST_LINE, UNINDENTED_2),
|
||||
(SAMPLE_3_NO_INDENT, UNINDENTED_3),
|
||||
(SAMPLE_4_NOOP, SAMPLE_4_NOOP),
|
||||
(SAMPLE_5_EMPTY, SAMPLE_5_EMPTY),
|
||||
];
|
||||
|
||||
// run const tests for each sample to ensure they work at compile time
|
||||
|
||||
#[test]
|
||||
fn test_unindent_const() {
|
||||
const UNINDENTED: &str = unindent!(SAMPLE_1_WITH_FIRST_LINE);
|
||||
assert_eq!(UNINDENTED, UNINDENTED_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unindent_const_removes_empty_first_line() {
|
||||
const UNINDENTED: &str = unindent!(SAMPLE_2_EMPTY_FIRST_LINE);
|
||||
assert_eq!(UNINDENTED, UNINDENTED_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unindent_const_no_indent() {
|
||||
const UNINDENTED: &str = unindent!(SAMPLE_3_NO_INDENT);
|
||||
assert_eq!(UNINDENTED, UNINDENTED_3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unindent_macro_runtime() {
|
||||
// this variation on the test ensures full coverage (const eval not included in coverage)
|
||||
const INDENTED: &str = SAMPLE_1_WITH_FIRST_LINE;
|
||||
const LEN: usize = INDENTED.len();
|
||||
let (unindented, unindented_size) = unindent_sized::<LEN>(INDENTED.as_bytes());
|
||||
let unindented = core::str::from_utf8(&unindented[..unindented_size]).unwrap();
|
||||
assert_eq!(unindented, UNINDENTED_1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unindent_function() {
|
||||
for (indented, expected) in ALL_CASES {
|
||||
let unindented = unindent(indented);
|
||||
assert_eq!(&unindented, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+197
@@ -0,0 +1,197 @@
|
||||
#![warn(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::{convert::Infallible, marker::PhantomData, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
ffi, types::PyNone, Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyResult, Python,
|
||||
};
|
||||
|
||||
/// Used to wrap values in `Option<T>` for default arguments.
|
||||
pub trait SomeWrap<T> {
|
||||
fn wrap(self) -> Option<T>;
|
||||
}
|
||||
|
||||
impl<T> SomeWrap<T> for T {
|
||||
fn wrap(self) -> Option<T> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SomeWrap<T> for Option<T> {
|
||||
fn wrap(self) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OkWrapper<T>(OkWrapperInner<T>);
|
||||
pub struct OkWrapperInner<T>(PhantomData<T>);
|
||||
|
||||
impl<T> OkWrapper<T> {
|
||||
pub fn new(_: &T) -> Self {
|
||||
Self(OkWrapperInner(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for OkWrapper<T> {
|
||||
type Target = OkWrapperInner<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> OkWrapper<Result<T, E>> {
|
||||
pub fn ok_wrap(&self, value: Result<T, E>) -> Result<T, E> {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> OkWrapperInner<T> {
|
||||
pub fn ok_wrap(&self, value: T) -> Result<T, Infallible> {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Hierarchy of conversions used in the function return type machinery
|
||||
pub struct Converter<T>(EmptyTupleConverter<T>);
|
||||
pub struct EmptyTupleConverter<T>(IntoPyObjectConverter<T>);
|
||||
pub struct IntoPyObjectConverter<T>(UnknownReturnResultType<T>);
|
||||
pub struct UnknownReturnResultType<T>(UnknownReturnType<T>);
|
||||
pub struct UnknownReturnType<T>(PhantomData<T>);
|
||||
|
||||
pub fn converter<T>(_: &T) -> Converter<T> {
|
||||
Converter(EmptyTupleConverter(IntoPyObjectConverter(
|
||||
UnknownReturnResultType(UnknownReturnType(PhantomData)),
|
||||
)))
|
||||
}
|
||||
|
||||
impl<T> Deref for Converter<T> {
|
||||
type Target = EmptyTupleConverter<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for EmptyTupleConverter<T> {
|
||||
type Target = IntoPyObjectConverter<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for IntoPyObjectConverter<T> {
|
||||
type Target = UnknownReturnResultType<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for UnknownReturnResultType<T> {
|
||||
type Target = UnknownReturnType<T>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl EmptyTupleConverter<PyResult<()>> {
|
||||
#[inline]
|
||||
pub fn map_into_ptr(&self, py: Python<'_>, obj: PyResult<()>) -> PyResult<*mut ffi::PyObject> {
|
||||
obj.map(|_| PyNone::get(py).to_owned().into_ptr())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_pyobject(&self, py: Python<'_>, obj: PyResult<()>) -> PyResult<Py<PyAny>> {
|
||||
obj.map(|_| PyNone::get(py).to_owned().into_any().unbind())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: IntoPyObject<'py>> IntoPyObjectConverter<T> {
|
||||
#[inline]
|
||||
pub fn wrap(&self, obj: T) -> Result<T, Infallible> {
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter<Result<T, E>> {
|
||||
#[inline]
|
||||
pub fn wrap(&self, obj: Result<T, E>) -> Result<T, E> {
|
||||
obj
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_pyobject(&self, py: Python<'py>, obj: PyResult<T>) -> PyResult<Py<PyAny>>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
obj.and_then(|obj| obj.into_py_any(py))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_ptr(&self, py: Python<'py>, obj: PyResult<T>) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
obj.and_then(|obj| obj.into_bound_py_any(py))
|
||||
.map(Bound::into_ptr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> UnknownReturnResultType<Result<T, E>> {
|
||||
#[inline]
|
||||
pub fn wrap<'py>(&self, _: Result<T, E>) -> Result<T, E>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unreachable!("should be handled by IntoPyObjectConverter")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UnknownReturnType<T> {
|
||||
#[inline]
|
||||
pub fn wrap<'py>(&self, _: T) -> T
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unreachable!("should be handled by IntoPyObjectConverter")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_pyobject<'py>(&self, _: Python<'py>, _: PyResult<T>) -> PyResult<Py<PyAny>>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unreachable!("should be handled by IntoPyObjectConverter")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_ptr<'py>(&self, _: Python<'py>, _: PyResult<T>) -> PyResult<*mut ffi::PyObject>
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unreachable!("should be handled by IntoPyObjectConverter")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wrap_option() {
|
||||
let a: Option<u8> = SomeWrap::wrap(42);
|
||||
assert_eq!(a, Some(42));
|
||||
|
||||
let b: Option<u8> = SomeWrap::wrap(None);
|
||||
assert_eq!(b, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_result() {
|
||||
let a = 42;
|
||||
let Ok(a) = OkWrapper::new(&a).ok_wrap(a);
|
||||
assert_eq!(a, 42);
|
||||
|
||||
let b = Result::<_, String>::Ok(42);
|
||||
let b = OkWrapper::new(&b).ok_wrap(b);
|
||||
assert_eq!(b, Ok(42));
|
||||
}
|
||||
}
|
||||
Vendored
+578
@@ -0,0 +1,578 @@
|
||||
//! Runtime inspection of objects exposed to Python.
|
||||
//!
|
||||
//! Tracking issue: <https://github.com/PyO3/pyo3/issues/2454>.
|
||||
|
||||
use crate::impl_::introspection::{escape_json_string, escaped_json_string_len};
|
||||
use core::fmt::{self, Display, Write};
|
||||
|
||||
/// Builds a type hint from a module name and a member name in the module
|
||||
///
|
||||
/// ```
|
||||
/// use pyo3::type_hint_identifier;
|
||||
/// use pyo3::inspect::PyStaticExpr;
|
||||
///
|
||||
/// const T: PyStaticExpr = type_hint_identifier!("datetime", "date");
|
||||
/// assert_eq!(T.to_string(), "datetime.date");
|
||||
///
|
||||
/// const T2: PyStaticExpr = type_hint_identifier!("builtins", "int");
|
||||
/// assert_eq!(T2.to_string(), "int");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! type_hint_identifier {
|
||||
("builtins", $name:expr) => {
|
||||
$crate::inspect::PyStaticExpr::Name { id: $name }
|
||||
};
|
||||
($module:expr, $name:expr) => {
|
||||
$crate::inspect::PyStaticExpr::Attribute {
|
||||
value: &$crate::inspect::PyStaticExpr::Name { id: $module },
|
||||
attr: $name,
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use type_hint_identifier;
|
||||
|
||||
/// Builds the union of multiple type hints
|
||||
///
|
||||
/// ```
|
||||
/// use pyo3::{type_hint_identifier, type_hint_union};
|
||||
/// use pyo3::inspect::PyStaticExpr;
|
||||
///
|
||||
/// const T: PyStaticExpr = type_hint_union!(type_hint_identifier!("builtins", "int"), type_hint_identifier!("builtins", "float"));
|
||||
/// assert_eq!(T.to_string(), "int | float");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! type_hint_union {
|
||||
($e:expr) => { $e };
|
||||
($l:expr , $($r:expr),+) => { $crate::inspect::PyStaticExpr::BinOp {
|
||||
left: &$l,
|
||||
op: $crate::inspect::PyStaticOperator::BitOr,
|
||||
right: &type_hint_union!($($r),+),
|
||||
} };
|
||||
}
|
||||
pub(crate) use type_hint_union;
|
||||
|
||||
/// Builds a subscribed type hint
|
||||
///
|
||||
/// ```
|
||||
/// use pyo3::{type_hint_identifier, type_hint_subscript};
|
||||
/// use pyo3::inspect::PyStaticExpr;
|
||||
///
|
||||
/// const T: PyStaticExpr = type_hint_subscript!(type_hint_identifier!("collections.abc", "Sequence"), type_hint_identifier!("builtins", "float"));
|
||||
/// assert_eq!(T.to_string(), "collections.abc.Sequence[float]");
|
||||
///
|
||||
/// const T2: PyStaticExpr = type_hint_subscript!(type_hint_identifier!("builtins", "dict"), type_hint_identifier!("builtins", "str"), type_hint_identifier!("builtins", "float"));
|
||||
/// assert_eq!(T2.to_string(), "dict[str, float]");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! type_hint_subscript {
|
||||
($l:expr, $r:expr) => {
|
||||
$crate::inspect::PyStaticExpr::Subscript {
|
||||
value: &$l,
|
||||
slice: &$r
|
||||
}
|
||||
};
|
||||
($l:expr, $($r:expr),*) => {
|
||||
$crate::inspect::PyStaticExpr::Subscript {
|
||||
value: &$l,
|
||||
slice: &$crate::inspect::PyStaticExpr::Tuple { elts: &[$($r),*] }
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use type_hint_subscript;
|
||||
|
||||
/// A Python expression.
|
||||
///
|
||||
/// This is the `expr` production of the [Python `ast` module grammar](https://docs.python.org/3/library/ast.html#abstract-grammar)
|
||||
///
|
||||
/// This struct aims at being used in `const` contexts like in [`FromPyObject::INPUT_TYPE`](crate::FromPyObject::INPUT_TYPE) and [`IntoPyObject::OUTPUT_TYPE`](crate::IntoPyObject::OUTPUT_TYPE).
|
||||
///
|
||||
/// Use macros like [`type_hint_identifier`], [`type_hint_union`] and [`type_hint_subscript`] to construct values.
|
||||
#[derive(Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum PyStaticExpr {
|
||||
/// A constant like `None` or `123`
|
||||
Constant { value: PyStaticConstant },
|
||||
/// A name
|
||||
Name { id: &'static str },
|
||||
/// An attribute `value.attr`
|
||||
Attribute {
|
||||
value: &'static Self,
|
||||
attr: &'static str,
|
||||
},
|
||||
/// A binary operator
|
||||
BinOp {
|
||||
left: &'static Self,
|
||||
op: PyStaticOperator,
|
||||
right: &'static Self,
|
||||
},
|
||||
/// A tuple
|
||||
Tuple { elts: &'static [Self] },
|
||||
/// A list
|
||||
List { elts: &'static [Self] },
|
||||
/// A subscript `value[slice]`
|
||||
Subscript {
|
||||
value: &'static Self,
|
||||
slice: &'static Self,
|
||||
},
|
||||
/// A `#[pyclass]` type. This is separated type for introspection reasons.
|
||||
PyClass(PyClassNameStaticExpr),
|
||||
}
|
||||
|
||||
/// Serialize the type for introspection and return the number of written bytes
|
||||
#[doc(hidden)]
|
||||
pub const fn serialize_for_introspection(expr: &PyStaticExpr, mut output: &mut [u8]) -> usize {
|
||||
let original_len = output.len();
|
||||
match expr {
|
||||
PyStaticExpr::Constant { value } => match value {
|
||||
PyStaticConstant::None => {
|
||||
output = write_slice_and_move_forward(
|
||||
b"{\"type\":\"constant\",\"kind\":\"none\"}",
|
||||
output,
|
||||
)
|
||||
}
|
||||
PyStaticConstant::Bool(value) => {
|
||||
output = write_slice_and_move_forward(
|
||||
if *value {
|
||||
b"{\"type\":\"constant\",\"kind\":\"bool\",\"value\":true}"
|
||||
} else {
|
||||
b"{\"type\":\"constant\",\"kind\":\"bool\",\"value\":false}"
|
||||
},
|
||||
output,
|
||||
)
|
||||
}
|
||||
PyStaticConstant::Int(value) => {
|
||||
output = write_slice_and_move_forward(
|
||||
b"{\"type\":\"constant\",\"kind\":\"int\",\"value\":\"",
|
||||
output,
|
||||
);
|
||||
output = write_slice_and_move_forward(value.as_bytes(), output);
|
||||
output = write_slice_and_move_forward(b"\"}", output);
|
||||
}
|
||||
PyStaticConstant::Float(value) => {
|
||||
output = write_slice_and_move_forward(
|
||||
b"{\"type\":\"constant\",\"kind\":\"float\",\"value\":\"",
|
||||
output,
|
||||
);
|
||||
output = write_slice_and_move_forward(value.as_bytes(), output);
|
||||
output = write_slice_and_move_forward(b"\"}", output);
|
||||
}
|
||||
PyStaticConstant::Str(value) => {
|
||||
output = write_slice_and_move_forward(
|
||||
b"{\"type\":\"constant\",\"kind\":\"str\",\"value\":",
|
||||
output,
|
||||
);
|
||||
output = write_json_string_and_move_forward(value, output);
|
||||
output = write_slice_and_move_forward(b"}", output);
|
||||
}
|
||||
PyStaticConstant::Ellipsis => {
|
||||
output = write_slice_and_move_forward(
|
||||
b"{\"type\":\"constant\",\"kind\":\"ellipsis\"}",
|
||||
output,
|
||||
)
|
||||
}
|
||||
},
|
||||
PyStaticExpr::Name { id } => {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"name\",\"id\":\"", output);
|
||||
output = write_slice_and_move_forward(id.as_bytes(), output);
|
||||
output = write_slice_and_move_forward(b"\"}", output);
|
||||
}
|
||||
PyStaticExpr::Attribute { value, attr } => {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"attribute\",\"value\":", output);
|
||||
output = write_expr_and_move_forward(value, output);
|
||||
output = write_slice_and_move_forward(b",\"attr\":\"", output);
|
||||
output = write_slice_and_move_forward(attr.as_bytes(), output);
|
||||
output = write_slice_and_move_forward(b"\"}", output);
|
||||
}
|
||||
PyStaticExpr::BinOp { left, op, right } => {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"binop\",\"left\":", output);
|
||||
output = write_expr_and_move_forward(left, output);
|
||||
output = write_slice_and_move_forward(b",\"op\":\"", output);
|
||||
output = write_slice_and_move_forward(
|
||||
match op {
|
||||
PyStaticOperator::BitOr => b"bitor",
|
||||
},
|
||||
output,
|
||||
);
|
||||
output = write_slice_and_move_forward(b"\",\"right\":", output);
|
||||
output = write_expr_and_move_forward(right, output);
|
||||
output = write_slice_and_move_forward(b"}", output);
|
||||
}
|
||||
PyStaticExpr::Tuple { elts } => {
|
||||
output = write_container_and_move_forward(b"tuple", elts, output);
|
||||
}
|
||||
PyStaticExpr::List { elts } => {
|
||||
output = write_container_and_move_forward(b"list", elts, output);
|
||||
}
|
||||
PyStaticExpr::Subscript { value, slice } => {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"subscript\",\"value\":", output);
|
||||
output = write_expr_and_move_forward(value, output);
|
||||
output = write_slice_and_move_forward(b",\"slice\":", output);
|
||||
output = write_expr_and_move_forward(slice, output);
|
||||
output = write_slice_and_move_forward(b"}", output);
|
||||
}
|
||||
PyStaticExpr::PyClass(expr) => {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"id\",\"id\":\"", output);
|
||||
output = write_slice_and_move_forward(expr.introspection_id.as_bytes(), output);
|
||||
output = write_slice_and_move_forward(b"\"}", output);
|
||||
}
|
||||
}
|
||||
original_len - output.len()
|
||||
}
|
||||
|
||||
/// Length required by [`serialize_for_introspection`]
|
||||
#[doc(hidden)]
|
||||
pub const fn serialized_len_for_introspection(expr: &PyStaticExpr) -> usize {
|
||||
match expr {
|
||||
PyStaticExpr::Constant { value } => match value {
|
||||
PyStaticConstant::None => 33,
|
||||
PyStaticConstant::Bool(value) => 42 + if *value { 4 } else { 5 },
|
||||
PyStaticConstant::Int(value) => 43 + value.len(),
|
||||
PyStaticConstant::Float(value) => 45 + value.len(),
|
||||
PyStaticConstant::Str(value) => 43 + escaped_json_string_len(value),
|
||||
PyStaticConstant::Ellipsis => 37,
|
||||
},
|
||||
PyStaticExpr::Name { id } => 23 + id.len(),
|
||||
PyStaticExpr::Attribute { value, attr } => {
|
||||
39 + serialized_len_for_introspection(value) + attr.len()
|
||||
}
|
||||
PyStaticExpr::BinOp { left, op, right } => {
|
||||
41 + serialized_len_for_introspection(left)
|
||||
+ match op {
|
||||
PyStaticOperator::BitOr => 5,
|
||||
}
|
||||
+ serialized_len_for_introspection(right)
|
||||
}
|
||||
PyStaticExpr::Tuple { elts } => 5 + serialized_container_len_for_introspection(elts),
|
||||
PyStaticExpr::List { elts } => 4 + serialized_container_len_for_introspection(elts),
|
||||
PyStaticExpr::Subscript { value, slice } => {
|
||||
38 + serialized_len_for_introspection(value) + serialized_len_for_introspection(slice)
|
||||
}
|
||||
PyStaticExpr::PyClass(expr) => 21 + expr.introspection_id.len(),
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyStaticExpr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Constant { value } => match value {
|
||||
PyStaticConstant::None => f.write_str("None"),
|
||||
PyStaticConstant::Bool(value) => f.write_str(if *value { "True" } else { "False" }),
|
||||
PyStaticConstant::Int(value) => f.write_str(value),
|
||||
PyStaticConstant::Float(value) => {
|
||||
f.write_str(value)?;
|
||||
if !value.contains(['.', 'e', 'E']) {
|
||||
// Makes sure it's not parsed as an int
|
||||
f.write_char('.')?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
PyStaticConstant::Str(value) => write!(f, "{value:?}"),
|
||||
PyStaticConstant::Ellipsis => f.write_str("..."),
|
||||
},
|
||||
Self::Name { id, .. } => f.write_str(id),
|
||||
Self::Attribute { value, attr } => {
|
||||
value.fmt(f)?;
|
||||
f.write_str(".")?;
|
||||
f.write_str(attr)
|
||||
}
|
||||
Self::BinOp { left, op, right } => {
|
||||
left.fmt(f)?;
|
||||
f.write_char(' ')?;
|
||||
f.write_char(match op {
|
||||
PyStaticOperator::BitOr => '|',
|
||||
})?;
|
||||
f.write_char(' ')?;
|
||||
right.fmt(f)
|
||||
}
|
||||
Self::Tuple { elts } => {
|
||||
f.write_char('(')?;
|
||||
fmt_elements(elts, f)?;
|
||||
if elts.len() == 1 {
|
||||
f.write_char(',')?;
|
||||
}
|
||||
f.write_char(')')
|
||||
}
|
||||
Self::List { elts } => {
|
||||
f.write_char('[')?;
|
||||
fmt_elements(elts, f)?;
|
||||
f.write_char(']')
|
||||
}
|
||||
Self::Subscript { value, slice } => {
|
||||
value.fmt(f)?;
|
||||
f.write_char('[')?;
|
||||
if let PyStaticExpr::Tuple { elts } = slice {
|
||||
// We don't display the tuple parentheses
|
||||
fmt_elements(elts, f)?;
|
||||
} else {
|
||||
slice.fmt(f)?;
|
||||
}
|
||||
f.write_char(']')
|
||||
}
|
||||
Self::PyClass(expr) => expr.expr.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A PyO3 extension to the Python AST to know more about [`PyStaticExpr::Constant`].
|
||||
///
|
||||
/// This enables advanced features like escaping.
|
||||
#[derive(Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum PyStaticConstant {
|
||||
/// None
|
||||
None,
|
||||
/// The `True` and `False` booleans
|
||||
Bool(bool),
|
||||
/// `int` value written in base 10 (`[+-]?[0-9]+`)
|
||||
Int(&'static str),
|
||||
/// `float` value written in base-10 (`[+-]?[0-9]*(.[0-9]*)*([eE])[0-9]*`), not including Inf and NaN
|
||||
Float(&'static str),
|
||||
/// `str` value unescaped and without quotes
|
||||
Str(&'static str),
|
||||
/// `...` value
|
||||
Ellipsis,
|
||||
}
|
||||
|
||||
/// An operator used in [`PyStaticExpr::BinOp`].
|
||||
#[derive(Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum PyStaticOperator {
|
||||
/// `|` operator
|
||||
BitOr,
|
||||
}
|
||||
|
||||
const fn write_slice_and_move_forward<'a>(value: &[u8], output: &'a mut [u8]) -> &'a mut [u8] {
|
||||
// TODO: use copy_from_slice with MSRV 1.87+
|
||||
let mut i = 0;
|
||||
while i < value.len() {
|
||||
output[i] = value[i];
|
||||
i += 1;
|
||||
}
|
||||
output.split_at_mut(value.len()).1
|
||||
}
|
||||
|
||||
const fn write_json_string_and_move_forward<'a>(value: &str, output: &'a mut [u8]) -> &'a mut [u8] {
|
||||
output[0] = b'"';
|
||||
let output = output.split_at_mut(1).1;
|
||||
let written = escape_json_string(value, output);
|
||||
output[written] = b'"';
|
||||
output.split_at_mut(written + 1).1
|
||||
}
|
||||
|
||||
const fn write_expr_and_move_forward<'a>(
|
||||
value: &PyStaticExpr,
|
||||
output: &'a mut [u8],
|
||||
) -> &'a mut [u8] {
|
||||
let written = serialize_for_introspection(value, output);
|
||||
output.split_at_mut(written).1
|
||||
}
|
||||
|
||||
const fn write_container_and_move_forward<'a>(
|
||||
name: &'static [u8],
|
||||
elts: &[PyStaticExpr],
|
||||
mut output: &'a mut [u8],
|
||||
) -> &'a mut [u8] {
|
||||
output = write_slice_and_move_forward(b"{\"type\":\"", output);
|
||||
output = write_slice_and_move_forward(name, output);
|
||||
output = write_slice_and_move_forward(b"\",\"elts\":[", output);
|
||||
let mut i = 0;
|
||||
while i < elts.len() {
|
||||
if i > 0 {
|
||||
output = write_slice_and_move_forward(b",", output);
|
||||
}
|
||||
output = write_expr_and_move_forward(&elts[i], output);
|
||||
i += 1;
|
||||
}
|
||||
write_slice_and_move_forward(b"]}", output)
|
||||
}
|
||||
|
||||
const fn serialized_container_len_for_introspection(elts: &[PyStaticExpr]) -> usize {
|
||||
let mut len = 21;
|
||||
let mut i = 0;
|
||||
while i < elts.len() {
|
||||
if i > 0 {
|
||||
len += 1;
|
||||
}
|
||||
len += serialized_len_for_introspection(&elts[i]);
|
||||
i += 1;
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
fn fmt_elements(elts: &[PyStaticExpr], f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (i, elt) in elts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
elt.fmt(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The full name of a `#[pyclass]` inside a [`PyStaticExpr`].
|
||||
///
|
||||
/// To get the underlying [`PyStaticExpr`] use [`expr`](PyClassNameStaticExpr::expr).
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PyClassNameStaticExpr {
|
||||
expr: &'static PyStaticExpr,
|
||||
introspection_id: &'static str,
|
||||
}
|
||||
|
||||
impl PyClassNameStaticExpr {
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
pub const fn new(expr: &'static PyStaticExpr, introspection_id: &'static str) -> Self {
|
||||
Self {
|
||||
expr,
|
||||
introspection_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// The pyclass type as an expression like `module.name`
|
||||
///
|
||||
/// This is based on the `name` and `module` parameter of the `#[pyclass]` macro.
|
||||
/// The `module` part might not be a valid module from which the type can be imported.
|
||||
#[inline]
|
||||
pub const fn expr(&self) -> &'static PyStaticExpr {
|
||||
self.expr
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<PyStaticExpr> for PyClassNameStaticExpr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &PyStaticExpr {
|
||||
self.expr
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
const T: PyStaticExpr = type_hint_subscript!(
|
||||
type_hint_identifier!("builtins", "dict"),
|
||||
type_hint_union!(
|
||||
type_hint_identifier!("builtins", "int"),
|
||||
type_hint_subscript!(
|
||||
type_hint_identifier!("typing", "Literal"),
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Str("\0\t\\\"")
|
||||
}
|
||||
)
|
||||
),
|
||||
type_hint_identifier!("datetime", "time")
|
||||
);
|
||||
assert_eq!(
|
||||
T.to_string(),
|
||||
"dict[int | typing.Literal[\"\\0\\t\\\\\\\"\"], datetime.time]"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_for_introspection() {
|
||||
fn check_serialization(expr: PyStaticExpr, expected: &str) {
|
||||
let mut out = vec![0; serialized_len_for_introspection(&expr)];
|
||||
serialize_for_introspection(&expr, &mut out);
|
||||
assert_eq!(core::str::from_utf8(&out).unwrap(), expected)
|
||||
}
|
||||
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::None,
|
||||
},
|
||||
r#"{"type":"constant","kind":"none"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
type_hint_identifier!("builtins", "int"),
|
||||
r#"{"type":"name","id":"int"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
type_hint_identifier!("datetime", "date"),
|
||||
r#"{"type":"attribute","value":{"type":"name","id":"datetime"},"attr":"date"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
type_hint_union!(
|
||||
type_hint_identifier!("builtins", "int"),
|
||||
type_hint_identifier!("builtins", "float")
|
||||
),
|
||||
r#"{"type":"binop","left":{"type":"name","id":"int"},"op":"bitor","right":{"type":"name","id":"float"}}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Tuple {
|
||||
elts: &[type_hint_identifier!("builtins", "list")],
|
||||
},
|
||||
r#"{"type":"tuple","elts":[{"type":"name","id":"list"}]}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::List {
|
||||
elts: &[type_hint_identifier!("builtins", "list")],
|
||||
},
|
||||
r#"{"type":"list","elts":[{"type":"name","id":"list"}]}"#,
|
||||
);
|
||||
check_serialization(
|
||||
type_hint_subscript!(
|
||||
type_hint_identifier!("builtins", "list"),
|
||||
type_hint_identifier!("builtins", "int")
|
||||
),
|
||||
r#"{"type":"subscript","value":{"type":"name","id":"list"},"slice":{"type":"name","id":"int"}}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::PyClass(PyClassNameStaticExpr::new(
|
||||
&type_hint_identifier!("builtins", "foo"),
|
||||
"foo",
|
||||
)),
|
||||
r#"{"type":"id","id":"foo"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Bool(true),
|
||||
},
|
||||
r#"{"type":"constant","kind":"bool","value":true}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Bool(false),
|
||||
},
|
||||
r#"{"type":"constant","kind":"bool","value":false}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Int("-123"),
|
||||
},
|
||||
r#"{"type":"constant","kind":"int","value":"-123"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Float("-2.1"),
|
||||
},
|
||||
r#"{"type":"constant","kind":"float","value":"-2.1"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Float("+2.1e10"),
|
||||
},
|
||||
r#"{"type":"constant","kind":"float","value":"+2.1e10"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Str("abc(1)"),
|
||||
},
|
||||
r#"{"type":"constant","kind":"str","value":"abc(1)"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Str("\"\\/\x08\x0C\n\r\t\0\x19a"),
|
||||
},
|
||||
r#"{"type":"constant","kind":"str","value":"\"\\/\b\f\n\r\t\u0000\u0019a"}"#,
|
||||
);
|
||||
check_serialization(
|
||||
PyStaticExpr::Constant {
|
||||
value: PyStaticConstant::Ellipsis,
|
||||
},
|
||||
r#"{"type":"constant","kind":"ellipsis"}"#,
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+2954
File diff suppressed because it is too large.
Load diff
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
//! Holding place for code which is not intended to be reachable from outside of PyO3.
|
||||
|
||||
#[macro_use]
|
||||
pub(crate) mod macros;
|
||||
|
||||
pub(crate) mod get_slot;
|
||||
pub(crate) mod pyclass_init;
|
||||
pub(crate) mod state;
|
||||
Vendored
+284
@@ -0,0 +1,284 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::{
|
||||
ffi,
|
||||
types::{PyType, PyTypeMethods},
|
||||
Borrowed, Bound,
|
||||
};
|
||||
use core::ffi::c_int;
|
||||
|
||||
impl Bound<'_, PyType> {
|
||||
#[inline]
|
||||
pub(crate) fn get_slot<const S: c_int>(&self, slot: Slot<S>) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
// SAFETY: `self` is a valid type object.
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
self.as_type_ptr(),
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(self.py()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrowed<'_, '_, PyType> {
|
||||
#[inline]
|
||||
pub(crate) fn get_slot<const S: c_int>(self, slot: Slot<S>) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
// SAFETY: `self` is a valid type object.
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
self.as_type_ptr(),
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(self.py()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a slot from a raw FFI pointer.
|
||||
///
|
||||
/// Safety:
|
||||
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
|
||||
/// - The Python runtime must be initialized
|
||||
pub(crate) unsafe fn get_slot<const S: c_int>(
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
slot: Slot<S>,
|
||||
) -> <Slot<S> as GetSlotImpl>::Type
|
||||
where
|
||||
Slot<S>: GetSlotImpl,
|
||||
{
|
||||
unsafe {
|
||||
slot.get_slot(
|
||||
ty,
|
||||
// SAFETY: the Python runtime is initialized
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
is_runtime_3_10(crate::Python::assume_attached()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait GetSlotImpl {
|
||||
type Type;
|
||||
|
||||
/// Gets the requested slot from a type object.
|
||||
///
|
||||
/// Safety:
|
||||
/// - `ty` must be a valid non-null pointer to a `PyTypeObject`.
|
||||
/// - `is_runtime_3_10` must be `false` if the runtime is not Python 3.10 or later.
|
||||
unsafe fn get_slot(
|
||||
self,
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool,
|
||||
) -> Self::Type;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct Slot<const S: c_int>;
|
||||
|
||||
macro_rules! impl_slots {
|
||||
($($name:ident: ($slot:ident, $field:ident) -> $tp:ty),+ $(,)?) => {
|
||||
$(
|
||||
pub (crate) const $name: Slot<{ ffi::$slot }> = Slot;
|
||||
|
||||
impl GetSlotImpl for Slot<{ ffi::$slot }> {
|
||||
type Type = $tp;
|
||||
|
||||
#[inline]
|
||||
unsafe fn get_slot(
|
||||
self,
|
||||
ty: *mut ffi::PyTypeObject,
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool
|
||||
) -> Self::Type {
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
{
|
||||
unsafe {(*ty).$field }
|
||||
}
|
||||
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
#[cfg(not(Py_3_10))]
|
||||
{
|
||||
// Calling PyType_GetSlot on static types is not valid before Python 3.10
|
||||
// ... so the workaround is to first do a runtime check for these versions
|
||||
// (3.8, 3.9) and then look in the type object anyway. This is only ok
|
||||
// because we know that the interpreter is not going to change the size
|
||||
// of the type objects for these historical versions.
|
||||
if !is_runtime_3_10 && unsafe {ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE)} == 0
|
||||
{
|
||||
return unsafe {(*ty.cast::<PyTypeObject39Snapshot>()).$field};
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: slot type is set carefully to be valid
|
||||
unsafe {core::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot))}
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
// Slots are implemented on-demand as needed.)
|
||||
impl_slots! {
|
||||
TP_NEW: (Py_tp_new, tp_new) -> Option<ffi::newfunc>,
|
||||
TP_DEALLOC: (Py_tp_dealloc, tp_dealloc) -> Option<ffi::destructor>,
|
||||
TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject,
|
||||
TP_CLEAR: (Py_tp_clear, tp_clear) -> Option<ffi::inquiry>,
|
||||
TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option<ffi::descrgetfunc>,
|
||||
TP_FREE: (Py_tp_free, tp_free) -> Option<ffi::freefunc>,
|
||||
TP_TRAVERSE: (Py_tp_traverse, tp_traverse) -> Option<ffi::traverseproc>,
|
||||
}
|
||||
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
fn is_runtime_3_10(py: crate::Python<'_>) -> bool {
|
||||
use crate::sync::PyOnceLock;
|
||||
|
||||
static IS_RUNTIME_3_10: PyOnceLock<bool> = PyOnceLock::new();
|
||||
*IS_RUNTIME_3_10.get_or_init(py, || py.version_info() >= (3, 10))
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyNumberMethods39Snapshot {
|
||||
pub nb_add: Option<ffi::binaryfunc>,
|
||||
pub nb_subtract: Option<ffi::binaryfunc>,
|
||||
pub nb_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_remainder: Option<ffi::binaryfunc>,
|
||||
pub nb_divmod: Option<ffi::binaryfunc>,
|
||||
pub nb_power: Option<ffi::ternaryfunc>,
|
||||
pub nb_negative: Option<ffi::unaryfunc>,
|
||||
pub nb_positive: Option<ffi::unaryfunc>,
|
||||
pub nb_absolute: Option<ffi::unaryfunc>,
|
||||
pub nb_bool: Option<ffi::inquiry>,
|
||||
pub nb_invert: Option<ffi::unaryfunc>,
|
||||
pub nb_lshift: Option<ffi::binaryfunc>,
|
||||
pub nb_rshift: Option<ffi::binaryfunc>,
|
||||
pub nb_and: Option<ffi::binaryfunc>,
|
||||
pub nb_xor: Option<ffi::binaryfunc>,
|
||||
pub nb_or: Option<ffi::binaryfunc>,
|
||||
pub nb_int: Option<ffi::unaryfunc>,
|
||||
pub nb_reserved: *mut core::ffi::c_void,
|
||||
pub nb_float: Option<ffi::unaryfunc>,
|
||||
pub nb_inplace_add: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_subtract: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_remainder: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_power: Option<ffi::ternaryfunc>,
|
||||
pub nb_inplace_lshift: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_rshift: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_and: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_xor: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_or: Option<ffi::binaryfunc>,
|
||||
pub nb_floor_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_true_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_floor_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_true_divide: Option<ffi::binaryfunc>,
|
||||
pub nb_index: Option<ffi::unaryfunc>,
|
||||
pub nb_matrix_multiply: Option<ffi::binaryfunc>,
|
||||
pub nb_inplace_matrix_multiply: Option<ffi::binaryfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PySequenceMethods39Snapshot {
|
||||
pub sq_length: Option<ffi::lenfunc>,
|
||||
pub sq_concat: Option<ffi::binaryfunc>,
|
||||
pub sq_repeat: Option<ffi::ssizeargfunc>,
|
||||
pub sq_item: Option<ffi::ssizeargfunc>,
|
||||
pub was_sq_slice: *mut core::ffi::c_void,
|
||||
pub sq_ass_item: Option<ffi::ssizeobjargproc>,
|
||||
pub was_sq_ass_slice: *mut core::ffi::c_void,
|
||||
pub sq_contains: Option<ffi::objobjproc>,
|
||||
pub sq_inplace_concat: Option<ffi::binaryfunc>,
|
||||
pub sq_inplace_repeat: Option<ffi::ssizeargfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyMappingMethods39Snapshot {
|
||||
pub mp_length: Option<ffi::lenfunc>,
|
||||
pub mp_subscript: Option<ffi::binaryfunc>,
|
||||
pub mp_ass_subscript: Option<ffi::objobjargproc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyAsyncMethods39Snapshot {
|
||||
pub am_await: Option<ffi::unaryfunc>,
|
||||
pub am_aiter: Option<ffi::unaryfunc>,
|
||||
pub am_anext: Option<ffi::unaryfunc>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
pub struct PyBufferProcs39Snapshot {
|
||||
// not available in limited api, but structure needs to have the right size
|
||||
pub bf_getbuffer: *mut core::ffi::c_void,
|
||||
pub bf_releasebuffer: *mut core::ffi::c_void,
|
||||
}
|
||||
|
||||
/// Snapshot of the structure of PyTypeObject for Python 3.8 through 3.9.
|
||||
///
|
||||
/// This is used as a fallback for static types in abi3 when the Python version is less than 3.10;
|
||||
/// this is a bit of a hack but there's no better option and the structure of the type object is
|
||||
/// not going to change for those historical versions.
|
||||
#[repr(C)]
|
||||
#[cfg(all(Py_LIMITED_API, not(Py_3_10)))]
|
||||
struct PyTypeObject39Snapshot {
|
||||
pub ob_base: ffi::PyVarObject,
|
||||
pub tp_name: *const core::ffi::c_char,
|
||||
pub tp_basicsize: ffi::Py_ssize_t,
|
||||
pub tp_itemsize: ffi::Py_ssize_t,
|
||||
pub tp_dealloc: Option<ffi::destructor>,
|
||||
pub tp_vectorcall_offset: ffi::Py_ssize_t,
|
||||
pub tp_getattr: Option<ffi::getattrfunc>,
|
||||
pub tp_setattr: Option<ffi::setattrfunc>,
|
||||
pub tp_as_async: *mut PyAsyncMethods39Snapshot,
|
||||
pub tp_repr: Option<ffi::reprfunc>,
|
||||
pub tp_as_number: *mut PyNumberMethods39Snapshot,
|
||||
pub tp_as_sequence: *mut PySequenceMethods39Snapshot,
|
||||
pub tp_as_mapping: *mut PyMappingMethods39Snapshot,
|
||||
pub tp_hash: Option<ffi::hashfunc>,
|
||||
pub tp_call: Option<ffi::ternaryfunc>,
|
||||
pub tp_str: Option<ffi::reprfunc>,
|
||||
pub tp_getattro: Option<ffi::getattrofunc>,
|
||||
pub tp_setattro: Option<ffi::setattrofunc>,
|
||||
pub tp_as_buffer: *mut PyBufferProcs39Snapshot,
|
||||
pub tp_flags: core::ffi::c_ulong,
|
||||
pub tp_doc: *const core::ffi::c_char,
|
||||
pub tp_traverse: Option<ffi::traverseproc>,
|
||||
pub tp_clear: Option<ffi::inquiry>,
|
||||
pub tp_richcompare: Option<ffi::richcmpfunc>,
|
||||
pub tp_weaklistoffset: ffi::Py_ssize_t,
|
||||
pub tp_iter: Option<ffi::getiterfunc>,
|
||||
pub tp_iternext: Option<ffi::iternextfunc>,
|
||||
pub tp_methods: *mut ffi::PyMethodDef,
|
||||
pub tp_members: *mut ffi::PyMemberDef,
|
||||
pub tp_getset: *mut ffi::PyGetSetDef,
|
||||
pub tp_base: *mut ffi::PyTypeObject,
|
||||
pub tp_dict: *mut ffi::PyObject,
|
||||
pub tp_descr_get: Option<ffi::descrgetfunc>,
|
||||
pub tp_descr_set: Option<ffi::descrsetfunc>,
|
||||
pub tp_dictoffset: ffi::Py_ssize_t,
|
||||
pub tp_init: Option<ffi::initproc>,
|
||||
pub tp_alloc: Option<ffi::allocfunc>,
|
||||
pub tp_new: Option<ffi::newfunc>,
|
||||
pub tp_free: Option<ffi::freefunc>,
|
||||
pub tp_is_gc: Option<ffi::inquiry>,
|
||||
pub tp_bases: *mut ffi::PyObject,
|
||||
pub tp_mro: *mut ffi::PyObject,
|
||||
pub tp_cache: *mut ffi::PyObject,
|
||||
pub tp_subclasses: *mut ffi::PyObject,
|
||||
pub tp_weaklist: *mut ffi::PyObject,
|
||||
pub tp_del: Option<ffi::destructor>,
|
||||
pub tp_version_tag: core::ffi::c_uint,
|
||||
pub tp_finalize: Option<ffi::destructor>,
|
||||
pub tp_vectorcall: Option<ffi::vectorcallfunc>,
|
||||
}
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
/// Polyfill of `cfg_select` for MSRV < 1.95
|
||||
///
|
||||
/// Note that this polyfill does not work in type position, so it is not as powerful as the
|
||||
/// real `cfg_select` macro. That is a limitation that PyO3 can live with for now.
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(cfg_select))]
|
||||
macro_rules! cfg_select {
|
||||
// Final arm with _ to match any non-existing clauses
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
_ => $final_arm:expr $(,)?
|
||||
) => {
|
||||
#[cfg(not(any($($clauses),+)))]
|
||||
{
|
||||
$final_arm
|
||||
}
|
||||
};
|
||||
|
||||
// Final arm has a clause, handle it plus a compile error if no clauses match
|
||||
// Need to allow for trailing comma on final expression to be optional
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $final_arm:expr $(,)?
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
{
|
||||
$final_arm
|
||||
}
|
||||
|
||||
#[cfg(not(any($($clauses,)* $cfg)))]
|
||||
compile_error!(
|
||||
"cfg_select! requires a final `_ => expr` arm to be used as a fallback when no other cfg matches"
|
||||
);
|
||||
};
|
||||
|
||||
// Non-terminating expression arm requires trailing comma
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $arm:expr, $($rest:tt)*
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
{
|
||||
$arm
|
||||
}
|
||||
|
||||
cfg_select! {
|
||||
@parsed($($clauses,)* $cfg)
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
|
||||
// Non-terminating block doesn't require trailing comma
|
||||
(
|
||||
@parsed($($clauses:meta),*)
|
||||
$cfg:meta => $arm:block $($rest:tt)*
|
||||
) => {
|
||||
#[cfg(all($cfg, not(any($($clauses),*))))]
|
||||
$arm
|
||||
|
||||
cfg_select! {
|
||||
@parsed($($clauses,)* $cfg)
|
||||
$($rest)*
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$cfg:meta => $($rest:tt)*
|
||||
) => {
|
||||
{
|
||||
cfg_select! {
|
||||
@parsed()
|
||||
$cfg => $($rest)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[cfg(not(cfg_select))]
|
||||
fn test_cfg_select_polyfill_short_circuit() {
|
||||
cfg_select! {
|
||||
all() => {},
|
||||
all() => {
|
||||
unreachable!("the first arm should be selected, so this arm should not be evaluated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Contains initialization utilities for `#[pyclass]`.
|
||||
use crate::exceptions::PyTypeError;
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
use crate::impl_::pyclass::PyClassBaseType;
|
||||
use crate::internal::get_slot::TP_NEW;
|
||||
use crate::types::{PyTuple, PyType, PyTypeMethods};
|
||||
use crate::{
|
||||
ffi, IntoPyObject, IntoPyObjectExt, PyClass, PyClassInitializer, PyErr, PyResult, Python,
|
||||
};
|
||||
use crate::{ffi::PyTypeObject, type_object::PyTypeInfo};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Initializer for Python types.
|
||||
///
|
||||
/// This trait is used internally for distinguishing `#[pyclass]` and
|
||||
/// Python native types.
|
||||
pub(crate) trait PyObjectInit<T>: Sized {
|
||||
/// # Safety
|
||||
/// - `subtype` must be a valid pointer to a type object of T or a subclass.
|
||||
unsafe fn into_new_object(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject>;
|
||||
}
|
||||
|
||||
/// Initializer for Python native types, like `PyDict`.
|
||||
pub struct PyNativeTypeInitializer<T: PyTypeInfo>(pub PhantomData<T>);
|
||||
|
||||
impl<T: PyTypeInfo> PyObjectInit<T> for PyNativeTypeInitializer<T> {
|
||||
unsafe fn into_new_object(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject> {
|
||||
unsafe fn inner(
|
||||
py: Python<'_>,
|
||||
type_ptr: *mut PyTypeObject,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject> {
|
||||
let tp_new = unsafe {
|
||||
type_ptr
|
||||
.cast::<ffi::PyObject>()
|
||||
.assume_borrowed_unchecked(py)
|
||||
.cast_unchecked::<PyType>()
|
||||
.get_slot(TP_NEW)
|
||||
.ok_or_else(|| PyTypeError::new_err("base type without tp_new"))?
|
||||
};
|
||||
|
||||
// TODO: make it possible to provide real arguments to the base tp_new
|
||||
let obj =
|
||||
unsafe { tp_new(subtype, PyTuple::empty(py).as_ptr(), core::ptr::null_mut()) };
|
||||
if obj.is_null() {
|
||||
Err(PyErr::fetch(py))
|
||||
} else {
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
unsafe { inner(py, T::type_object_raw(py), subtype) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TpNewValueTypeResolver<ClassT, ValueT>(TpNewTupleResolver<ClassT, ValueT>);
|
||||
pub struct TpNewTupleResolver<ClassT, ValueT>(
|
||||
ResolveToArbitraryObject,
|
||||
PhantomData<(ClassT, ValueT)>,
|
||||
);
|
||||
pub struct ResolveToArbitraryObject(());
|
||||
|
||||
/// First step of machinery for resolving the type of `#[new]` return values, which is used to
|
||||
/// implement specialization for the various cases.
|
||||
///
|
||||
/// Call `.resolve()` on the returned tag to get the final tag type.
|
||||
///
|
||||
/// This resolution step is necessary in order to encode the preference to go via `PyClassInitializer<T>`
|
||||
/// for new instances of `ClassT`. Without this step, the fallback to `IntoPyObject` conflicts for
|
||||
/// `ClassT` because that implementation ignores the `cls` parameter for `PyClassInit` (and would
|
||||
/// therefore be incorrect when instantiating subclasses).
|
||||
pub fn tp_new_resolver<ClassT, ValueT>(_: &ValueT) -> TpNewValueTypeResolver<ClassT, ValueT> {
|
||||
TpNewValueTypeResolver(TpNewTupleResolver(
|
||||
ResolveToArbitraryObject(()),
|
||||
PhantomData,
|
||||
))
|
||||
}
|
||||
|
||||
impl<ClassT, ValueT> core::ops::Deref for TpNewValueTypeResolver<ClassT, ValueT> {
|
||||
type Target = TpNewTupleResolver<ClassT, ValueT>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<ClassT, ValueT> core::ops::Deref for TpNewTupleResolver<ClassT, ValueT> {
|
||||
type Target = ResolveToArbitraryObject;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Second stage of machinery for resolving the type of `#[new]` return values. This
|
||||
/// is the specialized implementation for the case where the return type should be
|
||||
/// used to build a new class object (`T`, `PyClassInitializer<T>`, `(S, B)` tuples).
|
||||
#[expect(private_bounds, reason = "internal trait implementation")]
|
||||
impl<ClassT, ValueT> TpNewValueTypeResolver<ClassT, ValueT>
|
||||
where
|
||||
ClassT: PyClass,
|
||||
ValueT: IntoPyClassInitializer<ClassT>,
|
||||
{
|
||||
pub fn resolve(&self, value: ValueT) -> PyClassInitializer<ClassT> {
|
||||
value.into_pyclass_initializer()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> TpNewTupleResolver<S, (S, B)>
|
||||
where
|
||||
S: PyClass<BaseType = B>,
|
||||
B: PyClass + PyClassBaseType<Initializer = PyClassInitializer<B>>,
|
||||
B::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<B::BaseType>>,
|
||||
{
|
||||
#[deprecated(
|
||||
since = "0.29.0",
|
||||
note = "Tuple syntax for super class initialization is phased out. Use `PyClassInitializer` instead."
|
||||
)]
|
||||
pub fn resolve(&self, value: (S, B)) -> PyClassInitializer<S> {
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// All other conversions fall back to IntoPyObject via the deref implementation
|
||||
impl ResolveToArbitraryObject {
|
||||
pub fn resolve<ValueT>(&self, value: ValueT) -> ValueT {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "`{Self}` cannot be used as the return value for `#[new]` methods",
|
||||
note = "all types which implement `IntoPyObject` are suitable return values for `#[new]` methods",
|
||||
note = "`PyClassInitializer<T: PyClass>` may also be used and is necessary for `#[pyclass(extends = '...')]` types",
|
||||
label = "must be a type which implements `IntoPyObject`, a `PyClassInitializer<{Self}>`, or a `Result<T>` wrapping such types"
|
||||
)]
|
||||
pub(crate) trait PyClassInit<'py, T> {
|
||||
/// # Safety
|
||||
/// - `cls` must be the type object for `T` (or a subclass)
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>>;
|
||||
}
|
||||
|
||||
impl<'py, ClassT, T> PyClassInit<'py, ClassT> for T
|
||||
where
|
||||
T: IntoPyObject<'py>,
|
||||
{
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>> {
|
||||
self.into_bound_py_any(cls.py())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T> PyClassInit<'py, T> for PyClassInitializer<T>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
unsafe fn init(
|
||||
self,
|
||||
cls: crate::Borrowed<'_, 'py, crate::types::PyType>,
|
||||
) -> PyResult<crate::Bound<'py, crate::PyAny>> {
|
||||
// SAFETY: caller has guaranteed that `cls` is correct object
|
||||
unsafe { self.create_class_object_of_type(cls.py(), cls.as_type_ptr()) }
|
||||
.map(crate::Bound::into_any)
|
||||
}
|
||||
}
|
||||
|
||||
/// Analogous to `Into<PyClassInitializer<T>>`, but just an internal equivalent
|
||||
/// to avoid allowing user code to define custom return types from `#[new]`.
|
||||
trait IntoPyClassInitializer<T: PyClass>: Sized {
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T>;
|
||||
}
|
||||
|
||||
impl<T: PyClass> IntoPyClassInitializer<T> for PyClassInitializer<T> {
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoPyClassInitializer<T> for T
|
||||
where
|
||||
T: PyClass,
|
||||
T::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<T::BaseType>>,
|
||||
{
|
||||
fn into_pyclass_initializer(self) -> PyClassInitializer<T> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
Vendored
+602
@@ -0,0 +1,602 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Interaction with attachment of the current thread to the Python interpreter.
|
||||
|
||||
#[cfg(pyo3_disable_reference_pool)]
|
||||
use crate::impl_::panic::PanicTrap;
|
||||
use crate::{ffi, Python};
|
||||
|
||||
use core::cell::Cell;
|
||||
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))]
|
||||
use core::{mem, ptr::NonNull};
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
std::thread_local! {
|
||||
/// This is an internal counter in pyo3 monitoring whether this thread is attached to the interpreter.
|
||||
///
|
||||
/// It will be incremented whenever an AttachGuard is created, and decremented whenever
|
||||
/// they are dropped.
|
||||
///
|
||||
/// As a result, if this thread is attached to the interpreter, ATTACH_COUNT is greater than zero.
|
||||
///
|
||||
/// Additionally, we sometimes need to prevent safe access to the Python interpreter,
|
||||
/// e.g. when implementing `__traverse__`, which is represented by a negative value.
|
||||
static ATTACH_COUNT: Cell<isize> = const { Cell::new(0) };
|
||||
}
|
||||
|
||||
const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1;
|
||||
|
||||
/// Checks whether the thread is attached to the Python interpreter.
|
||||
///
|
||||
/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons:
|
||||
/// 1) for performance
|
||||
/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called,
|
||||
/// which could lead to incorrect conclusions that the thread is attached.
|
||||
#[inline(always)]
|
||||
pub(crate) fn thread_is_attached() -> bool {
|
||||
ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// RAII type that represents thread attachment to the interpreter.
|
||||
pub(crate) enum AttachGuard {
|
||||
/// Indicates the thread was already attached when this AttachGuard was acquired.
|
||||
Assumed,
|
||||
/// Indicates that we attached when this AttachGuard was acquired
|
||||
Ensured { gstate: ffi::PyGILState_STATE },
|
||||
}
|
||||
|
||||
/// Possible error when calling `try_attach()`
|
||||
pub(crate) enum AttachError {
|
||||
/// Forbidden during GC traversal.
|
||||
ForbiddenDuringTraverse,
|
||||
/// The interpreter is not initialized.
|
||||
NotInitialized,
|
||||
#[cfg(Py_3_13)]
|
||||
/// The interpreter is finalizing.
|
||||
Finalizing,
|
||||
}
|
||||
|
||||
impl AttachGuard {
|
||||
/// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach.
|
||||
///
|
||||
/// If the thread was already attached via PyO3, this returns
|
||||
/// `AttachGuard::Assumed`. Otherwise, the thread will attach now and
|
||||
/// `AttachGuard::Ensured` will be returned.
|
||||
pub(crate) fn attach() -> Self {
|
||||
match Self::try_attach() {
|
||||
Ok(guard) => guard,
|
||||
Err(AttachError::ForbiddenDuringTraverse) => {
|
||||
panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE)
|
||||
}
|
||||
Err(AttachError::NotInitialized) => {
|
||||
// try to initialize the interpreter and try again
|
||||
crate::interpreter_lifecycle::ensure_initialized();
|
||||
unsafe { Self::do_attach_unchecked() }
|
||||
}
|
||||
#[cfg(Py_3_13)]
|
||||
Err(AttachError::Finalizing) => {
|
||||
panic!("Cannot attach to the Python interpreter while it is finalizing.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Variant of the above which will will return gracefully if the interpreter cannot be attached to.
|
||||
pub(crate) fn try_attach() -> Result<Self, AttachError> {
|
||||
match ATTACH_COUNT.try_with(|c| c.get()) {
|
||||
Ok(i) if i > 0 => {
|
||||
// SAFETY: We just checked that the thread is already attached.
|
||||
return Ok(unsafe { Self::assume() });
|
||||
}
|
||||
// Cannot attach during GC traversal.
|
||||
Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => {
|
||||
return Err(AttachError::ForbiddenDuringTraverse)
|
||||
}
|
||||
// other cases handled below
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// SAFETY: always safe to call this
|
||||
if unsafe { ffi::Py_IsInitialized() } == 0 {
|
||||
return Err(AttachError::NotInitialized);
|
||||
}
|
||||
|
||||
// Py_IsInitialized() can return 1 while Py_InitializeEx is still
|
||||
// running (e.g. importing site.py). Block until any in-progress PyO3
|
||||
// initialization has fully completed.
|
||||
crate::interpreter_lifecycle::wait_for_initialization();
|
||||
|
||||
// Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable
|
||||
// ways, we'll make a best effort attempt here to avoid that. (There's a time of
|
||||
// check to time-of-use issue, but it's better than nothing.)
|
||||
//
|
||||
// SAFETY: always safe to call this
|
||||
#[cfg(Py_3_13)]
|
||||
if unsafe { ffi::Py_IsFinalizing() } != 0 {
|
||||
// If the interpreter is not initialized, we cannot attach.
|
||||
return Err(AttachError::Finalizing);
|
||||
}
|
||||
|
||||
// SAFETY: We have done everything reasonable to ensure we're in a safe state to
|
||||
// attach to the Python interpreter.
|
||||
Ok(unsafe { Self::do_attach_unchecked() })
|
||||
}
|
||||
|
||||
/// Acquires the `AttachGuard` without performing any state checking.
|
||||
///
|
||||
/// This can be called in "unsafe" contexts where the normal interpreter state
|
||||
/// checking performed by `AttachGuard::try_attach` may fail. This includes calling
|
||||
/// as part of multi-phase interpreter initialization.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the Python interpreter is sufficiently initialized
|
||||
/// for a thread to be able to attach to it.
|
||||
pub(crate) unsafe fn attach_unchecked() -> Self {
|
||||
if thread_is_attached() {
|
||||
return unsafe { Self::assume() };
|
||||
}
|
||||
|
||||
unsafe { Self::do_attach_unchecked() }
|
||||
}
|
||||
|
||||
/// Attach to the interpreter, without a fast-path to check if the thread is already attached.
|
||||
#[cold]
|
||||
unsafe fn do_attach_unchecked() -> Self {
|
||||
// SAFETY: interpreter is sufficiently initialized to attach a thread.
|
||||
let gstate = unsafe { ffi::PyGILState_Ensure() };
|
||||
increment_attach_count();
|
||||
// SAFETY: just attached to the interpreter
|
||||
drop_deferred_references(unsafe { Python::assume_attached() });
|
||||
AttachGuard::Ensured { gstate }
|
||||
}
|
||||
|
||||
/// Acquires the `AttachGuard` while assuming that the thread is already attached
|
||||
/// to the interpreter.
|
||||
pub(crate) unsafe fn assume() -> Self {
|
||||
increment_attach_count();
|
||||
// SAFETY: invariant of calling this function
|
||||
drop_deferred_references(unsafe { Python::assume_attached() });
|
||||
AttachGuard::Assumed
|
||||
}
|
||||
|
||||
/// Gets the Python token associated with this [`AttachGuard`].
|
||||
#[inline]
|
||||
pub(crate) fn python(&self) -> Python<'_> {
|
||||
// SAFETY: this guard guarantees the thread is attached
|
||||
unsafe { Python::assume_attached() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach).
|
||||
impl Drop for AttachGuard {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
AttachGuard::Assumed => {}
|
||||
AttachGuard::Ensured { gstate } => unsafe {
|
||||
// Drop the objects in the pool before attempting to release the thread state
|
||||
ffi::PyGILState_Release(*gstate);
|
||||
},
|
||||
}
|
||||
decrement_attach_count();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
type PyObjVec = Vec<NonNull<ffi::PyObject>>;
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
/// Thread-safe storage for objects which were dec_ref while not attached.
|
||||
struct ReferencePool {
|
||||
pending_decrefs: Mutex<PyObjVec>,
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
impl ReferencePool {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
pending_decrefs: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn register_decref(&self, obj: NonNull<ffi::PyObject>) {
|
||||
self.pending_decrefs.lock().unwrap().push(obj);
|
||||
}
|
||||
|
||||
fn drop_deferred_references(&self, _py: Python<'_>) {
|
||||
let mut pending_decrefs = self.pending_decrefs.lock().unwrap();
|
||||
if pending_decrefs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let decrefs = mem::take(&mut *pending_decrefs);
|
||||
drop(pending_decrefs);
|
||||
|
||||
for ptr in decrefs {
|
||||
unsafe { ffi::Py_DECREF(ptr.as_ptr()) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
unsafe impl Send for ReferencePool {}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
unsafe impl Sync for ReferencePool {}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
static POOL: OnceLock<ReferencePool> = OnceLock::new();
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn get_pool() -> &'static ReferencePool {
|
||||
POOL.get_or_init(ReferencePool::new)
|
||||
}
|
||||
|
||||
#[cfg_attr(pyo3_disable_reference_pool, inline(always))]
|
||||
#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))]
|
||||
fn drop_deferred_references(py: Python<'_>) {
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
if let Some(pool) = POOL.get() {
|
||||
pool.drop_deferred_references(py);
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`.
|
||||
pub(crate) struct SuspendAttach {
|
||||
count: isize,
|
||||
tstate: *mut ffi::PyThreadState,
|
||||
}
|
||||
|
||||
impl SuspendAttach {
|
||||
pub(crate) unsafe fn new() -> Self {
|
||||
let count = ATTACH_COUNT.with(|c| c.replace(0));
|
||||
let tstate = unsafe { ffi::PyEval_SaveThread() };
|
||||
|
||||
Self { count, tstate }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SuspendAttach {
|
||||
fn drop(&mut self) {
|
||||
ATTACH_COUNT.with(|c| c.set(self.count));
|
||||
unsafe {
|
||||
ffi::PyEval_RestoreThread(self.tstate);
|
||||
|
||||
// Update counts of `Py<T>` that were dropped while not attached.
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
if let Some(pool) = POOL.get() {
|
||||
pool.drop_deferred_references(Python::assume_attached());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to lock safe access to the interpreter
|
||||
pub(crate) struct ForbidAttaching {
|
||||
count: isize,
|
||||
}
|
||||
|
||||
impl ForbidAttaching {
|
||||
const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running.";
|
||||
|
||||
/// Lock access to the interpreter while an implementation of `__traverse__` is running
|
||||
pub fn during_traverse() -> Self {
|
||||
Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE)
|
||||
}
|
||||
|
||||
fn new(reason: isize) -> Self {
|
||||
let count = ATTACH_COUNT.with(|c| c.replace(reason));
|
||||
|
||||
Self { count }
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn bail(current: isize) {
|
||||
match current {
|
||||
ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE),
|
||||
_ => panic!("Attaching a thread to the interpreter is currently prohibited."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ForbidAttaching {
|
||||
fn drop(&mut self) {
|
||||
ATTACH_COUNT.with(|c| c.set(self.count));
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a Python object pointer inside the release pool, to have its reference count decreased
|
||||
/// the next time the thread is attached in pyo3.
|
||||
///
|
||||
/// If the thread is attached, the reference count will be decreased immediately instead of being queued
|
||||
/// for later.
|
||||
///
|
||||
/// # Safety
|
||||
/// - The object must be an owned Python reference.
|
||||
/// - The reference must not be used after calling this function.
|
||||
#[inline]
|
||||
pub unsafe fn register_decref(obj: NonNull<ffi::PyObject>) {
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
{
|
||||
get_pool().register_decref(obj);
|
||||
}
|
||||
#[cfg(all(
|
||||
pyo3_disable_reference_pool,
|
||||
not(pyo3_leak_on_drop_without_reference_pool)
|
||||
))]
|
||||
{
|
||||
let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop.");
|
||||
panic!("Cannot drop pointer into Python heap without the thread being attached.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3).
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
pub(crate) fn is_in_gc_traversal() -> bool {
|
||||
ATTACH_COUNT
|
||||
.try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created.
|
||||
#[inline(always)]
|
||||
fn increment_attach_count() {
|
||||
// Ignores the error in case this function called from `atexit`.
|
||||
let _ = ATTACH_COUNT.try_with(|c| {
|
||||
let current = c.get();
|
||||
if current < 0 {
|
||||
ForbidAttaching::bail(current);
|
||||
}
|
||||
c.set(current + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/// Decrements pyo3's internal attach count - to be called whenever AttachGuard is dropped.
|
||||
#[inline(always)]
|
||||
fn decrement_attach_count() {
|
||||
// Ignores the error in case this function called from `atexit`.
|
||||
let _ = ATTACH_COUNT.try_with(|c| {
|
||||
let current = c.get();
|
||||
debug_assert!(
|
||||
current > 0,
|
||||
"Negative attach count detected. Please report this error to the PyO3 repo as a bug."
|
||||
);
|
||||
c.set(current - 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::{Py, PyAny, Python};
|
||||
|
||||
fn get_object(py: Python<'_>) -> Py<PyAny> {
|
||||
py.eval(c"object()", None, None).unwrap().unbind()
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn pool_dec_refs_does_not_contain(obj: &Py<PyAny>) -> bool {
|
||||
!get_pool()
|
||||
.pending_decrefs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
|
||||
}
|
||||
|
||||
// With free-threading, threads can empty the POOL at any time, so this
|
||||
// function does not test anything meaningful
|
||||
#[cfg(not(any(pyo3_disable_reference_pool, Py_GIL_DISABLED)))]
|
||||
fn pool_dec_refs_contains(obj: &Py<PyAny>) -> bool {
|
||||
get_pool()
|
||||
.pending_decrefs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyobject_drop_attached_decreases_refcnt() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
// Create a reference to drop while attached.
|
||||
let reference = obj.clone_ref(py);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// While attached, reference count will be decreased immediately.
|
||||
drop(reference);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
#[cfg(not(any(pyo3_disable_reference_pool)))]
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] // We are building wasm Python with pthreads disabled
|
||||
fn test_pyobject_drop_detached_doesnt_decrease_refcnt() {
|
||||
let obj = Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
// Create a reference to drop while detached.
|
||||
let reference = obj.clone_ref(py);
|
||||
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// Drop reference in a separate (detached) thread.
|
||||
std::thread::spawn(move || drop(reference)).join().unwrap();
|
||||
|
||||
// The reference count should not have changed, it is remembered
|
||||
// to release later.
|
||||
assert_eq!(obj._get_refcnt(py), 2);
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
obj
|
||||
});
|
||||
|
||||
// On next attach, the reference is released
|
||||
#[allow(unused)]
|
||||
Python::attach(|py| {
|
||||
// With free-threading, another thread could still be processing
|
||||
// DECREFs after releasing the lock on the POOL, so the
|
||||
// refcnt could still be 2 when this assert happens
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attach_counts() {
|
||||
// Check `attach` and AttachGuard both increase counts correctly
|
||||
let get_attach_count = || ATTACH_COUNT.with(|c| c.get());
|
||||
|
||||
assert_eq!(get_attach_count(), 0);
|
||||
Python::attach(|_| {
|
||||
assert_eq!(get_attach_count(), 1);
|
||||
|
||||
let pool = unsafe { AttachGuard::assume() };
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
let pool2 = unsafe { AttachGuard::assume() };
|
||||
assert_eq!(get_attach_count(), 3);
|
||||
|
||||
drop(pool);
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
Python::attach(|_| {
|
||||
// nested `attach` updates attach count
|
||||
assert_eq!(get_attach_count(), 3);
|
||||
});
|
||||
assert_eq!(get_attach_count(), 2);
|
||||
|
||||
drop(pool2);
|
||||
assert_eq!(get_attach_count(), 1);
|
||||
});
|
||||
assert_eq!(get_attach_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detach() {
|
||||
assert!(!thread_is_attached());
|
||||
|
||||
Python::attach(|py| {
|
||||
assert!(thread_is_attached());
|
||||
|
||||
py.detach(move || {
|
||||
assert!(!thread_is_attached());
|
||||
|
||||
Python::attach(|_| assert!(thread_is_attached()));
|
||||
|
||||
assert!(!thread_is_attached());
|
||||
});
|
||||
|
||||
assert!(thread_is_attached());
|
||||
});
|
||||
|
||||
assert!(!thread_is_attached());
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_detach_updates_refcounts() {
|
||||
Python::attach(|py| {
|
||||
// Make a simple object with 1 reference
|
||||
let obj = get_object(py);
|
||||
assert_eq!(obj._get_refcnt(py), 1);
|
||||
// Cloning the object when detached should panic
|
||||
py.detach(|| obj.clone());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_attach_ok() {
|
||||
Python::attach(|py| {
|
||||
let obj = Python::attach(|_| py.eval(c"object()", None, None).unwrap());
|
||||
assert_eq!(obj._get_refcnt(), 1);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
fn test_clone_attached() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
let count = obj._get_refcnt(py);
|
||||
|
||||
// Cloning when attached should increase reference count immediately
|
||||
#[expect(clippy::redundant_clone)]
|
||||
let c = obj.clone();
|
||||
assert_eq!(count + 1, c._get_refcnt(py));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn test_drop_deferred_references_does_not_deadlock() {
|
||||
// drop_deferred_references can run arbitrary Python code during Py_DECREF.
|
||||
// if the locking is implemented incorrectly, it will deadlock.
|
||||
|
||||
use crate::ffi;
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
|
||||
// This line will implicitly call drop_deferred_references
|
||||
// -> and so cause deadlock if drop_deferred_references is not handling recursion correctly.
|
||||
let pool = unsafe { AttachGuard::assume() };
|
||||
|
||||
// Rebuild obj so that it can be dropped
|
||||
unsafe {
|
||||
use crate::Bound;
|
||||
|
||||
Bound::from_owned_ptr(
|
||||
pool.python(),
|
||||
ffi::PyCapsule_GetPointer(capsule, core::ptr::null()) as _,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let ptr = obj.into_ptr();
|
||||
|
||||
let capsule =
|
||||
unsafe { ffi::PyCapsule_New(ptr as _, core::ptr::null(), Some(capsule_drop)) };
|
||||
|
||||
get_pool().register_decref(NonNull::new(capsule).unwrap());
|
||||
|
||||
// Updating the counts will call decref on the capsule, which calls capsule_drop
|
||||
get_pool().drop_deferred_references(py);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
fn test_attach_guard_drop_deferred_references() {
|
||||
Python::attach(|py| {
|
||||
let obj = get_object(py);
|
||||
|
||||
// For AttachGuard::attach
|
||||
|
||||
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
let _guard = AttachGuard::attach();
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
|
||||
// For AttachGuard::assume
|
||||
|
||||
get_pool().register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap());
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
assert!(pool_dec_refs_contains(&obj));
|
||||
let _guard2 = unsafe { AttachGuard::assume() };
|
||||
assert!(pool_dec_refs_does_not_contain(&obj));
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
use core::ptr::NonNull;
|
||||
|
||||
use crate::ffi::{self, Py_ssize_t, PY_SSIZE_T_MAX};
|
||||
|
||||
macro_rules! pyo3_exception {
|
||||
($doc: expr, $name: ident, $base: ty) => {
|
||||
#[doc = $doc]
|
||||
#[repr(transparent)]
|
||||
pub struct $name($crate::PyAny);
|
||||
|
||||
$crate::impl_exception_boilerplate!($name);
|
||||
|
||||
$crate::create_exception_type_object!(pyo3_runtime, $name, $base, Some($doc));
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert an usize index into a Py_ssize_t index, clamping overflow to
|
||||
/// PY_SSIZE_T_MAX.
|
||||
pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t {
|
||||
index.min(PY_SSIZE_T_MAX as usize) as Py_ssize_t
|
||||
}
|
||||
|
||||
// TODO: use ptr::fn_addr_eq on MSRV 1.85
|
||||
pub(crate) fn clear_eq(f: Option<ffi::inquiry>, g: ffi::inquiry) -> bool {
|
||||
#[cfg(fn_ptr_eq)]
|
||||
#[expect(clippy::incompatible_msrv, reason = "guarded by cfg(fn_ptr_eq)")]
|
||||
{
|
||||
let Some(f) = f else { return false };
|
||||
core::ptr::fn_addr_eq(f, g)
|
||||
}
|
||||
|
||||
#[cfg(not(fn_ptr_eq))]
|
||||
{
|
||||
f == Some(g)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use ptr::fn_addr_eq on MSRV 1.85
|
||||
pub(crate) fn traverse_eq(f: Option<ffi::traverseproc>, g: ffi::traverseproc) -> bool {
|
||||
#[cfg(fn_ptr_eq)]
|
||||
#[expect(clippy::incompatible_msrv, reason = "guarded by cfg(fn_ptr_eq)")]
|
||||
{
|
||||
let Some(f) = f else { return false };
|
||||
core::ptr::fn_addr_eq(f, g)
|
||||
}
|
||||
|
||||
#[cfg(not(fn_ptr_eq))]
|
||||
{
|
||||
f == Some(g)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use Box::into_non_null when stabilized
|
||||
pub(crate) fn box_into_non_null<T>(b: Box<T>) -> NonNull<T> {
|
||||
// SAFETY: `Box::into_raw` guarantees an non-null pointer
|
||||
unsafe { NonNull::new_unchecked(Box::into_raw(b)) }
|
||||
}
|
||||
|
||||
/// Replacement for the unstable `<*mut [T; N]>::as_mut_ptr` method, which avoids
|
||||
/// possibility of type getting lost from using e.g. `.cast()` to change array
|
||||
/// type to point to the data.
|
||||
pub(crate) const fn array_ptr_as_mut<T, const N: usize>(ptr: *mut [T; N]) -> *mut T {
|
||||
ptr.cast()
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
use crate::{ffi, internal::state::AttachGuard, Python};
|
||||
|
||||
static START: std::sync::Once = std::sync::Once::new();
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
pub(crate) fn initialize() {
|
||||
// Protect against race conditions when Python is not yet initialized and multiple threads
|
||||
// concurrently call 'initialize()'. Note that we do not protect against
|
||||
// concurrent initialization of the Python runtime by other users of the Python C API.
|
||||
START.call_once_force(|_| unsafe {
|
||||
// Use call_once_force because if initialization panics, it's okay to try again.
|
||||
if ffi::Py_IsInitialized() == 0 {
|
||||
ffi::Py_InitializeEx(0);
|
||||
|
||||
// Release the GIL.
|
||||
ffi::PyEval_SaveThread();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Executes the provided closure with an embedded Python interpreter.
|
||||
///
|
||||
/// This function initializes the Python interpreter, executes the provided closure, and then
|
||||
/// finalizes the Python interpreter.
|
||||
///
|
||||
/// After execution all Python resources are cleaned up, and no further Python APIs can be called.
|
||||
/// Because many Python modules implemented in C do not support multiple Python interpreters in a
|
||||
/// single process, it is not safe to call this function more than once. (Many such modules will not
|
||||
/// initialize correctly on the second run.)
|
||||
///
|
||||
/// # Panics
|
||||
/// - If the Python interpreter is already initialized before calling this function.
|
||||
///
|
||||
/// # Safety
|
||||
/// - This function should only ever be called once per process (usually as part of the `main`
|
||||
/// function). It is also not thread-safe.
|
||||
/// - No Python APIs can be used after this function has finished executing.
|
||||
/// - The return value of the closure must not contain any Python value, _including_ `PyResult`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// unsafe {
|
||||
/// pyo3::with_embedded_python_interpreter(|py| {
|
||||
/// if let Err(e) = py.run(c"print('Hello World')", None, None) {
|
||||
/// // We must make sure to not return a `PyErr`!
|
||||
/// e.print(py);
|
||||
/// }
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
pub unsafe fn with_embedded_python_interpreter<F, R>(f: F) -> R
|
||||
where
|
||||
F: for<'p> FnOnce(Python<'p>) -> R,
|
||||
{
|
||||
assert_eq!(
|
||||
unsafe { ffi::Py_IsInitialized() },
|
||||
0,
|
||||
"called `with_embedded_python_interpreter` but a Python interpreter is already running."
|
||||
);
|
||||
|
||||
unsafe { ffi::Py_InitializeEx(0) };
|
||||
|
||||
let result = {
|
||||
let guard = unsafe { AttachGuard::assume() };
|
||||
let py = guard.python();
|
||||
// Import the threading module - this ensures that it will associate this thread as the "main"
|
||||
// thread, which is important to avoid an `AssertionError` at finalization.
|
||||
py.import("threading").unwrap();
|
||||
|
||||
// Execute the closure.
|
||||
f(py)
|
||||
};
|
||||
|
||||
// Finalize the Python interpreter.
|
||||
unsafe { ffi::Py_Finalize() };
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// If PyO3 is currently running `Py_InitializeEx` inside the `Once` guard,
|
||||
/// block until it completes. Needed because `Py_InitializeEx` sets the
|
||||
/// `initialized` flag in the interpreter to true before it finishes all its
|
||||
/// steps (in particular, before it imports `site.py`).
|
||||
///
|
||||
/// This must only be called after `Py_IsInitialized()` has returned true.
|
||||
///
|
||||
/// If the `Once` was never started (e.g. the interpreter was initialized
|
||||
/// externally, not through PyO3), `call_once` runs the empty closure and
|
||||
/// returns — this is fine because `initialize()` checks
|
||||
/// `Py_IsInitialized()` inside its closure and skips `Py_InitializeEx` if
|
||||
/// the interpreter is already running. If the `Once` is currently in
|
||||
/// progress (another thread is inside `initialize()`), `call_once` blocks
|
||||
/// until it completes.
|
||||
pub(crate) fn wait_for_initialization() {
|
||||
// TODO: use START.wait_force() on MSRV 1.86
|
||||
// TODO: may not be needed on Python 3.15 (https://github.com/python/cpython/pull/146303)
|
||||
START.call_once(|| {
|
||||
assert_ne!(unsafe { crate::ffi::Py_IsInitialized() }, 0);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_initialized() {
|
||||
// Maybe auto-initialize the interpreter:
|
||||
// - If auto-initialize feature set and supported, try to initialize the interpreter.
|
||||
// - If the auto-initialize feature is set but unsupported, emit hard errors only when the
|
||||
// extension-module feature is not activated - extension modules don't care about
|
||||
// auto-initialize so this avoids breaking existing builds.
|
||||
// - Otherwise, just check the interpreter is initialized.
|
||||
#[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))]
|
||||
{
|
||||
initialize();
|
||||
}
|
||||
#[cfg(not(all(feature = "auto-initialize", not(any(PyPy, GraalPy)))))]
|
||||
{
|
||||
// This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need
|
||||
// to specify `--features auto-initialize` manually). Tests within the crate itself
|
||||
// all depend on the auto-initialize feature for conciseness but Cargo does not
|
||||
// provide a mechanism to specify required features for tests.
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
if option_env!("CARGO_PRIMARY_PACKAGE").is_some() {
|
||||
initialize();
|
||||
}
|
||||
|
||||
START.call_once_force(|_| unsafe {
|
||||
// Use call_once_force because if there is a panic because the interpreter is
|
||||
// not initialized, it's fine for the user to initialize the interpreter and
|
||||
// retry.
|
||||
assert_ne!(
|
||||
crate::ffi::Py_IsInitialized(),
|
||||
0,
|
||||
"The Python interpreter is not initialized and the `auto-initialize` \
|
||||
feature is not enabled.\n\n\
|
||||
Consider calling `Python::initialize()` before attempting \
|
||||
to use Python APIs."
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+531
@@ -0,0 +1,531 @@
|
||||
#![warn(
|
||||
clippy::alloc_instead_of_core,
|
||||
clippy::std_instead_of_alloc,
|
||||
clippy::std_instead_of_core
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(
|
||||
feature = "nightly",
|
||||
feature(auto_traits, negative_impls, iter_advance_by)
|
||||
)]
|
||||
#![cfg_attr(all(feature = "nightly", Py_GIL_DISABLED), feature(try_trait_v2))]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
// Deny some lints in doctests.
|
||||
// Use `#[allow(...)]` locally to override.
|
||||
#![doc(test(attr(
|
||||
deny(
|
||||
rust_2018_idioms,
|
||||
unused_lifetimes,
|
||||
rust_2021_prelude_collisions,
|
||||
warnings
|
||||
),
|
||||
allow(
|
||||
unused_imports, // to make imports already in the prelude explicit
|
||||
unused_variables,
|
||||
unused_assignments,
|
||||
unused_extern_crates,
|
||||
// FIXME https://github.com/rust-lang/rust/issues/121621#issuecomment-1965156376
|
||||
unknown_lints,
|
||||
non_local_definitions,
|
||||
)
|
||||
)))]
|
||||
|
||||
//! Rust bindings to the Python interpreter.
|
||||
//!
|
||||
//! PyO3 can be used to write native Python modules or run Python code and modules from Rust.
|
||||
//!
|
||||
//! See [the guide] for a detailed introduction.
|
||||
//!
|
||||
//! # PyO3's object types
|
||||
//!
|
||||
//! PyO3 has several core types that you should familiarize yourself with:
|
||||
//!
|
||||
//! ## The `Python<'py>` object, and the `'py` lifetime
|
||||
//!
|
||||
//! Holding the [global interpreter lock] (GIL) is modeled with the [`Python<'py>`](Python) token. Many
|
||||
//! Python APIs require that the GIL is held, and PyO3 uses this token as proof that these APIs
|
||||
//! can be called safely. It can be explicitly acquired and is also implicitly acquired by PyO3
|
||||
//! as it wraps Rust functions and structs into Python functions and objects.
|
||||
//!
|
||||
//! The [`Python<'py>`](Python) token's lifetime `'py` is common to many PyO3 APIs:
|
||||
//! - Types that also have the `'py` lifetime, such as the [`Bound<'py, T>`](Bound) smart pointer, are
|
||||
//! bound to the Python GIL and rely on this to offer their functionality. These types often
|
||||
//! have a [`.py()`](Bound::py) method to get the associated [`Python<'py>`](Python) token.
|
||||
//! - Functions which depend on the `'py` lifetime, such as [`PyList::new`](types::PyList::new),
|
||||
//! require a [`Python<'py>`](Python) token as an input. Sometimes the token is passed implicitly by
|
||||
//! taking a [`Bound<'py, T>`](Bound) or other type which is bound to the `'py` lifetime.
|
||||
//! - Traits which depend on the `'py` lifetime, such as [`FromPyObject<'py>`](FromPyObject), usually have
|
||||
//! inputs or outputs which depend on the lifetime. Adding the lifetime to the trait allows
|
||||
//! these inputs and outputs to express their binding to the GIL in the Rust type system.
|
||||
//!
|
||||
//! ## Python object smart pointers
|
||||
//!
|
||||
//! PyO3 has two core smart pointers to refer to Python objects, [`Py<T>`](Py) and its GIL-bound
|
||||
//! form [`Bound<'py, T>`](Bound) which carries the `'py` lifetime. (There is also
|
||||
//! [`Borrowed<'a, 'py, T>`](instance::Borrowed), but it is used much more rarely).
|
||||
//!
|
||||
//! The type parameter `T` in these smart pointers can be filled by:
|
||||
//! - [`PyAny`], e.g. `Py<PyAny>` or `Bound<'py, PyAny>`, where the Python object type is not
|
||||
//! known.
|
||||
//! - Concrete Python types like [`PyList`](types::PyList) or [`PyTuple`](types::PyTuple).
|
||||
//! - Rust types which are exposed to Python using the [`#[pyclass]`](macro@pyclass) macro.
|
||||
//!
|
||||
//! See the [guide][types] for an explanation of the different Python object types.
|
||||
//!
|
||||
//! ## PyErr
|
||||
//!
|
||||
//! The vast majority of operations in this library will return [`PyResult<...>`](PyResult).
|
||||
//! This is an alias for the type `Result<..., PyErr>`.
|
||||
//!
|
||||
//! A `PyErr` represents a Python exception. A `PyErr` returned to Python code will be raised as a
|
||||
//! Python exception. Errors from `PyO3` itself are also exposed as Python exceptions.
|
||||
//!
|
||||
//! # Feature flags
|
||||
//!
|
||||
//! PyO3 uses [feature flags] to enable you to opt-in to additional functionality. For a detailed
|
||||
//! description, see the [Features chapter of the guide].
|
||||
//!
|
||||
//! ## Default feature flags
|
||||
//!
|
||||
//! The following features are turned on by default:
|
||||
//! - `macros`: Enables various macros, including all the attribute macros.
|
||||
//!
|
||||
//! ## Optional feature flags
|
||||
//!
|
||||
//! The following features customize PyO3's behavior:
|
||||
//!
|
||||
//! - `abi3`: Restricts PyO3's API to a subset of the full Python API which is guaranteed by
|
||||
//! [PEP 384] to be forward-compatible with future Python versions.
|
||||
//! - `auto-initialize`: Changes [`Python::attach`] to automatically initialize the Python
|
||||
//! interpreter if needed.
|
||||
//! - `multiple-pymethods`: Enables the use of multiple [`#[pymethods]`](macro@crate::pymethods)
|
||||
//! blocks per [`#[pyclass]`](macro@crate::pyclass). This adds a dependency on the [inventory]
|
||||
//! crate, which is not supported on all platforms.
|
||||
//!
|
||||
//! The following features enable interactions with other crates in the Rust ecosystem:
|
||||
//! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`].
|
||||
//! - [`chrono`]: Enables a conversion from [chrono]'s structures to the equivalent Python ones.
|
||||
//! - [`chrono-tz`]: Enables a conversion from [chrono-tz]'s `Tz` enum. Requires Python 3.9+.
|
||||
//! - [`either`]: Enables conversions between Python objects and [either]'s [`Either`] type.
|
||||
//! - [`eyre`]: Enables a conversion from [eyre]’s [`Report`] type to [`PyErr`].
|
||||
//! - [`hashbrown`]: Enables conversions between Python objects and [hashbrown]'s [`HashMap`] and
|
||||
//! [`HashSet`] types.
|
||||
//! - [`indexmap`][indexmap_feature]: Enables conversions between Python dictionary and [indexmap]'s [`IndexMap`].
|
||||
//! - [`num-bigint`]: Enables conversions between Python objects and [num-bigint]'s [`BigInt`] and
|
||||
//! [`BigUint`] types.
|
||||
//! - [`num-complex`]: Enables conversions between Python objects and [num-complex]'s [`Complex`]
|
||||
//! type.
|
||||
//! - [`num-rational`]: Enables conversions between Python's fractions.Fraction and [num-rational]'s types
|
||||
//! - [`ordered-float`]: Enables conversions between Python's float and [ordered-float]'s types
|
||||
//! - [`rust_decimal`]: Enables conversions between Python's decimal.Decimal and [rust_decimal]'s
|
||||
//! [`Decimal`] type.
|
||||
//! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for
|
||||
//! [`Py`]`<T>` for all `T` that implement [`Serialize`] and [`Deserialize`].
|
||||
//! - [`smallvec`][smallvec]: Enables conversions between Python list and [smallvec]'s [`SmallVec`].
|
||||
//!
|
||||
//! ## Unstable features
|
||||
//!
|
||||
//! - `nightly`: Uses `#![feature(auto_traits, negative_impls)]` to define [`Ungil`] as an auto trait.
|
||||
//
|
||||
//! ## `rustc` environment flags
|
||||
//! - `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`, `Py_3_14`: Marks code that is
|
||||
//! only enabled when compiling for a given minimum Python version.
|
||||
//! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled.
|
||||
//! - `Py_GIL_DISABLED`: Marks code that runs only in the free-threaded build of CPython.
|
||||
//! - `PyPy` - Marks code enabled when compiling for PyPy.
|
||||
//! - `GraalPy` - Marks code enabled when compiling for GraalPy.
|
||||
//!
|
||||
//! Additionally, you can query for the values `Py_DEBUG`, `Py_REF_DEBUG`,
|
||||
//! `Py_TRACE_REFS`, and `COUNT_ALLOCS` from `py_sys_config` to query for the
|
||||
//! corresponding C build-time defines. For example, to conditionally define
|
||||
//! debug code using `Py_DEBUG`, you could do:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! #[cfg(py_sys_config = "Py_DEBUG")]
|
||||
//! println!("only runs if python was compiled with Py_DEBUG")
|
||||
//! ```
|
||||
//! To use these attributes, add [`pyo3-build-config`] as a build dependency in
|
||||
//! your `Cargo.toml` and call `pyo3_build_config::use_pyo3_cfgs()` in a
|
||||
//! `build.rs` file.
|
||||
//!
|
||||
//! # Minimum supported Rust and Python versions
|
||||
//!
|
||||
//! Requires Rust 1.83 or greater.
|
||||
//!
|
||||
//! PyO3 supports the following Python distributions:
|
||||
//! - CPython 3.8 or greater
|
||||
//! - PyPy 7.3 (Python 3.11+)
|
||||
//! - GraalPy 24.0 or greater (Python 3.10+)
|
||||
//!
|
||||
//! # Example: Building a native Python module
|
||||
//!
|
||||
//! PyO3 can be used to generate a native Python module. The easiest way to try this out for the
|
||||
//! first time is to use [`maturin`]. `maturin` is a tool for building and publishing Rust-based
|
||||
//! Python packages with minimal configuration. The following steps set up some files for an example
|
||||
//! Python module, install `maturin`, and then show how to build and import the Python module.
|
||||
//!
|
||||
//! First, create a new folder (let's call it `string_sum`) containing the following two files:
|
||||
//!
|
||||
//! **`Cargo.toml`**
|
||||
//!
|
||||
//! ```toml
|
||||
//! [package]
|
||||
//! name = "string-sum"
|
||||
//! version = "0.1.0"
|
||||
//! edition = "2021"
|
||||
//!
|
||||
//! [lib]
|
||||
//! name = "string_sum"
|
||||
//! # "cdylib" is necessary to produce a shared library for Python to import from.
|
||||
//! #
|
||||
//! # Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
|
||||
//! # to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
|
||||
//! # crate-type = ["cdylib", "rlib"]
|
||||
//! crate-type = ["cdylib"]
|
||||
//!
|
||||
//! [dependencies]
|
||||
#![doc = concat!("pyo3 = \"", env!("CARGO_PKG_VERSION"), "\"")]
|
||||
//! ```
|
||||
//!
|
||||
//! **`src/lib.rs`**
|
||||
//! ```rust,no_run
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! /// Formats the sum of two numbers as string.
|
||||
//! #[pyfunction]
|
||||
//! fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
|
||||
//! Ok((a + b).to_string())
|
||||
//! }
|
||||
//!
|
||||
//! /// A Python module implemented in Rust.
|
||||
//! #[pymodule]
|
||||
//! fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
//! m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! With those two files in place, now `maturin` needs to be installed. This can be done using
|
||||
//! Python's package manager `pip`. First, load up a new Python `virtualenv`, and install `maturin`
|
||||
//! into it:
|
||||
//! ```bash
|
||||
//! $ cd string_sum
|
||||
//! $ python -m venv .env
|
||||
//! $ source .env/bin/activate
|
||||
//! $ pip install maturin
|
||||
//! ```
|
||||
//!
|
||||
//! Now build and execute the module:
|
||||
//! ```bash
|
||||
//! $ maturin develop
|
||||
//! # lots of progress output as maturin runs the compilation...
|
||||
//! $ python
|
||||
//! >>> import string_sum
|
||||
//! >>> string_sum.sum_as_string(5, 20)
|
||||
//! '25'
|
||||
//! ```
|
||||
//!
|
||||
//! As well as with `maturin`, it is possible to build using [setuptools-rust] or
|
||||
//! [manually][manual_builds]. Both offer more flexibility than `maturin` but require further
|
||||
//! configuration.
|
||||
//!
|
||||
//! # Example: Using Python from Rust
|
||||
//!
|
||||
//! To embed Python into a Rust binary, you need to ensure that your Python installation contains a
|
||||
//! shared library. The following steps demonstrate how to ensure this (for Ubuntu), and then give
|
||||
//! some example code which runs an embedded Python interpreter.
|
||||
//!
|
||||
//! To install the Python shared library on Ubuntu:
|
||||
//! ```bash
|
||||
//! sudo apt install python3-dev
|
||||
//! ```
|
||||
//!
|
||||
//! Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like this:
|
||||
//! ```toml
|
||||
//! [dependencies.pyo3]
|
||||
#![doc = concat!("version = \"", env!("CARGO_PKG_VERSION"), "\"")]
|
||||
//! # this is necessary to automatically initialize the Python interpreter
|
||||
//! features = ["auto-initialize"]
|
||||
//! ```
|
||||
//!
|
||||
//! Example program displaying the value of `sys.version` and the current user name:
|
||||
//! ```rust
|
||||
//! use pyo3::prelude::*;
|
||||
//! use pyo3::types::IntoPyDict;
|
||||
//!
|
||||
//! fn main() -> PyResult<()> {
|
||||
//! Python::attach(|py| {
|
||||
//! let sys = py.import("sys")?;
|
||||
//! let version: String = sys.getattr("version")?.extract()?;
|
||||
//!
|
||||
//! let locals = [("os", py.import("os")?)].into_py_dict(py)?;
|
||||
//! let code = c"os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'";
|
||||
//! let user: String = py.eval(code, None, Some(&locals))?.extract()?;
|
||||
//!
|
||||
//! println!("Hello {}, I'm Python {}", user, version);
|
||||
//! Ok(())
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The guide has [a section][calling_rust] with lots of examples about this topic.
|
||||
//!
|
||||
//! # Other Examples
|
||||
//!
|
||||
//! The PyO3 [README](https://github.com/PyO3/pyo3#readme) contains quick-start examples for both
|
||||
//! using [Rust from Python] and [Python from Rust].
|
||||
//!
|
||||
//! The PyO3 repository's [examples subdirectory]
|
||||
//! contains some basic packages to demonstrate usage of PyO3.
|
||||
//!
|
||||
//! There are many projects using PyO3 - see a list of some at
|
||||
//! <https://github.com/PyO3/pyo3#examples>.
|
||||
//!
|
||||
//! [anyhow]: https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications."
|
||||
//! [anyhow_error]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type"
|
||||
//! [`anyhow`]: ./anyhow/index.html "Documentation about the `anyhow` feature."
|
||||
//! [inventory]: https://docs.rs/inventory
|
||||
//! [`HashMap`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashMap.html
|
||||
//! [`HashSet`]: https://docs.rs/hashbrown/latest/hashbrown/struct.HashSet.html
|
||||
//! [`SmallVec`]: https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html
|
||||
//! [`Uuid`]: https://docs.rs/uuid/latest/uuid/struct.Uuid.html
|
||||
//! [`IndexMap`]: https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html
|
||||
//! [`BigInt`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html
|
||||
//! [`BigUint`]: https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html
|
||||
//! [`Complex`]: https://docs.rs/num-complex/latest/num_complex/struct.Complex.html
|
||||
//! [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html
|
||||
//! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
|
||||
//! [chrono]: https://docs.rs/chrono/ "Date and Time for Rust."
|
||||
//! [chrono-tz]: https://docs.rs/chrono-tz/ "TimeZone implementations for chrono from the IANA database."
|
||||
//! [`chrono`]: ./chrono/index.html "Documentation about the `chrono` feature."
|
||||
//! [`chrono-tz`]: ./chrono-tz/index.html "Documentation about the `chrono-tz` feature."
|
||||
//! [either]: https://docs.rs/either/ "A type that represents one of two alternatives."
|
||||
//! [`either`]: ./either/index.html "Documentation about the `either` feature."
|
||||
//! [`Either`]: https://docs.rs/either/latest/either/enum.Either.html
|
||||
//! [eyre]: https://docs.rs/eyre/ "A library for easy idiomatic error handling and reporting in Rust applications."
|
||||
//! [`Report`]: https://docs.rs/eyre/latest/eyre/struct.Report.html
|
||||
//! [`eyre`]: ./eyre/index.html "Documentation about the `eyre` feature."
|
||||
//! [`hashbrown`]: ./hashbrown/index.html "Documentation about the `hashbrown` feature."
|
||||
//! [indexmap_feature]: ./indexmap/index.html "Documentation about the `indexmap` feature."
|
||||
//! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
|
||||
//! [`num-bigint`]: ./num_bigint/index.html "Documentation about the `num-bigint` feature."
|
||||
//! [`num-complex`]: ./num_complex/index.html "Documentation about the `num-complex` feature."
|
||||
//! [`num-rational`]: ./num_rational/index.html "Documentation about the `num-rational` feature."
|
||||
//! [`ordered-float`]: ./ordered_float/index.html "Documentation about the `ordered-float` feature."
|
||||
//! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config
|
||||
//! [rust_decimal]: https://docs.rs/rust_decimal
|
||||
//! [`rust_decimal`]: ./rust_decimal/index.html "Documentation about the `rust_decimal` feature."
|
||||
//! [`Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
|
||||
//! [`serde`]: <./serde/index.html> "Documentation about the `serde` feature."
|
||||
#![doc = concat!("[calling_rust]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/python-from-rust.html \"Calling Python from Rust - PyO3 user guide\"")]
|
||||
//! [examples subdirectory]: https://github.com/PyO3/pyo3/tree/main/examples
|
||||
//! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book"
|
||||
//! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock
|
||||
//! [hashbrown]: https://docs.rs/hashbrown
|
||||
//! [smallvec]: https://docs.rs/smallvec
|
||||
//! [uuid]: https://docs.rs/uuid
|
||||
//! [indexmap]: https://docs.rs/indexmap
|
||||
#![doc = concat!("[manual_builds]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution.html#manual-builds \"Manual builds - Building and Distribution - PyO3 user guide\"")]
|
||||
//! [num-bigint]: https://docs.rs/num-bigint
|
||||
//! [num-complex]: https://docs.rs/num-complex
|
||||
//! [num-rational]: https://docs.rs/num-rational
|
||||
//! [ordered-float]: https://docs.rs/ordered-float
|
||||
//! [serde]: https://docs.rs/serde
|
||||
//! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions"
|
||||
//! [the guide]: https://pyo3.rs "PyO3 user guide"
|
||||
#![doc = concat!("[types]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/types.html \"GIL lifetimes, mutability and Python object types\"")]
|
||||
//! [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI"
|
||||
//! [Python from Rust]: https://github.com/PyO3/pyo3#using-python-from-rust
|
||||
//! [Rust from Python]: https://github.com/PyO3/pyo3#using-rust-from-python
|
||||
#![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")]
|
||||
//! [`Ungil`]: crate::marker::Ungil
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub use crate::class::*;
|
||||
pub use crate::conversion::{FromPyObject, IntoPyObject, IntoPyObjectExt};
|
||||
pub use crate::err::{CastError, CastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr};
|
||||
pub use crate::instance::{Borrowed, Bound, BoundObject, Py};
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
pub use crate::interpreter_lifecycle::with_embedded_python_interpreter;
|
||||
pub use crate::marker::Python;
|
||||
pub use crate::pycell::{PyRef, PyRefMut};
|
||||
pub use crate::pyclass::{PyClass, PyClassGuard, PyClassGuardMut};
|
||||
pub use crate::pyclass_init::PyClassInitializer;
|
||||
pub use crate::type_object::{PyTypeCheck, PyTypeInfo};
|
||||
pub use crate::types::PyAny;
|
||||
pub use crate::version::PythonVersionInfo;
|
||||
|
||||
pub(crate) mod ffi_ptr_ext;
|
||||
pub(crate) mod py_result_ext;
|
||||
pub(crate) mod sealed;
|
||||
|
||||
mod platform;
|
||||
|
||||
/// Old module which contained some implementation details of the `#[pyproto]` module.
|
||||
///
|
||||
/// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::CompareOp` instead
|
||||
/// of `use pyo3::class::basic::CompareOp`.
|
||||
///
|
||||
/// For compatibility reasons this has not yet been removed, however will be done so
|
||||
/// once <https://github.com/rust-lang/rust/issues/30827> is resolved.
|
||||
pub mod class {
|
||||
pub use self::gc::{PyTraverseError, PyVisit};
|
||||
|
||||
/// Old module which contained some implementation details of the `#[pyproto]` module.
|
||||
///
|
||||
/// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::CompareOp` instead
|
||||
/// of `use pyo3::class::basic::CompareOp`.
|
||||
///
|
||||
/// For compatibility reasons this has not yet been removed, however will be done so
|
||||
/// once <https://github.com/rust-lang/rust/issues/30827> is resolved.
|
||||
pub mod basic {
|
||||
pub use crate::pyclass::CompareOp;
|
||||
}
|
||||
|
||||
/// Old module which contained some implementation details of the `#[pyproto]` module.
|
||||
///
|
||||
/// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::PyTraverseError` instead
|
||||
/// of `use pyo3::class::gc::PyTraverseError`.
|
||||
///
|
||||
/// For compatibility reasons this has not yet been removed, however will be done so
|
||||
/// once <https://github.com/rust-lang/rust/issues/30827> is resolved.
|
||||
pub mod gc {
|
||||
pub use crate::pyclass::{PyTraverseError, PyVisit};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "macros", feature = "multiple-pymethods"))]
|
||||
#[doc(hidden)]
|
||||
pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `multiple-pymethods`.
|
||||
|
||||
/// Tests and helpers which reside inside PyO3's main library. Declared first so that macros
|
||||
/// are available in unit tests.
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[macro_use]
|
||||
mod internal;
|
||||
#[macro_use]
|
||||
mod internal_tricks;
|
||||
|
||||
// Macro dependencies, also contains macros exported for use across the codebase and
|
||||
// in expanded macros.
|
||||
#[doc(hidden)]
|
||||
pub mod impl_;
|
||||
|
||||
pub mod buffer;
|
||||
pub mod call;
|
||||
pub mod conversion;
|
||||
mod conversions;
|
||||
#[cfg(feature = "experimental-async")]
|
||||
pub mod coroutine;
|
||||
mod err;
|
||||
pub mod exceptions;
|
||||
pub mod ffi;
|
||||
pub(crate) mod fmt;
|
||||
mod instance;
|
||||
mod interpreter_lifecycle;
|
||||
pub mod marker;
|
||||
pub mod marshal;
|
||||
#[macro_use]
|
||||
pub mod sync;
|
||||
pub(crate) mod byteswriter;
|
||||
pub mod panic;
|
||||
pub mod pybacked;
|
||||
pub mod pycell;
|
||||
pub mod pyclass;
|
||||
pub mod pyclass_init;
|
||||
|
||||
pub mod type_object;
|
||||
pub mod types;
|
||||
mod version;
|
||||
|
||||
#[allow(
|
||||
unused_imports,
|
||||
reason = "with no features enabled this module has no public exports"
|
||||
)]
|
||||
pub use crate::conversions::*;
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
pub use pyo3_macros::{
|
||||
pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject, IntoPyObjectRef,
|
||||
};
|
||||
|
||||
/// A proc macro used to expose Rust structs and fieldless enums as Python objects.
|
||||
///
|
||||
#[doc = include_str!("../guide/pyclass-parameters.md")]
|
||||
///
|
||||
/// For more on creating Python classes,
|
||||
/// see the [class section of the guide][1].
|
||||
///
|
||||
#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html")]
|
||||
#[cfg(feature = "macros")]
|
||||
pub use pyo3_macros::pyclass;
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
pub mod inspect;
|
||||
|
||||
// Putting the declaration of prelude at the end seems to help encourage rustc and rustdoc to prefer using
|
||||
// other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`).
|
||||
pub mod prelude;
|
||||
|
||||
/// Test readme and user guide
|
||||
#[cfg(doctest)]
|
||||
pub mod doc_test {
|
||||
macro_rules! doctests {
|
||||
($($path:expr => $mod:ident),* $(,)?) => {
|
||||
$(
|
||||
#[doc = include_str!(concat!("../", $path))]
|
||||
mod $mod{}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
doctests! {
|
||||
"README.md" => readme_md,
|
||||
"guide/src/advanced.md" => guide_advanced_md,
|
||||
"guide/src/async-await.md" => guide_async_await_md,
|
||||
"guide/src/building-and-distribution.md" => guide_building_and_distribution_md,
|
||||
"guide/src/building-and-distribution/multiple-python-versions.md" => guide_bnd_multiple_python_versions_md,
|
||||
"guide/src/class.md" => guide_class_md,
|
||||
"guide/src/class/call.md" => guide_class_call,
|
||||
"guide/src/class/object.md" => guide_class_object,
|
||||
"guide/src/class/numeric.md" => guide_class_numeric,
|
||||
"guide/src/class/protocols.md" => guide_class_protocols_md,
|
||||
"guide/src/class/thread-safety.md" => guide_class_thread_safety_md,
|
||||
"guide/src/conversions.md" => guide_conversions_md,
|
||||
"guide/src/conversions/tables.md" => guide_conversions_tables_md,
|
||||
"guide/src/conversions/traits.md" => guide_conversions_traits_md,
|
||||
"guide/src/debugging.md" => guide_debugging_md,
|
||||
|
||||
// deliberate choice not to test guide/ecosystem because those pages depend on external
|
||||
// crates such as pyo3_asyncio.
|
||||
|
||||
"guide/src/exception.md" => guide_exception_md,
|
||||
"guide/src/faq.md" => guide_faq_md,
|
||||
"guide/src/features.md" => guide_features_md,
|
||||
"guide/src/free-threading.md" => guide_free_threading_md,
|
||||
"guide/src/function.md" => guide_function_md,
|
||||
"guide/src/function/error-handling.md" => guide_function_error_handling_md,
|
||||
"guide/src/function/signature.md" => guide_function_signature_md,
|
||||
"guide/src/migration.md" => guide_migration_md,
|
||||
"guide/src/module.md" => guide_module_md,
|
||||
"guide/src/parallelism.md" => guide_parallelism_md,
|
||||
"guide/src/performance.md" => guide_performance_md,
|
||||
"guide/src/python-from-rust.md" => guide_python_from_rust_md,
|
||||
"guide/src/python-from-rust/calling-existing-code.md" => guide_pfr_calling_existing_code_md,
|
||||
"guide/src/python-from-rust/function-calls.md" => guide_pfr_function_calls_md,
|
||||
"guide/src/python-typing-hints.md" => guide_python_typing_hints_md,
|
||||
"guide/src/rust-from-python.md" => guide_rust_from_python_md,
|
||||
"guide/src/trait-bounds.md" => guide_trait_bounds_md,
|
||||
"guide/src/types.md" => guide_types_md,
|
||||
}
|
||||
}
|
||||
Vendored
+230
@@ -0,0 +1,230 @@
|
||||
/// A convenient macro to execute a Python code snippet, with some local variables set.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This macro internally calls [`Python::run`](crate::Python::run) and panics
|
||||
/// if it returns `Err`, after printing the error to stdout.
|
||||
///
|
||||
/// If you need to handle failures, please use [`Python::run`](crate::marker::Python::run) instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use pyo3::{prelude::*, py_run, types::PyList};
|
||||
///
|
||||
/// # fn main() -> PyResult<()> {
|
||||
/// Python::attach(|py| {
|
||||
/// let list = PyList::new(py, &[1, 2, 3])?;
|
||||
/// py_run!(py, list, "assert list == [1, 2, 3]");
|
||||
/// # Ok(())
|
||||
/// })
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can use this macro to test pyfunctions or pyclasses quickly.
|
||||
///
|
||||
/// ```
|
||||
/// use pyo3::{prelude::*, py_run};
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// #[derive(Debug)]
|
||||
/// struct Time {
|
||||
/// hour: u32,
|
||||
/// minute: u32,
|
||||
/// second: u32,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Time {
|
||||
/// fn repl_japanese(&self) -> String {
|
||||
/// format!("{}時{}分{}秒", self.hour, self.minute, self.second)
|
||||
/// }
|
||||
/// #[getter]
|
||||
/// fn hour(&self) -> u32 {
|
||||
/// self.hour
|
||||
/// }
|
||||
/// fn as_tuple(&self) -> (u32, u32, u32) {
|
||||
/// (self.hour, self.minute, self.second)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Python::attach(|py| {
|
||||
/// let time = Py::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap();
|
||||
/// let time_as_tuple = (8, 43, 16);
|
||||
/// py_run!(py, time time_as_tuple, r#"
|
||||
/// assert time.hour == 8
|
||||
/// assert time.repl_japanese() == "8時43分16秒"
|
||||
/// assert time.as_tuple() == time_as_tuple
|
||||
/// "#);
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// If you need to prepare the `locals` dict by yourself, you can pass it as `*locals`.
|
||||
///
|
||||
/// ```
|
||||
/// use pyo3::prelude::*;
|
||||
/// use pyo3::types::IntoPyDict;
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// struct MyClass;
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl MyClass {
|
||||
/// #[new]
|
||||
/// fn new() -> Self {
|
||||
/// MyClass {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn main() -> PyResult<()> {
|
||||
/// Python::attach(|py| {
|
||||
/// let locals = [("C", py.get_type::<MyClass>())].into_py_dict(py)?;
|
||||
/// pyo3::py_run!(py, *locals, "c = C()");
|
||||
/// # Ok(())
|
||||
/// })
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! py_run {
|
||||
// unindent the code at compile time
|
||||
($py:expr, $($val:ident)+, $code:literal) => {{
|
||||
$crate::py_run_impl!($py, $($val)+, $crate::impl_::unindent::unindent!($code))
|
||||
}};
|
||||
($py:expr, *$dict:expr, $code:literal) => {{
|
||||
$crate::py_run_impl!($py, *$dict, $crate::impl_::unindent::unindent!($code))
|
||||
}};
|
||||
// unindent the code at runtime
|
||||
($py:expr, $($val:ident)+, $code:expr) => {{
|
||||
$crate::py_run_impl!($py, $($val)+, $crate::impl_::unindent::unindent($code))
|
||||
}};
|
||||
($py:expr, *$dict:expr, $code:expr) => {{
|
||||
$crate::py_run_impl!($py, *$dict, $crate::impl_::unindent::unindent($code))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Internal implementation of the `py_run!` macro.
|
||||
///
|
||||
/// FIXME: this currently unconditionally allocates a `CString`. We should consider making this not so:
|
||||
/// - Maybe require users to pass `&CStr` / `CString`?
|
||||
/// - Maybe adjust the `unindent` code to produce `&Cstr` / `Cstring`?
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! py_run_impl {
|
||||
($py:expr, $($val:ident)+, $code:expr) => {{
|
||||
use $crate::types::IntoPyDict;
|
||||
use $crate::conversion::IntoPyObject;
|
||||
use $crate::BoundObject;
|
||||
let d = [$((stringify!($val), (&$val).into_pyobject($py).unwrap().into_any().into_bound()),)+].into_py_dict($py).unwrap();
|
||||
$crate::py_run_impl!($py, *d, $code)
|
||||
}};
|
||||
($py:expr, *$dict:expr, $code:expr) => {{
|
||||
use ::core::option::Option::*;
|
||||
if let ::core::result::Result::Err(e) = $py.run(&::std::ffi::CString::new($code).unwrap(), None, Some(&$dict)) {
|
||||
e.print($py);
|
||||
// So when this c api function the last line called printed the error to stderr,
|
||||
// the output is only written into a buffer which is never flushed because we
|
||||
// panic before flushing. This is where this hack comes into place
|
||||
$py.run(c"import sys; sys.stderr.flush()", None, None)
|
||||
.unwrap();
|
||||
::core::panic!("{}", $code)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
/// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction).
|
||||
///
|
||||
/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to
|
||||
/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more
|
||||
/// information.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use pyo3::prelude::*;
|
||||
/// #[pyfunction]
|
||||
/// fn add(x: i32, y: i32) -> i32 {
|
||||
/// x + y
|
||||
/// }
|
||||
///
|
||||
/// # fn main() -> PyResult<()> {
|
||||
/// Python::attach(|py| {
|
||||
/// let example = PyModule::from_code(
|
||||
/// py,
|
||||
/// c"from collections.abc import Callable
|
||||
/// def add_two_and_three(add: 'Callable[[int, int], int]') -> int:
|
||||
/// return add(2, 3)",
|
||||
/// c"example.py",
|
||||
/// c"",
|
||||
/// )?;
|
||||
///
|
||||
/// // `add_two_and_three` is a Python function defined in the code above
|
||||
/// let add_two_and_three = example.getattr("add_two_and_three")?;
|
||||
///
|
||||
/// // `add` is a Python function defined by the `#[pyfunction]` macro
|
||||
/// let add = wrap_pyfunction!(add, py)?;
|
||||
///
|
||||
/// let result = add_two_and_three.call1((add,))?.extract::<i32>()?;
|
||||
///
|
||||
/// assert_eq!(result, 5);
|
||||
///
|
||||
/// # Ok(())
|
||||
/// })
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! wrap_pyfunction {
|
||||
($function:path) => {
|
||||
&|py_or_module| {
|
||||
use $function as wrapped_pyfunction;
|
||||
$crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction(
|
||||
py_or_module,
|
||||
&wrapped_pyfunction::_PYO3_DEF,
|
||||
)
|
||||
}
|
||||
};
|
||||
($function:path, $py_or_module:expr) => {{
|
||||
use $function as wrapped_pyfunction;
|
||||
$crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction(
|
||||
$py_or_module,
|
||||
&wrapped_pyfunction::_PYO3_DEF,
|
||||
)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Returns a function that takes a [`Python`](crate::Python) instance and returns a
|
||||
/// Python module.
|
||||
///
|
||||
/// Use this together with [`#[pymodule]`](crate::pymodule) and
|
||||
/// [`PyModule::add_wrapped`](crate::types::PyModuleMethods::add_wrapped).
|
||||
#[macro_export]
|
||||
macro_rules! wrap_pymodule {
|
||||
($module:path) => {
|
||||
&|py| {
|
||||
use $module as wrapped_pymodule;
|
||||
wrapped_pymodule::_PYO3_DEF
|
||||
.make_module(py)
|
||||
.expect("failed to wrap pymodule")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Add the module to the initialization table in order to make embedded Python code to use it.
|
||||
/// Module name is the argument.
|
||||
///
|
||||
/// Use it before [`Python::initialize`](crate::marker::Python::initialize) and
|
||||
/// leave feature `auto-initialize` off
|
||||
#[cfg(not(any(PyPy, GraalPy, all(Py_LIMITED_API, Py_GIL_DISABLED))))]
|
||||
#[macro_export]
|
||||
macro_rules! append_to_inittab {
|
||||
($module:ident) => {
|
||||
unsafe {
|
||||
if $crate::ffi::Py_IsInitialized() != 0 {
|
||||
::core::panic!(
|
||||
"called `append_to_inittab` but a Python interpreter is already running."
|
||||
);
|
||||
}
|
||||
$crate::ffi::PyImport_AppendInittab(
|
||||
$module::__PYO3_NAME.as_ptr(),
|
||||
::core::option::Option::Some($module::__pyo3_init),
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
Vendored
+1037
File diff suppressed because it is too large.
Load diff
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(not(Py_LIMITED_API))]
|
||||
|
||||
//! Support for the Python `marshal` format.
|
||||
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
use crate::py_result_ext::PyResultExt;
|
||||
use crate::types::{PyAny, PyBytes};
|
||||
use crate::{ffi, Bound};
|
||||
use crate::{PyResult, Python};
|
||||
use core::ffi::c_int;
|
||||
|
||||
/// The current version of the marshal binary format.
|
||||
pub const VERSION: i32 = 4;
|
||||
|
||||
/// Serialize an object to bytes using the Python built-in marshal module.
|
||||
///
|
||||
/// The built-in marshalling only supports a limited range of objects.
|
||||
/// The exact types supported depend on the version argument.
|
||||
/// The [`VERSION`] constant holds the highest version currently supported.
|
||||
///
|
||||
/// See the [Python documentation](https://docs.python.org/3/library/marshal.html) for more details.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use pyo3::{marshal, types::PyDict, prelude::PyDictMethods};
|
||||
/// # pyo3::Python::attach(|py| {
|
||||
/// let dict = PyDict::new(py);
|
||||
/// dict.set_item("aap", "noot").unwrap();
|
||||
/// dict.set_item("mies", "wim").unwrap();
|
||||
/// dict.set_item("zus", "jet").unwrap();
|
||||
///
|
||||
/// let bytes = marshal::dumps(&dict, marshal::VERSION);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn dumps<'py>(object: &Bound<'py, PyAny>, version: i32) -> PyResult<Bound<'py, PyBytes>> {
|
||||
unsafe {
|
||||
ffi::PyMarshal_WriteObjectToString(object.as_ptr(), version as c_int)
|
||||
.assume_owned_or_err(object.py())
|
||||
.cast_into_unchecked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserialize an object from bytes using the Python built-in marshal module.
|
||||
pub fn loads<'py, B>(py: Python<'py>, data: &B) -> PyResult<Bound<'py, PyAny>>
|
||||
where
|
||||
B: AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
let data = data.as_ref();
|
||||
unsafe {
|
||||
ffi::PyMarshal_ReadObjectFromString(data.as_ptr().cast(), data.len() as isize)
|
||||
.assume_owned_or_err(py)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{bytes::PyBytesMethods, dict::PyDictMethods, PyAnyMethods, PyDict};
|
||||
|
||||
#[test]
|
||||
fn marshal_roundtrip() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("aap", "noot").unwrap();
|
||||
dict.set_item("mies", "wim").unwrap();
|
||||
dict.set_item("zus", "jet").unwrap();
|
||||
|
||||
let pybytes = dumps(&dict, VERSION).expect("marshalling failed");
|
||||
let deserialized = loads(py, pybytes.as_bytes()).expect("unmarshalling failed");
|
||||
|
||||
assert!(dict.eq(&deserialized).unwrap());
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
//! Helper to convert Rust panics to Python exceptions.
|
||||
use crate::exceptions::PyBaseException;
|
||||
use crate::PyErr;
|
||||
use core::any::Any;
|
||||
|
||||
pyo3_exception!(
|
||||
"
|
||||
The exception raised when Rust code called from Python panics.
|
||||
|
||||
Like SystemExit, this exception is derived from BaseException so that
|
||||
it will typically propagate all the way through the stack and cause the
|
||||
Python interpreter to exit.
|
||||
",
|
||||
PanicException,
|
||||
PyBaseException
|
||||
);
|
||||
|
||||
impl PanicException {
|
||||
/// Creates a new PanicException from a panic payload.
|
||||
///
|
||||
/// Attempts to format the error in the same way panic does.
|
||||
#[cold]
|
||||
pub(crate) fn from_panic_payload(payload: Box<dyn Any + Send + 'static>) -> PyErr {
|
||||
if let Some(string) = payload.downcast_ref::<String>() {
|
||||
Self::new_err((string.clone(),))
|
||||
} else if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
Self::new_err((s.to_string(),))
|
||||
} else {
|
||||
Self::new_err(("panic from Rust code",))
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
//! This module is to support platform compatibility with `no_std` environments.
|
||||
#![allow(unused_imports)]
|
||||
|
||||
#[cfg(feature = "hashbrown")]
|
||||
pub use hashbrown::{HashMap, HashSet};
|
||||
|
||||
// TODO conditionally import these based on "std" feature
|
||||
#[cfg(not(feature = "hashbrown"))]
|
||||
pub use std::collections::{HashMap, HashSet};
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
//! PyO3's prelude.
|
||||
//!
|
||||
//! The purpose of this module is to alleviate imports of many commonly used items of the PyO3 crate
|
||||
//! by adding a glob import to the top of pyo3 heavy modules:
|
||||
//!
|
||||
//! ```
|
||||
//! # #![allow(unused_imports)]
|
||||
//! use pyo3::prelude::*;
|
||||
//! ```
|
||||
|
||||
pub use crate::conversion::{FromPyObject, FromPyObjectOwned, IntoPyObject};
|
||||
pub use crate::err::{PyErr, PyResult};
|
||||
pub use crate::instance::{Borrowed, Bound, Py};
|
||||
pub use crate::marker::Python;
|
||||
pub use crate::pycell::{PyRef, PyRefMut};
|
||||
pub use crate::pyclass_init::PyClassInitializer;
|
||||
pub use crate::types::{PyAny, PyModule};
|
||||
pub use crate::{PyClassGuard, PyClassGuardMut};
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
pub use pyo3_macros::{
|
||||
pyclass, pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject, IntoPyObjectRef,
|
||||
};
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
pub use crate::wrap_pyfunction;
|
||||
|
||||
pub use crate::types::any::PyAnyMethods;
|
||||
pub use crate::types::boolobject::PyBoolMethods;
|
||||
pub use crate::types::bytearray::PyByteArrayMethods;
|
||||
pub use crate::types::bytes::PyBytesMethods;
|
||||
pub use crate::types::capsule::PyCapsuleMethods;
|
||||
pub use crate::types::complex::PyComplexMethods;
|
||||
pub use crate::types::dict::PyDictMethods;
|
||||
pub use crate::types::float::PyFloatMethods;
|
||||
pub use crate::types::frozenset::PyFrozenSetMethods;
|
||||
pub use crate::types::list::PyListMethods;
|
||||
pub use crate::types::mapping::PyMappingMethods;
|
||||
pub use crate::types::mappingproxy::PyMappingProxyMethods;
|
||||
pub use crate::types::module::PyModuleMethods;
|
||||
pub use crate::types::sequence::PySequenceMethods;
|
||||
pub use crate::types::set::PySetMethods;
|
||||
pub use crate::types::slice::PySliceMethods;
|
||||
pub use crate::types::string::PyStringMethods;
|
||||
pub use crate::types::traceback::PyTracebackMethods;
|
||||
pub use crate::types::tuple::PyTupleMethods;
|
||||
pub use crate::types::typeobject::PyTypeMethods;
|
||||
pub use crate::types::weakref::PyWeakrefMethods;
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::{Bound, PyAny, PyResult, PyTypeCheck};
|
||||
|
||||
pub(crate) trait PyResultExt<'py>: crate::sealed::Sealed {
|
||||
fn cast_into<T: PyTypeCheck>(self) -> PyResult<Bound<'py, T>>;
|
||||
unsafe fn cast_into_unchecked<T>(self) -> PyResult<Bound<'py, T>>;
|
||||
}
|
||||
|
||||
impl<'py> PyResultExt<'py> for PyResult<Bound<'py, PyAny>> {
|
||||
#[inline]
|
||||
fn cast_into<T: PyTypeCheck>(self) -> PyResult<Bound<'py, T>> where {
|
||||
self.and_then(|instance| instance.cast_into().map_err(Into::into))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
unsafe fn cast_into_unchecked<T>(self) -> PyResult<Bound<'py, T>> {
|
||||
self.map(|instance| unsafe { instance.cast_into_unchecked() })
|
||||
}
|
||||
}
|
||||
Vendored
+776
@@ -0,0 +1,776 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Contains types for working with Python objects that own the underlying data.
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::type_hint_union;
|
||||
use crate::{
|
||||
types::{
|
||||
bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray,
|
||||
PyBytes, PyString, PyTuple,
|
||||
},
|
||||
Borrowed, Bound, CastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyTypeInfo, Python,
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use core::{borrow::Borrow, convert::Infallible, ops::Deref, ptr::NonNull};
|
||||
|
||||
/// An equivalent to `String` where the storage is owned by a Python `bytes` or `str` object.
|
||||
///
|
||||
/// On Python 3.10+ or when not using the stable API, this type is guaranteed to contain a Python `str`
|
||||
/// for the underlying data.
|
||||
///
|
||||
/// This type gives access to the underlying data via a `Deref` implementation.
|
||||
#[cfg_attr(feature = "py-clone", derive(Clone))]
|
||||
pub struct PyBackedStr {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
storage: Py<PyString>,
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
storage: Py<PyBytes>,
|
||||
data: NonNull<str>,
|
||||
}
|
||||
|
||||
impl PyBackedStr {
|
||||
/// Clones this by incrementing the reference count of the underlying Python object.
|
||||
///
|
||||
/// Similar to [`Py::clone_ref`], this method is always available, even when the `py-clone` feature is disabled.
|
||||
#[inline]
|
||||
pub fn clone_ref(&self, py: Python<'_>) -> Self {
|
||||
Self {
|
||||
storage: self.storage.clone_ref(py),
|
||||
data: self.data,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying data as a `&str` slice.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
// Safety: `data` is known to be immutable and owned by self
|
||||
unsafe { self.data.as_ref() }
|
||||
}
|
||||
|
||||
/// Returns the underlying data as a Python `str`.
|
||||
///
|
||||
/// Older versions of the Python stable API do not support this zero-cost conversion.
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
#[inline]
|
||||
pub fn as_py_str(&self) -> &Py<PyString> {
|
||||
&self.storage
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PyBackedStr {
|
||||
type Target = str;
|
||||
#[inline]
|
||||
fn deref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PyBackedStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PyBackedStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for PyBackedStr {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: the underlying Python str (or bytes) is immutable and
|
||||
// safe to share between threads
|
||||
unsafe impl Send for PyBackedStr {}
|
||||
unsafe impl Sync for PyBackedStr {}
|
||||
|
||||
impl core::fmt::Display for PyBackedStr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.deref().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl_traits!(PyBackedStr, str);
|
||||
|
||||
impl TryFrom<Bound<'_, PyString>> for PyBackedStr {
|
||||
type Error = PyErr;
|
||||
fn try_from(py_string: Bound<'_, PyString>) -> Result<Self, Self::Error> {
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
{
|
||||
let s = py_string.to_str()?;
|
||||
let data = NonNull::from(s);
|
||||
Ok(Self {
|
||||
storage: py_string.unbind(),
|
||||
data,
|
||||
})
|
||||
}
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
{
|
||||
let bytes = py_string.encode_utf8()?;
|
||||
let s = unsafe { core::str::from_utf8_unchecked(bytes.as_bytes()) };
|
||||
let data = NonNull::from(s);
|
||||
Ok(Self {
|
||||
storage: bytes.unbind(),
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromPyObject<'_, '_> for PyBackedStr {
|
||||
type Error = PyErr;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
#[inline]
|
||||
fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
|
||||
let py_string = obj.cast::<PyString>()?.to_owned();
|
||||
Self::try_from(py_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for PyBackedStr {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
#[inline]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.storage.into_bound(py))
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, &self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &PyBackedStr {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyString::TYPE_HINT;
|
||||
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.storage.bind(py).to_owned())
|
||||
}
|
||||
|
||||
#[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))]
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`.
|
||||
///
|
||||
/// This type gives access to the underlying data via a `Deref` implementation.
|
||||
#[cfg_attr(feature = "py-clone", derive(Clone))]
|
||||
pub struct PyBackedBytes {
|
||||
storage: PyBackedBytesStorage,
|
||||
data: NonNull<[u8]>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "py-clone", derive(Clone))]
|
||||
enum PyBackedBytesStorage {
|
||||
Python(Py<PyBytes>),
|
||||
Rust(Arc<[u8]>),
|
||||
}
|
||||
|
||||
impl PyBackedBytes {
|
||||
/// Clones this by incrementing the reference count of the underlying data.
|
||||
///
|
||||
/// Similar to [`Py::clone_ref`], this method is always available, even when the `py-clone` feature is disabled.
|
||||
pub fn clone_ref(&self, py: Python<'_>) -> Self {
|
||||
Self {
|
||||
storage: match &self.storage {
|
||||
PyBackedBytesStorage::Python(bytes) => {
|
||||
PyBackedBytesStorage::Python(bytes.clone_ref(py))
|
||||
}
|
||||
PyBackedBytesStorage::Rust(bytes) => PyBackedBytesStorage::Rust(bytes.clone()),
|
||||
},
|
||||
data: self.data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PyBackedBytes {
|
||||
type Target = [u8];
|
||||
fn deref(&self) -> &[u8] {
|
||||
// Safety: `data` is known to be immutable and owned by self
|
||||
unsafe { self.data.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PyBackedBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: the underlying Python bytes or Rust bytes is immutable and
|
||||
// safe to share between threads
|
||||
unsafe impl Send for PyBackedBytes {}
|
||||
unsafe impl Sync for PyBackedBytes {}
|
||||
|
||||
impl<const N: usize> PartialEq<[u8; N]> for PyBackedBytes {
|
||||
fn eq(&self, other: &[u8; N]) -> bool {
|
||||
self.deref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> PartialEq<PyBackedBytes> for [u8; N] {
|
||||
fn eq(&self, other: &PyBackedBytes) -> bool {
|
||||
self == other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> PartialEq<&[u8; N]> for PyBackedBytes {
|
||||
fn eq(&self, other: &&[u8; N]) -> bool {
|
||||
self.deref() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> PartialEq<PyBackedBytes> for &[u8; N] {
|
||||
fn eq(&self, other: &PyBackedBytes) -> bool {
|
||||
self == &other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl_traits!(PyBackedBytes, [u8]);
|
||||
|
||||
impl From<Bound<'_, PyBytes>> for PyBackedBytes {
|
||||
fn from(py_bytes: Bound<'_, PyBytes>) -> Self {
|
||||
let b = py_bytes.as_bytes();
|
||||
let data = NonNull::from(b);
|
||||
Self {
|
||||
storage: PyBackedBytesStorage::Python(py_bytes.to_owned().unbind()),
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bound<'_, PyByteArray>> for PyBackedBytes {
|
||||
fn from(py_bytearray: Bound<'_, PyByteArray>) -> Self {
|
||||
let s = Arc::<[u8]>::from(py_bytearray.to_vec());
|
||||
let data = NonNull::from(s.as_ref());
|
||||
Self {
|
||||
storage: PyBackedBytesStorage::Rust(s),
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py> FromPyObject<'a, 'py> for PyBackedBytes {
|
||||
type Error = CastError<'a, 'py>;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const INPUT_TYPE: PyStaticExpr = type_hint_union!(PyBytes::TYPE_HINT, PyByteArray::TYPE_HINT);
|
||||
|
||||
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
|
||||
if let Ok(bytes) = obj.cast::<PyBytes>() {
|
||||
Ok(Self::from(bytes.to_owned()))
|
||||
} else if let Ok(bytearray) = obj.cast::<PyByteArray>() {
|
||||
Ok(Self::from(bytearray.to_owned()))
|
||||
} else {
|
||||
Err(CastError::new(
|
||||
obj,
|
||||
PyTuple::new(
|
||||
obj.py(),
|
||||
[
|
||||
PyBytes::type_object(obj.py()),
|
||||
PyByteArray::type_object(obj.py()),
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
.into_any(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for PyBackedBytes {
|
||||
type Target = PyBytes;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match self.storage {
|
||||
PyBackedBytesStorage::Python(bytes) => Ok(bytes.into_bound(py)),
|
||||
PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &PyBackedBytes {
|
||||
type Target = PyBytes;
|
||||
type Output = Bound<'py, Self::Target>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = PyBytes::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
match &self.storage {
|
||||
PyBackedBytesStorage::Python(bytes) => Ok(bytes.bind(py).clone()),
|
||||
PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_traits {
|
||||
($slf:ty, $equiv:ty) => {
|
||||
impl core::fmt::Debug for $slf {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.deref().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for $slf {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.deref() == other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<$equiv> for $slf {
|
||||
#[inline]
|
||||
fn eq(&self, other: &$equiv) -> bool {
|
||||
self.deref() == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&$equiv> for $slf {
|
||||
#[inline]
|
||||
fn eq(&self, other: &&$equiv) -> bool {
|
||||
self.deref() == *other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<$slf> for $equiv {
|
||||
#[inline]
|
||||
fn eq(&self, other: &$slf) -> bool {
|
||||
self == other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<$slf> for &$equiv {
|
||||
#[inline]
|
||||
fn eq(&self, other: &$slf) -> bool {
|
||||
self == &other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for $slf {}
|
||||
|
||||
impl PartialOrd for $slf {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<$equiv> for $slf {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &$equiv) -> Option<core::cmp::Ordering> {
|
||||
self.deref().partial_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<$slf> for $equiv {
|
||||
#[inline]
|
||||
fn partial_cmp(&self, other: &$slf) -> Option<core::cmp::Ordering> {
|
||||
self.partial_cmp(other.deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for $slf {
|
||||
#[inline]
|
||||
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
|
||||
self.deref().cmp(other.deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for $slf {
|
||||
#[inline]
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.deref().hash(state)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
use impl_traits;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::impl_::pyclass::{value_of, IsSend, IsSync};
|
||||
use crate::types::PyAnyMethods as _;
|
||||
use crate::{IntoPyObject, Python};
|
||||
use core::hash::{Hash, Hasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
#[test]
|
||||
fn py_backed_str_empty() {
|
||||
Python::attach(|py| {
|
||||
let s = PyString::new(py, "");
|
||||
let py_backed_str = s.extract::<PyBackedStr>().unwrap();
|
||||
assert_eq!(&*py_backed_str, "");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_str() {
|
||||
Python::attach(|py| {
|
||||
let s = PyString::new(py, "hello");
|
||||
let py_backed_str = s.extract::<PyBackedStr>().unwrap();
|
||||
assert_eq!(&*py_backed_str, "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_str_try_from() {
|
||||
Python::attach(|py| {
|
||||
let s = PyString::new(py, "hello");
|
||||
let py_backed_str = PyBackedStr::try_from(s).unwrap();
|
||||
assert_eq!(&*py_backed_str, "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_str_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let orig_str = PyString::new(py, "hello");
|
||||
let py_backed_str = orig_str.extract::<PyBackedStr>().unwrap();
|
||||
let new_str = py_backed_str.into_pyobject(py).unwrap();
|
||||
assert_eq!(new_str.extract::<PyBackedStr>().unwrap(), "hello");
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
assert!(new_str.is(&orig_str));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_bytes_empty() {
|
||||
Python::attach(|py| {
|
||||
let b = PyBytes::new(py, b"");
|
||||
let py_backed_bytes = b.extract::<PyBackedBytes>().unwrap();
|
||||
assert_eq!(&*py_backed_bytes, b"");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_bytes() {
|
||||
Python::attach(|py| {
|
||||
let b = PyBytes::new(py, b"abcde");
|
||||
let py_backed_bytes = b.extract::<PyBackedBytes>().unwrap();
|
||||
assert_eq!(&*py_backed_bytes, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_bytes_from_bytes() {
|
||||
Python::attach(|py| {
|
||||
let b = PyBytes::new(py, b"abcde");
|
||||
let py_backed_bytes = PyBackedBytes::from(b);
|
||||
assert_eq!(&*py_backed_bytes, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_bytes_from_bytearray() {
|
||||
Python::attach(|py| {
|
||||
let b = PyByteArray::new(py, b"abcde");
|
||||
let py_backed_bytes = PyBackedBytes::from(b);
|
||||
assert_eq!(&*py_backed_bytes, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn py_backed_bytes_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let orig_bytes = PyBytes::new(py, b"abcde");
|
||||
let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone());
|
||||
assert!((&py_backed_bytes)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.is(&orig_bytes));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rust_backed_bytes_into_pyobject() {
|
||||
Python::attach(|py| {
|
||||
let orig_bytes = PyByteArray::new(py, b"abcde");
|
||||
let rust_backed_bytes = PyBackedBytes::from(orig_bytes);
|
||||
assert!(matches!(
|
||||
rust_backed_bytes.storage,
|
||||
PyBackedBytesStorage::Rust(_)
|
||||
));
|
||||
let to_object = (&rust_backed_bytes).into_pyobject(py).unwrap();
|
||||
assert!(&to_object.is_exact_instance_of::<PyBytes>());
|
||||
assert_eq!(&to_object.extract::<PyBackedBytes>().unwrap(), b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_types_send_sync() {
|
||||
assert!(value_of!(IsSend, PyBackedStr));
|
||||
assert!(value_of!(IsSync, PyBackedStr));
|
||||
|
||||
assert!(value_of!(IsSend, PyBackedBytes));
|
||||
assert!(value_of!(IsSync, PyBackedBytes));
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
fn test_backed_str_clone() {
|
||||
Python::attach(|py| {
|
||||
let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
let s2 = s1.clone();
|
||||
assert_eq!(s1, s2);
|
||||
|
||||
drop(s1);
|
||||
assert_eq!(s2, "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_clone_ref() {
|
||||
Python::attach(|py| {
|
||||
let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
let s2 = s1.clone_ref(py);
|
||||
assert_eq!(s1, s2);
|
||||
assert!(s1.storage.is(&s2.storage));
|
||||
|
||||
drop(s1);
|
||||
assert_eq!(s2, "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_eq() {
|
||||
Python::attach(|py| {
|
||||
let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
let s2: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
assert_eq!(s1, "hello");
|
||||
assert_eq!(s1, s2);
|
||||
|
||||
let s3: PyBackedStr = PyString::new(py, "abcde").try_into().unwrap();
|
||||
assert_eq!("abcde", s3);
|
||||
assert_ne!(s1, s3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_hash() {
|
||||
Python::attach(|py| {
|
||||
let h = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
"abcde".hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
let s1: PyBackedStr = PyString::new(py, "abcde").try_into().unwrap();
|
||||
let h1 = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
s1.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
assert_eq!(h, h1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_ord() {
|
||||
Python::attach(|py| {
|
||||
let mut a = vec!["a", "c", "d", "b", "f", "g", "e"];
|
||||
let mut b = a
|
||||
.iter()
|
||||
.map(|s| PyString::new(py, s).try_into().unwrap())
|
||||
.collect::<Vec<PyBackedStr>>();
|
||||
|
||||
a.sort();
|
||||
b.sort();
|
||||
|
||||
assert_eq!(a, b);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_map_key() {
|
||||
Python::attach(|py| {
|
||||
use crate::platform::HashMap;
|
||||
|
||||
let mut map: HashMap<PyBackedStr, usize> = HashMap::new();
|
||||
let s: PyBackedStr = PyString::new(py, "key1").try_into().unwrap();
|
||||
|
||||
map.insert(s, 1);
|
||||
|
||||
assert_eq!(map.get("key1"), Some(&1));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_str_as_str() {
|
||||
Python::attach(|py| {
|
||||
let s: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
assert_eq!(s.as_str(), "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_10, not(Py_LIMITED_API)))]
|
||||
fn test_backed_str_as_py_str() {
|
||||
Python::attach(|py| {
|
||||
let s: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
|
||||
let py_str = s.as_py_str().bind(py);
|
||||
assert!(py_str.is(&s.storage));
|
||||
assert_eq!(py_str.to_str().unwrap(), "hello");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
fn test_backed_bytes_from_bytes_clone() {
|
||||
Python::attach(|py| {
|
||||
let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
|
||||
let b2 = b1.clone();
|
||||
assert_eq!(b1, b2);
|
||||
|
||||
drop(b1);
|
||||
assert_eq!(b2, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_bytes_from_bytes_clone_ref() {
|
||||
Python::attach(|py| {
|
||||
let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
|
||||
let b2 = b1.clone_ref(py);
|
||||
assert_eq!(b1, b2);
|
||||
let (PyBackedBytesStorage::Python(s1), PyBackedBytesStorage::Python(s2)) =
|
||||
(&b1.storage, &b2.storage)
|
||||
else {
|
||||
panic!("Expected Python-backed bytes");
|
||||
};
|
||||
assert!(s1.is(s2));
|
||||
|
||||
drop(b1);
|
||||
assert_eq!(b2, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "py-clone")]
|
||||
#[test]
|
||||
fn test_backed_bytes_from_bytearray_clone() {
|
||||
Python::attach(|py| {
|
||||
let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
|
||||
let b2 = b1.clone();
|
||||
assert_eq!(b1, b2);
|
||||
|
||||
drop(b1);
|
||||
assert_eq!(b2, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_bytes_from_bytearray_clone_ref() {
|
||||
Python::attach(|py| {
|
||||
let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
|
||||
let b2 = b1.clone_ref(py);
|
||||
assert_eq!(b1, b2);
|
||||
let (PyBackedBytesStorage::Rust(s1), PyBackedBytesStorage::Rust(s2)) =
|
||||
(&b1.storage, &b2.storage)
|
||||
else {
|
||||
panic!("Expected Rust-backed bytes");
|
||||
};
|
||||
assert!(Arc::ptr_eq(s1, s2));
|
||||
|
||||
drop(b1);
|
||||
assert_eq!(b2, b"abcde");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_bytes_eq() {
|
||||
Python::attach(|py| {
|
||||
let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
|
||||
let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
|
||||
|
||||
assert_eq!(b1, b"abcde");
|
||||
assert_eq!(b1, b2);
|
||||
|
||||
let b3: PyBackedBytes = PyBytes::new(py, b"hello").into();
|
||||
assert_eq!(b"hello", b3);
|
||||
assert_ne!(b1, b3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_bytes_hash() {
|
||||
Python::attach(|py| {
|
||||
let h = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
b"abcde".hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
|
||||
let h1 = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
b1.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
|
||||
let h2 = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
b2.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
assert_eq!(h, h1);
|
||||
assert_eq!(h, h2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backed_bytes_ord() {
|
||||
Python::attach(|py| {
|
||||
let mut a = vec![b"a", b"c", b"d", b"b", b"f", b"g", b"e"];
|
||||
let mut b = a
|
||||
.iter()
|
||||
.map(|&b| PyBytes::new(py, b).into())
|
||||
.collect::<Vec<PyBackedBytes>>();
|
||||
|
||||
a.sort();
|
||||
b.sort();
|
||||
|
||||
assert_eq!(a, b);
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+952
@@ -0,0 +1,952 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! PyO3's interior mutability primitive.
|
||||
//!
|
||||
//! Rust has strict aliasing rules - you can either have any number of immutable (shared) references or one mutable
|
||||
//! reference. Python's ownership model is the complete opposite of that - any Python object
|
||||
//! can be referenced any number of times, and mutation is allowed from any reference.
|
||||
//!
|
||||
//! PyO3 deals with these differences by employing the [Interior Mutability]
|
||||
//! pattern. This requires that PyO3 enforces the borrowing rules and it has two mechanisms for
|
||||
//! doing so:
|
||||
//! - Statically it can enforce thread-safe access with the [`Python<'py>`](crate::Python) token.
|
||||
//! All Rust code holding that token, or anything derived from it, can assume that they have
|
||||
//! safe access to the Python interpreter's state. For this reason all the native Python objects
|
||||
//! can be mutated through shared references.
|
||||
//! - However, methods and functions in Rust usually *do* need `&mut` references. While PyO3 can
|
||||
//! use the [`Python<'py>`](crate::Python) token to guarantee thread-safe access to them, it cannot
|
||||
//! statically guarantee uniqueness of `&mut` references. As such those references have to be tracked
|
||||
//! dynamically at runtime, using `PyCell` and the other types defined in this module. This works
|
||||
//! similar to std's [`RefCell`](core::cell::RefCell) type.
|
||||
//!
|
||||
//! # When *not* to use PyCell
|
||||
//!
|
||||
//! Usually you can use `&mut` references as method and function receivers and arguments, and you
|
||||
//! won't need to use `PyCell` directly:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pyo3::prelude::*;
|
||||
//!
|
||||
//! #[pyclass]
|
||||
//! struct Number {
|
||||
//! inner: u32,
|
||||
//! }
|
||||
//!
|
||||
//! #[pymethods]
|
||||
//! impl Number {
|
||||
//! fn increment(&mut self) {
|
||||
//! self.inner += 1;
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The [`#[pymethods]`](crate::pymethods) proc macro will generate this wrapper function (and more),
|
||||
//! using `PyCell` under the hood:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! # use pyo3::prelude::*;
|
||||
//! # #[pyclass]
|
||||
//! # struct Number {
|
||||
//! # inner: u32,
|
||||
//! # }
|
||||
//! #
|
||||
//! # #[pymethods]
|
||||
//! # impl Number {
|
||||
//! # fn increment(&mut self) {
|
||||
//! # self.inner += 1;
|
||||
//! # }
|
||||
//! # }
|
||||
//! #
|
||||
//! // The function which is exported to Python looks roughly like the following
|
||||
//! unsafe extern "C" fn __pymethod_increment__(
|
||||
//! _slf: *mut pyo3::ffi::PyObject,
|
||||
//! _args: *mut pyo3::ffi::PyObject,
|
||||
//! ) -> *mut pyo3::ffi::PyObject {
|
||||
//! use :: pyo3 as _pyo3;
|
||||
//! _pyo3::impl_::trampoline::noargs(_slf, _args, |py, _slf| {
|
||||
//! # #[allow(deprecated)]
|
||||
//! let _cell = py
|
||||
//! .from_borrowed_ptr::<_pyo3::PyAny>(_slf)
|
||||
//! .cast::<_pyo3::PyCell<Number>>()?;
|
||||
//! let mut _ref = _cell.try_borrow_mut()?;
|
||||
//! let _slf: &mut Number = &mut *_ref;
|
||||
//! _pyo3::impl_::callback::convert(py, Number::increment(_slf))
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # When to use PyCell
|
||||
//! ## Using pyclasses from Rust
|
||||
//!
|
||||
//! However, we *do* need `PyCell` if we want to call its methods from Rust:
|
||||
//! ```rust
|
||||
//! # use pyo3::prelude::*;
|
||||
//! #
|
||||
//! # #[pyclass]
|
||||
//! # struct Number {
|
||||
//! # inner: u32,
|
||||
//! # }
|
||||
//! #
|
||||
//! # #[pymethods]
|
||||
//! # impl Number {
|
||||
//! # fn increment(&mut self) {
|
||||
//! # self.inner += 1;
|
||||
//! # }
|
||||
//! # }
|
||||
//! # fn main() -> PyResult<()> {
|
||||
//! Python::attach(|py| {
|
||||
//! let n = Py::new(py, Number { inner: 0 })?;
|
||||
//!
|
||||
//! // We borrow the guard and then dereference
|
||||
//! // it to get a mutable reference to Number
|
||||
//! let mut guard: PyRefMut<'_, Number> = n.bind(py).borrow_mut();
|
||||
//! let n_mutable: &mut Number = &mut *guard;
|
||||
//!
|
||||
//! n_mutable.increment();
|
||||
//!
|
||||
//! // To avoid panics we must dispose of the
|
||||
//! // `PyRefMut` before borrowing again.
|
||||
//! drop(guard);
|
||||
//!
|
||||
//! let n_immutable: &Number = &n.bind(py).borrow();
|
||||
//! assert_eq!(n_immutable.inner, 1);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! })
|
||||
//! # }
|
||||
//! ```
|
||||
//! ## Dealing with possibly overlapping mutable references
|
||||
//!
|
||||
//! It is also necessary to use `PyCell` if you can receive mutable arguments that may overlap.
|
||||
//! Suppose the following function that swaps the values of two `Number`s:
|
||||
//! ```
|
||||
//! # use pyo3::prelude::*;
|
||||
//! # #[pyclass]
|
||||
//! # pub struct Number {
|
||||
//! # inner: u32,
|
||||
//! # }
|
||||
//! #[pyfunction]
|
||||
//! fn swap_numbers(a: &mut Number, b: &mut Number) {
|
||||
//! core::mem::swap(&mut a.inner, &mut b.inner);
|
||||
//! }
|
||||
//! # fn main() {
|
||||
//! # Python::attach(|py| {
|
||||
//! # let n = Py::new(py, Number{inner: 35}).unwrap();
|
||||
//! # let n2 = n.clone_ref(py);
|
||||
//! # assert!(n.is(&n2));
|
||||
//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap();
|
||||
//! # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour.");
|
||||
//! # });
|
||||
//! # }
|
||||
//! ```
|
||||
//! When users pass in the same `Number` as both arguments, one of the mutable borrows will
|
||||
//! fail and raise a `RuntimeError`:
|
||||
//! ```text
|
||||
//! >>> a = Number()
|
||||
//! >>> swap_numbers(a, a)
|
||||
//! Traceback (most recent call last):
|
||||
//! File "<stdin>", line 1, in <module>
|
||||
//! RuntimeError: Already borrowed
|
||||
//! ```
|
||||
//!
|
||||
//! It is better to write that function like this:
|
||||
//! ```rust,ignore
|
||||
//! # #![allow(deprecated)]
|
||||
//! # use pyo3::prelude::*;
|
||||
//! # #[pyclass]
|
||||
//! # pub struct Number {
|
||||
//! # inner: u32,
|
||||
//! # }
|
||||
//! #[pyfunction]
|
||||
//! fn swap_numbers(a: &PyCell<Number>, b: &PyCell<Number>) {
|
||||
//! // Check that the pointers are unequal
|
||||
//! if !a.is(b) {
|
||||
//! core::mem::swap(&mut a.borrow_mut().inner, &mut b.borrow_mut().inner);
|
||||
//! } else {
|
||||
//! // Do nothing - they are the same object, so don't need swapping.
|
||||
//! }
|
||||
//! }
|
||||
//! # fn main() {
|
||||
//! # // With duplicate numbers
|
||||
//! # Python::attach(|py| {
|
||||
//! # let n = Py::new(py, Number{inner: 35}).unwrap();
|
||||
//! # let n2 = n.clone_ref(py);
|
||||
//! # assert!(n.is(&n2));
|
||||
//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap();
|
||||
//! # fun.call1((n, n2)).unwrap();
|
||||
//! # });
|
||||
//! #
|
||||
//! # // With two different numbers
|
||||
//! # Python::attach(|py| {
|
||||
//! # let n = Py::new(py, Number{inner: 35}).unwrap();
|
||||
//! # let n2 = Py::new(py, Number{inner: 42}).unwrap();
|
||||
//! # assert!(!n.is(&n2));
|
||||
//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap();
|
||||
//! # fun.call1((&n, &n2)).unwrap();
|
||||
//! # let n: u32 = n.borrow(py).inner;
|
||||
//! # let n2: u32 = n2.borrow(py).inner;
|
||||
//! # assert_eq!(n, 42);
|
||||
//! # assert_eq!(n2, 35);
|
||||
//! # });
|
||||
//! # }
|
||||
//! ```
|
||||
//! See the [guide] for more information.
|
||||
//!
|
||||
//! [guide]: https://pyo3.rs/latest/class.html#pycell-and-interior-mutability "PyCell and interior mutability"
|
||||
//! [Interior Mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html "RefCell<T> and the Interior Mutability Pattern - The Rust Programming Language"
|
||||
|
||||
use crate::conversion::IntoPyObject;
|
||||
use crate::exceptions::PyRuntimeError;
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
use crate::pyclass::{boolean_struct::False, PyClass};
|
||||
use crate::{ffi, Borrowed, Bound, PyErr, Python};
|
||||
use core::convert::Infallible;
|
||||
use core::fmt;
|
||||
use core::mem::ManuallyDrop;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use core::ptr::NonNull;
|
||||
|
||||
pub(crate) mod impl_;
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
use crate::inspect::PyStaticExpr;
|
||||
use impl_::{PyClassBorrowChecker, PyClassObjectBaseLayout, PyClassObjectLayout};
|
||||
|
||||
/// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`].
|
||||
///
|
||||
/// See the [`Bound`] documentation for more information.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can use [`PyRef`] as an alternative to a `&self` receiver when
|
||||
/// - you need to access the pointer of the [`Bound`], or
|
||||
/// - you want to get a super class.
|
||||
/// ```
|
||||
/// # use pyo3::prelude::*;
|
||||
/// #[pyclass(subclass)]
|
||||
/// struct Parent {
|
||||
/// basename: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pyclass(extends=Parent)]
|
||||
/// struct Child {
|
||||
/// name: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Child {
|
||||
/// #[new]
|
||||
/// fn new() -> PyClassInitializer<Self> {
|
||||
/// PyClassInitializer::from(Parent { basename: "Butterfly" })
|
||||
/// .add_subclass(Child { name: "Caterpillar" })
|
||||
/// }
|
||||
///
|
||||
/// fn format(slf: PyRef<'_, Self>) -> String {
|
||||
/// // We can get *mut ffi::PyObject from PyRef
|
||||
/// let refcnt = unsafe { pyo3::ffi::Py_REFCNT(slf.as_ptr()) };
|
||||
/// // We can get &Self::BaseType by as_ref
|
||||
/// let basename = slf.as_ref().basename;
|
||||
/// format!("{}(base: {}, cnt: {})", slf.name, basename, refcnt)
|
||||
/// }
|
||||
/// }
|
||||
/// # Python::attach(|py| {
|
||||
/// # let sub = Py::new(py, Child::new()).unwrap();
|
||||
/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)', sub.format()");
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// See the [module-level documentation](self) for more information.
|
||||
#[repr(transparent)]
|
||||
pub struct PyRef<'p, T: PyClass> {
|
||||
// TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to
|
||||
// store `Borrowed` here instead, avoiding reference counting overhead.
|
||||
inner: Bound<'p, T>,
|
||||
}
|
||||
|
||||
impl<'p, T: PyClass> PyRef<'p, T> {
|
||||
/// Returns a `Python` token that is bound to the lifetime of the `PyRef`.
|
||||
pub fn py(&self) -> Python<'p> {
|
||||
self.inner.py()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> AsRef<U> for PyRef<'_, T>
|
||||
where
|
||||
T: PyClass<BaseType = U>,
|
||||
U: PyClass,
|
||||
{
|
||||
fn as_ref(&self) -> &T::BaseType {
|
||||
self.as_super()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass> PyRef<'py, T> {
|
||||
/// Returns the raw FFI pointer represented by self.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers are responsible for ensuring that the pointer does not outlive self.
|
||||
///
|
||||
/// The reference is borrowed; callers should not decrease the reference count
|
||||
/// when they are finished with the pointer.
|
||||
#[inline]
|
||||
pub fn as_ptr(&self) -> *mut ffi::PyObject {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
/// Returns an owned raw FFI pointer represented by self.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The reference is owned; when finished the caller should either transfer ownership
|
||||
/// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)).
|
||||
#[inline]
|
||||
pub fn into_ptr(self) -> *mut ffi::PyObject {
|
||||
self.inner.clone().into_ptr()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self {
|
||||
Self::try_borrow(obj).expect("Already mutably borrowed")
|
||||
}
|
||||
|
||||
pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result<Self, PyBorrowError> {
|
||||
let cell = obj.get_class_object();
|
||||
cell.ensure_threadsafe();
|
||||
cell.borrow_checker()
|
||||
.try_borrow()
|
||||
.map(|_| Self { inner: obj.clone() })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'p, T> PyRef<'p, T>
|
||||
where
|
||||
T: PyClass,
|
||||
T::BaseType: PyClass,
|
||||
{
|
||||
/// Gets a `PyRef<T::BaseType>`.
|
||||
///
|
||||
/// While `as_ref()` returns a reference of type `&T::BaseType`, this cannot be
|
||||
/// used to get the base of `T::BaseType`.
|
||||
///
|
||||
/// But with the help of this method, you can get hold of instances of the
|
||||
/// super-superclass when needed.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use pyo3::prelude::*;
|
||||
/// #[pyclass(subclass)]
|
||||
/// struct Base1 {
|
||||
/// name1: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pyclass(extends=Base1, subclass)]
|
||||
/// struct Base2 {
|
||||
/// name2: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pyclass(extends=Base2)]
|
||||
/// struct Sub {
|
||||
/// name3: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Sub {
|
||||
/// #[new]
|
||||
/// fn new() -> PyClassInitializer<Self> {
|
||||
/// PyClassInitializer::from(Base1 { name1: "base1" })
|
||||
/// .add_subclass(Base2 { name2: "base2" })
|
||||
/// .add_subclass(Self { name3: "sub" })
|
||||
/// }
|
||||
/// fn name(slf: PyRef<'_, Self>) -> String {
|
||||
/// let subname = slf.name3;
|
||||
/// let super_ = slf.into_super();
|
||||
/// format!("{} {} {}", super_.as_ref().name1, super_.name2, subname)
|
||||
/// }
|
||||
/// }
|
||||
/// # Python::attach(|py| {
|
||||
/// # let sub = Py::new(py, Sub::new()).unwrap();
|
||||
/// # pyo3::py_run!(py, sub, "assert sub.name() == 'base1 base2 sub'")
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn into_super(self) -> PyRef<'p, T::BaseType> {
|
||||
let py = self.py();
|
||||
let t_not_frozen = !<T::Frozen as crate::pyclass::boolean_struct::private::Boolean>::VALUE;
|
||||
let u_frozen = <<T::BaseType as PyClass>::Frozen as crate::pyclass::boolean_struct::private::Boolean>::VALUE;
|
||||
if t_not_frozen && u_frozen {
|
||||
// If `T` is mutable subclass of `U` differ, then it is possible that we need to
|
||||
// release the borrow count now. (e.g. `U` may have a noop borrow checker so
|
||||
// dropping the `PyRef<U>` later would noop and leak the borrow we currently hold.)
|
||||
//
|
||||
// However it's nontrivial, if `U` itself has a mutable base class `V`,
|
||||
// then the borrow checker of both `T` and `U` is the shared borrow checker of `V`.
|
||||
//
|
||||
// But it's really hard to prove that in the type system, the soundest thing we
|
||||
// can do is just add a borrow to `U` now and then release the borrow of `T`.
|
||||
|
||||
self.inner
|
||||
.as_super()
|
||||
.get_class_object()
|
||||
.borrow_checker()
|
||||
.try_borrow()
|
||||
.expect("this object is already borrowed");
|
||||
|
||||
self.inner
|
||||
.get_class_object()
|
||||
.borrow_checker()
|
||||
.release_borrow()
|
||||
};
|
||||
PyRef {
|
||||
inner: unsafe {
|
||||
ManuallyDrop::new(self)
|
||||
.as_ptr()
|
||||
.assume_owned_unchecked(py)
|
||||
.cast_into_unchecked()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrows a shared reference to `PyRef<T::BaseType>`.
|
||||
///
|
||||
/// With the help of this method, you can access attributes and call methods
|
||||
/// on the superclass without consuming the `PyRef<T>`. This method can also
|
||||
/// be chained to access the super-superclass (and so on).
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use pyo3::prelude::*;
|
||||
/// #[pyclass(subclass)]
|
||||
/// struct Base {
|
||||
/// base_name: &'static str,
|
||||
/// }
|
||||
/// #[pymethods]
|
||||
/// impl Base {
|
||||
/// fn base_name_len(&self) -> usize {
|
||||
/// self.base_name.len()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[pyclass(extends=Base)]
|
||||
/// struct Sub {
|
||||
/// sub_name: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Sub {
|
||||
/// #[new]
|
||||
/// fn new() -> PyClassInitializer<Self> {
|
||||
/// PyClassInitializer::from(Base { base_name: "base_name" })
|
||||
/// .add_subclass(Self { sub_name: "sub_name" })
|
||||
/// }
|
||||
/// fn sub_name_len(&self) -> usize {
|
||||
/// self.sub_name.len()
|
||||
/// }
|
||||
/// fn format_name_lengths(slf: PyRef<'_, Self>) -> String {
|
||||
/// format!("{} {}", slf.as_super().base_name_len(), slf.sub_name_len())
|
||||
/// }
|
||||
/// }
|
||||
/// # Python::attach(|py| {
|
||||
/// # let sub = Py::new(py, Sub::new()).unwrap();
|
||||
/// # pyo3::py_run!(py, sub, "assert sub.format_name_lengths() == '9 8'")
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn as_super(&self) -> &PyRef<'p, T::BaseType> {
|
||||
let ptr = NonNull::from(&self.inner)
|
||||
// `Bound<T>` has the same layout as `Bound<T::BaseType>`
|
||||
.cast::<Bound<'p, T::BaseType>>()
|
||||
// `Bound<T::BaseType>` has the same layout as `PyRef<T::BaseType>`
|
||||
.cast::<PyRef<'p, T::BaseType>>();
|
||||
// SAFETY: lifetimes are correctly transferred, and `PyRef<T>` and `PyRef<U>` have the same layout
|
||||
unsafe { ptr.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass> Deref for PyRef<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &T {
|
||||
unsafe { &*self.inner.get_class_object().get_ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass> Drop for PyRef<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.inner
|
||||
.get_class_object()
|
||||
.borrow_checker()
|
||||
.release_borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Bound<'py, T>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.inner.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, T>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.inner.as_borrowed())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass + fmt::Debug> fmt::Debug for PyRef<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&**self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass> TryFrom<&Bound<'py, T>> for PyRef<'py, T> {
|
||||
type Error = PyBorrowError;
|
||||
#[inline]
|
||||
fn try_from(value: &Bound<'py, T>) -> Result<Self, Self::Error> {
|
||||
PyRef::try_borrow(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper type for a mutably borrowed value from a [`Bound<'py, T>`].
|
||||
///
|
||||
/// See the [module-level documentation](self) for more information.
|
||||
#[repr(transparent)]
|
||||
pub struct PyRefMut<'p, T: PyClass<Frozen = False>> {
|
||||
// TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to
|
||||
// store `Borrowed` here instead, avoiding reference counting overhead.
|
||||
inner: Bound<'p, T>,
|
||||
}
|
||||
|
||||
impl<'p, T: PyClass<Frozen = False>> PyRefMut<'p, T> {
|
||||
/// Returns a `Python` token that is bound to the lifetime of the `PyRefMut`.
|
||||
pub fn py(&self) -> Python<'p> {
|
||||
self.inner.py()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T::BaseType> for PyRefMut<'_, T>
|
||||
where
|
||||
T: PyClass<Frozen = False>,
|
||||
T::BaseType: PyClass<Frozen = False>,
|
||||
{
|
||||
fn as_ref(&self) -> &T::BaseType {
|
||||
PyRefMut::downgrade(self).as_super()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsMut<T::BaseType> for PyRefMut<'_, T>
|
||||
where
|
||||
T: PyClass<Frozen = False>,
|
||||
T::BaseType: PyClass<Frozen = False>,
|
||||
{
|
||||
fn as_mut(&mut self) -> &mut T::BaseType {
|
||||
self.as_super()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass<Frozen = False>> PyRefMut<'py, T> {
|
||||
/// Returns the raw FFI pointer represented by self.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// Callers are responsible for ensuring that the pointer does not outlive self.
|
||||
///
|
||||
/// The reference is borrowed; callers should not decrease the reference count
|
||||
/// when they are finished with the pointer.
|
||||
#[inline]
|
||||
pub fn as_ptr(&self) -> *mut ffi::PyObject {
|
||||
self.inner.as_ptr()
|
||||
}
|
||||
|
||||
/// Returns an owned raw FFI pointer represented by self.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The reference is owned; when finished the caller should either transfer ownership
|
||||
/// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)).
|
||||
#[inline]
|
||||
pub fn into_ptr(self) -> *mut ffi::PyObject {
|
||||
self.inner.clone().into_ptr()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self {
|
||||
Self::try_borrow(obj).expect("Already borrowed")
|
||||
}
|
||||
|
||||
pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result<Self, PyBorrowMutError> {
|
||||
let cell = obj.get_class_object();
|
||||
cell.ensure_threadsafe();
|
||||
cell.borrow_checker()
|
||||
.try_borrow_mut()
|
||||
.map(|_| Self { inner: obj.clone() })
|
||||
}
|
||||
|
||||
pub(crate) fn downgrade(slf: &Self) -> &PyRef<'py, T> {
|
||||
let ptr = NonNull::from(slf).cast();
|
||||
// SAFETY: `PyRefMut<T>` and `PyRef<T>` have the same layout
|
||||
unsafe { ptr.as_ref() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'p, T> PyRefMut<'p, T>
|
||||
where
|
||||
T: PyClass<Frozen = False>,
|
||||
T::BaseType: PyClass<Frozen = False>,
|
||||
{
|
||||
/// Gets a `PyRef<T::BaseType>`.
|
||||
///
|
||||
/// See [`PyRef::into_super`] for more.
|
||||
pub fn into_super(self) -> PyRefMut<'p, T::BaseType> {
|
||||
let py = self.py();
|
||||
PyRefMut {
|
||||
inner: unsafe {
|
||||
ManuallyDrop::new(self)
|
||||
.as_ptr()
|
||||
.assume_owned_unchecked(py)
|
||||
.cast_into_unchecked()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrows a mutable reference to `PyRefMut<T::BaseType>`.
|
||||
///
|
||||
/// With the help of this method, you can mutate attributes and call mutating
|
||||
/// methods on the superclass without consuming the `PyRefMut<T>`. This method
|
||||
/// can also be chained to access the super-superclass (and so on).
|
||||
///
|
||||
/// See [`PyRef::as_super`] for more.
|
||||
pub fn as_super(&mut self) -> &mut PyRefMut<'p, T::BaseType> {
|
||||
let mut ptr = NonNull::from(&mut self.inner)
|
||||
// `Bound<T>` has the same layout as `Bound<T::BaseType>`
|
||||
.cast::<Bound<'p, T::BaseType>>()
|
||||
// `Bound<T::BaseType>` has the same layout as `PyRefMut<T::BaseType>`,
|
||||
// and the mutable borrow on `self` prevents aliasing
|
||||
.cast::<PyRefMut<'p, T::BaseType>>();
|
||||
// SAFETY: lifetimes are correctly transferred, and `PyRefMut<T>` and `PyRefMut<U>` have the same layout
|
||||
unsafe { ptr.as_mut() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass<Frozen = False>> Deref for PyRefMut<'_, T> {
|
||||
type Target = T;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &T {
|
||||
unsafe { &*self.inner.get_class_object().get_ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass<Frozen = False>> DerefMut for PyRefMut<'_, T> {
|
||||
#[inline]
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
unsafe { &mut *self.inner.get_class_object().get_ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass<Frozen = False>> Drop for PyRefMut<'_, T> {
|
||||
fn drop(&mut self) {
|
||||
self.inner
|
||||
.get_class_object()
|
||||
.borrow_checker()
|
||||
.release_borrow_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass<Frozen = False>> IntoPyObject<'py> for PyRefMut<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Bound<'py, T>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.inner.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'py, T: PyClass<Frozen = False>> IntoPyObject<'py> for &'a PyRefMut<'py, T> {
|
||||
type Target = T;
|
||||
type Output = Borrowed<'a, 'py, T>;
|
||||
type Error = Infallible;
|
||||
|
||||
#[cfg(feature = "experimental-inspect")]
|
||||
const OUTPUT_TYPE: PyStaticExpr = T::TYPE_HINT;
|
||||
|
||||
fn into_pyobject(self, _py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(self.inner.as_borrowed())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass<Frozen = False> + fmt::Debug> fmt::Debug for PyRefMut<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(self.deref(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'py, T: PyClass<Frozen = False>> TryFrom<&Bound<'py, T>> for PyRefMut<'py, T> {
|
||||
type Error = PyBorrowMutError;
|
||||
#[inline]
|
||||
fn try_from(value: &Bound<'py, T>) -> Result<Self, Self::Error> {
|
||||
PyRefMut::try_borrow(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error type returned by [`Bound::try_borrow`].
|
||||
///
|
||||
/// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`.
|
||||
pub struct PyBorrowError {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl PyBorrowError {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PyBorrowError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PyBorrowError").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyBorrowError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt("Already mutably borrowed", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PyBorrowError> for PyErr {
|
||||
fn from(other: PyBorrowError) -> Self {
|
||||
PyRuntimeError::new_err(other.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// An error type returned by [`Bound::try_borrow_mut`].
|
||||
///
|
||||
/// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`.
|
||||
pub struct PyBorrowMutError {
|
||||
_private: (),
|
||||
}
|
||||
|
||||
impl PyBorrowMutError {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PyBorrowMutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PyBorrowMutError").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyBorrowMutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt("Already borrowed", f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PyBorrowMutError> for PyErr {
|
||||
fn from(other: PyBorrowMutError) -> Self {
|
||||
PyRuntimeError::new_err(other.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "macros")]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[crate::pyclass(skip_from_py_object)]
|
||||
#[pyo3(crate = "crate")]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
struct SomeClass(i32);
|
||||
|
||||
#[test]
|
||||
fn test_as_ptr() {
|
||||
Python::attach(|py| {
|
||||
let cell = Bound::new(py, SomeClass(0)).unwrap();
|
||||
let ptr = cell.as_ptr();
|
||||
|
||||
assert_eq!(cell.borrow().as_ptr(), ptr);
|
||||
assert_eq!(cell.borrow_mut().as_ptr(), ptr);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_ptr() {
|
||||
Python::attach(|py| {
|
||||
let cell = Bound::new(py, SomeClass(0)).unwrap();
|
||||
let ptr = cell.as_ptr();
|
||||
|
||||
assert_eq!(cell.borrow().into_ptr(), ptr);
|
||||
unsafe { ffi::Py_DECREF(ptr) };
|
||||
|
||||
assert_eq!(cell.borrow_mut().into_ptr(), ptr);
|
||||
unsafe { ffi::Py_DECREF(ptr) };
|
||||
})
|
||||
}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", subclass)]
|
||||
struct BaseClass {
|
||||
val1: usize,
|
||||
}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", extends=BaseClass, subclass)]
|
||||
struct SubClass {
|
||||
val2: usize,
|
||||
}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", extends=SubClass)]
|
||||
struct SubSubClass {
|
||||
val3: usize,
|
||||
}
|
||||
|
||||
#[crate::pymethods]
|
||||
#[pyo3(crate = "crate")]
|
||||
impl SubSubClass {
|
||||
#[new]
|
||||
fn new(py: Python<'_>) -> crate::Py<SubSubClass> {
|
||||
let init = crate::PyClassInitializer::from(BaseClass { val1: 10 })
|
||||
.add_subclass(SubClass { val2: 15 })
|
||||
.add_subclass(SubSubClass { val3: 20 });
|
||||
crate::Py::new(py, init).expect("allocation error")
|
||||
}
|
||||
|
||||
fn get_values(self_: PyRef<'_, Self>) -> (usize, usize, usize) {
|
||||
let val1 = self_.as_super().as_super().val1;
|
||||
let val2 = self_.as_super().val2;
|
||||
(val1, val2, self_.val3)
|
||||
}
|
||||
|
||||
fn double_values(mut self_: PyRefMut<'_, Self>) {
|
||||
self_.as_super().as_super().val1 *= 2;
|
||||
self_.as_super().val2 *= 2;
|
||||
self_.val3 *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyref_as_super() {
|
||||
Python::attach(|py| {
|
||||
let obj = SubSubClass::new(py).into_bound(py);
|
||||
let pyref = obj.borrow();
|
||||
assert_eq!(pyref.as_super().as_super().val1, 10);
|
||||
assert_eq!(pyref.as_super().val2, 15);
|
||||
assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works
|
||||
assert_eq!(pyref.val3, 20);
|
||||
assert_eq!(SubSubClass::get_values(pyref), (10, 15, 20));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyrefmut_as_super() {
|
||||
Python::attach(|py| {
|
||||
let obj = SubSubClass::new(py).into_bound(py);
|
||||
assert_eq!(SubSubClass::get_values(obj.borrow()), (10, 15, 20));
|
||||
{
|
||||
let mut pyrefmut = obj.borrow_mut();
|
||||
assert_eq!(pyrefmut.as_super().as_ref().val1, 10);
|
||||
pyrefmut.as_super().as_super().val1 -= 5;
|
||||
pyrefmut.as_super().val2 -= 3;
|
||||
pyrefmut.as_mut().val2 -= 2; // `as_mut` also works
|
||||
pyrefmut.val3 -= 5;
|
||||
}
|
||||
assert_eq!(SubSubClass::get_values(obj.borrow()), (5, 10, 15));
|
||||
SubSubClass::double_values(obj.borrow_mut());
|
||||
assert_eq!(SubSubClass::get_values(obj.borrow()), (10, 20, 30));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyrefs_in_python() {
|
||||
Python::attach(|py| {
|
||||
let obj = SubSubClass::new(py);
|
||||
crate::py_run!(py, obj, "assert obj.get_values() == (10, 15, 20)");
|
||||
crate::py_run!(py, obj, "assert obj.double_values() is None");
|
||||
crate::py_run!(py, obj, "assert obj.get_values() == (20, 30, 40)");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_frozen_super_released_borrow() {
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", subclass, frozen)]
|
||||
struct BaseClass {}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", extends=BaseClass, subclass)]
|
||||
struct SubClass {}
|
||||
|
||||
#[crate::pymethods]
|
||||
#[pyo3(crate = "crate")]
|
||||
impl SubClass {
|
||||
#[new]
|
||||
fn new(py: Python<'_>) -> Bound<'_, SubClass> {
|
||||
let init = crate::PyClassInitializer::from(BaseClass {}).add_subclass(SubClass {});
|
||||
Bound::new(py, init).expect("allocation error")
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = SubClass::new(py);
|
||||
drop(obj.borrow().into_super());
|
||||
assert!(obj.try_borrow_mut().is_ok());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_frozen_super_mutable_base_holds_borrow() {
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", subclass)]
|
||||
struct BaseClass {}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", extends=BaseClass, subclass, frozen)]
|
||||
struct SubClass {}
|
||||
|
||||
#[crate::pyclass]
|
||||
#[pyo3(crate = "crate", extends=SubClass, subclass)]
|
||||
struct SubSubClass {}
|
||||
|
||||
#[crate::pymethods]
|
||||
#[pyo3(crate = "crate")]
|
||||
impl SubSubClass {
|
||||
#[new]
|
||||
fn new(py: Python<'_>) -> Bound<'_, SubSubClass> {
|
||||
let init = crate::PyClassInitializer::from(BaseClass {})
|
||||
.add_subclass(SubClass {})
|
||||
.add_subclass(SubSubClass {});
|
||||
Bound::new(py, init).expect("allocation error")
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = SubSubClass::new(py);
|
||||
let _super_borrow = obj.borrow().into_super();
|
||||
// the whole object still has an immutable borrow, so we cannot
|
||||
// borrow any part mutably (the borrowflag is shared)
|
||||
assert!(obj.try_borrow_mut().is_err());
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+925
@@ -0,0 +1,925 @@
|
||||
#![allow(missing_docs)]
|
||||
//! Crate-private implementation of PyClassObject
|
||||
|
||||
use core::cell::UnsafeCell;
|
||||
use core::marker::PhantomData;
|
||||
use core::mem::{offset_of, ManuallyDrop, MaybeUninit};
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::impl_::pyclass::{
|
||||
PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, PyObjectOffset,
|
||||
};
|
||||
use crate::internal::get_slot::{TP_DEALLOC, TP_FREE};
|
||||
#[cfg(RustPython)]
|
||||
use crate::sync::PyOnceLock;
|
||||
use crate::type_object::{PyLayout, PySizedLayout, PyTypeInfo};
|
||||
use crate::types::PyType;
|
||||
use crate::{ffi, PyClass, Python};
|
||||
|
||||
use crate::types::PyTypeMethods;
|
||||
|
||||
use super::{PyBorrowError, PyBorrowMutError};
|
||||
|
||||
pub trait PyClassMutability {
|
||||
// The storage for this inheritance layer. Only the first mutable class in
|
||||
// an inheritance hierarchy needs to store the borrow flag.
|
||||
type Storage: PyClassBorrowChecker;
|
||||
// The borrow flag needed to implement this class' mutability. Empty until
|
||||
// the first mutable class, at which point it is BorrowChecker and will be
|
||||
// for all subclasses.
|
||||
type Checker: PyClassBorrowChecker;
|
||||
type ImmutableChild: PyClassMutability;
|
||||
type MutableChild: PyClassMutability;
|
||||
}
|
||||
|
||||
pub struct ImmutableClass(());
|
||||
pub struct MutableClass(());
|
||||
pub struct ExtendsMutableAncestor<M: PyClassMutability>(PhantomData<M>);
|
||||
|
||||
impl PyClassMutability for ImmutableClass {
|
||||
type Storage = EmptySlot;
|
||||
type Checker = EmptySlot;
|
||||
type ImmutableChild = ImmutableClass;
|
||||
type MutableChild = MutableClass;
|
||||
}
|
||||
|
||||
impl PyClassMutability for MutableClass {
|
||||
type Storage = BorrowChecker;
|
||||
type Checker = BorrowChecker;
|
||||
type ImmutableChild = ExtendsMutableAncestor<ImmutableClass>;
|
||||
type MutableChild = ExtendsMutableAncestor<MutableClass>;
|
||||
}
|
||||
|
||||
impl<M: PyClassMutability> PyClassMutability for ExtendsMutableAncestor<M> {
|
||||
type Storage = EmptySlot;
|
||||
type Checker = BorrowChecker;
|
||||
type ImmutableChild = ExtendsMutableAncestor<ImmutableClass>;
|
||||
type MutableChild = ExtendsMutableAncestor<MutableClass>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BorrowFlag(AtomicUsize);
|
||||
|
||||
impl BorrowFlag {
|
||||
pub(crate) const UNUSED: usize = 0;
|
||||
const HAS_MUTABLE_BORROW: usize = usize::MAX;
|
||||
fn increment(&self) -> Result<(), PyBorrowError> {
|
||||
// relaxed is OK because we will read the value again in the compare_exchange
|
||||
let mut value = self.0.load(Ordering::Relaxed);
|
||||
loop {
|
||||
if value == BorrowFlag::HAS_MUTABLE_BORROW {
|
||||
return Err(PyBorrowError { _private: () });
|
||||
}
|
||||
match self.0.compare_exchange(
|
||||
// only increment if the value hasn't changed since the
|
||||
// last atomic load
|
||||
value,
|
||||
value + 1,
|
||||
// reading the value is happens-after a previous write
|
||||
// writing the new value is happens-after the previous read
|
||||
Ordering::AcqRel,
|
||||
// relaxed is OK here because we're going to try to read again
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(..) => {
|
||||
break Ok(());
|
||||
}
|
||||
Err(changed_value) => {
|
||||
// value changed under us, need to try again
|
||||
value = changed_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn decrement(&self) {
|
||||
// relaxed load is OK but decrements must happen-before the next read
|
||||
self.0.fetch_sub(1, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmptySlot(());
|
||||
pub struct BorrowChecker(BorrowFlag);
|
||||
|
||||
pub trait PyClassBorrowChecker {
|
||||
/// Initial value for self
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Increments immutable borrow count, if possible
|
||||
fn try_borrow(&self) -> Result<(), PyBorrowError>;
|
||||
|
||||
/// Decrements immutable borrow count
|
||||
fn release_borrow(&self);
|
||||
/// Increments mutable borrow count, if possible
|
||||
fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError>;
|
||||
/// Decremements mutable borrow count
|
||||
fn release_borrow_mut(&self);
|
||||
}
|
||||
|
||||
impl PyClassBorrowChecker for EmptySlot {
|
||||
#[inline]
|
||||
fn new() -> Self {
|
||||
EmptySlot(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_borrow(&self) -> Result<(), PyBorrowError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn release_borrow(&self) {}
|
||||
|
||||
#[inline]
|
||||
fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn release_borrow_mut(&self) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl PyClassBorrowChecker for BorrowChecker {
|
||||
#[inline]
|
||||
fn new() -> Self {
|
||||
Self(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED)))
|
||||
}
|
||||
|
||||
fn try_borrow(&self) -> Result<(), PyBorrowError> {
|
||||
self.0.increment()
|
||||
}
|
||||
|
||||
fn release_borrow(&self) {
|
||||
self.0.decrement();
|
||||
}
|
||||
|
||||
fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError> {
|
||||
let flag = &self.0;
|
||||
match flag.0.compare_exchange(
|
||||
// only allowed to transition to mutable borrow if the reference is
|
||||
// currently unused
|
||||
BorrowFlag::UNUSED,
|
||||
BorrowFlag::HAS_MUTABLE_BORROW,
|
||||
// On success, reading the flag and updating its state are an atomic
|
||||
// operation
|
||||
Ordering::AcqRel,
|
||||
// It doesn't matter precisely when the failure gets turned
|
||||
// into an error
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(..) => Ok(()),
|
||||
Err(..) => Err(PyBorrowMutError { _private: () }),
|
||||
}
|
||||
}
|
||||
|
||||
fn release_borrow_mut(&self) {
|
||||
self.0 .0.store(BorrowFlag::UNUSED, Ordering::Release)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GetBorrowChecker<T: PyClassImpl> {
|
||||
fn borrow_checker(
|
||||
class_object: &T::Layout,
|
||||
) -> &<T::PyClassMutability as PyClassMutability>::Checker;
|
||||
}
|
||||
|
||||
impl<T: PyClassImpl<PyClassMutability = Self>> GetBorrowChecker<T> for MutableClass {
|
||||
fn borrow_checker(class_object: &T::Layout) -> &BorrowChecker {
|
||||
&class_object.contents().borrow_checker
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClassImpl<PyClassMutability = Self>> GetBorrowChecker<T> for ImmutableClass {
|
||||
fn borrow_checker(class_object: &T::Layout) -> &EmptySlot {
|
||||
&class_object.contents().borrow_checker
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClassImpl<PyClassMutability = Self>, M: PyClassMutability> GetBorrowChecker<T>
|
||||
for ExtendsMutableAncestor<M>
|
||||
where
|
||||
T::BaseType: PyClassImpl + PyClassBaseType<LayoutAsBase = <T::BaseType as PyClassImpl>::Layout>,
|
||||
<T::BaseType as PyClassImpl>::PyClassMutability: PyClassMutability<Checker = BorrowChecker>,
|
||||
{
|
||||
fn borrow_checker(class_object: &T::Layout) -> &BorrowChecker {
|
||||
<<T::BaseType as PyClassImpl>::PyClassMutability as GetBorrowChecker<T::BaseType>>::borrow_checker(class_object.ob_base())
|
||||
}
|
||||
}
|
||||
|
||||
/// Base layout of PyClassObject with a known sized base type.
|
||||
/// Corresponds to [PyObject](https://docs.python.org/3/c-api/structures.html#c.PyObject) from the C API.
|
||||
#[doc(hidden)]
|
||||
#[repr(C)]
|
||||
pub struct PyClassObjectBase<T> {
|
||||
ob_base: T,
|
||||
}
|
||||
|
||||
unsafe impl<T, U> PyLayout<T> for PyClassObjectBase<U> where U: PySizedLayout<T> {}
|
||||
|
||||
impl<T, U> PyClassObjectBaseLayout<T> for PyClassObjectBase<U>
|
||||
where
|
||||
U: PySizedLayout<T>,
|
||||
T: PyTypeInfo,
|
||||
{
|
||||
fn ensure_threadsafe(&self) {}
|
||||
fn check_threadsafe(&self) -> Result<(), PyBorrowError> {
|
||||
Ok(())
|
||||
}
|
||||
unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) {
|
||||
unsafe { tp_dealloc(slf, &T::type_object(py)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Base layout of PyClassObject with an unknown sized base type.
|
||||
/// Corresponds to [PyVarObject](https://docs.python.org/3/c-api/structures.html#c.PyVarObject) from the C API.
|
||||
#[doc(hidden)]
|
||||
#[repr(C)]
|
||||
pub struct PyVariableClassObjectBase {
|
||||
ob_base: ffi::PyVarObject,
|
||||
}
|
||||
|
||||
unsafe impl<T> PyLayout<T> for PyVariableClassObjectBase {}
|
||||
|
||||
impl<T: PyTypeInfo> PyClassObjectBaseLayout<T> for PyVariableClassObjectBase {
|
||||
fn ensure_threadsafe(&self) {}
|
||||
fn check_threadsafe(&self) -> Result<(), PyBorrowError> {
|
||||
Ok(())
|
||||
}
|
||||
unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) {
|
||||
unsafe { tp_dealloc(slf, &T::type_object(py)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of tp_dealloc.
|
||||
/// # Safety
|
||||
/// - `slf` must be a valid pointer to an instance of the type at `type_obj` or a subclass.
|
||||
/// - `slf` must not be used after this call (as it will be freed).
|
||||
unsafe fn tp_dealloc(slf: *mut ffi::PyObject, type_obj: &crate::Bound<'_, PyType>) {
|
||||
let py = type_obj.py();
|
||||
unsafe {
|
||||
// FIXME: there is potentially subtle issues here if the base is overwritten
|
||||
// at runtime? To be investigated.
|
||||
let type_ptr = type_obj.as_type_ptr();
|
||||
let actual_type_ptr = ffi::Py_TYPE(slf);
|
||||
|
||||
// For heap types, instances must decref the type object when they
|
||||
// are deallocated, so we create a bound from a borrowed pointer as
|
||||
// as if it was an owned pointer. In this way, when the bound is dropped,
|
||||
// it will decref the type object.
|
||||
debug_assert!(ffi::PyType_HasFeature(actual_type_ptr, ffi::Py_TPFLAGS_HEAPTYPE) != 0);
|
||||
let actual_type = cfg_select! {
|
||||
not(PyPy) => crate::Bound::from_owned_ptr(py, actual_type_ptr as *mut ffi::PyObject)
|
||||
.cast_into_unchecked::<PyType>(),
|
||||
// See https://github.com/pypy/pypy/issues/5555 - it seems that PyPy does not
|
||||
// support the CPython semantics properly, so we avoid taking ownership of the
|
||||
// type object on PyPy.
|
||||
//
|
||||
// TODO: If the PyPy bug is fixed we should remove this workaround and just create
|
||||
// a `Bound` as above.
|
||||
PyPy => crate::Borrowed::from_ptr(py, actual_type_ptr as *mut ffi::PyObject)
|
||||
.cast_unchecked::<PyType>(),
|
||||
};
|
||||
|
||||
// For `#[pyclass]` types which inherit from PyAny, we can just call tp_free
|
||||
#[cfg(not(RustPython))]
|
||||
let base_object_type_ptr = &raw const ffi::PyBaseObject_Type;
|
||||
#[cfg(RustPython)]
|
||||
let base_object_type_ptr = {
|
||||
static TYPE: PyOnceLock<crate::Py<PyType>> = PyOnceLock::new();
|
||||
TYPE.import(py, "builtins", "object").unwrap().as_type_ptr()
|
||||
};
|
||||
|
||||
if core::ptr::eq(type_ptr, base_object_type_ptr) {
|
||||
let tp_free = actual_type
|
||||
.get_slot(TP_FREE)
|
||||
.expect("PyBaseObject_Type should have tp_free");
|
||||
tp_free(slf.cast());
|
||||
}
|
||||
// More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc.
|
||||
// FIXME: should this be using actual_type.tp_dealloc?
|
||||
else if let Some(dealloc) = type_obj.get_slot(TP_DEALLOC) {
|
||||
// Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which
|
||||
// assumes the exception is currently GC tracked, so we have to re-track
|
||||
// before calling the dealloc so that it can safely call Py_GC_UNTRACK.
|
||||
#[cfg(not(any(Py_3_11, PyPy)))]
|
||||
if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 {
|
||||
ffi::PyObject_GC_Track(slf.cast());
|
||||
}
|
||||
dealloc(slf);
|
||||
} else {
|
||||
type_obj.get_slot(TP_FREE).expect("type missing tp_free")(slf.cast());
|
||||
}
|
||||
|
||||
// Cause the reference to the type to be decrefed for heap types, which
|
||||
// is necessary to avoid a reference leak.
|
||||
#[cfg_attr(
|
||||
PyPy,
|
||||
expect(dropping_copy_types, reason = "see PyPy workaround for decref above")
|
||||
)]
|
||||
drop(actual_type);
|
||||
}
|
||||
}
|
||||
|
||||
/// functionality common to all PyObjects regardless of the layout
|
||||
#[doc(hidden)]
|
||||
pub trait PyClassObjectBaseLayout<T>: PyLayout<T> {
|
||||
fn ensure_threadsafe(&self);
|
||||
fn check_threadsafe(&self) -> Result<(), PyBorrowError>;
|
||||
/// Implementation of tp_dealloc.
|
||||
/// # Safety
|
||||
/// - slf must be a valid pointer to an instance of a T or a subclass.
|
||||
/// - slf must not be used after this call (as it will be freed).
|
||||
unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject);
|
||||
}
|
||||
|
||||
/// Functionality required for creating and managing the memory associated with a pyclass annotated struct.
|
||||
#[doc(hidden)]
|
||||
#[diagnostic::on_unimplemented(
|
||||
message = "the class layout is not valid",
|
||||
label = "required for `#[pyclass(extends=...)]`",
|
||||
note = "the python version being built against influences which layouts are valid"
|
||||
)]
|
||||
pub trait PyClassObjectLayout<T: PyClassImpl>: PyClassObjectBaseLayout<T> {
|
||||
/// Gets the offset of the contents from the start of the struct in bytes.
|
||||
const CONTENTS_OFFSET: PyObjectOffset;
|
||||
|
||||
/// Used to set `PyType_Spec::basicsize`
|
||||
/// ([docs](https://docs.python.org/3/c-api/type.html#c.PyType_Spec.basicsize))
|
||||
const BASIC_SIZE: ffi::Py_ssize_t;
|
||||
|
||||
/// Gets the offset of the dictionary from the start of the struct in bytes.
|
||||
const DICT_OFFSET: PyObjectOffset;
|
||||
|
||||
/// Gets the offset of the weakref list from the start of the struct in bytes.
|
||||
const WEAKLIST_OFFSET: PyObjectOffset;
|
||||
|
||||
/// Obtain a pointer to the contents of an uninitialized PyObject of this type.
|
||||
///
|
||||
/// SAFETY: `obj` must have the layout that the implementation is expecting
|
||||
unsafe fn contents_uninit(
|
||||
obj: *mut ffi::PyObject,
|
||||
) -> *mut MaybeUninit<PyClassObjectContents<T>>;
|
||||
|
||||
/// Obtain a reference to the structure that contains the pyclass struct and associated metadata.
|
||||
fn contents(&self) -> &PyClassObjectContents<T>;
|
||||
|
||||
/// Obtain a mutable reference to the structure that contains the pyclass struct and associated metadata.
|
||||
fn contents_mut(&mut self) -> &mut PyClassObjectContents<T>;
|
||||
|
||||
/// Obtain a pointer to the pyclass struct.
|
||||
fn get_ptr(&self) -> *mut T;
|
||||
|
||||
/// obtain a reference to the data at the start of the PyObject.
|
||||
fn ob_base(&self) -> &<T::BaseType as PyClassBaseType>::LayoutAsBase;
|
||||
|
||||
fn borrow_checker(&self) -> &<T::PyClassMutability as PyClassMutability>::Checker;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct PyClassObjectContents<T: PyClassImpl> {
|
||||
pub(crate) value: ManuallyDrop<UnsafeCell<T>>,
|
||||
pub(crate) borrow_checker: <T::PyClassMutability as PyClassMutability>::Storage,
|
||||
pub(crate) thread_checker: T::ThreadChecker,
|
||||
pub(crate) dict: T::Dict,
|
||||
pub(crate) weakref: T::WeakRef,
|
||||
}
|
||||
|
||||
impl<T: PyClassImpl> PyClassObjectContents<T> {
|
||||
pub(crate) fn new(init: T) -> Self {
|
||||
PyClassObjectContents {
|
||||
value: ManuallyDrop::new(UnsafeCell::new(init)),
|
||||
borrow_checker: <T::PyClassMutability as PyClassMutability>::Storage::new(),
|
||||
thread_checker: T::ThreadChecker::new(),
|
||||
dict: T::Dict::INIT,
|
||||
weakref: T::WeakRef::INIT,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn dealloc(&mut self, py: Python<'_>, py_object: *mut ffi::PyObject) {
|
||||
if self.thread_checker.can_drop(py) {
|
||||
unsafe { ManuallyDrop::drop(&mut self.value) };
|
||||
}
|
||||
self.dict.release_dict(py);
|
||||
unsafe { self.weakref.clear_weakrefs(py_object, py) };
|
||||
}
|
||||
}
|
||||
|
||||
/// The layout of a PyClassObject with a known sized base class.
|
||||
#[repr(C)]
|
||||
pub struct PyStaticClassObject<T: PyClassImpl> {
|
||||
ob_base: <T::BaseType as PyClassBaseType>::LayoutAsBase,
|
||||
contents: PyClassObjectContents<T>,
|
||||
}
|
||||
|
||||
impl<T: PyClassImpl<Layout = Self>> PyClassObjectLayout<T> for PyStaticClassObject<T> {
|
||||
/// Gets the offset of the contents from the start of the struct in bytes.
|
||||
const CONTENTS_OFFSET: PyObjectOffset = {
|
||||
let offset = offset_of!(Self, contents);
|
||||
// Py_ssize_t may not be equal to isize on all platforms
|
||||
assert!(offset <= ffi::Py_ssize_t::MAX as usize);
|
||||
PyObjectOffset::Absolute(offset as ffi::Py_ssize_t)
|
||||
};
|
||||
|
||||
const BASIC_SIZE: ffi::Py_ssize_t = {
|
||||
let size = core::mem::size_of::<Self>();
|
||||
assert!(size <= ffi::Py_ssize_t::MAX as usize);
|
||||
size as _
|
||||
};
|
||||
|
||||
const DICT_OFFSET: PyObjectOffset = {
|
||||
let offset = offset_of!(PyStaticClassObject<T>, contents)
|
||||
+ offset_of!(PyClassObjectContents<T>, dict);
|
||||
assert!(offset <= ffi::Py_ssize_t::MAX as usize);
|
||||
PyObjectOffset::Absolute(offset as _)
|
||||
};
|
||||
|
||||
const WEAKLIST_OFFSET: PyObjectOffset = {
|
||||
let offset = offset_of!(PyStaticClassObject<T>, contents)
|
||||
+ offset_of!(PyClassObjectContents<T>, weakref);
|
||||
assert!(offset <= ffi::Py_ssize_t::MAX as usize);
|
||||
PyObjectOffset::Absolute(offset as _)
|
||||
};
|
||||
|
||||
unsafe fn contents_uninit(
|
||||
obj: *mut ffi::PyObject,
|
||||
) -> *mut MaybeUninit<PyClassObjectContents<T>> {
|
||||
#[repr(C)]
|
||||
struct PartiallyInitializedClassObject<T: PyClassImpl> {
|
||||
_ob_base: <T::BaseType as PyClassBaseType>::LayoutAsBase,
|
||||
contents: MaybeUninit<PyClassObjectContents<T>>,
|
||||
}
|
||||
let obj = obj.cast::<PartiallyInitializedClassObject<T>>();
|
||||
unsafe { &raw mut (*obj).contents }
|
||||
}
|
||||
|
||||
fn contents(&self) -> &PyClassObjectContents<T> {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
fn contents_mut(&mut self) -> &mut PyClassObjectContents<T> {
|
||||
&mut self.contents
|
||||
}
|
||||
|
||||
fn get_ptr(&self) -> *mut T {
|
||||
self.contents.value.get()
|
||||
}
|
||||
|
||||
fn ob_base(&self) -> &<T::BaseType as PyClassBaseType>::LayoutAsBase {
|
||||
&self.ob_base
|
||||
}
|
||||
|
||||
fn borrow_checker(&self) -> &<T::PyClassMutability as PyClassMutability>::Checker {
|
||||
T::PyClassMutability::borrow_checker(self)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: PyClassImpl> PyLayout<T> for PyStaticClassObject<T> {}
|
||||
impl<T: PyClass> PySizedLayout<T> for PyStaticClassObject<T> {}
|
||||
|
||||
impl<T: PyClassImpl<Layout = Self>> PyClassObjectBaseLayout<T> for PyStaticClassObject<T>
|
||||
where
|
||||
<T::BaseType as PyClassBaseType>::LayoutAsBase: PyClassObjectBaseLayout<T::BaseType>,
|
||||
{
|
||||
fn ensure_threadsafe(&self) {
|
||||
self.contents.thread_checker.ensure();
|
||||
self.ob_base.ensure_threadsafe();
|
||||
}
|
||||
fn check_threadsafe(&self) -> Result<(), PyBorrowError> {
|
||||
if !self.contents.thread_checker.check() {
|
||||
return Err(PyBorrowError { _private: () });
|
||||
}
|
||||
self.ob_base.check_threadsafe()
|
||||
}
|
||||
unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) {
|
||||
// Safety: Python only calls tp_dealloc when no references to the object remain.
|
||||
let class_object = unsafe { &mut *(slf.cast::<T::Layout>()) };
|
||||
unsafe { class_object.contents_mut().dealloc(py, slf) };
|
||||
unsafe { <T::BaseType as PyClassBaseType>::LayoutAsBase::tp_dealloc(py, slf) }
|
||||
}
|
||||
}
|
||||
|
||||
/// A layout for a PyClassObject with an unknown sized base type.
|
||||
///
|
||||
/// Utilises [PEP-697](https://peps.python.org/pep-0697/)
|
||||
#[doc(hidden)]
|
||||
#[repr(C)]
|
||||
pub struct PyVariableClassObject<T: PyClassImpl> {
|
||||
ob_base: <T::BaseType as PyClassBaseType>::LayoutAsBase,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
impl<T: PyClass<Layout = Self>> PyVariableClassObject<T> {
|
||||
/// # Safety
|
||||
/// - `obj` must have the layout that the implementation is expecting
|
||||
/// - thread must be attached to the interpreter
|
||||
unsafe fn get_contents_of_obj(
|
||||
obj: *mut ffi::PyObject,
|
||||
) -> *mut MaybeUninit<PyClassObjectContents<T>> {
|
||||
// TODO: it would be nice to eventually avoid coupling to the PyO3 statics here, maybe using
|
||||
// 3.14's PyType_GetBaseByToken, to support PEP 587 / multiple interpreters better
|
||||
// SAFETY: caller guarantees attached to the interpreter
|
||||
let type_obj = T::type_object_raw(unsafe { Python::assume_attached() });
|
||||
let pointer = unsafe { ffi::PyObject_GetTypeData(obj, type_obj) };
|
||||
pointer.cast()
|
||||
}
|
||||
|
||||
fn get_contents_ptr(&self) -> *mut PyClassObjectContents<T> {
|
||||
unsafe {
|
||||
Self::get_contents_of_obj(self as *const PyVariableClassObject<T> as *mut ffi::PyObject)
|
||||
}
|
||||
.cast()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
impl<T: PyClass<Layout = Self>> PyClassObjectLayout<T> for PyVariableClassObject<T> {
|
||||
/// Gets the offset of the contents from the start of the struct in bytes.
|
||||
const CONTENTS_OFFSET: PyObjectOffset = PyObjectOffset::Relative(0);
|
||||
const BASIC_SIZE: ffi::Py_ssize_t = {
|
||||
let size = core::mem::size_of::<PyClassObjectContents<T>>();
|
||||
assert!(size <= ffi::Py_ssize_t::MAX as usize);
|
||||
// negative to indicate 'extra' space that cpython will allocate for us
|
||||
-(size as ffi::Py_ssize_t)
|
||||
};
|
||||
const DICT_OFFSET: PyObjectOffset = {
|
||||
let offset = offset_of!(PyClassObjectContents<T>, dict);
|
||||
assert!(offset <= ffi::Py_ssize_t::MAX as usize);
|
||||
PyObjectOffset::Relative(offset as _)
|
||||
};
|
||||
const WEAKLIST_OFFSET: PyObjectOffset = {
|
||||
let offset = offset_of!(PyClassObjectContents<T>, weakref);
|
||||
assert!(offset <= ffi::Py_ssize_t::MAX as usize);
|
||||
PyObjectOffset::Relative(offset as _)
|
||||
};
|
||||
|
||||
unsafe fn contents_uninit(
|
||||
obj: *mut ffi::PyObject,
|
||||
) -> *mut MaybeUninit<PyClassObjectContents<T>> {
|
||||
unsafe { Self::get_contents_of_obj(obj) }
|
||||
}
|
||||
|
||||
fn get_ptr(&self) -> *mut T {
|
||||
self.contents().value.get()
|
||||
}
|
||||
|
||||
fn ob_base(&self) -> &<T::BaseType as PyClassBaseType>::LayoutAsBase {
|
||||
&self.ob_base
|
||||
}
|
||||
|
||||
fn contents(&self) -> &PyClassObjectContents<T> {
|
||||
unsafe { self.get_contents_ptr().cast_const().as_ref() }
|
||||
.expect("should be able to cast PyClassObjectContents pointer")
|
||||
}
|
||||
|
||||
fn contents_mut(&mut self) -> &mut PyClassObjectContents<T> {
|
||||
unsafe { self.get_contents_ptr().as_mut() }
|
||||
.expect("should be able to cast PyClassObjectContents pointer")
|
||||
}
|
||||
|
||||
fn borrow_checker(&self) -> &<T::PyClassMutability as PyClassMutability>::Checker {
|
||||
T::PyClassMutability::borrow_checker(self)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: PyClassImpl> PyLayout<T> for PyVariableClassObject<T> {}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
impl<T: PyClass<Layout = Self>> PyClassObjectBaseLayout<T> for PyVariableClassObject<T>
|
||||
where
|
||||
<T::BaseType as PyClassBaseType>::LayoutAsBase: PyClassObjectBaseLayout<T::BaseType>,
|
||||
{
|
||||
fn ensure_threadsafe(&self) {
|
||||
self.contents().thread_checker.ensure();
|
||||
self.ob_base.ensure_threadsafe();
|
||||
}
|
||||
fn check_threadsafe(&self) -> Result<(), PyBorrowError> {
|
||||
if !self.contents().thread_checker.check() {
|
||||
return Err(PyBorrowError { _private: () });
|
||||
}
|
||||
self.ob_base.check_threadsafe()
|
||||
}
|
||||
unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) {
|
||||
// Safety: Python only calls tp_dealloc when no references to the object remain.
|
||||
let class_object = unsafe { &mut *(slf.cast::<T::Layout>()) };
|
||||
unsafe { class_object.contents_mut().dealloc(py, slf) };
|
||||
unsafe { <T::BaseType as PyClassBaseType>::LayoutAsBase::tp_dealloc(py, slf) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "macros")]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::pyclass::boolean_struct::{False, True};
|
||||
|
||||
#[pyclass(crate = "crate", subclass)]
|
||||
struct MutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableBase, subclass)]
|
||||
struct MutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableBase, frozen, subclass)]
|
||||
struct ImmutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableChildOfMutableBase)]
|
||||
struct MutableChildOfMutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableChildOfMutableBase)]
|
||||
struct MutableChildOfImmutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableChildOfMutableBase, frozen)]
|
||||
struct ImmutableChildOfMutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableChildOfMutableBase, frozen)]
|
||||
struct ImmutableChildOfImmutableChildOfMutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", frozen, subclass)]
|
||||
struct ImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableBase, subclass)]
|
||||
struct MutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableBase, frozen, subclass)]
|
||||
struct ImmutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableChildOfImmutableBase)]
|
||||
struct MutableChildOfMutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase)]
|
||||
struct MutableChildOfImmutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = MutableChildOfImmutableBase, frozen)]
|
||||
struct ImmutableChildOfMutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)]
|
||||
struct ImmutableChildOfImmutableChildOfImmutableBase;
|
||||
|
||||
#[pyclass(crate = "crate", subclass)]
|
||||
struct BaseWithoutData;
|
||||
|
||||
#[pyclass(crate = "crate", subclass)]
|
||||
struct BaseWithData(#[allow(unused)] u64);
|
||||
|
||||
#[pyclass(crate = "crate", extends = BaseWithData)]
|
||||
struct ChildWithData(#[allow(unused)] u64);
|
||||
|
||||
#[pyclass(crate = "crate", extends = BaseWithData)]
|
||||
struct ChildWithoutData;
|
||||
|
||||
#[test]
|
||||
fn test_inherited_size() {
|
||||
#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))]
|
||||
type ClassObject<T> = PyVariableClassObject<T>;
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
type ClassObject<T> = PyStaticClassObject<T>;
|
||||
|
||||
let base_without_data_size = ClassObject::<BaseWithoutData>::BASIC_SIZE;
|
||||
let base_with_data_size = ClassObject::<BaseWithData>::BASIC_SIZE;
|
||||
let child_without_data_size = ClassObject::<ChildWithoutData>::BASIC_SIZE;
|
||||
let child_with_data_size = ClassObject::<ChildWithData>::BASIC_SIZE;
|
||||
#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))]
|
||||
{
|
||||
assert!(base_without_data_size < 0); // negative indicates variable sized
|
||||
assert!(base_with_data_size < base_without_data_size);
|
||||
assert_eq!(child_without_data_size, 0);
|
||||
assert_eq!(
|
||||
base_with_data_size - base_without_data_size,
|
||||
child_with_data_size
|
||||
);
|
||||
}
|
||||
#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))]
|
||||
{
|
||||
assert!(base_without_data_size > 0);
|
||||
assert!(base_with_data_size > base_without_data_size);
|
||||
assert_eq!(base_with_data_size, child_without_data_size);
|
||||
assert!(base_with_data_size < child_with_data_size);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_mutable<T: PyClass<Frozen = False, PyClassMutability = MutableClass>>() {}
|
||||
fn assert_immutable<T: PyClass<Frozen = True, PyClassMutability = ImmutableClass>>() {}
|
||||
fn assert_mutable_with_mutable_ancestor<
|
||||
T: PyClass<Frozen = False, PyClassMutability = ExtendsMutableAncestor<MutableClass>>,
|
||||
>() {
|
||||
}
|
||||
fn assert_immutable_with_mutable_ancestor<
|
||||
T: PyClass<Frozen = True, PyClassMutability = ExtendsMutableAncestor<ImmutableClass>>,
|
||||
>() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inherited_mutability() {
|
||||
// mutable base
|
||||
assert_mutable::<MutableBase>();
|
||||
|
||||
// children of mutable base have a mutable ancestor
|
||||
assert_mutable_with_mutable_ancestor::<MutableChildOfMutableBase>();
|
||||
assert_immutable_with_mutable_ancestor::<ImmutableChildOfMutableBase>();
|
||||
|
||||
// grandchildren of mutable base have a mutable ancestor
|
||||
assert_mutable_with_mutable_ancestor::<MutableChildOfMutableChildOfMutableBase>();
|
||||
assert_mutable_with_mutable_ancestor::<MutableChildOfImmutableChildOfMutableBase>();
|
||||
assert_immutable_with_mutable_ancestor::<ImmutableChildOfMutableChildOfMutableBase>();
|
||||
assert_immutable_with_mutable_ancestor::<ImmutableChildOfImmutableChildOfMutableBase>();
|
||||
|
||||
// immutable base and children
|
||||
assert_immutable::<ImmutableBase>();
|
||||
assert_immutable::<ImmutableChildOfImmutableBase>();
|
||||
assert_immutable::<ImmutableChildOfImmutableChildOfImmutableBase>();
|
||||
|
||||
// mutable children of immutable at any level are simply mutable
|
||||
assert_mutable::<MutableChildOfImmutableBase>();
|
||||
assert_mutable::<MutableChildOfImmutableChildOfImmutableBase>();
|
||||
|
||||
// children of the mutable child display this property
|
||||
assert_mutable_with_mutable_ancestor::<MutableChildOfMutableChildOfImmutableBase>();
|
||||
assert_immutable_with_mutable_ancestor::<ImmutableChildOfMutableChildOfImmutableBase>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mutable_borrow_prevents_further_borrows() {
|
||||
Python::attach(|py| {
|
||||
let mmm = Py::new(
|
||||
py,
|
||||
PyClassInitializer::from(MutableBase)
|
||||
.add_subclass(MutableChildOfMutableBase)
|
||||
.add_subclass(MutableChildOfMutableChildOfMutableBase),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py);
|
||||
|
||||
let mmm_refmut = mmm_bound.borrow_mut();
|
||||
|
||||
// Cannot take any other mutable or immutable borrows whilst the object is borrowed mutably
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound.extract::<PyRef<'_, MutableBase>>().is_err());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound.extract::<PyRefMut<'_, MutableBase>>().is_err());
|
||||
|
||||
// With the borrow dropped, all other borrow attempts will succeed
|
||||
drop(mmm_refmut);
|
||||
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound.extract::<PyRef<'_, MutableBase>>().is_ok());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound.extract::<PyRefMut<'_, MutableBase>>().is_ok());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_immutable_borrows_prevent_mutable_borrows() {
|
||||
Python::attach(|py| {
|
||||
let mmm = Py::new(
|
||||
py,
|
||||
PyClassInitializer::from(MutableBase)
|
||||
.add_subclass(MutableChildOfMutableBase)
|
||||
.add_subclass(MutableChildOfMutableChildOfMutableBase),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py);
|
||||
|
||||
let mmm_refmut = mmm_bound.borrow();
|
||||
|
||||
// Further immutable borrows are ok
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRef<'_, MutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound.extract::<PyRef<'_, MutableBase>>().is_ok());
|
||||
|
||||
// Further mutable borrows are not ok
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableBase>>()
|
||||
.is_err());
|
||||
assert!(mmm_bound.extract::<PyRefMut<'_, MutableBase>>().is_err());
|
||||
|
||||
// With the borrow dropped, all mutable borrow attempts will succeed
|
||||
drop(mmm_refmut);
|
||||
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound
|
||||
.extract::<PyRefMut<'_, MutableChildOfMutableBase>>()
|
||||
.is_ok());
|
||||
assert!(mmm_bound.extract::<PyRefMut<'_, MutableBase>>().is_ok());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn test_thread_safety() {
|
||||
#[crate::pyclass(crate = "crate")]
|
||||
struct MyClass {
|
||||
x: u64,
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, MyClass { x: 0 }).unwrap();
|
||||
|
||||
let total_modifications = py.detach(|| {
|
||||
std::thread::scope(|s| {
|
||||
// Spawn a bunch of threads all racing to write to
|
||||
// the same instance of `MyClass`.
|
||||
let threads = (0..10)
|
||||
.map(|_| {
|
||||
s.spawn(|| {
|
||||
Python::attach(|py| {
|
||||
// Each thread records its own view of how many writes it made
|
||||
let mut local_modifications = 0;
|
||||
for _ in 0..100 {
|
||||
if let Ok(mut i) = inst.try_borrow_mut(py) {
|
||||
i.x += 1;
|
||||
local_modifications += 1;
|
||||
}
|
||||
}
|
||||
local_modifications
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sum up the total number of writes made by all threads
|
||||
threads.into_iter().map(|t| t.join().unwrap()).sum::<u64>()
|
||||
})
|
||||
});
|
||||
|
||||
// If the implementation is free of data races, the total number of writes
|
||||
// should match the final value of `x`.
|
||||
assert_eq!(total_modifications, inst.borrow(py).x);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn test_thread_safety_2() {
|
||||
struct SyncUnsafeCell<T>(UnsafeCell<T>);
|
||||
unsafe impl<T> Sync for SyncUnsafeCell<T> {}
|
||||
|
||||
impl<T> SyncUnsafeCell<T> {
|
||||
fn get(&self) -> *mut T {
|
||||
self.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
let data = SyncUnsafeCell(UnsafeCell::new(0));
|
||||
let data2 = SyncUnsafeCell(UnsafeCell::new(0));
|
||||
let borrow_checker = BorrowChecker(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED)));
|
||||
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(|| {
|
||||
for _ in 0..1_000_000 {
|
||||
if borrow_checker.try_borrow_mut().is_ok() {
|
||||
// thread 1 writes to both values during the mutable borrow
|
||||
unsafe { *data.get() += 1 };
|
||||
unsafe { *data2.get() += 1 };
|
||||
borrow_checker.release_borrow_mut();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
s.spawn(|| {
|
||||
for _ in 0..1_000_000 {
|
||||
if borrow_checker.try_borrow().is_ok() {
|
||||
// if the borrow checker is working correctly, it should be impossible
|
||||
// for thread 2 to observe a difference in the two values
|
||||
assert_eq!(unsafe { *data.get() }, unsafe { *data2.get() });
|
||||
borrow_checker.release_borrow();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+141
@@ -0,0 +1,141 @@
|
||||
//! `PyClass` and related traits.
|
||||
use crate::{ffi, impl_::pyclass::PyClassImpl, PyTypeInfo};
|
||||
use core::{cmp::Ordering, ffi::c_int};
|
||||
|
||||
mod create_type_object;
|
||||
mod gc;
|
||||
mod guard;
|
||||
|
||||
pub(crate) use self::create_type_object::{create_type_object, PyClassTypeObject};
|
||||
|
||||
pub use self::gc::{PyTraverseError, PyVisit};
|
||||
pub use self::guard::{
|
||||
PyClassGuard, PyClassGuardError, PyClassGuardMap, PyClassGuardMut, PyClassGuardMutError,
|
||||
PyClassGuardMutSuper,
|
||||
};
|
||||
|
||||
/// Types that can be used as Python classes.
|
||||
///
|
||||
/// The `#[pyclass]` attribute implements this trait for your Rust struct -
|
||||
/// you shouldn't implement this trait directly.
|
||||
pub trait PyClass: PyTypeInfo + PyClassImpl {
|
||||
/// Name of the class.
|
||||
///
|
||||
/// This can be set via `#[pyclass(name = "...")]`, otherwise it defaults to the Rust type name.
|
||||
const NAME: &'static str;
|
||||
|
||||
/// Whether the pyclass is frozen.
|
||||
///
|
||||
/// This can be enabled via `#[pyclass(frozen)]`.
|
||||
type Frozen: Frozen;
|
||||
}
|
||||
|
||||
/// Operators for the `__richcmp__` method
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CompareOp {
|
||||
/// The *less than* operator.
|
||||
Lt = ffi::Py_LT as isize,
|
||||
/// The *less than or equal to* operator.
|
||||
Le = ffi::Py_LE as isize,
|
||||
/// The equality operator.
|
||||
Eq = ffi::Py_EQ as isize,
|
||||
/// The *not equal to* operator.
|
||||
Ne = ffi::Py_NE as isize,
|
||||
/// The *greater than* operator.
|
||||
Gt = ffi::Py_GT as isize,
|
||||
/// The *greater than or equal to* operator.
|
||||
Ge = ffi::Py_GE as isize,
|
||||
}
|
||||
|
||||
impl CompareOp {
|
||||
/// Conversion from the C enum.
|
||||
pub fn from_raw(op: c_int) -> Option<Self> {
|
||||
match op {
|
||||
ffi::Py_LT => Some(CompareOp::Lt),
|
||||
ffi::Py_LE => Some(CompareOp::Le),
|
||||
ffi::Py_EQ => Some(CompareOp::Eq),
|
||||
ffi::Py_NE => Some(CompareOp::Ne),
|
||||
ffi::Py_GT => Some(CompareOp::Gt),
|
||||
ffi::Py_GE => Some(CompareOp::Ge),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if a Rust [`core::cmp::Ordering`] matches this ordering query.
|
||||
///
|
||||
/// Usage example:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pyo3::prelude::*;
|
||||
/// # use pyo3::class::basic::CompareOp;
|
||||
///
|
||||
/// #[pyclass]
|
||||
/// struct Size {
|
||||
/// size: usize,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl Size {
|
||||
/// fn __richcmp__(&self, other: &Size, op: CompareOp) -> bool {
|
||||
/// op.matches(self.size.cmp(&other.size))
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn matches(&self, result: Ordering) -> bool {
|
||||
match self {
|
||||
CompareOp::Eq => result == Ordering::Equal,
|
||||
CompareOp::Ne => result != Ordering::Equal,
|
||||
CompareOp::Lt => result == Ordering::Less,
|
||||
CompareOp::Le => result != Ordering::Greater,
|
||||
CompareOp::Gt => result == Ordering::Greater,
|
||||
CompareOp::Ge => result != Ordering::Less,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A workaround for [associated const equality](https://github.com/rust-lang/rust/issues/92827).
|
||||
///
|
||||
/// This serves to have True / False values in the [`PyClass`] trait's `Frozen` type.
|
||||
#[doc(hidden)]
|
||||
pub mod boolean_struct {
|
||||
pub(crate) mod private {
|
||||
use super::*;
|
||||
|
||||
/// A way to "seal" the boolean traits.
|
||||
pub trait Boolean {
|
||||
const VALUE: bool;
|
||||
}
|
||||
|
||||
impl Boolean for True {
|
||||
const VALUE: bool = true;
|
||||
}
|
||||
impl Boolean for False {
|
||||
const VALUE: bool = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct True(());
|
||||
pub struct False(());
|
||||
}
|
||||
|
||||
/// A trait which is used to describe whether a `#[pyclass]` is frozen.
|
||||
#[doc(hidden)]
|
||||
pub trait Frozen: boolean_struct::private::Boolean {}
|
||||
|
||||
impl Frozen for boolean_struct::True {}
|
||||
impl Frozen for boolean_struct::False {}
|
||||
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_compare_op_matches() {
|
||||
use super::CompareOp;
|
||||
use core::cmp::Ordering;
|
||||
|
||||
assert!(CompareOp::Eq.matches(Ordering::Equal));
|
||||
assert!(CompareOp::Ne.matches(Ordering::Less));
|
||||
assert!(CompareOp::Ge.matches(Ordering::Greater));
|
||||
assert!(CompareOp::Gt.matches(Ordering::Greater));
|
||||
assert!(CompareOp::Le.matches(Ordering::Equal));
|
||||
assert!(CompareOp::Lt.matches(Ordering::Less));
|
||||
}
|
||||
}
|
||||
+831
@@ -0,0 +1,831 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use crate::exceptions::PyAttributeError;
|
||||
use crate::impl_::pymethods::{Deleter, PyDeleterDef};
|
||||
use crate::platform::HashMap;
|
||||
#[cfg(not(Py_3_10))]
|
||||
use crate::types::typeobject::PyTypeMethods;
|
||||
use crate::{
|
||||
exceptions::PyTypeError,
|
||||
ffi,
|
||||
ffi_ptr_ext::FfiPtrExt,
|
||||
impl_::{
|
||||
pyclass::{
|
||||
assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc,
|
||||
tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset,
|
||||
},
|
||||
pymethods::{
|
||||
synthesized_clear, synthesized_traverse, Getter, PyGetterDef, PyMethodDefType,
|
||||
PySetterDef, Setter,
|
||||
},
|
||||
trampoline::trampoline,
|
||||
},
|
||||
pycell::impl_::PyClassObjectLayout,
|
||||
types::PyType,
|
||||
Py, PyClass, PyResult, PyTypeInfo, Python,
|
||||
};
|
||||
use core::{
|
||||
ffi::CStr,
|
||||
ffi::{c_char, c_int, c_ulong, c_void},
|
||||
ptr::{self, NonNull},
|
||||
};
|
||||
use std::ffi::CString;
|
||||
|
||||
pub(crate) struct PyClassTypeObject {
|
||||
pub type_object: Py<PyType>,
|
||||
pub is_immutable_type: bool,
|
||||
#[expect(
|
||||
dead_code,
|
||||
reason = "this is just storage that must live as long as the type object"
|
||||
)]
|
||||
getset_defs: Vec<GetSetDefType>,
|
||||
}
|
||||
|
||||
pub(crate) fn create_type_object<T>(py: Python<'_>) -> PyResult<PyClassTypeObject>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
// Written this way to monomorphize the majority of the logic.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
unsafe fn inner(
|
||||
py: Python<'_>,
|
||||
base: *mut ffi::PyTypeObject,
|
||||
dealloc: unsafe extern "C" fn(*mut ffi::PyObject),
|
||||
dealloc_with_gc: unsafe extern "C" fn(*mut ffi::PyObject),
|
||||
synthesized_traverse: ffi::traverseproc,
|
||||
synthesized_clear: ffi::inquiry,
|
||||
is_mapping: bool,
|
||||
is_sequence: bool,
|
||||
is_immutable_type: bool,
|
||||
doc: &'static CStr,
|
||||
dict_offset: Option<PyObjectOffset>,
|
||||
weaklist_offset: Option<PyObjectOffset>,
|
||||
is_basetype: bool,
|
||||
items_iter: PyClassItemsIter,
|
||||
name: &'static str,
|
||||
module: Option<&'static str>,
|
||||
basicsize: ffi::Py_ssize_t,
|
||||
) -> PyResult<PyClassTypeObject> {
|
||||
unsafe {
|
||||
PyTypeBuilder {
|
||||
slots: Vec::new(),
|
||||
method_defs: Vec::new(),
|
||||
member_defs: Vec::new(),
|
||||
getset_builders: HashMap::new(),
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
|
||||
cleanup: Vec::new(),
|
||||
tp_base: base,
|
||||
tp_dealloc: dealloc,
|
||||
tp_dealloc_with_gc: dealloc_with_gc,
|
||||
synthesized_traverse,
|
||||
synthesized_clear,
|
||||
is_mapping,
|
||||
is_sequence,
|
||||
is_immutable_type,
|
||||
has_new: false,
|
||||
has_dealloc: false,
|
||||
has_getitem: false,
|
||||
has_setitem: false,
|
||||
has_traverse: false,
|
||||
has_clear: false,
|
||||
dict_offset: None,
|
||||
class_flags: 0,
|
||||
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
|
||||
buffer_procs: Default::default(),
|
||||
}
|
||||
.type_doc(doc)
|
||||
.offsets(dict_offset, weaklist_offset)
|
||||
.set_is_basetype(is_basetype)
|
||||
.class_items(items_iter)
|
||||
.build(py, name, module, basicsize)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
inner(
|
||||
py,
|
||||
T::BaseType::type_object_raw(py),
|
||||
tp_dealloc::<T>,
|
||||
tp_dealloc_with_gc::<T>,
|
||||
synthesized_traverse::<T>,
|
||||
synthesized_clear::<T>,
|
||||
T::IS_MAPPING,
|
||||
T::IS_SEQUENCE,
|
||||
T::IS_IMMUTABLE_TYPE,
|
||||
T::DOC,
|
||||
T::dict_offset(),
|
||||
T::weaklist_offset(),
|
||||
T::IS_BASETYPE,
|
||||
T::items_iter(),
|
||||
<T as PyClass>::NAME,
|
||||
<T as PyClassImpl>::MODULE,
|
||||
<T as PyClassImpl>::Layout::BASIC_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
|
||||
type PyTypeBuilderCleanup = Box<dyn Fn(&PyTypeBuilder, *mut ffi::PyTypeObject)>;
|
||||
|
||||
struct PyTypeBuilder {
|
||||
slots: Vec<ffi::PyType_Slot>,
|
||||
method_defs: Vec<ffi::PyMethodDef>,
|
||||
member_defs: Vec<ffi::PyMemberDef>,
|
||||
getset_builders: HashMap<&'static CStr, GetSetDefBuilder>,
|
||||
/// Used to patch the type objects for the things there's no
|
||||
/// PyType_FromSpec API for... there's no reason this should work,
|
||||
/// except for that it does and we have tests.
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
|
||||
cleanup: Vec<PyTypeBuilderCleanup>,
|
||||
tp_base: *mut ffi::PyTypeObject,
|
||||
tp_dealloc: ffi::destructor,
|
||||
tp_dealloc_with_gc: ffi::destructor,
|
||||
/// `tp_traverse` / `tp_clear` to install when the class needs the slot but defines no
|
||||
/// `__traverse__` / `__clear__` of its own.
|
||||
synthesized_traverse: ffi::traverseproc,
|
||||
synthesized_clear: ffi::inquiry,
|
||||
is_mapping: bool,
|
||||
is_sequence: bool,
|
||||
is_immutable_type: bool,
|
||||
has_new: bool,
|
||||
has_dealloc: bool,
|
||||
has_getitem: bool,
|
||||
has_setitem: bool,
|
||||
has_traverse: bool,
|
||||
has_clear: bool,
|
||||
dict_offset: Option<PyObjectOffset>,
|
||||
class_flags: c_ulong,
|
||||
// Before Python 3.9, need to patch in buffer methods manually (they don't work in slots)
|
||||
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
|
||||
buffer_procs: ffi::PyBufferProcs,
|
||||
}
|
||||
|
||||
impl PyTypeBuilder {
|
||||
/// # Safety
|
||||
/// The given pointer must be of the correct type for the given slot
|
||||
unsafe fn push_slot<T>(&mut self, slot: c_int, pfunc: *mut T) {
|
||||
match slot {
|
||||
ffi::Py_tp_new => self.has_new = true,
|
||||
ffi::Py_tp_dealloc => self.has_dealloc = true,
|
||||
ffi::Py_mp_subscript => self.has_getitem = true,
|
||||
ffi::Py_mp_ass_subscript => self.has_setitem = true,
|
||||
ffi::Py_tp_traverse => {
|
||||
self.has_traverse = true;
|
||||
self.class_flags |= ffi::Py_TPFLAGS_HAVE_GC;
|
||||
}
|
||||
ffi::Py_tp_clear => self.has_clear = true,
|
||||
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
|
||||
ffi::Py_bf_getbuffer => {
|
||||
// Safety: slot.pfunc is a valid function pointer
|
||||
self.buffer_procs.bf_getbuffer =
|
||||
Some(unsafe { core::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc) });
|
||||
}
|
||||
#[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))]
|
||||
ffi::Py_bf_releasebuffer => {
|
||||
// Safety: slot.pfunc is a valid function pointer
|
||||
self.buffer_procs.bf_releasebuffer =
|
||||
Some(unsafe { core::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc) });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.slots.push(ffi::PyType_Slot {
|
||||
slot,
|
||||
pfunc: pfunc as _,
|
||||
});
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// It is the caller's responsibility that `data` is of the correct type for the given slot.
|
||||
unsafe fn push_raw_vec_slot<T>(&mut self, slot: c_int, mut data: Vec<T>) {
|
||||
if !data.is_empty() {
|
||||
// Python expects a zeroed entry to mark the end of the defs
|
||||
unsafe {
|
||||
data.push(core::mem::zeroed());
|
||||
self.push_slot(slot, Box::into_raw(data.into_boxed_slice()) as *mut c_void);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pymethod_def(&mut self, def: &PyMethodDefType) {
|
||||
match def {
|
||||
PyMethodDefType::Getter(getter) => self
|
||||
.getset_builders
|
||||
.entry(getter.name)
|
||||
.or_default()
|
||||
.add_getter(getter),
|
||||
PyMethodDefType::Setter(setter) => self
|
||||
.getset_builders
|
||||
.entry(setter.name)
|
||||
.or_default()
|
||||
.add_setter(setter),
|
||||
PyMethodDefType::Deleter(deleter) => self
|
||||
.getset_builders
|
||||
.entry(deleter.name)
|
||||
.or_default()
|
||||
.add_deleter(deleter),
|
||||
PyMethodDefType::Method(def) => self.method_defs.push(def.into_raw()),
|
||||
// These class attributes are added after the type gets created by LazyStaticType
|
||||
PyMethodDefType::ClassAttribute(_) => {}
|
||||
PyMethodDefType::StructMember(def) => self.member_defs.push(*def),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_methods_and_properties(&mut self) -> Vec<GetSetDefType> {
|
||||
let method_defs: Vec<pyo3_ffi::PyMethodDef> = core::mem::take(&mut self.method_defs);
|
||||
// Safety: Py_tp_methods expects a raw vec of PyMethodDef
|
||||
unsafe { self.push_raw_vec_slot(ffi::Py_tp_methods, method_defs) };
|
||||
|
||||
let member_defs = core::mem::take(&mut self.member_defs);
|
||||
// Safety: Py_tp_members expects a raw vec of PyMemberDef
|
||||
unsafe { self.push_raw_vec_slot(ffi::Py_tp_members, member_defs) };
|
||||
|
||||
let mut getset_destructors = Vec::with_capacity(self.getset_builders.len());
|
||||
|
||||
#[allow(unused_mut, reason = "not modified on PyPy")]
|
||||
let mut property_defs: Vec<_> = self
|
||||
.getset_builders
|
||||
.iter()
|
||||
.map(|(name, builder)| {
|
||||
let (def, destructor) = builder.as_get_set_def(name);
|
||||
getset_destructors.push(destructor);
|
||||
def
|
||||
})
|
||||
.collect();
|
||||
|
||||
// PyPy automatically adds __dict__ getter / setter.
|
||||
#[cfg(not(PyPy))]
|
||||
// Supported on unlimited API for all versions, and on 3.9+ for limited API
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
if let Some(dict_offset) = self.dict_offset {
|
||||
let get_dict;
|
||||
let closure;
|
||||
// PyObject_GenericGetDict not in the limited API until Python 3.10.
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_10))]
|
||||
{
|
||||
let _ = dict_offset;
|
||||
get_dict = ffi::PyObject_GenericGetDict;
|
||||
closure = ptr::null_mut();
|
||||
}
|
||||
|
||||
// ... so we write a basic implementation ourselves
|
||||
#[cfg(not(any(not(Py_LIMITED_API), Py_3_10)))]
|
||||
{
|
||||
extern "C" fn get_dict_impl(
|
||||
object: *mut ffi::PyObject,
|
||||
closure: *mut c_void,
|
||||
) -> *mut ffi::PyObject {
|
||||
unsafe {
|
||||
trampoline(|_| {
|
||||
let dict_offset = closure as ffi::Py_ssize_t;
|
||||
// we don't support negative dict_offset here; PyO3 doesn't set it negative
|
||||
assert!(dict_offset > 0);
|
||||
let dict_ptr =
|
||||
object.byte_offset(dict_offset).cast::<*mut ffi::PyObject>();
|
||||
if (*dict_ptr).is_null() {
|
||||
core::ptr::write(dict_ptr, ffi::PyDict_New());
|
||||
}
|
||||
Ok(ffi::compat::Py_XNewRef(*dict_ptr))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get_dict = get_dict_impl;
|
||||
let PyObjectOffset::Absolute(offset) = dict_offset;
|
||||
closure = offset as _;
|
||||
}
|
||||
|
||||
property_defs.push(ffi::PyGetSetDef {
|
||||
name: c"__dict__".as_ptr(),
|
||||
get: Some(get_dict),
|
||||
set: Some(ffi::PyObject_GenericSetDict),
|
||||
doc: ptr::null(),
|
||||
closure,
|
||||
});
|
||||
}
|
||||
|
||||
// Safety: Py_tp_getset expects a raw vec of PyGetSetDef
|
||||
unsafe { self.push_raw_vec_slot(ffi::Py_tp_getset, property_defs) };
|
||||
|
||||
// If mapping methods implemented, define sequence methods get implemented too.
|
||||
// CPython does the same for Python `class` statements.
|
||||
|
||||
// NB we don't implement sq_length to avoid annoying CPython behaviour of automatically adding
|
||||
// the length to negative indices.
|
||||
|
||||
// Don't add these methods for "pure" mappings.
|
||||
|
||||
if !self.is_mapping && self.has_getitem {
|
||||
// Safety: This is the correct slot type for Py_sq_item
|
||||
unsafe {
|
||||
self.push_slot(
|
||||
ffi::Py_sq_item,
|
||||
get_sequence_item_from_mapping as *mut c_void,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !self.is_mapping && self.has_setitem {
|
||||
// Safety: This is the correct slot type for Py_sq_ass_item
|
||||
unsafe {
|
||||
self.push_slot(
|
||||
ffi::Py_sq_ass_item,
|
||||
assign_sequence_item_from_mapping as *mut c_void,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getset_destructors
|
||||
}
|
||||
|
||||
fn set_is_basetype(mut self, is_basetype: bool) -> Self {
|
||||
if is_basetype {
|
||||
self.class_flags |= ffi::Py_TPFLAGS_BASETYPE;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// All slots in the PyClassItemsIter should be correct
|
||||
unsafe fn class_items(mut self, iter: PyClassItemsIter) -> Self {
|
||||
for items in iter {
|
||||
for slot in items.slots {
|
||||
unsafe { self.push_slot(slot.slot, slot.pfunc) };
|
||||
}
|
||||
for method in items.methods {
|
||||
self.pymethod_def(method);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn type_doc(mut self, type_doc: &'static CStr) -> Self {
|
||||
if !type_doc.is_empty() {
|
||||
unsafe { self.push_slot(ffi::Py_tp_doc, type_doc.as_ptr() as *mut c_char) }
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
|
||||
{
|
||||
// Until CPython 3.10, tp_doc was treated specially for
|
||||
// heap-types, and it removed the text_signature value from it.
|
||||
// We go in after the fact and replace tp_doc with something
|
||||
// that _does_ include the text_signature value!
|
||||
self.cleanup
|
||||
.push(Box::new(move |_self, type_object| unsafe {
|
||||
ffi::PyObject_Free((*type_object).tp_doc as _);
|
||||
|
||||
let slice = type_doc.to_bytes_with_nul();
|
||||
let data = ffi::PyMem_Malloc(slice.len());
|
||||
data.copy_from(slice.as_ptr() as _, slice.len());
|
||||
(*type_object).tp_doc = data as _;
|
||||
}))
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn offsets(
|
||||
mut self,
|
||||
dict_offset: Option<PyObjectOffset>,
|
||||
#[allow(unused_variables)] weaklist_offset: Option<PyObjectOffset>,
|
||||
) -> Self {
|
||||
self.dict_offset = dict_offset;
|
||||
|
||||
#[cfg(Py_3_9)]
|
||||
{
|
||||
#[inline(always)]
|
||||
fn offset_def(name: &'static CStr, offset: PyObjectOffset) -> ffi::PyMemberDef {
|
||||
let (offset, flags) = match offset {
|
||||
PyObjectOffset::Absolute(offset) => (offset, ffi::Py_READONLY),
|
||||
#[cfg(Py_3_12)]
|
||||
PyObjectOffset::Relative(offset) => {
|
||||
(offset, ffi::Py_READONLY | ffi::Py_RELATIVE_OFFSET)
|
||||
}
|
||||
};
|
||||
ffi::PyMemberDef {
|
||||
name: name.as_ptr().cast(),
|
||||
type_code: ffi::Py_T_PYSSIZET,
|
||||
offset,
|
||||
flags,
|
||||
doc: core::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
// __dict__ support
|
||||
if let Some(dict_offset) = dict_offset {
|
||||
self.member_defs
|
||||
.push(offset_def(c"__dictoffset__", dict_offset));
|
||||
}
|
||||
|
||||
// weakref support
|
||||
if let Some(weaklist_offset) = weaklist_offset {
|
||||
self.member_defs
|
||||
.push(offset_def(c"__weaklistoffset__", weaklist_offset));
|
||||
}
|
||||
}
|
||||
|
||||
// Setting buffer protocols, tp_dictoffset and tp_weaklistoffset via slots doesn't work until
|
||||
// Python 3.9, so on older versions we must manually fixup the type object.
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_9)))]
|
||||
{
|
||||
self.cleanup
|
||||
.push(Box::new(move |builder, type_object| unsafe {
|
||||
(*(*type_object).tp_as_buffer).bf_getbuffer = builder.buffer_procs.bf_getbuffer;
|
||||
(*(*type_object).tp_as_buffer).bf_releasebuffer =
|
||||
builder.buffer_procs.bf_releasebuffer;
|
||||
|
||||
match dict_offset {
|
||||
Some(PyObjectOffset::Absolute(offset)) => {
|
||||
(*type_object).tp_dictoffset = offset;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
match weaklist_offset {
|
||||
Some(PyObjectOffset::Absolute(offset)) => {
|
||||
(*type_object).tp_weaklistoffset = offset;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn build(
|
||||
mut self,
|
||||
py: Python<'_>,
|
||||
name: &'static str,
|
||||
module_name: Option<&'static str>,
|
||||
basicsize: ffi::Py_ssize_t,
|
||||
) -> PyResult<PyClassTypeObject> {
|
||||
// `c_ulong` and `c_uint` have the same size
|
||||
// on some platforms (like windows)
|
||||
#![allow(clippy::useless_conversion)]
|
||||
|
||||
let getset_defs = self.finalize_methods_and_properties();
|
||||
|
||||
unsafe { self.push_slot(ffi::Py_tp_base, self.tp_base) }
|
||||
|
||||
if !self.has_new {
|
||||
#[cfg(not(Py_3_10))]
|
||||
{
|
||||
// Safety: This is the correct slot type for Py_tp_new
|
||||
unsafe { self.push_slot(ffi::Py_tp_new, no_constructor_defined as *mut c_void) }
|
||||
}
|
||||
#[cfg(Py_3_10)]
|
||||
{
|
||||
self.class_flags |= ffi::Py_TPFLAGS_DISALLOW_INSTANTIATION;
|
||||
}
|
||||
}
|
||||
|
||||
// A reference cycle can run through the instance `__dict__` (`obj.x = obj`), so a class
|
||||
// with a `__dict__` must be a GC type. `_call_traverse` / `_call_clear` service the
|
||||
// `__dict__` when the user defines `__traverse__` / `__clear__`; synthesize whichever
|
||||
// slot they did not.
|
||||
//
|
||||
// Must run before the `tp_dealloc` selection below, which keys off `has_traverse`.
|
||||
if self.dict_offset.is_some() {
|
||||
if !self.has_traverse {
|
||||
let synthesized_traverse = self.synthesized_traverse;
|
||||
// Safety: This is the correct slot type for Py_tp_traverse
|
||||
unsafe { self.push_slot(ffi::Py_tp_traverse, synthesized_traverse as *mut c_void) }
|
||||
}
|
||||
if !self.has_clear {
|
||||
let synthesized_clear = self.synthesized_clear;
|
||||
// Safety: This is the correct slot type for Py_tp_clear
|
||||
unsafe { self.push_slot(ffi::Py_tp_clear, synthesized_clear as *mut c_void) }
|
||||
}
|
||||
}
|
||||
|
||||
let base_is_gc = unsafe { ffi::PyType_IS_GC(self.tp_base) == 1 };
|
||||
let tp_dealloc = if self.has_traverse || base_is_gc {
|
||||
self.tp_dealloc_with_gc
|
||||
} else {
|
||||
self.tp_dealloc
|
||||
};
|
||||
unsafe { self.push_slot(ffi::Py_tp_dealloc, tp_dealloc as *mut c_void) }
|
||||
|
||||
if self.has_clear && !self.has_traverse {
|
||||
return Err(PyTypeError::new_err(format!(
|
||||
"`#[pyclass]` {name} implements __clear__ without __traverse__"
|
||||
)));
|
||||
}
|
||||
|
||||
// If this type is a GC type, and the base also is, we may need to add
|
||||
// `tp_traverse` / `tp_clear` implementations to call the base, if this type didn't
|
||||
// define `__traverse__` or `__clear__`.
|
||||
//
|
||||
// This is because when Py_TPFLAGS_HAVE_GC is set, then `tp_traverse` and
|
||||
// `tp_clear` are not inherited.
|
||||
if ((self.class_flags & ffi::Py_TPFLAGS_HAVE_GC) != 0) && base_is_gc {
|
||||
// If this assertion breaks, need to consider doing the same for __traverse__.
|
||||
assert!(self.has_traverse); // Py_TPFLAGS_HAVE_GC is set when a `__traverse__` method is found
|
||||
|
||||
if !self.has_clear {
|
||||
let synthesized_clear = self.synthesized_clear;
|
||||
// Safety: This is the correct slot type for Py_tp_clear
|
||||
unsafe { self.push_slot(ffi::Py_tp_clear, synthesized_clear as *mut c_void) }
|
||||
}
|
||||
}
|
||||
|
||||
// For sequences, implement sq_length instead of mp_length
|
||||
if self.is_sequence {
|
||||
for slot in &mut self.slots {
|
||||
if slot.slot == ffi::Py_mp_length {
|
||||
slot.slot = ffi::Py_sq_length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add empty sentinel at the end
|
||||
// Safety: python expects this empty slot
|
||||
unsafe { self.push_slot(0, ptr::null_mut::<c_void>()) }
|
||||
|
||||
let class_name = py_class_qualified_name(module_name, name)?;
|
||||
let flags = ffi::Py_TPFLAGS_DEFAULT | self.class_flags;
|
||||
|
||||
#[cfg(Py_3_15)]
|
||||
let type_object = {
|
||||
let mut slots = [
|
||||
ffi::PySlot_DATA(ffi::Py_tp_name, class_name.as_ptr() as *mut c_void),
|
||||
ffi::PySlot_UINT64(ffi::Py_tp_flags, flags.into()),
|
||||
ffi::PySlot_DATA(ffi::Py_tp_slots, self.slots.as_mut_ptr().cast::<c_void>()),
|
||||
match basicsize {
|
||||
1.. => ffi::PySlot_SIZE(ffi::Py_tp_basicsize, basicsize),
|
||||
// zero size; don't set the slot at all, the VM will use the parent size
|
||||
//
|
||||
// This can *ONLY* be hit on the variable-sized case with a rust ZST,
|
||||
// as a fully-sized case will always have size at least equal to `PyObject`
|
||||
0 => ffi::PySlot_END(),
|
||||
..0 => ffi::PySlot_SIZE(ffi::Py_tp_extra_basicsize, -basicsize),
|
||||
},
|
||||
// NB: insert additional slots BEFORE `basicsize` slot as it might be null
|
||||
ffi::PySlot_END(),
|
||||
];
|
||||
|
||||
// SAFETY: We've correctly setup the slots array at this point.
|
||||
// The FFI call is known to return a new type object or null on error.
|
||||
unsafe {
|
||||
ffi::PyType_FromSlots(slots.as_mut_ptr())
|
||||
.assume_owned_or_err(py)?
|
||||
.cast_into_unchecked::<PyType>()
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(Py_3_15))]
|
||||
let type_object = {
|
||||
let mut spec = ffi::PyType_Spec {
|
||||
name: class_name.as_ptr() as _,
|
||||
basicsize: basicsize as c_int,
|
||||
itemsize: 0,
|
||||
flags: flags.try_into().unwrap(),
|
||||
slots: self.slots.as_mut_ptr(),
|
||||
};
|
||||
|
||||
// SAFETY: We've correctly setup the PyType_Spec at this point.
|
||||
// The FFI call is known to return a new type object or null on error.
|
||||
unsafe {
|
||||
ffi::PyType_FromSpec(&mut spec)
|
||||
.assume_owned_or_err(py)?
|
||||
.cast_into_unchecked::<PyType>()
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(Py_3_11))]
|
||||
bpo_45315_workaround(py, class_name);
|
||||
|
||||
#[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))]
|
||||
for cleanup in core::mem::take(&mut self.cleanup) {
|
||||
cleanup(&self, type_object.as_type_ptr());
|
||||
}
|
||||
|
||||
Ok(PyClassTypeObject {
|
||||
type_object: type_object.unbind(),
|
||||
is_immutable_type: self.is_immutable_type,
|
||||
getset_defs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn py_class_qualified_name(module_name: Option<&str>, class_name: &str) -> PyResult<CString> {
|
||||
Ok(CString::new(format!(
|
||||
"{}.{}",
|
||||
module_name.unwrap_or("builtins"),
|
||||
class_name
|
||||
))?)
|
||||
}
|
||||
|
||||
/// Workaround for Python issue 45315; no longer necessary in Python 3.11
|
||||
#[inline]
|
||||
#[cfg(not(Py_3_11))]
|
||||
fn bpo_45315_workaround(py: Python<'_>, class_name: CString) {
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
{
|
||||
// Must check version at runtime for abi3 wheels - they could run against a higher version
|
||||
// than the build config suggests.
|
||||
use crate::sync::PyOnceLock;
|
||||
static IS_PYTHON_3_11: PyOnceLock<bool> = PyOnceLock::new();
|
||||
|
||||
if *IS_PYTHON_3_11.get_or_init(py, || py.version_info() >= (3, 11)) {
|
||||
// No fix needed - the wheel is running on a sufficiently new interpreter.
|
||||
return;
|
||||
}
|
||||
}
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
{
|
||||
// suppress unused variable warning
|
||||
let _ = py;
|
||||
}
|
||||
|
||||
core::mem::forget(class_name);
|
||||
}
|
||||
|
||||
/// Default new implementation
|
||||
#[cfg(not(Py_3_10))]
|
||||
unsafe extern "C" fn no_constructor_defined(
|
||||
subtype: *mut ffi::PyTypeObject,
|
||||
_args: *mut ffi::PyObject,
|
||||
_kwds: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject {
|
||||
unsafe {
|
||||
trampoline(|py| {
|
||||
let tpobj = PyType::from_borrowed_type_ptr(py, subtype);
|
||||
// unlike `fully_qualified_name`, this always include the module
|
||||
let module = tpobj
|
||||
.module()
|
||||
.map_or_else(|_| "<unknown>".into(), |s| s.to_string());
|
||||
let qualname = tpobj.qualname();
|
||||
let qualname = qualname.map_or_else(|_| "<unknown>".into(), |s| s.to_string());
|
||||
Err(crate::exceptions::PyTypeError::new_err(format!(
|
||||
"cannot create '{module}.{qualname}' instances"
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GetSetDefBuilder {
|
||||
doc: Option<&'static CStr>,
|
||||
getter: Option<Getter>,
|
||||
setter: Option<Setter>,
|
||||
deleter: Option<Deleter>,
|
||||
}
|
||||
|
||||
impl GetSetDefBuilder {
|
||||
fn add_getter(&mut self, getter: &PyGetterDef) {
|
||||
// TODO: be smarter about merging getter and setter docs
|
||||
if self.doc.is_none() {
|
||||
self.doc = getter.doc;
|
||||
}
|
||||
// TODO: return an error if getter already defined?
|
||||
self.getter = Some(getter.meth)
|
||||
}
|
||||
|
||||
fn add_setter(&mut self, setter: &PySetterDef) {
|
||||
// TODO: be smarter about merging getter and setter docs
|
||||
if self.doc.is_none() {
|
||||
self.doc = setter.doc;
|
||||
}
|
||||
// TODO: return an error if setter already defined?
|
||||
self.setter = Some(setter.meth)
|
||||
}
|
||||
|
||||
fn add_deleter(&mut self, deleter: &PyDeleterDef) {
|
||||
// TODO: be smarter about merging getter, setter and deleter docs
|
||||
if self.doc.is_none() {
|
||||
self.doc = deleter.doc;
|
||||
}
|
||||
// TODO: return an error if deleter already defined?
|
||||
self.deleter = Some(deleter.meth)
|
||||
}
|
||||
|
||||
fn as_get_set_def(&self, name: &'static CStr) -> (ffi::PyGetSetDef, GetSetDefType) {
|
||||
let getset_type = match (self.getter, self.setter, self.deleter) {
|
||||
(None, None, None) => {
|
||||
unreachable!("GetSetDefBuilder expected to always have either getter or setter")
|
||||
}
|
||||
(Some(getter), None, None) => GetSetDefType::Getter(getter),
|
||||
(None, Some(setter), None) => GetSetDefType::Setter(setter),
|
||||
(getter, setter, deleter) => {
|
||||
GetSetDefType::Combination(Box::new(GetSetDeleteCombination {
|
||||
getter,
|
||||
setter,
|
||||
deleter,
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
let getset_def = getset_type.create_py_get_set_def(name, self.doc);
|
||||
(getset_def, getset_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible forms of property - either a getter, setter, or both
|
||||
enum GetSetDefType {
|
||||
Getter(Getter),
|
||||
Setter(Setter),
|
||||
// The box is here so that the `GetSetDeleteCombination` has a stable
|
||||
// memory address even if the `GetSetDeleteCombination` enum is moved
|
||||
Combination(Box<GetSetDeleteCombination>),
|
||||
}
|
||||
|
||||
pub(crate) struct GetSetDeleteCombination {
|
||||
getter: Option<Getter>,
|
||||
setter: Option<Setter>,
|
||||
deleter: Option<Deleter>,
|
||||
}
|
||||
|
||||
impl GetSetDefType {
|
||||
/// Fills a PyGetSetDef structure
|
||||
/// It is only valid for as long as this GetSetDefType remains alive,
|
||||
/// as well as name and doc members
|
||||
pub(crate) fn create_py_get_set_def(
|
||||
&self,
|
||||
name: &CStr,
|
||||
doc: Option<&CStr>,
|
||||
) -> ffi::PyGetSetDef {
|
||||
let (get, set, closure): (Option<ffi::getter>, Option<ffi::setter>, *mut c_void) =
|
||||
match self {
|
||||
&Self::Getter(closure) => {
|
||||
unsafe extern "C" fn getter(
|
||||
slf: *mut ffi::PyObject,
|
||||
closure: *mut c_void,
|
||||
) -> *mut ffi::PyObject {
|
||||
let slf = unsafe { NonNull::new_unchecked(slf) };
|
||||
// Safety: PyO3 sets the closure when constructing the ffi getter so this cast should always be valid
|
||||
let getter: Getter = unsafe { core::mem::transmute(closure) };
|
||||
unsafe { trampoline(|py| getter(py, slf)) }
|
||||
}
|
||||
(Some(getter), None, closure as Getter as _)
|
||||
}
|
||||
&Self::Setter(closure) => {
|
||||
unsafe extern "C" fn setter(
|
||||
slf: *mut ffi::PyObject,
|
||||
value: *mut ffi::PyObject,
|
||||
closure: *mut c_void,
|
||||
) -> c_int {
|
||||
let slf = unsafe { NonNull::new_unchecked(slf) };
|
||||
// Safety: PyO3 sets the closure when constructing the ffi setter so this cast should always be valid
|
||||
let setter: Setter = unsafe { core::mem::transmute(closure) };
|
||||
unsafe {
|
||||
trampoline(|py| {
|
||||
if let Some(value) = NonNull::new(value) {
|
||||
setter(py, slf, value)
|
||||
} else {
|
||||
Err(PyAttributeError::new_err("property has no deleter"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
(None, Some(setter), closure as Setter as _)
|
||||
}
|
||||
Self::Combination(closure) => {
|
||||
unsafe extern "C" fn getset_getter(
|
||||
slf: *mut ffi::PyObject,
|
||||
closure: *mut c_void,
|
||||
) -> *mut ffi::PyObject {
|
||||
let slf = unsafe { NonNull::new_unchecked(slf) };
|
||||
let getset: &GetSetDeleteCombination = unsafe { &*closure.cast() };
|
||||
// we only call this method if getter is set
|
||||
unsafe { trampoline(|py| getset.getter.unwrap_unchecked()(py, slf)) }
|
||||
}
|
||||
|
||||
unsafe extern "C" fn getset_setter(
|
||||
slf: *mut ffi::PyObject,
|
||||
value: *mut ffi::PyObject,
|
||||
closure: *mut c_void,
|
||||
) -> c_int {
|
||||
let slf = unsafe { NonNull::new_unchecked(slf) };
|
||||
let getset: &GetSetDeleteCombination = unsafe { &*closure.cast() };
|
||||
unsafe {
|
||||
trampoline(|py| {
|
||||
if let Some(value) = NonNull::new(value) {
|
||||
getset.setter.ok_or_else(|| {
|
||||
PyAttributeError::new_err("property has no setter")
|
||||
})?(py, slf, value)
|
||||
} else {
|
||||
getset.deleter.ok_or_else(|| {
|
||||
PyAttributeError::new_err("property has no deleter")
|
||||
})?(py, slf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
(
|
||||
closure.getter.is_some().then_some(getset_getter),
|
||||
Some(getset_setter),
|
||||
NonNull::<GetSetDeleteCombination>::from(closure.as_ref())
|
||||
.cast()
|
||||
.as_ptr(),
|
||||
)
|
||||
}
|
||||
};
|
||||
ffi::PyGetSetDef {
|
||||
name: name.as_ptr(),
|
||||
doc: doc.map_or(ptr::null(), CStr::as_ptr),
|
||||
get,
|
||||
set,
|
||||
closure,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use core::{
|
||||
ffi::{c_int, c_void},
|
||||
marker::PhantomData,
|
||||
num::NonZero,
|
||||
};
|
||||
|
||||
use crate::{ffi, Py};
|
||||
|
||||
/// Error returned by a `__traverse__` visitor implementation.
|
||||
#[repr(transparent)]
|
||||
pub struct PyTraverseError(NonZero<c_int>);
|
||||
|
||||
impl PyTraverseError {
|
||||
/// Returns the error code.
|
||||
pub(crate) fn into_inner(self) -> c_int {
|
||||
self.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Object visitor for GC.
|
||||
#[derive(Clone)]
|
||||
pub struct PyVisit<'a> {
|
||||
pub(crate) visit: ffi::visitproc,
|
||||
pub(crate) arg: *mut c_void,
|
||||
/// Prevents the `PyVisit` from outliving the `__traverse__` call.
|
||||
pub(crate) _guard: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl PyVisit<'_> {
|
||||
/// Visit `obj`.
|
||||
///
|
||||
/// Note: `obj` accepts a variety of types, including
|
||||
/// - `&Py<T>`
|
||||
/// - `&Option<Py<T>>`
|
||||
/// - `Option<&Py<T>>`
|
||||
pub fn call<'a, T, U: 'a>(&self, obj: T) -> Result<(), PyTraverseError>
|
||||
where
|
||||
T: Into<Option<&'a Py<U>>>,
|
||||
{
|
||||
let ptr = obj.into().map_or_else(core::ptr::null_mut, Py::as_ptr);
|
||||
if !ptr.is_null() {
|
||||
match NonZero::new(unsafe { (self.visit)(ptr, self.arg) }) {
|
||||
None => Ok(()),
|
||||
Some(r) => Err(PyTraverseError(r)),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PyVisit;
|
||||
use static_assertions::assert_not_impl_any;
|
||||
|
||||
#[test]
|
||||
fn py_visit_not_send_sync() {
|
||||
assert_not_impl_any!(PyVisit<'_>: Send, Sync);
|
||||
}
|
||||
}
|
||||
Vendored
+1211
File diff suppressed because it is too large.
Load diff
Vendored
+262
@@ -0,0 +1,262 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
//! Contains initialization utilities for `#[pyclass]`.
|
||||
use crate::ffi_ptr_ext::FfiPtrExt;
|
||||
use crate::impl_::pyclass::{PyClassBaseType, PyClassImpl};
|
||||
use crate::impl_::pyclass_init::PyNativeTypeInitializer;
|
||||
use crate::internal::pyclass_init::PyObjectInit;
|
||||
use crate::pycell::impl_::PyClassObjectLayout;
|
||||
use crate::{ffi, Bound, PyClass, PyResult, Python};
|
||||
use crate::{ffi::PyTypeObject, pycell::impl_::PyClassObjectContents};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Initializer for our `#[pyclass]` system.
|
||||
///
|
||||
/// You can use this type to initialize complicatedly nested `#[pyclass]`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use pyo3::prelude::*;
|
||||
/// # use pyo3::py_run;
|
||||
/// #[pyclass(subclass)]
|
||||
/// struct BaseClass {
|
||||
/// #[pyo3(get)]
|
||||
/// basename: &'static str,
|
||||
/// }
|
||||
/// #[pyclass(extends=BaseClass, subclass)]
|
||||
/// struct SubClass {
|
||||
/// #[pyo3(get)]
|
||||
/// subname: &'static str,
|
||||
/// }
|
||||
/// #[pyclass(extends=SubClass)]
|
||||
/// struct SubSubClass {
|
||||
/// #[pyo3(get)]
|
||||
/// subsubname: &'static str,
|
||||
/// }
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl SubSubClass {
|
||||
/// #[new]
|
||||
/// fn new() -> PyClassInitializer<Self> {
|
||||
/// PyClassInitializer::from(BaseClass { basename: "base" })
|
||||
/// .add_subclass(SubClass { subname: "sub" })
|
||||
/// .add_subclass(SubSubClass {
|
||||
/// subsubname: "subsub",
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// Python::attach(|py| {
|
||||
/// let typeobj = py.get_type::<SubSubClass>();
|
||||
/// let sub_sub_class = typeobj.call((), None).unwrap();
|
||||
/// py_run!(
|
||||
/// py,
|
||||
/// sub_sub_class,
|
||||
/// r#"
|
||||
/// assert sub_sub_class.basename == 'base'
|
||||
/// assert sub_sub_class.subname == 'sub'
|
||||
/// assert sub_sub_class.subsubname == 'subsub'"#
|
||||
/// );
|
||||
/// });
|
||||
/// ```
|
||||
pub struct PyClassInitializer<T: PyClass> {
|
||||
init: T,
|
||||
super_init: <T::BaseType as PyClassBaseType>::Initializer,
|
||||
}
|
||||
|
||||
impl<T: PyClass> PyClassInitializer<T> {
|
||||
/// Constructs a new initializer from value `T` and base class' initializer.
|
||||
///
|
||||
/// It is recommended to use `add_subclass` instead of this method for most usage.
|
||||
#[track_caller]
|
||||
#[inline]
|
||||
pub fn new(init: T, super_init: <T::BaseType as PyClassBaseType>::Initializer) -> Self {
|
||||
Self { init, super_init }
|
||||
}
|
||||
|
||||
/// Constructs a new initializer from an initializer for the base class.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use pyo3::prelude::*;
|
||||
///
|
||||
/// #[pyclass(subclass)]
|
||||
/// struct BaseClass {
|
||||
/// #[pyo3(get)]
|
||||
/// value: i32,
|
||||
/// }
|
||||
///
|
||||
/// impl BaseClass {
|
||||
/// fn new(value: i32) -> PyResult<Self> {
|
||||
/// Ok(Self { value })
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[pyclass(extends=BaseClass)]
|
||||
/// struct SubClass {}
|
||||
///
|
||||
/// #[pymethods]
|
||||
/// impl SubClass {
|
||||
/// #[new]
|
||||
/// fn new(value: i32) -> PyResult<PyClassInitializer<Self>> {
|
||||
/// let base_init = PyClassInitializer::from(BaseClass::new(value)?);
|
||||
/// Ok(base_init.add_subclass(SubClass {}))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn main() -> PyResult<()> {
|
||||
/// Python::attach(|py| {
|
||||
/// let m = PyModule::new(py, "example")?;
|
||||
/// m.add_class::<SubClass>()?;
|
||||
/// m.add_class::<BaseClass>()?;
|
||||
///
|
||||
/// let instance = m.getattr("SubClass")?.call1((92,))?;
|
||||
///
|
||||
/// // `SubClass` does not have a `value` attribute, but `BaseClass` does.
|
||||
/// let n = instance.getattr("value")?.extract::<i32>()?;
|
||||
/// assert_eq!(n, 92);
|
||||
///
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
#[track_caller]
|
||||
#[inline]
|
||||
pub fn add_subclass<S>(self, subclass_value: S) -> PyClassInitializer<S>
|
||||
where
|
||||
T: PyClassBaseType<Initializer = Self>,
|
||||
S: PyClass<BaseType = T>,
|
||||
{
|
||||
PyClassInitializer::new(subclass_value, self)
|
||||
}
|
||||
|
||||
/// Creates a new class object and initializes it.
|
||||
pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult<Bound<'_, T>>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
unsafe { self.create_class_object_of_type(py, T::type_object_raw(py)) }
|
||||
}
|
||||
|
||||
/// Creates a new class object and initializes it given a typeobject `subtype`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `subtype` must be a valid pointer to the type object of T or a subclass.
|
||||
pub(crate) unsafe fn create_class_object_of_type(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
target_type: *mut crate::ffi::PyTypeObject,
|
||||
) -> PyResult<Bound<'_, T>>
|
||||
where
|
||||
T: PyClass,
|
||||
{
|
||||
let obj = unsafe { self.super_init.into_new_object(py, target_type)? };
|
||||
|
||||
// SAFETY: `obj` is constructed using `T::Layout` but has not been initialized yet
|
||||
let contents = unsafe { <T as PyClassImpl>::Layout::contents_uninit(obj) };
|
||||
|
||||
let new_contents = PyClassObjectContents::new(self.init);
|
||||
|
||||
// CPython 3.11 and 3.12 eagerly create the instance dict for types with a nonzero
|
||||
// `tp_dictoffset` in `_PyObject_InitializeDict`, storing an owned reference in
|
||||
// the `__dict__` slot, which lives inside `contents`. Carry that value over
|
||||
// instead of clobbering it below, otherwise it leaks. Python 3.13 returned to
|
||||
// creating the instance dict lazily
|
||||
//
|
||||
// The condition is `not(Py_3_13)` rather than `all(Py_3_11, not(Py_3_13))`
|
||||
// because an abi3 build with a lower minimum version can still run on 3.11 and
|
||||
// 3.12; on 3.10 and older this is a harmless no-op (the slot is always null
|
||||
// there).
|
||||
#[cfg(not(Py_3_13))]
|
||||
let new_contents = {
|
||||
let mut new_contents = new_contents;
|
||||
if eagerly_created_dict_possible::<T>(py) {
|
||||
// SAFETY: `tp_alloc` zero-initializes the object, so the slot contains either
|
||||
// zeroes (a valid empty slot value) or a valid owned pointer stored by the base
|
||||
// `tp_new` through the type's `tp_dictoffset`.
|
||||
unsafe {
|
||||
let contents_ptr = (*contents).as_mut_ptr();
|
||||
let dict_ptr = &raw const (*contents_ptr).dict;
|
||||
new_contents.dict = core::ptr::read(dict_ptr);
|
||||
}
|
||||
}
|
||||
new_contents
|
||||
};
|
||||
|
||||
// SAFETY: `contents` is a non-null pointer to the space allocated for our
|
||||
// `PyClassObjectContents` (either statically in Rust or dynamically by Python)
|
||||
unsafe { (*contents).write(new_contents) };
|
||||
|
||||
// Safety: obj is a valid pointer to an object of type `target_type`, which` is a known
|
||||
// subclass of `T`
|
||||
Ok(unsafe { obj.assume_owned(py).cast_into_unchecked() })
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the running interpreter may have eagerly created an instance dict for `T`
|
||||
/// during `tp_new` (CPython 3.11 and 3.12 only).
|
||||
///
|
||||
/// For native builds the compile-time `not(Py_3_13)` gate at the call site is exact; abi3
|
||||
/// builds with a minimum version below 3.13 must check the interpreter version at runtime
|
||||
#[cfg(not(Py_3_13))]
|
||||
#[inline]
|
||||
fn eagerly_created_dict_possible<T: PyClassImpl>(py: Python<'_>) -> bool {
|
||||
if core::mem::size_of::<T::Dict>() == 0 {
|
||||
return false;
|
||||
}
|
||||
cfg_select! {
|
||||
Py_LIMITED_API =>
|
||||
{
|
||||
use crate::sync::PyOnceLock;
|
||||
static IS_PYTHON_3_11_OR_3_12: PyOnceLock<bool> = PyOnceLock::new();
|
||||
*IS_PYTHON_3_11_OR_3_12.get_or_init(py, || {
|
||||
let version_info = py.version_info();
|
||||
matches!((version_info.major, version_info.minor), (3, 11) | (3, 12))
|
||||
})
|
||||
}
|
||||
not(Py_LIMITED_API) =>
|
||||
{
|
||||
let _ = py;
|
||||
cfg!(Py_3_11)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PyClass> PyObjectInit<T> for PyClassInitializer<T> {
|
||||
unsafe fn into_new_object(
|
||||
self,
|
||||
py: Python<'_>,
|
||||
subtype: *mut PyTypeObject,
|
||||
) -> PyResult<*mut ffi::PyObject> {
|
||||
unsafe {
|
||||
self.create_class_object_of_type(py, subtype)
|
||||
.map(Bound::into_ptr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for PyClassInitializer<T>
|
||||
where
|
||||
T: PyClass,
|
||||
T::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<T::BaseType>>,
|
||||
{
|
||||
#[inline]
|
||||
fn from(value: T) -> PyClassInitializer<T> {
|
||||
Self::new(value, PyNativeTypeInitializer(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, B> From<(S, B)> for PyClassInitializer<S>
|
||||
where
|
||||
S: PyClass<BaseType = B>,
|
||||
B: PyClass + PyClassBaseType<Initializer = PyClassInitializer<B>>,
|
||||
B::BaseType: PyClassBaseType<Initializer = PyNativeTypeInitializer<B::BaseType>>,
|
||||
{
|
||||
#[track_caller]
|
||||
#[inline]
|
||||
fn from(sub_and_base: (S, B)) -> PyClassInitializer<S> {
|
||||
let (sub, base) = sub_and_base;
|
||||
PyClassInitializer::from(base).add_subclass(sub)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1714 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user