Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 114 additions & 66 deletions bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,72 +43,120 @@ use pyo3_stub_gen::{define_stub_info_gatherer, derive::*};
pub use services::*;

#[pymodule(gil_used = false)]
fn _opendal(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
// Add version
m.add("__version__", env!("CARGO_PKG_VERSION"))?;

// Operator module
add_pymodule!(py, m, "operator", [Operator, AsyncOperator])?;

// File module
add_pymodule!(py, m, "file", [File, AsyncFile])?;

// Capability module
add_pymodule!(py, m, "capability", [Capability])?;

// Services module
add_pymodule!(py, m, "services", [PyScheme])?;

// Layers module
add_pymodule!(
py,
m,
"layers",
[
Layer,
CapabilityOverrideLayer,
RetryLayer,
ConcurrentLimitLayer,
MimeGuessLayer
]
)?;

// Types module
add_pymodule!(
py,
m,
"types",
[Entry, EntryMode, Metadata, PresignedRequest]
)?;

m.add_class::<WriteOptions>()?;
m.add_class::<ReadOptions>()?;
m.add_class::<ListOptions>()?;
m.add_class::<StatOptions>()?;
m.add_class::<DeleteOptions>()?;

// Exceptions module
add_pyexceptions!(
py,
m,
"exceptions",
[
Error,
Unexpected,
Unsupported,
ConfigInvalid,
NotFound,
PermissionDenied,
IsADirectory,
NotADirectory,
AlreadyExists,
IsSameFile,
ConditionNotMatch,
RateLimited,
RangeNotSatisfied,
]
)?;
Ok(())
mod _opendal {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does declarative syntax works for modules?

#[pymodule(gil_used = false)]
mod opendal {
}

@chitralverma chitralverma Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — mod _opendal here is the declarative #[pymodule] mod form (converted from the old fn _opendal).

It has to stay _opendal, not opendal: this is a mixed layout, so module-name = "opendal._opendal" builds opendal/_opendal.*.so and the hand-written opendal/__init__.py is the real package that re-exports from it. The mod name must match the .so's last path component (PyInit__opendal). Naming it opendal would require the native module to be the top-level package, colliding with the opendal/ package dir.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your detailed explanation! I would recommend documenting:

We are using a mixed Python and native package for python OpenDAL binding.
To avoid conflict of ... 
<And a concise explanation of how it works here should be enough>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment above mod _opendal explaining the mixed-layout naming (ba8627b).

use pyo3::prelude::*;

Comment thread
chitralverma marked this conversation as resolved.
#[pymodule]
mod operator {
#[pymodule_export]
use crate::{AsyncOperator, Operator};

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "operator")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use #[pymodule] for submodules too?

I know packaging might be a problem. Here is what I found.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at the linked comment and the alternative below it and modified the current implementation with a single recursive register_submodules walk called once from the root #[pymodule_init], replacing the per-submodule registration.

For opendal, we don't need the submodule flag or the PyModuleSubmoduleExt trait as all our submodules are nested inside mod _opendal, so PyO3 auto-marks them as submodules.

}
}

#[pymodule]
mod file {
#[pymodule_export]
use crate::{AsyncFile, File};

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "file")
}
}

#[pymodule]
mod capability {
#[pymodule_export]
use crate::Capability;

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "capability")
}
}

#[pymodule]
mod services {
#[pymodule_export]
use crate::PyScheme;

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "services")
}
}

#[pymodule]
mod layers {
#[pymodule_export]
use crate::{
CapabilityOverrideLayer, ConcurrentLimitLayer, Layer, MimeGuessLayer, RetryLayer,
};

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "layers")
}
}

#[pymodule]
mod types {
#[pymodule_export]
use crate::{Entry, EntryMode, Metadata, PresignedRequest};

#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
crate::register_in_sys(m, "types")
}
}

