Skip to content

Commit

Permalink
fix: return correct message when validation error occurs (#188)
Browse files Browse the repository at this point in the history
* fix: return correct message when validation error occurs

* fix: return correct message when validation error occurs

* fix: return correct messages when validation error occurs

* adding test case

* feat: update error messaging to contain all validation errors for structure ends

* tests: add test for get_error_message

* tests: add test case for multiple error messages in one ValidationError
  • Loading branch information
katiestahl authored Sep 25, 2024
1 parent 38e99b4 commit b947125
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 35 deletions.
8 changes: 5 additions & 3 deletions src/fusor/fusor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
UnknownGeneElement,
)
from fusor.nomenclature import generate_nomenclature
from fusor.tools import translate_identifier
from fusor.tools import get_error_message, translate_identifier

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -178,7 +178,8 @@ def categorical_fusion(
regulatoryElement=regulatory_element,
)
except ValidationError as e:
raise FUSORParametersException(str(e)) from e
error_message = get_error_message(e)
raise FUSORParametersException(error_message) from e
return fusion

@staticmethod
Expand Down Expand Up @@ -209,7 +210,8 @@ def assayed_fusion(
readingFramePreserved=reading_frame_preserved,
)
except ValidationError as e:
raise FUSORParametersException(str(e)) from e
error_message = get_error_message(e)
raise FUSORParametersException(error_message) from e
return fusion

