From 9bb69d6403b2640ea72c77b16e9d088c3f0fc6c7 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 08:36:29 -0400 Subject: [PATCH 01/15] use last segment of module name (path) when adding to parent --- src/types/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/module.rs b/src/types/module.rs index 72ba2d341d9..cb3aca500f2 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -514,7 +514,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { } fn add_submodule(&self, module: &Bound<'_, PyModule>) -> PyResult<()> { - let name = module.name()?; + let name = module.name()?.call_method1("rpartition", (".",))?.get_item(2)?.downcast_into::()?; self.add(name, module) } From f3a764519a24da0528dbdcac60e9f49cf4d0146e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 08:51:15 -0400 Subject: [PATCH 02/15] add newsfragment --- newsfragments/5375.fixed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/5375.fixed.md diff --git a/newsfragments/5375.fixed.md b/newsfragments/5375.fixed.md new file mode 100644 index 00000000000..ee6b3893e76 --- /dev/null +++ b/newsfragments/5375.fixed.md @@ -0,0 +1 @@ +Fixed `PyModuleMethods::add_submodul()` to use the last segment of the submodule name as the attribute name on the parent module instead of using the full name. \ No newline at end of file From 4455590c24c4a5aff64b69bce79894f6fec4caab Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 08:51:19 -0400 Subject: [PATCH 03/15] format --- src/types/module.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/module.rs b/src/types/module.rs index cb3aca500f2..1c67ca90080 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -514,7 +514,11 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { } fn add_submodule(&self, module: &Bound<'_, PyModule>) -> PyResult<()> { - let name = module.name()?.call_method1("rpartition", (".",))?.get_item(2)?.downcast_into::()?; + let name = module + .name()? + .call_method1("rpartition", (".",))? + .get_item(2)? + .downcast_into::()?; self.add(name, module) } From 01ca587c15afd92f6542955e0ca624e126c53c2e Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 09:03:58 -0400 Subject: [PATCH 04/15] preliminary tests --- tests/test_module.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 5e6244420e0..d55781a08f6 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -285,10 +285,10 @@ fn superfunction() -> String { #[pymodule] fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; - let module_to_add = PyModule::new(module.py(), "submodule")?; + let module_to_add = PyModule::new(module.py(), "supermodule.submodule")?; submodule(&module_to_add)?; module.add_submodule(&module_to_add)?; - let module_to_add = PyModule::new(module.py(), "submodule_with_init_fn")?; + let module_to_add = PyModule::new(module.py(), "supermodule.submodule_with_init_fn")?; submodule_with_init_fn(&module_to_add)?; module.add_submodule(&module_to_add)?; Ok(()) @@ -316,6 +316,21 @@ fn test_module_nesting() { supermodule, "supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'" ); + py_assert!( + py, + supermodule, + "supermodule.submodule.__name__ == 'supermodule.submodule'" + ); + py_assert!( + py, + supermodule, + "'submodule' in supermodule.__dict__" + ); + py_assert!( + py, + supermodule, + "'supermodule.submodule' not in supermodule.__dict__" + ); }); } From 0d6fdff677144e85361ce9cbf483d7c752e54534 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 09:21:12 -0400 Subject: [PATCH 05/15] fmt --- tests/test_module.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index d55781a08f6..ca154996839 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -321,11 +321,7 @@ fn test_module_nesting() { supermodule, "supermodule.submodule.__name__ == 'supermodule.submodule'" ); - py_assert!( - py, - supermodule, - "'submodule' in supermodule.__dict__" - ); + py_assert!(py, supermodule, "'submodule' in supermodule.__dict__"); py_assert!( py, supermodule, From fb5725e72d167bf97689a200a7de9edf24bd61ea Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 15:34:30 -0400 Subject: [PATCH 06/15] doc comment and more test (that fails right now) --- src/types/module.rs | 3 ++- tests/test_module.rs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/types/module.rs b/src/types/module.rs index 1c67ca90080..57e9177299c 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -35,7 +35,8 @@ pub struct PyModule(PyAny); pyobject_native_type_core!(PyModule, pyobject_native_static_type_object!(ffi::PyModule_Type), #checkfunction=ffi::PyModule_Check); impl PyModule { - /// Creates a new module object with the `__name__` attribute set to `name`. + /// Creates a new module object with the `__name__` attribute set to `name`. When creating + /// a submodule pass the full path as the name such as `top_level.name`. /// /// # Examples /// diff --git a/tests/test_module.rs b/tests/test_module.rs index ca154996839..09529eb5371 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -327,6 +327,17 @@ fn test_module_nesting() { supermodule, "'supermodule.submodule' not in supermodule.__dict__" ); + py_assert!( + py, + supermodule, + "supermodule.submodule.__name__ == 'supermodule.submodule_with_init_fn'" + ); + py_assert!(py, supermodule, "'submodule_with_init_fn' in supermodule.__dict__"); + py_assert!( + py, + supermodule, + "'supermodule.submodule_with_init_fn' not in supermodule.__dict__" + ); }); } From 7c4b890dd08ce307c8464a2626e8dbe6c3811f6f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 15:57:43 -0400 Subject: [PATCH 07/15] fmt --- tests/test_module.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 09529eb5371..1b05eddff90 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -332,7 +332,11 @@ fn test_module_nesting() { supermodule, "supermodule.submodule.__name__ == 'supermodule.submodule_with_init_fn'" ); - py_assert!(py, supermodule, "'submodule_with_init_fn' in supermodule.__dict__"); + py_assert!( + py, + supermodule, + "'submodule_with_init_fn' in supermodule.__dict__" + ); py_assert!( py, supermodule, From c05362287d96b485ea2c5ba53d7bef4524105193 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 16:17:41 -0400 Subject: [PATCH 08/15] separate --- tests/test_module.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_module.rs b/tests/test_module.rs index 1b05eddff90..250833b5f5c 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -316,6 +316,16 @@ fn test_module_nesting() { supermodule, "supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'" ); + }); +} + +#[test] +fn test_submodule_attribute_and_dunder_names() { + use pyo3::wrap_pymodule; + + Python::attach(|py| { + let supermodule = wrap_pymodule!(supermodule)(py); + py_assert!( py, supermodule, From ec749321cead6f3e9dbc08af6a88bfcc1cd2437b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 16:20:38 -0400 Subject: [PATCH 09/15] tweak test layout --- tests/test_module.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 250833b5f5c..91111898024 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -320,7 +320,7 @@ fn test_module_nesting() { } #[test] -fn test_submodule_attribute_and_dunder_names() { +fn test_submodule_attribute_and_dunder_names_submodule() { use pyo3::wrap_pymodule; Python::attach(|py| { @@ -337,10 +337,20 @@ fn test_submodule_attribute_and_dunder_names() { supermodule, "'supermodule.submodule' not in supermodule.__dict__" ); + }); +} + +#[test] +fn test_submodule_attribute_and_dunder_names_submodule_with_init_fn() { + use pyo3::wrap_pymodule; + + Python::attach(|py| { + let supermodule = wrap_pymodule!(supermodule)(py); + py_assert!( py, supermodule, - "supermodule.submodule.__name__ == 'supermodule.submodule_with_init_fn'" + "supermodule.submodule_with_init_fn.__name__ == 'supermodule.submodule_with_init_fn'" ); py_assert!( py, From 9c9f0026ada83a78e42f8f3005dae3c20a9ee657 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 16:34:10 -0400 Subject: [PATCH 10/15] recombine for PyO3 modules compiled for CPython 3.8 or older may only be initialized once per interpreter process --- tests/test_module.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 91111898024..80082921540 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -320,12 +320,13 @@ fn test_module_nesting() { } #[test] -fn test_submodule_attribute_and_dunder_names_submodule() { +fn test_submodule_attribute_and_dunder_names() { use pyo3::wrap_pymodule; Python::attach(|py| { let supermodule = wrap_pymodule!(supermodule)(py); + // submodule py_assert!( py, supermodule, @@ -337,16 +338,8 @@ fn test_submodule_attribute_and_dunder_names_submodule() { supermodule, "'supermodule.submodule' not in supermodule.__dict__" ); - }); -} - -#[test] -fn test_submodule_attribute_and_dunder_names_submodule_with_init_fn() { - use pyo3::wrap_pymodule; - - Python::attach(|py| { - let supermodule = wrap_pymodule!(supermodule)(py); + // submodule_with_init_fn py_assert!( py, supermodule, From 4cb3eb6263e865f989296239c17cb3a108c6f128 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 16:49:03 -0400 Subject: [PATCH 11/15] again --- tests/test_module.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 80082921540..4e998fc464b 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -316,17 +316,9 @@ fn test_module_nesting() { supermodule, "supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'" ); - }); -} -#[test] -fn test_submodule_attribute_and_dunder_names() { - use pyo3::wrap_pymodule; - - Python::attach(|py| { - let supermodule = wrap_pymodule!(supermodule)(py); - // submodule + // submodule dunder name and attribute name py_assert!( py, supermodule, @@ -339,7 +331,7 @@ fn test_submodule_attribute_and_dunder_names() { "'supermodule.submodule' not in supermodule.__dict__" ); - // submodule_with_init_fn + // submodule_with_init_fn dunder name and attribute name py_assert!( py, supermodule, From b69ef460dd1ece1ea4f5b47ec05d9e21b910a177 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 16:59:30 -0400 Subject: [PATCH 12/15] fmt --- tests/test_module.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_module.rs b/tests/test_module.rs index 4e998fc464b..b310c113c76 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -317,7 +317,6 @@ fn test_module_nesting() { "supermodule.submodule_with_init_fn.subfunction() == 'Subfunction'" ); - // submodule dunder name and attribute name py_assert!( py, From e1909d5a2989da9311bee1d330e11f72d07b94b0 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 3 Nov 2025 11:28:05 -0500 Subject: [PATCH 13/15] catchup --- src/types/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/module.rs b/src/types/module.rs index 389bf73680c..08ec262523f 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -518,7 +518,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { .name()? .call_method1("rpartition", (".",))? .get_item(2)? - .downcast_into::()?; + .cast_into::()?; self.add(name, module) } From e89192301924fbe456d91af2b8983d3458404384 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 3 Nov 2025 12:38:55 -0500 Subject: [PATCH 14/15] declarative --- pyo3-macros-backend/src/module.rs | 6 ++++-- tests/test_declarative_module.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index df00f319ac0..a876a724b9d 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -392,6 +392,7 @@ pub fn pymodule_module_impl( let gil_used = options.gil_used.is_some_and(|op| op.value.value); let initialization = module_initialization( + &full_name, &name, ctx, quote! { __pyo3_pymodule }, @@ -451,6 +452,7 @@ pub fn pymodule_function_impl( let gil_used = options.gil_used.is_some_and(|op| op.value.value); let initialization = module_initialization( + &name.to_string(), &name, ctx, quote! { ModuleExec::__pyo3_module_exec }, @@ -499,6 +501,7 @@ pub fn pymodule_function_impl( } fn module_initialization( + full_name: &str, name: &syn::Ident, ctx: &Ctx, module_exec: TokenStream, @@ -508,8 +511,7 @@ fn module_initialization( ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{name}"); - let name = name.to_string(); - let pyo3_name = LitCStr::new(&CString::new(name).unwrap(), Span::call_site()); + let pyo3_name = LitCStr::new(&CString::new(full_name).unwrap(), Span::call_site()); let mut result = quote! { #[doc(hidden)] diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index c01933c35e4..80eb8b87299 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -150,7 +150,7 @@ fn double_value(v: &ValueClass) -> usize { v.value * 2 } -#[pymodule] +#[pymodule(module="declarative_module")] mod declarative_submodule { #[pymodule_export] use super::{double, double_value}; @@ -204,6 +204,33 @@ fn test_declarative_module() { py_assert!(py, m, "not hasattr(m, 'BAR')"); py_assert!(py, m, "m.type == '!'"); py_assert!(py, m, "not hasattr(m, 'NOT_EXPORTED')"); + + // submodule dunder name and attribute name + // declarative_module.inner is declared inside + py_assert!( + py, + m, + "m.inner.__name__ == 'declarative_module.inner'" + ); + py_assert!(py, m, "'inner' in m.__dict__"); + py_assert!( + py, + m, + "'declarative_module.inner' not in m.__dict__" + ); + + // since declarative_submodule is declared outside, but the parent module name is passed + py_assert!( + py, + m, + "m.declarative_submodule.__name__ == 'declarative_module.declarative_submodule'" + ); + py_assert!(py, m, "'declarative_submodule' in m.__dict__"); + py_assert!( + py, + m, + "'declarative_module.declarative_submodule' not in m.__dict__" + ); }) } From cd69d883b75b993890274d91a6de27684135fbca Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Mon, 3 Nov 2025 13:43:01 -0500 Subject: [PATCH 15/15] fmt --- tests/test_declarative_module.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 80eb8b87299..e0d77f69e97 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -150,7 +150,7 @@ fn double_value(v: &ValueClass) -> usize { v.value * 2 } -#[pymodule(module="declarative_module")] +#[pymodule(module = "declarative_module")] mod declarative_submodule { #[pymodule_export] use super::{double, double_value}; @@ -207,17 +207,9 @@ fn test_declarative_module() { // submodule dunder name and attribute name // declarative_module.inner is declared inside - py_assert!( - py, - m, - "m.inner.__name__ == 'declarative_module.inner'" - ); + py_assert!(py, m, "m.inner.__name__ == 'declarative_module.inner'"); py_assert!(py, m, "'inner' in m.__dict__"); - py_assert!( - py, - m, - "'declarative_module.inner' not in m.__dict__" - ); + py_assert!(py, m, "'declarative_module.inner' not in m.__dict__"); // since declarative_submodule is declared outside, but the parent module name is passed py_assert!(