Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion petab/v1/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ def write_yaml(yaml_config: dict[str, Any], filename: str | Path) -> None:
"""
Path(filename).parent.mkdir(parents=True, exist_ok=True)
with open(filename, "w") as outfile:
yaml.dump(
yaml.safe_dump(
yaml_config, outfile, default_flow_style=False, sort_keys=False
)

Expand Down
29 changes: 23 additions & 6 deletions petab/v2/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ class Observable(BaseModel):

#: :meta private:
model_config = ConfigDict(
arbitrary_types_allowed=True, populate_by_name=True, extra="allow"
arbitrary_types_allowed=True,
populate_by_name=True,
extra="allow",
validate_assignment=True,
)

@field_validator(
Expand Down Expand Up @@ -344,6 +347,7 @@ class Change(BaseModel):
populate_by_name=True,
use_enum_values=True,
extra="allow",
validate_assignment=True,
)

@field_validator("target_value", mode="before")
Expand Down Expand Up @@ -385,7 +389,9 @@ class Condition(BaseModel):
changes: list[Change]

#: :meta private:
model_config = ConfigDict(populate_by_name=True, extra="allow")
model_config = ConfigDict(
populate_by_name=True, extra="allow", validate_assignment=True
)

def __add__(self, other: Change) -> Condition:
"""Add a change to the set."""
Expand Down Expand Up @@ -503,7 +509,9 @@ class ExperimentPeriod(BaseModel):
condition_ids: list[str] = Field(default_factory=list)

#: :meta private:
model_config = ConfigDict(populate_by_name=True, extra="allow")
model_config = ConfigDict(
populate_by_name=True, extra="allow", validate_assignment=True
)

@field_validator("condition_ids", mode="before")
@classmethod
Expand Down Expand Up @@ -544,7 +552,10 @@ class Experiment(BaseModel):

#: :meta private:
model_config = ConfigDict(
arbitrary_types_allowed=True, populate_by_name=True, extra="allow"
arbitrary_types_allowed=True,
populate_by_name=True,
extra="allow",
validate_assignment=True,
)

def __add__(self, other: ExperimentPeriod) -> Experiment:
Expand Down Expand Up @@ -682,7 +693,10 @@ class Measurement(BaseModel):

#: :meta private:
model_config = ConfigDict(
arbitrary_types_allowed=True, populate_by_name=True, extra="allow"
arbitrary_types_allowed=True,
populate_by_name=True,
extra="allow",
validate_assignment=True,
)

@field_validator(
Expand Down Expand Up @@ -806,7 +820,9 @@ class Mapping(BaseModel):
)

#: :meta private:
model_config = ConfigDict(populate_by_name=True, extra="allow")
model_config = ConfigDict(
populate_by_name=True, extra="allow", validate_assignment=True
)


class MappingTable(BaseModel):
Expand Down Expand Up @@ -909,6 +925,7 @@ class Parameter(BaseModel):
populate_by_name=True,
use_enum_values=True,
extra="allow",
validate_assignment=True,
)

@field_validator("id")
Expand Down
54 changes: 42 additions & 12 deletions petab/v2/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
import numpy as np
import pandas as pd
import sympy as sp
from pydantic import AnyUrl, BaseModel, Field, field_validator
from pydantic import (
AnyUrl,
BaseModel,
ConfigDict,
Field,
field_validator,
)

from ..v1 import (
parameter_mapping,
Expand Down Expand Up @@ -1124,9 +1130,13 @@ def model_dump(self, **kwargs) -> dict[str, Any]:
class ModelFile(BaseModel):
"""A file in the PEtab problem configuration."""

location: str | AnyUrl
location: AnyUrl | Path
language: str

model_config = ConfigDict(
validate_assignment=True,
)


class ExtensionConfig(BaseModel):
"""The configuration of a PEtab extension."""
Expand All @@ -1139,13 +1149,13 @@ class ProblemConfig(BaseModel):
"""The PEtab problem configuration."""

#: The path to the PEtab problem configuration.
filepath: str | AnyUrl | None = Field(
filepath: AnyUrl | Path | None = Field(
None,
description="The path to the PEtab problem configuration.",
exclude=True,
)
#: The base path to resolve relative paths.
base_path: str | AnyUrl | None = Field(
base_path: AnyUrl | Path | None = Field(
None,
description="The base path to resolve relative paths.",
exclude=True,
Expand All @@ -1156,21 +1166,24 @@ class ProblemConfig(BaseModel):
# TODO https://github.com/PEtab-dev/PEtab/pull/641:
# rename to parameter_files in yaml for consistency with other files?
# always a list?
parameter_files: list[str | AnyUrl] = Field(
parameter_files: list[AnyUrl | Path] = Field(
default=[], alias=PARAMETER_FILES
)

# TODO: consider changing str to Path
model_files: dict[str, ModelFile] | None = {}
measurement_files: list[str | AnyUrl] = []
condition_files: list[str | AnyUrl] = []
experiment_files: list[str | AnyUrl] = []
observable_files: list[str | AnyUrl] = []
mapping_files: list[str | AnyUrl] = []
measurement_files: list[AnyUrl | Path] = []
condition_files: list[AnyUrl | Path] = []
experiment_files: list[AnyUrl | Path] = []
observable_files: list[AnyUrl | Path] = []
mapping_files: list[AnyUrl | Path] = []

#: Extensions used by the problem.
extensions: list[ExtensionConfig] | dict = {}

model_config = ConfigDict(
validate_assignment=True,
)

# convert parameter_file to list
@field_validator(
"parameter_files",
Expand All @@ -1194,7 +1207,24 @@ def to_yaml(self, filename: str | Path):
"""
from ..v1.yaml import write_yaml

