diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cd51fe7..bcaad44 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -12,6 +12,7 @@ concurrency: env: CARGO_TERM_COLOR: always + PYTEST_ADDOPTS: '--color=yes' jobs: build_and_test: @@ -49,10 +50,17 @@ jobs: - name: Install python deps + Build run: | - uv pip install --system -e ".[test]" maturin --verbose + uv pip install --system -e ".[test,dev]" --verbose - name: Python Tests run: pytest -n auto - name: Rust Tests run: cargo test + + - name: Check formatting + # see “Type hints” section in contributing.md + run: | + cargo run --bin stub_gen + pre-commit run --all-files --show-diff-on-failure || true + git diff --exit-code HEAD diff --git a/.gitignore b/.gitignore index a0926d1..7999651 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ __pycache__/ # Build *.so -*.pyi /target/ /dist/ /docs/_build/ diff --git a/build.rs b/build.rs deleted file mode 100644 index 7e64a81..0000000 --- a/build.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::env; -use std::path::PathBuf; -use std::process::Command; - -fn main() -> Result<(), Box> { - maybe_generate_stubs()?; - Ok(()) -} - -/// Generate stubs if we’re compiling the library -/// Returns `true` if stubs were generated -fn maybe_generate_stubs() -> Result> { - if env::var("CARGO_BIN_NAME") != Err(env::VarError::NotPresent) { - return Ok(false); - } - - // Find an existing `stub_gen` binary or exit silently - let Some(bin_path) = ["debug", "release"] - .into_iter() - .filter_map(|mode| { - let p = PathBuf::from(format!("target/{mode}/stub_gen")); - p.exists().then_some(p) - }) - .next() - else { - return Ok(false); - }; - - // If we’re compiling the library, generate stubs first - Command::new(bin_path).spawn()?.wait()?; - Ok(true) -} diff --git a/docs/contributing.md b/docs/contributing.md index 3f7b3e4..612d198 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -32,10 +32,9 @@ for parallelized tests. Most tests have been copied from the `zarr-python` repo ## Type hints -When authoring Python code, your IDE will not be able to analyze the extension module `zarrs._internal`. -But thanks to [`pyo3-stub-gen`][], we can generate type stubs for it! +Thanks to [`pyo3-stub-gen`][], we can generate type stubs for the `zarrs._internal` module. +If the “Check formatting” CI step fails, run `cargo run --bin stub_gen`, then `pre-commit run --all-files`, and commit the changes. -To build the stub generator, run `cargo build --bin stub_gen`. -Afterwards, whenever you `cargo build`, `maturin build` or interact with your editor’s rust language server (e.g. `rust-analyzer`), the type hints will be updated. +Once `maturin` can be run as a `hatchling` plugin, this can be made automatic. [`pyo3-stub-gen`]: https://github.com/Jij-Inc/pyo3-stub-gen diff --git a/pyproject.toml b/pyproject.toml index f63a6a9..b567afd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ test = [ "hypothesis", "pytest-xdist", ] -dev = ["maturin", "pip"] +dev = ["maturin", "pip", "pre-commit"] doc = ["sphinx>=7.4.6", "myst-parser"] [tool.maturin] @@ -52,13 +52,13 @@ python-source = "python" module-name = "zarrs._internal" features = ["pyo3/extension-module"] - [tool.pytest.ini_options] minversion = "7" testpaths = ["tests"] log_cli_level = "INFO" xfail_strict = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" doctest_optionflags = [ "NORMALIZE_WHITESPACE", "ELLIPSIS", @@ -112,5 +112,7 @@ ignore = [ # allow I, O, l as variable names -> I is the identity matrix, i, j, k, l is reasonable indexing notation "E741", ] +[tool.ruff.lint.per-file-ignores] +"**/*.pyi" = ["ICN001"] [tool.ruff.lint.isort] known-first-party = ["zarrs"] diff --git a/python/zarrs/_internal.pyi b/python/zarrs/_internal.pyi new file mode 100644 index 0000000..6c8ce36 --- /dev/null +++ b/python/zarrs/_internal.pyi @@ -0,0 +1,47 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import typing + +import numpy +import numpy.typing + +class CodecPipelineImpl: + def __new__( + cls, + metadata, + *, + validate_checksums=..., + store_empty_chunks=..., + chunk_concurrent_minimum=..., + chunk_concurrent_maximum=..., + num_threads=..., + ): ... + def retrieve_chunks_and_apply_index( + self, + chunk_descriptions: typing.Sequence[ + tuple[ + tuple[str, typing.Sequence[int], str, typing.Sequence[int]], + typing.Sequence[slice], + typing.Sequence[slice], + ] + ], + value: numpy.NDArray[typing.Any], + ) -> None: ... + def retrieve_chunks( + self, + chunk_descriptions: typing.Sequence[ + tuple[str, typing.Sequence[int], str, typing.Sequence[int]] + ], + ) -> list[numpy.typing.NDArray[numpy.uint8]]: ... + def store_chunks_with_indices( + self, + chunk_descriptions: typing.Sequence[ + tuple[ + tuple[str, typing.Sequence[int], str, typing.Sequence[int]], + typing.Sequence[slice], + typing.Sequence[slice], + ] + ], + value: numpy.NDArray[typing.Any], + ) -> None: ... diff --git a/src/lib.rs b/src/lib.rs index 8cad80a..9c43ece 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -242,6 +242,7 @@ impl CodecPipelineImpl { impl CodecPipelineImpl { #[pyo3(signature = ( metadata, + *, validate_checksums=None, store_empty_chunks=None, chunk_concurrent_minimum=None, diff --git a/tests/test_codecs.py b/tests/test_codecs.py index 0f51eff..42606f0 100644 --- a/tests/test_codecs.py +++ b/tests/test_codecs.py @@ -305,7 +305,7 @@ def test_invalid_metadata(store: Store) -> None: ], ) spath4 = StorePath(store, "invalid_missing_bytes_codec") - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r".*[Cc]odec.*required"): Array.create( spath4, shape=(16, 16), @@ -317,7 +317,9 @@ def test_invalid_metadata(store: Store) -> None: ], ) spath5 = StorePath(store, "invalid_inner_chunk_shape") - with pytest.raises(ValueError): + with pytest.raises( + ValueError, match=r".*shard.*chunk_shape.*array.*shape.*need.*same.*dimensions" + ): Array.create( spath5, shape=(16, 16), @@ -329,7 +331,9 @@ def test_invalid_metadata(store: Store) -> None: ], ) spath6 = StorePath(store, "invalid_inner_chunk_shape") - with pytest.raises(ValueError): + with pytest.raises( + ValueError, match=r".*array.*chunk_shape.*divisible.*shard.*chunk_shape" + ): Array.create( spath6, shape=(16, 16), diff --git a/tests/test_transpose.py b/tests/test_transpose.py index e1764e3..ec91baa 100644 --- a/tests/test_transpose.py +++ b/tests/test_transpose.py @@ -92,7 +92,7 @@ def test_transpose_invalid( data = np.arange(0, 256, dtype="uint16").reshape((1, 32, 8)) spath = StorePath(store, "transpose_invalid") for order in [(1, 0), (3, 2, 1), (3, 3, 1)]: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r".*order"): Array.create( spath, shape=data.shape,