From 62b23955e8f85f5f822d8e999da63b542de95c47 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 28 Dec 2021 01:25:01 +0000 Subject: [PATCH] macros: accept paths in wrap_x macros --- CHANGELOG.md | 4 +- Cargo.toml | 3 +- pyo3-macros-backend/src/lib.rs | 4 +- pyo3-macros-backend/src/module.rs | 26 ++++++---- pyo3-macros-backend/src/pyfunction.rs | 7 +-- pyo3-macros-backend/src/wrap.rs | 67 +++++++++++++++++++++++++ pyo3-macros/Cargo.toml | 1 + pyo3-macros/src/lib.rs | 54 +++++++++++++++----- pytests/pyo3-pytests/src/datetime.rs | 2 +- pytests/pyo3-pytests/src/dict_iter.rs | 2 +- pytests/pyo3-pytests/src/lib.rs | 30 ++++------- pytests/pyo3-pytests/src/misc.rs | 2 +- pytests/pyo3-pytests/src/objstore.rs | 2 +- pytests/pyo3-pytests/src/othermod.rs | 2 +- pytests/pyo3-pytests/src/path.rs | 2 +- pytests/pyo3-pytests/src/subclassing.rs | 2 +- src/derive_utils.rs | 53 +++++++++++++------ src/lib.rs | 6 ++- src/macros.rs | 32 ------------ src/prelude.rs | 5 +- tests/test_module.rs | 2 +- 21 files changed, 197 insertions(+), 111 deletions(-) create mode 100644 pyo3-macros-backend/src/wrap.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e58036e522..f85aad9643c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) -- Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004) - Drop support for Python 3.6, remove `abi3-py36` feature. [#2006](https://github.com/PyO3/pyo3/pull/2006) - `pyo3-build-config` no longer enables the `resolve-config` feature by default. [#2008](https://github.com/PyO3/pyo3/pull/2008) - Update `inventory` optional dependency to 0.2. [#2019](https://github.com/PyO3/pyo3/pull/2019) +- Drop `paste` dependency. [#2081](https://github.com/PyO3/pyo3/pull/2081) ### Added @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All PyO3 proc-macros except the deprecated `#[pyproto]` now accept a supplemental attribute `#[pyo3(crate = "some::path")]` that specifies where to find the `pyo3` crate, in case it has been renamed or is re-exported and not found at the crate root. [#2022](https://github.com/PyO3/pyo3/pull/2022) - Expose `pyo3-build-config` APIs for cross-compiling and Python configuration discovery for use in other projects. [#1996](https://github.com/PyO3/pyo3/pull/1996) +- Accept paths in `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081) ### Changed @@ -55,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix memory leak in `PyErr::into_value`. [#2026](https://github.com/PyO3/pyo3/pull/2026) - Fix clippy warning `needless-option-as-deref` in code generated by `#[pyfunction]` and `#[pymethods]`. [#2040](https://github.com/PyO3/pyo3/pull/2040) - Fix undefined behavior in `PySlice::indices`. [#2061](https://github.com/PyO3/pyo3/pull/2061) +- Use the Rust function path for `wrap_pymodule!` of a `#[pymodule]` with a `#[pyo3(name = "..")]` attribute, not the Python name. [#2081](https://github.com/PyO3/pyo3/pull/2081) ## [0.15.1] - 2021-11-19 diff --git a/Cargo.toml b/Cargo.toml index d1af9f08cd5..229bf90edce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ parking_lot = "0.11.0" # support crates for macros feature pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true } indoc = { version = "1.0.3", optional = true } -paste = { version = "1.0.6", optional = true } unindent = { version = "0.1.4", optional = true } # support crate for multiple-pymethods feature @@ -53,7 +52,7 @@ pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features = default = ["macros"] # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. -macros = ["pyo3-macros", "indoc", "paste", "unindent"] +macros = ["pyo3-macros", "indoc", "unindent"] # Enables multiple #[pymethods] per #[pyclass] multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"] diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index deb9f3fa38e..54df212f7a4 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,11 +22,13 @@ mod pyfunction; mod pyimpl; mod pymethod; mod pyproto; +mod wrap; pub use frompyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, py_init, PyModuleOptions}; +pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use pyproto::build_py_proto; pub use utils::get_doc; +pub use wrap::{wrap_pyfunction_impl, wrap_pymodule_impl, WrapPyFunctionArgs}; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 1e6fe24aef1..26c1414256e 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -7,6 +7,7 @@ use crate::{ }, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, utils::{get_pyo3_crate, PythonDoc}, + wrap::module_def_ident, }; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -15,7 +16,7 @@ use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, token::Comma, - Ident, Path, Result, + Ident, Path, Result, Visibility, }; #[derive(Default)] @@ -61,25 +62,32 @@ impl PyModuleOptions { /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> TokenStream { +pub fn pymodule_impl( + fnname: &Ident, + options: PyModuleOptions, + doc: PythonDoc, + visibility: &Visibility, +) -> TokenStream { let name = options.name.unwrap_or_else(|| fnname.unraw()); let krate = get_pyo3_crate(&options.krate); let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); + let module_def_name = module_def_ident(fnname); + quote! { #[no_mangle] #[allow(non_snake_case)] /// This autogenerated function is called by the python interpreter when importing /// the module. pub unsafe extern "C" fn #cb_name() -> *mut #krate::ffi::PyObject { - use #krate as _pyo3; - use _pyo3::derive_utils::ModuleDef; - static NAME: &str = concat!(stringify!(#name), "\0"); - static DOC: &str = #doc; - static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; - - _pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) + use #krate::{self as _pyo3, IntoPyPointer}; + _pyo3::callback::handle_panic(|_py| ::std::result::Result::Ok(#module_def_name.make_module(_py)?.into_ptr())) } + + #[doc(hidden)] + #visibility static #module_def_name: #krate::derive_utils::ModuleDef = unsafe { + #krate::derive_utils::ModuleDef::new(concat!(stringify!(#name), "\0"), #doc, #krate::derive_utils::ModuleInitializer(#fnname)) + }; } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 28577bfb597..3e6bd3f7a4f 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -9,6 +9,7 @@ use crate::{ method::{self, CallingConvention, FnArg}, pymethod::check_generic, utils::{self, ensure_not_async_fn, get_pyo3_crate}, + wrap::function_wrapper_ident, }; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; @@ -366,12 +367,6 @@ pub fn build_py_function( Ok(impl_wrap_pyfunction(ast, options)?.1) } -/// Coordinates the naming of a the add-function-to-python-module function -fn function_wrapper_ident(name: &Ident) -> Ident { - // Make sure this ident matches the one of wrap_pyfunction - format_ident!("__pyo3_get_function_{}", name) -} - /// Generates python wrapper over a function that allows adding it to a python module as a python /// function pub fn impl_wrap_pyfunction( diff --git a/pyo3-macros-backend/src/wrap.rs b/pyo3-macros-backend/src/wrap.rs new file mode 100644 index 00000000000..e76d9cb6629 --- /dev/null +++ b/pyo3-macros-backend/src/wrap.rs @@ -0,0 +1,67 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse::Parse, spanned::Spanned, Ident, Token}; + +pub struct WrapPyFunctionArgs { + function: syn::Path, + comma_and_arg: Option<(Token![,], syn::Expr)>, +} + +impl Parse for WrapPyFunctionArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let function = input.parse()?; + let comma_and_arg = if !input.is_empty() { + Some((input.parse()?, input.parse()?)) + } else { + None + }; + Ok(Self { + function, + comma_and_arg, + }) + } +} + +pub fn wrap_pyfunction_impl(args: WrapPyFunctionArgs) -> syn::Result { + let WrapPyFunctionArgs { + mut function, + comma_and_arg, + } = args; + let span = function.span(); + let last_segment = function + .segments + .last_mut() + .ok_or_else(|| err_spanned!(span => "expected non-empty path"))?; + + last_segment.ident = function_wrapper_ident(&last_segment.ident); + + let output = if let Some((_, arg)) = comma_and_arg { + quote! { #function(#arg) } + } else { + quote! { &|arg| #function(arg) } + }; + Ok(output) +} + +pub fn wrap_pymodule_impl(mut module_path: syn::Path) -> syn::Result { + let span = module_path.span(); + let last_segment = module_path + .segments + .last_mut() + .ok_or_else(|| err_spanned!(span => "expected non-empty path"))?; + + last_segment.ident = module_def_ident(&last_segment.ident); + + Ok(quote! { + + &|py| unsafe { #module_path.make_module(py).expect("failed to wrap pymodule") } + }) +} + +pub(crate) fn function_wrapper_ident(name: &Ident) -> Ident { + format_ident!("__pyo3_get_function_{}", name) +} + +pub(crate) fn module_def_ident(name: &Ident) -> Ident { + format_ident!("__PYO3_PYMODULE_DEF_{}", name.to_string().to_uppercase()) +} diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a7e4550c7af..6f55d41a6e1 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true multiple-pymethods = [] [dependencies] +proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "1", features = ["full", "extra-traits"] } pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.1" } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index a6f6725c6e5..f01fa3892ba 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -6,10 +6,12 @@ extern crate proc_macro; use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + build_py_proto, get_doc, process_functions_in_module, pymodule_impl, wrap_pyfunction_impl, + wrap_pymodule_impl, PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, + WrapPyFunctionArgs, }; use quote::quote; use syn::{parse::Nothing, parse_macro_input}; @@ -46,7 +48,7 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { let doc = get_doc(&ast.attrs, None); - let expanded = py_init(&ast.sig.ident, options, doc); + let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis); quote!( #ast @@ -68,7 +70,7 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemImpl); - let expanded = build_py_proto(&mut ast).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_py_proto(&mut ast).unwrap_or_compile_error(); quote!( #ast @@ -180,7 +182,7 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemFn); let options = parse_macro_input!(attr as PyFunctionOptions); - let expanded = build_py_function(&mut ast, options).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_py_function(&mut ast, options).unwrap_or_compile_error(); quote!( #ast @@ -192,21 +194,41 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_derive(FromPyObject, attributes(pyo3))] pub fn derive_from_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); - let expanded = build_derive_from_pyobject(&ast).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_derive_from_pyobject(&ast).unwrap_or_compile_error(); quote!( #expanded ) .into() } +/// Wraps a Rust function annotated with [`#[pyfunction]`](crate::pyfunction), turning it into a +/// [`PyCFunction`](crate::types::PyCFunction). +/// +/// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add +/// free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// information. +#[proc_macro] +pub fn wrap_pyfunction(input: TokenStream) -> TokenStream { + let args = parse_macro_input!(input as WrapPyFunctionArgs); + wrap_pyfunction_impl(args).unwrap_or_compile_error().into() +} + +/// Returns a function that takes a [`Python`](crate::python::Python) instance and returns a Python module. +/// +/// Use this together with [`#[pymodule]`](crate::pymodule) and [crate::types::PyModule::add_wrapped]. +#[proc_macro] +pub fn wrap_pymodule(input: TokenStream) -> TokenStream { + let path = parse_macro_input!(input as syn::Path); + wrap_pymodule_impl(path).unwrap_or_compile_error().into() +} + fn pyclass_impl( attrs: TokenStream, mut ast: syn::ItemStruct, methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); - let expanded = - build_py_class(&mut ast, &args, methods_type).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -221,8 +243,7 @@ fn pyclass_enum_impl( methods_type: PyClassMethodsType, ) -> TokenStream { let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args); - let expanded = - build_py_enum(&mut ast, &args, methods_type).unwrap_or_else(|e| e.into_compile_error()); + let expanded = build_py_enum(&mut ast, &args, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -233,8 +254,7 @@ fn pyclass_enum_impl( fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemImpl); - let expanded = - build_py_methods(&mut ast, methods_type).unwrap_or_else(|e| e.to_compile_error()); + let expanded = build_py_methods(&mut ast, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -250,3 +270,13 @@ fn methods_type() -> PyClassMethodsType { PyClassMethodsType::Specialization } } + +trait UnwrapOrCompileError { + fn unwrap_or_compile_error(self) -> TokenStream2; +} + +impl UnwrapOrCompileError for syn::Result { + fn unwrap_or_compile_error(self) -> TokenStream2 { + self.unwrap_or_else(|e| e.into_compile_error()) + } +} diff --git a/pytests/pyo3-pytests/src/datetime.rs b/pytests/pyo3-pytests/src/datetime.rs index 11e33d11954..7106987e94f 100644 --- a/pytests/pyo3-pytests/src/datetime.rs +++ b/pytests/pyo3-pytests/src/datetime.rs @@ -203,7 +203,7 @@ impl TzClass { } #[pymodule] -fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; m.add_function(wrap_pyfunction!(date_from_timestamp, m)?)?; diff --git a/pytests/pyo3-pytests/src/dict_iter.rs b/pytests/pyo3-pytests/src/dict_iter.rs index e030f688ca0..35f3ad8d6b0 100644 --- a/pytests/pyo3-pytests/src/dict_iter.rs +++ b/pytests/pyo3-pytests/src/dict_iter.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; #[pymodule] -fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/pytests/pyo3-pytests/src/lib.rs b/pytests/pyo3-pytests/src/lib.rs index 3501e84fc4c..b435e668db5 100644 --- a/pytests/pyo3-pytests/src/lib.rs +++ b/pytests/pyo3-pytests/src/lib.rs @@ -12,31 +12,19 @@ pub mod path; pub mod pyclass_iter; pub mod subclassing; -#[cfg(not(Py_LIMITED_API))] -use buf_and_str::*; -#[cfg(not(Py_LIMITED_API))] -use datetime::*; -use dict_iter::*; -use misc::*; -use objstore::*; -use othermod::*; -use path::*; -use pyclass_iter::*; -use subclassing::*; - #[pymodule] fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> { #[cfg(not(Py_LIMITED_API))] - m.add_wrapped(wrap_pymodule!(buf_and_str))?; + m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; #[cfg(not(Py_LIMITED_API))] - m.add_wrapped(wrap_pymodule!(datetime))?; - m.add_wrapped(wrap_pymodule!(dict_iter))?; - m.add_wrapped(wrap_pymodule!(misc))?; - m.add_wrapped(wrap_pymodule!(objstore))?; - m.add_wrapped(wrap_pymodule!(othermod))?; - m.add_wrapped(wrap_pymodule!(path))?; - m.add_wrapped(wrap_pymodule!(pyclass_iter))?; - m.add_wrapped(wrap_pymodule!(subclassing))?; + m.add_wrapped(wrap_pymodule!(datetime::datetime))?; + m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; + m.add_wrapped(wrap_pymodule!(misc::misc))?; + m.add_wrapped(wrap_pymodule!(objstore::objstore))?; + m.add_wrapped(wrap_pymodule!(othermod::othermod))?; + m.add_wrapped(wrap_pymodule!(path::path))?; + m.add_wrapped(wrap_pymodule!(pyclass_iter::pyclass_iter))?; + m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas diff --git a/pytests/pyo3-pytests/src/misc.rs b/pytests/pyo3-pytests/src/misc.rs index 59dc63b669e..652f3e82cc0 100644 --- a/pytests/pyo3-pytests/src/misc.rs +++ b/pytests/pyo3-pytests/src/misc.rs @@ -8,7 +8,7 @@ fn issue_219() { } #[pymodule] -fn misc(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn misc(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; Ok(()) } diff --git a/pytests/pyo3-pytests/src/objstore.rs b/pytests/pyo3-pytests/src/objstore.rs index 5bf0a744ee4..d68149c7475 100644 --- a/pytests/pyo3-pytests/src/objstore.rs +++ b/pytests/pyo3-pytests/src/objstore.rs @@ -19,6 +19,6 @@ impl ObjStore { } #[pymodule] -fn objstore(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn objstore(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::() } diff --git a/pytests/pyo3-pytests/src/othermod.rs b/pytests/pyo3-pytests/src/othermod.rs index ba6319fe6d9..e439cacff79 100644 --- a/pytests/pyo3-pytests/src/othermod.rs +++ b/pytests/pyo3-pytests/src/othermod.rs @@ -29,7 +29,7 @@ fn double(x: i32) -> i32 { } #[pymodule] -fn othermod(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn othermod(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; m.add_class::()?; diff --git a/pytests/pyo3-pytests/src/path.rs b/pytests/pyo3-pytests/src/path.rs index 3f2a22fc8b7..b3e8f92bacf 100644 --- a/pytests/pyo3-pytests/src/path.rs +++ b/pytests/pyo3-pytests/src/path.rs @@ -12,7 +12,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { } #[pymodule] -fn path(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn path(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/pyo3-pytests/src/subclassing.rs b/pytests/pyo3-pytests/src/subclassing.rs index 20e7d431cdb..d54bbc4d972 100644 --- a/pytests/pyo3-pytests/src/subclassing.rs +++ b/pytests/pyo3-pytests/src/subclassing.rs @@ -21,7 +21,7 @@ impl pyo3::PyObjectProtocol for Subclassable { } #[pymodule] -fn subclassing(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn subclassing(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/src/derive_utils.rs b/src/derive_utils.rs index ab4e6a0ce69..39951579dde 100644 --- a/src/derive_utils.rs +++ b/src/derive_utils.rs @@ -4,12 +4,14 @@ //! Functionality for the code generated by the derive backend +use crate::callback::handle_panic; use crate::err::{PyErr, PyResult}; use crate::exceptions::PyTypeError; use crate::pyclass::PyClass; use crate::types::{PyAny, PyDict, PyModule, PyString, PyTuple}; -use crate::{ffi, PyCell, Python}; +use crate::{ffi, IntoPyPointer, Py, PyCell, PyObject, Python}; use std::cell::UnsafeCell; +use std::panic::AssertUnwindSafe; #[derive(Debug)] pub struct KeywordOnlyParameterDescription { @@ -298,16 +300,27 @@ impl FunctionDescription { } /// `Sync` wrapper of `ffi::PyModuleDef`. -pub struct ModuleDef(UnsafeCell); +pub struct ModuleDef { + // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability + ffi_def: UnsafeCell, + initializer: ModuleInitializer, +} + +/// Wrapper to enable initializer to be used in const fns. +pub struct ModuleInitializer(pub fn(Python, &PyModule) -> PyResult<()>); unsafe impl Sync for ModuleDef {} impl ModuleDef { - /// Make new module defenition with given module name. + /// Make new module definition with given module name. /// /// # Safety /// `name` and `doc` must be null-terminated strings. - pub const unsafe fn new(name: &'static str, doc: &'static str) -> Self { + pub const unsafe fn new( + name: &'static str, + doc: &'static str, + initializer: ModuleInitializer, + ) -> Self { const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, m_name: std::ptr::null(), @@ -320,22 +333,32 @@ impl ModuleDef { m_free: None, }; - ModuleDef(UnsafeCell::new(ffi::PyModuleDef { + let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr() as *const _, m_doc: doc.as_ptr() as *const _, ..INIT - })) + }); + + ModuleDef { + ffi_def, + initializer, + } } /// Builds a module using user given initializer. Used for `#[pymodule]`. - pub fn make_module( - &'static self, - py: Python, - initializer: impl Fn(Python, &PyModule) -> PyResult<()>, - ) -> PyResult<*mut ffi::PyObject> { - let module = - unsafe { py.from_owned_ptr_or_err::(ffi::PyModule_Create(self.0.get()))? }; - initializer(py, module)?; - Ok(crate::IntoPyPointer::into_ptr(module)) + pub fn make_module(&'static self, py: Python) -> PyResult { + let module = unsafe { + Py::::from_owned_ptr_or_err(py, ffi::PyModule_Create(self.ffi_def.get()))? + }; + (self.initializer.0)(py, module.as_ref(py))?; + Ok(module.into()) + } + /// Implementation of `PyInit_foo` functions generated in #[pymodule]. + /// + /// # Safety + /// The Python GIL must be held. + pub unsafe fn module_init(&'static self) -> *mut ffi::PyObject { + let unwind_safe_self = AssertUnwindSafe(self); + handle_panic(|py| Ok(unwind_safe_self.make_module(py)?.into_ptr())) } } diff --git a/src/lib.rs b/src/lib.rs index de5cdb683f5..85e39825463 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,7 +304,6 @@ pub use crate::types::PyAny; #[doc(hidden)] pub use { indoc, // Re-exported for py_run - paste, // Re-exported for wrap_function unindent, // Re-exported for py_run }; @@ -356,8 +355,11 @@ pub mod proc_macro { } #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, pyproto, FromPyObject}; +pub use pyo3_macros::{ + pyclass, pyfunction, pymethods, pymodule, pyproto, wrap_pyfunction, wrap_pymodule, FromPyObject, +}; +#[cfg(feature = "macros")] #[macro_use] mod macros; diff --git a/src/macros.rs b/src/macros.rs index a2bccab50f3..fafd738625f 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,33 +1,3 @@ -/// Wraps a Rust function annotated with [`#[pyfunction]`](crate::pyfunction), turning it into a -/// [`PyCFunction`](crate::types::PyCFunction). -/// -/// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add -/// free functions to a [`PyModule`](crate::types::PyModule) - see its documention for more information. -#[macro_export] -macro_rules! wrap_pyfunction { - ($function_name: ident) => {{ - &|py| $crate::paste::expr! { [<__pyo3_get_function_ $function_name>] }(py) - }}; - - ($function_name: ident, $arg: expr) => { - $crate::wrap_pyfunction!($function_name)( - <$crate::derive_utils::PyFunctionArguments as ::std::convert::From<_>>::from($arg), - ) - }; -} - -/// Returns a function that takes a [`Python`](crate::python::Python) instance and returns a Python module. -/// -/// Use this together with [`#[pymodule]`](crate::pymodule) and [crate::types::PyModule::add_wrapped]. -#[macro_export] -macro_rules! wrap_pymodule { - ($module_name:ident) => {{ - $crate::paste::expr! { - &|py| unsafe { $crate::PyObject::from_owned_ptr(py, []()) } - } - }}; -} - /// A convenient macro to execute a Python code snippet, with some local variables set. /// /// # Panics @@ -108,7 +78,6 @@ macro_rules! wrap_pymodule { /// }); /// ``` #[macro_export] -#[cfg(feature = "macros")] macro_rules! py_run { ($py:expr, $($val:ident)+, $code:literal) => {{ $crate::py_run_impl!($py, $($val)+, $crate::indoc::indoc!($code)) @@ -126,7 +95,6 @@ macro_rules! py_run { #[macro_export] #[doc(hidden)] -#[cfg(feature = "macros")] macro_rules! py_run_impl { ($py:expr, $($val:ident)+, $code:expr) => {{ use $crate::types::IntoPyDict; diff --git a/src/prelude.rs b/src/prelude.rs index a5c265c6fc8..52f79ac6dbb 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -20,7 +20,8 @@ pub use crate::pycell::{PyCell, PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; pub use crate::python::Python; pub use crate::types::{PyAny, PyModule}; -pub use crate::wrap_pyfunction; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, pyproto, FromPyObject}; +pub use pyo3_macros::{ + pyclass, pyfunction, pymethods, pymodule, pyproto, wrap_pyfunction, FromPyObject, +}; diff --git a/tests/test_module.rs b/tests/test_module.rs index e959fc13c20..6bb849b08ee 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -128,7 +128,7 @@ fn test_module_renaming() { let gil = Python::acquire_gil(); let py = gil.python(); - let d = [("different_name", wrap_pymodule!(other_name)(py))].into_py_dict(py); + let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict(py); py_run!(py, *d, "assert different_name.__name__ == 'other_name'"); }