Do not modify the caller's list in amino_acid_composition - #218
Merged
levitsky merged 1 commit intoSep 1, 2026
Merged
Conversation
With term_aa=True the terminal residues are removed with list.pop(). When `sequence` is a parsed list it was bound without copying, so the caller's list lost two elements and a second call on the same list returned a different composition. Copy the list before popping, and cover the list input in the tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
parser.amino_acid_composition(sequence, term_aa=True)modifies the list it is given.With
term_aa=Truethe terminal residues are removed withlist.pop()(pyteomics/parser.py:677-678). Whensequenceis already a parsed list,parser.py:663binds it without copying, so those pops hit the caller's list. Thestrbranch and the list-of-tuples branch both build a fresh list viaparse(); only this one aliases.On released 5.0.1:
Same input, two different answers, and two residues silently gone from the caller's list. The
strpath is idempotent, which is the control. The docstring documents a parsed list as valid input and says nothing about mutation.Downstream of this:
electrochem.py:218tells users to callparser.amino_acid_composition(..., term_aa=True)and feed the result tocharge/pI, andachrom.get_RCs_vary_lcpcallsget_RCsrepeatedly over the same input.Fix: copy the list before popping.
Tests, three cases in
ParserTest:show_unmodified_termini×term_aa;['H-', 'P', ..., '-OH']input. The literal was derived from the documented semantics (terminal residues relabelledntermX/ctermXand dropped from the plain counts, terminal groups retained), not read off the implementation.Measured with the CI command from
.github/workflows/pythonpackage.yml(cd tests; find . -name 'test_*.py' -print0 | xargs -0 -n1 python), same venv both sides, Python 3.14. Exit codes identical across all 27 test files; the only difference in the whole run istest_parser.pygoing from 22 to 25 tests. Six files fail identically before and after (test_fasta,test_featurexml,test_mgf,test_mzid,test_mzml,test_traml), all withOSError: [Errno 22]frommultiprocessingin atest_map*case — this sandbox cannot create POSIX semaphores. Unrelated to the change.Two mutants, each with the source marker checked before measuring:
Not covered: the
map()/multiprocessing tests and the PostgreSQL Unimod CI step, neither of which runs in my environment; Python versions other than 3.14; whether any third-party code depends on the current mutating behaviour.Prepared with AI tooling assistance (Claude Opus 5, used as a coding agent in this repository).