From 91f863366d150d0d3a0c940327c413d57c10608a Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Sat, 9 May 2026 13:00:47 +0200 Subject: [PATCH] - workflow updates - python stubs --- .github/workflows/cargo_test.yml | 52 ++++++++++++++ .github/workflows/publish_crates.yml | 6 +- .github/workflows/publish_pypi.yml | 4 +- .github/workflows/pytest.yml | 2 +- Cargo.toml | 3 +- README.md | 2 +- py/tiffwrite/__init__.py | 16 ++++- py/tiffwrite/py.typed | 0 py/tiffwrite/tiffwrite_rs.pyi | 101 +++++++++++++++++++++++++++ pyproject.toml | 3 + src/py.rs | 26 +++++++ 11 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/cargo_test.yml create mode 100644 py/tiffwrite/py.typed create mode 100644 py/tiffwrite/tiffwrite_rs.pyi diff --git a/.github/workflows/cargo_test.yml b/.github/workflows/cargo_test.yml new file mode 100644 index 0000000..b0c6a59 --- /dev/null +++ b/.github/workflows/cargo_test.yml @@ -0,0 +1,52 @@ +name: PyTest + +on: [push, pull_request, workflow_call] + +jobs: + pytest: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.12", "3.14"] + + steps: + - uses: actions/checkout@v6 + + - name: Restore cache + uses: actions/cache/restore@v4 + with: + path: | + ~/.cache/pip + ~/.cache/pip-wheel + ~/.cache/sccache + ~/.cache/cargo-xwin + ~/.cargo + ~/.osxcross + key: cache-ubuntu-maturin-cross-compile + + - name: Install Rust + run: | + export PATH="$HOME/.cargo/bin:$PATH" + if ! command -v rustc >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + else + rustup update + fi + shell: bash + + - name: "cargo test" + run: |- + export PATH="$HOME/.cargo/bin:$PATH" + cargo test + + - name: Store cache + uses: actions/cache/save@v4 + with: + path: | + ~/.cache/pip + ~/.cache/pip-wheel + ~/.cache/sccache + ~/.cache/cargo-xwin + ~/.cargo + ~/.osxcross + key: cache-ubuntu-maturin-cross-compile \ No newline at end of file diff --git a/.github/workflows/publish_crates.yml b/.github/workflows/publish_crates.yml index db73ee2..654803a 100644 --- a/.github/workflows/publish_crates.yml +++ b/.github/workflows/publish_crates.yml @@ -8,8 +8,10 @@ permissions: jobs: publish_pytest: uses: ./.github/workflows/pytest.yml + publish_cargo_test: + uses: ./.github/workflows/cargo_test.yml crates_io_publish: - needs: [ publish_pytest ] + needs: [ publish_pytest, publish_cargo_test ] name: Publish (crates.io) runs-on: ubuntu-latest timeout-minutes: 25 @@ -50,7 +52,7 @@ jobs: # have passed. - name: "cargo release publish" run: |- - export PATH="$HOME/.osxcross/bin:$PATH" + export PATH="$HOME/.cargo/bin:$PATH" cargo login ${{ secrets.CRATES_IO_API_TOKEN }} cargo release \ publish \ diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index 18b41f8..d438a48 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -8,8 +8,10 @@ permissions: jobs: publish_pytest: uses: ./.github/workflows/pytest.yml + publish_cargo_test: + uses: ./.github/workflows/cargo_test.yml pypi_publish: - needs: [ publish_pytest ] + needs: [ publish_pytest, publish_cargo_test ] name: Publish (pypi.org) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1e8082a..6cd5bca 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -42,7 +42,7 @@ jobs: run: | export PATH="$HOME/.cargo/bin:$PATH" python -m pip install --upgrade pip - pip install maturin ziglang + pip install maturin if ! command -v sccache >/dev/null 2>&1; then cargo install sccache || pip install sccache fi diff --git a/Cargo.toml b/Cargo.toml index d8c475d..3f8fc35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,11 @@ ndarray = "0.17" num = "0.4" numpy = { version = "0.28", optional = true } pyo3 = { version = "0.28", features = ["abi3-py310", "eyre", "generate-import-lib", "multiple-pymethods"], optional = true } +pyo3-stub-gen = { version = "0.22", optional = true } rayon = "1" thiserror = "2" tokio = { version = "1", features = ["fs", "rt", "rt-multi-thread", "time"] } zstd = "0.13" [features] -python = ["dep:pyo3", "dep:numpy", "dep:color-eyre"] +python = ["dep:pyo3", "dep:numpy", "dep:color-eyre", "dep:pyo3-stub-gen"] diff --git a/README.md b/README.md index 7db0fc7..17f5199 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![pytest](https://github.com/wimpomp/tiffwrite/actions/workflows/pytest.yml/badge.svg)](https://github.com/wimpomp/tiffwrite/actions/workflows/pytest.yml) +[![pytest](https://git.wimpomp.nl/wim/tiffwrite/actions/workflows/pytest.yml/badge.svg)](https://git.wimpomp.nl/wim/tiffwrite/actions/workflows/pytest.yml) # Tiffwrite Write BioFormats/ImageJ compatible tiffs with zstd compression in parallel using Rust. diff --git a/py/tiffwrite/__init__.py b/py/tiffwrite/__init__.py index 16ada99..fa4c804 100644 --- a/py/tiffwrite/__init__.py +++ b/py/tiffwrite/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from importlib.metadata import version from itertools import product from pathlib import Path @@ -10,7 +11,7 @@ import numpy as np from numpy.typing import ArrayLike, DTypeLike from tqdm.auto import tqdm -from . import tiffwrite_rs as rs # noqa +from . import tiffwrite_rs as rs __all__ = ["IJTiffFile", "IJTiffParallel", "FrameInfo", "Tag", "tiffwrite"] @@ -236,3 +237,16 @@ try: except ImportError: IJTiffParallel = None + + +def tiffwrite_generate_stub(): + if len(sys.argv) > 1: + path = Path(sys.argv[1]).resolve() + else: + path = Path.cwd().resolve() + if (path / "py" / "tiffwrite" / "__init__.py").exists(): + rs.generate_stub(str(path)) # noqa + (path / "py" / "tiffwrite_rs" / "__init__.pyi").rename(path / "py" / "tiffwrite" / "tiffwrite_rs.pyi") + (path / "py" / "tiffwrite_rs").rmdir() + else: + raise ModuleNotFoundError(str(path / "py" / "tiffwrite" / "__init__.py")) diff --git a/py/tiffwrite/py.typed b/py/tiffwrite/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/py/tiffwrite/tiffwrite_rs.pyi b/py/tiffwrite/tiffwrite_rs.pyi new file mode 100644 index 0000000..56376a6 --- /dev/null +++ b/py/tiffwrite/tiffwrite_rs.pyi @@ -0,0 +1,101 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401, F403, F405 + +import builtins +import typing + +import numpy +import numpy.typing + +__all__ = [ + "IJTiffFile", + "Tag", +] + +class IJTiffFile: + @property + def colors(self) -> typing.Optional[builtins.list[builtins.list[builtins.int]]]: ... + @colors.setter + def colors(self, value: typing.Sequence[builtins.str]) -> None: ... + @property + def colormap(self) -> typing.Optional[builtins.list[builtins.list[builtins.int]]]: ... + @colormap.setter + def colormap(self, value: builtins.str) -> None: ... + @property + def px_size(self) -> typing.Optional[builtins.float]: ... + @px_size.setter + def px_size(self, value: builtins.float) -> None: ... + @property + def delta_z(self) -> typing.Optional[builtins.float]: ... + @delta_z.setter + def delta_z(self, value: builtins.float) -> None: ... + @property + def time_interval(self) -> typing.Optional[builtins.float]: ... + @time_interval.setter + def time_interval(self, value: builtins.float) -> None: ... + @property + def comment(self) -> typing.Optional[builtins.str]: ... + @comment.setter + def comment(self, value: builtins.str) -> None: ... + def __new__(cls, path: builtins.str) -> IJTiffFile: ... + def set_compression(self, compression: builtins.int, level: builtins.int) -> None: + r""" + set zstd compression level: -7 ..= 22 + """ + def append_extra_tag( + self, tag: Tag, czt: typing.Optional[tuple[builtins.int, builtins.int, builtins.int]] = None + ) -> None: ... + def get_tags( + self, czt: typing.Optional[tuple[builtins.int, builtins.int, builtins.int]] = None + ) -> builtins.list[Tag]: ... + def close(self) -> None: ... + def save_f64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_u32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_u16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_i64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_f32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_u8(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_i32(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_u64(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_i16(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + def save_i8(self, frame: numpy.typing.ArrayLike, c: builtins.int, t: builtins.int, z: builtins.int) -> None: ... + +class Tag: + @staticmethod + def byte(code: builtins.int, byte: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def ascii(code: builtins.int, ascii: builtins.str) -> Tag: ... + @staticmethod + def short(code: builtins.int, short: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def long(code: builtins.int, long: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def rational(code: builtins.int, rational: typing.Sequence[builtins.float]) -> Tag: ... + @staticmethod + def sbyte(code: builtins.int, sbyte: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def sshort(code: builtins.int, sshort: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def slong(code: builtins.int, slong: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def srational(code: builtins.int, srational: typing.Sequence[builtins.float]) -> Tag: ... + @staticmethod + def float(code: builtins.int, float: typing.Sequence[builtins.float]) -> Tag: ... + @staticmethod + def double(code: builtins.int, double: typing.Sequence[builtins.float]) -> Tag: ... + @staticmethod + def ifd(code: builtins.int, ifd: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def unicode(code: builtins.int, unicode: builtins.str) -> Tag: ... + @staticmethod + def complex(code: builtins.int, complex: typing.Sequence[tuple[builtins.float, builtins.float]]) -> Tag: ... + @staticmethod + def long8(code: builtins.int, long8: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def slong8(code: builtins.int, slong8: typing.Sequence[builtins.int]) -> Tag: ... + @staticmethod + def ifd8(code: builtins.int, ifd8: typing.Sequence[builtins.int]) -> Tag: ... + def count(self) -> builtins.int: + r""" + get the number of values in the tag + """ diff --git a/pyproject.toml b/pyproject.toml index 55a7a23..d6d162f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ test = ["pytest", "tifffile", "imagecodecs"] homepage = "https://github.com/wimpomp/tiffwrite" repository = "https://github.com/wimpomp/tiffwrite" +[project.scripts] +tiffwrite_generate_stub = "tiffwrite:tiffwrite_generate_stub" + [tool.maturin] python-source = "py" features = ["pyo3/extension-module", "python"] diff --git a/src/py.rs b/src/py.rs index 14623b3..3b86a15 100644 --- a/src/py.rs +++ b/src/py.rs @@ -3,6 +3,9 @@ use num::{Complex, FromPrimitive, Rational32}; use numpy::{AllowTypeChange, PyArrayLike2}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; +use pyo3_stub_gen::{StubGenConfig, StubInfo}; +use std::path::PathBuf; impl From for PyErr { fn from(err: crate::error::Error) -> PyErr { @@ -10,6 +13,7 @@ impl From for PyErr { } } +#[gen_stub_pyclass] #[pyclass(name = "Tag", module = "tiffwrite_rs", subclass, from_py_object)] #[derive(Clone, Debug)] struct PyTag { @@ -17,6 +21,7 @@ struct PyTag { } /// Tiff tag, use one of the constructors to get a tag of a specific type +#[gen_stub_pymethods] #[pymethods] impl PyTag { #[staticmethod] @@ -162,12 +167,14 @@ impl PyTag { } } +#[gen_stub_pyclass] #[pyclass(name = "IJTiffFile", module = "tiffwrite_rs", subclass)] #[derive(Debug)] struct PyIJTiffFile { ijtifffile: Option, } +#[gen_stub_pymethods] #[pymethods] impl PyIJTiffFile { #[new] @@ -333,10 +340,12 @@ impl PyIJTiffFile { macro_rules! impl_save { ($($T:ty: $t:ident $(,)?)*) => { $( + #[gen_stub_pymethods] #[pymethods] impl PyIJTiffFile { fn $t( &mut self, + #[gen_stub(override_type(type_repr="numpy.typing.ArrayLike", imports=("numpy", "numpy.typing")))] frame: PyArrayLike2<$T, AllowTypeChange>, c: usize, t: usize, @@ -365,11 +374,28 @@ impl_save! { f64: save_f64, } +/// generates tiffwrite/tiffwrite_rs.pyi +#[pyfunction] +fn generate_stub(dest_path: String) -> PyResult<()> { + StubInfo::from_project_root( + "tiffwrite_rs".to_string(), + PathBuf::from(dest_path).join("py"), + true, + StubGenConfig::default(), + ) + .map_err(|e| PyValueError::new_err(format!("{:?}", e)))? + .generate() + .map_err(|e| PyValueError::new_err(format!("{:?}", e))) +} + #[pymodule] #[pyo3(name = "tiffwrite_rs")] mod tiffwrite_rs { use pyo3::prelude::*; + #[pymodule_export] + use super::generate_stub; + #[pymodule_export] use super::PyTag;