From 8ddf51ac3b8e0c23a0edafa792f147c149f4951f Mon Sep 17 00:00:00 2001 From: developer-rpai Date: Wed, 23 Sep 2026 23:19:44 -0700 Subject: [PATCH 1/2] fix: ga4gh_identify respects in_place=never for digest fields With in_place="never", ga4gh_identify computed digests on the live object: compute_digest() stores the digest on the object itself and the serialization step stores digests on every nested identifiable object (e.g. SequenceLocation). As a result the input object was mutated even though the caller asked for no in-place edits. Compute the identifier on a deep copy when in_place is "never", so the caller's object (including its digest fields) is left byte-identical and the only output is the returned identifier string. The "default" and "always" modes are unchanged. Closes #440 --- src/ga4gh/vrs/models.py | 11 +++++++++-- tests/test_vrs.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/ga4gh/vrs/models.py b/src/ga4gh/vrs/models.py index ad2978d5..8acdf33e 100644 --- a/src/ga4gh/vrs/models.py +++ b/src/ga4gh/vrs/models.py @@ -11,6 +11,7 @@ module name, e.g., `ga4gh.vrs.models.Allele` """ +import copy import inspect import sys from abc import ABC @@ -327,7 +328,10 @@ def get_or_create_ga4gh_identifier( - 'always': this will update the vro.id field any time the identifier is computed - 'never': the vro.id field will not be edited in-place, - even when empty + even when empty. The vro object is not mutated in any way: + digest computation is performed on a deep copy, so neither + the vro.digest field nor digest fields on nested objects + are set. Digests will be recalculated even if present if recompute is True. @@ -344,7 +348,10 @@ def get_or_create_ga4gh_identifier( elif in_place == "always": self.id = self.compute_ga4gh_identifier(recompute) elif in_place == "never": - return self.compute_ga4gh_identifier(recompute) + # Digest computation stores digests on the object and its nested + # identifiable objects, so compute on a deep copy to leave the + # caller's object (including its digest fields) untouched. + return copy.deepcopy(self).compute_ga4gh_identifier(recompute) else: msg = "Expected 'in_place' to be one of 'default', 'always', or 'never'" raise ValueError(msg) diff --git a/tests/test_vrs.py b/tests/test_vrs.py index 975854ab..ae5d120a 100644 --- a/tests/test_vrs.py +++ b/tests/test_vrs.py @@ -194,6 +194,38 @@ def test_cpb(): assert ga4gh_identify(cpb_431012) == "ga4gh:CPB.x8GH5G73cPMs37jy1-9mJjWynu324rxI" +def test_identify_in_place_never_does_not_mutate(): + """ga4gh_identify(..., in_place="never") must not mutate the input object. + + Regression test for https://github.com/ga4gh/vrs-python/issues/440: + digest computation used to set `digest` fields on the object and its + nested identifiable objects even when in_place="never". + """ + allele = models.Allele(**allele_dict) + before = allele.model_dump_json(exclude_none=True) + assert allele.digest is None + assert allele.location.digest is None + + obj_id = ga4gh_identify(allele, in_place="never") + + assert obj_id == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + assert allele.id is None + assert allele.digest is None + assert allele.location.digest is None + assert allele.model_dump_json(exclude_none=True) == before + + +def test_identify_in_place_modes_still_mutate(): + """Sanity check: in_place="default"/"always" keep their mutating behavior.""" + allele = models.Allele(**allele_dict) + assert ga4gh_identify(allele, in_place="default") == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + assert allele.id == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + + allele = models.Allele(**allele_dict) + assert ga4gh_identify(allele, in_place="always") == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + assert allele.id == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + + def test_ga4gh_iri(): iri = models.iriReference.model_construct( "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" From e246e0ea4b8bb0abd9476c076b82629825d6b69d Mon Sep 17 00:00:00 2001 From: Rakesh Pai <41351936+developer-rpai@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:15:21 -0700 Subject: [PATCH 2/2] style: apply ruff format (fix lint CI) --- tests/test_vrs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_vrs.py b/tests/test_vrs.py index ae5d120a..add14df4 100644 --- a/tests/test_vrs.py +++ b/tests/test_vrs.py @@ -218,11 +218,17 @@ def test_identify_in_place_never_does_not_mutate(): def test_identify_in_place_modes_still_mutate(): """Sanity check: in_place="default"/"always" keep their mutating behavior.""" allele = models.Allele(**allele_dict) - assert ga4gh_identify(allele, in_place="default") == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + assert ( + ga4gh_identify(allele, in_place="default") + == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + ) assert allele.id == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" allele = models.Allele(**allele_dict) - assert ga4gh_identify(allele, in_place="always") == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + assert ( + ga4gh_identify(allele, in_place="always") + == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE" + ) assert allele.id == "ga4gh:VA.Hy2XU_-rp4IMh6I_1NXNecBo8Qx8n0oE"