refactor(bindings/python): generate type stubs with pyo3 introspection#7824
Merged
Conversation
Replace pyo3-stub-gen with native PyO3 introspection (experimental-inspect) for type-stub generation. - Cargo.toml: add pyo3 experimental-inspect; drop pyo3-stub-gen dep and the stub_gen bin; lib crate-type cdylib-only (rlib was only for the deleted bin). - Delete src/bin/stub_gen.rs; drop define_stub_info_gatherer! and the pyo3_stub_gen use from lib.rs. - Strip all #[gen_stub*] attrs across capability/errors/file/operator/ metadata/layers; convert override_type/override_return_type to inline #[pyo3(signature = (arg: "T") -> "R")] annotations. errors.rs uses pyo3::create_exception. - python.j2: drop module_doc!/gen_methods_from_python! stub-gen wiring, keep the Scheme enum + impl_enum_to_str!. python.rs: keep the helper fns/registrations for a follow-up PR; only add a trailing-newline guard to the generated services.rs. Stub layout, the generation pipeline, and the handling of exceptions and per-service constructors are handled separately.
- stub-gen: generate services bindings, build with maturin --generate-stubs, run the stub post-processor, then `just fmt`. - fmt: ruff format then `ruff check --unsafe-fixes --fix` (fix must run after format), so generated stubs are normalised in place. - ruff.toml: keep only the *.pyi ignores that are not auto-fixable (PYI021 to retain introspected docstrings, ANN401, ANN003, TID252); the rest are handled by the fmt fixers. - Use --manifest-path instead of -m for maturin commands.
Delete the public opendal/*.pyi generated by pyo3-stub-gen. Stubs are now produced by pyo3 introspection under opendal/_opendal/ and resolved via the package's TYPE_CHECKING imports.
Explain that typing.Self (3.11+) is unavailable at the >=3.10 floor, so the introspected stub annotation uses typing_extensions.Self.
Add the type stubs generated by pyo3 introspection at the public ``opendal/<name>.pyi`` paths so ``from opendal.operator import X`` and friends resolve for type checkers and IDEs. ``__init__.py`` imports the native ``_opendal`` extension first (which registers the submodules in ``sys.modules``) and re-exports the public classes and submodules.
Rewrite the stub post-processor to move the introspected stubs from ``opendal/_opendal/`` to the public ``opendal/<name>.pyi`` paths (where type checkers resolve them), in addition to injecting forward-ref imports and the hand-written exceptions stub. Run ruff's fixers silently in ``just fmt`` so the relocated stubs are normalised.
Regenerate the service bindings from the updated codegen template: a lean `Scheme` enum plus `impl_enum_to_str!`, dropping the pyo3-stub-gen `gen_methods_from_python!` constructor overloads.
Add a `stubs` job that runs `just stub-gen` and `git diff --exit-code`, failing the build when the committed codegen or introspection stubs are stale. Release wheels ship the committed python/opendal tree verbatim, so it must stay fresh.
There was a problem hiding this comment.
Pull request overview
This PR refactors the Python binding’s type-stub generation pipeline to use PyO3’s introspection (experimental-inspect) via maturin --generate-stubs, removing the legacy pyo3-stub-gen-based workflow and reworking the binding code to express signature/type info through #[pyo3(signature = ...)]. It also adds a CI drift check to ensure committed .pyi stubs stay in sync.
Changes:
- Replace
pyo3-stub-gen+stub_genbinary withmaturin --generate-stubs, and remove the stub-gen macros/attributes from Rust sources. - Add
scripts/postprocess_stubs.pyto relocate generated stubs to public module paths and apply temporary fixups (imports + exceptions stub substitution). - Regenerate service scheme bindings and committed
.pyifiles; add a CI job to fail if regenerated stubs differ from committed ones.
Reviewed changes
Copilot reviewed 24 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| dev/src/generate/python.rs | Ensures generated services.rs ends with a trailing newline. |
| dev/src/generate/python.j2 | Updates the generator template to emit a lean Scheme and removes pyo3_stub_gen usage. |
| bindings/python/src/services.rs | Regenerated services enum and string conversion mappings (large shrink). |
| bindings/python/src/operator.rs | Replaces pyo3_stub_gen overrides with #[pyo3(signature=...)] annotations for stubs. |
| bindings/python/src/metadata.rs | Removes stub-gen attributes; relies on PyO3/jiff conversion for datetime types. |
| bindings/python/src/lib.rs | Drops stub-gen gatherer wiring; exports Scheme directly. |
| bindings/python/src/layers.rs | Removes stub-gen attributes and relies on PyO3 signatures/introspection. |
| bindings/python/src/file.rs | Adds/updates #[pyo3(signature=...)] annotations for generated stubs. |
| bindings/python/src/errors.rs | Switches exception macro import to pyo3::create_exception. |
| bindings/python/src/capability.rs | Removes stub-gen attribute wrapper on Capability. |
| bindings/python/src/bin/stub_gen.rs | Deletes the legacy stub generator binary. |
| bindings/python/scripts/postprocess_stubs.py | New stub post-processor to relocate stubs and patch known PyO3 introspection gaps. |
| bindings/python/ruff.toml | Adjusts .pyi per-file ignores for the new generated stub shape. |
| bindings/python/python/opendal/types.pyi | Updated generated stubs for Entry, Metadata, EntryMode, PresignedRequest. |
| bindings/python/python/opendal/services.pyi | Updated generated stubs for Scheme. |
| bindings/python/python/opendal/layers.pyi | Updated generated stubs for layers. |
| bindings/python/python/opendal/file.pyi | Updated generated stubs for File / AsyncFile. |
| bindings/python/python/opendal/exceptions.pyi | Replaced with simplified stub output (via postprocess hook). |
| bindings/python/python/opendal/capability.pyi | Updated generated stubs for Capability. |
| bindings/python/python/opendal/_opendal.pyi | New stub for the extension package _opendal namespace. |
| bindings/python/python/opendal/init.py | Adjusts import order to ensure _opendal registers submodules before re-exports. |
| bindings/python/pyproject.toml | Removes ty exclusion of .pyi stubs. |
| bindings/python/justfile | Rewires stub-gen to use maturin + postprocess; expands fmt to fix stubs via ruff. |
| bindings/python/Cargo.toml | Drops pyo3-stub-gen dep and stub_gen bin; enables pyo3/experimental-inspect. |
| .github/workflows/ci_bindings_python.yml | Adds a stubs job that regenerates stubs and fails on drift. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`maturin --generate-stubs` reads the introspection data from the built library. On Linux, `[tool.maturin] strip = true` strips that data out, so stub generation fails with "No module named _opendal found" (it happened to survive the strip on macOS). Drop the maturin strip; release wheels are still stripped via `[profile.release]`.
`just stub-gen` ends in `just fmt`, which runs taplo and hawkeye. Install both in the stubs job so stub regeneration completes (matching the tools used elsewhere in CI).
Use the prebuilt hawkeye installer instead of `cargo install`, which builds from source and is slow.
…stubs - readinto requires a writable buffer; type it `bytearray | memoryview` instead of `bytes | bytearray` (`bytes` is rejected at runtime). - __aenter__ returns an awaitable, so annotate it `Awaitable[Self]` rather than `Self`.
Contributor
Author
|
@erickguan here a big one, mostly deletions. |
Member
|
This is a really nice PR. Deleting code feels always great. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Closes #.
Rationale for this change
The Python binding generates its type stubs with
pyo3-stub-gen. This has a few issues:just stub-genhas not worked onmainfor months.pyo3-stub-gen0.18 changed the binding's module layout (in a stale, unmerged PR), later bumps moved it to 0.23, and the generator was never re-run. The committed stubs went stale, so the break stayed hidden. Regenerating now fails with a wrong-module error.#[gen_stub_*]macros andoverride_type/override_return_typeattributes spread across the Rust code, kept in sync by hand.stub_genbinary and a crate dependency.PyO3 0.29 can describe the extension through its
experimental-inspectfeature, andmaturin --generate-stubsturns that into.pyifiles. This is the direction PyO3 is moving in. It removes the extra dependency, the binary, and the hand-maintained macros, and derives the stubs straight from the built binary.What changes are included in this PR?
Switch the generator
pyo3-stub-gen, thestub_genbinary, anddefine_stub_info_gatherer!. Enablepyo3/experimental-inspect. The lib iscdylib-only now (therlibwas only for the deleted binary).#[gen_stub_*]attributes and convert theoverride_*attributes into inline#[pyo3(signature = (arg: "T") -> "R")]annotations.Schemeenum; regeneratesrc/services.rs(−5.3k lines).Generate stubs at the public paths
maturin --generate-stubswrites underopendal/_opendal/.scripts/postprocess_stubs.pymoves them toopendal/<name>.pyi, where type checkers resolvefrom opendal.operator import ….just stub-genand run ruff's fixers injust fmt.Guard against drift
stubsCI job that runsjust stub-genandgit diff --exit-code, so stale stubs fail CI. Release wheels ship the committedpython/opendaltree as-is, so it must stay fresh.Are there any user-facing changes?
Yes. The changes are to the generated stubs only; runtime behavior is unchanged.
Pros
EntryMode.*,Scheme.*) and several dunders are now described, andMetadata.last_modifiedis correctlydatetime | None(wasdatetime, which could not representNone).#[gen_stub_*]macros, nooverride_*attributes, nostub_genbinary, one fewer dependency.maturinand PyO3 directly, the future-facing path asexperimental-inspectmatures.Cons
@overload def __new__(...)entries (one per service, with a typedscheme=Literal[...]and per-service config kwargs); they are now a single generic__new__(scheme: str | Scheme, **kwargs). PyO3 introspection has no overload support. A follow-up will regenerate these from Rust.scripts/postprocess_stubs.pyapplies two temporary fixups: it injects the imports PyO3 omits for forward-ref annotations, and it substitutes a hand-writtenexceptions.pyiwhile the generated one is incomplete (create_exception!types are not introspectable). These are PyO3 limitations and should be removed asexperimental-inspectimproves in future releases.Please add the
breaking-changeslabel for the dropped constructor overloads.AI Usage Statement