Skip to content

Commit 2b3e6f3

Browse files
committed
fix: operation error when scores were null
Adds some new utilities and associated tests that check base assumptions of functions which construct annotated variant objects
1 parent 0e8cf25 commit 2b3e6f3

File tree

7 files changed

+255
-26
lines changed

7 files changed

+255
-26
lines changed

src/mavedb/lib/annotation/annotate.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,17 @@
1313
from ga4gh.va_spec.acmg_2015 import VariantPathogenicityEvidenceLine
1414
from ga4gh.va_spec.base.core import ExperimentalVariantFunctionalImpactStudyResult, Statement
1515

16-
from mavedb.lib.annotation.constants import FUNCTIONAL_RANGES, CLINICAL_RANGES
1716
from mavedb.lib.annotation.evidence_line import acmg_evidence_line, functional_evidence_line
1817
from mavedb.lib.annotation.proposition import (
1918
mapped_variant_to_experimental_variant_clinical_impact_proposition,
2019
mapped_variant_to_experimental_variant_functional_impact_proposition,
2120
)
2221
from mavedb.lib.annotation.statement import mapped_variant_to_functional_statement
2322
from mavedb.lib.annotation.study_result import mapped_variant_to_experimental_variant_impact_study_result
23+
from mavedb.lib.annotation.util import (
24+
can_annotate_variant_for_pathogenicity_evidence,
25+
can_annotate_variant_for_functional_statement,
26+
)
2427
from mavedb.models.mapped_variant import MappedVariant
2528

2629

@@ -29,14 +32,7 @@ def variant_study_result(mapped_variant: MappedVariant) -> ExperimentalVariantFu
2932

3033

3134
def variant_functional_impact_statement(mapped_variant: MappedVariant) -> Optional[Statement]:
32-
if mapped_variant.variant.score_set.score_ranges is None:
33-
return None
34-
35-
if not any(
36-
range_key in mapped_variant.variant.score_set.score_ranges
37-
and mapped_variant.variant.score_set.score_ranges[range_key] is not None
38-
for range_key in FUNCTIONAL_RANGES
39-
):
35+
if not can_annotate_variant_for_functional_statement(mapped_variant):
4036
return None
4137

4238
# TODO#494: Add support for multiple functional evidence lines. If a score set has multiple ranges
@@ -51,14 +47,7 @@ def variant_functional_impact_statement(mapped_variant: MappedVariant) -> Option
5147
def variant_pathogenicity_evidence(
5248
mapped_variant: MappedVariant,
5349
) -> Optional[VariantPathogenicityEvidenceLine]:
54-
if mapped_variant.variant.score_set.score_ranges is None:
55-
return None
56-
57-
if not any(
58-
range_key in mapped_variant.variant.score_set.score_ranges
59-
and mapped_variant.variant.score_set.score_ranges[range_key] is not None
60-
for range_key in CLINICAL_RANGES
61-
):
50+
if not can_annotate_variant_for_pathogenicity_evidence(mapped_variant):
6251
return None
6352

6453
study_result = mapped_variant_to_experimental_variant_impact_study_result(mapped_variant)

src/mavedb/lib/annotation/classification.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ def functional_classification_of_variant(
4040
)
4141

4242
# This property of this column is guaranteed to be defined.
43-
functional_score: float = mapped_variant.variant.data["score_data"]["score"] # type: ignore
43+
functional_score: Optional[float] = mapped_variant.variant.data["score_data"]["score"] # type: ignore
44+
if functional_score is None:
45+
raise ValueError(
46+
f"Variant {mapped_variant.variant.urn} does not have a functional score."
47+
" Unable to classify functional impact."
48+
)
49+
4450
for range in score_ranges.ranges:
4551
lower_bound, upper_bound = inf_or_float(range.range[0], lower=True), inf_or_float(range.range[1], lower=False)
4652
if functional_score > lower_bound and functional_score <= upper_bound:
@@ -72,7 +78,12 @@ def pillar_project_clinical_classification_of_variant(
7278
)
7379

7480
# This property of this column is guaranteed to be defined.
75-
functional_score: float = mapped_variant.variant.data["score_data"]["score"] # type: ignore
81+
functional_score: Optional[float] = mapped_variant.variant.data["score_data"]["score"] # type: ignore
82+
if functional_score is None:
83+
raise ValueError(
84+
f"Variant {mapped_variant.variant.urn} does not have a functional score."
85+
" Unable to classify clinical impact."
86+
)
7687

7788
for range in score_ranges.ranges:
7889
lower_bound, upper_bound = inf_or_float(range.range[0], lower=True), inf_or_float(range.range[1], lower=False)

