Skip to content

Commit b99e135

Browse files
committed
API: forbid overrides in BoundaryRegistry.register unless unsafe mutations are explicitly allowed.
1 parent 8bba9ba commit b99e135

File tree

3 files changed

+100
-52
lines changed

3 files changed

+100
-52
lines changed

src/gpgi/_boundaries.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import warnings
43
from collections.abc import Callable
54
from typing import TYPE_CHECKING, Any, Literal, cast
65

@@ -60,13 +59,59 @@ def _validate_recipe(recipe: BoundaryRecipeT) -> None:
6059
)
6160

6261
def register(
63-
self, key: str, recipe: BoundaryRecipeT, *, skip_validation: bool = False
62+
self,
63+
key: str,
64+
recipe: BoundaryRecipeT,
65+
*,
66+
skip_validation: bool = False,
67+
allow_unsafe_override: bool = False,
6468
) -> None:
69+
"""
70+
Register a new boundary function.
71+
72+
Parameters
73+
----------
74+
key: str
75+
A unique identifier (ideally a meaningful name) to associate with
76+
the function.
77+
78+
recipe: Callable
79+
A function matching the signature (order and names of arguments) of
80+
gpgi's builtin boundary recipes.
81+
82+
skip_validation: bool, optional, keyword-only (default: False)
83+
If set to True, signature validation is skipped.
84+
This is meant to allow bypassing hypothetical bugs in the validation
85+
routine.
86+
87+
allow_unsafe_override: bool, optional, keyword-only (default: False)
88+
If set to True, registering a new function under an existing key
89+
will not raise an exception. Note however that doing so is not
90+
thread-safe.
91+
92+
Raises
93+
------
94+
ValueError:
95+
- if skip_validation==False and the signature of the recipe doesn't meet
96+
the requirements.
97+
- if allow_unsafe_override==False and a new function is being registered
98+
under an already used key. Registering the same exact function under
99+
multiple times either under the same key or another, unused key, is
100+
always safe so it does not raise.
101+
"""
102+
if key in self._registry:
103+
if recipe is self._registry[key]:
104+
return
105+
elif not allow_unsafe_override:
106+
raise ValueError(
107+
f"Another function is already registered with {key=!r}. "
108+
"If you meant to override the existing function, "
109+
"consider setting allow_unsafe_override=True"
110+
)
111+
65112
if not skip_validation:
66113
self._validate_recipe(recipe)
67114

68-
if key in self._registry:
69-
warnings.warn(f"Overriding existing method {key!r}", stacklevel=2)
70115
self._registry[key] = recipe
71116

72117
def __getitem__(self, key: str) -> BoundaryRecipeT:

tests/test_boundary_registry.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import pytest
2+
3+
from gpgi._boundaries import BoundaryRegistry
4+
5+
6+
def test_boundary_register_overrides():
7+
registry = BoundaryRegistry()
8+
9+
def test_recipe1(
10+
same_side_active_layer,
11+
same_side_ghost_layer,
12+
opposite_side_active_layer,
13+
opposite_side_ghost_layer,
14+
weight_same_side_active_layer,
15+
weight_same_side_ghost_layer,
16+
weight_opposite_side_active_layer,
17+
weight_opposite_side_ghost_layer,
18+
side,
19+
metadata,
20+
): ...
21+
def test_recipe2(
22+
same_side_active_layer,
23+
same_side_ghost_layer,
24+
opposite_side_active_layer,
25+
opposite_side_ghost_layer,
26+
weight_same_side_active_layer,
27+
weight_same_side_ghost_layer,
28+
weight_opposite_side_active_layer,
29+
weight_opposite_side_ghost_layer,
30+
side,
31+
metadata,
32+
): ...
33+
34+
registry.register("test1", test_recipe1)
35+
assert registry["test1"] is test_recipe1
36+
37+
# registering the same function a second time shouldn't raise
38+
registry.register("test1", test_recipe1)
39+
40+
with pytest.raises(
41+
ValueError,
42+
match="Another function is already registered with key='test1'",
43+
):
44+
registry.register("test1", test_recipe2)
45+
46+
# check that we raised in time to preserve state
47+
assert registry["test1"] is test_recipe1
48+
49+
# if we explicitly allow unsafe mutations, this should not raise
50+
registry.register("test1", test_recipe2, allow_unsafe_override=True)
51+
assert registry["test1"] is test_recipe2

tests/test_deposit.py

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -396,54 +396,6 @@ def _my_recipe(a, b, c, d, e, f):
396396
ds.boundary_recipes.register("my", _my_recipe)
397397

398398

399-
def test_warn_register_override(capsys):
400-
nx = ny = 64
401-
nparticles = 100
402-
403-
prng = np.random.RandomState(0)
404-
ds = gpgi.load(
405-
geometry="cartesian",
406-
grid={
407-
"cell_edges": {
408-
"x": np.linspace(-1, 1, nx),
409-
"y": np.linspace(-1, 1, ny),
410-
},
411-
},
412-
particles={
413-
"coordinates": {
414-
"x": 2 * (prng.normal(0.5, 0.25, nparticles) % 1 - 0.5),
415-
"y": 2 * (prng.normal(0.5, 0.25, nparticles) % 1 - 0.5),
416-
},
417-
"fields": {
418-
"mass": np.ones(nparticles),
419-
},
420-
},
421-
metadata={"fac": 1},
422-
)
423-
424-
def _my_recipe(
425-
same_side_active_layer,
426-
same_side_ghost_layer,
427-
opposite_side_active_layer,
428-
opposite_side_ghost_layer,
429-
weight_same_side_active_layer,
430-
weight_same_side_ghost_layer,
431-
weight_opposite_side_active_layer,
432-
weight_opposite_side_ghost_layer,
433-
side,
434-
metadata,
435-
):
436-
print("gotcha")
437-
return same_side_active_layer * metadata["fac"]
438-
439-
with pytest.warns(UserWarning, match="Overriding existing method 'open'"):
440-
ds.boundary_recipes.register("open", _my_recipe)
441-
442-
ds.deposit("mass", method="tsc")
443-
out, err = capsys.readouterr()
444-
assert out == "gotcha\n" * 4
445-
446-
447399
def test_register_custom_boundary_recipe(sample_2D_dataset):
448400
def _my_recipe(
449401
same_side_active_layer,

0 commit comments

Comments
 (0)