diff --git a/src/py_class/members.rs b/src/py_class/members.rs index b7185baa..6d56a75a 100644 --- a/src/py_class/members.rs +++ b/src/py_class/members.rs @@ -107,6 +107,56 @@ impl TypeMember for InstanceMethodDescriptor where T: PythonObject { } } +#[macro_export] +#[doc(hidden)] +macro_rules! py_class_class_method { + ($py:ident, $class:ident :: $f:ident [ $( { $pname:ident : $ptype:ty = $detail:tt } )* ]) => {{ + unsafe extern "C" fn wrap_class_method( + cls: *mut $crate::_detail::ffi::PyObject, + args: *mut $crate::_detail::ffi::PyObject, + kwargs: *mut $crate::_detail::ffi::PyObject) + -> *mut $crate::_detail::ffi::PyObject + { + const LOCATION: &'static str = concat!(stringify!($class), ".", stringify!($f), "()"); + $crate::_detail::handle_callback( + LOCATION, + |py| { + py_argparse_raw!(py, Some(LOCATION), args, kwargs, + [ $( { $pname : $ptype = $detail } )* ] + { + let cls = $crate::PyObject::from_borrowed_ptr(py, cls).unchecked_cast_into::<$crate::PyType>(); + let ret = $class::$f(&cls, py $(, $pname )* ); + $crate::PyDrop::release_ref(cls, py); + ret + }) + }) + } + unsafe { + let method_def = py_method_def!(stringify!($f), + $crate::_detail::ffi::METH_CLASS, + wrap_class_method::<()>); + $crate::py_class::members::create_class_method_descriptor(method_def) + } + }} +} + +pub struct ClassMethodDescriptor(*mut ffi::PyMethodDef); + +#[inline] +pub unsafe fn create_class_method_descriptor(method_def: *mut ffi::PyMethodDef) + -> ClassMethodDescriptor +{ + ClassMethodDescriptor(method_def) +} + +impl TypeMember for ClassMethodDescriptor where T: PythonObject { + #[inline] + unsafe fn into_descriptor(self, py: Python, ty: *mut ffi::PyTypeObject) -> PyResult { + err::result_from_owned_ptr(py, ffi::PyDescr_NewClassMethod(ty, self.0)) + } +} + + #[macro_export] #[doc(hidden)] macro_rules! py_class_static_method { @@ -129,8 +179,10 @@ macro_rules! py_class_static_method { }) } unsafe { - $crate::_detail::py_fn_impl($py, - py_method_def!(stringify!($f), 0, wrap_static_method::<()>)) + let method_def = py_method_def!(stringify!($f), + $crate::_detail::ffi::METH_STATIC, + wrap_static_method::<()>); + $crate::_detail::py_fn_impl($py, method_def) } }} } diff --git a/src/py_class/py_class.rs b/src/py_class/py_class.rs index d137535d..94c5853d 100644 --- a/src/py_class/py_class.rs +++ b/src/py_class/py_class.rs @@ -128,15 +128,35 @@ Declares an instance method callable from Python. * For details on `parameter-list`, see the documentation of `py_argparse!()`. * The return type must be `PyResult` for some `T` that implements `ToPyObject`. +## Class methods +`@classmethod def method_name(cls, parameter-list) -> PyResult<...> { ... } + +Declares a class method callable from Python. + +* The first parameter is the type object of the class on which the method is called. + This may be the type object of a derived class. +* The first parameter implicitly has type `&PyType`. This type must not be explicitly specified. +* For details on `parameter-list`, see the documentation of `py_argparse!()`. +* The return type must be `PyResult` for some `T` that implements `ToPyObject`. + +## Static methods +`@staticmethod def method_name(parameter-list) -> PyResult<...> { ... } + +Declares a static method callable from Python. + +* For details on `parameter-list`, see the documentation of `py_argparse!()`. +* The return type must be `PyResult` for some `T` that implements `ToPyObject`. + ## __new__ `def __new__(cls, parameter-list) -> PyResult<...> { ... }` Declares a constructor method callable from Python. -* The first parameter is the type object of the class to create. - This may be the type object of a derived class declared in Python. * If no `__new__` method is declared, object instances can only be created from Rust (via `MyType::create_instance`), but not from Python. +* The first parameter is the type object of the class to create. + This may be the type object of a derived class declared in Python. +* The first parameter implicitly has type `&PyType`. This type must not be explicitly specified. * For details on `parameter-list`, see the documentation of `py_argparse!()`. * The return type must be `PyResult` for some `T` that implements `ToPyObject`. Usually, `T` will be `MyType`. @@ -993,6 +1013,49 @@ macro_rules! py_class_impl { $($tail)* }}; + // @classmethod def class_method(cls) + { $class:ident $py:ident $info:tt $slots:tt + { $( $imp:item )* } + { $( $member_name:ident = $member_expr:expr; )* }; + @classmethod def $name:ident ($cls:ident) + -> $res_type:ty { $( $body:tt )* } $($tail:tt)* + } => { py_class_impl! { + $class $py $info $slots + /* impl: */ { + $($imp)* + py_class_impl_item! { $class, $py, $name($cls: &$crate::PyType,) $res_type; { $($body)* } [] } + } + /* members: */ { + $( $member_name:ident = $member_expr:expr; )* + $name = py_class_class_method!{$py, $class::$name []}; + }; + $($tail)* + }}; + // @classmethod def class_method(cls, params) + { $class:ident $py:ident $info:tt $slots:tt + { $( $imp:item )* } + { $( $member_name:ident = $member_expr:expr; )* }; + @classmethod def $name:ident ($cls:ident, $($p:tt)+) + -> $res_type:ty { $( $body:tt )* } $($tail:tt)* + } => { py_class_impl! { + $class $py $info $slots + /* impl: */ { + $($imp)* + py_argparse_parse_plist_impl!{ + py_class_impl_item { $class, $py, $name($cls: &$crate::PyType,) $res_type; { $($body)* } } + [] ($($p)+,) + } + } + /* members: */ { + $( $member_name:ident = $member_expr:expr; )* + $name = py_argparse_parse_plist_impl!{ + py_class_class_method {$py, $class::$name} + [] ($($p)+,) + }; + }; + $($tail)* + }}; + // @staticmethod def static_method(params) { $class:ident $py:ident $info:tt $slots:tt { $( $imp:item )* } diff --git a/tests/test_class.rs b/tests/test_class.rs index 6df5837b..02d8ed92 100644 --- a/tests/test_class.rs +++ b/tests/test_class.rs @@ -137,7 +137,50 @@ fn instance_method_with_args() { py.run("assert obj.method(multiplier=6) == 42", None, Some(&d)).unwrap(); } +py_class!(class ClassMethod |py| { + def __new__(cls) -> PyResult { + ClassMethod::create_instance(py) + } + + @classmethod + def method(cls) -> PyResult { + Ok(format!("{}.method()!", cls.name(py))) + } +}); + +#[test] +fn class_method() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = PyDict::new(py); + d.set_item(py, "C", py.get_type::()).unwrap(); + py.run("assert C.method() == 'ClassMethod.method()!'", None, Some(&d)).unwrap(); + py.run("assert C().method() == 'ClassMethod.method()!'", None, Some(&d)).unwrap(); +} + +py_class!(class ClassMethodWithArgs |py| { + @classmethod + def method(cls, input: &str) -> PyResult { + Ok(format!("{}.method({})", cls.name(py), input)) + } +}); + +#[test] +fn class_method_with_args() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = PyDict::new(py); + d.set_item(py, "C", py.get_type::()).unwrap(); + py.run("assert C.method('abc') == 'ClassMethodWithArgs.method(abc)'", None, Some(&d)).unwrap(); +} + py_class!(class StaticMethod |py| { + def __new__(cls) -> PyResult { + StaticMethod::create_instance(py) + } + @staticmethod def method() -> PyResult<&'static str> { Ok("StaticMethod.method()!") @@ -153,6 +196,7 @@ fn static_method() { let d = PyDict::new(py); d.set_item(py, "C", py.get_type::()).unwrap(); py.run("assert C.method() == 'StaticMethod.method()!'", None, Some(&d)).unwrap(); + py.run("assert C().method() == 'StaticMethod.method()!'", None, Some(&d)).unwrap(); } py_class!(class StaticMethodWithArgs |py| {