diff --git a/lib/bx/align/core.py b/lib/bx/align/core.py index 4edaa59..e504c2e 100644 --- a/lib/bx/align/core.py +++ b/lib/bx/align/core.py @@ -219,34 +219,18 @@ def remove_all_gap_columns(self) -> None: Remove any columns containing only gaps from alignment components, text of components is modified IN PLACE. """ - seqs: list[list[str] | None] = [] - for c in self.components: - if c.empty or c.text is None: - seqs.append(None) - else: - seqs.append(list(c.text)) - i = 0 - text_size = self.text_size - while i < text_size: - all_gap = True - for seq in seqs: - if seq is None: - continue - if seq[i] != "-": - all_gap = False - if all_gap: - for seq in seqs: - if seq is None: - continue - del seq[i] - text_size -= 1 - else: - i += 1 - for component, seq in zip(self.components, seqs): - if seq is None: - continue - component.text = "".join(seq) - self.text_size = text_size + # A component can occur more than once; rebuild each object only once. + texts = list({id(c): (c, c.text) for c in self.components if not c.empty and c.text is not None}.values()) + if any("-" not in text for _, text in texts): + return + # Decide which columns to retain before modifying any component. Deleting + # individual gaps from lists repeatedly shifts the remaining alignment. + keep = [i for i in range(self.text_size) if any(text[i] != "-" for _, text in texts)] + if len(keep) == self.text_size: + return + for component, text in texts: + component.text = "".join(text[i] for i in keep) + self.text_size = len(keep) def __eq__(self, other: object) -> bool: if other is None or not isinstance(other, type(self)): diff --git a/lib/bx/align/maf_tests.py b/lib/bx/align/maf_tests.py index d343f0f..cf0710c 100644 --- a/lib/bx/align/maf_tests.py +++ b/lib/bx/align/maf_tests.py @@ -4,6 +4,8 @@ from io import StringIO +import pytest + from bx import align from bx.align import maf @@ -211,7 +213,9 @@ def test_column_iter(): assert c == expected[i] -def test_remove_all_gap_column(): +@pytest.mark.parametrize("empty_index", [0, 1, 3]) +@pytest.mark.parametrize("repeat_component", [False, True]) +def test_remove_all_gap_column(empty_index, repeat_component): complex_maf_gap = align.Alignment() complex_maf_gap.score = "7009" complex_maf_gap.components.append( @@ -231,7 +235,15 @@ def test_remove_all_gap_column(): complex_maf_gap.components[-1].empty = True complex_maf_gap.components[-1].synteny_empty = maf.MAF_INSERT_STATUS complex_maf_gap.text_size = 11 + empty = complex_maf_gap.components.pop() + complex_maf_gap.components.insert(empty_index, empty) + if repeat_component: + complex_maf_gap.add_component(next(c for c in complex_maf_gap.components if not c.empty)) complex_maf_gap.remove_all_gap_columns() + if repeat_component: + complex_maf_gap.components.pop() + # Return the empty row to its fixture position for the equality check. + complex_maf_gap.components.append(complex_maf_gap.components.pop(empty_index)) assert complex_maf_gap == complex_maf