experimental-inspect: no-parameter pymodule_init keeps module complete - #6271
Conversation
experimental-inspect no-parameter pymodule init keeps module completeexperimental-inspect: no-parameter pymodule init keeps module complete
experimental-inspect: no-parameter pymodule init keeps module completeexperimental-inspect: no-parameter pymodule_init keeps module complete
…plete `incomplete` was set to `pymodule_init.is_some()`, so any declarative module with an initialiser got `def __getattr__(name: str) -> Incomplete: ...` in its stubs and (since PyO3#6242) no `__all__` either. That flag is what makes every unknown attribute on the module resolve to `Any`, which is most of the value of having a stub at all. The flag is conservative for a good reason: `#[pymodule_init]` receives `&Bound<'_, PyModule>` and can add arbitrary attributes the macro cannot see. But the common case does not want the module. `pyo3_log::init()` is the motivating example — it installs a global `log` logger and takes nothing: #[pymodule_init] fn init() -> PyResult<()> { pyo3_log::init(); Ok(()) } An initialiser with no parameters cannot reach the module, so it cannot add members to it, so the module is still fully described. This is inferred from the signature rather than asserted by a new attribute: it is checked by the compiler instead of trusted. One-argument initialisers keep today's behaviour exactly. Two or more is now a clear error instead of a confusing one from the generated call site.
Codecov flagged three lines in `pymodule_module_impl` as uncovered: the two `ensure_spanned!` error arms and the no-argument codegen branch. - `tests/ui/invalid_pymodule_init_args.rs` covers the new arity check. - `tests/ui/invalid_pymodule_init_pyfunction.rs` covers the pre-existing `#[pyfunction]`-alongside-`#[pymodule_init]` check, which had no test. - `test_pymodule_init_without_module` compiles a module whose `#[pymodule_init]` takes no argument and asserts it still runs, covering the `#ident()?` branch.
3da1d09 to
f2b01cd
Compare
|
@davidhewitt I ran some additional cleanups over this PR and also introduced one additional change: |
Tpt
left a comment
There was a problem hiding this comment.
Thank you! Makes perfect sense to allow () as a return type
|
@Tpt cautious ping |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, a few small refinements suggested, overall I think this makes sense.
| let call = if pymodule_init_takes_module { | ||
| quote! { #ident(module) } | ||
| } else { | ||
| quote! { #ident() } | ||
| }; |
There was a problem hiding this comment.
It might be nice to use split_off_python_arg to optionally allow py: Python<'_> for initialization even if the module is not passed.
There was a problem hiding this comment.
py is now also allowed, plus a handful of new tests that show that this feature works
Co-authored-by: David Hewitt <mail@davidhewitt.dev>
…s-module-complete
- `#[pymodule_init]` may take a `Python<'_>` marker in front of the module argument or instead of it, via `split_off_python_arg`, which moves from `pymethod` to `method` next to `FnArg`. A `Python`-only initialiser is still not handed the module, so the module stays complete for introspection. - `PyModuleInitResult` gets a `#[diagnostic::on_unimplemented]` message naming `#[pymodule_init]` instead of reporting a raw trait bound.
|
Raised some issue for the CI failure for completeness here: #6352 |
davidhewitt
left a comment
There was a problem hiding this comment.
Thanks, some smaller suggestions and then I think will be ready to merge.
Nightly rust failure should hopefully not block CI.
| /// Split an argument of pyo3::Python from the front of the arg list, if present | ||
| pub fn split_off_python_arg<'a, 'b>( | ||
| args: &'a [FnArg<'b>], | ||
| ) -> (Option<&'a PyArg<'b>>, &'a [FnArg<'b>]) { | ||
| match args { | ||
| [FnArg::Py(py), args @ ..] => (Some(py), args), | ||
| args => (None, args), | ||
| } | ||
| } |
There was a problem hiding this comment.
Why did this move? Seemed fine where it was.
| pymodule_init = Some(quote! { | ||
| #pyo3_path::impl_::pymodule::PyModuleInitResult::into_result(#ident(#(#call_args),*))?; | ||
| }); |
There was a problem hiding this comment.
As a closing thought, I think if this is produced with the return type span the errors will be easier to locate.
| pymodule_init = Some(quote! { | |
| #pyo3_path::impl_::pymodule::PyModuleInitResult::into_result(#ident(#(#call_args),*))?; | |
| }); | |
| let return_span = item_fn.sig.output.span(); | |
| let pyo3_path = pyo3_path.to_tokens_spanned(return_span); | |
| pymodule_init = Some(quote_spanned! { return_span => | |
| #pyo3_path::impl_::pymodule::PyModuleInitResult::into_result(#ident(#(#call_args),*))?; | |
| }); |
|
@davidhewitt applied your suggestions, thanks! |
incompletewas set topymodule_init.is_some(), so any declarative module with an initialiser gotdef __getattr__(name: str) -> Incomplete: ...in its stubs. This is also relevant for #6242 where it would lead to no emission of__all__either, breakingstubtestcompliance. That flag is what makes every unknown attribute on the module resolve toAny, which is most of the value of having a stub at all.The flag is conservative for a good reason:
#[pymodule_init]receives&Bound<'_, PyModule>and can add arbitrary attributes the macro cannot see. But the common case does not want the module.pyo3_log::init()is the motivating example - it installs a globalloglogger and takes nothing, so it would be silly to have such a strong negative impact on typestubs:An initialiser with no parameters cannot reach the module, so it cannot add members to it, so the module is still fully described. This is inferred from the signature rather than asserted by a new attribute: it is checked by the compiler instead of trusted.
One-argument initialisers keep today's behaviour exactly. Two or more is now a clear error instead of a confusing one from the generated call site.