Skip to content

Commit 24eea5d

Browse files
authored
Merge pull request #2081 from davidhewitt/wrap-paths
macros: accept paths in wrap_x macros
2 parents 7c2c4da + ff37f24 commit 24eea5d

23 files changed

+309
-140
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
- Update MSRV to Rust 1.48. [#2004](https://github.com/PyO3/pyo3/pull/2004)
1414
- Update `indoc` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004)
15-
- Update `paste` optional dependency to 1.0. [#2004](https://github.com/PyO3/pyo3/pull/2004)
1615
- Drop support for Python 3.6, remove `abi3-py36` feature. [#2006](https://github.com/PyO3/pyo3/pull/2006)
1716
- `pyo3-build-config` no longer enables the `resolve-config` feature by default. [#2008](https://github.com/PyO3/pyo3/pull/2008)
1817
- Update `inventory` optional dependency to 0.2. [#2019](https://github.com/PyO3/pyo3/pull/2019)
18+
- Drop `paste` dependency. [#2081](https://github.com/PyO3/pyo3/pull/2081)
1919

2020
### Added
2121

@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- All PyO3 proc-macros except the deprecated `#[pyproto]` now accept a supplemental attribute `#[pyo3(crate = "some::path")]` that specifies
2525
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)
2626
- 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)
27+
- Accept paths in `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081)
2728

2829
### Changed
2930

@@ -55,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5556
- Fix memory leak in `PyErr::into_value`. [#2026](https://github.com/PyO3/pyo3/pull/2026)
5657
- Fix clippy warning `needless-option-as-deref` in code generated by `#[pyfunction]` and `#[pymethods]`. [#2040](https://github.com/PyO3/pyo3/pull/2040)
5758
- Fix undefined behavior in `PySlice::indices`. [#2061](https://github.com/PyO3/pyo3/pull/2061)
59+
- 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)
5860

5961
## [0.15.1] - 2021-11-19
6062

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ parking_lot = "0.11.0"
2222
# support crates for macros feature
2323
pyo3-macros = { path = "pyo3-macros", version = "=0.15.1", optional = true }
2424
indoc = { version = "1.0.3", optional = true }
25-
paste = { version = "1.0.6", optional = true }
2625
unindent = { version = "0.1.4", optional = true }
2726

2827
# support crate for multiple-pymethods feature
@@ -53,7 +52,7 @@ pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features =
5352
default = ["macros"]
5453

5554
# Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
56-
macros = ["pyo3-macros", "indoc", "paste", "unindent"]
55+
macros = ["pyo3-macros", "indoc", "unindent"]
5756

5857
# Enables multiple #[pymethods] per #[pyclass]
5958
multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"]

pyo3-macros-backend/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@ mod pyfunction;
2222
mod pyimpl;
2323
mod pymethod;
2424
mod pyproto;
25+
mod wrap;
2526

2627
pub use frompyobject::build_derive_from_pyobject;
27-
pub use module::{process_functions_in_module, py_init, PyModuleOptions};
28+
pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions};
2829
pub use pyclass::{build_py_class, build_py_enum, PyClassArgs};
2930
pub use pyfunction::{build_py_function, PyFunctionOptions};
3031
pub use pyimpl::{build_py_methods, PyClassMethodsType};
3132
pub use pyproto::build_py_proto;
3233
pub use utils::get_doc;
34+
pub use wrap::{wrap_pyfunction_impl, wrap_pymodule_impl, WrapPyFunctionArgs};

pyo3-macros-backend/src/module.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
},
88
pyfunction::{impl_wrap_pyfunction, PyFunctionOptions},
99
utils::{get_pyo3_crate, PythonDoc},
10+
wrap::module_def_ident,
1011
};
1112
use proc_macro2::{Span, TokenStream};
1213
use quote::quote;
@@ -15,7 +16,7 @@ use syn::{
1516
parse::{Parse, ParseStream},
1617
spanned::Spanned,
1718
token::Comma,
18-
Ident, Path, Result,
19+
Ident, Path, Result, Visibility,
1920
};
2021

