From 78e4e4d3c445c4a0ee468e9d1d539f6ae605c79a Mon Sep 17 00:00:00 2001 From: Techcable Date: Mon, 9 Aug 2021 12:34:05 -0700 Subject: [PATCH 1/4] Add support for the number protocol NOTE: We now get warnings because of the modularization of the macro system specified in RFC rust-lang/rfcs#1561 However, fixing these warnings is out of scope for this PR. --- src/lib.rs | 7 +- src/objectprotocol.rs | 4 + src/objectprotocol/number.rs | 323 +++++++++++++++++++++++++++++++++ src/py_class/py_class_impl.py | 2 +- src/py_class/py_class_impl3.rs | 28 ++- 5 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 src/objectprotocol/number.rs diff --git a/src/lib.rs b/src/lib.rs index 80e4274..9de6b0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,10 @@ clippy::manual_strip, clippy::match_like_matches_macro )] +#![warn( + // TODO: We need to fix this + macro_expanded_macro_exports_accessed_by_absolute_paths, +)] //! Rust bindings to the Python interpreter. //! @@ -210,13 +214,14 @@ pub mod buffer; mod conversion; mod err; mod function; -mod objectprotocol; mod objects; mod python; mod pythonrun; //pub mod rustobject; +#[macro_use] pub mod py_class; mod sharedref; +mod objectprotocol; #[cfg(feature = "serde-convert")] pub mod serde; diff --git a/src/objectprotocol.rs b/src/objectprotocol.rs index 385581d..2b2e62b 100644 --- a/src/objectprotocol.rs +++ b/src/objectprotocol.rs @@ -25,6 +25,10 @@ use crate::ffi; use crate::objects::{PyDict, PyObject, PyString, PyTuple}; use crate::python::{Python, PythonObject, ToPythonPointer}; +mod number; + +pub use self::number::NumberProtocol; + /// Trait that contains methods pub trait ObjectProtocol: PythonObject { /// Determines whether this object has the given attribute. diff --git a/src/objectprotocol/number.rs b/src/objectprotocol/number.rs new file mode 100644 index 0000000..e5389cc --- /dev/null +++ b/src/objectprotocol/number.rs @@ -0,0 +1,323 @@ +use std::cmp::Ordering; +use std::fmt; + +use crate::conversion::ToPyObject; +use crate::err::{self, PyErr, PyResult}; +use crate::ffi; +use crate::objects::{PyObject, PyInt, PyLong, PyFloat}; +use crate::python::{Python, PythonObject, ToPythonPointer}; + + +use super::ObjectProtocol; + +/// Operations on numeric objects +pub trait NumberProtocol: ObjectProtocol { + /// Perform addition (self + other) + /// + /// Invokes the `__add__` magic-method + #[inline] + fn add(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Add(self.as_ptr(), other)) + }) + } + /// Perform subtraction (self - other) + /// + /// Invokes the `__sub__` magic-method + #[inline] + fn subtract(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Subtract(self.as_ptr(), other)) + }) + } + /// Perform multiplication (self * other) + /// + /// Invokes the `__mul__` magic-method + #[inline] + fn multiply(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Multiply(self.as_ptr(), other)) + }) + } + /// Perform matrix multiplication, equivalent to the Python expression `self @ other` + /// + /// Invokes the `__matmul__` magic-method + /// + /// This was added in Python 3.5, and will unconditionally + /// throw an exception on any version before that. + /// + /// See [PEP 0456](https://www.python.org/dev/peps/pep-0465/) for details. + #[inline] + fn matrix_multiply(&self, py: Python, other: impl ToPyObject) -> PyResult { + #[cfg(not(any(feature = "python3-4", feature = "python2-sys")))] { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_MatrixMultiply(self.as_ptr(), other)) + }) + } + #[cfg(any(feature = "python3-4", feature = "python2-sys"))] { + + drop(other); + Err(crate::PyErr::new::( + py, + "Matrix multiplication is unsupported before Python 3.5" + )) + } + } + /// Perform exponentiation, equivalent to the Python expression `self ** other`, + /// or the two-argument form of the builtin method pow: `pow(self, other)` + /// + /// Invokes the `__pow__` magic-method + /// + /// See also [NumberProtocol::pow_mod]. + #[inline] + fn pow(&self, py: Python, other: impl ToPyObject) -> PyResult { + self.pow_mod(py, other, py.None()) + } + /// Perform exponentiation modulo an integer, + /// mathematically equivalent to `self ** other % mod` + /// but computed much more efficiently. + /// + /// Equivalent to invoking the three-argument form + /// of the builtin `pow` method: `pow(self, other, z)` + /// + /// Invoking with a `None` for modulo is equivalent to + /// the regular power operation. + /// + /// Invokes the `__pow__` magic-method + #[inline] + fn pow_mod(&self, py: Python, exp: impl ToPyObject, z: impl ToPyObject) -> PyResult { + exp.with_borrowed_ptr(py, |exp| { + z.with_borrowed_ptr(py, |z| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Power(self.as_ptr(), exp, z)) + }) + }) + } + /// Perform the "true division" operation, + /// equivalent to the Python expression `self / other`, + /// + /// Invokes the `__truediv__` magic-method. + #[inline] + fn true_div(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_TrueDivide(self.as_ptr(), other)) + }) + } + /// Perform the "floor division" operation, + /// equivalent to the Python expression `self // other`, + /// + /// This method was added in Python 3. + /// If compiling against Python 2, it unconditional throws an error. + /// + /// Invokes the `__floordiv__` magic-method. + #[inline] + fn div_floor(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_TrueDivide(self.as_ptr(), other)) + }) + } + /// Return the remainder of dividing `self` by `other`, + /// equivalent to the Python expression `self % other` + /// + /// Invokes the `__mod__` magic-method. + #[inline] + fn remainder(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Remainder(self.as_ptr(), other)) + }) + } + /// Perform combined division and modulo, + /// equivalent to the builtin method `divmod(self, other)` + /// + /// Invokes the `__divmod__` magic-method. + #[inline] + fn div_mod(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Divmod(self.as_ptr(), other)) + }) + } + /// Perform the negation of self (-self) + /// + /// Invokes the `__neg__` magic-method. + #[inline] + fn negative(&self, py: Python) -> PyResult { + unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Negative(self.as_ptr())) + } + } + /// Invoke the 'positive' operation, equivalent to the + /// Python expression `+self` + /// + /// Invokes the `__pos__` magic-method + #[inline] + fn positive(&self, py: Python) -> PyResult { + unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Positive(self.as_ptr())) + } + } + /// Return the absolute value of self, + /// equivalent to calling the builtin function `abs` + /// + /// Invokes the `__abs__` magic-method. + #[inline] + fn absolute(&self, py: Python) -> PyResult { + unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Absolute(self.as_ptr())) + } + } + /// Perform the bitwise negation of self, + /// equivalent to the Python expression `~self` + /// + /// Invokes the `__invert__` magic-method + #[inline] + fn bitwise_invert(&self, py: Python) -> PyResult { + unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Invert(self.as_ptr())) + } + } + /// Shift this value to the left by the specified number of bits, + /// equivalent to the Python expression `self << bits` + /// + /// Invokes the `__lshift__` magic-method + #[inline] + fn left_shift(&self, py: Python, bits: impl ToPyObject) -> PyResult { + bits.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Lshift(self.as_ptr(), other)) + }) + } + /// Shift this value to the right by the specified number of bits, + /// equivalent to the Python expression `self >> bits` + /// + /// Invokes the `__rshift__` magic-method + #[inline] + fn right_shift(&self, py: Python, bits: impl ToPyObject) -> PyResult { + bits.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Rshift(self.as_ptr(), other)) + }) + } + /// Perform the "bitwise and" of `self & other` + /// + /// Invokes the `__and__` magic-method. + #[inline] + fn bitwise_and(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_And(self.as_ptr(), other)) + }) + } + /// Perform the "bitwise exclusive or", + /// equivalent to Python expression `self ^ other` + /// + /// Invokes the `__xor__` magic-method. + #[inline] + fn bitwise_xor(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Xor(self.as_ptr(), other)) + }) + } + /// Perform the "bitwise or" of `self | other` + /// + /// Invokes the `__or__` magic-method. + #[inline] + fn bitwise_or(&self, py: Python, other: impl ToPyObject) -> PyResult { + other.with_borrowed_ptr(py, |other| unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Or(self.as_ptr(), other)) + }) + } + /// Convert this object to an integer, + /// equivalent to the builtin function `int(self)` + /// + /// Invokes the `__int__` magic-method. + /// + /// Throws an exception if unable to perform + /// the conversion. + #[inline] + fn as_int(&self, py: Python) -> PyResult { + let obj = unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Long(self.as_ptr()))? + }; + Ok(obj.cast_into::(py)?) + } + /// Convert this object to a float, + /// equivalent to the builtin function `float(self)` + /// + /// Invokes the `__float__` magic-method. + /// + /// Throws an exception if unable to perform + /// the conversion. + #[inline] + fn as_float(&self, py: Python) -> PyResult { + let obj = unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Float(self.as_ptr()))? + }; + Ok(obj.cast_into::(py)?) + } + /// Losslessly convert this object to an integer index, + /// as if calling `operator.index()` + /// + /// The presence of this method indicates + /// this object is an integer type. + /// + /// Calls the `__index__` magic-method. + /// + /// See also: [Documentation on the corresponding magic-method](https://docs.python.org/3/reference/datamodel.html?highlight=__index__#object.__index__) + #[inline] + fn as_int_index(&self, py: Python) -> PyResult { + let obj = unsafe { + err::result_from_owned_ptr(py, ffi::PyNumber_Long(self.as_ptr()))? + }; + Ok(obj.cast_into::(py)?) + } +} + +impl NumberProtocol for PyObject {} + + +#[cfg(test)] +mod test { + use crate::*; + use super::*; + + #[test] + fn addition() { + let guard = Python::acquire_gil(); + let py = guard.python(); + let i1 = (5i32).to_py_object(py).into_object(); + let i2 = (12i32).to_py_object(py).into_object(); + let actual_res = i1.add(py, i2).unwrap(); + let expected_res = (17i32).to_py_object(py).into_object(); + assert_eq!( + actual_res.compare(py, expected_res).unwrap(), + Ordering::Equal + ); + } + + py_class!(class DummyMatMul |py| { + data number: i32; + def __new__(_cls, arg: i32) -> PyResult { + DummyMatMul::create_instance(py, arg) + } + def __matmul__(left, other) -> PyResult { + // Do a dummy operation that can be easily tested + left.cast_as::(py)?.number(py) + .to_py_object(py) + .into_object() + .multiply(py, other)? + .add(py, 3) + } + }); + + #[test] + #[cfg_attr(any(feature = "python3-4", feature = "python2-sys"), should_panic)] + fn matrix_multiply() { + let guard = Python::acquire_gil(); + let py = guard.python(); + let first = DummyMatMul::create_instance(py, 5).unwrap().into_object(); + let seven = (7i32).to_py_object(py).into_object(); + let actual_res = first.matrix_multiply(py, seven).unwrap(); + // 5 * 7 + 3 => 38 + let expected_res = (38i32).to_py_object(py).into_object(); + assert_eq!( + actual_res.compare(py, expected_res).unwrap(), + Ordering::Equal + ); + } +} diff --git a/src/py_class/py_class_impl.py b/src/py_class/py_class_impl.py index e68148d..280f397 100644 --- a/src/py_class/py_class_impl.py +++ b/src/py_class/py_class_impl.py @@ -838,7 +838,7 @@ def inplace_numeric_operator(special_name, slot): '__add__': binary_numeric_operator('nb_add'), '__sub__': binary_numeric_operator('nb_subtract'), '__mul__': binary_numeric_operator('nb_multiply'), - '__matmul__': unimplemented(), + '__matmul__': binary_numeric_operator('nb_matrix_multiply') if not PY2 else unimplemented(), '__div__': unimplemented(), '__truediv__': unimplemented(), '__floordiv__': unimplemented(), diff --git a/src/py_class/py_class_impl3.rs b/src/py_class/py_class_impl3.rs index 3b7fdc1..e07f5c9 100644 --- a/src/py_class/py_class_impl3.rs +++ b/src/py_class/py_class_impl3.rs @@ -2108,9 +2108,35 @@ macro_rules! py_class_impl { { { def __lt__ $($tail:tt)* } $( $stuff:tt )* } => { $crate::py_error! { "__lt__ is not supported by py_class! use __richcmp__ instead." } }; + { { def __matmul__($left:ident, $right:ident) -> $res_type:ty { $($body:tt)* } $($tail:tt)* } + $class:ident $py:ident $info:tt + /* slots: */ { + $type_slots:tt + /* as_number */ [ $( $nb_slot_name:ident : $nb_slot_value:expr, )* ] + $as_sequence:tt $as_mapping:tt $setdelitem:tt + } + { $( $imp:item )* } + $members:tt $props:tt + } => { $crate::py_class_impl! { + { $($tail)* } + $class $py $info + /* slots: */ { + $type_slots + /* as_number */ [ + $( $nb_slot_name : $nb_slot_value, )* + nb_matrix_multiply: $crate::py_class_binary_numeric_slot!($class::__matmul__), + ] + $as_sequence $as_mapping $setdelitem + } + /* impl: */ { + $($imp)* + $crate::py_class_impl_item! { $class, $py, pub, __matmul__() $res_type; { $($body)* } [ { $left : &$crate::PyObject = {} } { $right : &$crate::PyObject = {} } ] } + } + $members $props + }}; { { def __matmul__ $($tail:tt)* } $( $stuff:tt )* } => { - $crate::py_error! { "__matmul__ is not supported by py_class! yet." } + $crate::py_error! { "Invalid signature for binary numeric operator __matmul__" } }; { { def __mod__ $($tail:tt)* } $( $stuff:tt )* } => { From a7e2478f7fdd597856ff50eea7ac814d9187b699 Mon Sep 17 00:00:00 2001 From: Techcable Date: Mon, 9 Aug 2021 13:45:21 -0700 Subject: [PATCH 2/4] Re-export NumberProtocol from crate-root Whoopsies --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9de6b0a..5269c28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,7 +104,7 @@ pub use ffi::Py_ssize_t; pub use crate::conversion::{FromPyObject, RefFromPyObject, ToPyObject}; pub use crate::err::{PyErr, PyResult}; -pub use crate::objectprotocol::ObjectProtocol; +pub use crate::objectprotocol::{ObjectProtocol, NumberProtocol}; pub use crate::objects::*; pub use crate::py_class::CompareOp; pub use crate::python::{ From e5ec0e33fe768c4a6ae2b8e612483dd127d24cb3 Mon Sep 17 00:00:00 2001 From: Techcable Date: Fri, 13 Aug 2021 19:36:02 -0700 Subject: [PATCH 3/4] Fix two wrong method calls in the number protocol --- src/objectprotocol/number.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/objectprotocol/number.rs b/src/objectprotocol/number.rs index e5389cc..0e2508f 100644 --- a/src/objectprotocol/number.rs +++ b/src/objectprotocol/number.rs @@ -112,7 +112,7 @@ pub trait NumberProtocol: ObjectProtocol { #[inline] fn div_floor(&self, py: Python, other: impl ToPyObject) -> PyResult { other.with_borrowed_ptr(py, |other| unsafe { - err::result_from_owned_ptr(py, ffi::PyNumber_TrueDivide(self.as_ptr(), other)) + err::result_from_owned_ptr(py, ffi::PyNumber_FloorDivide(self.as_ptr(), other)) }) } /// Return the remainder of dividing `self` by `other`, @@ -262,7 +262,7 @@ pub trait NumberProtocol: ObjectProtocol { #[inline] fn as_int_index(&self, py: Python) -> PyResult { let obj = unsafe { - err::result_from_owned_ptr(py, ffi::PyNumber_Long(self.as_ptr()))? + err::result_from_owned_ptr(py, ffi::PyNumber_Index(self.as_ptr()))? }; Ok(obj.cast_into::(py)?) } From 830fbed632a03bd206cda62722cdd9d81587e7b7 Mon Sep 17 00:00:00 2001 From: Techcable Date: Fri, 13 Aug 2021 19:39:37 -0700 Subject: [PATCH 4/4] Name NumberProtocol naming more consistent Now it mostly matches the corresponding magic methods and CAPI calls. In particular the mismatch between 'div_mod' and 'div_floor' has been annoying me in my code ;) --- src/objectprotocol/number.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/objectprotocol/number.rs b/src/objectprotocol/number.rs index 0e2508f..47f318c 100644 --- a/src/objectprotocol/number.rs +++ b/src/objectprotocol/number.rs @@ -68,10 +68,10 @@ pub trait NumberProtocol: ObjectProtocol { /// /// Invokes the `__pow__` magic-method /// - /// See also [NumberProtocol::pow_mod]. + /// See also [NumberProtocol::power_modulo]. #[inline] - fn pow(&self, py: Python, other: impl ToPyObject) -> PyResult { - self.pow_mod(py, other, py.None()) + fn power(&self, py: Python, other: impl ToPyObject) -> PyResult { + self.power_modulo(py, other, py.None()) } /// Perform exponentiation modulo an integer, /// mathematically equivalent to `self ** other % mod` @@ -85,7 +85,7 @@ pub trait NumberProtocol: ObjectProtocol { /// /// Invokes the `__pow__` magic-method #[inline] - fn pow_mod(&self, py: Python, exp: impl ToPyObject, z: impl ToPyObject) -> PyResult { + fn power_modulo(&self, py: Python, exp: impl ToPyObject, z: impl ToPyObject) -> PyResult { exp.with_borrowed_ptr(py, |exp| { z.with_borrowed_ptr(py, |z| unsafe { err::result_from_owned_ptr(py, ffi::PyNumber_Power(self.as_ptr(), exp, z)) @@ -97,7 +97,7 @@ pub trait NumberProtocol: ObjectProtocol { /// /// Invokes the `__truediv__` magic-method. #[inline] - fn true_div(&self, py: Python, other: impl ToPyObject) -> PyResult { + fn true_divide(&self, py: Python, other: impl ToPyObject) -> PyResult { other.with_borrowed_ptr(py, |other| unsafe { err::result_from_owned_ptr(py, ffi::PyNumber_TrueDivide(self.as_ptr(), other)) }) @@ -110,7 +110,7 @@ pub trait NumberProtocol: ObjectProtocol { /// /// Invokes the `__floordiv__` magic-method. #[inline] - fn div_floor(&self, py: Python, other: impl ToPyObject) -> PyResult { + fn floor_divide(&self, py: Python, other: impl ToPyObject) -> PyResult { other.with_borrowed_ptr(py, |other| unsafe { err::result_from_owned_ptr(py, ffi::PyNumber_FloorDivide(self.as_ptr(), other)) }) @@ -120,7 +120,7 @@ pub trait NumberProtocol: ObjectProtocol { /// /// Invokes the `__mod__` magic-method. #[inline] - fn remainder(&self, py: Python, other: impl ToPyObject) -> PyResult { + fn modulo(&self, py: Python, other: impl ToPyObject) -> PyResult { other.with_borrowed_ptr(py, |other| unsafe { err::result_from_owned_ptr(py, ffi::PyNumber_Remainder(self.as_ptr(), other)) })