Skip to content

Commit 09d1562

Browse files
committed
fix: robust synthetic property handling for experimentst
- Only generate synthetic fields for experiments (e.g., publication identifiers, score set URNs) when ORM attributes are present, avoiding dict-based synthesis. - Validators now check for ORM attribute presence before transformation, ensuring correct behavior for both ORM and API/dict contexts. - Updated tests to expect Pydantic validation errors when required synthetic fields are missing.
1 parent e75c25f commit 09d1562

File tree

2 files changed

+56
-27
lines changed

2 files changed

+56
-27
lines changed

src/mavedb/view_models/experiment.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from datetime import date
22
from typing import Any, Collection, Optional, Sequence
33

4-
from pydantic import field_validator, model_validator, ValidationInfo
4+
from pydantic import ValidationInfo, field_validator, model_validator
55

6+
from mavedb.lib.validation import urn_re
67
from mavedb.lib.validation.exceptions import ValidationError
78
from mavedb.lib.validation.transform import (
89
transform_experiment_set_to_urn,
9-
transform_score_set_list_to_urn_list,
1010
transform_record_publication_identifiers,
11+
transform_score_set_list_to_urn_list,
1112
)
12-
from mavedb.lib.validation import urn_re
1313
from mavedb.lib.validation.utilities import is_null
1414
from mavedb.view_models import record_type_validator, set_record_type
1515
from mavedb.view_models.base.base import BaseModel
@@ -129,12 +129,11 @@ def publication_identifiers_validator(cls, v: Any, info: ValidationInfo) -> list
129129
return list(v) # Re-cast into proper list-like type
130130

131131
# These 'synthetic' fields are generated from other model properties. Transform data from other properties as needed, setting
132-
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created.
132+
# the appropriate field on the model itself. Then, proceed with Pydantic ingestion once fields are created. Only perform these
133+
# transformations if the relevant attributes are present on the input data (i.e., when creating from an ORM object).
133134
@model_validator(mode="before")
134135
def generate_primary_and_secondary_publications(cls, data: Any):
135-
if not hasattr(data, "primary_publication_identifiers") or not hasattr(
136-
data, "secondary_publication_identifiers"
137-
):
136+
if hasattr(data, "publication_identifier_associations"):
138137
try:
139138
publication_identifiers = transform_record_publication_identifiers(
140139
data.publication_identifier_associations
@@ -145,28 +144,30 @@ def generate_primary_and_secondary_publications(cls, data: Any):
145144
data.__setattr__(
146145
"secondary_publication_identifiers", publication_identifiers["secondary_publication_identifiers"]
147146
)
148-
except AttributeError as exc:
147+
except (KeyError, AttributeError) as exc:
149148
raise ValidationError(
150-
f"Unable to create {cls.__name__} without attribute: {exc}." # type: ignore
149+
f"Unable to coerce publication identifier attributes from ORM for {cls.__name__}: {exc}." # type: ignore
151150
)
152151
return data
153152

154153
@model_validator(mode="before")
155154
def generate_score_set_urn_list(cls, data: Any):
156-
if not hasattr(data, "score_set_urns"):
155+
if hasattr(data, "score_sets"):
157156
try:
158157
data.__setattr__("score_set_urns", transform_score_set_list_to_urn_list(data.score_sets))
159-
except AttributeError as exc:
160-
raise ValidationError(f"Unable to create {cls.__name__} without attribute: {exc}.") # type: ignore
158+
except (KeyError, AttributeError) as exc:
159+
raise ValidationError(f"Unable to coerce associated score set URNs from ORM for {cls.__name__}: {exc}.") # type: ignore
161160
return data
162161

163162
@model_validator(mode="before")
164163
def generate_experiment_set_urn(cls, data: Any):
165-
if not hasattr(data, "experiment_set_urn"):
164+
if hasattr(data, "experiment_set"):
166165
try:
167166
data.__setattr__("experiment_set_urn", transform_experiment_set_to_urn(data.experiment_set))
168-
except AttributeError as exc:
169-
raise ValidationError(f"Unable to create {cls.__name__} without attribute: {exc}.") # type: ignore
167+
except (KeyError, AttributeError) as exc:
168+
raise ValidationError(
169+
f"Unable to coerce associated experiment set URN from ORM for {cls.__name__}: {exc}."
170+
) # type: ignore
170171
return data
171172

172173

tests/view_models/test_experiment.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import pytest
2+
from pydantic import ValidationError
23

3-
from mavedb.view_models.experiment import ExperimentCreate, SavedExperiment
4+
from mavedb.view_models.experiment import Experiment, ExperimentCreate, SavedExperiment
45
from mavedb.view_models.publication_identifier import PublicationIdentifier
5-
66
from tests.helpers.constants import (
7-
VALID_EXPERIMENT_URN,
8-
VALID_SCORE_SET_URN,
9-
VALID_EXPERIMENT_SET_URN,
7+
SAVED_BIORXIV_PUBLICATION,
8+
SAVED_PUBMED_PUBLICATION,
9+
TEST_BIORXIV_IDENTIFIER,
1010
TEST_MINIMAL_EXPERIMENT,
1111
TEST_MINIMAL_EXPERIMENT_RESPONSE,
12-
SAVED_PUBMED_PUBLICATION,
1312
TEST_PUBMED_IDENTIFIER,
14-
TEST_BIORXIV_IDENTIFIER,
13+
VALID_EXPERIMENT_SET_URN,
14+
VALID_EXPERIMENT_URN,
15+
VALID_SCORE_SET_URN,
1516
)
1617
from tests.helpers.util.common import dummy_attributed_object_from_dict
1718

@@ -237,8 +238,15 @@ def test_saved_experiment_synthetic_properties():
237238
)
238239

