Skip to content
Open
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
34 changes: 13 additions & 21 deletions isic_metadata/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ConfigDict,
Field,
ValidationError,
ValidationInfo,
computed_field,
field_validator,
model_validator,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 24 additions & 13 deletions tests/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,43 @@ 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
assert "have multiple macroscopic images" in excinfo.value.errors()[0]["msg"]

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"]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dependent_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down