diff --git a/src/fusor/fusor.py b/src/fusor/fusor.py index 505c836..cbf9319 100644 --- a/src/fusor/fusor.py +++ b/src/fusor/fusor.py @@ -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__) @@ -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 @@ -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( diff --git a/src/fusor/models.py b/src/fusor/models.py index 4db1f59..64ae576 100644 --- a/src/fusor/models.py +++ b/src/fusor/models.py @@ -512,13 +512,14 @@ 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]: @@ -526,12 +527,14 @@ def structure_ends(cls, values): 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 diff --git a/src/fusor/tools.py b/src/fusor/tools.py index bb07695..b02f4c4 100644 --- a/src/fusor/tools.py +++ b/src/fusor/tools.py @@ -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 @@ -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) diff --git a/tests/test_fusor.py b/tests/test_fusor.py index a3f6304..e504138 100644 --- a/tests/test_fusor.py +++ b/tests/test_fusor.py @@ -327,6 +327,19 @@ 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=[ @@ -334,18 +347,8 @@ def test_fusion( 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( @@ -418,6 +421,35 @@ 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( @@ -425,7 +457,6 @@ async def test_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 ) diff --git a/tests/test_models.py b/tests/test_models.py index 4150652..fed5800 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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", @@ -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", @@ -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=[ @@ -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( @@ -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, diff --git a/tests/test_tools.py b/tests/test_tools.py index ffd681a..4ea1916 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -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): @@ -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