experimental-inspect: no-parameter pymodule_init keeps module complete - #6271
experimental-inspect: no-parameter pymodule_init keeps module complete#6271jonasdedden wants to merge 9 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
|
@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.
| unsafe impl Sync for PyModuleDefSlots {} | ||
|
|
||
| /// Used to accept either `()` or `Result<(), E>` from a `#[pymodule_init]` function. | ||
| pub trait PyModuleInitResult { |
There was a problem hiding this comment.
I think this will make the UX nicer
| pub trait PyModuleInitResult { | |
| #[diagnostic::on_unimplemented( | |
| message = "`{Self}` is not a suitable return value for `#[pymodule_init]` functions | |
| )] | |
| pub trait PyModuleInitResult { |
There was a problem hiding this comment.
Implemented the suggested change and added an additional note too
| 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 |
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.