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