Skip to content

Commit

Permalink
macros: accept paths in wrap_x macros
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Dec 28, 2021
1 parent 19ecd17 commit 62b2395
Show file tree
Hide file tree
Showing 21 changed files with 197 additions and 111 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
4 changes: 3 additions & 1 deletion pyo3-macros-backend/src/lib.rs
Expand Up @@ -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};
26 changes: 17 additions & 9 deletions pyo3-macros-backend/src/module.rs
Expand Up @@ -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;
Expand All @@ -15,7 +16,7 @@ use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
token::Comma,
Ident, Path, Result,
Ident, Path, Result, Visibility,
};

#[derive(Default)]
Expand Down Expand Up @@ -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))
};
}
}

Expand Down
7 changes: 1 addition & 6 deletions pyo3-macros-backend/src/pyfunction.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand Down
67 changes: 67 additions & 0 deletions 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<Self> {
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<TokenStream> {
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<TokenStream> {
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())
}
1 change: 1 addition & 0 deletions pyo3-macros/Cargo.toml
Expand Up @@ -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" }
54 changes: 42 additions & 12 deletions pyo3-macros/src/lib.rs
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -250,3 +270,13 @@ fn methods_type() -> PyClassMethodsType {
PyClassMethodsType::Specialization
}
}

trait UnwrapOrCompileError {
fn unwrap_or_compile_error(self) -> TokenStream2;
}

impl UnwrapOrCompileError for syn::Result<TokenStream2> {
fn unwrap_or_compile_error(self) -> TokenStream2 {
self.unwrap_or_else(|e| e.into_compile_error())
}
}
2 changes: 1 addition & 1 deletion pytests/pyo3-pytests/src/datetime.rs
Expand Up @@ -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)?)?;
Expand Down
2 changes: 1 addition & 1 deletion pytests/pyo3-pytests/src/dict_iter.rs
Expand Up @@ -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::<DictSize>()?;
Ok(())
}
Expand Down
30 changes: 9 additions & 21 deletions pytests/pyo3-pytests/src/lib.rs
Expand Up @@ -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
Expand Down

0 comments on commit 62b2395

Please sign in to comment.