diff --git a/petab/v1/problem.py b/petab/v1/problem.py index 5c0dcf61..373b6b47 100644 --- a/petab/v1/problem.py +++ b/petab/v1/problem.py @@ -13,10 +13,10 @@ import pandas as pd from pydantic import AnyUrl, BaseModel, Field, RootModel +from ..versions import get_major_version from . import ( conditions, core, - format_version, mapping, measurements, observables, @@ -290,13 +290,13 @@ def get_path(filename): "petab.CompositeProblem.from_yaml() instead." ) - if yaml_config[FORMAT_VERSION] not in {"1", 1, "1.0.0", "2.0.0"}: + major_version = get_major_version(yaml_config) + if major_version not in {1, 2}: raise ValueError( "Provided PEtab files are of unsupported version " - f"{yaml_config[FORMAT_VERSION]}. Expected " - f"{format_version.__format_version__}." + f"{yaml_config[FORMAT_VERSION]}." ) - if yaml_config[FORMAT_VERSION] == "2.0.0": + if major_version == 2: warn("Support for PEtab2.0 is experimental!", stacklevel=2) warn( "Using petab.v1.Problem with PEtab2.0 is deprecated. " @@ -321,7 +321,7 @@ def get_path(filename): if config.parameter_file else None ) - if config.format_version.root in [1, "1", "1.0.0"]: + if major_version == 1: if len(problem0.sbml_files) > 1: # TODO https://github.com/PEtab-dev/libpetab-python/issues/6 raise NotImplementedError( diff --git a/petab/v1/yaml.py b/petab/v1/yaml.py index ecffc48e..51159453 100644 --- a/petab/v1/yaml.py +++ b/petab/v1/yaml.py @@ -12,15 +12,15 @@ import yaml from pandas.io.common import get_handle +from ..versions import parse_version from .C import * # noqa: F403 # directory with PEtab yaml schema files SCHEMA_DIR = Path(__file__).parent.parent / "schemas" # map of version number to validation schema SCHEMAS = { - "1": SCHEMA_DIR / "petab_schema.v1.0.0.yaml", - "1.0.0": SCHEMA_DIR / "petab_schema.v1.0.0.yaml", - "2.0.0": SCHEMA_DIR / "petab_schema.v2.0.0.yaml", + (1, 0): SCHEMA_DIR / "petab_schema.v1.0.0.yaml", + (2, 0): SCHEMA_DIR / "petab_schema.v2.0.0.yaml", } __all__ = [ @@ -71,14 +71,18 @@ def validate_yaml_syntax( yaml_config = load_yaml(yaml_config) if schema is None: - # try get PEtab version from yaml file + # try to get PEtab version from the yaml file # if this is not the available, the file is not valid anyways, # but let's still use the latest PEtab schema for full validation + version = yaml_config.get(FORMAT_VERSION, None) version = ( - yaml_config.get(FORMAT_VERSION, None) or list(SCHEMAS.values())[-1] + parse_version(version)[:2] + if version + else list(SCHEMAS.values())[-1] ) + try: - schema = SCHEMAS[str(version)] + schema = SCHEMAS[version] except KeyError as e: raise ValueError( "Unknown PEtab version given in problem " diff --git a/petab/v2/problem.py b/petab/v2/problem.py index 79bb6196..bcf38768 100644 --- a/petab/v2/problem.py +++ b/petab/v2/problem.py @@ -28,6 +28,7 @@ from ..v1.problem import ListOfFiles, VersionNumber from ..v1.yaml import get_path_prefix from ..v2.C import * # noqa: F403 +from ..versions import parse_version from . import conditions, experiments if TYPE_CHECKING: @@ -161,14 +162,15 @@ def get_path(filename): return filename return f"{base_path}/{filename}" - if yaml_config[FORMAT_VERSION] not in {"2.0.0"}: + if (format_version := parse_version(yaml_config[FORMAT_VERSION]))[ + 0 + ] != 2: # If we got a path to a v1 yaml file, try to auto-upgrade from tempfile import TemporaryDirectory - from ..versions import get_major_version from .petab1to2 import petab1to2 - if get_major_version(yaml_config) == 1 and yaml_file: + if format_version[0] == 1 and yaml_file: logging.debug( "Auto-upgrading problem from PEtab 1.0 to PEtab 2.0" ) @@ -185,7 +187,7 @@ def get_path(filename): ) raise ValueError( "Provided PEtab files are of unsupported version " - f"{yaml_config[FORMAT_VERSION]}. Expected 2.0.0." + f"{yaml_config[FORMAT_VERSION]}." ) if yaml.is_composite_problem(yaml_config): diff --git a/petab/versions.py b/petab/versions.py index 93f6a60a..e19d0cd0 100644 --- a/petab/versions.py +++ b/petab/versions.py @@ -1,35 +1,62 @@ """Handling of PEtab version numbers.""" from __future__ import annotations +import re from pathlib import Path import petab -from petab.v1 import Problem as V1Problem -from petab.v1.C import FORMAT_VERSION -from petab.v1.yaml import load_yaml __all__ = [ "get_major_version", + "parse_version", ] +from . import v1 + +# version regex pattern +_version_pattern = ( + r"(?P\d+)(?:\.(?P\d+))?" + r"(?:\.(?P\d+))?(?P[\w.]+)?" +) +_version_re = re.compile(_version_pattern) + + +def parse_version(version: str | int) -> tuple[int, int, int, str]: + """Parse a version string into a tuple of integers and suffix.""" + if isinstance(version, int): + return version, 0, 0, "" + + version = str(version) + match = _version_re.match(version) + if match is None: + raise ValueError(f"Invalid version string: {version}") + + major = int(match.group("major")) + minor = int(match.group("minor") or 0) + patch = int(match.group("patch") or 0) + suffix = match.group("suffix") or "" + + return major, minor, patch, suffix def get_major_version( - problem: str | dict | Path | V1Problem | petab.v2.Problem, + problem: str | dict | Path | petab.v1.Problem | petab.v2.Problem, ) -> int: """Get the major version number of the given problem.""" version = None if isinstance(problem, str | Path): + from petab.v1.yaml import load_yaml + yaml_config = load_yaml(problem) - version = yaml_config.get(FORMAT_VERSION) + version = yaml_config.get(v1.C.FORMAT_VERSION) elif isinstance(problem, dict): - version = problem.get(FORMAT_VERSION) + version = problem.get(v1.C.FORMAT_VERSION) if version is not None: version = str(version) return int(version.split(".")[0]) - if isinstance(problem, V1Problem): + if isinstance(problem, petab.v1.Problem): return 1 from . import v2 diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..757a9b50 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,13 @@ +"""Tests related to petab.versions""" + +from petab.versions import * + + +def test_parse_version(): + assert parse_version("1.2.3") == (1, 2, 3, "") + assert parse_version("1.2.3a") == (1, 2, 3, "a") + assert parse_version("1.2") == (1, 2, 0, "") + assert parse_version("1") == (1, 0, 0, "") + assert parse_version(1) == (1, 0, 0, "") + assert parse_version("1.2.3.a") == (1, 2, 3, ".a") + assert parse_version("1.2.3.4") == (1, 2, 3, ".4")