From 375726e24081bcad17ecdbca7f6624efce934b83 Mon Sep 17 00:00:00 2001 From: Brian Helba Date: Wed, 10 Jun 2026 11:14:11 -0400 Subject: [PATCH] Use Pydantic v2 context for `ignore_rcm_model_checks` --- isic_metadata/metadata.py | 34 ++++++++++++------------------- tests/test_batch.py | 37 ++++++++++++++++++++++------------ tests/test_dependent_fields.py | 2 +- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/isic_metadata/metadata.py b/isic_metadata/metadata.py index 5683f59..c478fc5 100644 --- a/isic_metadata/metadata.py +++ b/isic_metadata/metadata.py @@ -12,6 +12,7 @@ ConfigDict, Field, ValidationError, + ValidationInfo, computed_field, field_validator, model_validator, @@ -304,25 +305,6 @@ def anatom_site_4(self) -> str | None: def anatom_site_5(self) -> str | None: return AnatomSiteEnum.levels(self.anatom_site)[4] if self.anatom_site else None - __slots__ = ("_ignore_rcm_model_checks",) - _ignore_rcm_model_checks: bool - - # see https://github.com/pydantic/pydantic/issues/655#issuecomment-570312649 for details on - # implementing a private property to be used internally. - def __init__(self, *, _ignore_rcm_model_checks: bool = False, **kwargs: Any) -> None: - """ - Create a MetadataRow instance. - - _ignore_rcm_model_checks is a special attribute that allows disabling RCM model checks. It's - provided to allow batch checks to be performed on a set of metadata rows, some of which may - not be valid on a row level. e.g. if a batch of metadata rows contains RCM case ids that are - inconsistent with lesion ids, that ought to be validated even if the row with an RCM case id - has a missing image_type for instance. - """ - object.__setattr__(self, "_ignore_rcm_model_checks", _ignore_rcm_model_checks) - - super().__init__(**kwargs) - @model_validator(mode="before") @classmethod def handle_hierarchical_modes_and_unstructured_fields( @@ -408,8 +390,18 @@ def validate_melanoma_fields(self) -> MetadataRow: return self @model_validator(mode="after") - def validate_rcm_fields(self) -> MetadataRow: - if self._ignore_rcm_model_checks: + def validate_rcm_fields(self, info: ValidationInfo) -> MetadataRow: + """ + Validate that rcm_case_id is only used with RCM image types. + + These checks can be disabled by passing "ignore_rcm_model_checks" via the validation + context: MetadataRow.model_validate(..., context={"ignore_rcm_model_checks": True}). + This is provided to allow batch checks to be performed on a set of metadata rows, some + of which may not be valid on a row level. e.g. if a batch of metadata rows contains RCM + case ids that are inconsistent with lesion ids, that ought to be validated even if the + row with an RCM case id has a missing image_type for instance. + """ + if info.context and info.context.get("ignore_rcm_model_checks"): return self if not self.rcm_case_id: diff --git a/tests/test_batch.py b/tests/test_batch.py index 75607af..c241ff1 100644 --- a/tests/test_batch.py +++ b/tests/test_batch.py @@ -40,8 +40,12 @@ def test_rcm_case_has_at_most_one_macroscopic_image() -> None: with pytest.raises(ValidationError) as excinfo: MetadataBatch( items=[ - MetadataRow(image_type="RCM: macroscopic", rcm_case_id="foo"), - MetadataRow(image_type="RCM: macroscopic", rcm_case_id="foo"), + MetadataRow.model_validate( + {"image_type": "RCM: macroscopic", "rcm_case_id": "foo"} + ), + MetadataRow.model_validate( + {"image_type": "RCM: macroscopic", "rcm_case_id": "foo"} + ), ] ) assert len(excinfo.value.errors()) == 1 @@ -49,23 +53,30 @@ def test_rcm_case_has_at_most_one_macroscopic_image() -> None: MetadataBatch( items=[ - MetadataRow(image_type="RCM: macroscopic", rcm_case_id="foo"), - MetadataRow(image_type="RCM: macroscopic", rcm_case_id="bar"), + MetadataRow.model_validate({"image_type": "RCM: macroscopic", "rcm_case_id": "foo"}), + MetadataRow.model_validate({"image_type": "RCM: macroscopic", "rcm_case_id": "bar"}), ] ) def test_rcm_cases_belong_to_same_lesion() -> None: with pytest.raises(ValidationError) as excinfo: - MetadataBatch( - items=[ - MetadataRow( - rcm_case_id="foo", lesion_id="foolesion", _ignore_rcm_model_checks=True - ), - MetadataRow( - rcm_case_id="foo", lesion_id="barlesion", _ignore_rcm_model_checks=True - ), - ] + # passing model instances to MetadataBatch re-runs their "after" model validators, + # so the context must be provided here too. + MetadataBatch.model_validate( + { + "items": [ + MetadataRow.model_validate( + {"rcm_case_id": "foo", "lesion_id": "foolesion"}, + context={"ignore_rcm_model_checks": True}, + ), + MetadataRow.model_validate( + {"rcm_case_id": "foo", "lesion_id": "barlesion"}, + context={"ignore_rcm_model_checks": True}, + ), + ] + }, + context={"ignore_rcm_model_checks": True}, ) assert len(excinfo.value.errors()) == 1 assert "belong to multiple lesions" in excinfo.value.errors()[0]["msg"] diff --git a/tests/test_dependent_fields.py b/tests/test_dependent_fields.py index 10a4ee6..e1dc6e9 100644 --- a/tests/test_dependent_fields.py +++ b/tests/test_dependent_fields.py @@ -75,7 +75,7 @@ def test_rcm_model_checks_disabling() -> None: assert len(excinfo.value.errors()) == 1 assert "rcm_case_id requires setting image_type" in excinfo.value.errors()[0]["msg"] - MetadataRow(rcm_case_id="12345", _ignore_rcm_model_checks=True) + MetadataRow.model_validate({"rcm_case_id": "12345"}, context={"ignore_rcm_model_checks": True}) def test_tbp_tile_type_requires_image_type_tbp_tile() -> None: