diff --git a/pyproject.toml b/pyproject.toml index ebc54d3b..6e90ba45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ extras = [ "hgvs>=2.0.0a0,<3.0", "dill~=0.3.7", "click", - "pysam==0.23.0", # pinned pending https://github.com/ga4gh/vrs-python/issues/560 + "pysam>=0.24.0", ] tests = [ "pytest", diff --git a/src/ga4gh/vrs/extras/translator.py b/src/ga4gh/vrs/extras/translator.py index 327fa942..c903d4d2 100644 --- a/src/ga4gh/vrs/extras/translator.py +++ b/src/ga4gh/vrs/extras/translator.py @@ -156,16 +156,15 @@ def hgvs_tools(self) -> HgvsTools: return HgvsTools(self.data_proxy) def _from_vrs(self, var: dict, **kwargs) -> models._VariationBase | None: # noqa: ARG002 - """Convert from dict representation of VRS JSON to VRS object""" + """Convert from dict representation of VRS JSON to VRS object + + :raise pydantic.ValidationError: If `var` is not a valid VRS variation + """ if not isinstance(var, Mapping): return None if "type" not in var: return None - try: - model = models[var["type"]] - except KeyError: - return None - return model(**var) + return models.Variation.model_validate(var).root class AlleleTranslator(_Translator): @@ -254,7 +253,7 @@ def _from_gnomad(self, gnomad_expr: str, **kwargs) -> models.Allele | None: """ if not isinstance(gnomad_expr, str): return None - m = self.gnomad_re.match(gnomad_expr) + m = self.gnomad_re.fullmatch(gnomad_expr) if not m: return None @@ -341,7 +340,7 @@ def _from_spdi(self, spdi_expr: str, **kwargs) -> models.Allele | None: if not isinstance(spdi_expr, str): return None - m = self.spdi_re.match(spdi_expr) + m = self.spdi_re.fullmatch(spdi_expr) if not m: return None diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index fd900d70..2e4d3dff 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -1,4 +1,5 @@ import pytest +from pydantic import ValidationError from ga4gh.vrs import models from ga4gh.vrs.dataproxy import DataProxyValidationError @@ -335,6 +336,21 @@ def test_from_invalid(tlr): tlr.translate_from("BRAF amplication", assembly_name="GRCh37") +@pytest.mark.parametrize( + ("expr", "fmt"), + [ + # multi-allelic gnomAD-style input must not be truncated to its first ALT + ("1-55516888-G-GA,GT", "gnomad"), + ("19-44908822-C-T;foo", "gnomad"), + ("NC_000019.10:44908821:1:T:extra", "spdi"), + ], +) +def test_from_trailing_input_rejected(tlr, expr, fmt): + """Expressions with trailing content must be rejected, not partially parsed""" + with pytest.raises(ValueError, match=f"Unable to parse data as {fmt}"): + tlr.translate_from(expr, fmt=fmt) + + @pytest.mark.vcr def test_from_gnomad(tlr): do_normalize = False @@ -962,6 +978,48 @@ def test_translate_to_invalid_fmt(tlr): tlr.translate_to(models.Allele.model_validate(snv_output), fmt="gnomad") +def test_from_vrs_dict(): + """Regression test for ga4gh/vrs-python#489. + + Translating a VRS dict must resolve the model class from its `type` + instead of crashing with TypeError (the `models` module was subscripted). + """ + tlr = AlleleTranslator(data_proxy=None, identify=False) + + # valid VRS dict translates to a VRS object (previously raised TypeError) + allele = tlr.translate_from(snv_output, fmt="vrs") + assert isinstance(allele, models.Allele) + assert allele.type == "Allele" + assert allele.location.start == snv_output["location"]["start"] + assert allele.location.end == snv_output["location"]["end"] + + # non-dict and missing-type inputs still return None + assert tlr._from_vrs("NC_000019.10:g.44908822C>T") is None + assert tlr._from_vrs({"location": {}}) is None + + +from_vrs_non_variation_type_cases = [ + {"id": "unknown-name", "type": "NotARealModel"}, + {"id": "non-variation-vrs-class", "type": "SequenceLocation"}, + {"id": "non-model-module-attribute", "type": "Field"}, + {"id": "variation-base-class", "type": "_VariationBase"}, + {"id": "non-string", "type": 5}, +] + + +@pytest.mark.parametrize( + "case", from_vrs_non_variation_type_cases, ids=lambda c: c["id"] +) +def test_from_vrs_non_variation_type(case): + """A VRS dict whose `type` is not a VRS variation class is rejected, rather than + resolved to whatever `ga4gh.vrs.models` attribute has that name + """ + tlr = AlleleTranslator(data_proxy=None, identify=False) + with pytest.raises(ValidationError) as e: + tlr.translate_from({"type": case["type"]}, fmt="vrs") + assert e.value.errors()[0]["type"] == "union_tag_invalid" + + def test_from_hgvs_uncertain_range_raises(tlr): """Uncertain-range HGVS cannot be represented as an Allele with a literal sequence state; use CnvTranslator instead. See issue #609. diff --git a/tests/extras/test_annotate_vcf.py b/tests/extras/test_annotate_vcf.py index 0f5ca0e6..0d66dbe9 100644 --- a/tests/extras/test_annotate_vcf.py +++ b/tests/extras/test_annotate_vcf.py @@ -233,7 +233,7 @@ def test_get_vrs_object_invalid_input(vcf_annotator: VcfAnnotator, caplog): @pytest.mark.vcr -def test_annotate_vcf_rle(vcf_annotator: VcfAnnotator, vcr_cassette): +def test_annotate_vcf_rle(vcf_annotator: VcfAnnotator, vcr_cassette, tmp_path: Path): """Test VCF annotation with ReferenceLengthExpression states. Tests two variants that should produce ReferenceLengthExpression states: @@ -242,8 +242,8 @@ def test_annotate_vcf_rle(vcf_annotator: VcfAnnotator, vcr_cassette): """ vcr_cassette.allow_playback_repeats = False input_vcf = TEST_DATA_DIR / "test_rle.vcf" - output_vcf = TEST_DATA_DIR / "test_rle_output.vcf" - output_vrs_pkl = TEST_DATA_DIR / "test_rle_output.pkl" + output_vcf = tmp_path / "test_rle_output.vcf" + output_vrs_pkl = tmp_path / "test_rle_output.pkl" # Annotate the VCF with VRS attributes enabled vcf_annotator.annotate(