diff --git a/Cargo.toml b/Cargo.toml index 4d1899cbdb9..e7364a7c9f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,9 @@ default = ["macros"] # and IntoPy traits experimental-inspect = [] +# Enables annotating Rust inline modules with #[pymodule] to build Python modules declaratively +experimental-declarative-modules = ["pyo3-macros/experimental-declarative-modules", "macros"] + # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -114,6 +117,7 @@ full = [ "chrono-tz", "either", "experimental-inspect", + "experimental-declarative-modules", "eyre", "hashbrown", "indexmap", diff --git a/newsfragments/3815.added.md b/newsfragments/3815.added.md index dd450ca88c6..e4fd3e9315a 100644 --- a/newsfragments/3815.added.md +++ b/newsfragments/3815.added.md @@ -1 +1,2 @@ -The ability to create Python modules with a Rust `mod` block. \ No newline at end of file +The ability to create Python modules with a Rust `mod` block +behind the `experimental-declarative-modules` feature. \ No newline at end of file diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 576c94a2bc1..a0368a5f364 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] multiple-pymethods = [] +experimental-declarative-modules = [] [dependencies] proc-macro2 = { version = "1", default-features = false } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 8dbf2782d5b..64756a1c73b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -37,7 +37,14 @@ use syn::{parse::Nothing, parse_macro_input, Item}; pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); match parse_macro_input!(input as Item) { - Item::Mod(module) => pymodule_module_impl(module), + Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") { + pymodule_module_impl(module) + } else { + Err(syn::Error::new_spanned( + module, + "#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.", + )) + }, Item::Fn(function) => pymodule_function_impl(function), unsupported => Err(syn::Error::new_spanned( unsupported, diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 9ffbed9cf27..bfd80edb719 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -1,4 +1,6 @@ use pyo3::prelude::*; +use pyo3::types::PyDict; +use pyo3::wrap_pymodule; pub mod awaitable; pub mod buf_and_str; @@ -16,39 +18,43 @@ pub mod sequence; pub mod subclassing; #[pymodule] -mod pyo3_pytests { - use super::*; - use pyo3::types::PyDict; - #[pymodule_export] - use { - awaitable::awaitable, comparisons::comparisons, dict_iter::dict_iter, enums::enums, - misc::misc, objstore::objstore, othermod::othermod, path::path, pyclasses::pyclasses, - pyfunctions::pyfunctions, sequence::sequence, subclassing::subclassing, - }; +fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] - #[pymodule_export] - use {buf_and_str::buf_and_str, datetime::datetime}; + m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; + m.add_wrapped(wrap_pymodule!(comparisons::comparisons))?; + #[cfg(not(Py_LIMITED_API))] + m.add_wrapped(wrap_pymodule!(datetime::datetime))?; + m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; + m.add_wrapped(wrap_pymodule!(enums::enums))?; + 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!(pyclasses::pyclasses))?; + m.add_wrapped(wrap_pymodule!(pyfunctions::pyfunctions))?; + m.add_wrapped(wrap_pymodule!(sequence::sequence))?; + 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 + + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; + sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; + sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; + sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; + sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; + sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; + sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; + sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; + sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; + sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; + sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; + sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; + sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; + sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; + sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; - #[pymodule_init] - fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { - // Inserting to sys.modules allows importing submodules nicely from Python - // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import_bound(m.py(), "sys")?; - let sys_modules = sys.getattr("modules")?.downcast_into::()?; - sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; - sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; - sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; - sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; - sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; - sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; - sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; - sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; - sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; - sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; - sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; - sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; - sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; - sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; - Ok(()) - } + Ok(()) } diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 06ecc0ef893..59ecaf42909 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -13,6 +13,7 @@ fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] #[pymodule] mod module_mod_with_functions { #[pymodule_export] @@ -23,8 +24,12 @@ mod module_mod_with_functions { #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; + append_to_inittab!(module_fn_with_functions); + + #[cfg(feature = "experimental-declarative-modules")] append_to_inittab!(module_mod_with_functions); + Python::with_gil(|py| { py.run_bound( r#" @@ -37,6 +42,8 @@ assert module_fn_with_functions.foo() == 123 .map_err(|e| e.display(py)) .unwrap(); }); + + #[cfg(feature = "experimental-declarative-modules")] Python::with_gil(|py| { py.run_bound( r#" diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index d6df9c64e39..5f2d25db92f 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -40,8 +40,12 @@ fn test_compile_errors() { t.compile_fail("tests/ui/not_send2.rs"); t.compile_fail("tests/ui/get_set_all.rs"); t.compile_fail("tests/ui/traverse.rs"); + #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_in_root.rs"); + #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); + #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); + #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs new file mode 100644 index 00000000000..86913d9b800 --- /dev/null +++ b/tests/test_declarative_module.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "experimental-declarative-modules")] + +use pyo3::create_exception; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass] +struct ValueClass { + value: usize, +} + +#[pymethods] +impl ValueClass { + #[new] + fn new(value: usize) -> ValueClass { + ValueClass { value } + } +} + +#[pyclass(module = "module")] +struct LocatedClass {} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +create_exception!( + declarative_module, + MyError, + PyException, + "Some description." +); + +/// A module written using declarative syntax. +#[pymodule] +mod declarative_module { + #[pymodule_export] + use super::declarative_submodule; + #[pymodule_export] + // This is not a real constraint but to test cfg attribute support + #[cfg(not(Py_LIMITED_API))] + use super::LocatedClass; + use super::*; + #[pymodule_export] + use super::{declarative_module2, double, MyError, ValueClass as Value}; + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("double2", m.getattr("double")?) + } +} + +#[pyfunction] +fn double_value(v: &ValueClass) -> usize { + v.value * 2 +} + +#[pymodule] +mod declarative_submodule { + #[pymodule_export] + use super::{double, double_value}; +} + +/// A module written using declarative syntax. +#[pymodule] +#[pyo3(name = "declarative_module_renamed")] +mod declarative_module2 { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_declarative_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(declarative_module)(py).into_bound(py); + py_assert!( + py, + m, + "m.__doc__ == 'A module written using declarative syntax.'" + ); + + py_assert!(py, m, "m.double(2) == 4"); + py_assert!(py, m, "m.double2(3) == 6"); + py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); + py_assert!( + py, + m, + "m.declarative_submodule.double_value(m.ValueClass(1)) == 2" + ); + py_assert!(py, m, "str(m.MyError('foo')) == 'foo'"); + py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4"); + #[cfg(Py_LIMITED_API)] + py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); + #[cfg(not(Py_LIMITED_API))] + py_assert!(py, m, "hasattr(m, 'LocatedClass')"); + }) +} diff --git a/tests/test_module.rs b/tests/test_module.rs index 06174c05269..9d14f243d50 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -2,10 +2,9 @@ use pyo3::prelude::*; -use pyo3::exceptions::PyException; +use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; -use pyo3::{create_exception, py_run}; #[path = "../src/tests/common.rs"] mod common; @@ -485,75 +484,3 @@ fn test_module_doc_hidden() { py_assert!(py, m, "m.__doc__ == ''"); }) } - -create_exception!( - declarative_module, - MyError, - PyException, - "Some description." -); - -/// A module written using declarative syntax. -#[pymodule] -mod declarative_module { - #[pymodule_export] - use super::declarative_submodule; - #[pymodule_export] - // This is not a real constraint but to test cfg attribute support - #[cfg(not(Py_LIMITED_API))] - use super::LocatedClass; - use super::*; - #[pymodule_export] - use super::{declarative_module2, double, MyError, ValueClass as Value}; - - #[pymodule_init] - fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add("double2", m.getattr("double")?) - } -} - -#[pyfunction] -fn double_value(v: &ValueClass) -> usize { - v.value * 2 -} - -#[pymodule] -mod declarative_submodule { - #[pymodule_export] - use super::{double, double_value}; -} - -/// A module written using declarative syntax. -#[pymodule] -#[pyo3(name = "declarative_module_renamed")] -mod declarative_module2 { - #[pymodule_export] - use super::double; -} - -#[test] -fn test_declarative_module() { - Python::with_gil(|py| { - let m = pyo3::wrap_pymodule!(declarative_module)(py).into_ref(py); - py_assert!( - py, - m, - "m.__doc__ == 'A module written using declarative syntax.'" - ); - - py_assert!(py, m, "m.double(2) == 4"); - py_assert!(py, m, "m.double2(3) == 6"); - py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); - py_assert!( - py, - m, - "m.declarative_submodule.double_value(m.ValueClass(1)) == 2" - ); - py_assert!(py, m, "str(m.MyError('foo')) == 'foo'"); - py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4"); - #[cfg(Py_LIMITED_API)] - py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); - #[cfg(not(Py_LIMITED_API))] - py_assert!(py, m, "hasattr(m, 'LocatedClass')"); - }) -}