|
|
Pythonのサブモジュールは、PyO3でもRustのモジュールで実装できる。
pyo3 0.27でサブモジュールを実装する方法は2通りある。
親モジュールの中にネストしたサブモジュールを実装する方法。
use pyo3::prelude::*;
/// 親モジュール
///
/// NOTE:
/// サブモジュールの実験
#[pymodule]
mod submodule_example {
use pyo3::prelude::*;
/// インライン サブモジュール
#[pymodule]
mod submodule {
use pyo3::prelude::*;
#[pyfunction]
fn func() -> String {
"submodule.func".to_string()
}
}
}
$ uv run python >>> import submodule_example >>> submodule_example.submodule.func() 'submodule.func'
外側に定義したサブモジュールをエクスポートする方式。
use pyo3::prelude::*;
/// 親モジュール
///
/// NOTE:
/// サブモジュールの実験
#[pymodule]
mod submodule_example {
use pyo3::prelude::*;
#[pymodule_export]
use super::child_module;
}
/// サブモジュール
#[pymodule]
mod child_module {
use pyo3::prelude::*;
#[pyfunction]
fn func() -> String {
"child_module.func".to_string()
}
}
$ uv run python >>> import submodule_example >>> submodule_example.child_module.func() 'child_module.func'
サブモジュールを別のソースファイルで定義する例。
use pyo3::prelude::*; pub(crate) use sub::*;
/// 別ファイルのサブモジュール
#[pymodule]
pub(crate) mod sub {
use pyo3::prelude::*;
/// 別ファイルのサブモジュールのクラス1
#[pyclass]
pub struct SubClass1 {}
/// 別ファイルのサブモジュールのクラス2
#[pyclass]
pub struct SubClass2 {}
}
Pythonのサブモジュールとして扱うためには、modに#[pymodule]を付ける必要がある。
なので、わざわざmodを定義している。
ただ、sub.rs内にsubというモジュールを作ると、Rustとしてはcrate::sub::subになってしまう。
これだと他のソースファイルから使うには不格好なので、「use sub::*;」によってcrate::subとして扱えるようにしてみた。
use pyo3::prelude::*; mod sub;
/// 親モジュール
///
/// NOTE:
/// サブモジュールの実験
#[pymodule]
mod submodule_example {
use pyo3::prelude::*;
#[pymodule_export]
use super::sub::sub;
#[pyfunction]
pub fn create_sub_class1() -> super::sub::SubClass1 {
super::sub::SubClass1 {}
}
}
mod sub;」に#[pymodule]を付けてみたけど、コンパイルエラーになった。use super::sub::pysub as sub;」にして名前を付けてみたけど、Pythonとしてはpysubのままだった。$ uv run python >>> import submodule_example >>> help(submodule_example.sub.SubClass1) Help on class SubClass1: class SubClass1(builtins.object) | 別ファイルのサブモジュールのクラス1 >>> submodule_example.create_sub_class1() <sub.SubClass1 object at 0x00000270039D8870>
例外クラスをerror.rsで実装する例。
サブモジュールを別ファイルにする例と同様にpymoduleの中で例外クラスを定義してみたが、create_exception!マクロによって作られた例外クラスはエクスポートされなかった。
また、create_exception!マクロには#[pymodule_export]を付けることが出来ない。(コンパイルエラーになる)
そこで、modの外側でcreate_exception!マクロによって例外クラスを定義し、pyclassの中でエクスポートする。
use pyo3::{exceptions::PyException, *};
/// 例外モジュール
#[pymodule]
pub(crate) mod error {
#[pymodule_export]
use super::{MyError1, MyError2, MyError3};
}
create_exception!(submodule_example.error, MyError1, PyException, "エラー1");
create_exception!(submodule_example.error, MyError2, PyException, "エラー2");
create_exception!(submodule_example.error, MyError3, MyError1, "エラー3");
use pyo3::prelude::*; mod error;
/// 親モジュール
///
/// NOTE:
/// サブモジュールの実験
#[pymodule]
mod submodule_example {
use pyo3::prelude::*;
#[pymodule_export]
use super::error::error;
#[pyfunction]
pub fn raise1() -> PyResult<()> {
use crate::error::MyError1;
Err(MyError1::new_err("test1"))
}
}
$ uv run python
>>> import submodule_example
>>> submodule_example.raise1()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
submodule_example.error.MyError1: test1
>>> try:
... submodule_example.raise1()
... except submodule_example.error.MyError1 as e:
... print(f"1: {e}")
...
1: test1
pyo3-stub-genによってサブモジュール有りの型スタブファイルを作るには、プロジェクトの構成をPython/Rust混合構成にする必要がある。
また、型スタブを出力する対象のクラスや関数に、サブモジュール名を付ける必要がある。
PyO3のデフォルトのプロジェクト構成は「Pure Rust layout」という構成である。
(pyo3-stub-genによって型スタブファイルを生成すると、プロジェクトディレクトリー直下に1ファイルだけ作られる)
この状態でサブモジュールを実装してstub_genを実行すると、以下のようなエラーになる。
error: Pure Rust layout does not support multiple modules
or submodules. Found 4 modules: 'submodule_example',
'submodule_example.child_module', 'submodule_example.sub',
'submodule_example.submodule'. Please use mixed Python/Rust layout (add
`python-source` to [tool.maturin] in pyproject.toml) if you need multiple
modules or submodules.
「mixed Python/Rust layout(Python/Rust混合構成)」にする必要がある。
(Python/Rust混合構成にすると、stub_gen実行時にサブモジュール毎ディレクトリーが作られ、それぞれのディレクトリーの下に型スタブファイルが出力される)
Python/Rust混合構成(mixed Python/Rust layout)は、Rustのソースコードの他にPythonのソースコードも扱う構成。
Pythonプログラムから呼ばれるコードはPythonソースコードに記述し、そこからRustモジュールを呼び出す形になる。
型スタブファイル(拡張子pyiのファイル)もPythonソースコードと同じサブディレクトリーに配置されることになる。
Python/Rust混合構成にするには、pyproject.tomlに[tool.maturin]の設定を追加する。
ついでに、cache-keysにpyファイルも含めておくと(pyファイルを更新した際もuv run実行時に再ビルドされるので)便利。
〜
[tool.uv]
cache-keys = [{ file = "pyproject.toml" }, { file = "src/**/*.rs" }, { file = "Cargo.toml" }, { file = "Cargo.lock" }, { file = "python/**/*.py" }]
[tool.maturin]
python-source = "python"
それから、pythonというディレクトリーを作り、その下にPythonモジュール名のディレクトリーを作る。
さらに、そのディレクトリーの下に__init__pyを作成する。
from .submodule_example import *
__all__ = [name for name in globals() if not name.startswith("_")]
すなわち、以下のようなディレクトリー構成になる。
※stub_genを実行すると、python/モジュール/の下にサブモジュールのディレクトリーが作られ、それぞれの下に__init__.pyiファイルが生成される。
pyo3-stub-genで出力対象とするクラスや関数には#[gen_stub_pyclass]や#[gen_stub_pyfunction]を付けるが、さらにモジュール名も付ける必要がある。
※サブモジュール以外でもモジュール名を付けないと、出力対象外になってしまう。
クラスの場合はpyclassのmoduleでモジュール名を付ける。
#[pymodule]
pub(crate) mod sub {
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
/// 別ファイルのサブモジュールのクラス1
#[gen_stub_pyclass]
#[pyclass(module = "submodule_example.sub")]
pub struct SubClass1 {}
#[gen_stub_pymethods]
#[pymethods]
impl SubClass1 {
〜
}
}
関数の場合はgen_stub_pyfunctionのmoduleでモジュール名を付ける。
/// インライン サブモジュール
#[pymodule]
mod submodule {
use pyo3::prelude::*;
use pyo3_stub_gen::derive::*;
#[gen_stub_pyfunction(module = "submodule_example.submodule")]
#[pyfunction]
fn func() -> String {
"submodule.func".to_string()
}
}
例外クラスの場合は、(通常通り)create_exception!マクロにモジュール名を指定する。[2026-02-22]
ただし、pyo3のcreate_exception!マクロではなく、pyo3-stub-genのcreate_exception!マクロ に変更する。
use pyo3::{exceptions::PyException, *};
use pyo3_stub_gen::create_exception;
/// 例外モジュール
#[pymodule]
pub(crate) mod error {
#[pymodule_export]
use super::{MyError1, MyError2, MyError3};
}
create_exception!(submodule_example.error, MyError1, PyException, "エラー1");
create_exception!(submodule_example.error, MyError2, PyException, "エラー2");
create_exception!(submodule_example.error, MyError3, MyError1, "エラー3");
pyo3-stub-genのstub_genを実行すると、pythonディレクトリーの下にサブモジュールのディレクトリーが作られ、その下に型スタブファイル(__init__.pyi)が生成される。
> cd submodule-example > cargo run --bin stub_gen
pdoc3でAPIドキュメントを生成する場合も、Python/Rust混合構成にする必要がある。
それから、pythonディレクトリーの下のサブモジュールのディレクトリーを手動で作り、それぞれのディレクトリーの下に__init.pyを置く。
トップレベルディレクトリー以外の__init__.pyの内容は以下のようにする。
from submodule_example.submodule_example import child_module as _rust
for _name in _rust.__all__:
globals()[_name] = getattr(_rust, _name)
__all__ = _rust.__all__
submodule_example.submodule_exampleは、Rust側で定義されたsubmodule_exampleモジュールを指しているらしい。(ビルドして作られたpydファイル内にあるらしい)
submodule_exampleパッケージのDocstringを書きたい場合は、(Rustのソースコード上ではなく)submodule_example/__init__.pyに書く。
"""
サブモジュールの実験(トップ)
"""
from .submodule_example import *
__all__ = [name for name in globals() if not name.startswith("_")]
これで、pdoc3でサブモジュール付きのHTMLが生成できる。
> submodule-example
> uv run pdoc submodule_example -o docs --html --force
> tree /f docs
C:\TMP\SUBMODULE-EXAMPLE\DOCS
└─submodule_example
│ index.html
│ submodule_example.html
│
├─child_module
│ index.html
│
├─error
│ index.html
│
├─sub
│ index.html
│
└─submodule
index.html
ただ、生成されたHTMLを見ると、サブモジュール一覧に「submodule_example.submodule_example」が出ているのが気になる。
これを消すには、HTML生成時にフィルター条件を付けるスクリプトを用意する。
import pdoc
import os
def docfilter(obj):
return "submodule_example.submodule_example" not in obj.refname
for mod_name in ["submodule_example", "submodule_example.submodule", "submodule_example.sub", "submodule_example.child_module", "submodule_example.error"]:
out_path = f"docs/{mod_name.replace('.', '/')}/index.html"
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
f.write(pdoc.html(mod_name, docfilter=docfilter))
> cd submodule-example > uv run python generate_docs.py
だたまぁ、こんなスクリプトを使う方法よりも、Rustのモジュールを不可視にする(モジュール名の先頭にアンダースコアを付ける)方が素直だと思う。