Skip to content

Commit 014fe1d

Browse files
authored
Merge pull request #86 from wshlavacek/modelapi-centralize-action-validation
Centralize action-argument validation in modelapi
2 parents 88ca3cf + bd19901 commit 014fe1d

2 files changed

Lines changed: 96 additions & 21 deletions

File tree

bionetgen/core/utils/utils.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import shutil
33
import subprocess
44

5-
from bionetgen.core.exc import BNGPerlError
5+
from bionetgen.core.exc import BNGParseError, BNGPerlError
66
from bionetgen.core.utils.logging import BNGLogger
77

88

@@ -460,11 +460,101 @@ def __init__(self):
460460
self.irregular_args["blocks"] = "list"
461461
self.irregular_args["opts"] = "list"
462462

463+
# Expected positional arity (min, max) for actions whose arguments
464+
# are positional rather than `name=>value` keyword pairs. `max=None`
465+
# means unbounded. Actions absent from this table are treated as
466+
# variable-arity (`(0, None)`).
467+
self.positional_arity = {
468+
# no_setter_syntax
469+
"quit": (0, 0),
470+
"setModelName": (1, 1),
471+
"substanceUnits": (0, 1),
472+
"version": (0, 1),
473+
"setOption": (2, 2),
474+
"setConcentration": (2, 2),
475+
"addConcentration": (2, 2),
476+
"setParameter": (2, 2),
477+
# square_braces — list of zero or more entries
478+
"saveConcentrations": (0, None),
479+
"resetConcentrations": (0, None),
480+
"saveParameters": (0, None),
481+
"resetParameters": (0, None),
482+
}
483+
463484
def is_before_model(self, action_name):
464485
if action_name in self.before_model:
465486
return True
466487
return False
467488

489+
def validate_action(self, action_type, action_args):
490+
"""
491+
Centralized schema check shared by parse-time construction
492+
(BNGParser) and direct construction (Action.__init__). Raises
493+
BNGParseError on any inconsistency.
494+
495+
Positional actions (no_setter_syntax / square_braces) store their
496+
arguments as a dict whose keys are the literal positional values
497+
and whose values are None — this canonical shape matches what the
498+
parser emits and is what gen_string serializes back out.
499+
"""
500+
if action_type not in self.possible_types:
501+
raise BNGParseError(message=f"Action type {action_type} not recognized!")
502+
503+
if not isinstance(action_args, dict):
504+
raise BNGParseError(
505+
message=(
506+
f"Action {action_type} arguments must be a dict, "
507+
f"got {type(action_args).__name__}"
508+
)
509+
)
510+
511+
if action_type in self.normal_types:
512+
valid = self.arg_dict.get(action_type)
513+
if valid is None:
514+
if len(action_args) > 0:
515+
raise BNGParseError(
516+
message=(f"Action {action_type} does not take arguments")
517+
)
518+
return
519+
if len(valid) > 0:
520+
for arg_name in action_args:
521+
if arg_name not in valid:
522+
raise BNGParseError(
523+
message=(
524+
f"Action argument {arg_name} not recognized "
525+
f"for action {action_type}!"
526+
)
527+
)
528+
return
529+
530+
# Positional path covers no_setter_syntax and square_braces.
531+
for arg_name, arg_value in action_args.items():
532+
if arg_value is not None:
533+
raise BNGParseError(
534+
message=(
535+
f"Action {action_type} is positional; pass each "
536+
f"value as a dict key mapped to None (got "
537+
f"{arg_name!r}={arg_value!r}). For example: "
538+
f"{{'\"A()\"': None, '100': None}}."
539+
)
540+
)
541+
542+
mn, mx = self.positional_arity.get(action_type, (0, None))
543+
n = len(action_args)
544+
if n < mn or (mx is not None and n > mx):
545+
if mn == mx:
546+
expected = f"exactly {mn}"
547+
elif mx is None:
548+
expected = f"at least {mn}"
549+
else:
550+
expected = f"between {mn} and {mx}"
551+
raise BNGParseError(
552+
message=(
553+
f"Action {action_type} expects {expected} positional "
554+
f"argument(s); got {n}."
555+
)
556+
)
557+
468558
def define_parser(self):
469559
## Define action grammar
470560
import pyparsing as pp

bionetgen/modelapi/structs.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -302,36 +302,21 @@ class Action(ModelObj):
302302
action arguments as keys and their values as values
303303
"""
304304

305-
def __init__(self, action_type=None, action_args={}) -> None:
305+
def __init__(self, action_type=None, action_args=None) -> None:
306306
super().__init__()
307+
if action_args is None:
308+
action_args = {}
307309
AList = ActionList()
308310
self.normal_types = AList.normal_types
309311
self.no_setter_syntax = AList.no_setter_syntax
310312
self.square_braces = AList.square_braces
311313
self.possible_types = AList.possible_types
312-
# Set initial values
313314
self.name = action_type
314315
self.type = action_type
315316
self.args = action_args
316-
# check type
317-
if self.type not in self.possible_types:
318-
raise BNGParseError(message=f"Action type {self.type} not recognized!")
317+
AList.validate_action(action_type, action_args)
319318
seen_args = []
320-
for arg in action_args:
321-
arg_name, arg_value = arg, action_args[arg]
322-
valid_arg_list = AList.arg_dict[self.type]
323-
# TODO: actions that don't take argument names should be parsed separately to check validity of arg-val tuples
324-
# TODO: currently not type checking arguments
325-
if valid_arg_list is None:
326-
raise BNGParseError(
327-
message=f"Argument {arg_name} is given, but action {self.type} does not take arguments"
328-
)
329-
if len(valid_arg_list) > 0:
330-
if arg_name not in AList.arg_dict[self.type]:
331-
raise BNGParseError(
332-
message=f"Action argument {arg_name} not recognized!\nCheck to make sure action is correctly formatted"
333-
)
334-
# TODO: If arg_value is the correct type
319+
for arg_name, arg_value in action_args.items():
335320
if arg_name in seen_args:
336321
print(
337322
f"Warning: argument {arg_name} already given, using latter value {arg_value}"

0 commit comments

Comments
 (0)