From 802e7d91796d1d617ac38d7755b192b2609b8639 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Sun, 18 Oct 2026 16:35:35 -0600 Subject: [PATCH] add BmiStub for partial BMI implementations --- .pre-commit-config.yaml | 11 ++- CHANGES.md | 4 +- src/bmipy/__init__.py | 3 +- src/bmipy/_stub.py | 156 ++++++++++++++++++++++++++++++++++++++++ tests/stub_test.py | 30 ++++++++ 5 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 src/bmipy/_stub.py create mode 100644 tests/stub_test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88a9d92..ef0d327 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -81,11 +81,18 @@ repos: rev: 6.3.0 hooks: - id: pydocstyle - files: bmipy/.*\.py$ + files: ^src/bmipy/.*\.py$ + exclude: ^src/bmipy/_stub\.py$ args: - --convention=numpy - --add-select=D417 - additional_dependencies: [".[toml]"] + - id: pydocstyle + name: pydocstyle (BmiStub) + files: ^src/bmipy/_stub\.py$ + args: + - --convention=numpy + - --add-select=D417 + - --add-ignore=D102 - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.4.0 diff --git a/CHANGES.md b/CHANGES.md index c9d6967..b1626a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ # Release Notes -## v2.0.2 (2026-03-29) +## v2.0.2 (unreleased) ### Added @@ -11,6 +11,8 @@ * Add BMI template module for code generation ([#46](https://github.com/csdms/bmipy/pull/46)) * Add Python 3.13, 3.14, 3.15 to the CI test matrix ([#68](https://github.com/csdms/bmipy/pull/68), [#85](https://github.com/csdms/bmipy/pull/85)) +* Add `BmiStub` class for partial BMI implementations, with all methods raising + `NotImplementedError` unless overridden ([#84](https://github.com/csdms/bmipy/pull/84)) ### Changed diff --git a/src/bmipy/__init__.py b/src/bmipy/__init__.py index 7e31121..b61be0b 100644 --- a/src/bmipy/__init__.py +++ b/src/bmipy/__init__.py @@ -2,7 +2,8 @@ from __future__ import annotations +from bmipy._stub import BmiStub from bmipy._version import __version__ from bmipy.bmi import Bmi -__all__ = ["__version__", "Bmi"] +__all__ = ["__version__", "Bmi", "BmiStub"] diff --git a/src/bmipy/_stub.py b/src/bmipy/_stub.py new file mode 100644 index 0000000..3f9b771 --- /dev/null +++ b/src/bmipy/_stub.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +from bmipy.bmi import Bmi + +if TYPE_CHECKING: + import numpy as np + from numpy.typing import NDArray + + +class BmiStub(Bmi): + """Concrete BMI base for partial implementations. + + Unless overridden, all methods raise ``NotImplementedError``. + """ + + def initialize(self, config_file: str) -> None: + raise NotImplementedError("initialize") + + def update(self) -> None: + raise NotImplementedError("update") + + def update_until(self, time: float) -> None: + raise NotImplementedError("update_until") + + def finalize(self) -> None: + raise NotImplementedError("finalize") + + def get_component_name(self) -> str: + raise NotImplementedError("get_component_name") + + def get_input_item_count(self) -> int: + raise NotImplementedError("get_input_item_count") + + def get_input_var_names(self) -> tuple[str, ...]: + raise NotImplementedError("get_input_var_names") + + def get_output_item_count(self) -> int: + raise NotImplementedError("get_output_item_count") + + def get_output_var_names(self) -> tuple[str, ...]: + raise NotImplementedError("get_output_var_names") + + def get_var_grid(self, name: str) -> int: + raise NotImplementedError("get_var_grid") + + def get_var_itemsize(self, name: str) -> int: + raise NotImplementedError("get_var_itemsize") + + def get_var_location(self, name: str) -> str: + raise NotImplementedError("get_var_location") + + def get_var_nbytes(self, name: str) -> int: + raise NotImplementedError("get_var_nbytes") + + def get_var_type(self, name: str) -> str: + raise NotImplementedError("get_var_type") + + def get_var_units(self, name: str) -> str: + raise NotImplementedError("get_var_units") + + def get_current_time(self) -> float: + raise NotImplementedError("get_current_time") + + def get_end_time(self) -> float: + raise NotImplementedError("get_end_time") + + def get_start_time(self) -> float: + raise NotImplementedError("get_start_time") + + def get_time_step(self) -> float: + raise NotImplementedError("get_time_step") + + def get_time_units(self) -> str: + raise NotImplementedError("get_time_units") + + def get_value(self, name: str, dest: NDArray[Any]) -> NDArray[Any]: + raise NotImplementedError("get_value") + + def get_value_at_indices( + self, name: str, dest: NDArray[Any], inds: NDArray[np.int_] + ) -> NDArray[Any]: + raise NotImplementedError("get_value_at_indices") + + def get_value_ptr(self, name: str) -> NDArray[Any]: + raise NotImplementedError("get_value_ptr") + + def set_value(self, name: str, src: NDArray[Any]) -> None: + raise NotImplementedError("set_value") + + def set_value_at_indices( + self, name: str, inds: NDArray[np.int_], src: NDArray[Any] + ) -> None: + raise NotImplementedError("set_value_at_indices") + + def get_grid_edge_count(self, grid: int) -> int: + raise NotImplementedError("get_grid_edge_count") + + def get_grid_edge_nodes( + self, grid: int, edge_nodes: NDArray[np.int_] + ) -> NDArray[np.int_]: + raise NotImplementedError("get_grid_edge_nodes") + + def get_grid_face_count(self, grid: int) -> int: + raise NotImplementedError("get_grid_face_count") + + def get_grid_face_edges( + self, grid: int, face_edges: NDArray[np.int_] + ) -> NDArray[np.int_]: + raise NotImplementedError("get_grid_face_edges") + + def get_grid_face_nodes( + self, grid: int, face_nodes: NDArray[np.int_] + ) -> NDArray[np.int_]: + raise NotImplementedError("get_grid_face_nodes") + + def get_grid_node_count(self, grid: int) -> int: + raise NotImplementedError("get_grid_node_count") + + def get_grid_nodes_per_face( + self, grid: int, nodes_per_face: NDArray[np.int_] + ) -> NDArray[np.int_]: + raise NotImplementedError("get_grid_nodes_per_face") + + def get_grid_origin( + self, grid: int, origin: NDArray[np.float64] + ) -> NDArray[np.float64]: + raise NotImplementedError("get_grid_origin") + + def get_grid_rank(self, grid: int) -> int: + raise NotImplementedError("get_grid_rank") + + def get_grid_shape(self, grid: int, shape: NDArray[np.int_]) -> NDArray[np.int_]: + raise NotImplementedError("get_grid_shape") + + def get_grid_size(self, grid: int) -> int: + raise NotImplementedError("get_grid_size") + + def get_grid_spacing( + self, grid: int, spacing: NDArray[np.float64] + ) -> NDArray[np.float64]: + raise NotImplementedError("get_grid_spacing") + + def get_grid_type(self, grid: int) -> str: + raise NotImplementedError("get_grid_type") + + def get_grid_x(self, grid: int, x: NDArray[np.float64]) -> NDArray[np.float64]: + raise NotImplementedError("get_grid_x") + + def get_grid_y(self, grid: int, y: NDArray[np.float64]) -> NDArray[np.float64]: + raise NotImplementedError("get_grid_y") + + def get_grid_z(self, grid: int, z: NDArray[np.float64]) -> NDArray[np.float64]: + raise NotImplementedError("get_grid_z") diff --git a/tests/stub_test.py b/tests/stub_test.py new file mode 100644 index 0000000..fa3f6a3 --- /dev/null +++ b/tests/stub_test.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import inspect + +import pytest + +from bmipy import Bmi +from bmipy import BmiStub + + +def test_stub_implements_everything(): + assert isinstance(BmiStub(), Bmi) + + +@pytest.mark.parametrize("name", sorted(Bmi.__abstractmethods__)) +def test_stub_raises_not_implemented(name): + method = getattr(BmiStub(), name) + signature = inspect.signature(method) + + kwds = dict.fromkeys(signature.parameters) + + with pytest.raises(NotImplementedError, match=name): + method(**kwds) + + +@pytest.mark.parametrize("name", sorted(Bmi.__abstractmethods__)) +def test_stub_matches_signatures(name): + assert inspect.signature(getattr(BmiStub, name)) == inspect.signature( + getattr(Bmi, name) + )