Skip to content

Commit 880e10e

Browse files
authored
Merge pull request #99 from wshlavacek/block-dispatch-validation
Validate model and network block dispatch
2 parents a13f8b8 + 926f3cc commit 880e10e

3 files changed

Lines changed: 238 additions & 16 deletions

File tree

bionetgen/modelapi/model.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,25 +175,47 @@ def add_block(self, block):
175175
Adds the given block object to the model, uses the
176176
name of the block object to determine what block it is
177177
"""
178-
bname = block.name.replace(" ", "_")
179-
# TODO: fix this exception
180-
if bname == "reaction_rules":
181-
bname = "rules"
182-
block_adder = getattr(self, "add_{}_block".format(bname))
178+
block_adder = self._resolve_block_adder(block.name)
183179
block_adder(block)
184180

185181
def add_empty_block(self, block_name):
186182
"""
187183
Makes an empty block object from a given block name and
188184
adds it to the model object.
189185
"""
190-
bname = block_name.replace(" ", "_")
191-
# TODO: fix this exception
192-
if bname == "reaction_rules":
193-
bname = "rules"
194-
block_adder = getattr(self, "add_{}_block".format(bname))
186+
block_adder = self._resolve_block_adder(block_name)
195187
block_adder()
196188

189+
def _resolve_block_adder(self, block_name):
190+
"""
191+
Resolve supported block names to block adders.
192+
193+
Block names are normalized by replacing spaces with underscores, and
194+
the historical ``reaction_rules`` alias continues to map to ``rules``.
195+
"""
196+
normalized_name = block_name.replace(" ", "_")
197+
block_adders = {
198+
"parameters": self.add_parameters_block,
199+
"compartments": self.add_compartments_block,
200+
"molecule_types": self.add_molecule_types_block,
201+
"species": self.add_species_block,
202+
"observables": self.add_observables_block,
203+
"functions": self.add_functions_block,
204+
"energy_patterns": self.add_energy_patterns_block,
205+
"population_maps": self.add_population_maps_block,
206+
"rules": self.add_rules_block,
207+
"reaction_rules": self.add_rules_block,
208+
"protocol": self.add_protocol_block,
209+
"actions": self.add_actions_block,
210+
}
211+
if normalized_name not in block_adders:
212+
supported_names = ", ".join(block_adders)
213+
raise ValueError(
214+
f"Unsupported block name '{block_name}'. "
215+
f"Supported block names: {supported_names}"
216+
)
217+
return block_adders[normalized_name]
218+
197219
def add_parameters_block(self, block=None):
198220
"""
199221
Adds a parameters block to the model object.

bionetgen/network/network.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,35 @@ def __iter__(self):
109109
return active_ordered_blocks.__iter__()
110110

111111
def add_block(self, block):
112-
bname = block.name.replace(" ", "_")
113-
# TODO: fix this exception
114-
block_adder = getattr(self, "add_{}_block".format(bname))
112+
block_adder = self._resolve_block_adder(block.name)
115113
block_adder(block)
116114

117115
def add_empty_block(self, block_name):
118-
bname = block_name.replace(" ", "_")
119-
# TODO: fix this exception
120-
block_adder = getattr(self, "add_{}_block".format(bname))
116+
block_adder = self._resolve_block_adder(block_name)
121117
block_adder()
122118