async def transcript_segment_element(
Expand Down
11 changes: 7 additions & 4 deletions src/fusor/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,26 +512,29 @@ def structure_ends(cls, values):
their position.
"""
elements = values.structure
error_messages = []
if isinstance(elements[0], TranscriptSegmentElement):
if elements[0].exonEnd is None and not values.regulatoryElement:
msg = "5' TranscriptSegmentElement fusion partner must contain ending exon position"
raise ValueError(msg)
error_messages.append(msg)
elif isinstance(elements[0], LinkerElement):
msg = "First structural element cannot be LinkerSequence"
raise ValueError(msg)
error_messages.append(msg)

if len(elements) > 2:
for element in elements[1:-1]:
if isinstance(element, TranscriptSegmentElement) and (
element.exonStart is None or element.exonEnd is None
):
msg = "Connective TranscriptSegmentElement must include both start and end positions"
raise ValueError(msg)
error_messages.append(msg)
if isinstance(elements[-1], TranscriptSegmentElement) and (
elements[-1].exonStart is None
):
msg = "3' fusion partner junction must include " "starting position"
raise ValueError
error_messages.append(msg)
if error_messages:
raise ValueError("\n".join(error_messages))
return values


Expand Down
13 changes: 13 additions & 0 deletions src/fusor/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from gene.database import AbstractDatabase as GeneDatabase
from gene.database import create_db
from gene.schemas import CURIE
from pydantic import ValidationError

from fusor.exceptions import IDTranslationException

Expand Down Expand Up @@ -89,3 +90,15 @@ async def check_data_resources(
return FusorDataResourceStatus(
cool_seq_tool=all(cst_status), gene_normalizer=gene_status
)


def get_error_message(e: ValidationError) -> str:
"""Get all error messages from a pydantic ValidationError
:param e: the ValidationError to get the messages from
:return: string containing all of the extracted error messages, separated by newlines or the string
representation of the exception if 'msg' field is not present
"""
if e.errors():
return "\n".join(str(error["msg"]) for error in e.errors() if "msg" in error)
return str(e)
57 changes: 44 additions & 13 deletions tests/test_fusor.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,25 +327,28 @@ def test_fusion(
functional_domain,
):
"""Test that fusion methods work correctly."""
causative_event = {
"type": "CausativeEvent",
"eventType": "rearrangement",
"eventDescription": "chr2:g.pter_8,247,756::chr11:g.15,825,273_cen_qter (der11) and chr11:g.pter_15,825,272::chr2:g.8,247,757_cen_qter (der2)",
}
assay = {
"type": "Assay",
"methodUri": "pmid:33576979",
"assayId": "obi:OBI_0003094",
"assayName": "fluorescence in-situ hybridization assay",
"fusionDetection": "inferred",
}

# infer type from properties
f = fusor_instance.fusion(
structure=[
templated_sequence_element,
linker_element,
UnknownGeneElement(),
],
causative_event={
"type": "CausativeEvent",
"eventType": "rearrangement",
"eventDescription": "chr2:g.pter_8,247,756::chr11:g.15,825,273_cen_qter (der11) and chr11:g.pter_15,825,272::chr2:g.8,247,757_cen_qter (der2)",
},
assay={
"type": "Assay",
"methodUri": "pmid:33576979",
"assayId": "obi:OBI_0003094",
"assayName": "fluorescence in-situ hybridization assay",
"fusionDetection": "inferred",
},
causative_event=causative_event,
assay=assay,
)
assert isinstance(f, AssayedFusion)
f = fusor_instance.fusion(
Expand Down Expand Up @@ -418,14 +421,42 @@ def test_fusion(
msg = "Fusions must contain >= 2 structural elements, or >=1 structural element and a regulatory element"
assert msg in str(excinfo.value)

expected = copy.deepcopy(transcript_segment_element)
expected.exonStart = None
with pytest.raises(FUSORParametersException) as excinfo:
f = fusor_instance.fusion(
structure=[
linker_element,
expected,
],
causative_event=causative_event,
assay=assay,
)
msg = "First structural element cannot be LinkerSequence"
assert msg in str(excinfo.value)
msg = "3' fusion partner junction must include " "starting position"
assert msg in str(excinfo.value)

# catch multiple errors from different validators
with pytest.raises(FUSORParametersException) as excinfo:
f = fusor_instance.fusion(
structure=[
linker_element,
expected,
],
reading_frame_preserved="not a bool",
causative_event="other type",
)
msg = "Input should be a valid boolean\nInput should be a valid dictionary or instance of CausativeEvent"
assert msg in str(excinfo.value)


@pytest.mark.asyncio()
async def test_transcript_segment_element(
fusor_instance, transcript_segment_element, mane_transcript_segment_element
):
"""Test that transcript_segment_element method works correctly"""
# Transcript Input
# TODO: this test is now off by one after updating cool-seq-tool - need Jeremy's help in determining if the issue lies in fusor or CST
tsg = await fusor_instance.transcript_segment_element(
transcript="NM_152263.3", exon_start=1, exon_end=8, tx_to_genomic_coords=True
)
Expand Down
41 changes: 27 additions & 14 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def sequence_locations():
"start": 15565,
"end": 15566,
},
# TODO: the following 3 examples were made when intervals supported strings and need updated data chr12:p12.1-p12.2. I put in placeholders for now
{
"id": "ga4gh:SL.PPQ-aYd6dsSj7ulUEeqK8xZJP-yPrfdP",
"type": "SequenceLocation",
Expand Down Expand Up @@ -396,7 +395,6 @@ def test_transcript_segment_element(transcript_segments):
"id": "hgnc:1",
"label": "G1",
},
# TODO: get updated values for this from Jeremy
elementGenomicStart={
"location": {
"species_id": "taxonomy:9606",
Expand Down Expand Up @@ -699,6 +697,18 @@ def test_fusion(
assert CategoricalFusion(structure=[transcript_segments[1], transcript_segments[2]])

# test variety of element types
causative_event = {
"type": "CausativeEvent",
"eventType": "rearrangement",
"eventDescription": "chr2:g.pter_8,247,756::chr11:g.15,825,273_cen_qter (der11) and chr11:g.pter_15,825,272::chr2:g.8,247,757_cen_qter (der2)",
}
assay = {
"type": "Assay",
"methodUri": "pmid:33576979",
"assayId": "obi:OBI_0003094",
"assayName": "fluorescence in-situ hybridization assay",
"fusionDetection": "inferred",
}
assert AssayedFusion(
type="AssayedFusion",
structure=[
Expand All @@ -708,18 +718,8 @@ def test_fusion(
templated_sequence_elements[1],
linkers[0],
],
causativeEvent={
"type": "CausativeEvent",
"eventType": "rearrangement",
"eventDescription": "chr2:g.pter_8,247,756::chr11:g.15,825,273_cen_qter (der11) and chr11:g.pter_15,825,272::chr2:g.8,247,757_cen_qter (der2)",
},
assay={
"type": "Assay",
"methodUri": "pmid:33576979",
"assayId": "obi:OBI_0003094",
"assayName": "fluorescence in-situ hybridization assay",
"fusionDetection": "inferred",
},
causativeEvent=causative_event,
assay=assay,
)
with pytest.raises(ValidationError) as exc_info:
assert CategoricalFusion(
Expand All @@ -746,6 +746,19 @@ def test_fusion(
msg = "Value error, First structural element cannot be LinkerSequence"
check_validation_error(exc_info, msg)

with pytest.raises(ValidationError) as exc_info:
assert AssayedFusion(
type="AssayedFusion",
structure=[
transcript_segments[3],
transcript_segments[1],
],
causativeEvent=causative_event,
assay=assay,
)
msg = "Value error, 5' TranscriptSegmentElement fusion partner must contain ending exon position"
check_validation_error(exc_info, msg)


def test_fusion_element_count(
functional_domains,
Expand Down
26 changes: 25 additions & 1 deletion tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Test FUSOR tools."""

import pytest
from pydantic import BaseModel, ValidationError

from fusor.exceptions import IDTranslationException
from fusor.tools import translate_identifier
from fusor.tools import get_error_message, translate_identifier


def test_translate_identifier(fusor_instance):
Expand All @@ -30,3 +31,26 @@ def test_translate_identifier(fusor_instance):
identifier = translate_identifier(
fusor_instance.seqrepo, "fake_namespace:NM_152263.3"
)


class _TestModel(BaseModel):
field1: int
field2: str


def test_get_error_message():
"""Test that get_error_message works correctly"""
# test single error message
try:
_TestModel(field1="not_an_int", field2="valid_str")
except ValidationError as e:
error_message = get_error_message(e)
assert "should be a valid integer" in error_message

# test multiple error messages in one ValidationError
try:
_TestModel(field1="not_an_int", field2=123)
except ValidationError as e:
error_message = get_error_message(e)
assert "should be a valid integer" in error_message
assert "should be a valid string" in error_message

0 comments on commit b947125

Please sign in to comment.