239240

240-
@pytest.mark.parametrize("exclude", ["publication_identifier_associations", "score_sets", "experiment_set"])
241-
def test_cannot_create_saved_experiment_without_all_attributed_properties(exclude):
241+
@pytest.mark.parametrize(
242+
"exclude,expected_missing_fields",
243+
[
244+
("publication_identifier_associations", ["primaryPublicationIdentifiers", "secondaryPublicationIdentifiers"]),
245+
("score_sets", ["scoreSetUrns"]),
246+
("experiment_set", ["experimentSetUrn"]),
247+
],
248+
)
249+
def test_cannot_create_saved_experiment_without_all_attributed_properties(exclude, expected_missing_fields):
242250
experiment = TEST_MINIMAL_EXPERIMENT_RESPONSE.copy()
243251
experiment["urn"] = VALID_EXPERIMENT_URN
244252

@@ -280,11 +288,14 @@ def test_cannot_create_saved_experiment_without_all_attributed_properties(exclud
280288

281289
experiment.pop(exclude)
282290
experiment_attributed_object = dummy_attributed_object_from_dict(experiment)
283-
with pytest.raises(ValueError) as exc_info:
291+
with pytest.raises(ValidationError) as exc_info:
284292
SavedExperiment.model_validate(experiment_attributed_object)
285293

286-
assert "Unable to create SavedExperiment without attribute" in str(exc_info.value)
287-
assert exclude in str(exc_info.value)
294+
# Should fail with missing fields coerced from missing attributed properties
295+
msg = str(exc_info.value)
296+
assert "Field required" in msg
297+
for field in expected_missing_fields:
298+
assert field in msg
288299

289300

290301
def test_can_create_experiment_with_nonetype_experiment_set_urn():
@@ -303,3 +314,20 @@ def test_cant_create_experiment_with_invalid_experiment_set_urn():
303314
ExperimentCreate(**experiment_test)
304315

305316
assert f"'{experiment_test['experiment_set_urn']}' is not a valid experiment set URN" in str(exc_info.value)
317+
318+
319+
def test_can_create_experiment_from_non_orm_context():
320+
experiment = TEST_MINIMAL_EXPERIMENT_RESPONSE.copy()
321+
experiment["urn"] = VALID_EXPERIMENT_URN
322+
experiment["experimentSetUrn"] = VALID_EXPERIMENT_SET_URN
323+
experiment["scoreSetUrns"] = [VALID_SCORE_SET_URN]
324+
experiment["primaryPublicationIdentifiers"] = [SAVED_PUBMED_PUBLICATION]
325+
experiment["secondaryPublicationIdentifiers"] = [SAVED_PUBMED_PUBLICATION, SAVED_BIORXIV_PUBLICATION]
326+
327+
# Should not require any ORM attributes
328+
saved_experiment = Experiment.model_validate(experiment)
329+
assert saved_experiment.urn == VALID_EXPERIMENT_URN
330+
assert saved_experiment.experiment_set_urn == VALID_EXPERIMENT_SET_URN
331+
assert saved_experiment.score_set_urns == [VALID_SCORE_SET_URN]
332+
assert len(saved_experiment.primary_publication_identifiers) == 1
333+
assert len(saved_experiment.secondary_publication_identifiers) == 2

0 commit comments

Comments
 (0)