Skip to content

Commit

Permalink
pymodule: accept #[pyo3(name = "...")] option
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Jun 5, 2021
1 parent 106cf25 commit 4beb9bc
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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)

### Removed
- Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426)
Expand Down
5 changes: 3 additions & 2 deletions guide/src/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ use quote::{quote_spanned, ToTokens};
pub enum Deprecation {
NameAttribute,
PyfnNameArgument,
PyModuleNameArgument,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::NameAttribute => "NAME_ATTRIBUTE",
Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT",
Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT",
};
syn::Ident::new(string, span)
}
Expand Down
2 changes: 1 addition & 1 deletion pyo3-macros-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,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};
Expand Down
98 changes: 86 additions & 12 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,77 @@
// 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, get_pyo3_attributes},
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<syn::Ident>,
deprecations: Deprecations,
}

impl PyModuleOptions {
pub fn from_pymodule_arg_and_attrs(
deprecated_pymodule_name_arg: Option<syn::Ident>,
attrs: &mut Vec<syn::Attribute>,
) -> Result<Self> {
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,
};

take_attributes(attrs, |attr| {
if let Some(pyo3_attributes) = get_pyo3_attributes(attr)? {
for pyo3_attr in pyo3_attributes {
match pyo3_attr {
PyModulePyO3Option::Name(name) => options.set_name(name.0)?,
}
}
Ok(true)
} else {
Ok(false)
}
})?;

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());

quote! {
Expand All @@ -26,6 +85,8 @@ pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream {
static DOC: &str = concat!(#doc, "\0");
static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) };

#deprecations

pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) })
}
}
Expand All @@ -35,21 +96,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<syn::Stmt> = 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::Stmt> = 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;
Expand Down Expand Up @@ -119,3 +178,18 @@ fn get_pyfn_attr(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Option<PyFnArgs

Ok(pyfn_args)
}

enum PyModulePyO3Option {
Name(NameAttribute),
}

impl Parse for PyModulePyO3Option {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::name) {
input.parse().map(PyModulePyO3Option::Name)
} else {
Err(lookahead.error())
}
}
}
34 changes: 27 additions & 7 deletions pyo3-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/impl_/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ pub const NAME_ATTRIBUTE: () = ();
note = "use `#[pyfn(m)] #[pyo3(name = \"...\")]` instead of `#[pyfn(m, \"...\")]`"
)]
pub const PYFN_NAME_ARGUMENT: () = ();

#[deprecated(
since = "0.14.0",
note = "use `#[pymodule] #[pyo3(name = \"...\")]` instead of `#[pymodule(...)]`"
)]
pub const PYMODULE_NAME_ARGUMENT: () = ();
17 changes: 16 additions & 1 deletion tests/test_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,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(())
Expand Down Expand Up @@ -414,3 +415,17 @@ fn test_module_functions_with_module() {
== ('module_with_functions_with_module', 1, 2)"
);
}

#[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'");
})
}
2 changes: 1 addition & 1 deletion tests/ui/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl TestClass {
#[name = "foo"]
fn deprecated_name_pyfunction() { }

#[pymodule]
#[pymodule(deprecated_module_name)]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
#[pyfn(m, "some_name")]
fn deprecated_name_pyfn() { }
Expand Down
6 changes: 6 additions & 0 deletions tests/ui/deprecations.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT
|
30 | #[pyfn(m, "some_name")]
| ^^^^^^^^^^^

error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]`
--> $DIR/deprecations.rs:28:12
|
28 | #[pymodule(deprecated_module_name)]
| ^^^^^^^^^^^^^^^^^^^^^^

0 comments on commit 4beb9bc

Please sign in to comment.