From 18e0aa17e0a12286a9c6bc0e96369456549f6536 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 24 May 2021 07:48:22 +0100 Subject: [PATCH] pymodule: accept `#[pyo3(name = "...")]` option --- CHANGELOG.md | 1 + guide/src/module.md | 5 +- pyo3-macros-backend/src/deprecations.rs | 2 + pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros-backend/src/module.rs | 91 +++++++++++++++++++++---- pyo3-macros/src/lib.rs | 34 +++++++-- src/impl_/deprecations.rs | 6 ++ tests/test_module.rs | 17 ++++- tests/ui/deprecations.rs | 2 +- tests/ui/deprecations.stderr | 6 ++ 10 files changed, 142 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8825b1ea..f159f3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Reduce LLVM line counts to improve compilation times. [#1604](https://github.com/PyO3/pyo3/pull/1604) - Deprecate string-literal second argument to `#[pyfn(m, "name")]`. [#1610](https://github.com/PyO3/pyo3/pull/1610) - No longer call `PyEval_InitThreads()` in `#[pymodule]` init code. [#1630](https://github.com/PyO3/pyo3/pull/1630) +- Deprecate `#[pymodule(name)]` option in favor of `#[pyo3(name = "...")]`. [#1650](https://github.com/PyO3/pyo3/pull/1650) - Deprecate `#[text_signature = "..."]` attributes in favor of `#[pyo3(text_signature = "...")]`. [#1658](https://github.com/PyO3/pyo3/pull/1658) - Use `METH_FASTCALL` argument passing convention, when possible, to improve `#[pyfunction]` and method performance. [#1619](https://github.com/PyO3/pyo3/pull/1619), [#1660](https://github.com/PyO3/pyo3/pull/1660) diff --git a/guide/src/module.md b/guide/src/module.md index 9f97d972..4a8d9c1e 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -41,8 +41,9 @@ If the name of the module (the default being the function name) does not match t `.pyd` file, you will get an import error in Python with the following message: `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` -To import the module, either copy the shared library as described in [the README](https://github.com/PyO3/pyo3) -or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or +To import the module, either: + - copy the shared library as described in [Manual builds](building_and_distribution.html#manual-builds), or + - use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). ## Documentation diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index d365d176..7acf769c 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -4,6 +4,7 @@ use quote::{quote_spanned, ToTokens}; pub enum Deprecation { NameAttribute, PyfnNameArgument, + PyModuleNameArgument, TextSignatureAttribute, } @@ -12,6 +13,7 @@ impl Deprecation { let string = match self { Deprecation::NameAttribute => "NAME_ATTRIBUTE", Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", + Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT", Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE", }; syn::Ident::new(string, span) diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index e8645659..f42bd3b9 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -24,7 +24,7 @@ mod pymethod; mod pyproto; pub use from_pyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, py_init}; +pub use module::{process_functions_in_module, py_init, PyModuleOptions}; pub use pyclass::{build_py_class, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index e06bbed9..e4efe4fa 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,18 +1,70 @@ // Copyright (c) 2017-present PyO3 Project and Contributors //! Code generation for the function that initializes a python module and adds classes and function. -use crate::pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}; +use crate::{ + attributes::{self, take_pyo3_options}, + deprecations::Deprecations, + pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, +}; use crate::{ attributes::{is_attribute_ident, take_attributes, NameAttribute}, deprecations::Deprecation, }; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{parse::Parse, spanned::Spanned, token::Comma, Ident, Path}; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseStream}, + spanned::Spanned, + token::Comma, + Ident, Path, Result, +}; + +pub struct PyModuleOptions { + name: Option, + deprecations: Deprecations, +} + +impl PyModuleOptions { + pub fn from_pymodule_arg_and_attrs( + deprecated_pymodule_name_arg: Option, + attrs: &mut Vec, + ) -> Result { + let mut deprecations = Deprecations::new(); + if let Some(name) = &deprecated_pymodule_name_arg { + deprecations.push(Deprecation::PyModuleNameArgument, name.span()); + } + + let mut options: PyModuleOptions = PyModuleOptions { + name: deprecated_pymodule_name_arg, + deprecations, + }; + + for option in take_pyo3_options(attrs)? { + match option { + PyModulePyO3Option::Name(name) => options.set_name(name.0)?, + } + } + + Ok(options) + } + + fn set_name(&mut self, name: syn::Ident) -> Result<()> { + ensure_spanned!( + self.name.is_none(), + name.span() => "`name` may only be specified once" + ); + + self.name = Some(name); + Ok(()) + } +} /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { +pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: syn::LitStr) -> TokenStream { + let name = options.name.unwrap_or_else(|| fnname.unraw()); + let deprecations = options.deprecations; let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site()); assert!(doc.value().ends_with('\0')); @@ -27,6 +79,8 @@ pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { static DOC: &str = #doc; static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) }; + #deprecations + pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) }) } } @@ -36,21 +90,19 @@ pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream { pub fn process_functions_in_module(func: &mut syn::ItemFn) -> syn::Result<()> { let mut stmts: Vec = Vec::new(); - for stmt in func.block.stmts.iter_mut() { - if let syn::Stmt::Item(syn::Item::Fn(func)) = stmt { + for mut stmt in func.block.stmts.drain(..) { + if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { let module_name = pyfn_args.modname; let (ident, wrapped_function) = impl_wrap_pyfunction(func, pyfn_args.options)?; - let item: syn::ItemFn = syn::parse_quote! { - fn block_wrapper() { - #wrapped_function - #module_name.add_function(#ident(#module_name)?)?; - } + let statements: Vec = syn::parse_quote! { + #wrapped_function + #module_name.add_function(#ident(#module_name)?)?; }; - stmts.extend(item.block.stmts.into_iter()); + stmts.extend(statements); } }; - stmts.push(stmt.clone()); + stmts.push(stmt); } func.block.stmts = stmts; @@ -120,3 +172,18 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::name) { + input.parse().map(PyModulePyO3Option::Name) + } else { + Err(lookahead.error()) + } + } +} diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 1a21001f..50194ab1 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -9,23 +9,43 @@ use proc_macro::TokenStream; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods, build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, + PyFunctionOptions, PyModuleOptions, }; use quote::quote; use syn::parse_macro_input; /// A proc macro used to implement Python modules. /// -/// For more on creating Python modules -/// see the [module section of the guide](https://pyo3.rs/main/module.html). +/// The name of the module will be taken from the function name, unless `#[pyo3(name = "my_name")]` +/// is also annotated on the function to override the name. **Important**: the module name should +/// match the `lib.name` setting in `Cargo.toml`, so that Python is able to import the module +/// without needing a custom import loader. +/// +/// Functions annotated with `#[pymodule]` can also be annotated with the following: +/// +/// | Annotation | Description | +/// | :- | :- | +/// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. | +/// +/// For more on creating Python modules see the [module section of the guide][1]. +/// +/// [1]: https://pyo3.rs/main/module.html #[proc_macro_attribute] pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemFn); - let modname = if attr.is_empty() { - ast.sig.ident.clone() + let deprecated_pymodule_name_arg = if attr.is_empty() { + None } else { - parse_macro_input!(attr as syn::Ident) + Some(parse_macro_input!(attr as syn::Ident)) + }; + + let options = match PyModuleOptions::from_pymodule_arg_and_attrs( + deprecated_pymodule_name_arg, + &mut ast.attrs, + ) { + Ok(options) => options, + Err(e) => return e.to_compile_error().into(), }; if let Err(err) = process_functions_in_module(&mut ast) { @@ -37,7 +57,7 @@ pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream { Err(err) => return err.to_compile_error().into(), }; - let expanded = py_init(&ast.sig.ident, &modname, doc); + let expanded = py_init(&ast.sig.ident, options, doc); quote!( #ast diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index af1b7348..585b59dc 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -12,6 +12,12 @@ pub const NAME_ATTRIBUTE: () = (); )] pub const PYFN_NAME_ARGUMENT: () = (); +#[deprecated( + since = "0.14.0", + note = "use `#[pymodule] #[pyo3(name = \"...\")]` instead of `#[pymodule(...)]`" +)] +pub const PYMODULE_NAME_ARGUMENT: () = (); + #[deprecated( since = "0.14.0", note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`" diff --git a/tests/test_module.rs b/tests/test_module.rs index 55ed0459..ffb065b2 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -123,7 +123,8 @@ fn test_module_with_functions() { ); } -#[pymodule(other_name)] +#[pymodule] +#[pyo3(name = "other_name")] fn some_name(_: Python, m: &PyModule) -> PyResult<()> { m.add("other_name", "other_name")?; Ok(()) @@ -436,3 +437,17 @@ fn test_module_functions_with_module() { "m.pyfunction_with_pass_module_in_attribute() == 'module_with_functions_with_module'" ); } + +#[test] +#[allow(deprecated)] +fn test_module_with_deprecated_name() { + #[pymodule(custom_name)] + fn my_module(_py: Python, _m: &PyModule) -> PyResult<()> { + Ok(()) + } + + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(custom_name)(py); + py_assert!(py, m, "m.__name__ == 'custom_name'"); + }) +} diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 8dc86dae..ca4805a3 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -29,7 +29,7 @@ impl TestClass { #[text_signature = "()"] fn deprecated_name_pyfunction() { } -#[pymodule] +#[pymodule(deprecated_module_name)] fn my_module(_py: Python, m: &PyModule) -> PyResult<()> { #[pyfn(m, "some_name")] #[text_signature = "()"] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index e330c8df..c6db9544 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -58,6 +58,12 @@ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATT 35 | #[text_signature = "()"] | ^ +error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]` + --> $DIR/deprecations.rs:32:12 + | +32 | #[pymodule(deprecated_module_name)] + | ^^^^^^^^^^^^^^^^^^^^^^ + error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` --> $DIR/deprecations.rs:6:1 |