Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Release Notes


## v2.0.2 (2026-03-29)
## v2.0.2 (unreleased)

### Added

Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/bmipy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
156 changes: 156 additions & 0 deletions src/bmipy/_stub.py
Original file line number Diff line number Diff line change
@@ -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")
30 changes: 30 additions & 0 deletions tests/stub_test.py
Original file line number Diff line number Diff line change
@@ -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)
)
Loading