Skip to content

Commit

Permalink
Gates #[pymodule] mod behind the experimental-declarative-modules fea…
Browse files Browse the repository at this point in the history
…ture
  • Loading branch information
Tpt committed Feb 23, 2024
1 parent cff28c5 commit beb75da
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 109 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -114,6 +117,7 @@ full = [
"chrono-tz",
"either",
"experimental-inspect",
"experimental-declarative-modules",
"eyre",
"hashbrown",
"indexmap",
Expand Down
3 changes: 2 additions & 1 deletion newsfragments/3815.added.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
The ability to create Python modules with a Rust `mod` block.
The ability to create Python modules with a Rust `mod` block
behind the `experimental-declarative-modules` feature.
1 change: 1 addition & 0 deletions pyo3-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ proc-macro = true

[features]
multiple-pymethods = []
experimental-declarative-modules = []

[dependencies]
proc-macro2 = { version = "1", default-features = false }
Expand Down
9 changes: 8 additions & 1 deletion pyo3-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 39 additions & 33 deletions pytests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use pyo3::prelude::*;
use pyo3::types::PyDict;
use pyo3::wrap_pymodule;

pub mod awaitable;
pub mod buf_and_str;
Expand All @@ -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::<PyDict>()?;
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::<PyDict>()?;
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(())
}
7 changes: 7 additions & 0 deletions tests/test_append_to_inittab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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#"
Expand All @@ -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#"
Expand Down
4 changes: 4 additions & 0 deletions tests/test_compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
101 changes: 101 additions & 0 deletions tests/test_declarative_module.rs
Original file line number Diff line number Diff line change
@@ -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')");
})
}
75 changes: 1 addition & 74 deletions tests/test_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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')");
})
}

0 comments on commit beb75da

Please sign in to comment.