Vendor deps.
This commit is contained in:
1 parent
294527085e
commit
91e18fde3a
1714 files changed
+556667
-7
No files matched your search
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
#![cfg(feature = "anyhow")]
|
||||
|
||||
use pyo3::wrap_pyfunction;
|
||||
|
||||
#[test]
|
||||
fn test_anyhow_py_function_ok_result() {
|
||||
use pyo3::{py_run, pyfunction, Python};
|
||||
|
||||
#[pyfunction]
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn produce_ok_result() -> anyhow::Result<String> {
|
||||
Ok(String::from("OK buddy"))
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let func = wrap_pyfunction!(produce_ok_result)(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
func,
|
||||
r#"
|
||||
func()
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_anyhow_py_function_err_result() {
|
||||
use pyo3::prelude::PyDictMethods;
|
||||
use pyo3::{pyfunction, types::PyDict, Python};
|
||||
|
||||
#[pyfunction]
|
||||
fn produce_err_result() -> anyhow::Result<String> {
|
||||
anyhow::bail!("error time")
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let func = wrap_pyfunction!(produce_err_result)(py).unwrap();
|
||||
let locals = PyDict::new(py);
|
||||
locals.set_item("func", func).unwrap();
|
||||
|
||||
py.run(c"func()", None, Some(&locals)).unwrap_err();
|
||||
});
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#![cfg(feature = "macros")]
|
||||
#![cfg(not(PyPy))]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
fn foo() -> usize {
|
||||
123
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(foo, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
mod module_mod_with_functions {
|
||||
#[pymodule_export]
|
||||
use super::foo;
|
||||
}
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy, all(Py_LIMITED_API, Py_GIL_DISABLED))))]
|
||||
#[test]
|
||||
fn test_module_append_to_inittab() {
|
||||
use pyo3::append_to_inittab;
|
||||
|
||||
append_to_inittab!(module_fn_with_functions);
|
||||
|
||||
append_to_inittab!(module_mod_with_functions);
|
||||
|
||||
Python::attach(|py| {
|
||||
py.run(
|
||||
cr#"
|
||||
import module_fn_with_functions
|
||||
assert module_fn_with_functions.foo() == 123
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
Python::attach(|py| {
|
||||
py.run(
|
||||
cr#"
|
||||
import module_mod_with_functions
|
||||
assert module_mod_with_functions.foo() == 123
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
+803
@@ -0,0 +1,803 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::class::basic::CompareOp;
|
||||
use pyo3::py_run;
|
||||
use pyo3::{prelude::*, BoundObject};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct UnaryArithmetic {
|
||||
inner: f64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl UnaryArithmetic {
|
||||
#[new]
|
||||
fn new(value: f64) -> Self {
|
||||
UnaryArithmetic { inner: value }
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("UA({})", self.inner)
|
||||
}
|
||||
|
||||
fn __neg__(&self) -> Self {
|
||||
Self::new(-self.inner)
|
||||
}
|
||||
|
||||
fn __pos__(&self) -> Self {
|
||||
Self::new(self.inner)
|
||||
}
|
||||
|
||||
fn __abs__(&self) -> Self {
|
||||
Self::new(self.inner.abs())
|
||||
}
|
||||
|
||||
fn __invert__(&self) -> Self {
|
||||
Self::new(self.inner.recip())
|
||||
}
|
||||
|
||||
#[pyo3(signature=(_ndigits=None))]
|
||||
fn __round__(&self, _ndigits: Option<u32>) -> Self {
|
||||
Self::new(self.inner.round())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unary_arithmetic() {
|
||||
Python::attach(|py| {
|
||||
let c = Py::new(py, UnaryArithmetic::new(2.7)).unwrap();
|
||||
py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'");
|
||||
py_run!(py, c, "assert repr(+c) == 'UA(2.7)'");
|
||||
py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'");
|
||||
py_run!(py, c, "assert repr(~c) == 'UA(0.37037037037037035)'");
|
||||
py_run!(py, c, "assert repr(round(c)) == 'UA(3)'");
|
||||
py_run!(py, c, "assert repr(round(c, 1)) == 'UA(3)'");
|
||||
|
||||
let c: Bound<'_, PyAny> = c.extract(py).unwrap();
|
||||
assert_py_eq!(c.neg().unwrap().repr().unwrap().as_any(), "UA(-2.7)");
|
||||
assert_py_eq!(c.pos().unwrap().repr().unwrap().as_any(), "UA(2.7)");
|
||||
assert_py_eq!(c.abs().unwrap().repr().unwrap().as_any(), "UA(2.7)");
|
||||
assert_py_eq!(
|
||||
c.bitnot().unwrap().repr().unwrap().as_any(),
|
||||
"UA(0.37037037037037035)"
|
||||
);
|
||||
|
||||
// Ensure that passing a wrong self type from Python does not cause UB
|
||||
py_expect_exception!(py, c, "type(c).__neg__(object())", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__pos__(object())", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__abs__(object())", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__invert__(object())", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__round__(object())", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct Indexable(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Indexable {
|
||||
fn __index__(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn __int__(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn __float__(&self) -> f64 {
|
||||
f64::from(self.0)
|
||||
}
|
||||
|
||||
fn __invert__(&self) -> Self {
|
||||
Self(!self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexable() {
|
||||
Python::attach(|py| {
|
||||
let i = Py::new(py, Indexable(5)).unwrap();
|
||||
py_run!(py, i, "assert int(i) == 5");
|
||||
py_run!(py, i, "assert [0, 1, 2, 3, 4, 5][i] == 5");
|
||||
py_run!(py, i, "assert float(i) == 5.0");
|
||||
py_run!(py, i, "assert int(~i) == -6");
|
||||
|
||||
// Ensure that passing a wrong self type from Python does not cause UB
|
||||
py_expect_exception!(py, i, "type(i).__index__(object())", PyTypeError);
|
||||
py_expect_exception!(py, i, "type(i).__int__(object())", PyTypeError);
|
||||
py_expect_exception!(py, i, "type(i).__float__(object())", PyTypeError);
|
||||
py_expect_exception!(py, i, "type(i).__invert__(object())", PyTypeError);
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct InPlaceOperations {
|
||||
value: u32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl InPlaceOperations {
|
||||
fn __repr__(&self) -> String {
|
||||
format!("IPO({:?})", self.value)
|
||||
}
|
||||
|
||||
fn __iadd__(&mut self, other: u32) {
|
||||
self.value += other;
|
||||
}
|
||||
|
||||
fn __isub__(&mut self, other: u32) {
|
||||
self.value -= other;
|
||||
}
|
||||
|
||||
fn __imul__(&mut self, other: u32) {
|
||||
self.value *= other;
|
||||
}
|
||||
|
||||
fn __ilshift__(&mut self, other: u32) {
|
||||
self.value <<= other;
|
||||
}
|
||||
|
||||
fn __irshift__(&mut self, other: u32) {
|
||||
self.value >>= other;
|
||||
}
|
||||
|
||||
fn __iand__(&mut self, other: u32) {
|
||||
self.value &= other;
|
||||
}
|
||||
|
||||
fn __ixor__(&mut self, other: u32) {
|
||||
self.value ^= other;
|
||||
}
|
||||
|
||||
fn __ior__(&mut self, other: u32) {
|
||||
self.value |= other;
|
||||
}
|
||||
|
||||
fn __ipow__(&mut self, other: u32, _modulo: Option<u32>) {
|
||||
self.value = self.value.pow(other);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inplace_operations() {
|
||||
Python::attach(|py| {
|
||||
let init = |value, code| {
|
||||
let c = Py::new(py, InPlaceOperations { value }).unwrap();
|
||||
py_run!(py, c, code);
|
||||
};
|
||||
|
||||
init(0, "d = c; c += 1; assert repr(c) == repr(d) == 'IPO(1)'");
|
||||
init(10, "d = c; c -= 1; assert repr(c) == repr(d) == 'IPO(9)'");
|
||||
init(3, "d = c; c *= 3; assert repr(c) == repr(d) == 'IPO(9)'");
|
||||
init(3, "d = c; c <<= 2; assert repr(c) == repr(d) == 'IPO(12)'");
|
||||
init(12, "d = c; c >>= 2; assert repr(c) == repr(d) == 'IPO(3)'");
|
||||
init(12, "d = c; c &= 10; assert repr(c) == repr(d) == 'IPO(8)'");
|
||||
init(12, "d = c; c |= 3; assert repr(c) == repr(d) == 'IPO(15)'");
|
||||
init(12, "d = c; c ^= 5; assert repr(c) == repr(d) == 'IPO(9)'");
|
||||
init(3, "d = c; c **= 4; assert repr(c) == repr(d) == 'IPO(81)'");
|
||||
init(
|
||||
3,
|
||||
"d = c; c.__ipow__(4); assert repr(c) == repr(d) == 'IPO(81)'",
|
||||
);
|
||||
|
||||
let c = Py::new(py, InPlaceOperations { value: 0 }).unwrap();
|
||||
// Ensure that passing a wrong self type from Python does not cause UB
|
||||
py_expect_exception!(py, c, "type(c).__iadd__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__isub__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__imul__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ilshift__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__irshift__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__iand__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ixor__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ior__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ipow__(object(), 1)", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct BinaryArithmetic {}
|
||||
|
||||
#[pymethods]
|
||||
impl BinaryArithmetic {
|
||||
fn __repr__(&self) -> &'static str {
|
||||
"BA"
|
||||
}
|
||||
|
||||
fn __add__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA + {rhs:?}")
|
||||
}
|
||||
|
||||
fn __sub__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA - {rhs:?}")
|
||||
}
|
||||
|
||||
fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA * {rhs:?}")
|
||||
}
|
||||
|
||||
fn __matmul__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA @ {rhs:?}")
|
||||
}
|
||||
|
||||
fn __truediv__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA / {rhs:?}")
|
||||
}
|
||||
|
||||
fn __floordiv__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA // {rhs:?}")
|
||||
}
|
||||
|
||||
fn __mod__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA % {rhs:?}")
|
||||
}
|
||||
|
||||
fn __divmod__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("divmod(BA, {rhs:?})")
|
||||
}
|
||||
|
||||
fn __lshift__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA << {rhs:?}")
|
||||
}
|
||||
|
||||
fn __rshift__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA >> {rhs:?}")
|
||||
}
|
||||
|
||||
fn __and__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA & {rhs:?}")
|
||||
}
|
||||
|
||||
fn __xor__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA ^ {rhs:?}")
|
||||
}
|
||||
|
||||
fn __or__(&self, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("BA | {rhs:?}")
|
||||
}
|
||||
|
||||
fn __pow__(&self, rhs: &Bound<'_, PyAny>, mod_: Option<u32>) -> String {
|
||||
format!("BA ** {rhs:?} (mod: {mod_:?})")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_arithmetic() {
|
||||
Python::attach(|py| {
|
||||
let c = Py::new(py, BinaryArithmetic {}).unwrap();
|
||||
py_run!(py, c, "assert c + c == 'BA + BA'");
|
||||
py_run!(py, c, "assert c.__add__(c) == 'BA + BA'");
|
||||
py_run!(py, c, "assert c + 1 == 'BA + 1'");
|
||||
py_run!(py, c, "assert c - 1 == 'BA - 1'");
|
||||
py_run!(py, c, "assert c * 1 == 'BA * 1'");
|
||||
py_run!(py, c, "assert c @ 1 == 'BA @ 1'");
|
||||
py_run!(py, c, "assert c / 1 == 'BA / 1'");
|
||||
py_run!(py, c, "assert c // 1 == 'BA // 1'");
|
||||
py_run!(py, c, "assert c % 1 == 'BA % 1'");
|
||||
py_run!(py, c, "assert divmod(c, 1) == 'divmod(BA, 1)'");
|
||||
py_run!(py, c, "assert c << 1 == 'BA << 1'");
|
||||
py_run!(py, c, "assert c >> 1 == 'BA >> 1'");
|
||||
py_run!(py, c, "assert c & 1 == 'BA & 1'");
|
||||
py_run!(py, c, "assert c ^ 1 == 'BA ^ 1'");
|
||||
py_run!(py, c, "assert c | 1 == 'BA | 1'");
|
||||
py_run!(py, c, "assert c ** 1 == 'BA ** 1 (mod: None)'");
|
||||
|
||||
// Class with __add__ only should not allow the reverse op;
|
||||
// this is consistent with Python classes.
|
||||
|
||||
py_expect_exception!(py, c, "1 + c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 - c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 * c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 @ c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 / c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 // c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 % c", PyTypeError);
|
||||
py_expect_exception!(py, c, "divmod(1, c)", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 << c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 >> c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 & c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 ^ c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 | c", PyTypeError);
|
||||
py_expect_exception!(py, c, "1 ** c", PyTypeError);
|
||||
|
||||
py_run!(py, c, "assert pow(c, 1, 100) == 'BA ** 1 (mod: Some(100))'");
|
||||
|
||||
let c: Bound<'_, PyAny> = c.extract(py).unwrap();
|
||||
assert_py_eq!(c.add(&c).unwrap(), "BA + BA");
|
||||
assert_py_eq!(c.sub(&c).unwrap(), "BA - BA");
|
||||
assert_py_eq!(c.mul(&c).unwrap(), "BA * BA");
|
||||
assert_py_eq!(c.matmul(&c).unwrap(), "BA @ BA");
|
||||
assert_py_eq!(c.div(&c).unwrap(), "BA / BA");
|
||||
assert_py_eq!(c.floor_div(&c).unwrap(), "BA // BA");
|
||||
assert_py_eq!(c.rem(&c).unwrap(), "BA % BA");
|
||||
assert_py_eq!(c.divmod(&c).unwrap(), "divmod(BA, BA)");
|
||||
assert_py_eq!(c.lshift(&c).unwrap(), "BA << BA");
|
||||
assert_py_eq!(c.rshift(&c).unwrap(), "BA >> BA");
|
||||
assert_py_eq!(c.bitand(&c).unwrap(), "BA & BA");
|
||||
assert_py_eq!(c.bitor(&c).unwrap(), "BA | BA");
|
||||
assert_py_eq!(c.bitxor(&c).unwrap(), "BA ^ BA");
|
||||
assert_py_eq!(c.pow(&c, py.None()).unwrap(), "BA ** BA (mod: None)");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RhsArithmetic(String);
|
||||
|
||||
#[pymethods]
|
||||
impl RhsArithmetic {
|
||||
fn __radd__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} + {}", self.0)
|
||||
}
|
||||
|
||||
fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} - {}", self.0)
|
||||
}
|
||||
|
||||
fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} * {}", self.0)
|
||||
}
|
||||
|
||||
fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} << {}", self.0)
|
||||
}
|
||||
|
||||
fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} >> {}", self.0)
|
||||
}
|
||||
|
||||
fn __rand__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} & {}", self.0)
|
||||
}
|
||||
|
||||
fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} ^ {}", self.0)
|
||||
}
|
||||
|
||||
fn __ror__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} | {}", self.0)
|
||||
}
|
||||
|
||||
fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String {
|
||||
format!("{other:?} ** {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rhs_arithmetic() {
|
||||
Python::attach(|py| {
|
||||
let c = Py::new(py, RhsArithmetic("RA".to_string())).unwrap();
|
||||
py_run!(py, c, "assert c.__radd__(1) == '1 + RA'");
|
||||
py_run!(py, c, "assert 1 + c == '1 + RA'");
|
||||
py_run!(py, c, "assert c.__rsub__(1) == '1 - RA'");
|
||||
py_run!(py, c, "assert 1 - c == '1 - RA'");
|
||||
py_run!(py, c, "assert c.__rmul__(1) == '1 * RA'");
|
||||
py_run!(py, c, "assert 1 * c == '1 * RA'");
|
||||
py_run!(py, c, "assert c.__rlshift__(1) == '1 << RA'");
|
||||
py_run!(py, c, "assert 1 << c == '1 << RA'");
|
||||
py_run!(py, c, "assert c.__rrshift__(1) == '1 >> RA'");
|
||||
py_run!(py, c, "assert 1 >> c == '1 >> RA'");
|
||||
py_run!(py, c, "assert c.__rand__(1) == '1 & RA'");
|
||||
py_run!(py, c, "assert 1 & c == '1 & RA'");
|
||||
py_run!(py, c, "assert c.__rxor__(1) == '1 ^ RA'");
|
||||
py_run!(py, c, "assert 1 ^ c == '1 ^ RA'");
|
||||
py_run!(py, c, "assert c.__ror__(1) == '1 | RA'");
|
||||
py_run!(py, c, "assert 1 | c == '1 | RA'");
|
||||
py_run!(py, c, "assert c.__rpow__(1) == '1 ** RA'");
|
||||
py_run!(py, c, "assert 1 ** c == '1 ** RA'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct LhsAndRhs {}
|
||||
|
||||
impl std::fmt::Debug for LhsAndRhs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "LR")
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl LhsAndRhs {
|
||||
// fn __repr__(&self) -> &'static str {
|
||||
// "BA"
|
||||
// }
|
||||
|
||||
fn __add__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} + {rhs:?}")
|
||||
}
|
||||
|
||||
fn __sub__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} - {rhs:?}")
|
||||
}
|
||||
|
||||
fn __mul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} * {rhs:?}")
|
||||
}
|
||||
|
||||
fn __lshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} << {rhs:?}")
|
||||
}
|
||||
|
||||
fn __rshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} >> {rhs:?}")
|
||||
}
|
||||
|
||||
fn __and__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} & {rhs:?}")
|
||||
}
|
||||
|
||||
fn __xor__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} ^ {rhs:?}")
|
||||
}
|
||||
|
||||
fn __or__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} | {rhs:?}")
|
||||
}
|
||||
|
||||
fn __pow__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>, _mod: Option<usize>) -> String {
|
||||
format!("{lhs:?} ** {rhs:?}")
|
||||
}
|
||||
|
||||
fn __matmul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String {
|
||||
format!("{lhs:?} @ {rhs:?}")
|
||||
}
|
||||
|
||||
fn __radd__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} + RA")
|
||||
}
|
||||
|
||||
fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} - RA")
|
||||
}
|
||||
|
||||
fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} * RA")
|
||||
}
|
||||
|
||||
fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} << RA")
|
||||
}
|
||||
|
||||
fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} >> RA")
|
||||
}
|
||||
|
||||
fn __rand__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} & RA")
|
||||
}
|
||||
|
||||
fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} ^ RA")
|
||||
}
|
||||
|
||||
fn __ror__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} | RA")
|
||||
}
|
||||
|
||||
fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String {
|
||||
format!("{other:?} ** RA")
|
||||
}
|
||||
|
||||
fn __rmatmul__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} @ RA")
|
||||
}
|
||||
|
||||
fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} / RA")
|
||||
}
|
||||
|
||||
fn __rfloordiv__(&self, other: &Bound<'_, PyAny>) -> String {
|
||||
format!("{other:?} // RA")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lhs_fellback_to_rhs() {
|
||||
Python::attach(|py| {
|
||||
let c = Py::new(py, LhsAndRhs {}).unwrap();
|
||||
// If the left hand value is `LhsAndRhs`, LHS is used.
|
||||
py_run!(py, c, "assert c + 1 == 'LR + 1'");
|
||||
py_run!(py, c, "assert c - 1 == 'LR - 1'");
|
||||
py_run!(py, c, "assert c * 1 == 'LR * 1'");
|
||||
py_run!(py, c, "assert c << 1 == 'LR << 1'");
|
||||
py_run!(py, c, "assert c >> 1 == 'LR >> 1'");
|
||||
py_run!(py, c, "assert c & 1 == 'LR & 1'");
|
||||
py_run!(py, c, "assert c ^ 1 == 'LR ^ 1'");
|
||||
py_run!(py, c, "assert c | 1 == 'LR | 1'");
|
||||
py_run!(py, c, "assert c ** 1 == 'LR ** 1'");
|
||||
py_run!(py, c, "assert c @ 1 == 'LR @ 1'");
|
||||
// Fellback to RHS because of type mismatching
|
||||
py_run!(py, c, "assert 1 + c == '1 + RA'");
|
||||
py_run!(py, c, "assert 1 - c == '1 - RA'");
|
||||
py_run!(py, c, "assert 1 * c == '1 * RA'");
|
||||
py_run!(py, c, "assert 1 << c == '1 << RA'");
|
||||
py_run!(py, c, "assert 1 >> c == '1 >> RA'");
|
||||
py_run!(py, c, "assert 1 & c == '1 & RA'");
|
||||
py_run!(py, c, "assert 1 ^ c == '1 ^ RA'");
|
||||
py_run!(py, c, "assert 1 | c == '1 | RA'");
|
||||
py_run!(py, c, "assert 1 ** c == '1 ** RA'");
|
||||
py_run!(py, c, "assert 1 @ c == '1 @ RA'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RichComparisons {}
|
||||
|
||||
#[pymethods]
|
||||
impl RichComparisons {
|
||||
fn __repr__(&self) -> &'static str {
|
||||
"RC"
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> String {
|
||||
match op {
|
||||
CompareOp::Lt => format!("{} < {:?}", self.__repr__(), other),
|
||||
CompareOp::Le => format!("{} <= {:?}", self.__repr__(), other),
|
||||
CompareOp::Eq => format!("{} == {:?}", self.__repr__(), other),
|
||||
CompareOp::Ne => format!("{} != {:?}", self.__repr__(), other),
|
||||
CompareOp::Gt => format!("{} > {:?}", self.__repr__(), other),
|
||||
CompareOp::Ge => format!("{} >= {:?}", self.__repr__(), other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RichComparisons2 {}
|
||||
|
||||
#[pymethods]
|
||||
impl RichComparisons2 {
|
||||
fn __repr__(&self) -> &'static str {
|
||||
"RC2"
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
|
||||
match op {
|
||||
CompareOp::Eq => true
|
||||
.into_pyobject(other.py())
|
||||
.map_err(Into::into)
|
||||
.map(BoundObject::into_any)
|
||||
.map(BoundObject::unbind),
|
||||
CompareOp::Ne => false
|
||||
.into_pyobject(other.py())
|
||||
.map_err(Into::into)
|
||||
.map(BoundObject::into_any)
|
||||
.map(BoundObject::unbind),
|
||||
_ => Ok(other.py().NotImplemented()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rich_comparisons() {
|
||||
Python::attach(|py| {
|
||||
let c = Py::new(py, RichComparisons {}).unwrap();
|
||||
py_run!(py, c, "assert (c < c) == 'RC < RC'");
|
||||
py_run!(py, c, "assert (c < 1) == 'RC < 1'");
|
||||
py_run!(py, c, "assert (1 < c) == 'RC > 1'");
|
||||
py_run!(py, c, "assert (c <= c) == 'RC <= RC'");
|
||||
py_run!(py, c, "assert (c <= 1) == 'RC <= 1'");
|
||||
py_run!(py, c, "assert (1 <= c) == 'RC >= 1'");
|
||||
py_run!(py, c, "assert (c == c) == 'RC == RC'");
|
||||
py_run!(py, c, "assert (c == 1) == 'RC == 1'");
|
||||
py_run!(py, c, "assert (1 == c) == 'RC == 1'");
|
||||
py_run!(py, c, "assert (c != c) == 'RC != RC'");
|
||||
py_run!(py, c, "assert (c != 1) == 'RC != 1'");
|
||||
py_run!(py, c, "assert (1 != c) == 'RC != 1'");
|
||||
py_run!(py, c, "assert (c > c) == 'RC > RC'");
|
||||
py_run!(py, c, "assert (c > 1) == 'RC > 1'");
|
||||
py_run!(py, c, "assert (1 > c) == 'RC < 1'");
|
||||
py_run!(py, c, "assert (c >= c) == 'RC >= RC'");
|
||||
py_run!(py, c, "assert (c >= 1) == 'RC >= 1'");
|
||||
py_run!(py, c, "assert (1 >= c) == 'RC <= 1'");
|
||||
|
||||
// Ensure that passing a wrong self type from Python does not cause UB
|
||||
py_expect_exception!(py, c, "type(c).__lt__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__le__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__eq__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ne__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__gt__(object(), 1)", PyTypeError);
|
||||
py_expect_exception!(py, c, "type(c).__ge__(object(), 1)", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rich_comparisons_python_3_type_error() {
|
||||
Python::attach(|py| {
|
||||
let c2 = Py::new(py, RichComparisons2 {}).unwrap();
|
||||
py_expect_exception!(py, c2, "c2 < c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 < 1", PyTypeError);
|
||||
py_expect_exception!(py, c2, "1 < c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 <= c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 <= 1", PyTypeError);
|
||||
py_expect_exception!(py, c2, "1 <= c2", PyTypeError);
|
||||
py_run!(py, c2, "assert (c2 == c2) == True");
|
||||
py_run!(py, c2, "assert (c2 == 1) == True");
|
||||
py_run!(py, c2, "assert (1 == c2) == True");
|
||||
py_run!(py, c2, "assert (c2 != c2) == False");
|
||||
py_run!(py, c2, "assert (c2 != 1) == False");
|
||||
py_run!(py, c2, "assert (1 != c2) == False");
|
||||
py_expect_exception!(py, c2, "c2 > c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 > 1", PyTypeError);
|
||||
py_expect_exception!(py, c2, "1 > c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 >= c2", PyTypeError);
|
||||
py_expect_exception!(py, c2, "c2 >= 1", PyTypeError);
|
||||
py_expect_exception!(py, c2, "1 >= c2", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
// Checks that binary operations for which the arguments don't match the
|
||||
// required type, return NotImplemented.
|
||||
mod return_not_implemented {
|
||||
use super::*;
|
||||
|
||||
#[pyclass]
|
||||
struct RichComparisonToSelf {}
|
||||
|
||||
#[pymethods]
|
||||
impl RichComparisonToSelf {
|
||||
fn __repr__(&self) -> &'static str {
|
||||
"RC_Self"
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: PyRef<'_, Self>, _op: CompareOp) -> Py<PyAny> {
|
||||
other.py().None()
|
||||
}
|
||||
|
||||
fn __add__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __sub__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __mul__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __matmul__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __truediv__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __floordiv__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __mod__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __pow__(slf: PyRef<'_, Self>, _other: u8, _modulo: Option<u8>) -> PyRef<'_, Self> {
|
||||
slf
|
||||
}
|
||||
fn __lshift__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __rshift__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __divmod__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __and__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __or__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
fn __xor__<'p>(slf: PyRef<'p, Self>, _other: PyRef<'p, Self>) -> PyRef<'p, Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
// Inplace assignments
|
||||
fn __iadd__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __isub__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __imul__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __imatmul__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __itruediv__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __ifloordiv__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __imod__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __ilshift__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __irshift__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __iand__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __ior__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __ixor__(&mut self, _other: PyRef<'_, Self>) {}
|
||||
fn __ipow__(&mut self, _other: PyRef<'_, Self>, _modulo: Option<u8>) {}
|
||||
}
|
||||
|
||||
fn _test_binary_dunder(dunder: &str) {
|
||||
Python::attach(|py| {
|
||||
let c2 = Py::new(py, RichComparisonToSelf {}).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
c2,
|
||||
&format!("class Other: pass\nassert c2.__{dunder}__(Other()) is NotImplemented")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn _test_binary_operator(operator: &str, dunder: &str) {
|
||||
_test_binary_dunder(dunder);
|
||||
|
||||
Python::attach(|py| {
|
||||
let c2 = Py::new(py, RichComparisonToSelf {}).unwrap();
|
||||
py_expect_exception!(
|
||||
py,
|
||||
c2,
|
||||
format!("class Other: pass\nc2 {} Other()", operator),
|
||||
PyTypeError
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn _test_inplace_binary_operator(operator: &str, dunder: &str) {
|
||||
_test_binary_operator(operator, dunder);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equality() {
|
||||
_test_binary_dunder("eq");
|
||||
_test_binary_dunder("ne");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordering() {
|
||||
_test_binary_operator("<", "lt");
|
||||
_test_binary_operator("<=", "le");
|
||||
_test_binary_operator(">", "gt");
|
||||
_test_binary_operator(">=", "ge");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bitwise() {
|
||||
_test_binary_operator("&", "and");
|
||||
_test_binary_operator("|", "or");
|
||||
_test_binary_operator("^", "xor");
|
||||
_test_binary_operator("<<", "lshift");
|
||||
_test_binary_operator(">>", "rshift");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith() {
|
||||
_test_binary_operator("+", "add");
|
||||
_test_binary_operator("-", "sub");
|
||||
_test_binary_operator("*", "mul");
|
||||
_test_binary_operator("@", "matmul");
|
||||
_test_binary_operator("/", "truediv");
|
||||
_test_binary_operator("//", "floordiv");
|
||||
_test_binary_operator("%", "mod");
|
||||
_test_binary_operator("**", "pow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_arith() {
|
||||
_test_binary_dunder("radd");
|
||||
_test_binary_dunder("rsub");
|
||||
_test_binary_dunder("rmul");
|
||||
_test_binary_dunder("rmatmul");
|
||||
_test_binary_dunder("rtruediv");
|
||||
_test_binary_dunder("rfloordiv");
|
||||
_test_binary_dunder("rmod");
|
||||
_test_binary_dunder("rdivmod");
|
||||
_test_binary_dunder("rpow");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inplace_bitwise() {
|
||||
_test_inplace_binary_operator("&=", "iand");
|
||||
_test_inplace_binary_operator("|=", "ior");
|
||||
_test_inplace_binary_operator("^=", "ixor");
|
||||
_test_inplace_binary_operator("<<=", "ilshift");
|
||||
_test_inplace_binary_operator(">>=", "irshift");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inplace_arith() {
|
||||
_test_inplace_binary_operator("+=", "iadd");
|
||||
_test_inplace_binary_operator("-=", "isub");
|
||||
_test_inplace_binary_operator("*=", "imul");
|
||||
_test_inplace_binary_operator("@=", "imatmul");
|
||||
_test_inplace_binary_operator("/=", "itruediv");
|
||||
_test_inplace_binary_operator("//=", "ifloordiv");
|
||||
_test_inplace_binary_operator("%=", "imod");
|
||||
_test_inplace_binary_operator("**=", "ipow");
|
||||
}
|
||||
}
|
||||
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "macros")]
|
||||
#![cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
|
||||
use pyo3::{buffer::PyBuffer, exceptions::PyBufferError, ffi, prelude::*};
|
||||
use std::{
|
||||
ffi::{c_int, c_void},
|
||||
ptr,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
enum TestGetBufferError {
|
||||
NullShape,
|
||||
NullStrides,
|
||||
IncorrectItemSize,
|
||||
IncorrectFormat,
|
||||
IncorrectAlignment,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct TestBufferErrors {
|
||||
buf: Vec<u32>,
|
||||
error: Option<TestGetBufferError>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl TestBufferErrors {
|
||||
unsafe fn __getbuffer__(
|
||||
slf: PyRefMut<'_, Self>,
|
||||
view: *mut ffi::Py_buffer,
|
||||
flags: c_int,
|
||||
) -> PyResult<()> {
|
||||
if view.is_null() {
|
||||
return Err(PyBufferError::new_err("View is null"));
|
||||
}
|
||||
|
||||
if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE {
|
||||
return Err(PyBufferError::new_err("Object is not writable"));
|
||||
}
|
||||
|
||||
let bytes = &slf.buf;
|
||||
|
||||
unsafe {
|
||||
(*view).buf = bytes.as_ptr() as *mut c_void;
|
||||
(*view).len = bytes.len() as isize;
|
||||
(*view).readonly = 1;
|
||||
(*view).itemsize = std::mem::size_of::<u32>() as isize;
|
||||
|
||||
let msg = c"I";
|
||||
(*view).format = msg.as_ptr() as *mut _;
|
||||
|
||||
(*view).ndim = 1;
|
||||
(*view).shape = &mut (*view).len;
|
||||
|
||||
(*view).strides = &mut (*view).itemsize;
|
||||
|
||||
(*view).suboffsets = ptr::null_mut();
|
||||
(*view).internal = ptr::null_mut();
|
||||
|
||||
if let Some(err) = &slf.error {
|
||||
use TestGetBufferError::*;
|
||||
match err {
|
||||
NullShape => {
|
||||
(*view).shape = std::ptr::null_mut();
|
||||
}
|
||||
NullStrides => {
|
||||
(*view).strides = std::ptr::null_mut();
|
||||
}
|
||||
IncorrectItemSize => {
|
||||
(*view).itemsize += 1;
|
||||
}
|
||||
IncorrectFormat => {
|
||||
(*view).format = c"B".as_ptr() as _;
|
||||
}
|
||||
IncorrectAlignment => (*view).buf = (*view).buf.add(1),
|
||||
}
|
||||
}
|
||||
|
||||
(*view).obj = slf.into_ptr();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_buffer_errors() {
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(
|
||||
py,
|
||||
TestBufferErrors {
|
||||
buf: vec![0, 1, 2, 3],
|
||||
error: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(PyBuffer::<u32>::get(instance.bind(py)).is_ok());
|
||||
|
||||
instance.borrow_mut(py).error = Some(TestGetBufferError::NullShape);
|
||||
assert_eq!(
|
||||
PyBuffer::<u32>::get(instance.bind(py))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"BufferError: shape is null"
|
||||
);
|
||||
|
||||
instance.borrow_mut(py).error = Some(TestGetBufferError::NullStrides);
|
||||
assert_eq!(
|
||||
PyBuffer::<u32>::get(instance.bind(py))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"BufferError: strides is null"
|
||||
);
|
||||
|
||||
instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectItemSize);
|
||||
assert_eq!(
|
||||
PyBuffer::<u32>::get(instance.bind(py))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"BufferError: buffer contents are not compatible with u32"
|
||||
);
|
||||
|
||||
instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectFormat);
|
||||
assert_eq!(
|
||||
PyBuffer::<u32>::get(instance.bind(py))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"BufferError: buffer contents are not compatible with u32"
|
||||
);
|
||||
|
||||
instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectAlignment);
|
||||
assert_eq!(
|
||||
PyBuffer::<u32>::get(instance.bind(py))
|
||||
.unwrap_err()
|
||||
.to_string(),
|
||||
"BufferError: buffer contents are insufficiently aligned for u32"
|
||||
);
|
||||
});
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "macros")]
|
||||
#![cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
|
||||
use pyo3::buffer::PyBuffer;
|
||||
use pyo3::exceptions::PyBufferError;
|
||||
use pyo3::ffi;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::IntoPyDict;
|
||||
use std::ffi::CString;
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct TestBufferClass {
|
||||
vec: Vec<u8>,
|
||||
drop_called: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl TestBufferClass {
|
||||
unsafe fn __getbuffer__(
|
||||
slf: Bound<'_, Self>,
|
||||
view: *mut ffi::Py_buffer,
|
||||
flags: c_int,
|
||||
) -> PyResult<()> {
|
||||
unsafe { fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf.into_any()) }
|
||||
}
|
||||
|
||||
unsafe fn __releasebuffer__(&self, view: *mut ffi::Py_buffer) {
|
||||
// Release memory held by the format string
|
||||
drop(unsafe { CString::from_raw((*view).format) });
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestBufferClass {
|
||||
fn drop(&mut self) {
|
||||
print!("dropped");
|
||||
self.drop_called.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer() {
|
||||
let drop_called = Arc::new(AtomicBool::new(false));
|
||||
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(
|
||||
py,
|
||||
TestBufferClass {
|
||||
vec: vec![b' ', b'2', b'3'],
|
||||
drop_called: drop_called.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let env = [("ob", instance)].into_py_dict(py).unwrap();
|
||||
py_assert!(py, *env, "bytes(ob) == b' 23'");
|
||||
});
|
||||
|
||||
assert!(drop_called.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_referenced() {
|
||||
let drop_called = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let buf = {
|
||||
let input = vec![b' ', b'2', b'3'];
|
||||
Python::attach(|py| {
|
||||
let instance = TestBufferClass {
|
||||
vec: input.clone(),
|
||||
drop_called: drop_called.clone(),
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
|
||||
let buf = PyBuffer::<u8>::get(&instance).unwrap();
|
||||
assert_eq!(buf.to_vec(py).unwrap(), input);
|
||||
drop(instance);
|
||||
buf
|
||||
})
|
||||
};
|
||||
|
||||
assert!(!drop_called.load(Ordering::Relaxed));
|
||||
|
||||
Python::attach(|_| {
|
||||
drop(buf);
|
||||
});
|
||||
|
||||
assert!(drop_called.load(Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_releasebuffer_unraisable_error() {
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use test_utils::UnraisableCapture;
|
||||
|
||||
#[pyclass]
|
||||
struct ReleaseBufferError {}
|
||||
|
||||
#[pymethods]
|
||||
impl ReleaseBufferError {
|
||||
unsafe fn __getbuffer__(
|
||||
slf: Bound<'_, Self>,
|
||||
view: *mut ffi::Py_buffer,
|
||||
flags: c_int,
|
||||
) -> PyResult<()> {
|
||||
static BUF_BYTES: &[u8] = b"hello world";
|
||||
unsafe { fill_view_from_readonly_data(view, flags, BUF_BYTES, slf.into_any()) }
|
||||
}
|
||||
|
||||
unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) -> PyResult<()> {
|
||||
Err(PyValueError::new_err("oh dear"))
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(py, ReleaseBufferError {}).unwrap();
|
||||
|
||||
let (err, object) = UnraisableCapture::enter(py, |capture| {
|
||||
let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap();
|
||||
|
||||
assert!(capture.take_capture().is_none());
|
||||
|
||||
py_assert!(py, *env, "bytes(ob) == b'hello world'");
|
||||
|
||||
capture.take_capture().unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(err.to_string(), "ValueError: oh dear");
|
||||
assert!(object.is(&instance));
|
||||
});
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `view` must be a valid pointer to ffi::Py_buffer, or null
|
||||
/// `data` must outlive the Python lifetime of `owner` (i.e. data must be owned by owner, or data
|
||||
/// must be static data)
|
||||
unsafe fn fill_view_from_readonly_data(
|
||||
view: *mut ffi::Py_buffer,
|
||||
flags: c_int,
|
||||
data: &[u8],
|
||||
owner: Bound<'_, PyAny>,
|
||||
) -> PyResult<()> {
|
||||
if view.is_null() {
|
||||
return Err(PyBufferError::new_err("View is null"));
|
||||
}
|
||||
|
||||
if (flags & ffi::PyBUF_WRITABLE) == ffi::PyBUF_WRITABLE {
|
||||
return Err(PyBufferError::new_err("Object is not writable"));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
(*view).obj = owner.into_ptr();
|
||||
|
||||
(*view).buf = data.as_ptr() as *mut c_void;
|
||||
(*view).len = data.len() as isize;
|
||||
(*view).readonly = 1;
|
||||
(*view).itemsize = 1;
|
||||
|
||||
(*view).format = if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT {
|
||||
let msg = CString::new("B").unwrap();
|
||||
msg.into_raw()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
};
|
||||
|
||||
(*view).ndim = 1;
|
||||
(*view).shape = if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND {
|
||||
&mut (*view).len
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
};
|
||||
|
||||
(*view).strides = if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES {
|
||||
&mut (*view).itemsize
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
};
|
||||
|
||||
(*view).suboffsets = ptr::null_mut();
|
||||
(*view).internal = ptr::null_mut();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyBytes;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyfunction]
|
||||
fn bytes_pybytes_conversion(bytes: &[u8]) -> &[u8] {
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pybytes_bytes_conversion() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(bytes_pybytes_conversion)(py).unwrap();
|
||||
py_assert!(py, f, "f(b'Hello World') == b'Hello World'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn bytes_vec_conversion(py: Python<'_>, bytes: Vec<u8>) -> Bound<'_, PyBytes> {
|
||||
PyBytes::new(py, bytes.as_slice())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pybytes_vec_conversion() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
|
||||
py_assert!(py, f, "f(b'Hello World') == b'Hello World'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytearray_vec_conversion() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
|
||||
py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_py_as_bytes() {
|
||||
let pyobj: pyo3::Py<pyo3::types::PyBytes> =
|
||||
Python::attach(|py| pyo3::types::PyBytes::new(py, b"abc").unbind());
|
||||
|
||||
let data = Python::attach(|py| pyobj.as_bytes(py));
|
||||
|
||||
assert_eq!(data, b"abc");
|
||||
|
||||
Python::attach(move |_py| drop(pyobj));
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct Foo {
|
||||
#[pyo3(get)]
|
||||
x: i32,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct Bar {
|
||||
#[pyo3(get)]
|
||||
x: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Foo {
|
||||
#[classattr]
|
||||
const MY_CONST: &'static str = "foobar";
|
||||
|
||||
#[classattr]
|
||||
#[pyo3(name = "RENAMED_CONST")]
|
||||
const MY_CONST_2: &'static str = "foobar_2";
|
||||
|
||||
#[classattr]
|
||||
fn a() -> i32 {
|
||||
5
|
||||
}
|
||||
|
||||
#[classattr]
|
||||
#[pyo3(name = "B")]
|
||||
fn b() -> String {
|
||||
"bar".to_string()
|
||||
}
|
||||
|
||||
#[classattr]
|
||||
fn bar() -> Bar {
|
||||
Bar { x: 2 }
|
||||
}
|
||||
|
||||
#[classattr]
|
||||
fn a_foo() -> Foo {
|
||||
Foo { x: 1 }
|
||||
}
|
||||
|
||||
#[classattr]
|
||||
fn a_foo_with_py(py: Python<'_>) -> Py<Foo> {
|
||||
Py::new(py, Foo { x: 1 }).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_attributes() {
|
||||
Python::attach(|py| {
|
||||
let foo_obj = py.get_type::<Foo>();
|
||||
py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'");
|
||||
py_assert!(py, foo_obj, "foo_obj.RENAMED_CONST == 'foobar_2'");
|
||||
py_assert!(py, foo_obj, "foo_obj.a == 5");
|
||||
py_assert!(py, foo_obj, "foo_obj.B == 'bar'");
|
||||
py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1");
|
||||
py_assert!(py, foo_obj, "foo_obj.a_foo_with_py.x == 1");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_attributes_mutable() {
|
||||
#[pyclass]
|
||||
struct Foo {}
|
||||
|
||||
#[pymethods]
|
||||
impl Foo {
|
||||
#[classattr]
|
||||
const MY_CONST: &'static str = "foobar";
|
||||
|
||||
#[classattr]
|
||||
fn a() -> i32 {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = py.get_type::<Foo>();
|
||||
py_run!(py, obj, "obj.MY_CONST = 'BAZ'");
|
||||
py_run!(py, obj, "obj.a = 42");
|
||||
py_assert!(py, obj, "obj.MY_CONST == 'BAZ'");
|
||||
py_assert!(py, obj, "obj.a == 42");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_14, all(Py_3_10, not(Py_LIMITED_API))))]
|
||||
fn immutable_type_object() {
|
||||
#[pyclass(immutable_type)]
|
||||
struct ImmutableType {}
|
||||
|
||||
#[pymethods]
|
||||
impl ImmutableType {
|
||||
#[classattr]
|
||||
const MY_CONST: &'static str = "foobar";
|
||||
|
||||
#[classattr]
|
||||
fn a() -> i32 {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(immutable_type)]
|
||||
enum SimpleImmutable {
|
||||
Variant = 42,
|
||||
}
|
||||
|
||||
#[pyclass(immutable_type)]
|
||||
enum ComplexImmutable {
|
||||
Variant(u32),
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = py.get_type::<ImmutableType>();
|
||||
py_expect_exception!(py, obj, "obj.MY_CONST = 'FOOBAR'", PyTypeError);
|
||||
py_expect_exception!(py, obj, "obj.a = 6", PyTypeError);
|
||||
|
||||
let obj = py.get_type::<SimpleImmutable>();
|
||||
py_expect_exception!(py, obj, "obj.Variant = 0", PyTypeError);
|
||||
|
||||
let obj = py.get_type::<ComplexImmutable>();
|
||||
py_expect_exception!(py, obj, "obj.Variant = 0", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Bar {
|
||||
#[classattr]
|
||||
fn a_foo() -> Foo {
|
||||
Foo { x: 3 }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_class_attributes() {
|
||||
Python::attach(|py| {
|
||||
let foo_obj = py.get_type::<Foo>();
|
||||
let bar_obj = py.get_type::<Bar>();
|
||||
py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1");
|
||||
py_assert!(py, foo_obj, "foo_obj.bar.x == 2");
|
||||
py_assert!(py, bar_obj, "bar_obj.a_foo.x == 3");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(panic = "unwind")]
|
||||
fn test_fallible_class_attribute() {
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use test_utils::UnraisableCapture;
|
||||
|
||||
#[pyclass]
|
||||
struct BrokenClass;
|
||||
|
||||
#[pymethods]
|
||||
impl BrokenClass {
|
||||
#[classattr]
|
||||
fn fails_to_init() -> PyResult<i32> {
|
||||
Err(PyValueError::new_err("failed to create class attribute"))
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let (err, object) = UnraisableCapture::enter(py, |capture| {
|
||||
// Accessing the type will attempt to initialize the class attributes
|
||||
assert!(std::panic::catch_unwind(|| py.get_type::<BrokenClass>()).is_err());
|
||||
|
||||
capture.take_capture().unwrap()
|
||||
});
|
||||
|
||||
assert!(object.is_none());
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"RuntimeError: An error occurred while initializing class BrokenClass"
|
||||
);
|
||||
|
||||
let cause = err.cause(py).unwrap();
|
||||
assert_eq!(
|
||||
cause.to_string(),
|
||||
"RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`"
|
||||
);
|
||||
|
||||
let cause = cause.cause(py).unwrap();
|
||||
assert_eq!(
|
||||
cause.to_string(),
|
||||
"ValueError: failed to create class attribute"
|
||||
);
|
||||
assert!(cause.cause(py).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(get_all, set_all, rename_all = "camelCase")]
|
||||
struct StructWithRenamedFields {
|
||||
first_field: bool,
|
||||
second_field: u8,
|
||||
#[pyo3(name = "third_field")]
|
||||
fourth_field: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl StructWithRenamedFields {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
first_field: true,
|
||||
second_field: 5,
|
||||
fourth_field: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renaming_all_struct_fields() {
|
||||
use pyo3::types::PyBool;
|
||||
|
||||
Python::attach(|py| {
|
||||
let struct_class = py.get_type::<StructWithRenamedFields>();
|
||||
let struct_obj = struct_class.call0().unwrap();
|
||||
assert!(struct_obj
|
||||
.setattr("firstField", PyBool::new(py, false))
|
||||
.is_ok());
|
||||
py_assert!(py, struct_obj, "struct_obj.firstField == False");
|
||||
py_assert!(py, struct_obj, "struct_obj.secondField == 5");
|
||||
assert!(struct_obj
|
||||
.setattr("third_field", PyBool::new(py, true))
|
||||
.is_ok());
|
||||
py_assert!(py, struct_obj, "struct_obj.third_field == True");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(get_all, set_all, new = "from_fields")]
|
||||
struct AutoNewCls {
|
||||
a: i32,
|
||||
b: String,
|
||||
c: Option<f64>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_impl() {
|
||||
Python::attach(|py| {
|
||||
// python should be able to do AutoNewCls(1, "two", 3.0)
|
||||
let cls = py.get_type::<AutoNewCls>();
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
cls,
|
||||
"inst = cls(1, 'two', 3.0); assert inst.a == 1; assert inst.b == 'two'; assert inst.c == 3.0"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(new = "from_fields", get_all)]
|
||||
struct Point2d(#[pyo3(name = "first")] f64, #[pyo3(name = "second")] f64);
|
||||
|
||||
#[test]
|
||||
fn new_impl_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
// python should be able to do AutoNewCls(1, "two", 3.0)
|
||||
let cls = py.get_type::<Point2d>();
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
cls,
|
||||
"inst = cls(0.2, 0.3); assert inst.first == 0.2; assert inst.second == 0.3"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
macro_rules! test_case {
|
||||
($struct_name: ident, $rule: literal, $field_name: ident, $renamed_field_name: literal, $test_name: ident) => {
|
||||
#[pyclass(get_all, set_all, rename_all = $rule)]
|
||||
#[allow(non_snake_case)]
|
||||
struct $struct_name {
|
||||
$field_name: u8,
|
||||
}
|
||||
#[pymethods]
|
||||
impl $struct_name {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self { $field_name: 0 }
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
//use pyo3::types::PyInt;
|
||||
|
||||
Python::attach(|py| {
|
||||
let struct_class = py.get_type::<$struct_name>();
|
||||
let struct_obj = struct_class.call0().unwrap();
|
||||
assert!(struct_obj.setattr($renamed_field_name, 2).is_ok());
|
||||
let attr = struct_obj.getattr($renamed_field_name).unwrap();
|
||||
assert_eq!(2, attr.extract::<u8>().unwrap());
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_case!(
|
||||
LowercaseTest,
|
||||
"lowercase",
|
||||
fieldOne,
|
||||
"fieldone",
|
||||
test_rename_all_lowercase
|
||||
);
|
||||
test_case!(
|
||||
CamelCaseTest,
|
||||
"camelCase",
|
||||
field_one,
|
||||
"fieldOne",
|
||||
test_rename_all_camel_case
|
||||
);
|
||||
test_case!(
|
||||
KebabCaseTest,
|
||||
"kebab-case",
|
||||
field_one,
|
||||
"field-one",
|
||||
test_rename_all_kebab_case
|
||||
);
|
||||
test_case!(
|
||||
PascalCaseTest,
|
||||
"PascalCase",
|
||||
field_one,
|
||||
"FieldOne",
|
||||
test_rename_all_pascal_case
|
||||
);
|
||||
test_case!(
|
||||
ScreamingSnakeCaseTest,
|
||||
"SCREAMING_SNAKE_CASE",
|
||||
field_one,
|
||||
"FIELD_ONE",
|
||||
test_rename_all_screaming_snake_case
|
||||
);
|
||||
test_case!(
|
||||
ScreamingKebabCaseTest,
|
||||
"SCREAMING-KEBAB-CASE",
|
||||
field_one,
|
||||
"FIELD-ONE",
|
||||
test_rename_all_screaming_kebab_case
|
||||
);
|
||||
test_case!(
|
||||
SnakeCaseTest,
|
||||
"snake_case",
|
||||
fieldOne,
|
||||
"field_one",
|
||||
test_rename_all_snake_case
|
||||
);
|
||||
test_case!(
|
||||
UppercaseTest,
|
||||
"UPPERCASE",
|
||||
fieldOne,
|
||||
"FIELDONE",
|
||||
test_rename_all_uppercase
|
||||
);
|
||||
+820
@@ -0,0 +1,820 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::PyType;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use pyo3::PyClass;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct EmptyClass {}
|
||||
|
||||
#[test]
|
||||
fn empty_class() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<EmptyClass>();
|
||||
// By default, don't allow creating instances from python.
|
||||
assert!(typeobj.call((), None).is_err());
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__name__ == 'EmptyClass'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct UnitClass;
|
||||
|
||||
#[test]
|
||||
fn unit_class() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<UnitClass>();
|
||||
// By default, don't allow creating instances from python.
|
||||
assert!(typeobj.call((), None).is_err());
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__name__ == 'UnitClass'");
|
||||
});
|
||||
}
|
||||
|
||||
/// Line1
|
||||
///Line2
|
||||
/// Line3
|
||||
// this is not doc string
|
||||
#[pyclass]
|
||||
struct ClassWithDocs {
|
||||
/// Property field
|
||||
#[pyo3(get, set)]
|
||||
value: i32,
|
||||
|
||||
/// Read-only property field
|
||||
#[pyo3(get)]
|
||||
readonly: i32,
|
||||
|
||||
/// Write-only property field
|
||||
#[pyo3(set)]
|
||||
writeonly: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_with_docstr() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<ClassWithDocs>();
|
||||
py_run!(
|
||||
py,
|
||||
typeobj,
|
||||
"assert typeobj.__doc__ == 'Line1\\nLine2\\n Line3'"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
typeobj,
|
||||
"assert typeobj.value.__doc__ == 'Property field'"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
typeobj,
|
||||
"assert typeobj.readonly.__doc__ == 'Read-only property field'"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
typeobj,
|
||||
"assert typeobj.writeonly.__doc__ == 'Write-only property field'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(name = "CustomName")]
|
||||
struct EmptyClass2 {}
|
||||
|
||||
#[pymethods]
|
||||
impl EmptyClass2 {
|
||||
#[pyo3(name = "custom_fn")]
|
||||
fn bar(&self) {}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(name = "custom_static")]
|
||||
fn bar_static() {}
|
||||
|
||||
#[getter]
|
||||
#[pyo3(name = "custom_getter")]
|
||||
fn foo(&self) -> i32 {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_names() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<EmptyClass2>();
|
||||
py_assert!(py, typeobj, "typeobj.__name__ == 'CustomName'");
|
||||
py_assert!(py, typeobj, "typeobj.custom_fn.__name__ == 'custom_fn'");
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.custom_static.__name__ == 'custom_static'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.custom_getter.__name__ == 'custom_getter'"
|
||||
);
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'bar')");
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'bar_static')");
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'foo')");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(name = "loop")]
|
||||
struct ClassRustKeywords {
|
||||
#[pyo3(name = "unsafe", get, set)]
|
||||
unsafe_variable: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ClassRustKeywords {
|
||||
#[pyo3(name = "struct")]
|
||||
fn struct_method(&self) {}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(name = "type")]
|
||||
fn type_method() {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyword_names() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<ClassRustKeywords>();
|
||||
py_assert!(py, typeobj, "typeobj.__name__ == 'loop'");
|
||||
py_assert!(py, typeobj, "typeobj.struct.__name__ == 'struct'");
|
||||
py_assert!(py, typeobj, "typeobj.type.__name__ == 'type'");
|
||||
py_assert!(py, typeobj, "typeobj.unsafe.__name__ == 'unsafe'");
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'unsafe_variable')");
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'struct_method')");
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'type_method')");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RawIdents {
|
||||
#[pyo3(get, set)]
|
||||
r#type: i64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl RawIdents {
|
||||
fn r#fn(&self) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_idents() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<RawIdents>();
|
||||
py_assert!(py, typeobj, "not hasattr(typeobj, 'r#fn')");
|
||||
py_assert!(py, typeobj, "hasattr(typeobj, 'fn')");
|
||||
py_assert!(py, typeobj, "hasattr(typeobj, 'type')");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct EmptyClassInModule {}
|
||||
|
||||
// Ignored because heap types do not show up as being in builtins, instead they
|
||||
// raise AttributeError:
|
||||
// https://github.com/python/cpython/blob/v3.11.1/Objects/typeobject.c#L541-L570
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn empty_class_in_module() {
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::new(py, "test_module.nested").unwrap();
|
||||
module.add_class::<EmptyClassInModule>().unwrap();
|
||||
|
||||
let ty = module.getattr("EmptyClassInModule").unwrap();
|
||||
assert_eq!(
|
||||
ty.getattr("__name__").unwrap().extract::<String>().unwrap(),
|
||||
"EmptyClassInModule"
|
||||
);
|
||||
|
||||
let module: String = ty.getattr("__module__").unwrap().extract().unwrap();
|
||||
|
||||
// Rationale: The class can be added to many modules, but will only be initialized once.
|
||||
// We currently have no way of determining a canonical module, so builtins is better
|
||||
// than using whatever calls init first.
|
||||
assert_eq!(module, "builtins");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct ClassWithObjectField {
|
||||
// It used to be that PyObject was not supported with (get, set)
|
||||
// - this test is just ensuring it compiles.
|
||||
#[pyo3(get, set)]
|
||||
value: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ClassWithObjectField {
|
||||
#[new]
|
||||
fn new(value: Py<PyAny>) -> ClassWithObjectField {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_with_object_field() {
|
||||
Python::attach(|py| {
|
||||
let ty = py.get_type::<ClassWithObjectField>();
|
||||
py_assert!(py, ty, "ty(5).value == 5");
|
||||
py_assert!(py, ty, "ty(None).value == None");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(frozen, eq, hash)]
|
||||
#[derive(PartialEq, Hash)]
|
||||
struct ClassWithHash {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_with_hash() {
|
||||
Python::attach(|py| {
|
||||
use pyo3::types::IntoPyDict;
|
||||
let class = ClassWithHash { value: 42 };
|
||||
let hash = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
class.hash(&mut hasher);
|
||||
hasher.finish() as isize
|
||||
};
|
||||
|
||||
let env = [
|
||||
("obj", Py::new(py, class).unwrap().into_any()),
|
||||
("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, *env, "hash(obj) == hsh");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(unsendable, subclass)]
|
||||
struct UnsendableBase {
|
||||
value: std::rc::Rc<usize>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl UnsendableBase {
|
||||
#[new]
|
||||
fn new(value: usize) -> UnsendableBase {
|
||||
Self {
|
||||
value: std::rc::Rc::new(value),
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn value(&self) -> usize {
|
||||
*self.value
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=UnsendableBase)]
|
||||
struct UnsendableChild {}
|
||||
|
||||
#[pymethods]
|
||||
impl UnsendableChild {
|
||||
#[new]
|
||||
fn new(value: usize) -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(UnsendableBase::new(value)).add_subclass(UnsendableChild {})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn test_unsendable<T: PyClass + 'static>() -> PyResult<()> {
|
||||
let (keep_obj_here, obj) = Python::attach(|py| -> PyResult<_> {
|
||||
let obj: Py<T> = PyType::new::<T>(py).call1((5,))?.extract()?;
|
||||
|
||||
// Accessing the value inside this thread should not panic
|
||||
let caught_panic =
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> PyResult<_> {
|
||||
assert_eq!(obj.getattr(py, "value")?.extract::<usize>(py)?, 5);
|
||||
Ok(())
|
||||
}))
|
||||
.is_err();
|
||||
|
||||
assert!(!caught_panic);
|
||||
|
||||
Ok((obj.clone_ref(py), obj))
|
||||
})?;
|
||||
|
||||
let caught_panic = std::thread::spawn(move || {
|
||||
// This access must panic
|
||||
Python::attach(move |py| {
|
||||
obj.borrow(py);
|
||||
});
|
||||
})
|
||||
.join();
|
||||
|
||||
Python::attach(|_py| drop(keep_obj_here));
|
||||
|
||||
if let Err(err) = caught_panic {
|
||||
if let Some(msg) = err.downcast_ref::<String>() {
|
||||
panic!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If a class is marked as `unsendable`, it panics when accessed by another thread.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[should_panic(
|
||||
expected = "test_class_basics::UnsendableBase is unsendable, but sent to another thread"
|
||||
)]
|
||||
fn panic_unsendable_base() {
|
||||
test_unsendable::<UnsendableBase>().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[should_panic(
|
||||
expected = "test_class_basics::UnsendableBase is unsendable, but sent to another thread"
|
||||
)]
|
||||
fn panic_unsendable_child() {
|
||||
test_unsendable::<UnsendableChild>().unwrap();
|
||||
}
|
||||
|
||||
fn get_length(obj: &Bound<'_, PyAny>) -> PyResult<usize> {
|
||||
let length = obj.len()?;
|
||||
|
||||
Ok(length)
|
||||
}
|
||||
|
||||
fn is_even(obj: &Bound<'_, PyAny>) -> PyResult<bool> {
|
||||
obj.extract::<i32>().map(|i| i % 2 == 0)
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct ClassWithFromPyWithMethods {}
|
||||
|
||||
#[pymethods]
|
||||
impl ClassWithFromPyWithMethods {
|
||||
fn instance_method(&self, #[pyo3(from_py_with = get_length)] argument: usize) -> usize {
|
||||
argument
|
||||
}
|
||||
#[classmethod]
|
||||
fn classmethod(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
#[pyo3(from_py_with = Bound::<'_, PyAny>::len)] argument: usize,
|
||||
) -> usize {
|
||||
argument
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn staticmethod(#[pyo3(from_py_with = get_length)] argument: usize) -> usize {
|
||||
argument
|
||||
}
|
||||
|
||||
fn __contains__(&self, #[pyo3(from_py_with = is_even)] obj: bool) -> bool {
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pymethods_from_py_with() {
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(py, ClassWithFromPyWithMethods {}).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
instance,
|
||||
r#"
|
||||
arg = {1: 1, 2: 3}
|
||||
|
||||
assert instance.instance_method(arg) == 2
|
||||
assert instance.classmethod(arg) == 2
|
||||
assert instance.staticmethod(arg) == 2
|
||||
|
||||
assert 42 in instance
|
||||
assert 73 not in instance
|
||||
"#
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct TupleClass(#[pyo3(get, set, name = "value")] i32);
|
||||
|
||||
#[test]
|
||||
fn test_tuple_struct_class() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<TupleClass>();
|
||||
assert!(typeobj.call((), None).is_err());
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'");
|
||||
|
||||
let instance = Py::new(py, TupleClass(5)).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
instance,
|
||||
r#"
|
||||
assert instance.value == 5;
|
||||
instance.value = 1234;
|
||||
assert instance.value == 1234;
|
||||
"#
|
||||
);
|
||||
|
||||
assert_eq!(instance.borrow(py).0, 1234);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(dict, subclass)]
|
||||
struct DunderDictSupport {
|
||||
// Make sure that dict_offset runs with non-zero sized Self
|
||||
_pad: [u8; 32],
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn dunder_dict_support() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
r#"
|
||||
inst.a = 1
|
||||
assert inst.a == 1
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn access_dunder_dict() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
r#"
|
||||
inst.a = 1
|
||||
assert inst.__dict__ == {'a': 1}
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[test]
|
||||
fn dunder_dict_is_released() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
inst.setattr(py, "a", 1).unwrap();
|
||||
|
||||
let dict = inst.bind(py).getattr("__dict__").unwrap();
|
||||
let get_refcnt = || {
|
||||
// SAFETY: `dict` holds a valid reference while its reference count is read.
|
||||
unsafe { pyo3::ffi::Py_REFCNT(dict.as_ptr()) }
|
||||
};
|
||||
let refcnt = get_refcnt();
|
||||
|
||||
drop(inst);
|
||||
|
||||
assert_eq!(get_refcnt(), refcnt - 1);
|
||||
py_assert!(py, dict, "dict == {'a': 1}");
|
||||
});
|
||||
}
|
||||
|
||||
// The `__dict__` slot must hold exactly what CPython's `tp_new` left there: CPython 3.11
|
||||
// and 3.12 eagerly create the instance dict in `object.__new__` for types with a nonzero
|
||||
// `tp_dictoffset`, and the pyclass contents initialization must preserve it rather than
|
||||
// clobber it. All other versions create the dict lazily on first access.
|
||||
#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
|
||||
#[test]
|
||||
fn instance_dict_slot_is_not_clobbered() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Read the `__dict__` slot directly (see `_PyObject_GetDictPtr`), *without*
|
||||
// going through `__dict__`.
|
||||
// SAFETY: `inst` is a valid object whose type has a positive `tp_dictoffset`.
|
||||
let read_slot = || unsafe {
|
||||
let offset = (*pyo3::ffi::Py_TYPE(inst.as_ptr())).tp_dictoffset;
|
||||
assert!(offset > 0);
|
||||
*inst
|
||||
.as_ptr()
|
||||
.cast::<u8>()
|
||||
.offset(offset)
|
||||
.cast::<*mut pyo3::ffi::PyObject>()
|
||||
};
|
||||
|
||||
let slot_dict = read_slot();
|
||||
if cfg!(all(Py_3_11, not(Py_3_13))) {
|
||||
// `object.__new__` created the dict; it must survive pyclass initialization.
|
||||
assert!(
|
||||
!slot_dict.is_null(),
|
||||
"the eagerly created __dict__ was clobbered during pyclass initialization"
|
||||
);
|
||||
// The slot holds the only reference to it.
|
||||
// SAFETY: previous assert guarantees it's a valid PyObject
|
||||
assert_eq!(unsafe { pyo3::ffi::Py_REFCNT(slot_dict) }, 1);
|
||||
} else {
|
||||
// No eager creation on these versions; the slot starts out empty.
|
||||
assert!(slot_dict.is_null());
|
||||
}
|
||||
|
||||
// Whichever way the dict comes into existence, `__dict__` must be the dict
|
||||
// stored in the slot.
|
||||
let dict_attr = inst.bind(py).getattr("__dict__").unwrap();
|
||||
assert_eq!(read_slot(), dict_attr.as_ptr());
|
||||
});
|
||||
}
|
||||
|
||||
// If the base class has dict support, child class also has dict
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(extends=DunderDictSupport)]
|
||||
struct InheritDict {
|
||||
_value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn inherited_dict() {
|
||||
Python::attach(|py| {
|
||||
let initializer = PyClassInitializer::from(DunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
})
|
||||
.add_subclass(InheritDict { _value: 0 });
|
||||
let inst = Py::new(py, initializer).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
r#"
|
||||
inst.a = 1
|
||||
assert inst.a == 1
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(weakref, dict)]
|
||||
struct WeakRefDunderDictSupport {
|
||||
// Make sure that weaklist_offset runs with non-zero sized Self
|
||||
_pad: [u8; 32],
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn weakref_dunder_dict_support() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
WeakRefDunderDictSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"import weakref; assert weakref.ref(inst)() is inst; inst.a = 1; assert inst.a == 1"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(weakref, subclass)]
|
||||
struct WeakRefSupport {
|
||||
_pad: [u8; 32],
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn weakref_support() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
WeakRefSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"import weakref; assert weakref.ref(inst)() is inst"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// If the base class has weakref support, child class also has weakref.
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(extends=WeakRefSupport)]
|
||||
struct InheritWeakRef {
|
||||
_value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn inherited_weakref() {
|
||||
Python::attach(|py| {
|
||||
let initializer = PyClassInitializer::from(WeakRefSupport {
|
||||
_pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF",
|
||||
})
|
||||
.add_subclass(InheritWeakRef { _value: 0 });
|
||||
let inst = Py::new(py, initializer).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"import weakref; assert weakref.ref(inst)() is inst"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_frozen_class_without_gil() {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[pyclass(frozen)]
|
||||
struct FrozenCounter {
|
||||
value: AtomicUsize,
|
||||
}
|
||||
|
||||
let py_counter: Py<FrozenCounter> = Python::attach(|py| {
|
||||
let counter = FrozenCounter {
|
||||
value: AtomicUsize::new(0),
|
||||
};
|
||||
|
||||
let cell = Bound::new(py, counter).unwrap();
|
||||
|
||||
cell.get().value.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
cell.into()
|
||||
});
|
||||
|
||||
assert_eq!(py_counter.get().value.load(Ordering::Relaxed), 1);
|
||||
|
||||
Python::attach(move |_py| drop(py_counter));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)]
|
||||
fn drop_unsendable_elsewhere() {
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread::spawn;
|
||||
use test_utils::UnraisableCapture;
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
struct Unsendable {
|
||||
dropped: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for Unsendable {
|
||||
fn drop(&mut self) {
|
||||
self.dropped.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let (err, object) = UnraisableCapture::enter(py, |capture| {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let unsendable = Py::new(
|
||||
py,
|
||||
Unsendable {
|
||||
dropped: dropped.clone(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py.detach(|| {
|
||||
spawn(move || {
|
||||
Python::attach(move |py| {
|
||||
drop(unsendable);
|
||||
// On the free-threaded build, dropping an object on its non-origin thread
|
||||
// will not immediately drop it because the refcounts need to be merged.
|
||||
//
|
||||
// Force GC to ensure the drop happens now on the wrong thread.
|
||||
py.run(c"import gc; gc.collect()", None, None).unwrap();
|
||||
});
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert!(!dropped.load(Ordering::SeqCst));
|
||||
|
||||
capture.take_capture().unwrap()
|
||||
});
|
||||
|
||||
assert_eq!(err.to_string(), "RuntimeError: test_class_basics::drop_unsendable_elsewhere::Unsendable is unsendable, but is being dropped on another thread");
|
||||
assert!(object.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn test_unsendable_dict() {
|
||||
#[pyclass(dict, unsendable)]
|
||||
struct UnsendableDictClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl UnsendableDictClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
UnsendableDictClass {}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, UnsendableDictClass {}).unwrap();
|
||||
py_run!(py, inst, "assert inst.__dict__ == {}");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn test_unsendable_dict_with_weakref() {
|
||||
#[pyclass(dict, unsendable, weakref)]
|
||||
struct UnsendableDictClassWithWeakRef {}
|
||||
|
||||
#[pymethods]
|
||||
impl UnsendableDictClassWithWeakRef {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, UnsendableDictClassWithWeakRef {}).unwrap();
|
||||
py_run!(py, inst, "assert inst.__dict__ == {}");
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"import weakref; assert weakref.ref(inst)() is inst; inst.a = 1; assert inst.a == 1"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(Py_3_9)]
|
||||
#[pyclass(generic)]
|
||||
struct ClassWithRuntimeParametrization {
|
||||
#[pyo3(get, set)]
|
||||
value: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_9)]
|
||||
#[pymethods]
|
||||
impl ClassWithRuntimeParametrization {
|
||||
#[new]
|
||||
fn new(value: Py<PyAny>) -> ClassWithRuntimeParametrization {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(Py_3_9)]
|
||||
fn test_runtime_parametrization() {
|
||||
Python::attach(|py| {
|
||||
let ty = py.get_type::<ClassWithRuntimeParametrization>();
|
||||
py_assert!(py, ty, "ty[int] == ty.__class_getitem__((int,))");
|
||||
py_run!(
|
||||
py,
|
||||
ty,
|
||||
"import types;
|
||||
assert ty.__class_getitem__((int,)) == types.GenericAlias(ty, (int,))"
|
||||
);
|
||||
});
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(eq, skip_from_py_object)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MyEnum {
|
||||
Variant,
|
||||
OtherVariant,
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub enum MyEnumOrd {
|
||||
Variant,
|
||||
OtherVariant,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_eq_enum() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let var2 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let other_var = Py::new(py, MyEnum::OtherVariant).unwrap();
|
||||
py_assert!(py, var1 var2, "var1 == var2");
|
||||
py_assert!(py, var1 other_var, "var1 != other_var");
|
||||
py_assert!(py, var1 var2, "(var1 != var2) == False");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_eq_incomparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
py_assert!(py, var1, "(var1 == 'foo') == False");
|
||||
py_assert!(py, var1, "(var1 != 'foo') == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_ord_comparable_opt_in_only() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let var2 = Py::new(py, MyEnum::OtherVariant).unwrap();
|
||||
// ordering on simple enums if opt in only, thus raising an error below
|
||||
py_expect_exception!(py, var1 var2, "(var1 > var2) == False", PyTypeError);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_enum_ord_comparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnumOrd::Variant).unwrap();
|
||||
let var2 = Py::new(py, MyEnumOrd::OtherVariant).unwrap();
|
||||
let var3 = Py::new(py, MyEnumOrd::OtherVariant).unwrap();
|
||||
py_assert!(py, var1 var2, "(var1 > var2) == False");
|
||||
py_assert!(py, var1 var2, "(var1 < var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 >= var2) == False");
|
||||
py_assert!(py, var2 var3, "(var3 >= var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 <= var2) == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub enum MyComplexEnumOrd {
|
||||
Variant(i32),
|
||||
OtherVariant(String),
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub enum MyComplexEnumOrd2 {
|
||||
Variant { msg: String, idx: u32 },
|
||||
OtherVariant { name: String, idx: u32 },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_enum_ord_comparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyComplexEnumOrd::Variant(-2)).unwrap();
|
||||
let var2 = Py::new(py, MyComplexEnumOrd::Variant(5)).unwrap();
|
||||
let var3 = Py::new(py, MyComplexEnumOrd::OtherVariant("a".to_string())).unwrap();
|
||||
let var4 = Py::new(py, MyComplexEnumOrd::OtherVariant("b".to_string())).unwrap();
|
||||
py_assert!(py, var1 var2, "(var1 > var2) == False");
|
||||
py_assert!(py, var1 var2, "(var1 < var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 >= var2) == False");
|
||||
py_assert!(py, var1 var2, "(var1 <= var2) == True");
|
||||
|
||||
py_assert!(py, var1 var3, "(var1 >= var3) == False");
|
||||
py_assert!(py, var1 var3, "(var1 <= var3) == True");
|
||||
|
||||
py_assert!(py, var3 var4, "(var3 >= var4) == False");
|
||||
py_assert!(py, var3 var4, "(var3 <= var4) == True");
|
||||
|
||||
let var5 = Py::new(
|
||||
py,
|
||||
MyComplexEnumOrd2::Variant {
|
||||
msg: "hello".to_string(),
|
||||
idx: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var6 = Py::new(
|
||||
py,
|
||||
MyComplexEnumOrd2::Variant {
|
||||
msg: "hello".to_string(),
|
||||
idx: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var7 = Py::new(
|
||||
py,
|
||||
MyComplexEnumOrd2::Variant {
|
||||
msg: "goodbye".to_string(),
|
||||
idx: 7,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var8 = Py::new(
|
||||
py,
|
||||
MyComplexEnumOrd2::Variant {
|
||||
msg: "about".to_string(),
|
||||
idx: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var9 = Py::new(
|
||||
py,
|
||||
MyComplexEnumOrd2::OtherVariant {
|
||||
name: "albert".to_string(),
|
||||
idx: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, var5 var6, "(var5 == var6) == True");
|
||||
py_assert!(py, var5 var6, "(var5 <= var6) == True");
|
||||
py_assert!(py, var6 var7, "(var6 <= var7) == False");
|
||||
py_assert!(py, var6 var7, "(var6 >= var7) == True");
|
||||
py_assert!(py, var5 var8, "(var5 > var8) == True");
|
||||
py_assert!(py, var8 var9, "(var9 > var8) == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub struct Point {
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_numeric_ord_comparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, Point { x: 10, y: 2, z: 3 }).unwrap();
|
||||
let var2 = Py::new(py, Point { x: 2, y: 2, z: 3 }).unwrap();
|
||||
let var3 = Py::new(py, Point { x: 1, y: 22, z: 4 }).unwrap();
|
||||
let var4 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap();
|
||||
let var5 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap();
|
||||
py_assert!(py, var1 var2, "(var1 > var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 <= var2) == False");
|
||||
py_assert!(py, var2 var3, "(var3 < var2) == True");
|
||||
py_assert!(py, var3 var4, "(var3 > var4) == True");
|
||||
py_assert!(py, var4 var5, "(var4 == var5) == True");
|
||||
py_assert!(py, var3 var5, "(var3 != var5) == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub struct Person {
|
||||
surname: String,
|
||||
given_name: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_string_ord_comparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(
|
||||
py,
|
||||
Person {
|
||||
surname: "zzz".to_string(),
|
||||
given_name: "bob".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var2 = Py::new(
|
||||
py,
|
||||
Person {
|
||||
surname: "aaa".to_string(),
|
||||
given_name: "sally".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var3 = Py::new(
|
||||
py,
|
||||
Person {
|
||||
surname: "eee".to_string(),
|
||||
given_name: "qqq".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var4 = Py::new(
|
||||
py,
|
||||
Person {
|
||||
surname: "ddd".to_string(),
|
||||
given_name: "aaa".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, var1 var2, "(var1 > var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 <= var2) == False");
|
||||
py_assert!(py, var1 var3, "(var1 >= var3) == True");
|
||||
py_assert!(py, var2 var3, "(var2 >= var3) == False");
|
||||
py_assert!(py, var3 var4, "(var3 >= var4) == True");
|
||||
py_assert!(py, var3 var4, "(var3 != var4) == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, ord, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Record {
|
||||
name: String,
|
||||
title: String,
|
||||
idx: u32,
|
||||
}
|
||||
|
||||
impl PartialOrd for Record {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.idx.partial_cmp(&other.idx).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_custom_ord_comparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(
|
||||
py,
|
||||
Record {
|
||||
name: "zzz".to_string(),
|
||||
title: "bbb".to_string(),
|
||||
idx: 9,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var2 = Py::new(
|
||||
py,
|
||||
Record {
|
||||
name: "ddd".to_string(),
|
||||
title: "aaa".to_string(),
|
||||
idx: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var3 = Py::new(
|
||||
py,
|
||||
Record {
|
||||
name: "vvv".to_string(),
|
||||
title: "ggg".to_string(),
|
||||
idx: 19,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let var4 = Py::new(
|
||||
py,
|
||||
Record {
|
||||
name: "vvv".to_string(),
|
||||
title: "ggg".to_string(),
|
||||
idx: 19,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, var1 var2, "(var1 > var2) == True");
|
||||
py_assert!(py, var1 var2, "(var1 <= var2) == False");
|
||||
py_assert!(py, var1 var3, "(var1 >= var3) == False");
|
||||
py_assert!(py, var2 var3, "(var2 >= var3) == False");
|
||||
py_assert!(py, var3 var4, "(var3 == var4) == True");
|
||||
py_assert!(py, var2 var4, "(var2 != var4) == True");
|
||||
})
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(from_py_object)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Cloneable {
|
||||
x: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cloneable_pyclass() {
|
||||
let c = Cloneable { x: 10 };
|
||||
|
||||
Python::attach(|py| {
|
||||
let py_c = Py::new(py, c.clone()).unwrap();
|
||||
|
||||
let c2: Cloneable = py_c.extract(py).unwrap();
|
||||
assert_eq!(c, c2);
|
||||
{
|
||||
let rc: PyRef<'_, Cloneable> = py_c.extract(py).unwrap();
|
||||
assert_eq!(&c, &*rc);
|
||||
// Drops PyRef before taking PyRefMut
|
||||
}
|
||||
let mrc: PyRefMut<'_, Cloneable> = py_c.extract(py).unwrap();
|
||||
assert_eq!(&c, &*mrc);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
#[derive(Default)]
|
||||
struct BaseClass {
|
||||
value: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BaseClass {
|
||||
fn foo(&self) -> &'static str {
|
||||
"BaseClass"
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=BaseClass)]
|
||||
struct SubClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl SubClass {
|
||||
fn foo(&self) -> &'static str {
|
||||
"SubClass"
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct PolymorphicContainer {
|
||||
#[pyo3(get, set)]
|
||||
inner: Py<BaseClass>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphic_container_stores_base_class() {
|
||||
Python::attach(|py| {
|
||||
let p = Py::new(
|
||||
py,
|
||||
PolymorphicContainer {
|
||||
inner: Py::new(py, BaseClass::default()).unwrap(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, p, "p.inner.foo() == 'BaseClass'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphic_container_stores_sub_class() {
|
||||
Python::attach(|py| {
|
||||
let p = Py::new(
|
||||
py,
|
||||
PolymorphicContainer {
|
||||
inner: Py::new(py, BaseClass::default()).unwrap(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
p.bind(py)
|
||||
.setattr(
|
||||
"inner",
|
||||
Py::new(
|
||||
py,
|
||||
PyClassInitializer::from(BaseClass::default()).add_subclass(SubClass {}),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, p, "p.inner.foo() == 'SubClass'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polymorphic_container_does_not_accept_other_types() {
|
||||
Python::attach(|py| {
|
||||
let p = Py::new(
|
||||
py,
|
||||
PolymorphicContainer {
|
||||
inner: Py::new(py, BaseClass::default()).unwrap(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let setattr = |value: Bound<'_, PyAny>| p.bind(py).setattr("inner", value);
|
||||
|
||||
assert!(setattr(1i32.into_pyobject(py).unwrap().into_any()).is_err());
|
||||
assert!(setattr(py.None().into_bound(py)).is_err());
|
||||
assert!(setattr((1i32, 2i32).into_pyobject(py).unwrap().into_any()).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyref_as_base() {
|
||||
Python::attach(|py| {
|
||||
let initializer =
|
||||
PyClassInitializer::from(BaseClass { value: 120 }).add_subclass(SubClass {});
|
||||
let cell = Bound::new(py, initializer).unwrap();
|
||||
|
||||
// First try PyRefMut
|
||||
let sub: PyRefMut<'_, SubClass> = cell.borrow_mut();
|
||||
let mut base: PyRefMut<'_, BaseClass> = sub.into_super();
|
||||
assert_eq!(120, base.value);
|
||||
base.value = 999;
|
||||
assert_eq!(999, base.value);
|
||||
drop(base);
|
||||
|
||||
// Repeat for PyRef
|
||||
let sub: PyRef<'_, SubClass> = cell.borrow();
|
||||
let base: PyRef<'_, BaseClass> = sub.into_super();
|
||||
assert_eq!(999, base.value);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pycell_deref() {
|
||||
Python::attach(|py| {
|
||||
let initializer =
|
||||
PyClassInitializer::from(BaseClass { value: 120 }).add_subclass(SubClass {});
|
||||
let obj = Bound::new(py, initializer).unwrap();
|
||||
|
||||
// Should be able to deref as PyAny
|
||||
assert_eq!(
|
||||
obj.call_method0("foo")
|
||||
.and_then(|e| e.extract::<String>())
|
||||
.unwrap(),
|
||||
"SubClass"
|
||||
);
|
||||
});
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(eq, str)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MyEnum2 {
|
||||
Variant,
|
||||
OtherVariant,
|
||||
}
|
||||
|
||||
impl Display for MyEnum2 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(eq, str)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MyEnum3 {
|
||||
#[pyo3(name = "AwesomeVariant")]
|
||||
Variant,
|
||||
OtherVariant,
|
||||
}
|
||||
|
||||
impl Display for MyEnum3 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let variant = match self {
|
||||
MyEnum3::Variant => "AwesomeVariant",
|
||||
MyEnum3::OtherVariant => "OtherVariant",
|
||||
};
|
||||
write!(f, "MyEnum.{variant}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_class_fmt() {
|
||||
Python::attach(|py| {
|
||||
let var2 = Py::new(py, MyEnum2::Variant).unwrap();
|
||||
let var3 = Py::new(py, MyEnum3::Variant).unwrap();
|
||||
let var4 = Py::new(py, MyEnum3::OtherVariant).unwrap();
|
||||
py_assert!(py, var2, "str(var2) == 'Variant'");
|
||||
py_assert!(py, var3, "str(var3) == 'MyEnum.AwesomeVariant'");
|
||||
py_assert!(py, var4, "str(var4) == 'MyEnum.OtherVariant'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str = "X: {x}, Y: {y}, Z: {z}", skip_from_py_object)]
|
||||
#[derive(PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub struct Point {
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_struct_custom_str() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, Point { x: 1, y: 2, z: 3 }).unwrap();
|
||||
py_assert!(py, var1, "str(var1) == 'X: 1, Y: 2, Z: 3'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str, skip_from_py_object)]
|
||||
#[derive(PartialEq, Eq, Clone, PartialOrd)]
|
||||
pub struct Point2 {
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: i32,
|
||||
}
|
||||
|
||||
impl Display for Point2 {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {}, {})", self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_str() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, Point2 { x: 1, y: 2, z: 3 }).unwrap();
|
||||
py_assert!(py, var1, "str(var1) == '(1, 2, 3)'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str)]
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum ComplexEnumWithStr {
|
||||
A(u32),
|
||||
B { msg: String },
|
||||
}
|
||||
|
||||
impl Display for ComplexEnumWithStr {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{self:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_complex_enum_str() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, ComplexEnumWithStr::A(45)).unwrap();
|
||||
let var2 = Py::new(
|
||||
py,
|
||||
ComplexEnumWithStr::B {
|
||||
msg: "Hello".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_assert!(py, var1, "str(var1) == 'A(45)'");
|
||||
py_assert!(py, var2, "str(var2) == 'B { msg: \"Hello\" }'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str = "{0}, {1}, {2}")]
|
||||
#[derive(PartialEq)]
|
||||
struct Coord(u32, u32, u32);
|
||||
|
||||
#[pyclass(str = "{{{0}, {1}, {2}}}")]
|
||||
#[derive(PartialEq)]
|
||||
struct Coord2(u32, u32, u32);
|
||||
|
||||
#[test]
|
||||
fn test_str_representation_by_position() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, Coord(1, 2, 3)).unwrap();
|
||||
let var2 = Py::new(py, Coord2(1, 2, 3)).unwrap();
|
||||
py_assert!(py, var1, "str(var1) == '1, 2, 3'");
|
||||
py_assert!(py, var2, "str(var2) == '{1, 2, 3}'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str = "name: {name}: {name}, idn: {idn:03} with message: {msg}")]
|
||||
#[derive(PartialEq, Debug)]
|
||||
struct Point4 {
|
||||
name: String,
|
||||
msg: String,
|
||||
idn: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_and_repeated_str_formats() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(
|
||||
py,
|
||||
Point4 {
|
||||
name: "aaa".to_string(),
|
||||
msg: "hello".to_string(),
|
||||
idn: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
var1,
|
||||
r#"
|
||||
assert str(var1) == 'name: aaa: aaa, idn: 001 with message: hello'
|
||||
"#
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(str = "type: {r#type}")]
|
||||
struct Foo {
|
||||
r#type: u32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_identifier_struct_custom_str() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, Foo { r#type: 3 }).unwrap();
|
||||
py_assert!(py, var1, "str(var1) == 'type: 3'");
|
||||
})
|
||||
}
|
||||
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct Base {
|
||||
num: u32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Base {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self { num: 0 }
|
||||
}
|
||||
|
||||
fn __init__(&mut self) {
|
||||
self.num += 42
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base_init() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<Base>();
|
||||
let obj = typeobj.call((), None).unwrap().cast_into::<Base>().unwrap();
|
||||
// check __init__ was called
|
||||
assert_eq!(obj.borrow().num, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(extends=Base)]
|
||||
struct SubWithoutInit;
|
||||
|
||||
#[pymethods]
|
||||
impl SubWithoutInit {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(Base::new()).add_subclass(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subclass_without_init_calls_base_init() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<SubWithoutInit>();
|
||||
let obj = typeobj
|
||||
.call((), None)
|
||||
.unwrap()
|
||||
.cast_into::<SubWithoutInit>()
|
||||
.unwrap();
|
||||
// check Base.__init__ was called
|
||||
assert_eq!(obj.as_super().borrow().num, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(extends=Base)]
|
||||
struct SubWithInit;
|
||||
|
||||
#[pymethods]
|
||||
impl SubWithInit {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(Base::new()).add_subclass(Self)
|
||||
}
|
||||
|
||||
fn __init__(mut slf: pyo3::PyClassGuardMut<'_, Self>) {
|
||||
slf.as_super().__init__(); // need to call super __init__ manually
|
||||
slf.as_super().num += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subclass_with_init() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<SubWithInit>();
|
||||
let obj = typeobj
|
||||
.call((), None)
|
||||
.unwrap()
|
||||
.cast_into::<SubWithInit>()
|
||||
.unwrap();
|
||||
// check SubWithInit.__init__ was called, and Base.__init__ was only called once (through
|
||||
// SubWithInit.__init__)
|
||||
assert_eq!(obj.as_super().borrow().num, 43);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_arbitrary_object() {
|
||||
#[pyclass]
|
||||
struct A {}
|
||||
|
||||
#[pyclass]
|
||||
struct B {}
|
||||
|
||||
#[pymethods]
|
||||
impl A {
|
||||
#[new]
|
||||
fn __new__() -> B {
|
||||
B {}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<A>();
|
||||
let obj = typeobj.call((), None).unwrap();
|
||||
|
||||
assert!(obj.is_instance_of::<B>());
|
||||
});
|
||||
}
|
||||
Vendored
+325
@@ -0,0 +1,325 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::sync::PyOnceLock;
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
#[pyclass]
|
||||
struct EmptyClassWithNew {}
|
||||
|
||||
#[pymethods]
|
||||
impl EmptyClassWithNew {
|
||||
#[new]
|
||||
fn new() -> EmptyClassWithNew {
|
||||
EmptyClassWithNew {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_class_with_new() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<EmptyClassWithNew>();
|
||||
assert!(typeobj
|
||||
.call((), None)
|
||||
.unwrap()
|
||||
.cast::<EmptyClassWithNew>()
|
||||
.is_ok());
|
||||
|
||||
// Calling with arbitrary args or kwargs is not ok
|
||||
assert!(typeobj.call(("some", "args"), None).is_err());
|
||||
assert!(typeobj
|
||||
.call((), Some(&[("some", "kwarg")].into_py_dict(py).unwrap()))
|
||||
.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct UnitClassWithNew;
|
||||
|
||||
#[pymethods]
|
||||
impl UnitClassWithNew {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_class_with_new() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<UnitClassWithNew>();
|
||||
assert!(typeobj
|
||||
.call((), None)
|
||||
.unwrap()
|
||||
.cast::<UnitClassWithNew>()
|
||||
.is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct TupleClassWithNew(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl TupleClassWithNew {
|
||||
#[new]
|
||||
fn new(arg: i32) -> Self {
|
||||
Self(arg)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_class_with_new() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<TupleClassWithNew>();
|
||||
let wrp = typeobj.call((42,), None).unwrap();
|
||||
let obj = wrp.cast::<TupleClassWithNew>().unwrap();
|
||||
let obj_ref = obj.borrow();
|
||||
assert_eq!(obj_ref.0, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug)]
|
||||
struct NewWithOneArg {
|
||||
data: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl NewWithOneArg {
|
||||
#[new]
|
||||
fn new(arg: i32) -> NewWithOneArg {
|
||||
NewWithOneArg { data: arg }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_one_arg() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<NewWithOneArg>();
|
||||
let wrp = typeobj.call((42,), None).unwrap();
|
||||
let obj = wrp.cast::<NewWithOneArg>().unwrap();
|
||||
let obj_ref = obj.borrow();
|
||||
assert_eq!(obj_ref.data, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct NewWithTwoArgs {
|
||||
data1: i32,
|
||||
data2: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl NewWithTwoArgs {
|
||||
#[new]
|
||||
fn new(arg1: i32, arg2: i32) -> Self {
|
||||
NewWithTwoArgs {
|
||||
data1: arg1,
|
||||
data2: arg2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_two_args() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<NewWithTwoArgs>();
|
||||
let wrp = typeobj
|
||||
.call((10, 20), None)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
let obj = wrp.cast::<NewWithTwoArgs>().unwrap();
|
||||
let obj_ref = obj.borrow();
|
||||
assert_eq!(obj_ref.data1, 10);
|
||||
assert_eq!(obj_ref.data2, 20);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct SuperClass {
|
||||
#[pyo3(get)]
|
||||
from_rust: bool,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SuperClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
SuperClass { from_rust: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that `subclass.__new__` works correctly.
|
||||
/// See https://github.com/PyO3/pyo3/issues/947 for the corresponding bug.
|
||||
#[test]
|
||||
fn subclass_new() {
|
||||
Python::attach(|py| {
|
||||
let super_cls = py.get_type::<SuperClass>();
|
||||
let source = pyo3_ffi::c_str!(
|
||||
r#"
|
||||
class Class(SuperClass):
|
||||
def __new__(cls):
|
||||
return super().__new__(cls) # This should return an instance of Class
|
||||
|
||||
@property
|
||||
def from_rust(self):
|
||||
return False
|
||||
c = Class()
|
||||
assert c.from_rust is False
|
||||
"#
|
||||
);
|
||||
let globals = PyModule::import(py, "__main__").unwrap().dict();
|
||||
globals.set_item("SuperClass", super_cls).unwrap();
|
||||
py.run(source, Some(&globals), None)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug)]
|
||||
struct NewWithCustomError {}
|
||||
|
||||
struct CustomError;
|
||||
|
||||
impl From<CustomError> for PyErr {
|
||||
fn from(_error: CustomError) -> PyErr {
|
||||
PyValueError::new_err("custom error")
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl NewWithCustomError {
|
||||
#[new]
|
||||
fn new() -> Result<NewWithCustomError, CustomError> {
|
||||
Err(CustomError)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_with_custom_error() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<NewWithCustomError>();
|
||||
let err = typeobj.call0().unwrap_err();
|
||||
assert_eq!(err.to_string(), "ValueError: custom error");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct NewExisting {
|
||||
#[pyo3(get)]
|
||||
num: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl NewExisting {
|
||||
#[new]
|
||||
fn new(py: pyo3::Python<'_>, val: usize) -> pyo3::Py<NewExisting> {
|
||||
static PRE_BUILT: PyOnceLock<[pyo3::Py<NewExisting>; 2]> = PyOnceLock::new();
|
||||
let existing = PRE_BUILT.get_or_init(py, || {
|
||||
[
|
||||
pyo3::Py::new(py, NewExisting { num: 0 }).unwrap(),
|
||||
pyo3::Py::new(py, NewExisting { num: 1 }).unwrap(),
|
||||
]
|
||||
});
|
||||
|
||||
if val < existing.len() {
|
||||
return existing[val].clone_ref(py);
|
||||
}
|
||||
|
||||
pyo3::Py::new(py, NewExisting { num: val }).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_existing() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<NewExisting>();
|
||||
|
||||
let obj1 = typeobj.call1((0,)).unwrap();
|
||||
let obj2 = typeobj.call1((0,)).unwrap();
|
||||
let obj3 = typeobj.call1((1,)).unwrap();
|
||||
let obj4 = typeobj.call1((1,)).unwrap();
|
||||
let obj5 = typeobj.call1((2,)).unwrap();
|
||||
let obj6 = typeobj.call1((2,)).unwrap();
|
||||
|
||||
assert_eq!(obj1.getattr("num").unwrap().extract::<u32>().unwrap(), 0);
|
||||
assert_eq!(obj2.getattr("num").unwrap().extract::<u32>().unwrap(), 0);
|
||||
assert_eq!(obj3.getattr("num").unwrap().extract::<u32>().unwrap(), 1);
|
||||
assert_eq!(obj4.getattr("num").unwrap().extract::<u32>().unwrap(), 1);
|
||||
assert_eq!(obj5.getattr("num").unwrap().extract::<u32>().unwrap(), 2);
|
||||
assert_eq!(obj6.getattr("num").unwrap().extract::<u32>().unwrap(), 2);
|
||||
|
||||
assert!(obj1.is(&obj2));
|
||||
assert!(obj3.is(&obj4));
|
||||
assert!(!obj1.is(&obj3));
|
||||
assert!(!obj1.is(&obj5));
|
||||
assert!(!obj5.is(&obj6));
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct NewReturnsPy;
|
||||
|
||||
#[pymethods]
|
||||
impl NewReturnsPy {
|
||||
#[new]
|
||||
fn new(py: Python<'_>) -> PyResult<Py<NewReturnsPy>> {
|
||||
Py::new(py, NewReturnsPy)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_returns_py() {
|
||||
Python::attach(|py| {
|
||||
let type_ = py.get_type::<NewReturnsPy>();
|
||||
let obj = type_.call0().unwrap();
|
||||
assert!(obj.is_exact_instance_of::<NewReturnsPy>());
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct NewReturnsBound;
|
||||
|
||||
#[pymethods]
|
||||
impl NewReturnsBound {
|
||||
#[new]
|
||||
fn new(py: Python<'_>) -> PyResult<Bound<'_, NewReturnsBound>> {
|
||||
Bound::new(py, NewReturnsBound)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_returns_bound() {
|
||||
Python::attach(|py| {
|
||||
let type_ = py.get_type::<NewReturnsBound>();
|
||||
let obj = type_.call0().unwrap();
|
||||
assert!(obj.is_exact_instance_of::<NewReturnsBound>());
|
||||
})
|
||||
}
|
||||
|
||||
#[pyo3::pyclass]
|
||||
struct NewClassMethod {
|
||||
#[pyo3(get)]
|
||||
cls: pyo3::Py<PyAny>,
|
||||
}
|
||||
|
||||
#[pyo3::pymethods]
|
||||
impl NewClassMethod {
|
||||
#[new]
|
||||
#[classmethod]
|
||||
fn new(cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self {
|
||||
Self {
|
||||
cls: cls.clone().into_any().unbind(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_class_method() {
|
||||
pyo3::Python::attach(|py| {
|
||||
let cls = py.get_type::<NewClassMethod>();
|
||||
pyo3::py_run!(py, cls, "assert cls().cls is cls");
|
||||
});
|
||||
}
|
||||
Vendored
+480
@@ -0,0 +1,480 @@
|
||||
#![cfg(feature = "experimental-async")]
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
use std::{ffi::CString, task::Poll, thread, time::Duration};
|
||||
|
||||
use futures::{channel::oneshot, future::poll_fn, FutureExt};
|
||||
#[cfg(not(target_has_atomic = "64"))]
|
||||
use portable_atomic::{AtomicBool, Ordering};
|
||||
use pyo3::{
|
||||
coroutine::CancelHandle,
|
||||
prelude::*,
|
||||
py_run,
|
||||
types::{IntoPyDict, PyDict, PyTuple, PyType},
|
||||
};
|
||||
#[cfg(target_has_atomic = "64")]
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
fn handle_windows(test: &str) -> String {
|
||||
let set_event_loop_policy = r#"
|
||||
import asyncio, sys
|
||||
if sys.platform == "win32":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
"#;
|
||||
pyo3::impl_::unindent::unindent(set_event_loop_policy) + &pyo3::impl_::unindent::unindent(test)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noop_coroutine() {
|
||||
#[pyfunction]
|
||||
async fn noop() -> usize {
|
||||
42
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let noop = wrap_pyfunction!(noop, py).unwrap();
|
||||
let test = "import asyncio; assert asyncio.run(noop()) == 42";
|
||||
py_run!(py, noop, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_function_returns_unit_none() {
|
||||
#[pyfunction]
|
||||
async fn returns_unit() {}
|
||||
|
||||
Python::attach(|py| {
|
||||
let returns_unit = wrap_pyfunction!(returns_unit, py).unwrap();
|
||||
let test = "import asyncio; assert asyncio.run(returns_unit()) is None";
|
||||
pyo3::py_run!(py, returns_unit, &handle_windows(test));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coroutine_qualname() {
|
||||
#[pyfunction]
|
||||
async fn my_fn() {}
|
||||
#[pyclass]
|
||||
struct MyClass;
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
// TODO use &self when possible
|
||||
async fn my_method(_self: Py<Self>) {}
|
||||
#[classmethod]
|
||||
async fn my_classmethod(_cls: Py<PyType>) {}
|
||||
#[staticmethod]
|
||||
async fn my_staticmethod() {}
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
for coro, name, qualname in [
|
||||
(my_fn(), "my_fn", "my_fn"),
|
||||
(MyClass().my_method(), "my_method", "MyClass.my_method"),
|
||||
#(MyClass().my_classmethod(), "my_classmethod", "MyClass.my_classmethod"),
|
||||
(MyClass.my_staticmethod(), "my_staticmethod", "MyClass.my_staticmethod"),
|
||||
]:
|
||||
assert coro.__name__ == name and coro.__qualname__ == qualname
|
||||
"#;
|
||||
let locals = [
|
||||
("my_fn", wrap_pyfunction!(my_fn, py).unwrap().as_any()),
|
||||
("MyClass", py.get_type::<MyClass>().as_any()),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *locals, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_0_like_coroutine() {
|
||||
#[pyfunction]
|
||||
async fn sleep_0() -> usize {
|
||||
let mut waken = false;
|
||||
poll_fn(|cx| {
|
||||
if !waken {
|
||||
cx.waker().wake_by_ref();
|
||||
waken = true;
|
||||
return Poll::Pending;
|
||||
}
|
||||
Poll::Ready(42)
|
||||
})
|
||||
.await
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let sleep_0 = wrap_pyfunction!(sleep_0, py).unwrap();
|
||||
let test = "import asyncio; assert asyncio.run(sleep_0()) == 42";
|
||||
py_run!(py, sleep_0, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
async fn sleep(seconds: f64) -> usize {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
thread::spawn(move || {
|
||||
thread::sleep(Duration::from_secs_f64(seconds));
|
||||
tx.send(42).unwrap();
|
||||
});
|
||||
rx.await.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_coroutine() {
|
||||
Python::attach(|py| {
|
||||
let sleep = wrap_pyfunction!(sleep, py).unwrap();
|
||||
let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#;
|
||||
py_run!(py, sleep, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
async fn return_tuple() -> (usize, usize) {
|
||||
(42, 43)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_coroutine() {
|
||||
Python::attach(|py| {
|
||||
let func = wrap_pyfunction!(return_tuple, py).unwrap();
|
||||
let test = r#"import asyncio; assert asyncio.run(func()) == (42, 43)"#;
|
||||
py_run!(py, func, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelled_coroutine() {
|
||||
Python::attach(|py| {
|
||||
let sleep = wrap_pyfunction!(sleep, py).unwrap();
|
||||
let test = r#"
|
||||
import asyncio
|
||||
async def main():
|
||||
task = asyncio.create_task(sleep(999))
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await task
|
||||
asyncio.run(main())
|
||||
"#;
|
||||
let globals = PyDict::new(py);
|
||||
globals.set_item("sleep", sleep).unwrap();
|
||||
let err = py
|
||||
.run(
|
||||
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
|
||||
Some(&globals),
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err.value(py).get_type().qualname().unwrap(),
|
||||
"CancelledError"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coroutine_cancel_handle() {
|
||||
#[pyfunction]
|
||||
async fn cancellable_sleep(
|
||||
seconds: f64,
|
||||
#[pyo3(cancel_handle)] mut cancel: CancelHandle,
|
||||
) -> usize {
|
||||
futures::select! {
|
||||
_ = sleep(seconds).fuse() => 42,
|
||||
_ = cancel.cancelled().fuse() => 0,
|
||||
}
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, py).unwrap();
|
||||
let test = r#"
|
||||
import asyncio;
|
||||
async def main():
|
||||
task = asyncio.create_task(cancellable_sleep(999))
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
return await task
|
||||
assert asyncio.run(main()) == 0
|
||||
"#;
|
||||
let globals = PyDict::new(py);
|
||||
globals
|
||||
.set_item("cancellable_sleep", cancellable_sleep)
|
||||
.unwrap();
|
||||
py.run(
|
||||
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
|
||||
Some(&globals),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coroutine_is_cancelled() {
|
||||
#[pyfunction]
|
||||
async fn sleep_loop(#[pyo3(cancel_handle)] cancel: CancelHandle) {
|
||||
while !cancel.is_cancelled() {
|
||||
sleep(0.001).await;
|
||||
}
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let sleep_loop = wrap_pyfunction!(sleep_loop, py).unwrap();
|
||||
let test = r#"
|
||||
import asyncio;
|
||||
async def main():
|
||||
task = asyncio.create_task(sleep_loop())
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await task
|
||||
asyncio.run(main())
|
||||
"#;
|
||||
let globals = PyDict::new(py);
|
||||
globals.set_item("sleep_loop", sleep_loop).unwrap();
|
||||
py.run(
|
||||
&CString::new(pyo3::impl_::unindent::unindent(&handle_windows(test))).unwrap(),
|
||||
Some(&globals),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coroutine_panic() {
|
||||
#[pyfunction]
|
||||
async fn panic() {
|
||||
panic!("test panic");
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let panic = wrap_pyfunction!(panic, py).unwrap();
|
||||
let test = r#"
|
||||
import asyncio
|
||||
coro = panic()
|
||||
try:
|
||||
asyncio.run(coro)
|
||||
except BaseException as err:
|
||||
assert type(err).__name__ == "PanicException"
|
||||
assert str(err) == "test panic"
|
||||
else:
|
||||
assert False
|
||||
try:
|
||||
coro.send(None)
|
||||
except RuntimeError as err:
|
||||
assert str(err) == "cannot reuse already awaited coroutine"
|
||||
else:
|
||||
assert False;
|
||||
"#;
|
||||
py_run!(py, panic, &handle_windows(test));
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_method_receiver() {
|
||||
#[pyclass]
|
||||
struct Counter(usize);
|
||||
#[pymethods]
|
||||
impl Counter {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
async fn get(&self, resolve: bool) -> usize {
|
||||
if !resolve {
|
||||
// hang the future to test borrow checking
|
||||
std::future::pending().await
|
||||
}
|
||||
self.0
|
||||
}
|
||||
async fn incr(&mut self, resolve: bool) -> usize {
|
||||
if !resolve {
|
||||
// hang the future to test borrow checking
|
||||
std::future::pending().await
|
||||
}
|
||||
self.0 += 1;
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
static IS_DROPPED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
impl Drop for Counter {
|
||||
fn drop(&mut self) {
|
||||
IS_DROPPED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
import asyncio
|
||||
|
||||
obj = Counter()
|
||||
|
||||
assert asyncio.run(obj.get(True)) == 0
|
||||
assert asyncio.run(obj.incr(True)) == 1
|
||||
|
||||
for left in [obj.get, obj.incr]:
|
||||
for right in [obj.get, obj.incr]:
|
||||
# first future will not resolve to hold the borrow
|
||||
coro1 = left(False)
|
||||
coro2 = right(True)
|
||||
try:
|
||||
asyncio.run(asyncio.gather(coro1, coro2))
|
||||
except RuntimeError as err:
|
||||
ran = False
|
||||
else:
|
||||
ran = True
|
||||
if left is obj.incr or right is obj.incr:
|
||||
assert not ran, "mutable method calls should not run concurrently with other method calls"
|
||||
"#;
|
||||
let locals = [("Counter", py.get_type::<Counter>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *locals, test);
|
||||
});
|
||||
|
||||
assert!(IS_DROPPED.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_method_receiver_with_other_args() {
|
||||
#[pyclass]
|
||||
struct Value(i32);
|
||||
#[pymethods]
|
||||
impl Value {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
async fn get_value_plus_with(&self, v1: i32, v2: i32) -> i32 {
|
||||
self.0 + v1 + v2
|
||||
}
|
||||
async fn set_value(&mut self, new_value: i32) -> i32 {
|
||||
self.0 = new_value;
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
import asyncio
|
||||
|
||||
v = Value()
|
||||
assert asyncio.run(v.get_value_plus_with(3, 0)) == 3
|
||||
assert asyncio.run(v.set_value(10)) == 10
|
||||
assert asyncio.run(v.get_value_plus_with(1, 1)) == 12
|
||||
"#;
|
||||
let locals = [("Value", py.get_type::<Value>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *locals, test);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_fn_borrowed_values() {
|
||||
#[pyclass]
|
||||
struct Data {
|
||||
value: String,
|
||||
}
|
||||
#[pymethods]
|
||||
impl Data {
|
||||
#[new]
|
||||
fn new(value: String) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
async fn borrow_value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
async fn borrow_value_or_default<'a>(&'a self, default: &'a str) -> &'a str {
|
||||
if self.value.is_empty() {
|
||||
default
|
||||
} else {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
}
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
import asyncio
|
||||
|
||||
v = Data('hello')
|
||||
assert asyncio.run(v.borrow_value()) == 'hello'
|
||||
assert asyncio.run(v.borrow_value_or_default('')) == 'hello'
|
||||
|
||||
v_empty = Data('')
|
||||
assert asyncio.run(v_empty.borrow_value_or_default('default')) == 'default'
|
||||
"#;
|
||||
let locals = [("Data", py.get_type::<Data>())].into_py_dict(py).unwrap();
|
||||
py_run!(py, *locals, test);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_fn_class_values() {
|
||||
#[pyclass]
|
||||
struct Value(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl Value {
|
||||
#[new]
|
||||
fn new(x: i32) -> Self {
|
||||
Self(x)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn value(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
async fn add_two_values(obj: &Value, obj2: &Value) -> Value {
|
||||
Value(obj.0 + obj2.0)
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
import asyncio
|
||||
|
||||
v1 = Value(1)
|
||||
v2 = Value(2)
|
||||
assert asyncio.run(add_two_values(v1, v2)).value == 3
|
||||
"#;
|
||||
let locals = [
|
||||
("Value", py.get_type::<Value>().into_any()),
|
||||
(
|
||||
"add_two_values",
|
||||
wrap_pyfunction!(add_two_values, py).unwrap().into_any(),
|
||||
),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *locals, test);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_function_args_kwargs() {
|
||||
#[pyfunction(signature = (*args, **kwargs))]
|
||||
async fn args_kwargs(
|
||||
args: Py<PyTuple>,
|
||||
kwargs: Option<Py<PyDict>>,
|
||||
) -> (Py<PyTuple>, Option<Py<PyDict>>) {
|
||||
(args, kwargs)
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let test = r#"
|
||||
import asyncio
|
||||
|
||||
assert asyncio.run(args_kwargs(1, 2, a=3, b=4)) == ((1, 2), {'a': 3, 'b': 4})
|
||||
"#;
|
||||
let locals = [(
|
||||
"args_kwargs",
|
||||
wrap_pyfunction!(args_kwargs, py).unwrap().into_any(),
|
||||
)]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *locals, test);
|
||||
});
|
||||
}
|
||||
Vendored
+226
@@ -0,0 +1,226 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(not(Py_LIMITED_API))]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{IntoPyDict, PyDate, PyDateTime, PyTime, PyTzInfo};
|
||||
use pyo3_ffi::PyDateTime_IMPORT;
|
||||
use std::ffi::CString;
|
||||
|
||||
fn _get_subclasses<'py>(
|
||||
py: Python<'py>,
|
||||
py_type: &str,
|
||||
args: &str,
|
||||
) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>)> {
|
||||
// Import the class from Python and create some subclasses
|
||||
let datetime = py.import("datetime")?;
|
||||
|
||||
let locals = [(py_type, datetime.getattr(py_type)?)]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
let make_subclass_py = CString::new(format!("class Subklass({py_type}):\n pass"))?;
|
||||
|
||||
let make_sub_subclass_py = c"class SubSubklass(Subklass):\n pass";
|
||||
|
||||
py.run(&make_subclass_py, None, Some(&locals))?;
|
||||
py.run(make_sub_subclass_py, None, Some(&locals))?;
|
||||
|
||||
// Construct an instance of the base class
|
||||
let obj = py.eval(
|
||||
&CString::new(format!("{py_type}({args})"))?,
|
||||
None,
|
||||
Some(&locals),
|
||||
)?;
|
||||
|
||||
// Construct an instance of the subclass
|
||||
let sub_obj = py.eval(
|
||||
&CString::new(format!("Subklass({args})"))?,
|
||||
None,
|
||||
Some(&locals),
|
||||
)?;
|
||||
|
||||
// Construct an instance of the sub-subclass
|
||||
let sub_sub_obj = py.eval(
|
||||
&CString::new(format!("SubSubklass({args})"))?,
|
||||
None,
|
||||
Some(&locals),
|
||||
)?;
|
||||
|
||||
Ok((obj, sub_obj, sub_sub_obj))
|
||||
}
|
||||
|
||||
macro_rules! assert_check_exact {
|
||||
($check_func:ident, $check_func_exact:ident, $obj: expr) => {
|
||||
unsafe {
|
||||
use pyo3::ffi::*;
|
||||
assert_ne!($check_func(($obj).as_ptr()), 0);
|
||||
assert_ne!($check_func_exact(($obj).as_ptr()), 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_check_only {
|
||||
($check_func:ident, $check_func_exact:ident, $obj: expr) => {
|
||||
unsafe {
|
||||
use pyo3::ffi::*;
|
||||
assert_ne!($check_func(($obj).as_ptr()), 0);
|
||||
assert_eq!($check_func_exact(($obj).as_ptr()), 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_check() {
|
||||
Python::attach(|py| {
|
||||
let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "date", "2018, 1, 1").unwrap();
|
||||
unsafe { PyDateTime_IMPORT() }
|
||||
assert_check_exact!(PyDate_Check, PyDate_CheckExact, obj);
|
||||
assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_obj);
|
||||
assert_check_only!(PyDate_Check, PyDate_CheckExact, sub_sub_obj);
|
||||
assert!(obj.is_instance_of::<PyDate>());
|
||||
assert!(!obj.is_instance_of::<PyTime>());
|
||||
assert!(!obj.is_instance_of::<PyDateTime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_check() {
|
||||
Python::attach(|py| {
|
||||
let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "time", "12, 30, 15").unwrap();
|
||||
unsafe { PyDateTime_IMPORT() }
|
||||
|
||||
assert_check_exact!(PyTime_Check, PyTime_CheckExact, obj);
|
||||
assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_obj);
|
||||
assert_check_only!(PyTime_Check, PyTime_CheckExact, sub_sub_obj);
|
||||
assert!(!obj.is_instance_of::<PyDate>());
|
||||
assert!(obj.is_instance_of::<PyTime>());
|
||||
assert!(!obj.is_instance_of::<PyDateTime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_check() {
|
||||
Python::attach(|py| {
|
||||
let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "datetime", "2018, 1, 1, 13, 30, 15")
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
unsafe { PyDateTime_IMPORT() }
|
||||
|
||||
assert_check_only!(PyDate_Check, PyDate_CheckExact, obj);
|
||||
assert_check_exact!(PyDateTime_Check, PyDateTime_CheckExact, obj);
|
||||
assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_obj);
|
||||
assert_check_only!(PyDateTime_Check, PyDateTime_CheckExact, sub_sub_obj);
|
||||
assert!(obj.is_instance_of::<PyDate>());
|
||||
assert!(!obj.is_instance_of::<PyTime>());
|
||||
assert!(obj.is_instance_of::<PyDateTime>());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delta_check() {
|
||||
Python::attach(|py| {
|
||||
let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "timedelta", "1, -3").unwrap();
|
||||
unsafe { PyDateTime_IMPORT() }
|
||||
|
||||
assert_check_exact!(PyDelta_Check, PyDelta_CheckExact, obj);
|
||||
assert_check_only!(PyDelta_Check, PyDelta_CheckExact, sub_obj);
|
||||
assert_check_only!(PyDelta_Check, PyDelta_CheckExact, sub_sub_obj);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_utc() {
|
||||
use assert_approx_eq::assert_approx_eq;
|
||||
use pyo3::types::PyDateTime;
|
||||
|
||||
Python::attach(|py| {
|
||||
let utc = PyTzInfo::utc(py).unwrap();
|
||||
|
||||
let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap();
|
||||
|
||||
let locals = [("dt", dt)].into_py_dict(py).unwrap();
|
||||
|
||||
let offset: f32 = py
|
||||
.eval(c"dt.utcoffset().total_seconds()", None, Some(&locals))
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
assert_approx_eq!(offset, 0f32);
|
||||
});
|
||||
}
|
||||
|
||||
static INVALID_DATES: &[(i32, u8, u8)] = &[
|
||||
(-1, 1, 1),
|
||||
(0, 1, 1),
|
||||
(10000, 1, 1),
|
||||
(2 << 30, 1, 1),
|
||||
(2018, 0, 1),
|
||||
(2018, 13, 1),
|
||||
(2018, 1, 0),
|
||||
(2017, 2, 29),
|
||||
(2018, 1, 32),
|
||||
];
|
||||
|
||||
static INVALID_TIMES: &[(u8, u8, u8, u32)] =
|
||||
&[(25, 0, 0, 0), (255, 0, 0, 0), (0, 60, 0, 0), (0, 0, 61, 0)];
|
||||
|
||||
#[test]
|
||||
fn test_pydate_out_of_bounds() {
|
||||
use pyo3::types::PyDate;
|
||||
|
||||
Python::attach(|py| {
|
||||
for val in INVALID_DATES {
|
||||
let (year, month, day) = val;
|
||||
let dt = PyDate::new(py, *year, *month, *day);
|
||||
dt.unwrap_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pytime_out_of_bounds() {
|
||||
use pyo3::types::PyTime;
|
||||
|
||||
Python::attach(|py| {
|
||||
for val in INVALID_TIMES {
|
||||
let (hour, minute, second, microsecond) = val;
|
||||
let dt = PyTime::new(py, *hour, *minute, *second, *microsecond, None);
|
||||
dt.unwrap_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pydatetime_out_of_bounds() {
|
||||
use pyo3::types::PyDateTime;
|
||||
use std::iter;
|
||||
|
||||
Python::attach(|py| {
|
||||
let valid_time = (0, 0, 0, 0);
|
||||
let valid_date = (2018, 1, 1);
|
||||
|
||||
let invalid_dates = INVALID_DATES.iter().zip(iter::repeat(&valid_time));
|
||||
let invalid_times = iter::repeat(&valid_date).zip(INVALID_TIMES.iter());
|
||||
|
||||
let vals = invalid_dates.chain(invalid_times);
|
||||
|
||||
for val in vals {
|
||||
let (date, time) = val;
|
||||
let (year, month, day) = date;
|
||||
let (hour, minute, second, microsecond) = time;
|
||||
let dt = PyDateTime::new(
|
||||
py,
|
||||
*year,
|
||||
*month,
|
||||
*day,
|
||||
*hour,
|
||||
*minute,
|
||||
*second,
|
||||
*microsecond,
|
||||
None,
|
||||
);
|
||||
dt.unwrap_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#![cfg(not(Py_LIMITED_API))]
|
||||
|
||||
use pyo3::{prelude::*, types::PyDate};
|
||||
use tempfile::Builder;
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "module 'datetime' has no attribute 'datetime_CAPI'")]
|
||||
fn test_bad_datetime_module_panic() {
|
||||
// Create an empty temporary directory
|
||||
// with an empty "datetime" module which we'll put on the sys.path
|
||||
let tmpdir = Builder::new()
|
||||
.prefix("pyo3_test_data_check")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
std::fs::File::create(tmpdir.path().join("datetime.py")).unwrap();
|
||||
|
||||
Python::attach(|py: Python<'_>| {
|
||||
let sys = py.import("sys").unwrap();
|
||||
sys.getattr("path")
|
||||
.unwrap()
|
||||
.call_method1("insert", (0, tmpdir.path().as_os_str()))
|
||||
.unwrap();
|
||||
|
||||
// This should panic because the "datetime" module is empty
|
||||
PyDate::new(py, 2018, 1, 1).unwrap();
|
||||
});
|
||||
tmpdir.close().unwrap();
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use pyo3::create_exception;
|
||||
use pyo3::exceptions::PyException;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::sync::OnceLockExt;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
mod some_module {
|
||||
use pyo3::create_exception;
|
||||
use pyo3::exceptions::PyException;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass]
|
||||
pub struct SomePyClass;
|
||||
|
||||
create_exception!(some_module, SomeException, PyException);
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct ValueClass {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ValueClass {
|
||||
#[new]
|
||||
fn new(value: usize) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = "module")]
|
||||
pub struct LocatedClass {}
|
||||
|
||||
#[pyfunction]
|
||||
fn double(x: usize) -> usize {
|
||||
x * 2
|
||||
}
|
||||
|
||||
create_exception!(
|
||||
declarative_module,
|
||||
MyError,
|
||||
PyException,
|
||||
"Some description."
|
||||
);
|
||||
|
||||
#[pymodule(submodule)]
|
||||
mod external_submodule {}
|
||||
|
||||
/// A module written using declarative syntax.
|
||||
#[pymodule]
|
||||
mod declarative_module {
|
||||
#[pymodule_export]
|
||||
use super::declarative_submodule;
|
||||
#[pymodule_export]
|
||||
// This is not a real constraint but to test cfg attribute support
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
use super::LocatedClass;
|
||||
use super::*;
|
||||
#[pymodule_export]
|
||||
use super::{declarative_module2, double, MyError, ValueClass as Value};
|
||||
|
||||
// test for #4036
|
||||
#[pymodule_export]
|
||||
use super::some_module::SomePyClass;
|
||||
|
||||
// test for #4036
|
||||
#[pymodule_export]
|
||||
use super::some_module::SomeException;
|
||||
|
||||
#[pymodule_export]
|
||||
use super::external_submodule;
|
||||
|
||||
#[pymodule_export]
|
||||
const FOO: u32 = 42;
|
||||
|
||||
#[pymodule_export]
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
const BAR: &str = "BAR";
|
||||
|
||||
#[pymodule_export]
|
||||
#[expect(non_upper_case_globals)]
|
||||
const r#type: char = '!';
|
||||
|
||||
#[expect(unused)]
|
||||
const NOT_EXPORTED: &str = "not exported";
|
||||
|
||||
#[pymodule]
|
||||
mod inner {
|
||||
use super::*;
|
||||
|
||||
#[pyfunction]
|
||||
fn triple(x: usize) -> usize {
|
||||
x * 3
|
||||
}
|
||||
|
||||
#[pyclass(name = "Struct")]
|
||||
struct Struct;
|
||||
|
||||
#[pymethods]
|
||||
impl Struct {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = "foo")]
|
||||
struct StructInCustomModule;
|
||||
|
||||
#[pyclass(eq, eq_int, name = "Enum")]
|
||||
#[derive(PartialEq)]
|
||||
enum Enum {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, module = "foo")]
|
||||
#[derive(PartialEq)]
|
||||
enum EnumInCustomModule {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
#[pyo3(module = "custom_root")]
|
||||
mod inner_custom_root {
|
||||
use super::*;
|
||||
|
||||
#[pyclass]
|
||||
struct Struct;
|
||||
}
|
||||
|
||||
#[pyo3::prelude::pymodule]
|
||||
mod full_path_inner {}
|
||||
|
||||
#[pymodule_init]
|
||||
fn init(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add("double2", m.getattr("double")?)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn double_value(v: &ValueClass) -> usize {
|
||||
v.value * 2
|
||||
}
|
||||
|
||||
#[pymodule(module = "declarative_module")]
|
||||
mod declarative_submodule {
|
||||
#[pymodule_export]
|
||||
use super::{double, double_value};
|
||||
}
|
||||
|
||||
#[pymodule(name = "declarative_module_renamed")]
|
||||
mod declarative_module2 {
|
||||
#[pymodule_export]
|
||||
use super::double;
|
||||
}
|
||||
|
||||
fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> {
|
||||
static MODULE: OnceLock<Py<PyModule>> = OnceLock::new();
|
||||
|
||||
MODULE
|
||||
.get_or_init_py_attached(py, || pyo3::wrap_pymodule!(declarative_module)(py))
|
||||
.bind(py)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_declarative_module() {
|
||||
Python::attach(|py| {
|
||||
let m = declarative_module(py);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.__doc__ == 'A module written using declarative syntax.'"
|
||||
);
|
||||
|
||||
py_assert!(py, m, "m.double(2) == 4");
|
||||
py_assert!(py, m, "m.inner.triple(3) == 9");
|
||||
py_assert!(py, m, "m.declarative_submodule.double(4) == 8");
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.declarative_submodule.double_value(m.ValueClass(1)) == 2"
|
||||
);
|
||||
py_assert!(py, m, "str(m.MyError('foo')) == 'foo'");
|
||||
py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4");
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
py_assert!(py, m, "not hasattr(m, 'LocatedClass')");
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
py_assert!(py, m, "hasattr(m, 'LocatedClass')");
|
||||
py_assert!(py, m, "isinstance(m.inner.Struct(), m.inner.Struct)");
|
||||
py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)");
|
||||
py_assert!(py, m, "hasattr(m, 'external_submodule')");
|
||||
py_assert!(py, m, "m.FOO == 42");
|
||||
#[cfg(Py_LIMITED_API)]
|
||||
py_assert!(py, m, "m.BAR == 'BAR'");
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
py_assert!(py, m, "not hasattr(m, 'BAR')");
|
||||
py_assert!(py, m, "m.type == '!'");
|
||||
py_assert!(py, m, "not hasattr(m, 'NOT_EXPORTED')");
|
||||
|
||||
// submodule dunder name and attribute name
|
||||
// declarative_module.inner is declared inside
|
||||
py_assert!(py, m, "m.inner.__name__ == 'declarative_module.inner'");
|
||||
py_assert!(py, m, "'inner' in m.__dict__");
|
||||
py_assert!(py, m, "'declarative_module.inner' not in m.__dict__");
|
||||
|
||||
// since declarative_submodule is declared outside, but the parent module name is passed
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.declarative_submodule.__name__ == 'declarative_module.declarative_submodule'"
|
||||
);
|
||||
py_assert!(py, m, "'declarative_submodule' in m.__dict__");
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"'declarative_module.declarative_submodule' not in m.__dict__"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
mod r#type {
|
||||
#[pymodule_export]
|
||||
use super::double;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_ident_module() {
|
||||
Python::attach(|py| {
|
||||
let m = pyo3::wrap_pymodule!(r#type)(py).into_bound(py);
|
||||
py_assert!(py, m, "m.double(2) == 4");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_names() {
|
||||
Python::attach(|py| {
|
||||
let m = declarative_module(py);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.inner.Struct.__module__ == 'declarative_module.inner'"
|
||||
);
|
||||
py_assert!(py, m, "m.inner.StructInCustomModule.__module__ == 'foo'");
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.inner.Enum.__module__ == 'declarative_module.inner'"
|
||||
);
|
||||
py_assert!(py, m, "m.inner.EnumInCustomModule.__module__ == 'foo'");
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.inner_custom_root.Struct.__module__ == 'custom_root.inner_custom_root'"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inner_module_full_path() {
|
||||
Python::attach(|py| {
|
||||
let m = declarative_module(py);
|
||||
py_assert!(py, m, "m.full_path_inner");
|
||||
})
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
// Test default generated __repr__.
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(PartialEq)]
|
||||
enum TestDefaultRepr {
|
||||
Var,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_slot_exists() {
|
||||
Python::attach(|py| {
|
||||
let test_object = Py::new(py, TestDefaultRepr::Var).unwrap();
|
||||
py_assert!(
|
||||
py,
|
||||
test_object,
|
||||
"repr(test_object) == 'TestDefaultRepr.Var'"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(PartialEq)]
|
||||
enum OverrideSlot {
|
||||
Var,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl OverrideSlot {
|
||||
fn __repr__(&self) -> &str {
|
||||
"overridden"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_override_slot() {
|
||||
Python::attach(|py| {
|
||||
let test_object = Py::new(py, OverrideSlot::Var).unwrap();
|
||||
py_assert!(py, test_object, "repr(test_object) == 'overridden'");
|
||||
})
|
||||
}
|
||||
Vendored
+459
@@ -0,0 +1,459 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::PyString;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(eq, eq_int, from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum MyEnum {
|
||||
Variant,
|
||||
OtherVariant,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_class_attr() {
|
||||
Python::attach(|py| {
|
||||
let my_enum = py.get_type::<MyEnum>();
|
||||
let var = Py::new(py, MyEnum::Variant).unwrap();
|
||||
py_assert!(py, my_enum var, "my_enum.Variant == var");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_intopyobject() {
|
||||
// Variant of the above that goes via `.into_pyobject()`
|
||||
// - regression test for https://github.com/PyO3/pyo3/issues/5927
|
||||
//
|
||||
#[pyclass(eq, eq_int, from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum MyEnum {
|
||||
A,
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let var = MyEnum::A.into_pyobject(py).unwrap();
|
||||
// NB important to call this after `into_pyobject` - this ensures
|
||||
// that `.into_pyobject()` has to lazily initialize the enum type object
|
||||
let my_enum = py.get_type::<MyEnum>();
|
||||
py_assert!(py, my_enum var, "my_enum.A == var");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_eq_enum() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let var2 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let other_var = Py::new(py, MyEnum::OtherVariant).unwrap();
|
||||
py_assert!(py, var1 var2, "var1 == var2");
|
||||
py_assert!(py, var1 other_var, "var1 != other_var");
|
||||
py_assert!(py, var1 var2, "(var1 != var2) == False");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_eq_incomparable() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, MyEnum::Variant).unwrap();
|
||||
py_assert!(py, var1, "(var1 == 'foo') == False");
|
||||
py_assert!(py, var1, "(var1 != 'foo') == True");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn return_enum() -> MyEnum {
|
||||
MyEnum::Variant
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return_enum() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(return_enum)(py).unwrap();
|
||||
let mynum = py.get_type::<MyEnum>();
|
||||
|
||||
py_run!(py, f mynum, "assert f() == mynum.Variant");
|
||||
py_run!(py, f mynum, "assert f() is mynum.Variant");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn enum_arg(e: MyEnum) {
|
||||
assert_eq!(MyEnum::OtherVariant, e)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_arg() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(enum_arg)(py).unwrap();
|
||||
let mynum = py.get_type::<MyEnum>();
|
||||
|
||||
py_run!(py, f mynum, "f(mynum.OtherVariant)")
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
enum CustomDiscriminant {
|
||||
One = 1,
|
||||
Two = 2,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_discriminant() {
|
||||
Python::attach(|py| {
|
||||
#[allow(non_snake_case)]
|
||||
let CustomDiscriminant = py.get_type::<CustomDiscriminant>();
|
||||
let one = Py::new(py, CustomDiscriminant::One).unwrap();
|
||||
let two = Py::new(py, CustomDiscriminant::Two).unwrap();
|
||||
py_run!(py, CustomDiscriminant one two, r#"
|
||||
assert CustomDiscriminant.One == one
|
||||
assert CustomDiscriminant.Two == two
|
||||
assert CustomDiscriminant.One == 1
|
||||
assert CustomDiscriminant.Two == 2
|
||||
assert one != two
|
||||
assert CustomDiscriminant.One != 2
|
||||
assert CustomDiscriminant.Two != 1
|
||||
"#);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_to_int() {
|
||||
Python::attach(|py| {
|
||||
let one = Py::new(py, CustomDiscriminant::One).unwrap();
|
||||
py_assert!(py, one, "int(one) == 1");
|
||||
let v = Py::new(py, MyEnum::Variant).unwrap();
|
||||
let v_value = MyEnum::Variant as isize;
|
||||
py_run!(py, v v_value, "int(v) == v_value");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_compare_int() {
|
||||
Python::attach(|py| {
|
||||
let one = Py::new(py, CustomDiscriminant::One).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
one,
|
||||
r#"
|
||||
assert one == 1
|
||||
assert 1 == one
|
||||
assert one != 2
|
||||
"#
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
enum SmallEnum {
|
||||
V = 1,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_compare_int_no_throw_when_overflow() {
|
||||
Python::attach(|py| {
|
||||
let v = Py::new(py, SmallEnum::V).unwrap();
|
||||
py_assert!(py, v, "v != 1<<30")
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(usize)]
|
||||
#[allow(clippy::enum_clike_unportable_variant)]
|
||||
enum BigEnum {
|
||||
V = usize::MAX,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_big_enum_no_overflow() {
|
||||
Python::attach(|py| {
|
||||
let usize_max = usize::MAX;
|
||||
let v = Py::new(py, BigEnum::V).unwrap();
|
||||
|
||||
py_assert!(py, usize_max v, "v == usize_max");
|
||||
py_assert!(py, usize_max v, "int(v) == usize_max");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u16, align(8))]
|
||||
enum TestReprParse {
|
||||
V,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repr_parse() {
|
||||
assert_eq!(std::mem::align_of::<TestReprParse>(), 8);
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, name = "MyEnum", skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum RenameEnum {
|
||||
Variant,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_enum_repr_correct() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, RenameEnum::Variant).unwrap();
|
||||
py_assert!(py, var1, "repr(var1) == 'MyEnum.Variant'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum RenameVariantEnum {
|
||||
#[pyo3(name = "VARIANT")]
|
||||
Variant,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_variant_repr_correct() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, RenameVariantEnum::Variant).unwrap();
|
||||
py_assert!(py, var1, "repr(var1) == 'RenameVariantEnum.VARIANT'");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int, rename_all = "SCREAMING_SNAKE_CASE", skip_from_py_object)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[expect(clippy::enum_variant_names)]
|
||||
enum RenameAllVariantsEnum {
|
||||
VariantOne,
|
||||
VariantTwo,
|
||||
#[pyo3(name = "VariantThree")]
|
||||
VariantFour,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_renaming_all_enum_variants() {
|
||||
Python::attach(|py| {
|
||||
let enum_obj = py.get_type::<RenameAllVariantsEnum>();
|
||||
py_assert!(py, enum_obj, "enum_obj.VARIANT_ONE == enum_obj.VARIANT_ONE");
|
||||
py_assert!(py, enum_obj, "enum_obj.VARIANT_TWO == enum_obj.VARIANT_TWO");
|
||||
py_assert!(
|
||||
py,
|
||||
enum_obj,
|
||||
"enum_obj.VariantThree == enum_obj.VariantThree"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(module = "custom_module")]
|
||||
#[derive(Debug)]
|
||||
enum CustomModuleComplexEnum {
|
||||
Variant(),
|
||||
Py(Py<PyAny>),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_module() {
|
||||
Python::attach(|py| {
|
||||
let enum_obj = py.get_type::<CustomModuleComplexEnum>();
|
||||
py_assert!(
|
||||
py,
|
||||
enum_obj,
|
||||
"enum_obj.Variant.__module__ == 'custom_module'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(eq, skip_from_py_object)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum EqOnly {
|
||||
VariantA,
|
||||
VariantB,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_enum_eq_only() {
|
||||
Python::attach(|py| {
|
||||
let var1 = Py::new(py, EqOnly::VariantA).unwrap();
|
||||
let var2 = Py::new(py, EqOnly::VariantA).unwrap();
|
||||
let var3 = Py::new(py, EqOnly::VariantB).unwrap();
|
||||
py_assert!(py, var1 var2, "var1 == var2");
|
||||
py_assert!(py, var1 var3, "var1 != var3");
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(frozen, eq, eq_int, hash)]
|
||||
#[derive(PartialEq, Hash)]
|
||||
enum SimpleEnumWithHash {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_enum_with_hash() {
|
||||
Python::attach(|py| {
|
||||
use pyo3::types::IntoPyDict;
|
||||
let class = SimpleEnumWithHash::A;
|
||||
let hash = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
class.hash(&mut hasher);
|
||||
hasher.finish() as isize
|
||||
};
|
||||
|
||||
let env = [
|
||||
("obj", Py::new(py, class).unwrap().into_any()),
|
||||
("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, *env, "hash(obj) == hsh");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(eq, hash)]
|
||||
#[derive(PartialEq, Hash)]
|
||||
enum ComplexEnumWithHash {
|
||||
A(u32),
|
||||
B { msg: String },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_enum_with_hash() {
|
||||
Python::attach(|py| {
|
||||
use pyo3::types::IntoPyDict;
|
||||
let class = ComplexEnumWithHash::B {
|
||||
msg: String::from("Hello"),
|
||||
};
|
||||
let hash = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
class.hash(&mut hasher);
|
||||
hasher.finish() as isize
|
||||
};
|
||||
|
||||
let env = [
|
||||
("obj", Py::new(py, class).unwrap().into_any()),
|
||||
("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, *env, "hash(obj) == hsh");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_eq() {
|
||||
#[pyclass(frozen)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum CustomPyEq {
|
||||
A,
|
||||
B,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl CustomPyEq {
|
||||
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
|
||||
if let Ok(rhs) = other.cast::<PyString>() {
|
||||
rhs.to_cow().is_ok_and(|rhs| self.__str__() == rhs)
|
||||
} else if let Ok(rhs) = other.cast::<Self>() {
|
||||
self == rhs.get()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
match self {
|
||||
CustomPyEq::A => "A".to_string(),
|
||||
CustomPyEq::B => "B".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let a = Bound::new(py, CustomPyEq::A).unwrap();
|
||||
let b = Bound::new(py, CustomPyEq::B).unwrap();
|
||||
|
||||
assert!(a.as_any().eq(&a).unwrap());
|
||||
assert!(a.as_any().eq("A").unwrap());
|
||||
assert!(a.as_any().ne(&b).unwrap());
|
||||
assert!(a.as_any().ne("B").unwrap());
|
||||
|
||||
assert!(b.as_any().eq(&b).unwrap());
|
||||
assert!(b.as_any().eq("B").unwrap());
|
||||
assert!(b.as_any().ne(&a).unwrap());
|
||||
assert!(b.as_any().ne("A").unwrap());
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(skip_from_py_object)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ComplexEnumWithRaw {
|
||||
Raw { r#type: i32 },
|
||||
}
|
||||
|
||||
// Cover simple field lookups with raw identifiers
|
||||
#[test]
|
||||
fn complex_enum_with_raw() {
|
||||
Python::attach(|py| {
|
||||
let complex = ComplexEnumWithRaw::Raw { r#type: 314159 };
|
||||
|
||||
py_assert!(py, complex, "complex.type == 314159");
|
||||
});
|
||||
}
|
||||
|
||||
// Cover pattern matching with raw identifiers
|
||||
#[test]
|
||||
#[cfg(Py_3_10)]
|
||||
fn complex_enum_with_raw_pattern_match() {
|
||||
Python::attach(|py| {
|
||||
let complex = ComplexEnumWithRaw::Raw { r#type: 314159 };
|
||||
let cls = py.get_type::<ComplexEnumWithRaw>();
|
||||
|
||||
// Cover destructuring by pattern matching
|
||||
py_run!(py, cls complex, r#"
|
||||
match complex:
|
||||
case cls.Raw(type=ty):
|
||||
assert ty == 314159
|
||||
case _:
|
||||
assert False, "no matching variant found"
|
||||
"#);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_enum_variant_qualname() {
|
||||
#[pyclass(skip_from_py_object)]
|
||||
pub enum ComplexEnum {
|
||||
A(i32),
|
||||
B { msg: String },
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<ComplexEnum>();
|
||||
py_assert!(py, cls, "cls.A.__qualname__ == 'ComplexEnum.A'");
|
||||
py_assert!(py, cls, "cls.B.__qualname__ == 'ComplexEnum.B'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_enum_renamed_variant_qualname() {
|
||||
#[pyclass(name = "ComplexEnum", skip_from_py_object)]
|
||||
pub enum PyComplexEnum {
|
||||
#[pyo3(name = "A")]
|
||||
PyA(i32),
|
||||
B {
|
||||
msg: String,
|
||||
},
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<PyComplexEnum>();
|
||||
py_assert!(py, cls, "cls.A.__qualname__ == 'ComplexEnum.A'");
|
||||
py_assert!(py, cls, "cls.B.__qualname__ == 'ComplexEnum.B'");
|
||||
});
|
||||
}
|
||||
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::{exceptions, py_run};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use std::fs::File;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyfunction]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn fail_to_open_file() -> PyResult<()> {
|
||||
File::open("not_there.txt")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(target_arch = "wasm32", ignore)] // Not sure why this fails.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn test_filenotfounderror() {
|
||||
Python::attach(|py| {
|
||||
let fail_to_open_file = wrap_pyfunction!(fail_to_open_file)(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
fail_to_open_file,
|
||||
r#"
|
||||
try:
|
||||
fail_to_open_file()
|
||||
except FileNotFoundError as e:
|
||||
assert str(e) == "No such file or directory (os error 2)"
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CustomError;
|
||||
|
||||
impl Error for CustomError {}
|
||||
|
||||
impl fmt::Display for CustomError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Oh no!")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<CustomError> for PyErr {
|
||||
fn from(err: CustomError) -> PyErr {
|
||||
exceptions::PyOSError::new_err(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn fail_with_custom_error() -> Result<(), CustomError> {
|
||||
Err(CustomError)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn call_fail_with_custom_error() -> PyResult<()> {
|
||||
fail_with_custom_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_error() {
|
||||
Python::attach(|py| {
|
||||
let call_fail_with_custom_error =
|
||||
wrap_pyfunction!(call_fail_with_custom_error)(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
call_fail_with_custom_error,
|
||||
r#"
|
||||
try:
|
||||
call_fail_with_custom_error()
|
||||
except OSError as e:
|
||||
assert str(e) == "Oh no!"
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exception_nosegfault() {
|
||||
use std::net::TcpListener;
|
||||
fn io_err() -> PyResult<()> {
|
||||
TcpListener::bind("no:address")?;
|
||||
Ok(())
|
||||
}
|
||||
fn parse_int() -> PyResult<()> {
|
||||
"@_@".parse::<i64>()?;
|
||||
Ok(())
|
||||
}
|
||||
assert!(io_err().is_err());
|
||||
assert!(parse_int().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_unraisable() {
|
||||
use pyo3::{exceptions::PyRuntimeError, types::PyNotImplemented};
|
||||
use test_utils::UnraisableCapture;
|
||||
|
||||
Python::attach(|py| {
|
||||
UnraisableCapture::enter(py, |capture| {
|
||||
let err = PyRuntimeError::new_err("foo");
|
||||
err.write_unraisable(py, None);
|
||||
|
||||
let (err, object) = capture.take_capture().unwrap();
|
||||
|
||||
assert_eq!(err.to_string(), "RuntimeError: foo");
|
||||
assert!(object.is_none());
|
||||
|
||||
let err = PyRuntimeError::new_err("bar");
|
||||
err.write_unraisable(py, Some(&PyNotImplemented::get(py)));
|
||||
|
||||
let (err, object) = capture.take_capture().unwrap();
|
||||
|
||||
assert_eq!(err.to_string(), "RuntimeError: bar");
|
||||
assert!(object.is(PyNotImplemented::get(py)));
|
||||
});
|
||||
});
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyclass]
|
||||
struct CfgClass {
|
||||
#[pyo3(get, set)]
|
||||
#[cfg(any())]
|
||||
pub a: u32,
|
||||
#[pyo3(get, set)]
|
||||
// This is always true
|
||||
#[cfg(any(
|
||||
target_family = "unix",
|
||||
target_family = "windows",
|
||||
target_family = "wasm"
|
||||
))]
|
||||
pub b: u32,
|
||||
}
|
||||
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(PartialEq)]
|
||||
enum CfgSimpleEnum {
|
||||
#[cfg(any())]
|
||||
DisabledVariant,
|
||||
#[cfg(not(any()))]
|
||||
EnabledVariant,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cfg() {
|
||||
Python::attach(|py| {
|
||||
let cfg = CfgClass { b: 3 };
|
||||
let py_cfg = Py::new(py, cfg).unwrap();
|
||||
assert!(py_cfg.bind(py).getattr("a").is_err());
|
||||
let b: u32 = py_cfg.bind(py).getattr("b").unwrap().extract().unwrap();
|
||||
assert_eq!(b, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cfg_simple_enum() {
|
||||
Python::attach(|py| {
|
||||
let simple = py.get_type::<CfgSimpleEnum>();
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
simple,
|
||||
r#"
|
||||
assert hasattr(simple, "EnabledVariant")
|
||||
assert not hasattr(simple, "DisabledVariant")
|
||||
"#
|
||||
);
|
||||
})
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::types::{PyDict, PyString};
|
||||
use pyo3::{prelude::*, IntoPyObject, IntoPyObjectExt, IntoPyObjectRef};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
#[derive(Debug, Clone, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
pub struct A<'py> {
|
||||
#[pyo3(item)]
|
||||
s: String,
|
||||
#[pyo3(item)]
|
||||
t: Bound<'py, PyString>,
|
||||
#[pyo3(item("foo"))]
|
||||
p: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_fields_struct() {
|
||||
Python::attach(|py| {
|
||||
let a = A {
|
||||
s: "Hello".into(),
|
||||
t: PyString::new(py, "World"),
|
||||
p: 42i32.into_pyobject(py).unwrap().into_any(),
|
||||
};
|
||||
let pya = (&a).into_pyobject(py).unwrap();
|
||||
let new_a = pya.extract::<A<'_>>().unwrap();
|
||||
|
||||
assert_eq!(a.s, new_a.s);
|
||||
assert_eq!(a.t.to_cow().unwrap(), new_a.t.to_cow().unwrap());
|
||||
assert_eq!(
|
||||
a.p.extract::<i32>().unwrap(),
|
||||
new_a.p.extract::<i32>().unwrap()
|
||||
);
|
||||
|
||||
let pya = a.clone().into_pyobject(py).unwrap();
|
||||
let new_a = pya.extract::<A<'_>>().unwrap();
|
||||
|
||||
assert_eq!(a.s, new_a.s);
|
||||
assert_eq!(a.t.to_cow().unwrap(), new_a.t.to_cow().unwrap());
|
||||
assert_eq!(
|
||||
a.p.extract::<i32>().unwrap(),
|
||||
new_a.p.extract::<i32>().unwrap()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct B {
|
||||
test: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let b = B {
|
||||
test: "test".into(),
|
||||
};
|
||||
let pyb = (&b).into_pyobject(py).unwrap();
|
||||
let new_b = pyb.extract::<B>().unwrap();
|
||||
assert_eq!(b, new_b);
|
||||
|
||||
let pyb = b.clone().into_pyobject(py).unwrap();
|
||||
let new_b = pyb.extract::<B>().unwrap();
|
||||
assert_eq!(b, new_b);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct D<T> {
|
||||
test: T,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let d = D {
|
||||
test: String::from("test"),
|
||||
};
|
||||
let pyd = (&d).into_pyobject(py).unwrap();
|
||||
let new_d = pyd.extract::<D<String>>().unwrap();
|
||||
assert_eq!(d, new_d);
|
||||
|
||||
let d = D { test: 1usize };
|
||||
let pyd = (&d).into_pyobject(py).unwrap();
|
||||
let new_d = pyd.extract::<D<usize>>().unwrap();
|
||||
assert_eq!(d, new_d);
|
||||
|
||||
let d = D {
|
||||
test: String::from("test"),
|
||||
};
|
||||
let pyd = d.clone().into_pyobject(py).unwrap();
|
||||
let new_d = pyd.extract::<D<String>>().unwrap();
|
||||
assert_eq!(d, new_d);
|
||||
|
||||
let d = D { test: 1usize };
|
||||
let pyd = d.clone().into_pyobject(py).unwrap();
|
||||
let new_d = pyd.extract::<D<usize>>().unwrap();
|
||||
assert_eq!(d, new_d);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
pub struct GenericWithBound<K: Hash + Eq, V>(HashMap<K, V>);
|
||||
|
||||
#[test]
|
||||
fn test_generic_with_bound() {
|
||||
Python::attach(|py| {
|
||||
let mut hash_map = HashMap::<String, i32>::new();
|
||||
hash_map.insert("1".into(), 1);
|
||||
hash_map.insert("2".into(), 2);
|
||||
let map = GenericWithBound(hash_map);
|
||||
let py_map = (&map).into_pyobject(py).unwrap();
|
||||
assert_eq!(py_map.len(), 2);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item("1")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item("2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert!(py_map.get_item("3").unwrap().is_none());
|
||||
|
||||
let py_map = map.into_pyobject(py).unwrap();
|
||||
assert_eq!(py_map.len(), 2);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item("1")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
py_map
|
||||
.get_item("2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert!(py_map.get_item("3").unwrap().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
pub struct Tuple(String, usize);
|
||||
|
||||
#[test]
|
||||
fn test_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = Tuple(String::from("test"), 1);
|
||||
let tuple = (&tup).into_pyobject(py).unwrap();
|
||||
let new_tup = tuple.extract::<Tuple>().unwrap();
|
||||
assert_eq!(tup, new_tup);
|
||||
|
||||
let tuple = tup.clone().into_pyobject(py).unwrap();
|
||||
let new_tup = tuple.extract::<Tuple>().unwrap();
|
||||
assert_eq!(tup, new_tup);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
pub struct TransparentTuple(String);
|
||||
|
||||
#[test]
|
||||
fn test_transparent_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = TransparentTuple(String::from("test"));
|
||||
let tuple = (&tup).into_pyobject(py).unwrap();
|
||||
let new_tup = tuple.extract::<TransparentTuple>().unwrap();
|
||||
assert_eq!(tup, new_tup);
|
||||
|
||||
let tuple = tup.clone().into_pyobject(py).unwrap();
|
||||
let new_tup = tuple.extract::<TransparentTuple>().unwrap();
|
||||
assert_eq!(tup, new_tup);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct IntWrapper(u32);
|
||||
|
||||
fn int_wrapper_into_py<'py>(
|
||||
v: std::borrow::Cow<'_, IntWrapper>,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
v.0.into_bound_py_any(py)
|
||||
}
|
||||
|
||||
fn int_wrapper_from_py(v: &Bound<'_, PyAny>) -> PyResult<IntWrapper> {
|
||||
v.extract().map(IntWrapper)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)]
|
||||
pub enum Foo {
|
||||
TupleVar(usize, String),
|
||||
StructVar {
|
||||
#[pyo3(item)]
|
||||
test: char,
|
||||
#[pyo3(item, into_py_with=int_wrapper_into_py, from_py_with=int_wrapper_from_py)]
|
||||
int: IntWrapper,
|
||||
},
|
||||
#[pyo3(transparent)]
|
||||
TransparentTuple(usize),
|
||||
#[pyo3(transparent)]
|
||||
TransparentStructVar {
|
||||
a: Option<String>,
|
||||
},
|
||||
#[pyo3(rename_all = "camelCase", from_item_all)]
|
||||
RenameAll {
|
||||
long_field_name: [u16; 2],
|
||||
other_field: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum() {
|
||||
Python::attach(|py| {
|
||||
let tuple_var = Foo::TupleVar(1, "test".into());
|
||||
let foo = (&tuple_var).into_pyobject(py).unwrap();
|
||||
assert_eq!(tuple_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let foo = tuple_var.clone().into_pyobject(py).unwrap();
|
||||
assert_eq!(tuple_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let struct_var = Foo::StructVar {
|
||||
test: 'b',
|
||||
int: IntWrapper(42),
|
||||
};
|
||||
let foo = (&struct_var)
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.cast_into::<PyDict>()
|
||||
.unwrap();
|
||||
assert_eq!(struct_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let foo = struct_var
|
||||
.clone()
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.cast_into::<PyDict>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(struct_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let transparent_tuple = Foo::TransparentTuple(1);
|
||||
let foo = (&transparent_tuple).into_pyobject(py).unwrap();
|
||||
assert_eq!(transparent_tuple, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let foo = transparent_tuple.clone().into_pyobject(py).unwrap();
|
||||
assert_eq!(transparent_tuple, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let transparent_struct_var = Foo::TransparentStructVar { a: None };
|
||||
let foo = (&transparent_struct_var).into_pyobject(py).unwrap();
|
||||
assert_eq!(transparent_struct_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let foo = transparent_struct_var.clone().into_pyobject(py).unwrap();
|
||||
assert_eq!(transparent_struct_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let rename_all_struct_var = Foo::RenameAll {
|
||||
long_field_name: [1, 2],
|
||||
other_field: None,
|
||||
};
|
||||
let foo = (&rename_all_struct_var).into_pyobject(py).unwrap();
|
||||
assert_eq!(rename_all_struct_var, foo.extract::<Foo>().unwrap());
|
||||
|
||||
let foo = rename_all_struct_var.clone().into_pyobject(py).unwrap();
|
||||
assert_eq!(rename_all_struct_var, foo.extract::<Foo>().unwrap());
|
||||
});
|
||||
}
|
||||
+879
@@ -0,0 +1,879 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{IntoPyDict, PyDict, PyList, PyString, PyTuple};
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
/// Helper function that concatenates the error message from
|
||||
/// each error in the traceback into a single string that can
|
||||
/// be tested.
|
||||
fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String {
|
||||
let mut error_msg = error.to_string();
|
||||
while let Some(cause) = error.cause(py) {
|
||||
error_msg.push_str(": ");
|
||||
error_msg.push_str(&cause.to_string());
|
||||
error = cause
|
||||
}
|
||||
error_msg
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct A<'py> {
|
||||
#[pyo3(attribute)]
|
||||
s: String,
|
||||
#[pyo3(item)]
|
||||
t: Bound<'py, PyString>,
|
||||
#[pyo3(attribute("foo"))]
|
||||
p: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct PyA {
|
||||
#[pyo3(get)]
|
||||
s: String,
|
||||
#[pyo3(get)]
|
||||
foo: Option<String>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyA {
|
||||
fn __getitem__(&self, key: String) -> pyo3::PyResult<String> {
|
||||
if key == "t" {
|
||||
Ok("bar".into())
|
||||
} else {
|
||||
Err(PyValueError::new_err("Failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_fields_struct() {
|
||||
Python::attach(|py| {
|
||||
let pya = PyA {
|
||||
s: "foo".into(),
|
||||
foo: None,
|
||||
};
|
||||
let py_c = Py::new(py, pya).unwrap();
|
||||
let a = py_c
|
||||
.extract::<A<'_>>(py)
|
||||
.expect("Failed to extract A from PyA");
|
||||
assert_eq!(a.s, "foo");
|
||||
assert_eq!(a.t.to_string_lossy(), "bar");
|
||||
assert!(a.p.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct B {
|
||||
test: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let test = "test".into_pyobject(py).unwrap();
|
||||
let b = test
|
||||
.extract::<B>()
|
||||
.expect("Failed to extract B from String");
|
||||
assert_eq!(b.test, "test");
|
||||
let test = 1i32.into_pyobject(py).unwrap();
|
||||
let b = test.extract::<B>();
|
||||
assert!(b.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct D<T> {
|
||||
test: T,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let test = "test".into_pyobject(py).unwrap();
|
||||
let d = test
|
||||
.extract::<D<String>>()
|
||||
.expect("Failed to extract D<String> from String");
|
||||
assert_eq!(d.test, "test");
|
||||
let test = 1usize.into_pyobject(py).unwrap();
|
||||
let d = test
|
||||
.extract::<D<usize>>()
|
||||
.expect("Failed to extract D<usize> from String");
|
||||
assert_eq!(d.test, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct GenericWithBound<K: std::hash::Hash + Eq, V>(std::collections::HashMap<K, V>);
|
||||
|
||||
#[test]
|
||||
fn test_generic_with_bound() {
|
||||
Python::attach(|py| {
|
||||
let dict = [("1", 1), ("2", 2)].into_py_dict(py).unwrap();
|
||||
let map = dict.extract::<GenericWithBound<String, i32>>().unwrap().0;
|
||||
assert_eq!(map.len(), 2);
|
||||
assert_eq!(map["1"], 1);
|
||||
assert_eq!(map["2"], 2);
|
||||
assert!(!map.contains_key("3"));
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct E<T, T2> {
|
||||
test: T,
|
||||
test2: T2,
|
||||
}
|
||||
|
||||
#[pyclass(skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct PyE {
|
||||
#[pyo3(get)]
|
||||
test: String,
|
||||
#[pyo3(get)]
|
||||
test2: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_named_fields_struct() {
|
||||
Python::attach(|py| {
|
||||
let pye = PyE {
|
||||
test: "test".into(),
|
||||
test2: 2,
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
|
||||
let e = pye
|
||||
.extract::<E<String, usize>>()
|
||||
.expect("Failed to extract E<String, usize> from PyE");
|
||||
assert_eq!(e.test, "test");
|
||||
assert_eq!(e.test2, 2);
|
||||
let e = pye.extract::<E<usize, usize>>();
|
||||
assert!(e.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct C {
|
||||
#[pyo3(attribute("test"))]
|
||||
test: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_field_with_ext_fn() {
|
||||
Python::attach(|py| {
|
||||
let pyc = PyE {
|
||||
test: "foo".into(),
|
||||
test2: 0,
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
let c = pyc.extract::<C>().expect("Failed to extract C from PyE");
|
||||
assert_eq!(c.test, "foo");
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Tuple(String, usize);
|
||||
|
||||
#[test]
|
||||
fn test_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = PyTuple::new(
|
||||
py,
|
||||
&[
|
||||
1i32.into_pyobject(py).unwrap().into_any(),
|
||||
"test".into_pyobject(py).unwrap().into_any(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let tup = tup.extract::<Tuple>();
|
||||
assert!(tup.is_err());
|
||||
let tup = PyTuple::new(
|
||||
py,
|
||||
&[
|
||||
"test".into_pyobject(py).unwrap().into_any(),
|
||||
1i32.into_pyobject(py).unwrap().into_any(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let tup = tup
|
||||
.extract::<Tuple>()
|
||||
.expect("Failed to extract Tuple from PyTuple");
|
||||
assert_eq!(tup.0, "test");
|
||||
assert_eq!(tup.1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct TransparentTuple(String);
|
||||
|
||||
#[test]
|
||||
fn test_transparent_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = 1i32.into_pyobject(py).unwrap();
|
||||
let tup = tup.extract::<TransparentTuple>();
|
||||
assert!(tup.is_err());
|
||||
let test = "test".into_pyobject(py).unwrap();
|
||||
let tup = test
|
||||
.extract::<TransparentTuple>()
|
||||
.expect("Failed to extract TransparentTuple from PyTuple");
|
||||
assert_eq!(tup.0, "test");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct PyBaz {
|
||||
#[pyo3(get)]
|
||||
tup: (String, String),
|
||||
#[pyo3(get)]
|
||||
e: PyE,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
#[expect(dead_code)]
|
||||
struct Baz<U, T> {
|
||||
e: E<U, T>,
|
||||
tup: Tuple,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_nested_type_errors() {
|
||||
Python::attach(|py| {
|
||||
let pybaz = PyBaz {
|
||||
tup: ("test".into(), "test".into()),
|
||||
e: PyE {
|
||||
test: "foo".into(),
|
||||
test2: 0,
|
||||
},
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
|
||||
let test = pybaz.extract::<Baz<String, usize>>();
|
||||
assert!(test.is_err());
|
||||
assert_eq!(
|
||||
extract_traceback(py,test.unwrap_err()),
|
||||
"TypeError: failed to extract field Baz.tup: TypeError: failed to extract field Tuple.1: \
|
||||
TypeError: \'str\' object cannot be interpreted as an integer"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_nested_type_errors_with_generics() {
|
||||
Python::attach(|py| {
|
||||
let pybaz = PyBaz {
|
||||
tup: ("test".into(), "test".into()),
|
||||
e: PyE {
|
||||
test: "foo".into(),
|
||||
test2: 0,
|
||||
},
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
|
||||
let test = pybaz.extract::<Baz<usize, usize>>();
|
||||
assert!(test.is_err());
|
||||
assert_eq!(
|
||||
extract_traceback(py, test.unwrap_err()),
|
||||
"TypeError: failed to extract field Baz.e: TypeError: failed to extract field E.test: \
|
||||
TypeError: \'str\' object cannot be interpreted as an integer",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_struct_error_message() {
|
||||
Python::attach(|py| {
|
||||
let tup = 1i32.into_pyobject(py).unwrap();
|
||||
let tup = tup.extract::<B>();
|
||||
assert!(tup.is_err());
|
||||
assert_eq!(
|
||||
extract_traceback(py, tup.unwrap_err()),
|
||||
"TypeError: failed to extract field B.test: TypeError: \'int\' object is not an \
|
||||
instance of \'str\'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_struct_error_message() {
|
||||
Python::attach(|py| {
|
||||
let tup = (1, "test").into_pyobject(py).unwrap();
|
||||
let tup = tup.extract::<Tuple>();
|
||||
assert!(tup.is_err());
|
||||
assert_eq!(
|
||||
extract_traceback(py, tup.unwrap_err()),
|
||||
"TypeError: failed to extract field Tuple.0: TypeError: \'int\' object is not an \
|
||||
instance of \'str\'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_tuple_error_message() {
|
||||
Python::attach(|py| {
|
||||
let tup = 1i32.into_pyobject(py).unwrap();
|
||||
let tup = tup.extract::<TransparentTuple>();
|
||||
assert!(tup.is_err());
|
||||
assert_eq!(
|
||||
extract_traceback(py, tup.unwrap_err()),
|
||||
"TypeError: failed to extract field TransparentTuple.0: TypeError: 'int' object \
|
||||
is not an instance of 'str'",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RenameAllCls {}
|
||||
|
||||
#[pymethods]
|
||||
impl RenameAllCls {
|
||||
#[getter]
|
||||
#[pyo3(name = "someField")]
|
||||
fn some_field(&self) -> &'static str {
|
||||
"Foo"
|
||||
}
|
||||
|
||||
#[getter]
|
||||
#[pyo3(name = "customNumber")]
|
||||
fn custom_number(&self) -> i32 {
|
||||
42
|
||||
}
|
||||
|
||||
fn __getitem__(&self, key: &str) -> PyResult<f32> {
|
||||
match key {
|
||||
"otherField" => Ok(42.0),
|
||||
_ => Err(pyo3::exceptions::PyKeyError::new_err("foo")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_rename_all() {
|
||||
#[derive(FromPyObject)]
|
||||
#[pyo3(rename_all = "camelCase")]
|
||||
struct RenameAll {
|
||||
some_field: String,
|
||||
#[pyo3(item)]
|
||||
other_field: f32,
|
||||
#[pyo3(attribute("customNumber"))]
|
||||
custom_name: i32,
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let RenameAll {
|
||||
some_field,
|
||||
other_field,
|
||||
custom_name,
|
||||
} = RenameAllCls {}
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(some_field, "Foo");
|
||||
assert_eq!(other_field, 42.0);
|
||||
assert_eq!(custom_name, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_rename_all() {
|
||||
#[derive(FromPyObject)]
|
||||
#[pyo3(rename_all = "camelCase")]
|
||||
enum RenameAll {
|
||||
Foo {
|
||||
some_field: String,
|
||||
#[pyo3(item)]
|
||||
other_field: f32,
|
||||
#[pyo3(attribute("customNumber"))]
|
||||
custom_name: i32,
|
||||
},
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let RenameAll::Foo {
|
||||
some_field,
|
||||
other_field,
|
||||
custom_name,
|
||||
} = RenameAllCls {}
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.extract()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(some_field, "Foo");
|
||||
assert_eq!(other_field, 42.0);
|
||||
assert_eq!(custom_name, 42);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub enum Foo<'py> {
|
||||
TupleVar(usize, String),
|
||||
StructVar {
|
||||
test: Bound<'py, PyString>,
|
||||
},
|
||||
#[pyo3(transparent)]
|
||||
TransparentTuple(usize),
|
||||
#[pyo3(transparent)]
|
||||
TransparentStructVar {
|
||||
a: Option<String>,
|
||||
},
|
||||
StructVarGetAttrArg {
|
||||
#[pyo3(attribute("bla"))]
|
||||
a: bool,
|
||||
},
|
||||
StructWithGetItem {
|
||||
#[pyo3(item)]
|
||||
a: String,
|
||||
},
|
||||
StructWithGetItemArg {
|
||||
#[pyo3(item("foo"))]
|
||||
a: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct PyBool {
|
||||
#[pyo3(get)]
|
||||
bla: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum() {
|
||||
Python::attach(|py| {
|
||||
let tup = PyTuple::new(
|
||||
py,
|
||||
&[
|
||||
1i32.into_pyobject(py).unwrap().into_any(),
|
||||
"test".into_pyobject(py).unwrap().into_any(),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let f = tup
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from tuple");
|
||||
match f {
|
||||
Foo::TupleVar(test, test2) => {
|
||||
assert_eq!(test, 1);
|
||||
assert_eq!(test2, "test");
|
||||
}
|
||||
_ => panic!("Expected extracting Foo::TupleVar, got {f:?}"),
|
||||
}
|
||||
|
||||
let pye = PyE {
|
||||
test: "foo".into(),
|
||||
test2: 0,
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
let f = pye
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from PyE");
|
||||
match f {
|
||||
Foo::StructVar { test } => assert_eq!(test.to_string_lossy(), "foo"),
|
||||
_ => panic!("Expected extracting Foo::StructVar, got {f:?}"),
|
||||
}
|
||||
|
||||
let int = 1i32.into_pyobject(py).unwrap();
|
||||
let f = int
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from int");
|
||||
match f {
|
||||
Foo::TransparentTuple(test) => assert_eq!(test, 1),
|
||||
_ => panic!("Expected extracting Foo::TransparentTuple, got {f:?}"),
|
||||
}
|
||||
let none = py.None();
|
||||
let f = none
|
||||
.extract::<Foo<'_>>(py)
|
||||
.expect("Failed to extract Foo from int");
|
||||
match f {
|
||||
Foo::TransparentStructVar { a } => assert!(a.is_none()),
|
||||
_ => panic!("Expected extracting Foo::TransparentStructVar, got {f:?}"),
|
||||
}
|
||||
|
||||
let pybool = PyBool { bla: true }.into_pyobject(py).unwrap();
|
||||
let f = pybool
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from PyBool");
|
||||
match f {
|
||||
Foo::StructVarGetAttrArg { a } => assert!(a),
|
||||
_ => panic!("Expected extracting Foo::StructVarGetAttrArg, got {f:?}"),
|
||||
}
|
||||
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("a", "test").expect("Failed to set item");
|
||||
let f = dict
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from dict");
|
||||
match f {
|
||||
Foo::StructWithGetItem { a } => assert_eq!(a, "test"),
|
||||
_ => panic!("Expected extracting Foo::StructWithGetItem, got {f:?}"),
|
||||
}
|
||||
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("foo", "test").expect("Failed to set item");
|
||||
let f = dict
|
||||
.extract::<Foo<'_>>()
|
||||
.expect("Failed to extract Foo from dict");
|
||||
match f {
|
||||
Foo::StructWithGetItemArg { a } => assert_eq!(a, "test"),
|
||||
_ => panic!("Expected extracting Foo::StructWithGetItemArg, got {f:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_error() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
let err = dict.extract::<Foo<'_>>().unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"\
|
||||
TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg')
|
||||
- variant TupleVar (TupleVar): TypeError: 'dict' object is not an instance of 'tuple'
|
||||
- variant StructVar (StructVar): AttributeError: 'dict' object has no attribute 'test'
|
||||
- variant TransparentTuple (TransparentTuple): TypeError: failed to extract field Foo::TransparentTuple.0, caused by TypeError: 'dict' object cannot be interpreted as an integer
|
||||
- variant TransparentStructVar (TransparentStructVar): TypeError: failed to extract field Foo::TransparentStructVar.a, caused by TypeError: 'dict' object is not an instance of 'str'
|
||||
- variant StructVarGetAttrArg (StructVarGetAttrArg): AttributeError: 'dict' object has no attribute 'bla'
|
||||
- variant StructWithGetItem (StructWithGetItem): KeyError: 'a'
|
||||
- variant StructWithGetItemArg (StructWithGetItemArg): KeyError: 'foo'"
|
||||
);
|
||||
|
||||
let tup = PyTuple::empty(py);
|
||||
let err = tup.extract::<Foo<'_>>().unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"\
|
||||
TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple | TransparentStructVar | StructVarGetAttrArg | StructWithGetItem | StructWithGetItemArg')
|
||||
- variant TupleVar (TupleVar): ValueError: expected tuple of length 2, but got tuple of length 0
|
||||
- variant StructVar (StructVar): AttributeError: 'tuple' object has no attribute 'test'
|
||||
- variant TransparentTuple (TransparentTuple): TypeError: failed to extract field Foo::TransparentTuple.0, caused by TypeError: 'tuple' object cannot be interpreted as an integer
|
||||
- variant TransparentStructVar (TransparentStructVar): TypeError: failed to extract field Foo::TransparentStructVar.a, caused by TypeError: 'tuple' object is not an instance of 'str'
|
||||
- variant StructVarGetAttrArg (StructVarGetAttrArg): AttributeError: 'tuple' object has no attribute 'bla'
|
||||
- variant StructWithGetItem (StructWithGetItem): TypeError: tuple indices must be integers or slices, not str
|
||||
- variant StructWithGetItemArg (StructWithGetItemArg): TypeError: tuple indices must be integers or slices, not str"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
enum EnumWithCatchAll<'py> {
|
||||
#[expect(dead_code)]
|
||||
#[pyo3(transparent)]
|
||||
Foo(Foo<'py>),
|
||||
#[pyo3(transparent)]
|
||||
CatchAll(Bound<'py, PyAny>),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_catch_all() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
let f = dict
|
||||
.extract::<EnumWithCatchAll<'_>>()
|
||||
.expect("Failed to extract EnumWithCatchAll from dict");
|
||||
match f {
|
||||
EnumWithCatchAll::CatchAll(any) => {
|
||||
let d = any.extract::<Bound<'_, PyDict>>().expect("Expected pydict");
|
||||
assert!(d.is_empty());
|
||||
}
|
||||
_ => panic!("Expected extracting EnumWithCatchAll::CatchAll, got {f:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub enum Bar {
|
||||
#[pyo3(annotation = "str")]
|
||||
A(String),
|
||||
#[pyo3(annotation = "uint")]
|
||||
B(usize),
|
||||
#[pyo3(annotation = "int", transparent)]
|
||||
C(isize),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_err_rename() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
let f = dict.extract::<Bar>();
|
||||
assert!(f.is_err());
|
||||
assert_eq!(
|
||||
f.unwrap_err().to_string(),
|
||||
"\
|
||||
TypeError: failed to extract enum Bar ('str | uint | int')
|
||||
- variant A (str): TypeError: failed to extract field Bar::A.0, caused by TypeError: 'dict' object is not an instance of 'str'
|
||||
- variant B (uint): TypeError: failed to extract field Bar::B.0, caused by TypeError: 'dict' object cannot be interpreted as an integer
|
||||
- variant C (int): TypeError: failed to extract field Bar::C.0, caused by TypeError: 'dict' object cannot be interpreted as an integer"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct Zap {
|
||||
#[pyo3(item)]
|
||||
name: String,
|
||||
|
||||
#[pyo3(from_py_with = Bound::<'_, PyAny>::len, item("my_object"))]
|
||||
some_object_length: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_py_with() {
|
||||
Python::attach(|py| {
|
||||
let py_zap = py
|
||||
.eval(
|
||||
cr#"{"name": "whatever", "my_object": [1, 2, 3]}"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("failed to create dict");
|
||||
|
||||
let zap = py_zap.extract::<Zap>().unwrap();
|
||||
|
||||
assert_eq!(zap.name, "whatever");
|
||||
assert_eq!(zap.some_object_length, 3usize);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject)]
|
||||
pub struct ZapTuple(
|
||||
String,
|
||||
#[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize,
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_from_py_with_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let py_zap = py
|
||||
.eval(cr#"("whatever", [1, 2, 3])"#, None, None)
|
||||
.expect("failed to create tuple");
|
||||
|
||||
let zap = py_zap.extract::<ZapTuple>().unwrap();
|
||||
|
||||
assert_eq!(zap.0, "whatever");
|
||||
assert_eq!(zap.1, 3usize);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_py_with_tuple_struct_error() {
|
||||
Python::attach(|py| {
|
||||
let py_zap = py
|
||||
.eval(cr#"("whatever", [1, 2, 3], "third")"#, None, None)
|
||||
.expect("failed to create tuple");
|
||||
|
||||
let f = py_zap.extract::<ZapTuple>();
|
||||
|
||||
assert!(f.is_err());
|
||||
assert_eq!(
|
||||
f.unwrap_err().to_string(),
|
||||
"ValueError: expected tuple of length 2, but got tuple of length 3"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub enum ZapEnum {
|
||||
Zip(#[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize),
|
||||
Zap(
|
||||
String,
|
||||
#[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize,
|
||||
),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_py_with_enum() {
|
||||
Python::attach(|py| {
|
||||
let py_zap = py
|
||||
.eval(cr#"("whatever", [1, 2, 3])"#, None, None)
|
||||
.expect("failed to create tuple");
|
||||
|
||||
let zap = py_zap.extract::<ZapEnum>().unwrap();
|
||||
let expected_zap = ZapEnum::Zip(2);
|
||||
|
||||
assert_eq!(zap, expected_zap);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct TransparentFromPyWith {
|
||||
#[pyo3(from_py_with = Bound::<'_, PyAny>::len)]
|
||||
len: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_from_py_with() {
|
||||
Python::attach(|py| {
|
||||
let result = PyList::new(py, [1, 2, 3])
|
||||
.unwrap()
|
||||
.extract::<TransparentFromPyWith>()
|
||||
.unwrap();
|
||||
let expected = TransparentFromPyWith { len: 3 };
|
||||
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub struct WithKeywordAttr {
|
||||
r#box: usize,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
pub struct WithKeywordAttrC {
|
||||
#[pyo3(get)]
|
||||
r#box: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_keyword_attr() {
|
||||
Python::attach(|py| {
|
||||
let cls = WithKeywordAttrC { r#box: 3 }.into_pyobject(py).unwrap();
|
||||
let result = cls.extract::<WithKeywordAttr>().unwrap();
|
||||
let expected = WithKeywordAttr { r#box: 3 };
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub struct WithKeywordItem {
|
||||
#[pyo3(item)]
|
||||
r#box: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_keyword_item() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("box", 3).unwrap();
|
||||
let result = dict.extract::<WithKeywordItem>().unwrap();
|
||||
let expected = WithKeywordItem { r#box: 3 };
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub struct WithDefaultItem {
|
||||
#[pyo3(item, default)]
|
||||
opt: Option<usize>,
|
||||
#[pyo3(item)]
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_default_item() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("value", 3).unwrap();
|
||||
let result = dict.extract::<WithDefaultItem>().unwrap();
|
||||
let expected = WithDefaultItem {
|
||||
value: 3,
|
||||
opt: None,
|
||||
};
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub struct WithExplicitDefaultItem {
|
||||
#[pyo3(item, default = 1)]
|
||||
opt: usize,
|
||||
#[pyo3(item)]
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_explicit_default_item() {
|
||||
Python::attach(|py| {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("value", 3).unwrap();
|
||||
let result = dict.extract::<WithExplicitDefaultItem>().unwrap();
|
||||
let expected = WithExplicitDefaultItem { value: 3, opt: 1 };
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub struct WithDefaultItemAndConversionFunction {
|
||||
#[pyo3(item, default, from_py_with = Bound::<'_, PyAny>::len)]
|
||||
opt: usize,
|
||||
#[pyo3(item)]
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_default_item_and_conversion_function() {
|
||||
Python::attach(|py| {
|
||||
// Filled case
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("opt", (1,)).unwrap();
|
||||
dict.set_item("value", 3).unwrap();
|
||||
let result = dict
|
||||
.extract::<WithDefaultItemAndConversionFunction>()
|
||||
.unwrap();
|
||||
let expected = WithDefaultItemAndConversionFunction { opt: 1, value: 3 };
|
||||
assert_eq!(result, expected);
|
||||
|
||||
// Empty case
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("value", 3).unwrap();
|
||||
let result = dict
|
||||
.extract::<WithDefaultItemAndConversionFunction>()
|
||||
.unwrap();
|
||||
let expected = WithDefaultItemAndConversionFunction { opt: 0, value: 3 };
|
||||
assert_eq!(result, expected);
|
||||
|
||||
// Error case
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("value", 3).unwrap();
|
||||
dict.set_item("opt", 1).unwrap();
|
||||
assert!(dict
|
||||
.extract::<WithDefaultItemAndConversionFunction>()
|
||||
.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, FromPyObject, PartialEq, Eq)]
|
||||
pub enum WithDefaultItemEnum {
|
||||
#[pyo3(from_item_all)]
|
||||
Foo {
|
||||
a: usize,
|
||||
#[pyo3(default)]
|
||||
b: usize,
|
||||
},
|
||||
NeverUsedA {
|
||||
a: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_default_item_enum() {
|
||||
Python::attach(|py| {
|
||||
// A and B filled
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("a", 1).unwrap();
|
||||
dict.set_item("b", 2).unwrap();
|
||||
let result = dict.extract::<WithDefaultItemEnum>().unwrap();
|
||||
let expected = WithDefaultItemEnum::Foo { a: 1, b: 2 };
|
||||
assert_eq!(result, expected);
|
||||
|
||||
// A filled
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("a", 1).unwrap();
|
||||
let result = dict.extract::<WithDefaultItemEnum>().unwrap();
|
||||
let expected = WithDefaultItemEnum::Foo { a: 1, b: 0 };
|
||||
assert_eq!(result, expected);
|
||||
});
|
||||
}
|
||||
Vendored
+994
@@ -0,0 +1,994 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::class::PyTraverseError;
|
||||
use pyo3::class::PyVisit;
|
||||
use pyo3::ffi;
|
||||
use pyo3::prelude::*;
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
use pyo3::py_run;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
|
||||
use std::sync::Once;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(freelist = 2)]
|
||||
struct ClassWithFreelist {}
|
||||
|
||||
#[test]
|
||||
fn class_with_freelist() {
|
||||
let ptr = Python::attach(|py| {
|
||||
let inst = Py::new(py, ClassWithFreelist {}).unwrap();
|
||||
let _inst2 = Py::new(py, ClassWithFreelist {}).unwrap();
|
||||
let ptr = inst.as_ptr();
|
||||
drop(inst);
|
||||
ptr
|
||||
});
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst3 = Py::new(py, ClassWithFreelist {}).unwrap();
|
||||
assert_eq!(ptr, inst3.as_ptr());
|
||||
|
||||
let inst4 = Py::new(py, ClassWithFreelist {}).unwrap();
|
||||
assert_ne!(ptr, inst4.as_ptr())
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(freelist = 2)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct ClassWithFreelistAndData {
|
||||
data: Option<usize>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spin_freelist(py: Python<'_>, data: usize) {
|
||||
for _ in 0..500 {
|
||||
let inst1 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
|
||||
let inst2 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap();
|
||||
assert_eq!(inst1.borrow(py).data, Some(data));
|
||||
assert_eq!(inst2.borrow(py).data, Some(data));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn multithreaded_class_with_freelist() {
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(|| {
|
||||
Python::attach(|py| spin_freelist(py, 12));
|
||||
});
|
||||
s.spawn(|| {
|
||||
Python::attach(|py| spin_freelist(py, 0x4d3d3d3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper function to create a pair of objects that can be used to test drops;
|
||||
/// the first object is a guard that records when it has been dropped, the second
|
||||
/// object is a check that can be used to assert that the guard has been dropped.
|
||||
fn drop_check() -> (DropGuard, DropCheck) {
|
||||
let flag = Arc::new(Once::new());
|
||||
(DropGuard(flag.clone()), DropCheck(flag))
|
||||
}
|
||||
|
||||
/// Helper structure that records when it has been dropped in the cor
|
||||
struct DropGuard(Arc<Once>);
|
||||
impl Drop for DropGuard {
|
||||
fn drop(&mut self) {
|
||||
self.0.call_once(|| ());
|
||||
}
|
||||
}
|
||||
|
||||
struct DropCheck(Arc<Once>);
|
||||
impl DropCheck {
|
||||
#[track_caller]
|
||||
fn assert_not_dropped(&self) {
|
||||
assert!(!self.0.is_completed());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_dropped(&self) {
|
||||
assert!(self.0.is_completed());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_drops_with_gc(&self, object: *mut pyo3::ffi::PyObject) {
|
||||
// running the GC might take a few cycles to collect an object
|
||||
for _ in 0..100 {
|
||||
if self.0.is_completed() {
|
||||
return;
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
py.run(c"import gc; gc.collect()", None, None).unwrap();
|
||||
});
|
||||
#[cfg(Py_GIL_DISABLED)]
|
||||
{
|
||||
// on the free-threaded build, the GC might be running in a separate thread, allow
|
||||
// some time for this
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
panic!(
|
||||
"Object was not dropped after 100 GC cycles, refcount is {}",
|
||||
// this could be garbage, but it's in a test and we're just printing the value
|
||||
unsafe { ffi::Py_REFCNT(object) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_is_dropped() {
|
||||
#[pyclass]
|
||||
struct DataIsDropped {
|
||||
_guard1: DropGuard,
|
||||
_guard2: DropGuard,
|
||||
}
|
||||
|
||||
let (guard1, check1) = drop_check();
|
||||
let (guard2, check2) = drop_check();
|
||||
|
||||
Python::attach(|py| {
|
||||
let data_is_dropped = DataIsDropped {
|
||||
_guard1: guard1,
|
||||
_guard2: guard2,
|
||||
};
|
||||
let inst = Py::new(py, data_is_dropped).unwrap();
|
||||
check1.assert_not_dropped();
|
||||
check2.assert_not_dropped();
|
||||
drop(inst);
|
||||
});
|
||||
|
||||
check1.assert_dropped();
|
||||
check2.assert_dropped();
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct CycleWithClear {
|
||||
cycle: Option<Py<PyAny>>,
|
||||
_guard: DropGuard,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl CycleWithClear {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.cycle)
|
||||
}
|
||||
|
||||
fn __clear__(slf: &Bound<'_, Self>) {
|
||||
slf.borrow_mut().cycle = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_clear() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let ptr = Python::attach(|py| {
|
||||
let inst = Bound::new(
|
||||
py,
|
||||
CycleWithClear {
|
||||
cycle: None,
|
||||
_guard: guard,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
inst.borrow_mut().cycle = Some(inst.clone().into_any().unbind());
|
||||
|
||||
// gc.get_objects can create references to partially initialized objects,
|
||||
// leading to races on the free-threaded build.
|
||||
// see https://github.com/python/cpython/issues/130421#issuecomment-2682924142
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
py_run!(py, inst, "import gc; assert inst in gc.get_objects()");
|
||||
check.assert_not_dropped();
|
||||
inst.as_ptr()
|
||||
});
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
|
||||
/// Test that traversing `None` of `Option<Py<T>>` does not cause a segfault
|
||||
#[test]
|
||||
fn gc_null_traversal() {
|
||||
#[pyclass]
|
||||
struct GcNullTraversal {
|
||||
cycle: Option<Py<Self>>,
|
||||
null: Option<Py<Self>>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl GcNullTraversal {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.cycle)?;
|
||||
visit.call(&self.null)?; // Should not segfault
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __clear__(&mut self) {
|
||||
self.cycle = None;
|
||||
self.null = None;
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let obj = Py::new(
|
||||
py,
|
||||
GcNullTraversal {
|
||||
cycle: None,
|
||||
null: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
obj.borrow_mut(py).cycle = Some(obj.clone_ref(py));
|
||||
|
||||
// the object doesn't have to be cleaned up, it just needs to be traversed.
|
||||
py.run(c"import gc; gc.collect()", None, None).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inheritance_with_new_methods_with_drop() {
|
||||
#[pyclass(subclass)]
|
||||
struct BaseClassWithDrop {
|
||||
guard: Option<DropGuard>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BaseClassWithDrop {
|
||||
#[new]
|
||||
fn new() -> BaseClassWithDrop {
|
||||
BaseClassWithDrop { guard: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends = BaseClassWithDrop)]
|
||||
struct SubClassWithDrop {
|
||||
guard: Option<DropGuard>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SubClassWithDrop {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(BaseClassWithDrop { guard: None })
|
||||
.add_subclass(SubClassWithDrop { guard: None })
|
||||
}
|
||||
}
|
||||
|
||||
let (guard_base, check_base) = drop_check();
|
||||
let (guard_sub, check_sub) = drop_check();
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<SubClassWithDrop>();
|
||||
let inst = typeobj
|
||||
.call((), None)
|
||||
.unwrap()
|
||||
.cast_into::<SubClassWithDrop>()
|
||||
.unwrap();
|
||||
|
||||
inst.as_super().borrow_mut().guard = Some(guard_base);
|
||||
inst.borrow_mut().guard = Some(guard_sub);
|
||||
|
||||
check_base.assert_not_dropped();
|
||||
check_sub.assert_not_dropped();
|
||||
});
|
||||
|
||||
check_base.assert_dropped();
|
||||
check_sub.assert_dropped();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_during_borrow() {
|
||||
#[pyclass]
|
||||
struct TraversableClass {
|
||||
traversed: AtomicBool,
|
||||
}
|
||||
|
||||
impl TraversableClass {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
traversed: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl TraversableClass {
|
||||
fn __clear__(&mut self) {}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
self.traversed.store(true, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
// get the traverse function
|
||||
let ty = py.get_type::<TraversableClass>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
// create an object and check that traversing it works normally
|
||||
// when it's not borrowed
|
||||
let cell = Bound::new(py, TraversableClass::new()).unwrap();
|
||||
assert!(!cell.borrow().traversed.load(Ordering::Relaxed));
|
||||
unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
|
||||
assert!(cell.borrow().traversed.load(Ordering::Relaxed));
|
||||
|
||||
// create an object and check that it is not traversed if the GC
|
||||
// is invoked while it is already borrowed mutably
|
||||
let cell2 = Bound::new(py, TraversableClass::new()).unwrap();
|
||||
let guard = cell2.borrow_mut();
|
||||
assert!(!guard.traversed.load(Ordering::Relaxed));
|
||||
unsafe { traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()) };
|
||||
assert!(!guard.traversed.load(Ordering::Relaxed));
|
||||
drop(guard);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traverse_partial() {
|
||||
#[pyclass]
|
||||
struct PartialTraverse {
|
||||
member: Py<PyAny>,
|
||||
}
|
||||
|
||||
impl PartialTraverse {
|
||||
fn new(py: Python<'_>) -> Self {
|
||||
Self { member: py.None() }
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PartialTraverse {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.member)?;
|
||||
// In the test, we expect this to never be hit
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
// get the traverse function
|
||||
let ty = py.get_type::<PartialTraverse>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
// confirm that traversing errors
|
||||
let obj = Py::new(py, PartialTraverse::new(py)).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(panic = "unwind")]
|
||||
fn traverse_panic() {
|
||||
#[pyclass]
|
||||
struct PanickyTraverse {
|
||||
member: Py<PyAny>,
|
||||
}
|
||||
|
||||
impl PanickyTraverse {
|
||||
fn new(py: Python<'_>) -> Self {
|
||||
Self { member: py.None() }
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PanickyTraverse {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.member)?;
|
||||
panic!("at the disco");
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
// get the traverse function
|
||||
let ty = py.get_type::<PanickyTraverse>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
// confirm that traversing errors
|
||||
let obj = Py::new(py, PanickyTraverse::new(py)).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(panic = "unwind")]
|
||||
fn tries_gil_in_traverse() {
|
||||
#[pyclass]
|
||||
struct TriesGILInTraverse {}
|
||||
|
||||
#[pymethods]
|
||||
impl TriesGILInTraverse {
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
Python::attach(|_py| Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
// get the traverse function
|
||||
let ty = py.get_type::<TriesGILInTraverse>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
// confirm that traversing panicks
|
||||
let obj = Py::new(py, TriesGILInTraverse {}).unwrap();
|
||||
assert_eq!(
|
||||
unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) },
|
||||
-1
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traverse_cannot_be_hijacked() {
|
||||
#[pyclass]
|
||||
struct HijackedTraverse {
|
||||
traversed: AtomicBool,
|
||||
hijacked: AtomicBool,
|
||||
}
|
||||
|
||||
impl HijackedTraverse {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
traversed: AtomicBool::new(false),
|
||||
hijacked: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn traversed_and_hijacked(&self) -> (bool, bool) {
|
||||
(
|
||||
self.traversed.load(Ordering::Acquire),
|
||||
self.hijacked.load(Ordering::Acquire),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl HijackedTraverse {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
self.traversed.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code, reason = "used to test the trait method is not used")]
|
||||
trait Traversable {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>;
|
||||
}
|
||||
|
||||
impl Traversable for PyRef<'_, HijackedTraverse> {
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
self.hijacked.store(true, Ordering::Release);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
// get the traverse function
|
||||
let ty = py.get_type::<HijackedTraverse>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
let cell = Bound::new(py, HijackedTraverse::new()).unwrap();
|
||||
assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false));
|
||||
unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) };
|
||||
assert_eq!(cell.borrow().traversed_and_hijacked(), (true, false));
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct DropDuringTraversal {
|
||||
cycle: Mutex<Option<Py<Self>>>,
|
||||
_guard: DropGuard,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl DropDuringTraversal {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
let mut cycle_ref = self.cycle.lock().unwrap();
|
||||
*cycle_ref = None;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
#[test]
|
||||
fn drop_during_traversal_with_gil() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let ptr = Python::attach(|py| {
|
||||
let cycle = Mutex::new(None);
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DropDuringTraversal {
|
||||
cycle,
|
||||
_guard: guard,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
*inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));
|
||||
|
||||
check.assert_not_dropped();
|
||||
let ptr = inst.as_ptr();
|
||||
drop(inst); // drop the object while holding the GIL
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
{
|
||||
// other thread might have caused GC on free-threaded build
|
||||
check.assert_not_dropped();
|
||||
}
|
||||
|
||||
ptr
|
||||
});
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
|
||||
#[cfg(not(pyo3_disable_reference_pool))]
|
||||
#[test]
|
||||
fn drop_during_traversal_without_gil() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let inst = Python::attach(|py| {
|
||||
let cycle = Mutex::new(None);
|
||||
let inst = Py::new(
|
||||
py,
|
||||
DropDuringTraversal {
|
||||
cycle,
|
||||
_guard: guard,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
*inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py));
|
||||
|
||||
check.assert_not_dropped();
|
||||
inst
|
||||
});
|
||||
|
||||
let ptr = inst.as_ptr();
|
||||
drop(inst); // drop the object without holding the GIL
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
|
||||
fn unsendable_are_not_traversed_on_foreign_thread() {
|
||||
#[pyclass(unsendable)]
|
||||
struct UnsendableTraversal {
|
||||
traversed: Cell<bool>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl UnsendableTraversal {
|
||||
fn __clear__(&mut self) {}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
self.traversed.set(true);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SendablePtr(*mut pyo3::ffi::PyObject);
|
||||
|
||||
unsafe impl Send for SendablePtr {}
|
||||
|
||||
Python::attach(|py| {
|
||||
let ty = py.get_type::<UnsendableTraversal>();
|
||||
let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
|
||||
|
||||
let obj = Bound::new(
|
||||
py,
|
||||
UnsendableTraversal {
|
||||
traversed: Cell::new(false),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ptr = SendablePtr(obj.as_ptr());
|
||||
|
||||
std::thread::spawn(move || {
|
||||
// traversal on foreign thread is a no-op
|
||||
assert_eq!(
|
||||
unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
|
||||
assert!(!obj.borrow().traversed.get());
|
||||
|
||||
// traversal on home thread still works
|
||||
assert_eq!(
|
||||
unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) },
|
||||
0
|
||||
);
|
||||
|
||||
assert!(obj.borrow().traversed.get());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_traverse_subclass() {
|
||||
#[pyclass(extends = CycleWithClear)]
|
||||
struct SubOverrideTraverse {}
|
||||
|
||||
#[pymethods]
|
||||
impl SubOverrideTraverse {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
// subclass traverse overrides the base class traverse
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
Python::attach(|py| {
|
||||
let base = CycleWithClear {
|
||||
cycle: None,
|
||||
_guard: guard,
|
||||
};
|
||||
let obj = Bound::new(
|
||||
py,
|
||||
PyClassInitializer::from(base).add_subclass(SubOverrideTraverse {}),
|
||||
)
|
||||
.unwrap();
|
||||
obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());
|
||||
|
||||
check.assert_not_dropped();
|
||||
let ptr = obj.as_ptr();
|
||||
drop(obj);
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
{
|
||||
// other thread might have caused GC on free-threaded build
|
||||
check.assert_not_dropped();
|
||||
}
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_traverse_subclass_override_clear() {
|
||||
#[pyclass(extends = CycleWithClear)]
|
||||
struct SubOverrideClear {}
|
||||
|
||||
#[pymethods]
|
||||
impl SubOverrideClear {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
// subclass traverse overrides the base class traverse, necessary for
|
||||
// the sub clear to be called
|
||||
// FIXME: should this really need to be the case?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __clear__(&self) {
|
||||
// subclass clear overrides the base class clear
|
||||
}
|
||||
}
|
||||
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
Python::attach(|py| {
|
||||
let base = CycleWithClear {
|
||||
cycle: None,
|
||||
_guard: guard,
|
||||
};
|
||||
let obj = Bound::new(
|
||||
py,
|
||||
PyClassInitializer::from(base).add_subclass(SubOverrideClear {}),
|
||||
)
|
||||
.unwrap();
|
||||
obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind());
|
||||
|
||||
check.assert_not_dropped();
|
||||
let ptr = obj.as_ptr();
|
||||
drop(obj);
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
{
|
||||
// other thread might have caused GC on free-threaded build
|
||||
check.assert_not_dropped();
|
||||
}
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
});
|
||||
}
|
||||
|
||||
// Manual traversal utilities
|
||||
|
||||
unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option<pyo3::ffi::traverseproc> {
|
||||
unsafe { std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) }
|
||||
}
|
||||
|
||||
// a dummy visitor function
|
||||
extern "C" fn novisit(
|
||||
_object: *mut pyo3::ffi::PyObject,
|
||||
_arg: *mut core::ffi::c_void,
|
||||
) -> std::ffi::c_int {
|
||||
0
|
||||
}
|
||||
|
||||
// a visitor function which errors (returns nonzero code)
|
||||
extern "C" fn visit_error(
|
||||
_object: *mut pyo3::ffi::PyObject,
|
||||
_arg: *mut core::ffi::c_void,
|
||||
) -> std::ffi::c_int {
|
||||
-1
|
||||
}
|
||||
|
||||
// the fields visited below, set before driving the traversal
|
||||
static BASE_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static CHILD_FIELD: AtomicPtr<pyo3::ffi::PyObject> = AtomicPtr::new(std::ptr::null_mut());
|
||||
static BASE_VISITED: AtomicBool = AtomicBool::new(false);
|
||||
static CHILD_VISITED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// a visitor function which errors on `BASE_FIELD` only
|
||||
extern "C" fn visit_error_on_base_field(
|
||||
object: *mut pyo3::ffi::PyObject,
|
||||
_arg: *mut core::ffi::c_void,
|
||||
) -> std::ffi::c_int {
|
||||
if object == CHILD_FIELD.load(Ordering::SeqCst) {
|
||||
CHILD_VISITED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
if object == BASE_FIELD.load(Ordering::SeqCst) {
|
||||
BASE_VISITED.store(true, Ordering::SeqCst);
|
||||
return -1;
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))] // buffer availability
|
||||
fn test_drop_buffer_during_traversal_without_gil() {
|
||||
use pyo3::buffer::PyBuffer;
|
||||
use pyo3::types::PyBytes;
|
||||
|
||||
// `PyBuffer` has a drop method which attempts to attach to the Python interpreter,
|
||||
// if the thread is during traverse we leak it for safety. This should _never_ be happening
|
||||
// so it's purely a user bug, but we leak to be safe.
|
||||
|
||||
#[pyclass]
|
||||
struct BufferDropDuringTraversal {
|
||||
inner: Mutex<Option<(DropGuard, PyBuffer<u8>)>>,
|
||||
cycle: Option<Py<PyAny>>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BufferDropDuringTraversal {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
self.inner.lock().unwrap().take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __clear__(&mut self) {
|
||||
self.cycle = None;
|
||||
}
|
||||
}
|
||||
|
||||
let (guard, check) = drop_check();
|
||||
Python::attach(|py| {
|
||||
let obj = Py::new(
|
||||
py,
|
||||
BufferDropDuringTraversal {
|
||||
inner: Mutex::new(Some((
|
||||
guard,
|
||||
PyBuffer::get(&PyBytes::new(py, b"test")).unwrap(),
|
||||
))),
|
||||
cycle: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
obj.borrow_mut(py).cycle = Some(obj.clone_ref(py).into_any());
|
||||
|
||||
let ptr = obj.as_ptr();
|
||||
drop(obj);
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_super_traverse_early_return_does_not_abort() {
|
||||
#[pyclass(subclass)]
|
||||
struct TraverseBase {
|
||||
field: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl TraverseBase {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.field)
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=TraverseBase)]
|
||||
struct TraverseChild {
|
||||
field: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl TraverseChild {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
visit.call(&self.field)
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let base_field = pyo3::types::PyList::empty(py).into_any().unbind();
|
||||
let child_field = pyo3::types::PyList::empty(py).into_any().unbind();
|
||||
|
||||
let initializer = PyClassInitializer::from(TraverseBase {
|
||||
field: base_field.clone_ref(py),
|
||||
})
|
||||
.add_subclass(TraverseChild {
|
||||
field: child_field.clone_ref(py),
|
||||
});
|
||||
let child = Bound::new(py, initializer).unwrap();
|
||||
|
||||
BASE_FIELD.store(base_field.as_ptr(), Ordering::SeqCst);
|
||||
CHILD_FIELD.store(child_field.as_ptr(), Ordering::SeqCst);
|
||||
|
||||
let traverse =
|
||||
unsafe { get_type_traverse(py.get_type::<TraverseChild>().as_type_ptr()).unwrap() };
|
||||
|
||||
let retval = unsafe {
|
||||
traverse(
|
||||
child.as_ptr(),
|
||||
visit_error_on_base_field,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
assert!(
|
||||
BASE_VISITED.load(Ordering::SeqCst),
|
||||
"super-type traverse never visited its field, so the early return was not exercised"
|
||||
);
|
||||
assert_eq!(
|
||||
retval, -1,
|
||||
"traverse did not propagate the non-zero return of the super-type traverse"
|
||||
);
|
||||
assert!(
|
||||
!CHILD_VISITED.load(Ordering::SeqCst),
|
||||
"child __traverse__ ran despite the super-type traverse returning non-zero"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// A `#[pyclass(dict)]` can form a reference cycle through its instance `__dict__`
|
||||
// (`obj.attr = obj`). The tests below cover each valid combination of user-defined
|
||||
// `__traverse__` / `__clear__`; a `__clear__` without a `__traverse__` is rejected at
|
||||
// type-creation time.
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[test]
|
||||
fn dict_class_is_a_gc_type() {
|
||||
Python::attach(|py| {
|
||||
let ty = py.get_type::<DictCycleNoTraverse>();
|
||||
let flags = unsafe { ffi::PyType_GetFlags(ty.as_type_ptr()) };
|
||||
assert_ne!(flags & ffi::Py_TPFLAGS_HAVE_GC, 0);
|
||||
});
|
||||
}
|
||||
|
||||
/// `#[pyclass(dict)]` with neither `__traverse__` nor `__clear__`: both slots are synthesized.
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(dict)]
|
||||
struct DictCycleNoTraverse {
|
||||
_guard: DropGuard,
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[test]
|
||||
fn dict_cycle_collected_without_traverse() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let ptr = Python::attach(|py| {
|
||||
let inst = Bound::new(py, DictCycleNoTraverse { _guard: guard }).unwrap();
|
||||
// Reference cycle through the instance `__dict__`: inst.__dict__["cycle"] -> inst.
|
||||
inst.setattr("cycle", &inst).unwrap();
|
||||
check.assert_not_dropped();
|
||||
inst.as_ptr()
|
||||
});
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
|
||||
/// `#[pyclass(dict)]` with `__traverse__` but no `__clear__`: the `__dict__` is visited by
|
||||
/// `_call_traverse` and cleared by a synthesized `tp_clear`.
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(dict)]
|
||||
struct DictCycleTraverseOnly {
|
||||
_guard: DropGuard,
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pymethods]
|
||||
impl DictCycleTraverseOnly {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
// No Rust references to visit; the `__dict__` is visited by `_call_traverse` itself.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[test]
|
||||
fn dict_cycle_collected_with_traverse_only() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let ptr = Python::attach(|py| {
|
||||
let inst = Bound::new(py, DictCycleTraverseOnly { _guard: guard }).unwrap();
|
||||
inst.setattr("cycle", &inst).unwrap();
|
||||
check.assert_not_dropped();
|
||||
inst.as_ptr()
|
||||
});
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
|
||||
/// `#[pyclass(dict)]` with both `__traverse__` and `__clear__`: the `__dict__` is folded into
|
||||
/// the user-defined slots by `_call_traverse` / `_call_clear`.
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pyclass(dict)]
|
||||
struct DictCycleTraverseAndClear {
|
||||
_guard: DropGuard,
|
||||
field: Option<Py<PyAny>>,
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[pymethods]
|
||||
impl DictCycleTraverseAndClear {
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
if let Some(field) = &self.field {
|
||||
visit.call(field)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __clear__(&mut self) {
|
||||
self.field = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
#[test]
|
||||
fn dict_cycle_collected_with_traverse_and_clear() {
|
||||
let (guard, check) = drop_check();
|
||||
|
||||
let ptr = Python::attach(|py| {
|
||||
let inst = Bound::new(
|
||||
py,
|
||||
DictCycleTraverseAndClear {
|
||||
_guard: guard,
|
||||
field: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
inst.setattr("cycle", &inst).unwrap();
|
||||
check.assert_not_dropped();
|
||||
inst.as_ptr()
|
||||
});
|
||||
|
||||
check.assert_drops_with_gc(ptr);
|
||||
}
|
||||
+356
@@ -0,0 +1,356 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::PyString;
|
||||
use pyo3::types::{IntoPyDict, PyList};
|
||||
use std::cell::Cell;
|
||||
use std::convert::Infallible;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct ClassWithProperties {
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ClassWithProperties {
|
||||
fn get_num(&self) -> i32 {
|
||||
self.num
|
||||
}
|
||||
|
||||
#[getter(DATA)]
|
||||
/// a getter for data
|
||||
fn get_data(&self) -> i32 {
|
||||
self.num
|
||||
}
|
||||
#[setter(DATA)]
|
||||
fn set_data(&mut self, value: i32) {
|
||||
self.num = value;
|
||||
}
|
||||
|
||||
#[getter]
|
||||
/// a getter with a type un-wrapped by PyResult
|
||||
fn get_unwrapped(&self) -> i32 {
|
||||
self.num
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_unwrapped(&mut self, value: i32) {
|
||||
self.num = value;
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_from_len(&mut self, #[pyo3(from_py_with = extract_len)] value: i32) {
|
||||
self.num = value;
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_from_any(&mut self, value: &Bound<'_, PyAny>) -> PyResult<()> {
|
||||
self.num = value.extract()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn get_data_list<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
|
||||
PyList::new(py, [self.num])
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_len(any: &Bound<'_, PyAny>) -> PyResult<i32> {
|
||||
any.len().map(|len| len as i32)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_with_properties() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, ClassWithProperties { num: 10 }).unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.get_num() == 10");
|
||||
py_run!(py, inst, "assert inst.get_num() == inst.DATA");
|
||||
py_run!(py, inst, "inst.DATA = 20");
|
||||
py_run!(py, inst, "assert inst.get_num() == 20 == inst.DATA");
|
||||
|
||||
py_expect_exception!(py, inst, "del inst.DATA", PyAttributeError);
|
||||
|
||||
py_run!(py, inst, "assert inst.get_num() == inst.unwrapped == 20");
|
||||
py_run!(py, inst, "inst.unwrapped = 42");
|
||||
py_run!(py, inst, "assert inst.get_num() == inst.unwrapped == 42");
|
||||
py_run!(py, inst, "assert inst.data_list == [42]");
|
||||
|
||||
py_run!(py, inst, "inst.from_len = [0, 0, 0]");
|
||||
py_run!(py, inst, "assert inst.get_num() == 3");
|
||||
|
||||
py_run!(py, inst, "inst.from_any = 15");
|
||||
py_run!(py, inst, "assert inst.get_num() == 15");
|
||||
|
||||
let d = [("C", py.get_type::<ClassWithProperties>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct GetterSetter {
|
||||
#[pyo3(get, set)]
|
||||
num: i32,
|
||||
#[pyo3(get, set)]
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl GetterSetter {
|
||||
fn get_num2(&self) -> i32 {
|
||||
self.num
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn getter_setter_autogen() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
GetterSetter {
|
||||
num: 10,
|
||||
text: "Hello".to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.num == 10");
|
||||
py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"assert inst.text == 'Hello'; inst.text = 'There'; assert inst.text == 'There'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct RefGetterSetter {
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl RefGetterSetter {
|
||||
#[getter]
|
||||
fn get_num(slf: PyRef<'_, Self>) -> i32 {
|
||||
slf.num
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_num(mut slf: PyRefMut<'_, Self>, value: i32) {
|
||||
slf.num = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_getter_setter() {
|
||||
// Regression test for #837
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, RefGetterSetter { num: 10 }).unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.num == 10");
|
||||
py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct TupleClassGetterSetter(i32);
|
||||
|
||||
#[pymethods]
|
||||
impl TupleClassGetterSetter {
|
||||
#[getter(num)]
|
||||
fn get_num(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[setter(num)]
|
||||
fn set_num(&mut self, value: i32) {
|
||||
self.0 = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tuple_struct_getter_setter() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, TupleClassGetterSetter(10)).unwrap();
|
||||
|
||||
py_assert!(py, inst, "inst.num == 10");
|
||||
py_run!(py, inst, "inst.num = 20");
|
||||
py_assert!(py, inst, "inst.num == 20");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(get_all, set_all)]
|
||||
struct All {
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_set_all() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, All { num: 10 }).unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.num == 10");
|
||||
py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(get_all)]
|
||||
struct All2 {
|
||||
#[pyo3(set)]
|
||||
num: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_all_and_set() {
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, All2 { num: 10 }).unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.num == 10");
|
||||
py_run!(py, inst, "inst.num = 20; assert inst.num == 20");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
struct CellGetterSetter {
|
||||
#[pyo3(get, set)]
|
||||
cell_inner: Cell<i32>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cell_getter_setter() {
|
||||
let c = CellGetterSetter {
|
||||
cell_inner: Cell::new(10),
|
||||
};
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, c).unwrap();
|
||||
let cell = Cell::new(20i32).into_pyobject(py).unwrap();
|
||||
|
||||
py_run!(py, cell, "assert cell == 20");
|
||||
py_run!(py, inst, "assert inst.cell_inner == 10");
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
"inst.cell_inner = 20; assert inst.cell_inner == 20"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borrowed_value_with_lifetime_of_self() {
|
||||
#[pyclass]
|
||||
struct BorrowedValue {}
|
||||
|
||||
#[pymethods]
|
||||
impl BorrowedValue {
|
||||
#[getter]
|
||||
fn value(&self) -> &str {
|
||||
"value"
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(py, BorrowedValue {}).unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.value == 'value'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frozen_py_field_get() {
|
||||
#[pyclass(frozen)]
|
||||
struct FrozenPyField {
|
||||
#[pyo3(get)]
|
||||
value: Py<PyString>,
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let inst = Py::new(
|
||||
py,
|
||||
FrozenPyField {
|
||||
value: "value".into_pyobject(py).unwrap().unbind(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_run!(py, inst, "assert inst.value == 'value'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_setter() {
|
||||
#[pyclass]
|
||||
struct SimpleClass {
|
||||
field: Option<u32>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SimpleClass {
|
||||
#[getter]
|
||||
fn get_field(&self) -> Option<u32> {
|
||||
self.field
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_field(&mut self, field: Option<u32>) {
|
||||
self.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(py, SimpleClass { field: None }).unwrap();
|
||||
py_run!(py, instance, "assert instance.field is None");
|
||||
py_run!(
|
||||
py,
|
||||
instance,
|
||||
"instance.field = 42; assert instance.field == 42"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
instance,
|
||||
"instance.field = None; assert instance.field is None"
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ref_only_getter() {
|
||||
#[derive(Clone)]
|
||||
struct RefOnly;
|
||||
|
||||
impl<'py> IntoPyObject<'py> for &RefOnly {
|
||||
type Target = PyString;
|
||||
type Output = Bound<'py, PyString>;
|
||||
type Error = Infallible;
|
||||
|
||||
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
||||
Ok(PyString::new(py, "value"))
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct Container {
|
||||
#[pyo3(get)]
|
||||
value: RefOnly,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit_getter() {
|
||||
#[derive(Clone)]
|
||||
#[pyclass(skip_from_py_object)]
|
||||
struct Container {
|
||||
#[pyo3(get)]
|
||||
value: (),
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let instance = Py::new(py, Container { value: () }).unwrap();
|
||||
py_run!(py, instance, "assert instance.value is ()");
|
||||
})
|
||||
}
|
||||
+541
@@ -0,0 +1,541 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
/// Macro to generate refcount leak tests for types.
|
||||
/// Ensures that creating and destroying instances doesn't leak references to the type.
|
||||
/// Regression test for issues #1363 and #6223.
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
macro_rules! assert_type_refcount_stable {
|
||||
// Simple case: type with parameterless constructor
|
||||
($type_name:ty) => {
|
||||
assert_type_refcount_stable!($type_name, stringify!($type_name), "Type()");
|
||||
};
|
||||
// With custom constructor
|
||||
($type_name:ty, $test_name:expr, $ctor:expr) => {{
|
||||
Python::attach(|py| {
|
||||
#[expect(non_snake_case)]
|
||||
let Type = py.get_type::<$type_name>();
|
||||
let ctor_code = $ctor;
|
||||
py_run!(
|
||||
py,
|
||||
Type,
|
||||
&format!(
|
||||
r#"
|
||||
import gc
|
||||
import sys
|
||||
|
||||
gc.collect()
|
||||
count = sys.getrefcount(Type)
|
||||
|
||||
for i in range(1000):
|
||||
obj = {}
|
||||
del obj
|
||||
|
||||
gc.collect()
|
||||
after = sys.getrefcount(Type)
|
||||
assert after == count, f"Type ref count leaked: {{after}} vs {{count}}"
|
||||
"#,
|
||||
ctor_code
|
||||
)
|
||||
);
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct BaseClass {
|
||||
#[pyo3(get)]
|
||||
val1: usize,
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct SubclassAble {}
|
||||
|
||||
#[test]
|
||||
fn subclass() {
|
||||
Python::attach(|py| {
|
||||
let d = [("SubclassAble", py.get_type::<SubclassAble>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py.run(
|
||||
c"class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)",
|
||||
None,
|
||||
Some(&d),
|
||||
)
|
||||
.map_err(|e| e.display(py))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BaseClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
BaseClass { val1: 10 }
|
||||
}
|
||||
fn base_method(&self, x: usize) -> usize {
|
||||
x * self.val1
|
||||
}
|
||||
fn base_set(&mut self, fn_: &Bound<'_, PyAny>) -> PyResult<()> {
|
||||
let value: usize = fn_.call0()?.extract()?;
|
||||
self.val1 = value;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=BaseClass)]
|
||||
struct SubClass {
|
||||
#[pyo3(get)]
|
||||
val2: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SubClass {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(BaseClass { val1: 10 }).add_subclass(SubClass { val2: 5 })
|
||||
}
|
||||
fn sub_method(&self, x: usize) -> usize {
|
||||
x * self.val2
|
||||
}
|
||||
fn sub_set_and_ret(&mut self, x: usize) -> usize {
|
||||
self.val2 = x;
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inheritance_with_new_methods() {
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<SubClass>();
|
||||
let inst = typeobj.call((), None).unwrap();
|
||||
py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_base_and_sub_methods() {
|
||||
Python::attach(|py| {
|
||||
let obj = Py::new(py, SubClass::new()).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
obj,
|
||||
r#"
|
||||
assert obj.base_method(10) == 100
|
||||
assert obj.sub_method(10) == 50
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_fails() {
|
||||
Python::attach(|py| {
|
||||
let obj = Py::new(py, SubClass::new()).unwrap();
|
||||
let global = [("obj", obj)].into_py_dict(py).unwrap();
|
||||
let e = py
|
||||
.run(
|
||||
c"obj.base_set(lambda: obj.sub_set_and_ret(1))",
|
||||
Some(&global),
|
||||
None,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(&e.to_string(), "RuntimeError: Already borrowed");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_subclass_and_is_instance() {
|
||||
Python::attach(|py| {
|
||||
let sub_ty = py.get_type::<SubClass>();
|
||||
let base_ty = py.get_type::<BaseClass>();
|
||||
assert!(sub_ty.is_subclass_of::<BaseClass>().unwrap());
|
||||
assert!(sub_ty.is_subclass(&base_ty).unwrap());
|
||||
|
||||
let obj = Bound::new(py, SubClass::new()).unwrap().into_any();
|
||||
assert!(obj.is_instance_of::<SubClass>());
|
||||
assert!(obj.is_instance_of::<BaseClass>());
|
||||
assert!(obj.is_instance(&sub_ty).unwrap());
|
||||
assert!(obj.is_instance(&base_ty).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct BaseClassWithResult {
|
||||
_val: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BaseClassWithResult {
|
||||
#[new]
|
||||
fn new(value: isize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
_val: std::convert::TryFrom::try_from(value)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=BaseClassWithResult)]
|
||||
struct SubClass2 {}
|
||||
|
||||
#[pymethods]
|
||||
impl SubClass2 {
|
||||
#[new]
|
||||
fn new(value: isize) -> PyResult<PyClassInitializer<Self>> {
|
||||
let base = BaseClassWithResult::new(value)?;
|
||||
Ok(PyClassInitializer::from(base).add_subclass(Self {}))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_result_in_new() {
|
||||
Python::attach(|py| {
|
||||
let subclass = py.get_type::<SubClass2>();
|
||||
py_run!(
|
||||
py,
|
||||
subclass,
|
||||
r#"
|
||||
try:
|
||||
subclass(-10)
|
||||
assert False
|
||||
except ValueError as e:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise e
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Subclassing builtin types is not possible in the LIMITED API before 3.12
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
mod inheriting_native_type {
|
||||
use super::*;
|
||||
use pyo3::exceptions::PyException;
|
||||
|
||||
#[cfg(not(GraalPy))]
|
||||
use {
|
||||
pyo3::types::{PyCapsule, PyDict},
|
||||
std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
use pyo3::types::PySet;
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
#[pyclass(extends=PySet)]
|
||||
#[derive(Debug)]
|
||||
pub struct SetWithName {
|
||||
#[pyo3(get, name = "name")]
|
||||
_name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
#[pymethods]
|
||||
impl SetWithName {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
SetWithName { _name: "Hello :)" }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(PyPy, GraalPy)))]
|
||||
#[test]
|
||||
fn inherit_set() {
|
||||
Python::attach(|py| {
|
||||
let set_sub = pyo3::Py::new(py, SetWithName::new()).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
set_sub,
|
||||
r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub.name == "Hello :)""#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(GraalPy))]
|
||||
#[pyclass(extends=PyDict)]
|
||||
#[derive(Debug)]
|
||||
struct DictWithName {
|
||||
#[pyo3(get, name = "name")]
|
||||
_name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(not(GraalPy))]
|
||||
#[pymethods]
|
||||
impl DictWithName {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
DictWithName { _name: "Hello :)" }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(GraalPy))]
|
||||
#[test]
|
||||
fn inherit_dict() {
|
||||
Python::attach(|py| {
|
||||
let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
dict_sub,
|
||||
r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub.name == "Hello :)""#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(GraalPy))]
|
||||
#[test]
|
||||
fn inherit_dict_drop() {
|
||||
Python::attach(|py| {
|
||||
let dropped = Arc::new(AtomicBool::new(false));
|
||||
let destructor_drop = Arc::clone(&dropped);
|
||||
let item = PyCapsule::new_with_value_and_destructor(
|
||||
py,
|
||||
0,
|
||||
c"inherit_dict_drop",
|
||||
move |_, _| destructor_drop.store(true, Ordering::Relaxed),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
|
||||
dict_sub.bind(py).set_item("foo", &item).unwrap();
|
||||
drop(item);
|
||||
assert!(!dropped.load(Ordering::Relaxed));
|
||||
drop(dict_sub);
|
||||
assert!(dropped.load(Ordering::Relaxed));
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass(extends=PyException)]
|
||||
struct CustomException {
|
||||
#[pyo3(get)]
|
||||
context: &'static str,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl CustomException {
|
||||
#[new]
|
||||
fn new(_exc_arg: &Bound<'_, PyAny>) -> Self {
|
||||
CustomException {
|
||||
context: "Hello :)",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_exception() {
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<CustomException>();
|
||||
let dict = [("cls", &cls)].into_py_dict(py).unwrap();
|
||||
let res = py.run(
|
||||
c"e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e",
|
||||
None,
|
||||
Some(&dict)
|
||||
);
|
||||
let err = res.unwrap_err();
|
||||
assert!(err.matches(py, &cls).unwrap(), "{}", err);
|
||||
|
||||
// catching the exception in Python also works:
|
||||
py_run!(
|
||||
py,
|
||||
cls,
|
||||
r#"
|
||||
try:
|
||||
raise cls("foo")
|
||||
except cls:
|
||||
pass
|
||||
"#
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pyclass(extends=pyo3::types::PyTzInfo)]
|
||||
struct TzInfoWithName {
|
||||
#[pyo3(get)]
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pymethods]
|
||||
impl TzInfoWithName {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self { name: "Hello :)" }
|
||||
}
|
||||
|
||||
#[pyo3(signature = (_dt, /))]
|
||||
fn utcoffset<'py>(
|
||||
&self,
|
||||
_dt: Option<&Bound<'_, pyo3::types::PyDateTime>>,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<Bound<'py, pyo3::types::PyDelta>> {
|
||||
pyo3::types::PyDelta::new(py, 0, 3600, 0, true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[test]
|
||||
fn inherit_tzinfo() {
|
||||
Python::attach(|py| {
|
||||
let tz = pyo3::Py::new(py, TzInfoWithName::new()).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
tz,
|
||||
r#"
|
||||
import datetime
|
||||
|
||||
assert isinstance(tz, datetime.tzinfo)
|
||||
assert tz.name == "Hello :)"
|
||||
|
||||
dt = datetime.datetime(2024, 1, 1, tzinfo=tz)
|
||||
assert dt.utcoffset() == datetime.timedelta(hours=1)
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pyclass(extends=pyo3::types::PyList, subclass)]
|
||||
struct ListWithName {
|
||||
#[pyo3(get)]
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pymethods]
|
||||
impl ListWithName {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self { name: "Hello :)" }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pyclass(extends=ListWithName)]
|
||||
struct SubListWithName {
|
||||
#[pyo3(get)]
|
||||
sub_name: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[pymethods]
|
||||
impl SubListWithName {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(ListWithName::new()).add_subclass(Self {
|
||||
sub_name: "Sublist",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_12)]
|
||||
#[test]
|
||||
fn inherit_list() {
|
||||
Python::attach(|py| {
|
||||
let list_with_name = pyo3::Bound::new(py, ListWithName::new()).unwrap();
|
||||
let sub_list_with_name = pyo3::Bound::new(py, SubListWithName::new()).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
list_with_name sub_list_with_name,
|
||||
r#"
|
||||
list_with_name.append(1)
|
||||
assert list_with_name[0] == 1
|
||||
assert list_with_name.name == "Hello :)", list_with_name.name
|
||||
|
||||
sub_list_with_name.append(1)
|
||||
assert sub_list_with_name[0] == 1
|
||||
assert sub_list_with_name.name == "Hello :)", sub_list_with_name.name
|
||||
assert sub_list_with_name.sub_name == "Sublist", sub_list_with_name.sub_name
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Refcount tests for native type classes
|
||||
#[cfg(not(any(PyPy, GraalPy, Py_GIL_DISABLED)))]
|
||||
#[test]
|
||||
fn test_setwitname_ref_counts() {
|
||||
assert_type_refcount_stable!(SetWithName);
|
||||
}
|
||||
|
||||
#[cfg(not(any(GraalPy, Py_GIL_DISABLED)))]
|
||||
#[test]
|
||||
fn test_dictwithname_ref_counts() {
|
||||
assert_type_refcount_stable!(DictWithName);
|
||||
}
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_customexception_ref_counts() {
|
||||
assert_type_refcount_stable!(CustomException, "custom_exception", r#"Type('test')"#);
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
|
||||
#[test]
|
||||
fn test_tzinfowithname_ref_counts() {
|
||||
assert_type_refcount_stable!(TzInfoWithName);
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
|
||||
#[test]
|
||||
fn test_listwithname_ref_counts() {
|
||||
assert_type_refcount_stable!(ListWithName);
|
||||
}
|
||||
|
||||
#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))]
|
||||
#[test]
|
||||
fn test_sublistwithname_ref_counts() {
|
||||
assert_type_refcount_stable!(SubListWithName);
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct SimpleClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl SimpleClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate refcount tests for all top-level types
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_baseclass_ref_counts() {
|
||||
assert_type_refcount_stable!(BaseClass);
|
||||
}
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_subclass_ref_counts() {
|
||||
assert_type_refcount_stable!(SubClass);
|
||||
}
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_base_class_with_result_ref_counts() {
|
||||
assert_type_refcount_stable!(BaseClassWithResult, "base_class_with_result", "Type(10)");
|
||||
}
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_subclass2_ref_counts() {
|
||||
assert_type_refcount_stable!(SubClass2, "subclass2", "Type(10)");
|
||||
}
|
||||
|
||||
#[cfg(not(Py_GIL_DISABLED))]
|
||||
#[test]
|
||||
fn test_simpleclass_ref_counts() {
|
||||
assert_type_refcount_stable!(SimpleClass);
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
#![cfg(not(any(PyPy, GraalPy)))]
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use pyo3::types::PyAnyMethods;
|
||||
|
||||
// Regression test for `try_attach` fast-path returning before site.py had
|
||||
// finished initializing.
|
||||
#[test]
|
||||
fn test_concurrent_init_site_race() {
|
||||
let tmpdir = std::env::temp_dir().join("pyo3_test_init_race");
|
||||
std::fs::create_dir_all(&tmpdir).unwrap();
|
||||
|
||||
std::fs::write(
|
||||
tmpdir.join("sitecustomize.py"),
|
||||
"import sys, time\n\
|
||||
sys._pyo3_site_done = False\n\
|
||||
time.sleep(2)\n\
|
||||
sys._pyo3_site_done = True\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
std::env::set_var("PYTHONPATH", &tmpdir);
|
||||
|
||||
std::thread::scope(|s| {
|
||||
s.spawn(|| {
|
||||
pyo3::Python::initialize();
|
||||
});
|
||||
|
||||
s.spawn(|| loop {
|
||||
let result = pyo3::Python::try_attach(|py| {
|
||||
let done = py
|
||||
.import("sys")
|
||||
.unwrap()
|
||||
.getattr("_pyo3_site_done")
|
||||
.unwrap()
|
||||
.extract::<bool>()
|
||||
.unwrap();
|
||||
assert!(done);
|
||||
});
|
||||
if result.is_some() {
|
||||
break;
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
});
|
||||
});
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::types::{PyDict, PyList, PyString};
|
||||
use pyo3::{prelude::*, py_run, IntoPyObject, IntoPyObjectExt};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
pub struct A<'py> {
|
||||
s: String,
|
||||
t: Bound<'py, PyString>,
|
||||
p: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_fields_struct() {
|
||||
Python::attach(|py| {
|
||||
let a = A {
|
||||
s: "Hello".into(),
|
||||
t: PyString::new(py, "World"),
|
||||
p: 42i32.into_pyobject(py).unwrap().into_any(),
|
||||
};
|
||||
let pya = a.into_pyobject(py).unwrap();
|
||||
assert_eq!(
|
||||
pya.get_item("s")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.cast::<PyString>()
|
||||
.unwrap(),
|
||||
"Hello"
|
||||
);
|
||||
assert_eq!(
|
||||
pya.get_item("t")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.cast::<PyString>()
|
||||
.unwrap(),
|
||||
"World"
|
||||
);
|
||||
assert_eq!(
|
||||
pya.get_item("p")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
42
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct B<'a> {
|
||||
test: &'a str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let pyb = B { test: "test" }.into_pyobject(py).unwrap();
|
||||
let b = pyb.extract::<String>().unwrap();
|
||||
assert_eq!(b, "test");
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
#[pyo3(transparent)]
|
||||
pub struct D<T> {
|
||||
test: T,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generic_transparent_named_field_struct() {
|
||||
Python::attach(|py| {
|
||||
let pyd = D {
|
||||
test: String::from("test"),
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
let d = pyd.extract::<String>().unwrap();
|
||||
assert_eq!(d, "test");
|
||||
|
||||
let pyd = D { test: 1usize }.into_pyobject(py).unwrap();
|
||||
let d = pyd.extract::<usize>().unwrap();
|
||||
assert_eq!(d, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
pub struct GenericWithBound<K: Hash + Eq, V>(HashMap<K, V>);
|
||||
|
||||
#[test]
|
||||
fn test_generic_with_bound() {
|
||||
Python::attach(|py| {
|
||||
let mut hash_map = HashMap::<String, i32>::new();
|
||||
hash_map.insert("1".into(), 1);
|
||||
hash_map.insert("2".into(), 2);
|
||||
let map = GenericWithBound(hash_map).into_pyobject(py).unwrap();
|
||||
assert_eq!(map.len(), 2);
|
||||
assert_eq!(
|
||||
map.get_item("1")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
map.get_item("2")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.extract::<i32>()
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
assert!(map.get_item("3").unwrap().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
pub struct Tuple(String, usize);
|
||||
|
||||
#[test]
|
||||
fn test_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = Tuple(String::from("test"), 1).into_pyobject(py).unwrap();
|
||||
assert!(tup.extract::<(usize, String)>().is_err());
|
||||
let tup = tup.extract::<(String, usize)>().unwrap();
|
||||
assert_eq!(tup.0, "test");
|
||||
assert_eq!(tup.1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject)]
|
||||
pub struct TransparentTuple(String);
|
||||
|
||||
#[test]
|
||||
fn test_transparent_tuple_struct() {
|
||||
Python::attach(|py| {
|
||||
let tup = TransparentTuple(String::from("test"))
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
assert!(tup.extract::<(String,)>().is_err());
|
||||
let tup = tup.extract::<String>().unwrap();
|
||||
assert_eq!(tup, "test");
|
||||
});
|
||||
}
|
||||
|
||||
fn phantom_into_py<'py, T>(
|
||||
_: std::borrow::Cow<'_, std::marker::PhantomData<T>>,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
std::any::type_name::<T>().into_bound_py_any(py)
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject, IntoPyObjectRef)]
|
||||
pub enum Foo<'py> {
|
||||
TupleVar(
|
||||
usize,
|
||||
String,
|
||||
#[pyo3(into_py_with = phantom_into_py::<()>)] std::marker::PhantomData<()>,
|
||||
),
|
||||
StructVar {
|
||||
test: Bound<'py, PyString>,
|
||||
},
|
||||
#[pyo3(transparent)]
|
||||
TransparentTuple(usize),
|
||||
#[pyo3(transparent)]
|
||||
TransparentStructVar {
|
||||
a: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum() {
|
||||
Python::attach(|py| {
|
||||
let foo = Foo::TupleVar(1, "test".into(), std::marker::PhantomData)
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
foo.extract::<(usize, String, String)>().unwrap(),
|
||||
(1, String::from("test"), String::from("()"))
|
||||
);
|
||||
|
||||
let foo = Foo::StructVar {
|
||||
test: PyString::new(py, "test"),
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap()
|
||||
.cast_into::<PyDict>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
foo.get_item("test")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.cast_into::<PyString>()
|
||||
.unwrap(),
|
||||
"test"
|
||||
);
|
||||
|
||||
let foo = Foo::TransparentTuple(1).into_pyobject(py).unwrap();
|
||||
assert_eq!(foo.extract::<usize>().unwrap(), 1);
|
||||
|
||||
let foo = Foo::TransparentStructVar { a: None }
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
assert!(foo.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, IntoPyObject, IntoPyObjectRef)]
|
||||
pub struct Zap {
|
||||
#[pyo3(item)]
|
||||
name: String,
|
||||
|
||||
#[pyo3(into_py_with = zap_into_py, item("my_object"))]
|
||||
some_object_length: usize,
|
||||
}
|
||||
|
||||
fn zap_into_py<'py>(
|
||||
len: std::borrow::Cow<'_, usize>,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
Ok(PyList::new(py, 1..*len + 1)?.into_any())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_py_with() {
|
||||
Python::attach(|py| {
|
||||
let zap = Zap {
|
||||
name: "whatever".into(),
|
||||
some_object_length: 3,
|
||||
};
|
||||
|
||||
let py_zap_ref = (&zap).into_pyobject(py).unwrap();
|
||||
let py_zap = zap.into_pyobject(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
py_zap_ref,
|
||||
"assert py_zap_ref == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap_ref}'"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
py_zap,
|
||||
"assert py_zap == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap}'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_struct_into_py_rename_all() {
|
||||
#[derive(IntoPyObject, IntoPyObjectRef)]
|
||||
#[pyo3(rename_all = "camelCase")]
|
||||
struct Foo {
|
||||
foo_bar: String,
|
||||
#[pyo3(item("BAZ"))]
|
||||
baz: usize,
|
||||
#[pyo3(item)]
|
||||
long_field_name: f32,
|
||||
}
|
||||
|
||||
let foo = Foo {
|
||||
foo_bar: "foobar".into(),
|
||||
baz: 42,
|
||||
long_field_name: 0.0,
|
||||
};
|
||||
|
||||
Python::attach(|py| {
|
||||
let py_foo_ref = (&foo).into_pyobject(py).unwrap();
|
||||
let py_foo = foo.into_pyobject(py).unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
py_foo_ref,
|
||||
"assert py_foo_ref == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo_ref}'"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
py_foo,
|
||||
"assert py_foo == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo}'"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
/// The MacroDocs class.
|
||||
#[doc = concat!("Some macro ", "class ", "docs.")]
|
||||
/// A very interesting type!
|
||||
struct MacroDocs {}
|
||||
|
||||
#[pymethods]
|
||||
impl MacroDocs {
|
||||
#[doc = concat!("A macro ", "example.")]
|
||||
/// With mixed doc types.
|
||||
fn macro_doc(&self) {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meth_doc() {
|
||||
Python::attach(|py| {
|
||||
let d = [("C", py.get_type::<MacroDocs>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"C.__doc__ == 'The MacroDocs class.\\nSome macro class docs.\\nA very interesting type!'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"C.macro_doc.__doc__ == 'A macro example.\\nWith mixed doc types.'"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
//! Ensure that pyo3 macros can be used inside macro_rules!
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
macro_rules! make_struct_using_macro {
|
||||
// Ensure that one doesn't need to fall back on the escape type: tt
|
||||
// in order to macro create pyclass.
|
||||
($class_name:ident, $py_name:literal) => {
|
||||
#[pyclass(name=$py_name, subclass)]
|
||||
struct $class_name {}
|
||||
};
|
||||
}
|
||||
|
||||
make_struct_using_macro!(MyBaseClass, "MyClass");
|
||||
|
||||
macro_rules! set_extends_via_macro {
|
||||
($class_name:ident, $base_class:path) => {
|
||||
// Try and pass a variable into the extends parameter
|
||||
#[allow(dead_code)]
|
||||
#[pyclass(extends=$base_class)]
|
||||
struct $class_name {}
|
||||
};
|
||||
}
|
||||
|
||||
set_extends_via_macro!(MyClass2, MyBaseClass);
|
||||
|
||||
//
|
||||
// Check that pyfunctions and text_signature can be called with macro arguments.
|
||||
//
|
||||
|
||||
macro_rules! fn_macro {
|
||||
($sig:literal, $a_exp:expr, $b_exp:expr, $c_exp: expr) => {
|
||||
// Try and pass a variable into the signature parameter
|
||||
#[pyfunction(signature = ($a_exp, $b_exp, *, $c_exp))]
|
||||
#[pyo3(text_signature = $sig)]
|
||||
fn my_function_in_macro(a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn_macro!("(a, b=None, *, c=42)", a, b = None, c = 42);
|
||||
|
||||
macro_rules! property_rename_via_macro {
|
||||
($prop_name:ident) => {
|
||||
#[pyclass]
|
||||
struct ClassWithProperty {
|
||||
member: u64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ClassWithProperty {
|
||||
#[getter($prop_name)]
|
||||
fn get_member(&self) -> u64 {
|
||||
self.member
|
||||
}
|
||||
|
||||
#[setter($prop_name)]
|
||||
fn set_member(&mut self, member: u64) {
|
||||
self.member = member;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
property_rename_via_macro!(my_new_property_name);
|
||||
|
||||
#[test]
|
||||
fn test_macro_rules_interactions() {
|
||||
Python::attach(|py| {
|
||||
let my_base = py.get_type::<MyBaseClass>();
|
||||
py_assert!(py, my_base, "my_base.__name__ == 'MyClass'");
|
||||
|
||||
let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap();
|
||||
py_assert!(
|
||||
py,
|
||||
my_func,
|
||||
"my_func.__text_signature__ == '(a, b=None, *, c=42)'"
|
||||
);
|
||||
|
||||
let renamed_prop = py.get_type::<ClassWithProperty>();
|
||||
py_assert!(
|
||||
py,
|
||||
renamed_prop,
|
||||
"hasattr(renamed_prop, 'my_new_property_name')"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pyo3::exceptions::PyKeyError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::IntoPyDict;
|
||||
use pyo3::types::PyList;
|
||||
use pyo3::types::PyMapping;
|
||||
use pyo3::types::PySequence;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass(mapping)]
|
||||
struct Mapping {
|
||||
index: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Mapping {
|
||||
#[new]
|
||||
#[pyo3(signature=(elements=None))]
|
||||
fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult<Self> {
|
||||
if let Some(pylist) = elements {
|
||||
let mut elems = HashMap::with_capacity(pylist.len());
|
||||
for (i, pyelem) in pylist.into_iter().enumerate() {
|
||||
let elem = pyelem.extract()?;
|
||||
elems.insert(elem, i);
|
||||
}
|
||||
Ok(Self { index: elems })
|
||||
} else {
|
||||
Ok(Self {
|
||||
index: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn __len__(&self) -> usize {
|
||||
self.index.len()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, query: String) -> PyResult<usize> {
|
||||
self.index
|
||||
.get(&query)
|
||||
.copied()
|
||||
.ok_or_else(|| PyKeyError::new_err("unknown key"))
|
||||
}
|
||||
|
||||
fn __setitem__(&mut self, key: String, value: usize) {
|
||||
self.index.insert(key, value);
|
||||
}
|
||||
|
||||
fn __delitem__(&mut self, key: String) -> PyResult<()> {
|
||||
if self.index.remove(&key).is_none() {
|
||||
Err(PyKeyError::new_err("unknown key"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyo3(signature=(key, default=None))]
|
||||
fn get(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
key: &str,
|
||||
default: Option<Py<PyAny>>,
|
||||
) -> PyResult<Option<Py<PyAny>>> {
|
||||
match self.index.get(key) {
|
||||
Some(value) => Ok(Some(value.into_pyobject(py)?.into_any().unbind())),
|
||||
None => Ok(default),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a dict with `m = Mapping(['1', '2', '3'])`.
|
||||
fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
|
||||
let d = [("Mapping", py.get_type::<Mapping>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
py_run!(py, *d, "m = Mapping(['1', '2', '3'])");
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_getitem() {
|
||||
Python::attach(|py| {
|
||||
let d = map_dict(py);
|
||||
|
||||
py_assert!(py, *d, "m['1'] == 0");
|
||||
py_assert!(py, *d, "m['2'] == 1");
|
||||
py_assert!(py, *d, "m['3'] == 2");
|
||||
py_expect_exception!(py, *d, "print(m['4'])", PyKeyError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setitem() {
|
||||
Python::attach(|py| {
|
||||
let d = map_dict(py);
|
||||
|
||||
py_run!(py, *d, "m['1'] = 4; assert m['1'] == 4");
|
||||
py_run!(py, *d, "m['0'] = 0; assert m['0'] == 0");
|
||||
py_assert!(py, *d, "len(m) == 4");
|
||||
py_expect_exception!(py, *d, "m[0] = 'hello'", PyTypeError);
|
||||
py_expect_exception!(py, *d, "m[0] = -1", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delitem() {
|
||||
Python::attach(|py| {
|
||||
let d = map_dict(py);
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"del m['1']; assert len(m) == 2 and m['2'] == 1 and m['3'] == 2"
|
||||
);
|
||||
py_expect_exception!(py, *d, "del m[-1]", PyTypeError);
|
||||
py_expect_exception!(py, *d, "del m['4']", PyKeyError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_is_not_sequence() {
|
||||
Python::attach(|py| {
|
||||
let mut index = HashMap::new();
|
||||
index.insert("Foo".into(), 1);
|
||||
index.insert("Bar".into(), 2);
|
||||
let m = Py::new(py, Mapping { index }).unwrap();
|
||||
|
||||
PyMapping::register::<Mapping>(py).unwrap();
|
||||
|
||||
assert!(m.bind(py).cast::<PyMapping>().is_ok());
|
||||
assert!(m.bind(py).cast::<PySequence>().is_err());
|
||||
});
|
||||
}
|
||||
Vendored
+1569
File diff suppressed because it is too large.
Load diff
Vendored
+565
@@ -0,0 +1,565 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::PyString;
|
||||
use pyo3::types::{IntoPyDict, PyDict, PyTuple};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct AnonClass {}
|
||||
|
||||
#[pyclass]
|
||||
struct ValueClass {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ValueClass {
|
||||
#[new]
|
||||
fn new(value: usize) -> ValueClass {
|
||||
ValueClass { value }
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(module = "module")]
|
||||
struct LocatedClass {}
|
||||
|
||||
#[pyfunction]
|
||||
/// Doubles the given value
|
||||
fn double(x: usize) -> usize {
|
||||
x * 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_with_functions() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
/// This module is implemented in Rust.
|
||||
#[pymodule]
|
||||
mod module_with_functions {
|
||||
use super::*;
|
||||
|
||||
#[pymodule_export]
|
||||
use super::{AnonClass, ValueClass};
|
||||
|
||||
#[pymodule_init]
|
||||
fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<LocatedClass>()?;
|
||||
m.add("foo", "bar")?;
|
||||
m.add_function(wrap_pyfunction!(double, m)?)?;
|
||||
m.add("also_double", wrap_pyfunction!(double, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(name = "no_parameters")]
|
||||
fn function_with_name() -> usize {
|
||||
42
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(pass_module)]
|
||||
fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
|
||||
module.name()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn double_value(v: &ValueClass) -> usize {
|
||||
v.value * 2
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let d = [(
|
||||
"module_with_functions",
|
||||
wrap_pymodule!(module_with_functions)(py),
|
||||
)]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.__doc__ == 'This module is implemented in Rust.'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_functions.no_parameters() == 42");
|
||||
py_assert!(py, *d, "module_with_functions.foo == 'bar'");
|
||||
py_assert!(py, *d, "module_with_functions.AnonClass != None");
|
||||
py_assert!(py, *d, "module_with_functions.LocatedClass != None");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.LocatedClass.__module__ == 'module'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_functions.double(3) == 6");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.double.__doc__ == 'Doubles the given value'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_functions.also_double(3) == 6");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.also_double.__doc__ == 'Doubles the given value'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.double_value(module_with_functions.ValueClass(1)) == 2"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_functions.with_module() == 'module_with_functions'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_module_with_pyfn() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
/// This module is implemented in Rust.
|
||||
#[pymodule]
|
||||
fn module_with_pyfn(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
#[pyfn(m)]
|
||||
#[pyo3(name = "no_parameters")]
|
||||
fn function_with_name() -> usize {
|
||||
42
|
||||
}
|
||||
|
||||
#[pyfn(m)]
|
||||
#[pyo3(pass_module)]
|
||||
fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
|
||||
module.name()
|
||||
}
|
||||
|
||||
#[pyfn(m)]
|
||||
fn double_value(v: &ValueClass) -> usize {
|
||||
v.value * 2
|
||||
}
|
||||
|
||||
m.add_class::<AnonClass>()?;
|
||||
m.add_class::<ValueClass>()?;
|
||||
m.add_class::<LocatedClass>()?;
|
||||
|
||||
m.add("foo", "bar")?;
|
||||
|
||||
m.add_function(wrap_pyfunction!(double, m)?)?;
|
||||
m.add("also_double", wrap_pyfunction!(double, m)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let d = [("module_with_pyfn", wrap_pymodule!(module_with_pyfn)(py))]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.__doc__ == 'This module is implemented in Rust.'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_pyfn.no_parameters() == 42");
|
||||
py_assert!(py, *d, "module_with_pyfn.foo == 'bar'");
|
||||
py_assert!(py, *d, "module_with_pyfn.AnonClass != None");
|
||||
py_assert!(py, *d, "module_with_pyfn.LocatedClass != None");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.LocatedClass.__module__ == 'module'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_pyfn.double(3) == 6");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.double.__doc__ == 'Doubles the given value'"
|
||||
);
|
||||
py_assert!(py, *d, "module_with_pyfn.also_double(3) == 6");
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.also_double.__doc__ == 'Doubles the given value'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.double_value(module_with_pyfn.ValueClass(1)) == 2"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
*d,
|
||||
"module_with_pyfn.with_module() == 'module_with_pyfn'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// This module uses a legacy two-argument module function.
|
||||
#[pymodule]
|
||||
fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(double, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_with_explicit_py_arg() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
Python::attach(|py| {
|
||||
let d = [(
|
||||
"module_with_explicit_py_arg",
|
||||
wrap_pymodule!(module_with_explicit_py_arg)(py),
|
||||
)]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, *d, "module_with_explicit_py_arg.double(3) == 6");
|
||||
});
|
||||
}
|
||||
|
||||
#[pymodule(name = "other_name")]
|
||||
fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add("other_name", "other_name")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_renaming() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
Python::attach(|py| {
|
||||
let d = [("different_name", wrap_pymodule!(some_name)(py))]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_run!(py, *d, "assert different_name.__name__ == 'other_name'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_from_code_bound() {
|
||||
Python::attach(|py| {
|
||||
let adder_mod = PyModule::from_code(
|
||||
py,
|
||||
c"def add(a,b):\n\treturn a+b",
|
||||
c"adder_mod.py",
|
||||
&test_utils::generate_unique_module_name("adder_mod"),
|
||||
)
|
||||
.expect("Module code should be loaded");
|
||||
|
||||
let add_func = adder_mod
|
||||
.getattr("add")
|
||||
.expect("Add function should be in the module");
|
||||
|
||||
let ret_value: i32 = add_func
|
||||
.call1((1, 2))
|
||||
.expect("A value should be returned")
|
||||
.extract()
|
||||
.expect("The value should be able to be converted to an i32");
|
||||
|
||||
assert_eq!(ret_value, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn r#move() -> usize {
|
||||
42
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn raw_ident_module(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(r#move, module)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_idents() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = wrap_pymodule!(raw_ident_module)(py);
|
||||
|
||||
py_assert!(py, module, "module.move() == 42");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(name = "foobar")]
|
||||
fn custom_named_fn() -> usize {
|
||||
42
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_names() {
|
||||
#[pymodule]
|
||||
fn custom_names(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = pyo3::wrap_pymodule!(custom_names)(py);
|
||||
|
||||
py_assert!(py, module, "not hasattr(module, 'custom_named_fn')");
|
||||
py_assert!(py, module, "module.foobar() == 42");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_dict() {
|
||||
#[pymodule]
|
||||
fn module_dict(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.dict().set_item("yay", "me")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = pyo3::wrap_pymodule!(module_dict)(py);
|
||||
|
||||
py_assert!(py, module, "module.yay == 'me'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_dunder_all() {
|
||||
Python::attach(|py| {
|
||||
#[pymodule]
|
||||
fn dunder_all(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.dict().set_item("yay", "me")?;
|
||||
m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let module = pyo3::wrap_pymodule!(dunder_all)(py);
|
||||
|
||||
py_assert!(py, module, "module.__all__ == ['foobar']");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn subfunction() -> String {
|
||||
"Subfunction".to_string()
|
||||
}
|
||||
|
||||
fn submodule(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(subfunction, module)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn submodule_with_init_fn(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(subfunction, module)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn superfunction() -> String {
|
||||
"Superfunction".to_string()
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
module.add_function(wrap_pyfunction!(superfunction, module)?)?;
|
||||
let module_to_add = PyModule::new(module.py(), "supermodule.submodule")?;
|
||||
submodule(&module_to_add)?;
|
||||
module.add_submodule(&module_to_add)?;
|
||||
let module_to_add = PyModule::new(module.py(), "supermodule.submodule_with_init_fn")?;
|
||||
submodule_with_init_fn(&module_to_add)?;
|
||||
module.add_submodule(&module_to_add)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_nesting() {
|
||||
use pyo3::wrap_pymodule;
|
||||
|
||||
Python::attach(|py| {
|
||||
let supermodule = wrap_pymodule!(supermodule)(py);
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"supermodule.superfunction() == 'Superfunction'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"supermodule.submodule.subfunction() == 'Subfunction'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'"
|
||||
);
|
||||
|
||||
// submodule dunder name and attribute name
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"supermodule.submodule.__name__ == 'supermodule.submodule'"
|
||||
);
|
||||
py_assert!(py, supermodule, "'submodule' in supermodule.__dict__");
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"'supermodule.submodule' not in supermodule.__dict__"
|
||||
);
|
||||
|
||||
// submodule_with_init_fn dunder name and attribute name
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"supermodule.submodule_with_init_fn.__name__ == 'supermodule.submodule_with_init_fn'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"'submodule_with_init_fn' in supermodule.__dict__"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
supermodule,
|
||||
"'supermodule.submodule_with_init_fn' not in supermodule.__dict__"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_with_constant() {
|
||||
// Regression test for #1102
|
||||
|
||||
#[pymodule]
|
||||
fn module_with_constant(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
const ANON: AnonClass = AnonClass {};
|
||||
|
||||
m.add("ANON", ANON)?;
|
||||
m.add_class::<AnonClass>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let m = pyo3::wrap_pymodule!(module_with_constant)(py);
|
||||
py_assert!(py, m, "isinstance(m.ANON, m.AnonClass)");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult<Bound<'py, PyString>> {
|
||||
module.name()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module_owned(
|
||||
module: Py<PyModule>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Bound<'_, PyString>> {
|
||||
module.bind(py).name()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module_and_py<'py>(
|
||||
module: &Bound<'py, PyModule>,
|
||||
_python: Python<'py>,
|
||||
) -> PyResult<Bound<'py, PyString>> {
|
||||
module.name()
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module_and_arg<'py>(
|
||||
module: &Bound<'py, PyModule>,
|
||||
string: String,
|
||||
) -> PyResult<(Bound<'py, PyString>, String)> {
|
||||
module.name().map(|s| (s, string))
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (string="foo"))]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module_and_default_arg<'py>(
|
||||
module: &Bound<'py, PyModule>,
|
||||
string: &str,
|
||||
) -> PyResult<(Bound<'py, PyString>, String)> {
|
||||
module.name().map(|s| (s, string.into()))
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (*args, **kwargs))]
|
||||
#[pyo3(pass_module)]
|
||||
fn pyfunction_with_module_and_args_kwargs<'py>(
|
||||
module: &Bound<'py, PyModule>,
|
||||
args: &Bound<'py, PyTuple>,
|
||||
kwargs: Option<&Bound<'py, PyDict>>,
|
||||
) -> PyResult<(Bound<'py, PyString>, usize, Option<usize>)> {
|
||||
module
|
||||
.name()
|
||||
.map(|s| (s, args.len(), kwargs.map(|d| d.len())))
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn module_with_functions_with_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module_and_py, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module_and_arg, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module_and_default_arg, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module_and_args_kwargs, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_functions_with_module() {
|
||||
Python::attach(|py| {
|
||||
let m = pyo3::wrap_pymodule!(module_with_functions_with_module)(py);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.pyfunction_with_module() == 'module_with_functions_with_module'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.pyfunction_with_module_owned() == 'module_with_functions_with_module'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.pyfunction_with_module_and_py() == 'module_with_functions_with_module'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.pyfunction_with_module_and_default_arg() \
|
||||
== ('module_with_functions_with_module', 'foo')"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.pyfunction_with_module_and_args_kwargs(1, x=1, y=2) \
|
||||
== ('module_with_functions_with_module', 1, 2)"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_doc_hidden() {
|
||||
#[doc(hidden)]
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
#[pymodule]
|
||||
fn my_module(_m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let m = pyo3::wrap_pymodule!(my_module)(py);
|
||||
py_assert!(py, m, "m.__doc__ == ''");
|
||||
})
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#![cfg(feature = "multiple-pymethods")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyType;
|
||||
|
||||
#[macro_use]
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct PyClassWithMultiplePyMethods {}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
fn __call__(&self) -> &'static str {
|
||||
"call"
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
fn method(&self) -> &'static str {
|
||||
"method"
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
#[classmethod]
|
||||
fn classmethod(_ty: &Bound<'_, PyType>) -> &'static str {
|
||||
"classmethod"
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
#[staticmethod]
|
||||
fn staticmethod() -> &'static str {
|
||||
"staticmethod"
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
#[classattr]
|
||||
fn class_attribute() -> &'static str {
|
||||
"class_attribute"
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyClassWithMultiplePyMethods {
|
||||
#[classattr]
|
||||
const CLASS_ATTRIBUTE: &'static str = "CLASS_ATTRIBUTE";
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_class_with_multiple_pymethods() {
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<PyClassWithMultiplePyMethods>();
|
||||
py_assert!(py, cls, "cls()() == 'call'");
|
||||
py_assert!(py, cls, "cls().method() == 'method'");
|
||||
py_assert!(py, cls, "cls.classmethod() == 'classmethod'");
|
||||
py_assert!(py, cls, "cls.staticmethod() == 'staticmethod'");
|
||||
py_assert!(py, cls, "cls.class_attribute == 'class_attribute'");
|
||||
py_assert!(py, cls, "cls.CLASS_ATTRIBUTE == 'CLASS_ATTRIBUTE'");
|
||||
})
|
||||
}
|
||||
+1010
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,38 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
// buffer availability
|
||||
#![cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
// cannot control interpreter lifecycle in PyPy or GraalPy
|
||||
#![cfg(not(any(PyPy, GraalPy)))]
|
||||
|
||||
//! Dropping `Py<T>` after the interpreter has been finalized should be sound.
|
||||
//!
|
||||
//! See e.g. https://github.com/PyO3/pyo3/issues/4632 for an extension of this problem
|
||||
//! where the interpreter was finalized before `PyBuffer<T>` was dropped.
|
||||
//!
|
||||
//! This test runs in its own process to control the interpreter lifecycle.
|
||||
|
||||
use pyo3::{buffer::PyBuffer, types::PyBytes};
|
||||
|
||||
#[test]
|
||||
fn test_pybuffer_drop_without_interpreter() {
|
||||
// SAFETY: this is knowingly unsafe as we're preserving the `Py<T>` object
|
||||
// after the Python interpreter has been finalized.
|
||||
//
|
||||
// However we should still be able to drop it without causing undefined behavior,
|
||||
// so that process shutdown is sound.
|
||||
let obj: PyBuffer<u8> = unsafe {
|
||||
pyo3::with_embedded_python_interpreter(|py| {
|
||||
PyBuffer::get(&PyBytes::new(py, b"abcdef")).unwrap()
|
||||
})
|
||||
};
|
||||
|
||||
// there should be no interpreter outside of the `with_embedded_python_interpreter` block
|
||||
assert_eq!(unsafe { pyo3_ffi::Py_IsInitialized() }, 0);
|
||||
|
||||
// dropping object should be sound
|
||||
drop(obj);
|
||||
|
||||
// dropping object should not re-initialize the interpreter
|
||||
assert_eq!(unsafe { pyo3_ffi::Py_IsInitialized() }, 0);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use pyo3::prelude::*;
|
||||
|
||||
// This test mucks around with sys.modules, so run it separately to prevent it
|
||||
// from potentially corrupting the state of the python interpreter used in other
|
||||
// tests.
|
||||
|
||||
#[test]
|
||||
fn err_debug_unformattable() {
|
||||
// Debug representation should be like the following (without the newlines):
|
||||
// PyErr {
|
||||
// type: <class 'Exception'>,
|
||||
// value: Exception('banana'),
|
||||
// traceback: Some(\"<unformattable <traceback object at 0x...>>\")
|
||||
// }
|
||||
|
||||
Python::attach(|py| {
|
||||
// PyTracebackMethods::format uses io.StringIO. Mock it out to trigger a
|
||||
// formatting failure:
|
||||
// TypeError: 'Mock' object is not an instance of 'str'
|
||||
let err = py
|
||||
.run(
|
||||
cr#"
|
||||
import io, sys, unittest.mock
|
||||
sys.modules['orig_io'] = sys.modules['io']
|
||||
sys.modules['io'] = unittest.mock.Mock()
|
||||
raise Exception('banana')"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect_err("raising should have given us an error");
|
||||
|
||||
let debug_str = format!("{err:?}");
|
||||
assert!(debug_str.starts_with("PyErr { "));
|
||||
assert!(debug_str.ends_with(" }"));
|
||||
|
||||
// Strip "PyErr { " and " }". Split into 3 substrings to separate type,
|
||||
// value, and traceback while not splitting the string within traceback.
|
||||
let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].splitn(3, ", ");
|
||||
|
||||
assert_eq!(fields.next().unwrap(), "type: <class 'Exception'>");
|
||||
assert_eq!(fields.next().unwrap(), "value: Exception('banana')");
|
||||
|
||||
let traceback = fields.next().unwrap();
|
||||
assert!(
|
||||
traceback.starts_with("traceback: Some(\"<unformattable <traceback object at 0x"),
|
||||
"assertion failed, actual traceback str: {traceback:?}"
|
||||
);
|
||||
assert!(fields.next().is_none());
|
||||
|
||||
py.run(
|
||||
cr#"
|
||||
import io, sys, unittest.mock
|
||||
sys.modules['io'] = sys.modules['orig_io']
|
||||
del sys.modules['orig_io']
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
Vendored
+807
@@ -0,0 +1,807 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
use pyo3::buffer::PyBuffer;
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
use pyo3::exceptions::PyWarning;
|
||||
use pyo3::exceptions::{PyFutureWarning, PyUserWarning};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDateTime;
|
||||
#[cfg(not(PyPy))]
|
||||
use pyo3::types::PyFunction;
|
||||
use pyo3::types::{self, PyCFunction};
|
||||
use pyo3_macros::pyclass;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyfunction(name = "struct")]
|
||||
fn struct_function() {}
|
||||
|
||||
#[test]
|
||||
fn test_rust_keyword_name() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(struct_function)(py).unwrap();
|
||||
|
||||
py_assert!(py, f, "f.__name__ == 'struct'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (arg = true))]
|
||||
fn optional_bool(arg: Option<bool>) -> String {
|
||||
format!("{arg:?}")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_bool() {
|
||||
// Regression test for issue #932
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(optional_bool)(py).unwrap();
|
||||
|
||||
py_assert!(py, f, "f() == 'Some(true)'");
|
||||
py_assert!(py, f, "f(True) == 'Some(true)'");
|
||||
py_assert!(py, f, "f(False) == 'Some(false)'");
|
||||
py_assert!(py, f, "f(None) == 'None'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_optional_no_signature() {
|
||||
// Since PyO3 0.24, trailing optional arguments are treated like any other required argument
|
||||
// (previously would get an implicit default of `None`)
|
||||
|
||||
#[pyfunction]
|
||||
fn trailing_optional(x: i32, y: Option<i32>) -> String {
|
||||
format!("x={x:?} y={y:?}")
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(trailing_optional)(py).unwrap();
|
||||
|
||||
py_assert!(py, f, "f(1, 2) == 'x=1 y=Some(2)'");
|
||||
py_assert!(py, f, "f(2, None) == 'x=2 y=None'");
|
||||
|
||||
py_expect_exception!(
|
||||
py,
|
||||
f,
|
||||
"f(3)",
|
||||
PyTypeError,
|
||||
"trailing_optional() missing 1 required positional argument: 'y'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature=(arg))]
|
||||
fn required_optional_str(arg: Option<&str>) -> &str {
|
||||
arg.unwrap_or("")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_str() {
|
||||
// Regression test for issue #4965
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(required_optional_str)(py).unwrap();
|
||||
|
||||
py_assert!(py, f, "f('') == ''");
|
||||
py_assert!(py, f, "f('foo') == 'foo'");
|
||||
py_assert!(py, f, "f(None) == ''");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct MyClass();
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature=(arg))]
|
||||
fn required_optional_class(arg: Option<&MyClass>) {
|
||||
let _ = arg;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_optional_class() {
|
||||
// Regression test for issue #4965
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(required_optional_class)(py).unwrap();
|
||||
let val = Bound::new(py, MyClass()).unwrap();
|
||||
|
||||
py_assert!(py, f val, "f(val) is None");
|
||||
py_assert!(py, f, "f(None) is None");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
#[pyfunction]
|
||||
fn buffer_inplace_add(py: Python<'_>, x: PyBuffer<i32>, y: PyBuffer<i32>) {
|
||||
let x = x.as_mut_slice(py).unwrap();
|
||||
let y = y.as_slice(py).unwrap();
|
||||
for (xi, yi) in x.iter().zip(y) {
|
||||
let xi_plus_yi = xi.get() + yi.get();
|
||||
xi.set(xi_plus_yi);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_11))]
|
||||
#[test]
|
||||
fn test_buffer_add() {
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(buffer_inplace_add)(py).unwrap();
|
||||
|
||||
py_expect_exception!(
|
||||
py,
|
||||
f,
|
||||
r#"
|
||||
import array
|
||||
a = array.array("i", [0, 1, 2, 3])
|
||||
b = array.array("I", [0, 1, 2, 3])
|
||||
f(a, b)
|
||||
"#,
|
||||
PyBufferError
|
||||
);
|
||||
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
f,
|
||||
r#"
|
||||
import array
|
||||
a = array.array("i", [0, 1, 2, 3])
|
||||
b = array.array("i", [2, 3, 4, 5])
|
||||
f(a, b)
|
||||
assert a, array.array("i", [2, 4, 6, 8])
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(PyPy))]
|
||||
#[pyfunction]
|
||||
fn function_with_pyfunction_arg<'py>(fun: &Bound<'py, PyFunction>) -> PyResult<Bound<'py, PyAny>> {
|
||||
fun.call((), None)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn function_with_pycfunction_arg<'py>(
|
||||
fun: &Bound<'py, PyCFunction>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
fun.call((), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_functions_with_function_args() {
|
||||
Python::attach(|py| {
|
||||
let py_cfunc_arg = wrap_pyfunction!(function_with_pycfunction_arg)(py).unwrap();
|
||||
let bool_to_string = wrap_pyfunction!(optional_bool)(py).unwrap();
|
||||
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
py_cfunc_arg
|
||||
bool_to_string,
|
||||
r#"
|
||||
assert py_cfunc_arg(bool_to_string) == "Some(true)"
|
||||
"#
|
||||
);
|
||||
|
||||
#[cfg(not(PyPy))]
|
||||
{
|
||||
let py_func_arg = wrap_pyfunction!(function_with_pyfunction_arg)(py).unwrap();
|
||||
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
py_func_arg,
|
||||
r#"
|
||||
def foo(): return "bar"
|
||||
assert py_func_arg(foo) == "bar"
|
||||
"#
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn datetime_to_timestamp(dt: &Bound<'_, PyAny>) -> PyResult<i64> {
|
||||
let dt = dt.cast::<PyDateTime>()?;
|
||||
let ts: f64 = dt.call_method0("timestamp")?.extract()?;
|
||||
|
||||
Ok(ts as i64)
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn function_with_custom_conversion(
|
||||
#[pyo3(from_py_with = datetime_to_timestamp)] timestamp: i64,
|
||||
) -> i64 {
|
||||
timestamp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_custom_conversion() {
|
||||
Python::attach(|py| {
|
||||
let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap();
|
||||
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
custom_conv_func,
|
||||
r#"
|
||||
import datetime
|
||||
|
||||
dt = datetime.datetime.fromtimestamp(1612040400)
|
||||
assert custom_conv_func(dt) == 1612040400
|
||||
"#
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_with_custom_conversion_error() {
|
||||
Python::attach(|py| {
|
||||
let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap();
|
||||
|
||||
py_expect_exception!(
|
||||
py,
|
||||
custom_conv_func,
|
||||
"custom_conv_func(['a'])",
|
||||
PyTypeError,
|
||||
"'list' object is not an instance of 'datetime'",
|
||||
"while processing 'timestamp'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_py_with_defaults() {
|
||||
fn optional_int(x: &Bound<'_, PyAny>) -> PyResult<Option<i32>> {
|
||||
if x.is_none() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Some(x.extract()).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
// issue 2280 combination of from_py_with and Option<T> did not compile
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (int=None))]
|
||||
fn from_py_with_option(#[pyo3(from_py_with = optional_int)] int: Option<i32>) -> i32 {
|
||||
int.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (len=0))]
|
||||
fn from_py_with_default(
|
||||
#[pyo3(from_py_with = <Bound<'_, _> as PyAnyMethods>::len)] len: usize,
|
||||
) -> usize {
|
||||
len
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(from_py_with_option)(py).unwrap();
|
||||
|
||||
assert_eq!(f.call0().unwrap().extract::<i32>().unwrap(), 0);
|
||||
assert_eq!(f.call1((123,)).unwrap().extract::<i32>().unwrap(), 123);
|
||||
assert_eq!(f.call1((999,)).unwrap().extract::<i32>().unwrap(), 999);
|
||||
|
||||
let f2 = wrap_pyfunction!(from_py_with_default)(py).unwrap();
|
||||
|
||||
assert_eq!(f2.call0().unwrap().extract::<usize>().unwrap(), 0);
|
||||
assert_eq!(f2.call1(("123",)).unwrap().extract::<usize>().unwrap(), 3);
|
||||
assert_eq!(f2.call1(("1234",)).unwrap().extract::<usize>().unwrap(), 4);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug, FromPyObject)]
|
||||
struct ValueClass {
|
||||
#[pyo3(get)]
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature=(str_arg, int_arg, tuple_arg, option_arg = None, struct_arg = None))]
|
||||
fn conversion_error(
|
||||
str_arg: &str,
|
||||
int_arg: i64,
|
||||
tuple_arg: (String, f64),
|
||||
option_arg: Option<i64>,
|
||||
struct_arg: Option<ValueClass>,
|
||||
) {
|
||||
println!("{str_arg:?} {int_arg:?} {tuple_arg:?} {option_arg:?} {struct_arg:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_error() {
|
||||
Python::attach(|py| {
|
||||
let conversion_error = wrap_pyfunction!(conversion_error)(py).unwrap();
|
||||
py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"conversion_error(None, None, None, None, None)",
|
||||
PyTypeError,
|
||||
"'None' is not an instance of 'str'",
|
||||
"while processing 'str_arg'"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"conversion_error(100, None, None, None, None)",
|
||||
PyTypeError,
|
||||
"'int' object is not an instance of 'str'",
|
||||
"while processing 'str_arg'"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"conversion_error('string1', 'string2', None, None, None)",
|
||||
PyTypeError,
|
||||
"'str' object cannot be interpreted as an integer",
|
||||
"while processing 'int_arg'"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"conversion_error('string1', -100, 'string2', None, None)",
|
||||
PyTypeError,
|
||||
"'str' object is not an instance of 'tuple'",
|
||||
"while processing 'tuple_arg'"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"conversion_error('string1', -100, ('string2', 10.), 'string3', None)",
|
||||
PyTypeError,
|
||||
"'str' object cannot be interpreted as an integer",
|
||||
"while processing 'option_arg'"
|
||||
);
|
||||
let exception = py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"
|
||||
class ValueClass:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
conversion_error('string1', -100, ('string2', 10.), None, ValueClass(\"no_expected_type\"))",
|
||||
PyTypeError
|
||||
);
|
||||
if exception.value(py).hasattr("add_note").unwrap() {
|
||||
assert_eq!(
|
||||
exception
|
||||
.value(py)
|
||||
.getattr("__notes__")
|
||||
.unwrap()
|
||||
.get_item(0)
|
||||
.unwrap()
|
||||
.extract::<std::borrow::Cow<'_, str>>()
|
||||
.unwrap(),
|
||||
"while processing 'struct_arg'"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
extract_traceback(py, exception),
|
||||
"TypeError: failed to \
|
||||
extract field ValueClass.value: TypeError: 'str' object cannot be interpreted as an integer"
|
||||
);
|
||||
|
||||
let exception = py_expect_exception!(
|
||||
py,
|
||||
conversion_error,
|
||||
"
|
||||
class ValueClass:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
conversion_error('string1', -100, ('string2', 10.), None, ValueClass(-5))",
|
||||
PyTypeError
|
||||
);
|
||||
if exception.value(py).hasattr("add_note").unwrap() {
|
||||
assert_eq!(
|
||||
exception
|
||||
.value(py)
|
||||
.getattr("__notes__")
|
||||
.unwrap()
|
||||
.get_item(0)
|
||||
.unwrap()
|
||||
.extract::<std::borrow::Cow<'_, str>>()
|
||||
.unwrap(),
|
||||
"while processing 'struct_arg'"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
extract_traceback(py, exception),
|
||||
"TypeError: failed to \
|
||||
extract field ValueClass.value: OverflowError: can't convert negative int to unsigned"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Helper function that concatenates the error message from
|
||||
/// each error in the traceback into a single string that can
|
||||
/// be tested.
|
||||
fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String {
|
||||
let mut error_msg = error.to_string();
|
||||
while let Some(cause) = error.cause(py) {
|
||||
error_msg.push_str(": ");
|
||||
error_msg.push_str(&cause.to_string());
|
||||
error = cause
|
||||
}
|
||||
error_msg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pycfunction_new() {
|
||||
use pyo3::ffi;
|
||||
|
||||
Python::attach(|py| {
|
||||
unsafe extern "C" fn c_fn(
|
||||
_self: *mut ffi::PyObject,
|
||||
_args: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject {
|
||||
unsafe { ffi::PyLong_FromLong(4200) }
|
||||
}
|
||||
|
||||
let py_fn = PyCFunction::new(
|
||||
py,
|
||||
c_fn,
|
||||
c"py_fn",
|
||||
c"py_fn for test (this is the docstring)",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, py_fn, "py_fn() == 4200");
|
||||
py_assert!(
|
||||
py,
|
||||
py_fn,
|
||||
"py_fn.__doc__ == 'py_fn for test (this is the docstring)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pycfunction_new_with_keywords() {
|
||||
use pyo3::ffi;
|
||||
use std::ffi::c_long;
|
||||
use std::ptr;
|
||||
|
||||
Python::attach(|py| {
|
||||
unsafe extern "C" fn c_fn(
|
||||
_self: *mut ffi::PyObject,
|
||||
args: *mut ffi::PyObject,
|
||||
kwds: *mut ffi::PyObject,
|
||||
) -> *mut ffi::PyObject {
|
||||
let mut foo: c_long = 0;
|
||||
let mut bar: c_long = 0;
|
||||
|
||||
#[cfg(not(Py_3_13))]
|
||||
let foo_name = std::ffi::CString::new("foo").unwrap();
|
||||
#[cfg(not(Py_3_13))]
|
||||
let kw_bar_name = std::ffi::CString::new("kw_bar").unwrap();
|
||||
#[cfg(not(Py_3_13))]
|
||||
let mut args_names = [foo_name.into_raw(), kw_bar_name.into_raw(), ptr::null_mut()];
|
||||
|
||||
#[cfg(Py_3_13)]
|
||||
let args_names = [c"foo".as_ptr(), c"kw_bar".as_ptr(), ptr::null_mut()];
|
||||
|
||||
unsafe {
|
||||
ffi::PyArg_ParseTupleAndKeywords(
|
||||
args,
|
||||
kwds,
|
||||
c"l|l".as_ptr(),
|
||||
#[cfg(Py_3_13)]
|
||||
args_names.as_ptr(),
|
||||
#[cfg(not(Py_3_13))]
|
||||
args_names.as_mut_ptr(),
|
||||
&mut foo,
|
||||
&mut bar,
|
||||
)
|
||||
};
|
||||
|
||||
#[cfg(not(Py_3_13))]
|
||||
drop(unsafe { std::ffi::CString::from_raw(args_names[0]) });
|
||||
#[cfg(not(Py_3_13))]
|
||||
drop(unsafe { std::ffi::CString::from_raw(args_names[1]) });
|
||||
|
||||
unsafe { ffi::PyLong_FromLong(foo * bar) }
|
||||
}
|
||||
|
||||
let py_fn = PyCFunction::new_with_keywords(
|
||||
py,
|
||||
c_fn,
|
||||
c"py_fn",
|
||||
c"py_fn for test (this is the docstring)",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, py_fn, "py_fn(42, kw_bar=100) == 4200");
|
||||
py_assert!(py, py_fn, "py_fn(foo=42, kw_bar=100) == 4200");
|
||||
py_assert!(
|
||||
py,
|
||||
py_fn,
|
||||
"py_fn.__doc__ == 'py_fn for test (this is the docstring)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closure() {
|
||||
Python::attach(|py| {
|
||||
let f = |args: &Bound<'_, types::PyTuple>,
|
||||
_kwargs: Option<&Bound<'_, types::PyDict>>|
|
||||
-> PyResult<_> {
|
||||
Python::attach(|py| {
|
||||
let res: PyResult<Vec<_>> = args
|
||||
.iter()
|
||||
.map(|elem| {
|
||||
if let Ok(i) = elem.extract::<i64>() {
|
||||
Ok((i + 1).into_pyobject(py)?.into_any().unbind())
|
||||
} else if let Ok(f) = elem.extract::<f64>() {
|
||||
Ok((2. * f).into_pyobject(py)?.into_any().unbind())
|
||||
} else if let Ok(mut s) = elem.extract::<String>() {
|
||||
s.push_str("-py");
|
||||
Ok(s.into_pyobject(py)?.into_any().unbind())
|
||||
} else {
|
||||
panic!("unexpected argument type for {elem:?}")
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
res
|
||||
})
|
||||
};
|
||||
let closure_py =
|
||||
PyCFunction::new_closure(py, Some(c"test_fn"), Some(c"test_fn doc"), f).unwrap();
|
||||
|
||||
py_assert!(py, closure_py, "closure_py(42) == [43]");
|
||||
py_assert!(py, closure_py, "closure_py.__name__ == 'test_fn'");
|
||||
py_assert!(py, closure_py, "closure_py.__doc__ == 'test_fn doc'");
|
||||
py_assert!(
|
||||
py,
|
||||
closure_py,
|
||||
"closure_py(42, 3.14, 'foo') == [43, 6.28, 'foo-py']"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closure_counter() {
|
||||
Python::attach(|py| {
|
||||
let counter = AtomicI32::new(0);
|
||||
let counter_fn = move |_args: &Bound<'_, types::PyTuple>,
|
||||
_kwargs: Option<&Bound<'_, types::PyDict>>|
|
||||
-> PyResult<i32> {
|
||||
let prev_count = counter.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(prev_count + 1)
|
||||
};
|
||||
let counter_py = PyCFunction::new_closure(py, None, None, counter_fn).unwrap();
|
||||
|
||||
py_assert!(py, counter_py, "counter_py() == 1");
|
||||
py_assert!(py, counter_py, "counter_py() == 2");
|
||||
py_assert!(py, counter_py, "counter_py() == 3");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_pyfunction() {
|
||||
mod function_in_module {
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
pub fn foo(x: i32) -> i32 {
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
use function_in_module::foo;
|
||||
|
||||
// check imported name can be wrapped
|
||||
let f = wrap_pyfunction!(foo, py).unwrap();
|
||||
assert_eq!(f.call1((5,)).unwrap().extract::<i32>().unwrap(), 5);
|
||||
assert_eq!(f.call1((42,)).unwrap().extract::<i32>().unwrap(), 42);
|
||||
|
||||
// check path import can be wrapped
|
||||
let f2 = wrap_pyfunction!(function_in_module::foo, py).unwrap();
|
||||
assert_eq!(f2.call1((5,)).unwrap().extract::<i32>().unwrap(), 5);
|
||||
assert_eq!(f2.call1((42,)).unwrap().extract::<i32>().unwrap(), 42);
|
||||
})
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct Key(String);
|
||||
|
||||
#[pyclass]
|
||||
struct Value(i32);
|
||||
|
||||
#[pyfunction]
|
||||
fn return_value_borrows_from_arguments<'py>(
|
||||
py: Python<'py>,
|
||||
key: &'py Key,
|
||||
value: &'py Value,
|
||||
) -> HashMap<&'py str, i32> {
|
||||
py.detach(move || {
|
||||
let mut map = HashMap::new();
|
||||
map.insert(key.0.as_str(), value.0);
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_return_value_borrows_from_arguments() {
|
||||
Python::attach(|py| {
|
||||
let function = wrap_pyfunction!(return_value_borrows_from_arguments, py).unwrap();
|
||||
|
||||
let key = Py::new(py, Key("key".to_owned())).unwrap();
|
||||
let value = Py::new(py, Value(42)).unwrap();
|
||||
|
||||
py_assert!(py, function key value, "function(key, value) == { \"key\": 42 }");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_some_wrap_arguments() {
|
||||
// Option<T> arguments get special treatment in pyfunction default values where it's
|
||||
// valid to pass the inner type without wrapping in `Some()`.
|
||||
//
|
||||
// See also https://github.com/PyO3/pyo3/issues/3460
|
||||
const NONE: Option<u8> = None;
|
||||
#[pyfunction(signature = (a = 1, b = Some(2), c = None, d = NONE))]
|
||||
fn some_wrap_arguments(
|
||||
a: Option<u8>,
|
||||
b: Option<u8>,
|
||||
c: Option<u8>,
|
||||
d: Option<u8>,
|
||||
) -> [Option<u8>; 4] {
|
||||
[a, b, c, d]
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let function = wrap_pyfunction!(some_wrap_arguments, py).unwrap();
|
||||
py_assert!(py, function, "function() == [1, 2, None, None]");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_to_bound_arguments() {
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (x, y = None))]
|
||||
fn reference_args<'py>(
|
||||
x: &Bound<'py, PyAny>,
|
||||
y: Option<&Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
y.map_or_else(|| Ok(x.clone()), |y| y.add(x))
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let function = wrap_pyfunction!(reference_args, py).unwrap();
|
||||
py_assert!(py, function, "function(1) == 1");
|
||||
py_assert!(py, function, "function(1, 2) == 3");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyfunction_raw_ident() {
|
||||
#[pyfunction]
|
||||
fn r#struct() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(name = "r#enum")]
|
||||
fn raw_ident() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn m(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(r#struct, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(raw_ident, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let m = pyo3::wrap_pymodule!(m)(py);
|
||||
py_assert!(py, m, "m.struct()");
|
||||
py_assert!(py, m, "m.enum()");
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
#[pyclass(extends=PyWarning)]
|
||||
pub struct UserDefinedWarning {}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
#[pymethods]
|
||||
impl UserDefinedWarning {
|
||||
#[new]
|
||||
#[pyo3(signature = (*_args, **_kwargs))]
|
||||
fn new(_args: Bound<'_, PyAny>, _kwargs: Option<Bound<'_, PyAny>>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyfunction_warn() {
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPW: this function raises warning"))]
|
||||
fn function_with_warning() {}
|
||||
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_warning,
|
||||
f,
|
||||
[("TPW: this function raises warning", PyUserWarning)]
|
||||
);
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPW: this function raises warning with category", category = PyFutureWarning))]
|
||||
fn function_with_warning_with_category() {}
|
||||
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_warning_with_category,
|
||||
f,
|
||||
[(
|
||||
"TPW: this function raises warning with category",
|
||||
PyFutureWarning
|
||||
)]
|
||||
);
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPW: custom deprecated category", category = pyo3::exceptions::PyDeprecationWarning))]
|
||||
fn function_with_warning_with_custom_category() {}
|
||||
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_warning_with_custom_category,
|
||||
f,
|
||||
[(
|
||||
"TPW: custom deprecated category",
|
||||
pyo3::exceptions::PyDeprecationWarning
|
||||
)]
|
||||
);
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPW: this function raises user-defined warning", category = UserDefinedWarning))]
|
||||
fn function_with_warning_and_user_defined_category() {}
|
||||
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_12))]
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_warning_and_user_defined_category,
|
||||
f,
|
||||
[(
|
||||
"TPW: this function raises user-defined warning",
|
||||
UserDefinedWarning
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pyfunction_multiple_warnings() {
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPMW: this function raises warning"))]
|
||||
#[pyo3(warn(message = "TPMW: this function raises FutureWarning", category = PyFutureWarning))]
|
||||
fn function_with_multiple_warnings() {}
|
||||
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_multiple_warnings,
|
||||
f,
|
||||
[
|
||||
("TPMW: this function raises warning", PyUserWarning),
|
||||
("TPMW: this function raises FutureWarning", PyFutureWarning)
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
#[pyfunction]
|
||||
#[pyo3(warn(message = "TPMW: this function raises FutureWarning", category = PyFutureWarning))]
|
||||
#[pyo3(warn(message = "TPMW: this function raises user-defined warning", category = UserDefinedWarning))]
|
||||
fn function_with_multiple_custom_warnings() {}
|
||||
|
||||
#[cfg(not(Py_LIMITED_API))]
|
||||
py_expect_warning_for_fn!(
|
||||
function_with_multiple_custom_warnings,
|
||||
f,
|
||||
[
|
||||
("TPMW: this function raises FutureWarning", PyFutureWarning),
|
||||
(
|
||||
"TPMW: this function raises user-defined warning",
|
||||
UserDefinedWarning
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
Vendored
+125
@@ -0,0 +1,125 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
//! Test slf: PyRef/PyMutRef<Self>(especially, slf.into::<Py>) works
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyBytes, PyString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
/// Assumes it's a file reader or so.
|
||||
/// Inspired by https://github.com/jothan/cordoba, thanks.
|
||||
#[pyclass(skip_from_py_object)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Reader {
|
||||
inner: HashMap<u8, String>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Reader {
|
||||
fn clone_ref<'a, 'py>(slf: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> {
|
||||
slf
|
||||
}
|
||||
fn clone_ref_with_py<'a, 'py>(
|
||||
slf: &'a Bound<'py, Self>,
|
||||
_py: Python<'py>,
|
||||
) -> &'a Bound<'py, Self> {
|
||||
slf
|
||||
}
|
||||
fn get_iter(slf: &Bound<'_, Self>, keys: Py<PyBytes>) -> Iter {
|
||||
Iter {
|
||||
reader: slf.clone().unbind(),
|
||||
keys,
|
||||
idx: 0,
|
||||
}
|
||||
}
|
||||
fn get_iter_and_reset(
|
||||
mut slf: PyRefMut<'_, Self>,
|
||||
keys: Py<PyBytes>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Iter> {
|
||||
let reader = Py::new(py, slf.clone())?;
|
||||
slf.inner.clear();
|
||||
Ok(Iter {
|
||||
reader,
|
||||
keys,
|
||||
idx: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug)]
|
||||
struct Iter {
|
||||
reader: Py<Reader>,
|
||||
keys: Py<PyBytes>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Iter {
|
||||
#[expect(clippy::self_named_constructors)]
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult<Option<Py<PyAny>>> {
|
||||
let bytes = slf.keys.bind(slf.py()).as_bytes();
|
||||
match bytes.get(slf.idx) {
|
||||
Some(&b) => {
|
||||
slf.idx += 1;
|
||||
let py = slf.py();
|
||||
let reader = slf.reader.bind(py);
|
||||
let reader_ref = reader.try_borrow()?;
|
||||
let res = reader_ref
|
||||
.inner
|
||||
.get(&b)
|
||||
.map(|s| PyString::new(py, s).into());
|
||||
Ok(res)
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reader() -> Reader {
|
||||
let reader = [(1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e")];
|
||||
Reader {
|
||||
inner: reader.iter().map(|(k, v)| (*k, (*v).to_string())).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_iter() {
|
||||
Python::attach(|py| {
|
||||
let reader = reader().into_pyobject(py).unwrap();
|
||||
py_assert!(
|
||||
py,
|
||||
reader,
|
||||
"list(reader.get_iter(bytes([3, 5, 2]))) == ['c', 'e', 'b']"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_ref() {
|
||||
Python::attach(|py| {
|
||||
let reader = reader().into_pyobject(py).unwrap();
|
||||
py_assert!(py, reader, "reader == reader.clone_ref()");
|
||||
py_assert!(py, reader, "reader == reader.clone_ref_with_py()");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_iter_reset() {
|
||||
Python::attach(|py| {
|
||||
let reader = Bound::new(py, reader()).unwrap();
|
||||
py_assert!(
|
||||
py,
|
||||
reader,
|
||||
"list(reader.get_iter_and_reset(bytes([3, 5, 2]))) == ['c', 'e', 'b']"
|
||||
);
|
||||
let reader_ref = reader.borrow();
|
||||
assert!(reader_ref.inner.is_empty());
|
||||
});
|
||||
}
|
||||
Vendored
+468
@@ -0,0 +1,468 @@
|
||||
// TODO https://github.com/PyO3/pyo3/issues/5487
|
||||
#![allow(clippy::undocumented_unsafe_blocks)]
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::exceptions::{PyIndexError, PyValueError};
|
||||
use pyo3::types::{IntoPyDict, PyList, PyMapping, PySequence};
|
||||
use pyo3::{ffi, prelude::*};
|
||||
|
||||
use pyo3::py_run;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct ByteSequence {
|
||||
elements: Vec<u8>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ByteSequence {
|
||||
#[new]
|
||||
#[pyo3(signature=(elements = None))]
|
||||
fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult<Self> {
|
||||
if let Some(pylist) = elements {
|
||||
let mut elems = Vec::with_capacity(pylist.len());
|
||||
for pyelem in pylist {
|
||||
let elem = pyelem.extract()?;
|
||||
elems.push(elem);
|
||||
}
|
||||
Ok(Self { elements: elems })
|
||||
} else {
|
||||
Ok(Self {
|
||||
elements: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn __len__(&self) -> usize {
|
||||
self.elements.len()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, idx: isize) -> PyResult<u8> {
|
||||
self.elements
|
||||
.get(idx as usize)
|
||||
.copied()
|
||||
.ok_or_else(|| PyIndexError::new_err("list index out of range"))
|
||||
}
|
||||
|
||||
fn __setitem__(&mut self, idx: isize, value: u8) {
|
||||
self.elements[idx as usize] = value;
|
||||
}
|
||||
|
||||
fn __delitem__(&mut self, mut idx: isize) -> PyResult<()> {
|
||||
let self_len = self.elements.len() as isize;
|
||||
if idx < 0 {
|
||||
idx += self_len;
|
||||
}
|
||||
if (idx < self_len) && (idx >= 0) {
|
||||
self.elements.remove(idx as usize);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(PyIndexError::new_err("list index out of range"))
|
||||
}
|
||||
}
|
||||
|
||||
fn __contains__(&self, other: &Bound<'_, PyAny>) -> bool {
|
||||
match other.extract::<u8>() {
|
||||
Ok(x) => self.elements.contains(&x),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn __concat__(&self, other: &Self) -> Self {
|
||||
let mut elements = self.elements.clone();
|
||||
elements.extend_from_slice(&other.elements);
|
||||
Self { elements }
|
||||
}
|
||||
|
||||
fn __inplace_concat__(mut slf: PyRefMut<'_, Self>, other: &Self) -> Py<Self> {
|
||||
slf.elements.extend_from_slice(&other.elements);
|
||||
slf.into()
|
||||
}
|
||||
|
||||
fn __repeat__(&self, count: isize) -> PyResult<Self> {
|
||||
if count >= 0 {
|
||||
let mut elements = Vec::with_capacity(self.elements.len() * count as usize);
|
||||
for _ in 0..count {
|
||||
elements.extend(&self.elements);
|
||||
}
|
||||
Ok(Self { elements })
|
||||
} else {
|
||||
Err(PyValueError::new_err("invalid repeat count"))
|
||||
}
|
||||
}
|
||||
|
||||
fn __inplace_repeat__(mut slf: PyRefMut<'_, Self>, count: isize) -> PyResult<Py<Self>> {
|
||||
if count >= 0 {
|
||||
let mut elements = Vec::with_capacity(slf.elements.len() * count as usize);
|
||||
for _ in 0..count {
|
||||
elements.extend(&slf.elements);
|
||||
}
|
||||
slf.elements = elements;
|
||||
Ok(slf.into())
|
||||
} else {
|
||||
Err(PyValueError::new_err("invalid repeat count"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a dict with `s = ByteSequence([1, 2, 3])`.
|
||||
fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
|
||||
let d = [("ByteSequence", py.get_type::<ByteSequence>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
// Though we can construct `s` in Rust, let's test `__new__` works.
|
||||
py_run!(py, *d, "s = ByteSequence([1, 2, 3])");
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_getitem() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_assert!(py, *d, "s[0] == 1");
|
||||
py_assert!(py, *d, "s[1] == 2");
|
||||
py_assert!(py, *d, "s[2] == 3");
|
||||
py_expect_exception!(py, *d, "print(s[-4])", PyIndexError);
|
||||
py_expect_exception!(py, *d, "print(s[4])", PyIndexError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_setitem() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_run!(py, *d, "s[0] = 4; assert list(s) == [4, 2, 3]");
|
||||
py_expect_exception!(py, *d, "s[0] = 'hello'", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delitem() {
|
||||
Python::attach(|py| {
|
||||
let d = [("ByteSequence", py.get_type::<ByteSequence>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[0]; assert list(s) == [2, 3]"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[1]; assert list(s) == [1, 3]"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[-1]; assert list(s) == [1, 2]"
|
||||
);
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[-2]; assert list(s) == [1, 3]"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[-4]; print(list(s))",
|
||||
PyIndexError
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2, 3]); del s[4]",
|
||||
PyIndexError
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_assert!(py, *d, "1 in s");
|
||||
py_assert!(py, *d, "2 in s");
|
||||
py_assert!(py, *d, "3 in s");
|
||||
py_assert!(py, *d, "4 not in s");
|
||||
py_assert!(py, *d, "'hello' not in s");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concat() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s1 = ByteSequence([1, 2]); s2 = ByteSequence([3, 4]); assert list(s1 + s2) == [1, 2, 3, 4]"
|
||||
);
|
||||
py_expect_exception!(
|
||||
py,
|
||||
*d,
|
||||
"s1 = ByteSequence([1, 2]); s2 = 'hello'; s1 + s2",
|
||||
PyTypeError
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inplace_concat() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s += ByteSequence([4, 5]); assert list(s) == [1, 2, 3, 4, 5]"
|
||||
);
|
||||
py_expect_exception!(py, *d, "s += 'hello'", PyTypeError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repeat() {
|
||||
Python::attach(|py| {
|
||||
let d = seq_dict(py);
|
||||
|
||||
py_run!(py, *d, "s2 = s * 2; assert list(s2) == [1, 2, 3, 1, 2, 3]");
|
||||
py_expect_exception!(py, *d, "s2 = s * -1", PyValueError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inplace_repeat() {
|
||||
Python::attach(|py| {
|
||||
let d = [("ByteSequence", py.get_type::<ByteSequence>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_run!(
|
||||
py,
|
||||
*d,
|
||||
"s = ByteSequence([1, 2]); s *= 3; assert list(s) == [1, 2, 1, 2, 1, 2]"
|
||||
);
|
||||
py_expect_exception!(py, *d, "s = ByteSequence([1, 2]); s *= -1", PyValueError);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(sequence)]
|
||||
struct SequenceOperators;
|
||||
|
||||
#[pymethods]
|
||||
impl SequenceOperators {
|
||||
fn __concat__(&self, _other: &Self) -> &'static str {
|
||||
"concat"
|
||||
}
|
||||
|
||||
fn __inplace_concat__(&self, _other: &Self) -> &'static str {
|
||||
"inplace_concat"
|
||||
}
|
||||
|
||||
fn __repeat__(&self, _count: isize) -> &'static str {
|
||||
"repeat"
|
||||
}
|
||||
|
||||
fn __inplace_repeat__(&self, _count: isize) -> &'static str {
|
||||
"inplace_repeat"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_operators_use_distinct_slots() {
|
||||
Python::attach(|py| {
|
||||
let d = [
|
||||
("left", Bound::new(py, SequenceOperators).unwrap()),
|
||||
("right", Bound::new(py, SequenceOperators).unwrap()),
|
||||
]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, *d, "left + right == 'concat'");
|
||||
py_run!(py, *d, "result = left; result += right");
|
||||
py_assert!(py, *d, "result == 'inplace_concat'");
|
||||
py_assert!(py, *d, "left * 2 == 'repeat'");
|
||||
py_run!(py, *d, "result = left; result *= 2");
|
||||
py_assert!(py, *d, "result == 'inplace_repeat'");
|
||||
});
|
||||
}
|
||||
|
||||
// Check that #[pyo3(get, set)] works correctly for Vec<PyObject>
|
||||
|
||||
#[pyclass]
|
||||
struct AnyObjectList {
|
||||
#[pyo3(get, set)]
|
||||
items: Vec<Py<PyAny>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_object_list_get() {
|
||||
Python::attach(|py| {
|
||||
let list = AnyObjectList {
|
||||
items: [1i32, 2, 3]
|
||||
.iter()
|
||||
.map(|i| i.into_pyobject(py).unwrap().into_any().unbind())
|
||||
.collect(),
|
||||
}
|
||||
.into_pyobject(py)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, list, "list.items == [1, 2, 3]");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_any_object_list_set() {
|
||||
Python::attach(|py| {
|
||||
let list = Bound::new(py, AnyObjectList { items: vec![] }).unwrap();
|
||||
|
||||
py_run!(py, list, "list.items = [1, 2, 3]");
|
||||
assert!(list
|
||||
.borrow()
|
||||
.items
|
||||
.iter()
|
||||
.zip(&[1u32, 2, 3])
|
||||
.all(|(a, b)| a.bind(py).eq(b.into_pyobject(py).unwrap()).unwrap()));
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass(sequence)]
|
||||
struct OptionList {
|
||||
#[pyo3(get, set)]
|
||||
items: Vec<Option<i64>>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl OptionList {
|
||||
fn __len__(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, idx: isize) -> PyResult<Option<i64>> {
|
||||
match self.items.get(idx as usize) {
|
||||
Some(x) => Ok(*x),
|
||||
None => Err(PyIndexError::new_err("Index out of bounds")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_option_list_get() {
|
||||
// Regression test for #798
|
||||
Python::attach(|py| {
|
||||
let list = Py::new(
|
||||
py,
|
||||
OptionList {
|
||||
items: vec![Some(1), None],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
py_assert!(py, list, "list[0] == 1");
|
||||
py_assert!(py, list, "list[1] == None");
|
||||
py_expect_exception!(py, list, "list[2]", PyIndexError);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_is_not_mapping() {
|
||||
Python::attach(|py| {
|
||||
let list = Bound::new(
|
||||
py,
|
||||
OptionList {
|
||||
items: vec![Some(1), None],
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.into_any();
|
||||
|
||||
PySequence::register::<OptionList>(py).unwrap();
|
||||
|
||||
assert!(list.cast::<PyMapping>().is_err());
|
||||
assert!(list.cast::<PySequence>().is_ok());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_length() {
|
||||
Python::attach(|py| {
|
||||
let list = Bound::new(
|
||||
py,
|
||||
OptionList {
|
||||
items: vec![Some(1), None],
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.into_any();
|
||||
|
||||
assert_eq!(list.len().unwrap(), 2);
|
||||
assert_eq!(unsafe { ffi::PySequence_Length(list.as_ptr()) }, 2);
|
||||
|
||||
assert_eq!(unsafe { ffi::PyMapping_Length(list.as_ptr()) }, -1);
|
||||
unsafe { ffi::PyErr_Clear() };
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(Py_3_10)]
|
||||
#[pyclass(generic, sequence)]
|
||||
struct GenericList {
|
||||
#[pyo3(get, set)]
|
||||
items: Vec<Py<PyAny>>,
|
||||
}
|
||||
|
||||
#[cfg(Py_3_10)]
|
||||
#[pymethods]
|
||||
impl GenericList {
|
||||
fn __len__(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, idx: isize) -> PyResult<Py<PyAny>> {
|
||||
match self.items.get(idx as usize) {
|
||||
Some(x) => pyo3::Python::attach(|py| Ok(x.clone_ref(py))),
|
||||
None => Err(PyIndexError::new_err("Index out of bounds")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(Py_3_10)]
|
||||
#[test]
|
||||
fn test_generic_both_subscriptions_types() {
|
||||
use pyo3::types::PyInt;
|
||||
use std::convert::Infallible;
|
||||
|
||||
Python::attach(|py| {
|
||||
let l = Bound::new(
|
||||
py,
|
||||
GenericList {
|
||||
items: [1, 2, 3]
|
||||
.iter()
|
||||
.map(|x| -> Py<PyAny> {
|
||||
let x: Result<Bound<'_, PyInt>, Infallible> = x.into_pyobject(py);
|
||||
x.unwrap().into_any().unbind()
|
||||
})
|
||||
.chain([py.None()])
|
||||
.collect(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let ty = py.get_type::<GenericList>();
|
||||
py_assert!(py, l, "l[0] == 1");
|
||||
py_run!(
|
||||
py,
|
||||
ty,
|
||||
"import types;
|
||||
import typing;
|
||||
IntOrNone: typing.TypeAlias = typing.Union[int, None];
|
||||
assert ty[IntOrNone] == types.GenericAlias(ty, (IntOrNone,))"
|
||||
);
|
||||
py_assert!(py, l, "list(reversed(l)) == [None, 3, 2, 1]");
|
||||
});
|
||||
}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
#![cfg(feature = "serde")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Group {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct User {
|
||||
username: String,
|
||||
group: Option<Py<Group>>,
|
||||
friends: Vec<Py<User>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let friend1 = User {
|
||||
username: "friend 1".into(),
|
||||
group: None,
|
||||
friends: vec![],
|
||||
};
|
||||
let friend2 = User {
|
||||
username: "friend 2".into(),
|
||||
group: None,
|
||||
friends: vec![],
|
||||
};
|
||||
|
||||
let user = Python::attach(|py| {
|
||||
let py_friend1 = Py::new(py, friend1).expect("failed to create friend 1");
|
||||
let py_friend2 = Py::new(py, friend2).expect("failed to create friend 2");
|
||||
|
||||
let friends = vec![py_friend1, py_friend2];
|
||||
let py_group = Py::new(
|
||||
py,
|
||||
Group {
|
||||
name: "group name".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
User {
|
||||
username: "danya".into(),
|
||||
group: Some(py_group),
|
||||
friends,
|
||||
}
|
||||
});
|
||||
|
||||
let serialized = serde_json::to_string(&user).expect("failed to serialize");
|
||||
assert_eq!(
|
||||
serialized,
|
||||
r#"{"username":"danya","group":{"name":"group name"},"friends":[{"username":"friend 1","group":null,"friends":[]},{"username":"friend 2","group":null,"friends":[]}]}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize() {
|
||||
let serialized = r#"{"username": "danya", "friends":
|
||||
[{"username": "friend", "group": {"name": "danya's friends"}, "friends": []}]}"#;
|
||||
let user: User = serde_json::from_str(serialized).expect("failed to deserialize");
|
||||
|
||||
assert_eq!(user.username, "danya");
|
||||
assert!(user.group.is_none());
|
||||
assert_eq!(user.friends.len(), 1usize);
|
||||
let friend = user.friends.first().unwrap();
|
||||
|
||||
Python::attach(|py| {
|
||||
assert_eq!(friend.borrow(py).username, "friend");
|
||||
assert_eq!(
|
||||
friend.borrow(py).group.as_ref().unwrap().borrow(py).name,
|
||||
"danya's friends"
|
||||
)
|
||||
});
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::exceptions::PyIndexError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::IntoPyDict;
|
||||
|
||||
use pyo3::py_run;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct Count5();
|
||||
|
||||
#[pymethods]
|
||||
impl Count5 {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self()
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn __len__() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn __getitem__(idx: isize) -> PyResult<f64> {
|
||||
if idx < 0 {
|
||||
Err(PyIndexError::new_err("Count5 cannot count backwards"))
|
||||
} else if idx > 4 {
|
||||
Err(PyIndexError::new_err("Count5 cannot count higher than 5"))
|
||||
} else {
|
||||
Ok(idx as f64 + 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a dict with `s = Count5()`.
|
||||
fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
|
||||
let d = [("Count5", py.get_type::<Count5>())]
|
||||
.into_py_dict(py)
|
||||
.unwrap();
|
||||
// Though we can construct `s` in Rust, let's test `__new__` works.
|
||||
py_run!(py, *d, "s = Count5()");
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len() {
|
||||
Python::attach(|py| {
|
||||
let d = test_dict(py);
|
||||
|
||||
py_assert!(py, *d, "len(s) == 5");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_getitem() {
|
||||
Python::attach(|py| {
|
||||
let d = test_dict(py);
|
||||
|
||||
py_assert!(py, *d, "s[4] == 5.0");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
Python::attach(|py| {
|
||||
let d = test_dict(py);
|
||||
|
||||
py_assert!(py, *d, "list(s) == [1.0, 2.0, 3.0, 4.0, 5.0]");
|
||||
});
|
||||
}
|
||||
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyfunction]
|
||||
fn take_str(_s: &str) {}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_encode_error() {
|
||||
Python::attach(|py| {
|
||||
let take_str = wrap_pyfunction!(take_str)(py).unwrap();
|
||||
py_expect_exception!(
|
||||
py,
|
||||
take_str,
|
||||
"take_str('\\ud800')",
|
||||
PyUnicodeEncodeError,
|
||||
"'utf-8' codec can't encode character '\\ud800' in position 0: surrogates not allowed"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
#![cfg(feature = "macros")]
|
||||
#![cfg(not(any(PyPy, GraalPy)))]
|
||||
|
||||
use pyo3::{prelude::*, types::PySuper};
|
||||
|
||||
#[pyclass(subclass)]
|
||||
struct BaseClass {
|
||||
val1: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl BaseClass {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
BaseClass { val1: 10 }
|
||||
}
|
||||
|
||||
pub fn method(&self) -> usize {
|
||||
self.val1
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(extends=BaseClass)]
|
||||
struct SubClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl SubClass {
|
||||
#[new]
|
||||
fn new() -> PyClassInitializer<Self> {
|
||||
PyClassInitializer::from(BaseClass::new()).add_subclass(Self {})
|
||||
}
|
||||
|
||||
fn method<'py>(self_: &Bound<'py, Self>) -> PyResult<Bound<'py, PyAny>> {
|
||||
let super_ = self_.py_super()?;
|
||||
super_.call_method("method", (), None)
|
||||
}
|
||||
|
||||
fn method_super_new<'py>(self_: &Bound<'py, Self>) -> PyResult<Bound<'py, PyAny>> {
|
||||
let super_ = PySuper::new(&self_.get_type(), self_)?;
|
||||
super_.call_method("method", (), None)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_call_super_method() {
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<SubClass>();
|
||||
pyo3::py_run!(
|
||||
py,
|
||||
cls,
|
||||
r#"
|
||||
obj = cls()
|
||||
assert obj.method() == 10
|
||||
assert obj.method_super_new() == 10
|
||||
"#
|
||||
)
|
||||
});
|
||||
}
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyTuple};
|
||||
use pyo3::{types::PyType, wrap_pymodule};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[test]
|
||||
fn class_without_docs_or_signature() {
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__doc__ is None");
|
||||
py_assert!(py, typeobj, "typeobj.__text_signature__ is None");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_with_docs() {
|
||||
/// docs line1
|
||||
#[pyclass]
|
||||
/// docs line2
|
||||
struct MyClass {}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'");
|
||||
py_assert!(py, typeobj, "typeobj.__text_signature__ is None");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)]
|
||||
fn class_with_signature_no_doc() {
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
#[pyo3(signature = (a, b=None, *, c=42), text_signature = "(a, b=None, *, c=42)")]
|
||||
fn __new__(a: i32, b: Option<i32>, c: i32) -> Self {
|
||||
let _ = (a, b, c);
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
py_assert!(py, typeobj, "typeobj.__doc__ == ''");
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.__text_signature__ == '(a, b=None, *, c=42)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)]
|
||||
fn class_with_docs_and_signature() {
|
||||
/// docs line1
|
||||
#[pyclass]
|
||||
/// docs line2
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
#[pyo3(signature = (a, b=None, *, c=42), text_signature = "(a, b=None, *, c=42)")]
|
||||
fn __new__(a: i32, b: Option<i32>, c: i32) -> Self {
|
||||
let _ = (a, b, c);
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'");
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.__text_signature__ == '(a, b=None, *, c=42)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function() {
|
||||
#[pyfunction(signature = (a, b=None, *, c=42))]
|
||||
#[pyo3(text_signature = "(a, b=None, *, c=42)")]
|
||||
fn my_function(a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(my_function)(py).unwrap();
|
||||
|
||||
py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_test_signature_function() {
|
||||
#[pyfunction]
|
||||
fn my_function(a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyfunction(pass_module)]
|
||||
fn my_function_2(module: &Bound<'_, PyModule>, a: i32, b: i32, c: i32) {
|
||||
let _ = (module, a, b, c);
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (a, /, b = None, *, c = 5))]
|
||||
fn my_function_3(a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
|
||||
fn my_function_4(
|
||||
a: i32,
|
||||
b: Option<i32>,
|
||||
args: &Bound<'_, PyTuple>,
|
||||
c: i32,
|
||||
d: i32,
|
||||
kwargs: Option<&Bound<'_, PyDict>>,
|
||||
) {
|
||||
let _ = (a, b, args, c, d, kwargs);
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (a = 1, /, b = None, c = 1.5, d=5, e = "pyo3", f = 'f', h = true))]
|
||||
fn my_function_5(a: i32, b: Option<i32>, c: f32, d: i32, e: &str, f: char, h: bool) {
|
||||
let _ = (a, b, c, d, e, f, h);
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature=(a, b=None, c=None))]
|
||||
fn my_function_6(a: i32, b: Option<i32>, c: Option<i32>) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn trailing_optional_required(a: i32, b: Option<i32>, c: Option<i32>) {
|
||||
// Since PyO3 0.24, trailing optional arguments are treated like any other required argument
|
||||
// (previously would get an implicit default of `None`)
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
macro_rules! assert_text_signature {
|
||||
($py:expr, $func:ident, $expected:expr) => {
|
||||
assert_eq!(
|
||||
wrap_pyfunction!($func, $py)
|
||||
.unwrap()
|
||||
.getattr("__text_signature__")
|
||||
.unwrap()
|
||||
.cast_into::<pyo3::types::PyString>()
|
||||
.unwrap(),
|
||||
$expected
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
assert_text_signature!(py, my_function, "(a, b, c)");
|
||||
|
||||
assert_text_signature!(py, my_function_2, "($module, a, b, c)");
|
||||
|
||||
assert_text_signature!(py, my_function_3, "(a, /, b=None, *, c=5)");
|
||||
|
||||
assert_text_signature!(py, my_function_4, "(a, /, b=None, *args, c, d=5, **kwargs)");
|
||||
|
||||
assert_text_signature!(
|
||||
py,
|
||||
my_function_5,
|
||||
"(a=1, /, b=None, c=1.5, d=5, e=\"pyo3\", f='f', h=True)"
|
||||
);
|
||||
|
||||
assert_text_signature!(py, my_function_6, "(a, b=None, c=None)");
|
||||
|
||||
assert_text_signature!(py, trailing_optional_required, "(a, b, c)");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_test_signature_method() {
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new(a: i32, b: i32, c: i32) -> Self {
|
||||
let _ = (a, b, c);
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn method(&self, a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyo3(signature = (a, /, b = None, *, c = 5))]
|
||||
fn method_2(&self, a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyo3(signature = (a, /, b = None, *args, c, d=5, **kwargs))]
|
||||
fn method_3(
|
||||
&self,
|
||||
a: i32,
|
||||
b: Option<i32>,
|
||||
args: &Bound<'_, PyTuple>,
|
||||
c: i32,
|
||||
d: i32,
|
||||
kwargs: Option<&Bound<'_, PyDict>>,
|
||||
) {
|
||||
let _ = (a, b, args, c, d, kwargs);
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
fn staticmethod(a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
fn classmethod(cls: &Bound<'_, PyType>, a: i32, b: i32, c: i32) {
|
||||
let _ = (cls, a, b, c);
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let cls = py.get_type::<MyClass>();
|
||||
#[cfg(any(not(Py_LIMITED_API), Py_3_10))]
|
||||
py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'");
|
||||
py_assert!(
|
||||
py,
|
||||
cls,
|
||||
"cls.method.__text_signature__ == '($self, a, b, c)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
cls,
|
||||
"cls.method_2.__text_signature__ == '($self, a, /, b=None, *, c=5)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
cls,
|
||||
"cls.method_3.__text_signature__ == '($self, a, /, b=None, *args, c, d=5, **kwargs)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
cls,
|
||||
"cls.staticmethod.__text_signature__ == '(a, b, c)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
cls,
|
||||
"cls.classmethod.__text_signature__ == '($cls, a, b, c)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_test_signature_opt_out() {
|
||||
#[pyfunction(text_signature = None)]
|
||||
fn my_function(a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyfunction(signature = (a, /, b = None, *, c = 5), text_signature = None)]
|
||||
fn my_function_2(a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
#[pyo3(text_signature = None)]
|
||||
fn new(a: i32, b: i32, c: i32) -> Self {
|
||||
let _ = (a, b, c);
|
||||
Self {}
|
||||
}
|
||||
|
||||
#[pyo3(text_signature = None)]
|
||||
fn method(&self, a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[pyo3(signature = (a, /, b = None, *, c = 5), text_signature = None)]
|
||||
fn method_2(&self, a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(text_signature = None)]
|
||||
fn staticmethod(a: i32, b: i32, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(text_signature = None)]
|
||||
fn classmethod(cls: &Bound<'_, PyType>, a: i32, b: i32, c: i32) {
|
||||
let _ = (cls, a, b, c);
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let f = wrap_pyfunction!(my_function)(py).unwrap();
|
||||
py_assert!(py, f, "f.__text_signature__ == None");
|
||||
|
||||
let f = wrap_pyfunction!(my_function_2)(py).unwrap();
|
||||
py_assert!(py, f, "f.__text_signature__ == None");
|
||||
|
||||
let cls = py.get_type::<MyClass>();
|
||||
py_assert!(py, cls, "cls.__text_signature__ == None");
|
||||
py_assert!(py, cls, "cls.method.__text_signature__ == None");
|
||||
py_assert!(py, cls, "cls.method_2.__text_signature__ == None");
|
||||
py_assert!(py, cls, "cls.staticmethod.__text_signature__ == None");
|
||||
py_assert!(py, cls, "cls.classmethod.__text_signature__ == None");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_pyfn() {
|
||||
#[pymodule]
|
||||
fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
#[pyfn(m, signature = (a, b=None, *, c=42))]
|
||||
#[pyo3(text_signature = "(a, b=None, *, c=42)")]
|
||||
fn my_function(a: i32, b: Option<i32>, c: i32) {
|
||||
let _ = (a, b, c);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let m = wrap_pymodule!(my_module)(py);
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
m,
|
||||
"m.my_function.__text_signature__ == '(a, b=None, *, c=42)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_methods() {
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[pyo3(text_signature = "($self, a)")]
|
||||
fn method(&self, a: i32) {
|
||||
let _ = a;
|
||||
}
|
||||
#[pyo3(text_signature = "($self, b)")]
|
||||
fn pyself_method(_this: &Bound<'_, Self>, b: i32) {
|
||||
let _ = b;
|
||||
}
|
||||
#[classmethod]
|
||||
#[pyo3(text_signature = "($cls, c)")]
|
||||
fn class_method(_cls: &Bound<'_, PyType>, c: i32) {
|
||||
let _ = c;
|
||||
}
|
||||
#[staticmethod]
|
||||
#[pyo3(text_signature = "(d)")]
|
||||
fn static_method(d: i32) {
|
||||
let _ = d;
|
||||
}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.method.__text_signature__ == '($self, a)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.pyself_method.__text_signature__ == '($self, b)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.class_method.__text_signature__ == '($cls, c)'"
|
||||
);
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.static_method.__text_signature__ == '(d)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)]
|
||||
fn test_raw_identifiers() {
|
||||
#[pyclass]
|
||||
struct r#MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[new]
|
||||
fn new() -> MyClass {
|
||||
MyClass {}
|
||||
}
|
||||
fn r#method(&self) {}
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let typeobj = py.get_type::<MyClass>();
|
||||
|
||||
py_assert!(py, typeobj, "typeobj.__text_signature__ == '()'");
|
||||
|
||||
py_assert!(
|
||||
py,
|
||||
typeobj,
|
||||
"typeobj.method.__text_signature__ == '($self)'"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+291
@@ -0,0 +1,291 @@
|
||||
// Common macros and helpers for tests
|
||||
//
|
||||
// This file is used in two different ways, which makes it a bit of a pain to build:
|
||||
// - as a module `include!`-ed from `src/test_utils.rs`
|
||||
// - as a module `mod test_utils` in various integration tests
|
||||
//
|
||||
// the inner mod enables the #![allow(dead_code)] to
|
||||
// be applied - `src/test_utils.rs` uses `include!` to pull in this file
|
||||
|
||||
#[allow(
|
||||
dead_code,
|
||||
unused_macros,
|
||||
reason = "many tests do not use the complete set of functionality offered here"
|
||||
)]
|
||||
#[allow(missing_docs, reason = "only used in tests")]
|
||||
#[macro_use]
|
||||
mod inner {
|
||||
|
||||
#[allow(
|
||||
unused_imports,
|
||||
reason = "pulls in `use crate as pyo3` from src/test_utils.rs, no function in integration tests"
|
||||
)]
|
||||
use super::*;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), feature = "macros"))]
|
||||
use pyo3::sync::MutexExt;
|
||||
use pyo3::types::{IntoPyDict, PyList};
|
||||
|
||||
#[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), feature = "macros"))]
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! py_assert {
|
||||
($py:expr, $($val:ident)+, $assertion:literal) => {
|
||||
pyo3::py_run!($py, $($val)+, concat!("assert ", $assertion))
|
||||
};
|
||||
($py:expr, *$dict:expr, $assertion:literal) => {
|
||||
pyo3::py_run!($py, *$dict, concat!("assert ", $assertion))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_py_eq {
|
||||
($val:expr, $expected:expr) => {
|
||||
assert!($val.eq($expected).unwrap());
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! py_expect_exception {
|
||||
// Case1: idents & no err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{
|
||||
use pyo3::types::IntoPyDict;
|
||||
use pyo3::BoundObject;
|
||||
let d = [$((stringify!($val), (&$val).into_pyobject($py).unwrap().into_any().into_bound()),)+].into_py_dict($py).unwrap();
|
||||
py_expect_exception!($py, *d, $code, $err)
|
||||
}};
|
||||
// Case2: dict & no err_msg
|
||||
($py:expr, *$dict:expr, $code:expr, $err:ident) => {{
|
||||
let res = $py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed()));
|
||||
let err = res.expect_err(&format!("Did not raise {}", stringify!($err)));
|
||||
if !err.matches($py, $py.get_type::<pyo3::exceptions::$err>()).unwrap() {
|
||||
panic!("Expected {} but got {:?}", stringify!($err), err)
|
||||
}
|
||||
err
|
||||
}};
|
||||
// Case3: idents & err_msg
|
||||
($py:expr, $($val:ident)+, $code:expr, $err:ident, $err_msg:literal $(,$notes:literal)*) => {{
|
||||
let err = py_expect_exception!($py, $($val)+, $code, $err);
|
||||
// Suppose that the error message looks like 'TypeError: ~'
|
||||
assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg));
|
||||
if err.value($py).hasattr("add_note").unwrap() {
|
||||
let notes = err.value($py).getattr("__notes__").map(|n| n.cast_into::<pyo3::types::PyList>().unwrap()).unwrap_or_else(|_| pyo3::types::PyList::empty($py));
|
||||
let mut _notes_iter = notes.iter();
|
||||
$(
|
||||
assert_eq!(_notes_iter.next().as_ref().map(|v| v.extract::<std::borrow::Cow<'_, str>>().unwrap()).as_deref(), Some($notes));
|
||||
)*
|
||||
}
|
||||
err
|
||||
}};
|
||||
// Case4: dict & err_msg
|
||||
($py:expr, *$dict:expr, $code:expr, $err:ident, $err_msg:literal) => {{
|
||||
let err = py_expect_exception!($py, *$dict, $code, $err);
|
||||
assert_eq!(format!("Py{}", err), concat!(stringify!($err), ": ", $err_msg));
|
||||
err
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! py_expect_warning {
|
||||
($py:expr, $($val:ident)+, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{
|
||||
use pyo3::types::IntoPyDict;
|
||||
let d = [$((stringify!($val), ($val.as_ref() as &Bound<'_, PyAny>).into_pyobject($py).expect("Failed to create test dict element")),)+].into_py_dict($py).expect("Failed to create test dict");
|
||||
py_expect_warning!($py, *d, $code, [$(($warning_msg, $warning_category)),+])
|
||||
}};
|
||||
($py:expr, *$dict:expr, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{
|
||||
$crate::test_utils::CatchWarnings::enter($py, |warning_record| {
|
||||
$py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code");
|
||||
let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object($py))),+];
|
||||
|
||||
assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len());
|
||||
|
||||
for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) {
|
||||
let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string();
|
||||
let actual_category = warning.getattr("category").unwrap();
|
||||
|
||||
assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg);
|
||||
assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}).expect("failed to test warnings");
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! py_expect_warning_for_fn {
|
||||
($fn:ident, $($val:ident)+, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {
|
||||
pyo3::Python::attach(|py| {
|
||||
let f = wrap_pyfunction!($fn)(py).unwrap();
|
||||
py_expect_warning!(
|
||||
py,
|
||||
f,
|
||||
"f()",
|
||||
[$(($warning_msg, $warning_category)),+]
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
pub struct UnraisableCapture<'py> {
|
||||
hook: Bound<'py, UnraisableCaptureHook>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
impl<'py> UnraisableCapture<'py> {
|
||||
/// Runs the closure `f` with a custom sys.unraisablehook installed.
|
||||
///
|
||||
/// `f`
|
||||
pub fn enter<R>(py: Python<'py>, f: impl FnOnce(&Self) -> R) -> R {
|
||||
// unraisablehook is a global, so only one thread can be using this struct at a time.
|
||||
static UNRAISABLE_HOOK_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
// NB this is best-effort, other tests could always modify sys.unraisablehook directly.
|
||||
let mutex_guard = UNRAISABLE_HOOK_MUTEX
|
||||
.lock_py_attached(py)
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
|
||||
let guard = Self {
|
||||
hook: UnraisableCaptureHook::install(py),
|
||||
};
|
||||
|
||||
let result = f(&guard);
|
||||
|
||||
drop(guard);
|
||||
drop(mutex_guard);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Takes the captured unraisable error, if any.
|
||||
pub fn take_capture(&self) -> Option<(PyErr, Bound<'py, PyAny>)> {
|
||||
let mut guard = self.hook.get().capture.lock().unwrap();
|
||||
guard.take().map(|(e, o)| (e, o.into_bound(self.hook.py())))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
impl Drop for UnraisableCapture<'_> {
|
||||
fn drop(&mut self) {
|
||||
let py = self.hook.py();
|
||||
self.hook.get().uninstall(py);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
#[pyclass(crate = "pyo3", frozen)]
|
||||
struct UnraisableCaptureHook {
|
||||
pub capture: Mutex<Option<(PyErr, Py<PyAny>)>>,
|
||||
old_hook: Py<PyAny>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
#[pymethods(crate = "pyo3")]
|
||||
impl UnraisableCaptureHook {
|
||||
pub fn hook(&self, unraisable: Bound<'_, PyAny>) {
|
||||
let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap());
|
||||
let instance = unraisable.getattr("object").unwrap();
|
||||
self.capture.lock().unwrap().replace((err, instance.into()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
impl UnraisableCaptureHook {
|
||||
fn install(py: Python<'_>) -> Bound<'_, Self> {
|
||||
let sys = py.import("sys").unwrap();
|
||||
|
||||
let old_hook = sys.getattr("unraisablehook").unwrap().into();
|
||||
let capture = Mutex::new(None);
|
||||
|
||||
let capture = Bound::new(py, UnraisableCaptureHook { capture, old_hook }).unwrap();
|
||||
|
||||
sys.setattr("unraisablehook", capture.getattr("hook").unwrap())
|
||||
.unwrap();
|
||||
|
||||
capture
|
||||
}
|
||||
|
||||
fn uninstall(&self, py: Python<'_>) {
|
||||
let sys = py.import("sys").unwrap();
|
||||
sys.setattr("unraisablehook", &self.old_hook).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CatchWarnings<'py> {
|
||||
catch_warnings: Bound<'py, PyAny>,
|
||||
}
|
||||
|
||||
/// catch_warnings is not thread-safe, so only one thread can be using this struct at
|
||||
/// a time.
|
||||
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))] // Python 3.14t has thread-safe catch_warnings
|
||||
static CATCH_WARNINGS_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
impl<'py> CatchWarnings<'py> {
|
||||
pub fn enter<R>(
|
||||
py: Python<'py>,
|
||||
f: impl FnOnce(&Bound<'py, PyList>) -> PyResult<R>,
|
||||
) -> PyResult<R> {
|
||||
// NB this is best-effort, other tests could always call the warnings API directly.
|
||||
#[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))]
|
||||
let _mutex_guard = CATCH_WARNINGS_MUTEX
|
||||
.lock_py_attached(py)
|
||||
.unwrap_or_else(PoisonError::into_inner);
|
||||
let warnings = py.import("warnings")?;
|
||||
let kwargs = [("record", true)].into_py_dict(py)?;
|
||||
let catch_warnings = warnings
|
||||
.getattr("catch_warnings")?
|
||||
.call((), Some(&kwargs))?;
|
||||
let list = catch_warnings.call_method0("__enter__")?.cast_into()?;
|
||||
let _guard = Self { catch_warnings };
|
||||
f(&list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CatchWarnings<'_> {
|
||||
fn drop(&mut self) {
|
||||
let py = self.catch_warnings.py();
|
||||
self.catch_warnings
|
||||
.call_method1("__exit__", (py.None(), py.None(), py.None()))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! assert_warnings {
|
||||
($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{
|
||||
$crate::test_utils::CatchWarnings::enter($py, |w| {
|
||||
use $crate::types::{PyListMethods, PyStringMethods};
|
||||
$body;
|
||||
let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+];
|
||||
assert_eq!(w.len(), expected_warnings.len());
|
||||
for (warning, (category, message)) in w.iter().zip(expected_warnings) {
|
||||
|
||||
assert!(warning.getattr("category").unwrap().is(&category));
|
||||
assert_eq!(
|
||||
warning.getattr("message").unwrap().str().unwrap().to_string_lossy(),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_imports, reason = "not all tests use this macro")]
|
||||
pub(crate) use assert_warnings;
|
||||
|
||||
pub fn generate_unique_module_name(base: &str) -> std::ffi::CString {
|
||||
let uuid = Uuid::new_v4().simple().to_string();
|
||||
std::ffi::CString::new(format!("{base}_{uuid}")).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_imports, reason = "some integration tests use just the macros")]
|
||||
pub use inner::*;
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyTuple};
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct MyClass {}
|
||||
|
||||
#[pymethods]
|
||||
impl MyClass {
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (*args))]
|
||||
fn test_args(args: Bound<'_, PyTuple>) -> Bound<'_, PyTuple> {
|
||||
args
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (**kwargs))]
|
||||
fn test_kwargs(kwargs: Option<Bound<'_, PyDict>>) -> Option<Bound<'_, PyDict>> {
|
||||
kwargs
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_args() {
|
||||
Python::attach(|py| {
|
||||
let my_obj = py.get_type::<MyClass>();
|
||||
py_assert!(py, my_obj, "my_obj.test_args() == ()");
|
||||
py_assert!(py, my_obj, "my_obj.test_args(1) == (1,)");
|
||||
py_assert!(py, my_obj, "my_obj.test_args(1, 2) == (1, 2)");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_kwargs() {
|
||||
Python::attach(|py| {
|
||||
let my_obj = py.get_type::<MyClass>();
|
||||
py_assert!(py, my_obj, "my_obj.test_kwargs() == None");
|
||||
py_assert!(py, my_obj, "my_obj.test_kwargs(test=1) == {'test': 1}");
|
||||
py_assert!(
|
||||
py,
|
||||
my_obj,
|
||||
"my_obj.test_kwargs(test1=1, test2=2) == {'test1':1, 'test2':2}"
|
||||
);
|
||||
});
|
||||
}
|
||||
Vendored
+208
@@ -0,0 +1,208 @@
|
||||
#![cfg(feature = "macros")]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::py_run;
|
||||
use pyo3::types::PyTuple;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
mod test_utils;
|
||||
|
||||
#[pyclass]
|
||||
struct MutRefArg {
|
||||
n: i32,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl MutRefArg {
|
||||
fn get(&self) -> i32 {
|
||||
self.n
|
||||
}
|
||||
fn set_other(&self, mut other: PyRefMut<'_, MutRefArg>) {
|
||||
other.n = 100;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mut_ref_arg() {
|
||||
Python::attach(|py| {
|
||||
let inst1 = Py::new(py, MutRefArg { n: 0 }).unwrap();
|
||||
let inst2 = Py::new(py, MutRefArg { n: 0 }).unwrap();
|
||||
|
||||
py_run!(py, inst1 inst2, "inst1.set_other(inst2)");
|
||||
let inst2 = inst2.bind(py).borrow();
|
||||
assert_eq!(inst2.n, 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct PyUsize {
|
||||
#[pyo3(get)]
|
||||
pub value: usize,
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn get_zero() -> PyUsize {
|
||||
PyUsize { value: 0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Checks that we can use return a custom class in arbitrary function and use those functions
|
||||
/// both in rust and python
|
||||
fn return_custom_class() {
|
||||
Python::attach(|py| {
|
||||
// Using from rust
|
||||
assert_eq!(get_zero().value, 0);
|
||||
|
||||
// Using from python
|
||||
let get_zero = wrap_pyfunction!(get_zero)(py).unwrap();
|
||||
py_assert!(py, get_zero, "get_zero().value == 0");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intopytuple_primitive() {
|
||||
Python::attach(|py| {
|
||||
let tup = (1, 2, "foo");
|
||||
py_assert!(py, tup, "tup == (1, 2, 'foo')");
|
||||
py_assert!(py, tup, "tup[0] == 1");
|
||||
py_assert!(py, tup, "tup[1] == 2");
|
||||
py_assert!(py, tup, "tup[2] == 'foo'");
|
||||
});
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct SimplePyClass {}
|
||||
|
||||
#[test]
|
||||
fn intopytuple_pyclass() {
|
||||
Python::attach(|py| {
|
||||
let tup = (
|
||||
Py::new(py, SimplePyClass {}).unwrap(),
|
||||
Py::new(py, SimplePyClass {}).unwrap(),
|
||||
);
|
||||
py_assert!(py, tup, "type(tup[0]).__name__ == 'SimplePyClass'");
|
||||
py_assert!(py, tup, "type(tup[0]).__name__ == type(tup[1]).__name__");
|
||||
py_assert!(py, tup, "tup[0] != tup[1]");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pytuple_primitive_iter() {
|
||||
Python::attach(|py| {
|
||||
let tup = PyTuple::new(py, [1u32, 2, 3].iter()).unwrap();
|
||||
py_assert!(py, tup, "tup == (1, 2, 3)");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pytuple_pyclass_iter() {
|
||||
Python::attach(|py| {
|
||||
let tup = PyTuple::new(
|
||||
py,
|
||||
[
|
||||
Py::new(py, SimplePyClass {}).unwrap(),
|
||||
Py::new(py, SimplePyClass {}).unwrap(),
|
||||
]
|
||||
.iter(),
|
||||
)
|
||||
.unwrap();
|
||||
py_assert!(py, tup, "type(tup[0]).__name__ == 'SimplePyClass'");
|
||||
py_assert!(py, tup, "type(tup[0]).__name__ == type(tup[0]).__name__");
|
||||
py_assert!(py, tup, "tup[0] != tup[1]");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
|
||||
fn test_pickle() {
|
||||
use pyo3::types::PyDict;
|
||||
|
||||
#[pyclass(dict, module = "test_module")]
|
||||
struct PickleSupport {}
|
||||
|
||||
#[pymethods]
|
||||
impl PickleSupport {
|
||||
#[new]
|
||||
fn new() -> PickleSupport {
|
||||
PickleSupport {}
|
||||
}
|
||||
|
||||
pub fn __reduce__<'py>(
|
||||
slf: &Bound<'py, Self>,
|
||||
py: Python<'py>,
|
||||
) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyTuple>, Bound<'py, PyAny>)> {
|
||||
let cls = slf.getattr("__class__")?;
|
||||
let dict = slf.getattr("__dict__")?;
|
||||
Ok((cls, PyTuple::empty(py), dict))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> {
|
||||
PyModule::import(module.py(), "sys")?
|
||||
.dict()
|
||||
.get_item("modules")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.cast::<PyDict>()?
|
||||
.set_item(module.name()?, module)
|
||||
}
|
||||
|
||||
Python::attach(|py| {
|
||||
let module = PyModule::new(py, "test_module").unwrap();
|
||||
module.add_class::<PickleSupport>().unwrap();
|
||||
add_module(module).unwrap();
|
||||
let inst = Py::new(py, PickleSupport {}).unwrap();
|
||||
py_run!(
|
||||
py,
|
||||
inst,
|
||||
r#"
|
||||
inst.a = 1
|
||||
assert inst.__dict__ == {'a': 1}
|
||||
|
||||
import pickle
|
||||
inst2 = pickle.loads(pickle.dumps(inst))
|
||||
|
||||
assert inst2.__dict__ == {'a': 1}
|
||||
"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Testing https://github.com/PyO3/pyo3/issues/1106. A result type that
|
||||
/// implements `From<MyError> for PyErr` should be automatically converted
|
||||
/// when using `#[pyfunction]`.
|
||||
///
|
||||
/// This only makes sure that valid `Result` types do work. For an invalid
|
||||
/// enum type, see `ui/invalid_result_conversion.py`.
|
||||
#[derive(Debug)]
|
||||
struct MyError {
|
||||
pub descr: &'static str,
|
||||
}
|
||||
|
||||
impl fmt::Display for MyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "My error message: {}", self.descr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Important for the automatic conversion to `PyErr`.
|
||||
impl From<MyError> for PyErr {
|
||||
fn from(err: MyError) -> pyo3::PyErr {
|
||||
pyo3::exceptions::PyOSError::new_err(err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyfunction]
|
||||
fn result_conversion_function() -> Result<(), MyError> {
|
||||
Err(MyError {
|
||||
descr: "something went wrong",
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_conversion() {
|
||||
Python::attach(|py| {
|
||||
wrap_pyfunction!(result_conversion_function)(py).unwrap();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user