Skip to content

Commit 00eabe5

Browse files
committed
add BmiStub for partial BMI implementations
1 parent 03e8e44 commit 00eabe5

5 files changed

Lines changed: 200 additions & 4 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,18 @@ repos:
8181
rev: 6.3.0
8282
hooks:
8383
- id: pydocstyle
84-
files: bmipy/.*\.py$
84+
files: ^src/bmipy/.*\.py$
85+
exclude: ^src/bmipy/_stub\.py$
8586
args:
8687
- --convention=numpy
8788
- --add-select=D417
88-
additional_dependencies: [".[toml]"]
89+
- id: pydocstyle
90+
name: pydocstyle (BmiStub)
91+
files: ^src/bmipy/_stub\.py$
92+
args:
93+
- --convention=numpy
94+
- --add-select=D417
95+
- --add-ignore=D102
8996

9097
- repo: https://github.com/pre-commit/mirrors-mypy
9198
rev: v2.4.0

‎CHANGES.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Release Notes
22

33

4-
## v2.0.2 (2026-03-29)
4+
## v2.0.2 (unreleased)
55

66
### Added
77

@@ -11,6 +11,8 @@
1111
* Add BMI template module for code generation ([#46](https://github.com/csdms/bmipy/pull/46))
1212
* Add Python 3.13, 3.14, 3.15 to the CI test matrix
1313
([#68](https://github.com/csdms/bmipy/pull/68), [#85](https://github.com/csdms/bmipy/pull/85))
14+
* Add `BmiStub` class for partial BMI implementations, with all methods raising
15+
`NotImplementedError` unless overridden ([#84](https://github.com/csdms/bmipy/pull/84))
1416

1517
### Changed
1618

‎src/bmipy/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from __future__ import annotations
44

5+
from bmipy._stub import BmiStub
56
from bmipy._version import __version__
67
from bmipy.bmi import Bmi
78

8-
__all__ = ["__version__", "Bmi"]
9+
__all__ = ["__version__", "Bmi", "BmiStub"]

‎src/bmipy/_stub.py‎

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import Any
5+
6+
from bmipy.bmi import Bmi
7+
8+
if TYPE_CHECKING:
9+
import numpy as np
10+
from numpy.typing import NDArray
11+
12+
13+
class BmiStub(Bmi):
14+
"""Concrete BMI base for partial implementations.
15+
16+
Unless overridden, all methods raise ``NotImplementedError``.
17+
"""
18+
19+
def initialize(self, config_file: str) -> None:
20+
raise NotImplementedError("initialize")
21+
22+
def update(self) -> None:
23+
raise NotImplementedError("update")
24+
25+
def update_until(self, time: float) -> None:
26+
raise NotImplementedError("update_until")
27+
28+
def finalize(self) -> None:
29+
raise NotImplementedError("finalize")
30+
31+
def get_component_name(self) -> str:
32+
raise NotImplementedError("get_component_name")
33+
34+
def get_input_item_count(self) -> int:
35+
raise NotImplementedError("get_input_item_count")
36+
37+
def get_input_var_names(self) -> tuple[str, ...]:
38+
raise NotImplementedError("get_input_var_names")
39+
40+
def get_output_item_count(self) -> int:
41+
raise NotImplementedError("get_output_item_count")
42+
43+
def get_output_var_names(self) -> tuple[str, ...]:
44+
raise NotImplementedError("get_output_var_names")
45+
46+
def get_var_grid(self, name: str) -> int:
47+
raise NotImplementedError("get_var_grid")
48+
49+
def get_var_itemsize(self, name: str) -> int:
50+
raise NotImplementedError("get_var_itemsize")
51+
52+
def get_var_location(self, name: str) -> str:
53+
raise NotImplementedError("get_var_location")
54+
55+
def get_var_nbytes(self, name: str) -> int:
56+
raise NotImplementedError("get_var_nbytes")
57+
58+
def get_var_type(self, name: str) -> str:
59+
raise NotImplementedError("get_var_type")
60+
61+
def get_var_units(self, name: str) -> str:
62+
raise NotImplementedError("get_var_units")
63+
64+
def get_current_time(self) -> float:
65+
raise NotImplementedError("get_current_time")
66+
67+
def get_end_time(self) -> float:
68+
raise NotImplementedError("get_end_time")
69+
70+
def get_start_time(self) -> float:
71+
raise NotImplementedError("get_start_time")
72+
73+
def get_time_step(self) -> float:
74+
raise NotImplementedError("get_time_step")
75+
76+
def get_time_units(self) -> str:
77+
raise NotImplementedError("get_time_units")
78+
79+
def get_value(self, name: str, dest: NDArray[Any]) -> NDArray[Any]:
80+
raise NotImplementedError("get_value")
81+
82+
def get_value_at_indices(
83+
self, name: str, dest: NDArray[Any], inds: NDArray[np.int_]
84+
) -> NDArray[Any]:
85+
raise NotImplementedError("get_value_at_indices")
86+
87+
def get_value_ptr(self, name: str) -> NDArray[Any]:
88+
raise NotImplementedError("get_value_ptr")
89+
90+
def set_value(self, name: str, src: NDArray[Any]) -> None:
91+
raise NotImplementedError("set_value")
92+
93+
def set_value_at_indices(
94+
self, name: str, inds: NDArray[np.int_], src: NDArray[Any]
95+
) -> None:
96+
raise NotImplementedError("set_value_at_indices")
97+
98+
def get_grid_edge_count(self, grid: int) -> int:
99+
raise NotImplementedError("get_grid_edge_count")
100+
101+
def get_grid_edge_nodes(
102+
self, grid: int, edge_nodes: NDArray[np.int_]
103+
) -> NDArray[np.int_]:
104+
raise NotImplementedError("get_grid_edge_nodes")
105+
106+
def get_grid_face_count(self, grid: int) -> int:
107+
raise NotImplementedError("get_grid_face_count")
108+
109+
def get_grid_face_edges(
110+
self, grid: int, face_edges: NDArray[np.int_]
111+
) -> NDArray[np.int_]:
112+
raise NotImplementedError("get_grid_face_edges")
113+
114+
def get_grid_face_nodes(
115+
self, grid: int, face_nodes: NDArray[np.int_]
116+
) -> NDArray[np.int_]:
117+
raise NotImplementedError("get_grid_face_nodes")
118+
119+
def get_grid_node_count(self, grid: int) -> int:
120+
raise NotImplementedError("get_grid_node_count")
121+
122+
def get_grid_nodes_per_face(
123+
self, grid: int, nodes_per_face: NDArray[np.int_]
124+
) -> NDArray[np.int_]:
125+
raise NotImplementedError("get_grid_nodes_per_face")
126+
127+
def get_grid_origin(
128+
self, grid: int, origin: NDArray[np.float64]
129+
) -> NDArray[np.float64]:
130+
raise NotImplementedError("get_grid_origin")
131+
132+
def get_grid_rank(self, grid: int) -> int:
133+
raise NotImplementedError("get_grid_rank")
134+
135+
def get_grid_shape(self, grid: int, shape: NDArray[np.int_]) -> NDArray[np.int_]:
136+
raise NotImplementedError("get_grid_shape")
137+
138+
def get_grid_size(self, grid: int) -> int:
139+
raise NotImplementedError("get_grid_size")
140+
141+
def get_grid_spacing(
142+
self, grid: int, spacing: NDArray[np.float64]
143+
) -> NDArray[np.float64]:
144+
raise NotImplementedError("get_grid_spacing")
145+
146+
def get_grid_type(self, grid: int) -> str:
147+
raise NotImplementedError("get_grid_type")
148+
149+
def get_grid_x(self, grid: int, x: NDArray[np.float64]) -> NDArray[np.float64]:
150+
raise NotImplementedError("get_grid_x")
151+
152+
def get_grid_y(self, grid: int, y: NDArray[np.float64]) -> NDArray[np.float64]:
153+
raise NotImplementedError("get_grid_y")
154+
155+
def get_grid_z(self, grid: int, z: NDArray[np.float64]) -> NDArray[np.float64]:
156+
raise NotImplementedError("get_grid_z")

‎tests/stub_test.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
import inspect
4+
5+
import pytest
6+
7+
from bmipy import Bmi
8+
from bmipy import BmiStub
9+
10+
11+
def test_stub_implements_everything():
12+
assert isinstance(BmiStub(), Bmi)
13+
14+
15+
@pytest.mark.parametrize("name", sorted(Bmi.__abstractmethods__))
16+
def test_stub_raises_not_implemented(name):
17+
method = getattr(BmiStub(), name)
18+
signature = inspect.signature(method)
19+
20+
kwds = dict.fromkeys(signature.parameters)
21+
22+
with pytest.raises(NotImplementedError, match=name):
23+
method(**kwds)
24+
25+
26+
@pytest.mark.parametrize("name", sorted(Bmi.__abstractmethods__))
27+
def test_stub_matches_signatures(name):
28+
assert inspect.signature(getattr(BmiStub, name)) == inspect.signature(
29+
getattr(Bmi, name)
30+
)

0 commit comments

Comments
 (0)