experimental-inspect: no-parameter pymodule_init keeps module complete - #6271
experimental-inspect: no-parameter pymodule_init keeps module complete#6271jonasdedden wants to merge 6 commits into
experimental-inspect: no-parameter pymodule_init keeps module complete#6271Conversation
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
| pymodule_init = Some(quote! { #ident(module)?; }); | ||
| ensure_spanned!( | ||
| item_fn.sig.inputs.len() <= 1, | ||
| item_fn.sig.inputs[1].span() => "`#[pymodule_init]` takes either no argument or the module" |
There was a problem hiding this comment.
hyper-nit: it's a bit weird to anchor the error on the first argument and not on the full function signature
| item_fn.sig.inputs[1].span() => "`#[pymodule_init]` takes either no argument or the module" | |
| item_fn.sig.span() => "`#[pymodule_init]` takes either no argument or the module" |
There was a problem hiding this comment.
Implemented the suggestion
| impl<T, E> PyModuleInitResult for Result<T, E> | ||
| where | ||
| PyErr: From<E>, | ||
| { | ||
| fn into_result(self) -> PyResult<()> { | ||
| self.map(|_| ()).map_err(PyErr::from) | ||
| } | ||
| } |
There was a problem hiding this comment.
This ignores the return type. Is it something we want to do? I would tend to think it might be cleaner to fail on everything that is not ():
| impl<T, E> PyModuleInitResult for Result<T, E> | |
| where | |
| PyErr: From<E>, | |
| { | |
| fn into_result(self) -> PyResult<()> { | |
| self.map(|_| ()).map_err(PyErr::from) | |
| } | |
| } | |
| impl<E> PyModuleInitResult for Result<(), E> | |
| where | |
| PyErr: From<E>, | |
| { | |
| fn into_result(self) -> PyResult<()> { | |
| self.map_err(PyErr::from) | |
| } | |
| } |
There was a problem hiding this comment.
Now, only () and Result<(), E> are allowed, as suggested
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.