// Exceptions are `PyErr` subtypes, not `#[pyclass]`es, so they are added
// procedurally rather than via `#[pymodule_export]`.
#[pymodule]
mod exceptions {
#[pymodule_init]
fn init(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> {
use pyo3::prelude::*;

Comment thread
chitralverma marked this conversation as resolved.
use crate::{
AlreadyExists, ConditionNotMatch, ConfigInvalid, Error, IsADirectory, IsSameFile,
NotADirectory, NotFound, PermissionDenied, RangeNotSatisfied, RateLimited,
Unexpected, Unsupported, add_exceptions,
};

add_exceptions!(
Comment thread
chitralverma marked this conversation as resolved.
m,
[
Error,
Unexpected,
Unsupported,
ConfigInvalid,
NotFound,
PermissionDenied,
IsADirectory,
NotADirectory,
AlreadyExists,
IsSameFile,
ConditionNotMatch,
RateLimited,
RangeNotSatisfied,
]
)?;
crate::register_in_sys(m, "exceptions")
}
}

// Option types live directly on the top-level `opendal` module.
#[pymodule_export]
use crate::{DeleteOptions, ListOptions, ReadOptions, StatOptions, WriteOptions};

#[pymodule_export]
#[allow(non_upper_case_globals)]
pub const __version__: &str = env!("CARGO_PKG_VERSION");
}

define_stub_info_gatherer!(stub_info);
50 changes: 18 additions & 32 deletions bindings/python/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,44 +77,30 @@ impl Buffer {
}
}

/// Macro to create and register a PyO3 submodule with multiple classes.
///
/// Example:
/// ```rust
/// add_pymodule!(py, m, "services", [PyScheme, PyOtherClass]);
/// ```
#[macro_export]
macro_rules! add_pymodule {
($py:expr, $parent:expr, $name:expr, [$($cls:ty),* $(,)?]) => {{
let sub_module = pyo3::types::PyModule::new($py, $name)?;
$(
sub_module.add_class::<$cls>()?;
)*
$parent.add_submodule(&sub_module)?;
$py.import("sys")?
.getattr("modules")?
.set_item(format!("opendal.{}", $name), &sub_module)?;
Ok::<_, pyo3::PyErr>(())
}};
/// Register a submodule in `sys.modules` as `opendal.{name}` and set its
/// `__name__` to match, so dotted imports resolve and the module reports its
/// qualified name rather than PyO3's default `_opendal.{name}`.
pub fn register_in_sys(module: &Bound<'_, PyModule>, name: &str) -> PyResult<()> {
let qualified_name = format!("opendal.{name}");
module.setattr("__name__", &qualified_name)?;
Comment thread
chitralverma marked this conversation as resolved.
Outdated
module
.py()
.import("sys")?
.getattr("modules")?
.set_item(qualified_name, module)?;
Ok(())
}

/// Macro to create and register a PyO3 submodule containing exception types.
/// Add exception types to a module by their Rust identifier.
///
/// Example:
/// ```rust
/// add_pyexceptions!(py, m, "exceptions", [Error, Unexpected]);
/// ```
/// `create_exception!` types are `PyErr` subtypes, not `#[pyclass]`es, so they
/// cannot be listed with `#[pymodule_export]`.
#[macro_export]
macro_rules! add_pyexceptions {
($py:expr, $parent:expr, $name:expr, [$($exc:ty),* $(,)?]) => {{
let sub_module = pyo3::types::PyModule::new($py, $name)?;
macro_rules! add_exceptions {
($module:expr, [$($exc:ty),* $(,)?]) => {{
$(
sub_module.add(stringify!($exc), $py.get_type::<$exc>())?;
$module.add(stringify!($exc), $module.py().get_type::<$exc>())?;
)*
$parent.add_submodule(&sub_module)?;
$py.import("sys")?
.getattr("modules")?
.set_item(format!("opendal.{}", $name), &sub_module)?;
Ok::<_, pyo3::PyErr>(())
}};
}
Loading
Loading