|
29 | 29 | UPPER_BOUND, |
30 | 30 | ) |
31 | 31 | from petab.v2.core import * |
| 32 | +from petab.v2.core import ExtensionConfig |
| 33 | +from petab.v2.lint import ValidationIssueSeverity |
32 | 34 | from petab.v2.models.sbml_model import SbmlModel |
33 | 35 | from petab.v2.petab1to2 import petab1to2 |
34 | 36 |
|
@@ -595,6 +597,83 @@ def test_problem_config_paths(): |
595 | 597 | # see also https://github.com/pydantic/pydantic/issues/8575 |
596 | 598 |
|
597 | 599 |
|
| 600 | +def test_problem_config_generic_extension(): |
| 601 | + """A generic (non-sciml) extension is parsed per the PEtab v2 schema: |
| 602 | + `version` and `required` at the top level, plus arbitrary |
| 603 | + extension-specific keys alongside them.""" |
| 604 | + pc = ProblemConfig( |
| 605 | + parameter_files=["parameters.tsv"], |
| 606 | + measurement_files=["measurements.tsv"], |
| 607 | + observable_files=["observables.tsv"], |
| 608 | + extensions={ |
| 609 | + "my_ext": { |
| 610 | + "version": "1.0.0", |
| 611 | + "required": False, |
| 612 | + "some_key": "some_value", |
| 613 | + } |
| 614 | + }, |
| 615 | + ) |
| 616 | + ext = pc.extensions["my_ext"] |
| 617 | + assert isinstance(ext, ExtensionConfig) |
| 618 | + assert ext.version == "1.0.0" |
| 619 | + assert ext.required is False |
| 620 | + assert ext.some_key == "some_value" |
| 621 | + |
| 622 | + dumped = pc.model_dump(by_alias=True)["extensions"]["my_ext"] |
| 623 | + assert dumped == { |
| 624 | + "version": "1.0.0", |
| 625 | + "required": False, |
| 626 | + "some_key": "some_value", |
| 627 | + } |
| 628 | + |
| 629 | + |
| 630 | +def test_problem_config_extensions_rejects_non_dict(): |
| 631 | + """`extensions` must be a dict keyed by extension ID (see #474) -- a |
| 632 | + list is not a valid PEtab v2 problem configuration.""" |
| 633 | + with pytest.raises(ValidationError): |
| 634 | + ProblemConfig( |
| 635 | + parameter_files=["parameters.tsv"], |
| 636 | + measurement_files=["measurements.tsv"], |
| 637 | + observable_files=["observables.tsv"], |
| 638 | + extensions=[{"version": "1.0.0", "required": False}], |
| 639 | + ) |
| 640 | + |
| 641 | + |
| 642 | +def test_validate_unsupported_extension_severity(): |
| 643 | + """libpetab-python doesn't mathematically interpret extensions, so an |
| 644 | + unsupported extension only ever produces a WARNING (that the problem |
| 645 | + can't be fully linted) -- regardless of `required`. Rejecting a problem |
| 646 | + that uses an unsupported `required` extension is up to the consumer |
| 647 | + (e.g. a simulator) that actually interprets it.""" |
| 648 | + problem = Problem() |
| 649 | + problem.model = SbmlModel.from_antimony(""" |
| 650 | + model m |
| 651 | + species A; |
| 652 | + A = 1; |
| 653 | + k1 = 1; |
| 654 | + R1: A -> ; k1 * A; |
| 655 | + end |
| 656 | + """) |
| 657 | + problem.add_observable("obs_A", "A", noise_formula="1") |
| 658 | + problem.add_parameter( |
| 659 | + "k1", estimate=True, lb=1e-5, ub=1e5, nominal_value=1 |
| 660 | + ) |
| 661 | + problem.add_measurement("obs_A", time=1, measurement=1, experiment_id="") |
| 662 | + assert problem.validate() == [] |
| 663 | + |
| 664 | + for required in (False, True): |
| 665 | + problem.config = ProblemConfig( |
| 666 | + extensions={"my_ext": {"version": "1.0.0", "required": required}} |
| 667 | + ) |
| 668 | + results = problem.validate() |
| 669 | + assert not results.has_errors() |
| 670 | + assert any( |
| 671 | + r.level == ValidationIssueSeverity.WARNING |
| 672 | + and "my_ext" in r.message |
| 673 | + for r in results |
| 674 | + ) |
| 675 | + |
| 676 | + |
598 | 677 | def test_get_changes_for_period(): |
599 | 678 | """Test getting changes for a specific period.""" |
600 | 679 | problem = Problem() |
|
0 commit comments