src/mavedb/lib/annotation/util.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Expression,
99
LiteralSequenceExpression,
1010
)
11+
from mavedb.lib.annotation.constants import CLINICAL_RANGES, FUNCTIONAL_RANGES
1112
from mavedb.models.mapped_variant import MappedVariant
1213
from mavedb.lib.annotation.exceptions import MappingDataDoesntExistException
1314

@@ -137,3 +138,114 @@ def variation_from_mapped_variant(mapped_variant: MappedVariant) -> MolecularVar
137138
)
138139

139140
return vrs_object_from_mapped_variant(mapped_variant.post_mapped)
141+
142+
143+
def _can_annotate_variant_base_assumptions(mapped_variant: MappedVariant) -> bool:
144+
"""
145+
Check if a mapped variant meets the basic requirements for annotation.
146+
147+
This function validates that a mapped variant has the necessary data
148+
to proceed with annotation by checking for a valid score value.
149+
150+
Args:
151+
mapped_variant (MappedVariant): The mapped variant to check for
152+
annotation eligibility.
153+
154+
Returns:
155+
bool: True if the variant can be annotated (has score ranges and
156+
a non-None score), False otherwise.
157+
"""
158+
# This property is guaranteed to exist for all variants.
159+
if mapped_variant.variant.data["score_data"]["score"] is None: # type: ignore
160+
return False
161+
162+
return True
163+
164+
165+
def _variant_score_ranges_have_required_keys_for_annotation(
166+
mapped_variant: MappedVariant, key_options: list[str]
167+
) -> bool:
168+
"""
169+
Check if a mapped variant's score set contains any of the required score range keys for annotation and is present.
170+
171+
Args:
172+
mapped_variant (MappedVariant): The mapped variant object containing the variant with score set data.
173+
key_options (list[str]): List of possible score range keys to check for in the score set.
174+
175+
Returns:
176+
bool: False if none of the required keys are found or if all found keys have None values.
177+
Returns True (implicitly) if at least one required key exists with a non-None value.
178+
"""
179+
if mapped_variant.variant.score_set.score_ranges is None:
180+
return False
181+
182+
if not any(
183+
range_key in mapped_variant.variant.score_set.score_ranges
184+
and mapped_variant.variant.score_set.score_ranges[range_key] is not None
185+
for range_key in key_options
186+
):
187+
return False
188+
189+
return True
190+
191+
192+
def can_annotate_variant_for_pathogenicity_evidence(mapped_variant: MappedVariant) -> bool:
193+
"""
194+
Determine if a mapped variant can be annotated for pathogenicity evidence.
195+
196+
This function checks whether a given mapped variant meets all the necessary
197+
requirements to receive pathogenicity evidence annotations. It validates
198+
both basic annotation assumptions and the presence of required clinical
199+
score range keys.
200+
201+
Args:
202+
mapped_variant (MappedVariant): The mapped variant object to evaluate
203+
for pathogenicity evidence annotation eligibility.
204+
205+
Returns:
206+
bool: True if the variant can be annotated for pathogenicity evidence,
207+
False otherwise.
208+
209+
Notes:
210+
The function performs two main validation checks:
211+
1. Basic annotation assumptions via _can_annotate_variant_base_assumptions
212+
2. Required clinical range keys via _variant_score_ranges_have_required_keys_for_annotation
213+
214+
Both checks must pass for the variant to be considered eligible for
215+
pathogenicity evidence annotation.
216+
"""
217+
if not _can_annotate_variant_base_assumptions(mapped_variant):
218+
return False
219+
if not _variant_score_ranges_have_required_keys_for_annotation(mapped_variant, CLINICAL_RANGES):
220+
return False
221+
222+
return True
223+
224+
225+
def can_annotate_variant_for_functional_statement(mapped_variant: MappedVariant) -> bool:
226+
"""
227+
Determine if a mapped variant can be annotated for functional statements.
228+
229+
This function checks if a variant meets all the necessary conditions to receive
230+
functional annotations by validating base assumptions and ensuring the variant's
231+
score ranges contain the required keys for functional annotation.
232+
233+
Args:
234+
mapped_variant (MappedVariant): The variant object to check for annotation
235+
eligibility, containing mapping information and score data.
236+
237+
Returns:
238+
bool: True if the variant can be annotated for functional statements,
239+
False otherwise.
240+
241+
Notes:
242+
The function performs two main checks:
243+
1. Validates base assumptions using _can_annotate_variant_base_assumptions
244+
2. Verifies score ranges have required keys using FUNCTIONAL_RANGES
245+
"""
246+
if not _can_annotate_variant_base_assumptions(mapped_variant):
247+
return False
248+
if not _variant_score_ranges_have_required_keys_for_annotation(mapped_variant, FUNCTIONAL_RANGES):
249+
return False
250+
251+
return True