write_yaml(self.model_dump(by_alias=True), filename)
data = self.model_dump(by_alias=True)
# convert Paths to strings for YAML serialization
for key in (
"measurement_files",
"condition_files",
"experiment_files",
"observable_files",
"mapping_files",
"parameter_files",
):
data[key] = list(map(str, data[key]))

for model_id in data.get("model_files", {}):
data["model_files"][model_id][MODEL_LOCATION] = str(
data["model_files"][model_id]["location"]
)

write_yaml(data, filename)

@property
def format_version_tuple(self) -> tuple[int, int, int, str]:
Expand Down
27 changes: 27 additions & 0 deletions tests/v2/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
import pandas as pd
from pandas.testing import assert_frame_equal
from pydantic import AnyUrl

import petab.v2 as petab
from petab.v2 import Problem
Expand Down Expand Up @@ -198,3 +199,29 @@ def test_sample_startpoint_shape():
n_starts = 10
sp = problem.sample_parameter_startpoints(n_starts=n_starts)
assert sp.shape == (n_starts, 2)


def test_problem_config_paths():
"""Test handling of URLS and local paths in ProblemConfig."""

pc = petab.ProblemConfig(
parameter_files=["https://example.com/params.tsv"],
condition_files=["conditions.tsv"],
measurement_files=["measurements.tsv"],
observable_files=["observables.tsv"],
experiment_files=["experiments.tsv"],
)
assert isinstance(pc.parameter_files[0], AnyUrl)
assert isinstance(pc.condition_files[0], Path)
assert isinstance(pc.measurement_files[0], Path)
assert isinstance(pc.observable_files[0], Path)
assert isinstance(pc.experiment_files[0], Path)

# Auto-convert to Path on assignment
pc.parameter_files = ["foo.tsv"]
assert isinstance(pc.parameter_files[0], Path)

# We can't easily intercept mutations to the list:
# pc.parameter_files[0] = "foo.tsv"
# assert isinstance(pc.parameter_files[0], Path)
# see also https://github.com/pydantic/pydantic/issues/8575
Loading