fix: ga4gh_identify respects in_place=never for digest fields - #657
developer-rpai wants to merge 3 commits into
Conversation
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 ga4gh#440
|
I am not 100% on this fix; would really prefer to avoid an unnecessary deepcopy, but I'm not sure if that's avoidable |
korikuzma
left a comment
There was a problem hiding this comment.
See @jsstevenson comment above. Are you able to investigate this @developer-rpai ?
…ever Address reviewer feedback on the deepcopy in get_or_create_ga4gh_identifier: compute the identifier for in_place="never" without copying the object. - Add _recurse_ga4gh_serialize_no_store, a serialization recursion that never stores digests: nested identifiable objects contribute their existing digest (unless recompute), otherwise one computed with store=False. - Add _ValueObject._ga4gh_serialize_no_store (+ CisPhasedBlock override preserving the sorted-members tweak) and Ga4ghIdentifiableObject._compute_digest_no_store. - in_place="never" now builds the identifier from the no-store digest; default/always modes are untouched. Identifier values are unchanged vs the deepcopy approach (verified against the pinned values in test_identify_in_place_never_does_not_mutate and test_identify_in_place_modes_still_mutate).
|
Good news: the deepcopy is gone. I replaced it with a no-store computation path: Verified locally: identifier values are unchanged vs the deepcopy approach (allele with and without |
Bug
ga4gh_identify(allele, in_place="never")mutated the input object: it injecteddigestfields onto the Allele and its nested identifiable objects (e.g.SequenceLocation), even though the caller asked for no in-place edits. Reproducer from the issue:model_dump_json(exclude_none=True)grew from 323 to 411 chars after the call.Closes #440
Root cause
The
in_place="never"branch ofget_or_create_ga4gh_identifierrancompute_ga4gh_identifieron the live object. That path always mutates:compute_digest()stores the digest on the object itself (store=Truedefault), andga4gh_serialize()calls_recurse_ga4gh_serialize, which callsget_or_create_digest()on every nested identifiable object, setting theirdigestfields in place. So both the top-leveldigestand nested digests were written into the caller's object.Fix
When
in_placeis"never", compute the identifier on acopy.deepcopyof the object (src/ga4gh/vrs/models.py). The caller's object is left byte-identical; the only output is the returned identifier string. Thedefaultandalwaysmodes are unchanged.Verification
main(object JSON grew 275 to 363 chars;allele.digestandallele.location.digestinjected), then confirmed the fix leaves the object unchanged and returns the same identifier value.test_identify_in_place_never_does_not_mutate(fails on pristine code, passes with the fix) andtest_identify_in_place_modes_still_mutate(sanity check thatdefault/alwaysstill setid).tests/test_vrs.py: all 13 tests pass. Doctests insrc/ga4gh/core/identifiers.pyandsrc/ga4gh/vrs/models.py: 3 pass.Not verified
tests/validation/test_models.pyneeds thesubmodules/vrscheckout (submodule fetch is blocked in this sandbox), andtest_vrs_normalize.py/ parts oftest_dataproxy.pyneed the local seqrepo REST service on localhost:5000 (unavailable here; they fail with connection refused). Neither failure is related to this change.