src/mavedb/routers/mapped_variant.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ async def show_mapped_variant_functional_impact_statement(
139139

140140
if not functional_impact:
141141
logger.info(
142-
msg="Could not construct a functional impact statement for this mapped variant; No score range evidence exists for this score set.",
142+
msg="Could not construct a functional impact statement for this mapped variant. Variant does not have sufficient evidence to evaluate its functional impact.",
143143
extra=logging_context(),
144144
)
145145
raise HTTPException(
146146
status_code=404,
147-
detail=f"Could not construct a functional impact statement for mapped variant {urn}: No score range evidence found",
147+
detail=f"Could not construct a functional impact statement for mapped variant {urn}. Variant does not have sufficient evidence to evaluate its functional impact.",
148148
)
149149

150150
return functional_impact
@@ -180,12 +180,12 @@ async def show_mapped_variant_acmg_evidence_line(
180180

181181
if not pathogenicity_evidence:
182182
logger.info(
183-
msg="Could not construct a pathogenicity evidence line for this mapped variant; No calibrations exist for this score set.",
183+
msg="Could not construct a pathogenicity evidence line for this mapped variant; Variant does not have sufficient evidence to evaluate its pathogenicity.",
184184
extra=logging_context(),
185185
)
186186
raise HTTPException(
187187
status_code=404,
188-
detail=f"Could not construct a pathogenicity evidence line for mapped variant {urn}; No calibrations exist for this score set",
188+
detail=f"Could not construct a pathogenicity evidence line for mapped variant {urn}; Variant does not have sufficient evidence to evaluate its pathogenicity.",
189189
)
190190

191191
return pathogenicity_evidence

tests/lib/annotation/test_annotate.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ def test_variant_functional_impact_statement_no_score_ranges(mock_mapped_variant
1919
assert result is None
2020

2121

22+
def test_variant_functional_impact_statement_no_score(mock_mapped_variant):
23+
mock_mapped_variant.variant.data = {"score_data": {"score": None}}
24+
result = variant_functional_impact_statement(mock_mapped_variant)
25+
26+
assert result is None
27+
28+
2229
def test_variant_functional_impact_statement_with_score_ranges(mock_mapped_variant):
2330
result = variant_functional_impact_statement(mock_mapped_variant)
2431

@@ -40,6 +47,13 @@ def test_variant_pathogenicity_evidence_no_score_ranges_no_thresholds(mock_mappe
4047
assert result is None
4148

4249

50+
def test_variant_pathogenicity_evidence_no_score(mock_mapped_variant):
51+
mock_mapped_variant.variant.data = {"score_data": {"score": None}}
52+
result = variant_pathogenicity_evidence(mock_mapped_variant)
53+
54+
assert result is None
55+
56+
4357
def test_variant_pathogenicity_evidence_no_score_ranges_with_thresholds(mock_mapped_variant):
4458
mock_mapped_variant.variant.score_set.score_ranges.pop("investigator_provided")
4559
result = variant_pathogenicity_evidence(mock_mapped_variant)

tests/lib/annotation/test_util.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import pytest
22

33
from mavedb.lib.annotation.exceptions import MappingDataDoesntExistException
4-
from mavedb.lib.annotation.util import variation_from_mapped_variant
4+
from mavedb.lib.annotation.util import (
5+
variation_from_mapped_variant,
6+
_can_annotate_variant_base_assumptions,
7+
_variant_score_ranges_have_required_keys_for_annotation,
8+
can_annotate_variant_for_functional_statement,
9+
can_annotate_variant_for_pathogenicity_evidence,
10+
)
511

612
from tests.helpers.constants import TEST_VALID_POST_MAPPED_VRS_ALLELE, TEST_SEQUENCE_LOCATION_ACCESSION
13+
from unittest.mock import patch
714

815

916
@pytest.mark.parametrize(
@@ -24,3 +31,99 @@ def test_variation_from_mapped_variant_no_post_mapped(mock_mapped_variant):
2431

2532
with pytest.raises(MappingDataDoesntExistException):
2633
variation_from_mapped_variant(mock_mapped_variant)
34+
35+
36+
## Test base annotation assumptions
37+
38+
39+
def test_base_assumption_check_returns_false_when_score_is_none(mock_mapped_variant):
40+
mock_mapped_variant.variant.data = {"score_data": {"score": None}}
41+
42+
assert _can_annotate_variant_base_assumptions(mock_mapped_variant) is False
43+
44+
45+
def test_base_assumption_check_returns_true_when_all_conditions_met(mock_mapped_variant):
46+
assert _can_annotate_variant_base_assumptions(mock_mapped_variant) is True
47+
48+
49+
## Test variant score ranges have required keys for annotation
50+
51+
52+
def test_score_range_check_returns_false_when_keys_are_none(mock_mapped_variant):
53+
mock_mapped_variant.variant.score_set.score_ranges = None
54+
key_options = ["required_key1", "required_key2"]
55+
56+
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
57+
58+
59+
def test_score_range_check_returns_false_when_no_keys_present(mock_mapped_variant):
60+
mock_mapped_variant.variant.score_set.score_ranges = {"other_key": "value"}
61+
key_options = ["required_key1", "required_key2"]
62+
63+
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
64+
65+
66+
def test_score_range_check_returns_false_when_key_present_but_value_is_none(mock_mapped_variant):
67+
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": None}
68+
key_options = ["required_key1", "required_key2"]
69+
70+
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is False
71+
72+
73+
def test_score_range_check_returns_none_when_at_least_one_key_has_value(mock_mapped_variant):
74+
mock_mapped_variant.variant.score_set.score_ranges = {"required_key1": "value"}
75+
key_options = ["required_key1", "required_key2"]
76+
77+
assert _variant_score_ranges_have_required_keys_for_annotation(mock_mapped_variant, key_options) is True
78+
79+
80+
## Test clinical range check
81+
82+
83+
def test_clinical_range_check_returns_false_when_base_assumptions_fail(mock_mapped_variant):
84+
mock_mapped_variant.variant.score_set.score_ranges = None
85+
result = can_annotate_variant_for_pathogenicity_evidence(mock_mapped_variant)
86+
87+
assert result is False
88+
89+
90+
@pytest.mark.parametrize("clinical_ranges", [["clinical_range"], ["other_clinical_range"]])
91+
def test_clinical_range_check_returns_false_when_clinical_ranges_check_fails(mock_mapped_variant, clinical_ranges):
92+
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": "value"}
93+
94+
with patch("mavedb.lib.annotation.util.CLINICAL_RANGES", clinical_ranges):
95+
result = can_annotate_variant_for_pathogenicity_evidence(mock_mapped_variant)
96+
97+
assert result is False
98+
99+
100+
# The default mock_mapped_variant object should be valid
101+
def test_clinical_range_check_returns_true_when_all_conditions_met(mock_mapped_variant):
102+
assert can_annotate_variant_for_pathogenicity_evidence(mock_mapped_variant) is True
103+
104+
105+
## Test functional range check
106+
107+
108+
def test_functional_range_check_returns_false_when_base_assumptions_fail(mock_mapped_variant):
109+
mock_mapped_variant.variant.score_set.score_ranges = None
110+
result = can_annotate_variant_for_functional_statement(mock_mapped_variant)
111+
112+
assert result is False
113+
114+
115+
@pytest.mark.parametrize("functional_ranges", [["functional_range"], ["other_functional_range"]])
116+
def test_functional_range_check_returns_false_when_functional_ranges_check_fails(
117+
mock_mapped_variant, functional_ranges
118+
):
119+
mock_mapped_variant.variant.score_set.score_ranges = {"unrelated_key": "value"}
120+
121+
with patch("mavedb.lib.annotation.util.FUNCTIONAL_RANGES", functional_ranges):
122+
result = can_annotate_variant_for_functional_statement(mock_mapped_variant)
123+
124+
assert result is False
125+
126+
127+
# The default mock_mapped_variant object should be valid
128+
def test_functional_range_check_returns_true_when_all_conditions_met(mock_mapped_variant):
129+
assert can_annotate_variant_for_functional_statement(mock_mapped_variant) is True

tests/routers/test_mapped_variants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ def test_cannot_show_mapped_variant_functional_impact_statement_when_no_score_ra
419419

420420
assert response.status_code == 404
421421
assert (
422-
f"Could not construct a functional impact statement for mapped variant {score_set['urn']}#1: No score range evidence found"
422+
f"Could not construct a functional impact statement for mapped variant {score_set['urn']}#1. Variant does not have sufficient evidence to evaluate its functional impact"
423423
in response_data["detail"]
424424
)
425425

@@ -583,7 +583,7 @@ def test_cannot_show_mapped_variant_clinical_evidence_line_when_no_score_calibra
583583

584584
assert response.status_code == 404
585585
assert (
586-
f"Could not construct a pathogenicity evidence line for mapped variant {score_set['urn']}#1; No calibrations exist"
586+
f"Could not construct a pathogenicity evidence line for mapped variant {score_set['urn']}#1; Variant does not have sufficient evidence to evaluate its pathogenicity"
587587
in response_data["detail"]
588588
)
589589

0 commit comments

Comments
 (0)