2122
#[derive(Default)]
@@ -61,25 +62,32 @@ impl PyModuleOptions {
6162

6263
/// Generates the function that is called by the python interpreter to initialize the native
6364
/// module
64-
pub fn py_init(fnname: &Ident, options: PyModuleOptions, doc: PythonDoc) -> TokenStream {
65+
pub fn pymodule_impl(
66+
fnname: &Ident,
67+
options: PyModuleOptions,
68+
doc: PythonDoc,
69+
visibility: &Visibility,
70+
) -> TokenStream {
6571
let name = options.name.unwrap_or_else(|| fnname.unraw());
6672
let krate = get_pyo3_crate(&options.krate);
6773
let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site());
6874

75+
let module_def_name = module_def_ident(fnname);
76+
6977
quote! {
7078
#[no_mangle]
7179
#[allow(non_snake_case)]
7280
/// This autogenerated function is called by the python interpreter when importing
7381
/// the module.
7482
pub unsafe extern "C" fn #cb_name() -> *mut #krate::ffi::PyObject {
75-
use #krate as _pyo3;
76-
use _pyo3::derive_utils::ModuleDef;
77-
static NAME: &str = concat!(stringify!(#name), "\0");
78-
static DOC: &str = #doc;
79-
static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) };
80-
81-
_pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) })
83+
use #krate::{self as _pyo3, IntoPyPointer};
84+
_pyo3::callback::handle_panic(|_py| ::std::result::Result::Ok(#module_def_name.make_module(_py)?.into_ptr()))
8285
}
86+
87+
#[doc(hidden)]
88+
#visibility static #module_def_name: #krate::impl_::pymodule::ModuleDef = unsafe {
89+
#krate::impl_::pymodule::ModuleDef::new(concat!(stringify!(#name), "\0"), #doc, #krate::impl_::pymodule::ModuleInitializer(#fnname))
90+
};
8391
}
8492
}
8593

pyo3-macros-backend/src/pyfunction.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
method::{self, CallingConvention, FnArg},
1010
pymethod::check_generic,
1111
utils::{self, ensure_not_async_fn, get_pyo3_crate},
12+
wrap::function_wrapper_ident,
1213
};
1314
use proc_macro2::{Span, TokenStream};
1415
use quote::{format_ident, quote};
@@ -366,12 +367,6 @@ pub fn build_py_function(
366367
Ok(impl_wrap_pyfunction(ast, options)?.1)
367368
}
368369

