|
2 | 2 | import shutil |
3 | 3 | import subprocess |
4 | 4 |
|
5 | | -from bionetgen.core.exc import BNGPerlError |
| 5 | +from bionetgen.core.exc import BNGParseError, BNGPerlError |
6 | 6 | from bionetgen.core.utils.logging import BNGLogger |
7 | 7 |
|
8 | 8 |
|
@@ -460,11 +460,101 @@ def __init__(self): |
460 | 460 | self.irregular_args["blocks"] = "list" |
461 | 461 | self.irregular_args["opts"] = "list" |
462 | 462 |
|
| 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 | + |
463 | 484 | def is_before_model(self, action_name): |
464 | 485 | if action_name in self.before_model: |
465 | 486 | return True |
466 | 487 | return False |
467 | 488 |
|
| 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 | + |
468 | 558 | def define_parser(self): |
469 | 559 | ## Define action grammar |
470 | 560 | import pyparsing as pp |
|
0 commit comments