119+
def _resolve_block_adder(self, block_name):
120+
"""
121+
Resolve supported block names to block adders.
122+
123+
Block names are normalized by replacing spaces with underscores before
124+
dispatch so callers can use parser-style or attribute-style names.
125+
"""
126+
normalized_name = block_name.replace(" ", "_")
127+
block_adders = {
128+
"parameters": self.add_parameters_block,
129+
"species": self.add_species_block,
130+
"reactions": self.add_reactions_block,
131+
"groups": self.add_groups_block,
132+
}
133+
if normalized_name not in block_adders:
134+
supported_names = ", ".join(block_adders)
135+
raise ValueError(
136+
f"Unsupported block name '{block_name}'. "
137+
f"Supported block names: {supported_names}"
138+
)
139+
return block_adders[normalized_name]
140+
123141
def add_parameters_block(self, block=None):
124142
if block is not None:
125143
# TODO: Transition to BNGErrors and logging
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Focused tests for model and network block dispatch validation."""
2+
3+
import pytest
4+
5+
from bionetgen.modelapi.blocks import (
6+
ActionBlock,
7+
CompartmentBlock,
8+
EnergyPatternBlock,
9+
FunctionBlock,
10+
MoleculeTypeBlock,
11+
ObservableBlock,
12+
ParameterBlock,
13+
PopulationMapBlock,
14+
ProtocolBlock,
15+
RuleBlock,
16+
SpeciesBlock,
17+
)
18+
from bionetgen.network.blocks import (
19+
NetworkGroupBlock,
20+
NetworkParameterBlock,
21+
NetworkReactionBlock,
22+
NetworkSpeciesBlock,
23+
)
24+
25+
26+
def _make_model_bypass_init():
27+
from bionetgen.modelapi.model import bngmodel
28+
29+
model = object.__new__(bngmodel)
30+
model.active_blocks = []
31+
model._block_order = [
32+
"parameters",
33+
"compartments",
34+
"molecule_types",
35+
"species",
36+
"observables",
37+
"functions",
38+
"energy_patterns",
39+
"population_maps",
40+
"rules",
41+
"protocol",
42+
"actions",
43+
]
44+
model.model_name = "test_model"
45+
model.model_path = "/fake/test.bngl"
46+
model.parameters = ParameterBlock()
47+
model.compartments = CompartmentBlock()
48+
model.molecule_types = MoleculeTypeBlock()
49+
model.species = SpeciesBlock()
50+
model.observables = ObservableBlock()
51+
model.functions = FunctionBlock()
52+
model.energy_patterns = EnergyPatternBlock()
53+
model.population_maps = PopulationMapBlock()
54+
model.rules = RuleBlock()
55+
model.protocol = ProtocolBlock()
56+
model.actions = ActionBlock()
57+
return model
58+
59+
60+
def _make_network_bypass_init():
61+
from bionetgen.network.network import Network
62+
63+
net = object.__new__(Network)
64+
net.active_blocks = []
65+
net.block_order = ["parameters", "species", "reactions", "groups"]
66+
net.network_name = "test"
67+
net.parameters = NetworkParameterBlock()
68+
net.species = NetworkSpeciesBlock()
69+
net.reactions = NetworkReactionBlock()
70+
net.groups = NetworkGroupBlock()
71+
return net
72+
73+
74+
@pytest.mark.parametrize(
75+
("block_cls", "attr_name"),
76+
[
77+
(ParameterBlock, "parameters"),
78+
(RuleBlock, "rules"),
79+
(ProtocolBlock, "protocol"),
80+
],
81+
)
82+
def test_model_add_block_dispatches_supported_block(block_cls, attr_name):
83+
model = _make_model_bypass_init()
84+
block = block_cls()
85+
86+
model.add_block(block)
87+
88+
assert getattr(model, attr_name) is block
89+
assert attr_name in model.active_blocks
90+
91+
92+
@pytest.mark.parametrize(
93+
("block_name", "attr_name", "block_cls"),
94+
[
95+
("observables", "observables", ObservableBlock),
96+
("reaction_rules", "rules", RuleBlock),
97+
("protocol", "protocol", ProtocolBlock),
98+
],
99+
)
100+
def test_model_add_empty_block_dispatches_supported_name(
101+
block_name, attr_name, block_cls
102+
):
103+
model = _make_model_bypass_init()
104+
delattr(model, attr_name)
105+
106+
model.add_empty_block(block_name)
107+
108+
assert isinstance(getattr(model, attr_name), block_cls)
109+
110+
111+
def test_model_add_block_invalid_name_raises_value_error():
112+
model = _make_model_bypass_init()
113+
114+
class FakeBlock:
115+
name = "not a block"
116+
117+
with pytest.raises(ValueError, match="Unsupported block name 'not a block'"):
118+
model.add_block(FakeBlock())
119+
120+
assert "not_a_block" not in model.active_blocks
121+
assert not hasattr(model, "not_a_block")
122+
123+
124+
def test_model_add_empty_block_invalid_name_raises_value_error():
125+
model = _make_model_bypass_init()
126+
127+
with pytest.raises(ValueError, match="Unsupported block name 'not a block'"):
128+
model.add_empty_block("not a block")
129+
130+
assert "not_a_block" not in model.active_blocks
131+
assert not hasattr(model, "not_a_block")
132+
133+
134+
@pytest.mark.parametrize(
135+
("block_cls", "attr_name"),
136+
[
137+
(NetworkParameterBlock, "parameters"),
138+
(NetworkSpeciesBlock, "species"),
139+
(NetworkReactionBlock, "reactions"),
140+
(NetworkGroupBlock, "groups"),
141+
],
142+
)
143+
def test_network_add_block_dispatches_supported_block(block_cls, attr_name):
144+
net = _make_network_bypass_init()
145+
block = block_cls()
146+
147+
net.add_block(block)
148+
149+
assert getattr(net, attr_name) is block
150+
assert attr_name in net.active_blocks
151+
152+
153+
def test_network_add_empty_block_dispatches_supported_name():
154+
net = _make_network_bypass_init()
155+
delattr(net, "groups")
156+
157+
net.add_empty_block("groups")
158+
159+
assert isinstance(net.groups, NetworkGroupBlock)
160+
161+
162+
def test_network_add_block_invalid_name_raises_value_error():
163+
net = _make_network_bypass_init()
164+
165+
class FakeBlock:
166+
name = "not a block"
167+
168+
with pytest.raises(ValueError, match="Unsupported block name 'not a block'"):
169+
net.add_block(FakeBlock())
170+
171+
assert "not_a_block" not in net.active_blocks
172+
assert not hasattr(net, "not_a_block")
173+
174+
175+
def test_network_add_empty_block_invalid_name_raises_value_error():
176+
net = _make_network_bypass_init()
177+
178+
with pytest.raises(ValueError, match="Unsupported block name 'not a block'"):
179+
net.add_empty_block("not a block")
180+
181+
assert "not_a_block" not in net.active_blocks
182+
assert not hasattr(net, "not_a_block")

0 commit comments

Comments
 (0)