Skip to content

Commit 797fb3d

Browse files
t0mdavid-mclaude
andauthored
fix(results): support pyOpenMS 3.5.0 IdXMLFile.load signature (#32)
Loading any idXML on the cluster failed with: Exception: can not handle type of ('..._comet.idXML', [], []) pyOpenMS 3.5.0 changed the third parameter of IdXMLFile.load()/store() from a libcpp_vector[PeptideIdentification], which accepted an ordinary Python list, to a dedicated PeptideIdentificationList container. Passing a list there matches no overload, so autowrap's dispatcher raises before any file I/O happens. protein_ids is unaffected and still takes a list. Bisected across locally installed releases: 3.1.0, 3.2.0, 3.3.0, 3.4.0 and 3.4.1 all accept a list; 3.5.0 does not. requirements.txt pins pyopenms==3.5.0 and Dockerfile builds OpenMS release/3.5.0, so deployed images hit this on every results page, while dev environments still on 3.3.x do not. Add load_idxml() in results_helpers, which feature-detects PeptideIdentificationList and normalises the result back to a plain list, and route the three call sites through it. Feature detection rather than a version check keeps the app working on 3.4.x and earlier too. IdXMLFile is no longer referenced in WorkflowTest, so drop the import. Verified end-to-end against a generated idXML on 3.1.0, 3.3.0, 3.4.0 (legacy list path) and 3.5.0 (new container), all yielding identical parsed output. The added regression tests reproduce the exact production error message when the fix is reverted. Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 87c0449 commit 797fb3d

3 files changed

Lines changed: 98 additions & 11 deletions

File tree

src/WorkflowTest.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import pandas as pd
55
import plotly.express as px
66
from streamlit_plotly_events import plotly_events
7-
from pyopenms import IdXMLFile
87
from scipy.stats import ttest_ind
98
import numpy as np
109
import mygene
@@ -14,7 +13,7 @@
1413
from src.workflow.WorkflowManager import WorkflowManager
1514
from src.common.common import page_setup
1615
from src.common.results_helpers import get_abundance_data
17-
from src.common.results_helpers import parse_idxml, build_spectra_cache
16+
from src.common.results_helpers import parse_idxml, build_spectra_cache, load_idxml
1817
from openms_insight import Table, Heatmap, LinePlot, SequenceView
1918

2019
# params = page_setup()
@@ -1636,9 +1635,7 @@ def results(self) -> None:
16361635
selected_file = st.selectbox("📁 Select Identification result file", comet_files)
16371636

16381637
def idxml_to_df(idxml_file):
1639-
proteins = []
1640-
peptides = []
1641-
IdXMLFile().load(str(idxml_file), proteins, peptides)
1638+
proteins, peptides = load_idxml(idxml_file)
16421639

16431640
records = []
16441641
for pep in peptides:

src/common/results_helpers.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,37 @@
66
import streamlit as st
77
from pathlib import Path
88
from pyopenms import IdXMLFile, MSExperiment, MzMLFile
9+
try: # pyOpenMS >= 3.5.0
10+
from pyopenms import PeptideIdentificationList
11+
except ImportError: # pyOpenMS <= 3.4.x
12+
PeptideIdentificationList = None
913
from src.workflow.ParameterManager import ParameterManager
1014

1115
def get_workflow_dir(workspace):
1216
"""Get the workflow directory path."""
1317
return Path(workspace, "topp-workflow")
1418

1519

16-
def idxml_to_df(idxml_file):
17-
"""Parse idXML file and return DataFrame with peptide hits."""
20+
def load_idxml(idxml_file):
21+
"""Load an idXML file, returning ``(protein_ids, peptide_ids)`` as plain lists.
22+
23+
pyOpenMS 3.5.0 changed the third parameter of ``IdXMLFile.load()`` from a
24+
``libcpp_vector[PeptideIdentification]``, which accepted an ordinary Python
25+
list, to a dedicated ``PeptideIdentificationList`` container. A list no longer
26+
matches any overload there and pyOpenMS raises
27+
``Exception: can not handle type of (<path>, [], [])``. Feature-detect the
28+
container so the app runs on 3.5.0 as well as earlier releases, and hand
29+
callers back an ordinary list either way.
30+
"""
1831
proteins = []
19-
peptides = []
32+
peptides = PeptideIdentificationList() if PeptideIdentificationList else []
2033
IdXMLFile().load(str(idxml_file), proteins, peptides)
34+
return proteins, list(peptides)
35+
36+
37+
def idxml_to_df(idxml_file):
38+
"""Parse idXML file and return DataFrame with peptide hits."""
39+
proteins, peptides = load_idxml(idxml_file)
2140

2241
records = []
2342
for pep in peptides:
@@ -99,9 +118,7 @@ def parse_idxml(idxml_path: Path) -> tuple[pl.DataFrame, list[str]]:
99118
Returns:
100119
Tuple of (id_df, spectra_data list of source filenames)
101120
"""
102-
proteins = []
103-
peptides = []
104-
IdXMLFile().load(str(idxml_path), proteins, peptides)
121+
proteins, peptides = load_idxml(idxml_path)
105122

106123
# Derive mzML filename from idXML filename (e.g., 02COVID_filter.idXML -> 02COVID.mzML)
107124
spectra_data = [extract_filename_from_idxml(idxml_path)]

tests/test_results_helpers.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from pathlib import Path
22

3+
import pyopenms as poms
4+
35
from src.common.results_helpers import (
46
extract_filename_from_idxml,
57
extract_scan_from_ref,
68
extract_scan_number,
79
get_workflow_dir,
10+
load_idxml,
811
)
912

1013

@@ -25,3 +28,73 @@ def test_extract_filename_from_idxml_strips_suffixes():
2528
assert extract_filename_from_idxml(Path("02COVID_filter.idXML")) == "02COVID.mzML"
2629
assert extract_filename_from_idxml(Path("sample_comet.idXML")) == "sample.mzML"
2730
assert extract_filename_from_idxml(Path("run_per.idXML")) == "run.mzML"
31+
32+
33+
def _write_idxml(path):
34+
"""Write a small idXML with one protein and two peptide hits."""
35+
prot = poms.ProteinIdentification()
36+
prot.setIdentifier("SEARCH_1")
37+
hit = poms.ProteinHit()
38+
hit.setAccession("sp|P12345|TEST")
39+
prot.setHits([hit])
40+
41+
peptides = []
42+
for i, seq in enumerate(("PEPTIDEK", "ELVISLIVESR")):
43+
pep = poms.PeptideIdentification()
44+
pep.setIdentifier("SEARCH_1")
45+
pep.setRT(100.0 + i)
46+
pep.setMZ(500.0 + i)
47+
pep.setMetaValue("spectrum_reference", f"scan={i + 1}")
48+
pep_hit = poms.PeptideHit()
49+
pep_hit.setSequence(poms.AASequence.fromString(seq))
50+
pep_hit.setCharge(2)
51+
evidence = poms.PeptideEvidence()
52+
evidence.setProteinAccession("sp|P12345|TEST")
53+
pep_hit.setPeptideEvidences([evidence])
54+
pep.setHits([pep_hit])
55+
peptides.append(pep)
56+
57+
# pyOpenMS >= 3.5.0 wants the dedicated container here too.
58+
if hasattr(poms, "PeptideIdentificationList"):
59+
container = poms.PeptideIdentificationList()
60+
for pep in peptides:
61+
container.push_back(pep)
62+
peptides = container
63+
64+
poms.IdXMLFile().store(str(path), [prot], peptides)
65+
66+
67+
def test_load_idxml_reads_identifications(tmp_path):
68+
"""Regression test for pyOpenMS 3.5.0.
69+
70+
3.5.0 changed the third parameter of ``IdXMLFile.load()`` from a plain
71+
``libcpp_vector[PeptideIdentification]`` to a ``PeptideIdentificationList``,
72+
so passing ``[]`` raised ``can not handle type of (<path>, [], [])``.
73+
"""
74+
idxml = tmp_path / "sample_comet.idXML"
75+
_write_idxml(idxml)
76+
77+
proteins, peptides = load_idxml(idxml)
78+
79+
# A plain list either way, so callers can index and len() it.
80+
assert isinstance(peptides, list)
81+
assert isinstance(proteins, list)
82+
assert len(proteins) == 1
83+
assert len(peptides) == 2
84+
85+
sequences = [
86+
hit.getSequence().toString()
87+
for pep in peptides
88+
for hit in pep.getHits()
89+
]
90+
assert sequences == ["PEPTIDEK", "ELVISLIVESR"]
91+
assert peptides[0].getRT() == 100.0
92+
assert peptides[0].getHits()[0].getCharge() == 2
93+
94+
95+
def test_load_idxml_accepts_str_path(tmp_path):
96+
"""Callers pass both Path and str; load_idxml must handle either."""
97+
idxml = tmp_path / "run_per.idXML"
98+
_write_idxml(idxml)
99+
100+
assert len(load_idxml(str(idxml))[1]) == 2

0 commit comments

Comments
 (0)