From 7bee159ac9d479475d41e001ecbda2b7aec2f518 Mon Sep 17 00:00:00 2001 From: developer-rpai Date: Wed, 23 Sep 2026 23:07:55 -0700 Subject: [PATCH 1/6] fix: resolve VRS model class via getattr in from-VRS translation _from_vrs subscripted the ga4gh.vrs.models module with var["type"], raising TypeError ('module' object is not subscriptable) for any VRS dict input. Resolve the model class with getattr(models, var["type"], None) and return None gracefully for unknown types, preserving the original intent of the KeyError guard. Closes #489 --- src/ga4gh/vrs/extras/translator.py | 5 ++--- tests/extras/test_allele_translator.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ga4gh/vrs/extras/translator.py b/src/ga4gh/vrs/extras/translator.py index 91c45532..b10b62b6 100644 --- a/src/ga4gh/vrs/extras/translator.py +++ b/src/ga4gh/vrs/extras/translator.py @@ -166,9 +166,8 @@ def _from_vrs(self, var: dict, **kwargs) -> models._VariationBase | None: # noq return None if "type" not in var: return None - try: - model = models[var["type"]] - except KeyError: + model = getattr(models, var["type"], None) + if model is None: return None return model(**var) diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index 15f87a89..02f0d56c 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -982,3 +982,26 @@ def test_normalize_microsatellite_counts(tlr, case): def test_translate_to_invalid_fmt(tlr): with pytest.raises(NotImplementedError, match="gnomad is not supported"): 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 the `models` + module via getattr (modules are not subscriptable) instead of crashing + with TypeError; unknown types return None gracefully. + """ + 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"] + + # unknown type returns None rather than raising + assert tlr._from_vrs({"type": "NotARealModel"}) is None + # 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 c783e4b346b8f0826d4023f0b1cd4e8db5ee9702 Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Sat, 3 Oct 2026 15:56:41 -0400 Subject: [PATCH 2/6] build: require pysam>=0.24.0 pysam 0.24.0 fixes the Python 3.13 + pytest-cov failure (pysam-developers/pysam#1350) that the pysam==0.23.0 pin worked around (#560). Closes #653 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f14f78f2..e7da4062 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ extras = [ "hgvs>=1.5.5,<2.0", "dill~=0.3.7", "click", - "pysam==0.23.0", # pinned pending https://github.com/ga4gh/vrs-python/issues/560 + "pysam>=0.24.0", ] dev = [ # tests From 39fb5c2faf1e6119e72f5729993e414ef623988a Mon Sep 17 00:00:00 2001 From: Kyle Ferriter Date: Tue, 6 Oct 2026 00:33:43 -0400 Subject: [PATCH 3/6] fix(translator): reject non-variation types in _from_vrs Validate VRS dict input against the models.Variation union instead of looking up any attribute of the models module by name. Types that are not VRS variations (e.g. SequenceLocation, or unrelated module attributes like Field) now raise a pydantic ValidationError listing the accepted types, rather than building a non-variation object or returning None. --- src/ga4gh/vrs/extras/translator.py | 10 ++++----- tests/extras/test_allele_translator.py | 30 +++++++++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/ga4gh/vrs/extras/translator.py b/src/ga4gh/vrs/extras/translator.py index b10b62b6..269c2598 100644 --- a/src/ga4gh/vrs/extras/translator.py +++ b/src/ga4gh/vrs/extras/translator.py @@ -161,15 +161,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 - model = getattr(models, var["type"], None) - if model is None: - return None - return model(**var) + return models.Variation.model_validate(var).root class AlleleTranslator(_Translator): diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index 02f0d56c..e5e48d34 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 @@ -987,9 +988,8 @@ def test_translate_to_invalid_fmt(tlr): def test_from_vrs_dict(): """Regression test for ga4gh/vrs-python#489. - Translating a VRS dict must resolve the model class from the `models` - module via getattr (modules are not subscriptable) instead of crashing - with TypeError; unknown types return None gracefully. + 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) @@ -1000,8 +1000,28 @@ def test_from_vrs_dict(): assert allele.location.start == snv_output["location"]["start"] assert allele.location.end == snv_output["location"]["end"] - # unknown type returns None rather than raising - assert tlr._from_vrs({"type": "NotARealModel"}) is None # 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" From 0542a2ca2485e240a625b765c63ea58c2356a2a6 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 19 Oct 2026 09:42:30 -0400 Subject: [PATCH 4/6] fix(test): write vcf rle output to temp dir (#671) Prevents test_rle_output.pkl and test_rle_output.vcf from being written to tests/extras/data dir --- tests/extras/test_annotate_vcf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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( From 44e6db44b6af59a7f5a7b4acc2bca697d121b83e Mon Sep 17 00:00:00 2001 From: Syed Ahnaf Raza <119487797+syahra712@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:49:33 +0500 Subject: [PATCH 5/6] fix: reject trailing input in beacon, gnomAD and SPDI translators (#670) --- src/ga4gh/vrs/extras/translator.py | 6 +++--- tests/extras/test_allele_translator.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/ga4gh/vrs/extras/translator.py b/src/ga4gh/vrs/extras/translator.py index b10b62b6..2f385561 100644 --- a/src/ga4gh/vrs/extras/translator.py +++ b/src/ga4gh/vrs/extras/translator.py @@ -257,7 +257,7 @@ def _from_beacon(self, beacon_expr: str, **kwargs) -> models.Allele | None: if not isinstance(beacon_expr, str): return None - m = self.beacon_re.match(beacon_expr.replace(" ", "")) + m = self.beacon_re.fullmatch(beacon_expr.replace(" ", "")) if not m: return None @@ -321,7 +321,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 @@ -395,7 +395,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 02f0d56c..dda62972 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -337,6 +337,22 @@ 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"), + ("19 : 44908822 C > T,G", "beacon"), + ], +) +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_beacon(tlr): do_normalize = False From d65f1247d5498e82d1a7013aae849a3942088148 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Tue, 27 Oct 2026 07:10:52 -0400 Subject: [PATCH 6/6] test: remove stale beacon test --- tests/extras/test_allele_translator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/extras/test_allele_translator.py b/tests/extras/test_allele_translator.py index 13036dca..2e4d3dff 100644 --- a/tests/extras/test_allele_translator.py +++ b/tests/extras/test_allele_translator.py @@ -343,7 +343,6 @@ def test_from_invalid(tlr): ("1-55516888-G-GA,GT", "gnomad"), ("19-44908822-C-T;foo", "gnomad"), ("NC_000019.10:44908821:1:T:extra", "spdi"), - ("19 : 44908822 C > T,G", "beacon"), ], ) def test_from_trailing_input_rejected(tlr, expr, fmt):