From 133fcbc9d25b7525c79ffea13c2b4c09cbbbf440 Mon Sep 17 00:00:00 2001 From: Tom David Mueller Date: Tue, 1 Sep 2026 11:57:46 +0200 Subject: [PATCH] fix(results): support pyOpenMS 3.5.0 IdXMLFile.load signature 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. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku --- src/WorkflowTest.py | 7 +--- src/common/results_helpers.py | 29 +++++++++++--- tests/test_results_helpers.py | 73 +++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/WorkflowTest.py b/src/WorkflowTest.py index 120efb0..a2e1274 100644 --- a/src/WorkflowTest.py +++ b/src/WorkflowTest.py @@ -4,7 +4,6 @@ import pandas as pd import plotly.express as px from streamlit_plotly_events import plotly_events -from pyopenms import IdXMLFile from scipy.stats import ttest_ind import numpy as np import mygene @@ -14,7 +13,7 @@ from src.workflow.WorkflowManager import WorkflowManager from src.common.common import page_setup from src.common.results_helpers import get_abundance_data -from src.common.results_helpers import parse_idxml, build_spectra_cache +from src.common.results_helpers import parse_idxml, build_spectra_cache, load_idxml from openms_insight import Table, Heatmap, LinePlot, SequenceView # params = page_setup() @@ -1636,9 +1635,7 @@ def results(self) -> None: selected_file = st.selectbox("📁 Select Identification result file", comet_files) def idxml_to_df(idxml_file): - proteins = [] - peptides = [] - IdXMLFile().load(str(idxml_file), proteins, peptides) + proteins, peptides = load_idxml(idxml_file) records = [] for pep in peptides: diff --git a/src/common/results_helpers.py b/src/common/results_helpers.py index 2d38ad9..39b129c 100644 --- a/src/common/results_helpers.py +++ b/src/common/results_helpers.py @@ -6,6 +6,10 @@ import streamlit as st from pathlib import Path from pyopenms import IdXMLFile, MSExperiment, MzMLFile +try: # pyOpenMS >= 3.5.0 + from pyopenms import PeptideIdentificationList +except ImportError: # pyOpenMS <= 3.4.x + PeptideIdentificationList = None from src.workflow.ParameterManager import ParameterManager def get_workflow_dir(workspace): @@ -13,11 +17,26 @@ def get_workflow_dir(workspace): return Path(workspace, "topp-workflow") -def idxml_to_df(idxml_file): - """Parse idXML file and return DataFrame with peptide hits.""" +def load_idxml(idxml_file): + """Load an idXML file, returning ``(protein_ids, peptide_ids)`` as plain lists. + + pyOpenMS 3.5.0 changed the third parameter of ``IdXMLFile.load()`` from a + ``libcpp_vector[PeptideIdentification]``, which accepted an ordinary Python + list, to a dedicated ``PeptideIdentificationList`` container. A list no longer + matches any overload there and pyOpenMS raises + ``Exception: can not handle type of (, [], [])``. Feature-detect the + container so the app runs on 3.5.0 as well as earlier releases, and hand + callers back an ordinary list either way. + """ proteins = [] - peptides = [] + peptides = PeptideIdentificationList() if PeptideIdentificationList else [] IdXMLFile().load(str(idxml_file), proteins, peptides) + return proteins, list(peptides) + + +def idxml_to_df(idxml_file): + """Parse idXML file and return DataFrame with peptide hits.""" + proteins, peptides = load_idxml(idxml_file) records = [] for pep in peptides: @@ -99,9 +118,7 @@ def parse_idxml(idxml_path: Path) -> tuple[pl.DataFrame, list[str]]: Returns: Tuple of (id_df, spectra_data list of source filenames) """ - proteins = [] - peptides = [] - IdXMLFile().load(str(idxml_path), proteins, peptides) + proteins, peptides = load_idxml(idxml_path) # Derive mzML filename from idXML filename (e.g., 02COVID_filter.idXML -> 02COVID.mzML) spectra_data = [extract_filename_from_idxml(idxml_path)] diff --git a/tests/test_results_helpers.py b/tests/test_results_helpers.py index 5723318..3d7b669 100644 --- a/tests/test_results_helpers.py +++ b/tests/test_results_helpers.py @@ -1,10 +1,13 @@ from pathlib import Path +import pyopenms as poms + from src.common.results_helpers import ( extract_filename_from_idxml, extract_scan_from_ref, extract_scan_number, get_workflow_dir, + load_idxml, ) @@ -25,3 +28,73 @@ def test_extract_filename_from_idxml_strips_suffixes(): assert extract_filename_from_idxml(Path("02COVID_filter.idXML")) == "02COVID.mzML" assert extract_filename_from_idxml(Path("sample_comet.idXML")) == "sample.mzML" assert extract_filename_from_idxml(Path("run_per.idXML")) == "run.mzML" + + +def _write_idxml(path): + """Write a small idXML with one protein and two peptide hits.""" + prot = poms.ProteinIdentification() + prot.setIdentifier("SEARCH_1") + hit = poms.ProteinHit() + hit.setAccession("sp|P12345|TEST") + prot.setHits([hit]) + + peptides = [] + for i, seq in enumerate(("PEPTIDEK", "ELVISLIVESR")): + pep = poms.PeptideIdentification() + pep.setIdentifier("SEARCH_1") + pep.setRT(100.0 + i) + pep.setMZ(500.0 + i) + pep.setMetaValue("spectrum_reference", f"scan={i + 1}") + pep_hit = poms.PeptideHit() + pep_hit.setSequence(poms.AASequence.fromString(seq)) + pep_hit.setCharge(2) + evidence = poms.PeptideEvidence() + evidence.setProteinAccession("sp|P12345|TEST") + pep_hit.setPeptideEvidences([evidence]) + pep.setHits([pep_hit]) + peptides.append(pep) + + # pyOpenMS >= 3.5.0 wants the dedicated container here too. + if hasattr(poms, "PeptideIdentificationList"): + container = poms.PeptideIdentificationList() + for pep in peptides: + container.push_back(pep) + peptides = container + + poms.IdXMLFile().store(str(path), [prot], peptides) + + +def test_load_idxml_reads_identifications(tmp_path): + """Regression test for pyOpenMS 3.5.0. + + 3.5.0 changed the third parameter of ``IdXMLFile.load()`` from a plain + ``libcpp_vector[PeptideIdentification]`` to a ``PeptideIdentificationList``, + so passing ``[]`` raised ``can not handle type of (, [], [])``. + """ + idxml = tmp_path / "sample_comet.idXML" + _write_idxml(idxml) + + proteins, peptides = load_idxml(idxml) + + # A plain list either way, so callers can index and len() it. + assert isinstance(peptides, list) + assert isinstance(proteins, list) + assert len(proteins) == 1 + assert len(peptides) == 2 + + sequences = [ + hit.getSequence().toString() + for pep in peptides + for hit in pep.getHits() + ] + assert sequences == ["PEPTIDEK", "ELVISLIVESR"] + assert peptides[0].getRT() == 100.0 + assert peptides[0].getHits()[0].getCharge() == 2 + + +def test_load_idxml_accepts_str_path(tmp_path): + """Callers pass both Path and str; load_idxml must handle either.""" + idxml = tmp_path / "run_per.idXML" + _write_idxml(idxml) + + assert len(load_idxml(str(idxml))[1]) == 2