369-
/// Coordinates the naming of a the add-function-to-python-module function
370-
fn function_wrapper_ident(name: &Ident) -> Ident {
371-
// Make sure this ident matches the one of wrap_pyfunction
372-
format_ident!("__pyo3_get_function_{}", name)
373-
}
374-
375370
/// Generates python wrapper over a function that allows adding it to a python module as a python
376371
/// function
377372
pub fn impl_wrap_pyfunction(

pyo3-macros-backend/src/wrap.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::{parse::Parse, spanned::Spanned, Ident, Token};
4+
5+
pub struct WrapPyFunctionArgs {
6+
function: syn::Path,
7+
comma_and_arg: Option<(Token![,], syn::Expr)>,
8+
}
9+
10+
impl Parse for WrapPyFunctionArgs {
11+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
12+
let function = input.parse()?;
13+
let comma_and_arg = if !input.is_empty() {
14+
Some((input.parse()?, input.parse()?))
15+
} else {
16+
None
17+
};
18+
Ok(Self {
19+
function,
20+
comma_and_arg,
21+
})
22+
}
23+
}
24+
25+
pub fn wrap_pyfunction_impl(args: WrapPyFunctionArgs) -> syn::Result<TokenStream> {
26+
let WrapPyFunctionArgs {
27+
mut function,
28+
comma_and_arg,
29+
} = args;
30+
let span = function.span();
31+
let last_segment = function
32+
.segments
33+
.last_mut()
34+
.ok_or_else(|| err_spanned!(span => "expected non-empty path"))?;
35+
36+
last_segment.ident = function_wrapper_ident(&last_segment.ident);
37+
38+
let output = if let Some((_, arg)) = comma_and_arg {
39+
quote! { #function(#arg) }
40+
} else {
41+
quote! { &|arg| #function(arg) }
42+
};
43+
Ok(output)
44+
}
45+
46+
pub fn wrap_pymodule_impl(mut module_path: syn::Path) -> syn::Result<TokenStream> {
47+
let span = module_path.span();
48+
let last_segment = module_path
49+
.segments
50+
.last_mut()
51+
.ok_or_else(|| err_spanned!(span => "expected non-empty path"))?;
52+
53+
last_segment.ident = module_def_ident(&last_segment.ident);
54+
55+
Ok(quote! {
56+
57+
&|py| unsafe { #module_path.make_module(py).expect("failed to wrap pymodule") }
58+
})
59+
}
60+
61+
pub(crate) fn function_wrapper_ident(name: &Ident) -> Ident {
62+
format_ident!("__pyo3_get_function_{}", name)
63+
}
64+
65+
pub(crate) fn module_def_ident(name: &Ident) -> Ident {
66+
format_ident!("__PYO3_PYMODULE_DEF_{}", name.to_string().to_uppercase())
67+
}

pyo3-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ proc-macro = true
1717
multiple-pymethods = []
1818

1919
[dependencies]
20+
proc-macro2 = { version = "1", default-features = false }
2021
quote = "1"
2122
syn = { version = "1", features = ["full", "extra-traits"] }
2223
pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.15.1" }

pyo3-macros/src/lib.rs

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
extern crate proc_macro;
77

88
use proc_macro::TokenStream;
9+
use proc_macro2::TokenStream as TokenStream2;
910
use pyo3_macros_backend::{
1011
build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods,
11-
build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType,
12-
PyFunctionOptions, PyModuleOptions,
12+
build_py_proto, get_doc, process_functions_in_module, pymodule_impl, wrap_pyfunction_impl,
13+
wrap_pymodule_impl, PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions,
14+
WrapPyFunctionArgs,
1315
};
1416
use quote::quote;
1517
use syn::{parse::Nothing, parse_macro_input};
@@ -46,7 +48,7 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
4648

4749
let doc = get_doc(&ast.attrs, None);
4850

49-
let expanded = py_init(&ast.sig.ident, options, doc);
51+
let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis);
5052

5153
quote!(
5254
#ast
@@ -68,7 +70,7 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream {
6870
#[proc_macro_attribute]
6971
pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
7072
let mut ast = parse_macro_input!(input as syn::ItemImpl);
71-
let expanded = build_py_proto(&mut ast).unwrap_or_else(|e| e.to_compile_error());
73+
let expanded = build_py_proto(&mut ast).unwrap_or_compile_error();
7274

7375
quote!(
7476
#ast
@@ -180,7 +182,7 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream {
180182
let mut ast = parse_macro_input!(input as syn::ItemFn);
181183
let options = parse_macro_input!(attr as PyFunctionOptions);
182184

183-
let expanded = build_py_function(&mut ast, options).unwrap_or_else(|e| e.to_compile_error());
185+
let expanded = build_py_function(&mut ast, options).unwrap_or_compile_error();
184186

185187
quote!(
186188
#ast
@@ -192,21 +194,39 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream {
192194
#[proc_macro_derive(FromPyObject, attributes(pyo3))]
193195
pub fn derive_from_py_object(item: TokenStream) -> TokenStream {
194196
let ast = parse_macro_input!(item as syn::DeriveInput);
195-
let expanded = build_derive_from_pyobject(&ast).unwrap_or_else(|e| e.to_compile_error());
197+
let expanded = build_derive_from_pyobject(&ast).unwrap_or_compile_error();
196198
quote!(
197199
#expanded
198200
)
199201
.into()
200202
}
201203

204+
/// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction).
205+
///
206+
/// This can be used with `PyModule::add_function` to add free functions to a `PyModule` - see its
207+
/// documentation for more information.
208+
#[proc_macro]
209+
pub fn wrap_pyfunction(input: TokenStream) -> TokenStream {
210+
let args = parse_macro_input!(input as WrapPyFunctionArgs);
211+
wrap_pyfunction_impl(args).unwrap_or_compile_error().into()
212+
}
213+
214+
/// Returns a function that takes a `Python` instance and returns a Python module.
215+
///
216+
/// Use this together with [`#[pymodule]`](macro@crate::pymodule) and `PyModule::add_wrapped`.
217+
#[proc_macro]
218+
pub fn wrap_pymodule(input: TokenStream) -> TokenStream {
219+
let path = parse_macro_input!(input as syn::Path);
220+
wrap_pymodule_impl(path).unwrap_or_compile_error().into()
221+
}
222+
202223
fn pyclass_impl(
203224
attrs: TokenStream,
204225
mut ast: syn::ItemStruct,
205226
methods_type: PyClassMethodsType,
206227
) -> TokenStream {
207228
let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args);
208-
let expanded =
209-
build_py_class(&mut ast, &args, methods_type).unwrap_or_else(|e| e.to_compile_error());
229+
let expanded = build_py_class(&mut ast, &args, methods_type).unwrap_or_compile_error();
210230

211231
quote!(
212232
#ast
@@ -221,8 +241,7 @@ fn pyclass_enum_impl(
221241
methods_type: PyClassMethodsType,
222242
) -> TokenStream {
223243
let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args);
224-
let expanded =
225-
build_py_enum(&mut ast, &args, methods_type).unwrap_or_else(|e| e.into_compile_error());
244+
let expanded = build_py_enum(&mut ast, &args, methods_type).unwrap_or_compile_error();
226245

227246
quote!(
228247
#ast
@@ -233,8 +252,7 @@ fn pyclass_enum_impl(
233252

234253
fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> TokenStream {
235254
let mut ast = parse_macro_input!(input as syn::ItemImpl);
236-
let expanded =
237-
build_py_methods(&mut ast, methods_type).unwrap_or_else(|e| e.to_compile_error());
255+
let expanded = build_py_methods(&mut ast, methods_type).unwrap_or_compile_error();
238256

239257
quote!(
240258
#ast
@@ -250,3 +268,13 @@ fn methods_type() -> PyClassMethodsType {
250268
PyClassMethodsType::Specialization
251269
}
252270
}
271+
272+
trait UnwrapOrCompileError {
273+
fn unwrap_or_compile_error(self) -> TokenStream2;
274+
}
275+
276+
impl UnwrapOrCompileError for syn::Result<TokenStream2> {
277+
fn unwrap_or_compile_error(self) -> TokenStream2 {
278+
self.unwrap_or_else(|e| e.into_compile_error())
279+
}
280+
}

pytests/pyo3-pytests/src/datetime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ impl TzClass {
203203
}
204204

205205
#[pymodule]
206-
fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
206+
pub fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
207207
m.add_function(wrap_pyfunction!(make_date, m)?)?;
208208
m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?;
209209
m.add_function(wrap_pyfunction!(date_from_timestamp, m)?)?;

pytests/pyo3-pytests/src/dict_iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use pyo3::prelude::*;
33
use pyo3::types::PyDict;
44

55
#[pymodule]
6-
fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
6+
pub fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
77
m.add_class::<DictSize>()?;
88
Ok(())
99
}

pytests/pyo3-pytests/src/lib.rs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,19 @@ pub mod path;
1212
pub mod pyclass_iter;
1313
pub mod subclassing;
1414

15-
#[cfg(not(Py_LIMITED_API))]
16-
use buf_and_str::*;
17-
#[cfg(not(Py_LIMITED_API))]
18-
use datetime::*;
19-
use dict_iter::*;
20-
use misc::*;
21-
use objstore::*;
22-
use othermod::*;
23-
use path::*;
24-
use pyclass_iter::*;
25-
use subclassing::*;
26-
2715
#[pymodule]
2816
fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> {
2917
#[cfg(not(Py_LIMITED_API))]
30-
m.add_wrapped(wrap_pymodule!(buf_and_str))?;
18+
m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?;
3119
#[cfg(not(Py_LIMITED_API))]
32-
m.add_wrapped(wrap_pymodule!(datetime))?;
33-
m.add_wrapped(wrap_pymodule!(dict_iter))?;
34-
m.add_wrapped(wrap_pymodule!(misc))?;
35-
m.add_wrapped(wrap_pymodule!(objstore))?;
36-
m.add_wrapped(wrap_pymodule!(othermod))?;
37-
m.add_wrapped(wrap_pymodule!(path))?;
38-
m.add_wrapped(wrap_pymodule!(pyclass_iter))?;
39-
m.add_wrapped(wrap_pymodule!(subclassing))?;
20+
m.add_wrapped(wrap_pymodule!(datetime::datetime))?;
21+
m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?;
22+
m.add_wrapped(wrap_pymodule!(misc::misc))?;
23+
m.add_wrapped(wrap_pymodule!(objstore::objstore))?;
24+
m.add_wrapped(wrap_pymodule!(othermod::othermod))?;
25+
m.add_wrapped(wrap_pymodule!(path::path))?;
26+
m.add_wrapped(wrap_pymodule!(pyclass_iter::pyclass_iter))?;
27+
m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?;
4028

4129
// Inserting to sys.modules allows importing submodules nicely from Python
4230
// e.g. import pyo3_pytests.buf_and_str as bas

0 commit comments

Comments
 (0)