Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pymodule: accept #[pyo3(name = "...")] option #1650

Merged
merged 1 commit into from
Jun 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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,6 +4,7 @@ use quote::{quote_spanned, ToTokens};
pub enum Deprecation {
NameAttribute,
PyfnNameArgument,
PyModuleNameArgument,
TextSignatureAttribute,
}

Expand All @@ -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)
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 @@ -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};
Expand Down
91 changes: 79 additions & 12 deletions pyo3-macros-backend/src/module.rs
Original file line number Diff line number Diff line change
@@ -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<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,
};

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

Expand All @@ -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) })
}
}
Expand All @@ -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<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 @@ -120,3 +172,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 @@ -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 = \"...\"]`"
Expand Down
17 changes: 16 additions & 1 deletion tests/test_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down Expand Up @@ -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'");
})
}
2 changes: 1 addition & 1 deletion tests/ui/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "()"]
Expand Down
6 changes: 6 additions & 0 deletions tests/ui/deprecations.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down