Skip to content

Commit fe8a01e

Browse files
committed
Centralize action-argument validation in modelapi
Route `Action.__init__` (in `structs.py`) through a single `ActionList.validate_action` method (added to `utils.py`) so that direct construction of `Action` instances applies the same schema check as the parser. Previously the inline validation in `Action.__init__` had two bugs: - For positional actions (`no_setter_syntax` / `square_braces`, e.g. `setConcentration`, `quit`, `saveConcentrations`) the `arg_dict[type]` lookup returns an empty list `[]`, so the `len(valid_arg_list) > 0` guard skipped validation entirely — any args were accepted. - For unknown action types the error message was good, but it ran AFTER setting `self.type`, leaving a half-constructed object alive in the rare case someone catches the exception. The new `validate_action` adds: - positional actions store args as a dict with `None` values (the canonical shape the parser emits) — keyword-shape dicts passed by hand are rejected with a clear error. - per-action positional arity (`positional_arity` table) catches `Action("quit", {'"x"': None})` (too many) and `Action("setConcentration", {'"A()"': None})` (too few). - normal-type validation: arg names must be in `arg_dict[type]` (existing behavior, just centralized). `Action.__init__` is simplified to call `validate_action` and then just track duplicate-arg warnings in a small loop. No behavior change for parse-time construction: the parser already emits the canonical positional-args-as-`{value: None}` shape, so the stricter direct-construction path doesn't reject anything the parser produces.
1 parent f571e3a commit fe8a01e

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
@@ -1,5 +1,5 @@
11
import os, subprocess
2-
from bionetgen.core.exc import BNGPerlError
2+
from bionetgen.core.exc import BNGParseError, BNGPerlError
33
from distutils import spawn
44

55
from bionetgen.core.utils.logging import BNGLogger
@@ -458,11 +458,101 @@ def __init__(self):
458458
self.irregular_args["blocks"] = "list"
459459
self.irregular_args["opts"] = "list"
460460

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

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