Repository: matchms/matchms (python)

Issue:
Load from mgf does not close the file after the generator is empty. 
When creating a test for save_spectra I noticed that load_from_mgf does not correctly close the file after completely loading, while other loading methods do.

The test I wrote did run this. 
```python
spectrum_list = load_test_spectra_file()
    with tempfile.TemporaryDirectory() as temp_dir:
        filename = os.path.join(temp_dir, file_name)
        save_spectra(spectrum_list, filename)
        if not os.path.exists(filename):
            FileNotFoundError("No file was created")
        reloaded_spectra = list(load_spectra(filename))
```
And resulted in the error:
E           NotADirectoryError: [WinError 267] The directory name is invalid: 'C:\\Users\\jonge094\\AppData\\Local\\Temp\\tmpv9hy574j\\spectra.mgf'
The issue only occurs on windows and the problem is that the file cannot be removed before being closed, see https://python-forum.io/thread-33169.html 

This indicates that the file is still kept open after everything was loaded, which is not ideal. I will solve this by passing an opened file to the MGF class in #543 


Pull request description:
Prevent accidental file overwriting
@hechth @florian-huber I agreed with your comments that the initial solution was not ideal. Therefore I reimplemented this in a better way. The saving now happens within spectrumprocessor, to make this possible I also added an save_spectra function, that saves as the filetype specified in the file name.
- [x] Add save_spectra, see #441 
- [x] Fix #555 (loading from mgf should use file object instead of file name)
- [x] Check if file exists before running pipeline
- [x] Save file auotmatically in pipeline
- [x] Add to changelog
- [x] Add tests
- [x] linting


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 915c0a00..5153e9d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,13 +5,17 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+
 ## [unreleased]
 ### Added
 - Option to set custom key replacements [#547](https://github.com/matchms/matchms/pull/547)
 - Option to set the export style in `save_as_mgf` and `save_as_json` to choose other than matchms styles such as `nist`, `riken`, `gnps` [#557](https://github.com/matchms/matchms/pull/557)
-
+- Added a save spectra function. To automatically save in the specified file format. [#543](https://github.com/matchms/matchms/pull/543)
+- Add saving function in SpectrumProcessor [#543](https://github.com/matchms/matchms/pull/543)
 ### Fixed
 - Handle missing `precursor_mz` in representation and [#452](https://github.com/matchms/matchms/issues/452) introduced by [#514](https://github.com/matchms/matchms/pull/514/files)[#540](https://github.com/matchms/matchms/pull/540)
+- Fix closing mgf file after loading and prevent reopening. [#555](https://github.com/matchms/matchms/issues/555)
+
 ### Changed
 - Renamed derive_smiles_from_pubchem_compound_name_search to derive_annotation_from_compound_name. [#559](https://github.com/matchms/matchms/pull/559)
 - Derive_annotation_from_compound_name does not add smile or inchi when this cannot be interpreted by rdkit. [#559](https://github.com/matchms/matchms/pull/559)
diff --git a/README.rst b/README.rst
index 112248a3..386ab5ad 100644
--- a/README.rst
+++ b/README.rst
@@ -174,11 +174,14 @@ followed by calculating the Cosine score between mass Spectrums in the `tests/te
 Below a more advanced code example showing how you can make a specific pipeline for your needs.
 
 .. code-block:: python
-
+    import os
     from matchms.Pipeline import Pipeline, create_workflow
+    from matchms.exporting.save_as_mgf import save_as_mgf
+    results_folder = "./results"
+    os.makedirs(results_folder, exist_ok=True)
 
     workflow = create_workflow(
-        yaml_file_name="my_config_file.yaml", # The workflow will be stored in a yaml file.
+        yaml_file_name=os.path.join(results_folder, "my_config_file.yaml"), # The workflow will be stored in a yaml file.
         predefined_processing_queries="basic",
         additional_filters_queries=[
            ["add_parent_mass"],
@@ -193,9 +196,11 @@ Below a more advanced code example showing how you can make a specific pipeline
                             ["filter_by_range", {"name": "CosineGreedy_score", "low": 0.2}]],
         )
     pipeline = Pipeline(workflow)
-    pipeline.logging_file = "my_pipeline.log"  # for pipeline and logging message
-    pipeline.logging_level = "INFO" #To define the verbosety of the logging
-    pipeline.run("tests/testdata/pesticides.mgf", "my_reference_library.mgf")
+    pipeline.logging_file = os.path.join(results_folder, "my_pipeline.log")  # for pipeline and logging message
+    pipeline.logging_level = "WARNING" #To define the verbosety of the logging
+    pipeline.run("tests/testdata/pesticides.mgf", "my_reference_library.mgf", 
+    cleaned_query_file=os.path.join(results_folder, "cleaned_query_spectra.mgf"), 
+    cleaned_reference_file=os.path.join(results_folder, "cleaned_library_spectra.mgf")) # choose your own files
 
 
 Alternatively, in particular, if you need more room to add custom functions and steps, the individual steps can run without using the matchms ``Pipeline``:
diff --git a/matchms/Pipeline.py b/matchms/Pipeline.py
index f8f3c342..00673164 100644
--- a/matchms/Pipeline.py
+++ b/matchms/Pipeline.py
@@ -1,7 +1,8 @@
 import logging
+import os
 from collections import OrderedDict
 from datetime import datetime
-from typing import Iterable, List, Optional, Union
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 import matchms.similarity as mssimilarity
 from matchms import calculate_scores
 from matchms.filtering.filter_order_and_default_pipelines import ALL_FILTERS
@@ -23,8 +24,8 @@ logger = logging.getLogger("matchms")
 def create_workflow(yaml_file_name: Optional[str] = None,
                     predefined_processing_queries: Optional[str] = "default",
                     predefined_processing_reference: Optional[str] = "default",
-                    additional_filters_queries: Iterable[Union[str, List[dict]]] = (),
-                    additional_filters_references: Iterable[Union[str, List[dict]]] = (),
+                    additional_filters_queries: Iterable[Union[str, Tuple[str, Dict[str, Any]]]] = (),
+                    additional_filters_references: Iterable[Union[str, Tuple[str, Dict[str, Any]]]] = (),
                     score_computations: Iterable[Union[str, List[dict]]] = (),
                     ) -> OrderedDict:
     """Creates a workflow that specifies the filters and scores needed to be run by Pipeline
@@ -57,6 +58,10 @@ def create_workflow(yaml_file_name: Optional[str] = None,
     workflow["reference_filters"] = reference_processor.processing_steps
     workflow["score_computations"] = score_computations
     if yaml_file_name is not None:
+        assert not os.path.exists(yaml_file_name), \
+            "This yaml file name already exists. " \
+            "To use the settings in the yaml file, please use the load_workflow_from_yaml_file function " \
+            "in yaml_file_functions.py or check the tutorial."
         with open(yaml_file_name, 'w', encoding="utf-8") as file:
             file.write("# Matchms pipeline config file \n")
             file.write("# Change and adapt fields where necessary \n")
@@ -181,7 +186,7 @@ class Pipeline:
         assert set(self.__workflow.keys()) == expected_keys
         check_score_computation(score_computations=self.score_computations)
 
-    def run(self, query_files, reference_files = None):
+    def run(self, query_files, reference_files = None, cleaned_query_file=None, cleaned_reference_file=None):
         """Execute the defined Pipeline workflow.
 
         This method will execute all steps of the workflow.
@@ -189,6 +194,13 @@ class Pipeline:
         2) Spectrum processing (using matchms filters)
         3) Score Computations
         """
+        if cleaned_reference_file is not None:
+            if os.path.exists(cleaned_reference_file):
+                raise FileExistsError("The specified save references file already exists")
+        if cleaned_query_file is not None:
+            if os.path.exists(cleaned_query_file):
+                raise FileExistsError("The specified save queries file already exists")
+
         self.set_logging()
         self.write_to_logfile("--- Start running matchms pipeline. ---")
         self.write_to_logfile(f"Start time: {str(datetime.now())}")
@@ -198,18 +210,15 @@ class Pipeline:
         self.write_to_logfile("--- Processing spectra ---")
         self.write_to_logfile(f"Time: {str(datetime.now())}")
         # Process query spectra
-        spectrums, report = self.processing_queries.process_spectrums(
-            self._spectrums_queries,
-            create_report=True,
-            progress_bar=self.progress_bar)
+        spectrums, report = self.processing_queries.process_spectrums(self._spectrums_queries,
+                                                                      progress_bar=self.progress_bar,
+                                                                      cleaned_spectra_file=cleaned_query_file)
         self._spectrums_queries = spectrums
         self.write_to_logfile(str(report))
         # Process reference spectra (if necessary)
         if self.is_symmetric is False:
             self._spectrums_references, report = self.processing_references.process_spectrums(
-                self._spectrums_references,
-                create_report=True,
-                progress_bar=self.progress_bar)
+                self._spectrums_references, progress_bar=self.progress_bar, cleaned_spectra_file=cleaned_reference_file)
             self.write_to_logfile(str(report))
         else:
             self._spectrums_references = self._spectrums_queries
diff --git a/matchms/exporting/__init__.py b/matchms/exporting/__init__.py
index 4de3e38f..982ad6dc 100644
--- a/matchms/exporting/__init__.py
+++ b/matchms/exporting/__init__.py
@@ -8,10 +8,12 @@ can be exported to json, mgf, or msp files.
 from .save_as_json import save_as_json
 from .save_as_mgf import save_as_mgf
 from .save_as_msp import save_as_msp
+from .save_spectra import save_spectra
 
 
 __all__ = [
     "save_as_json",
     "save_as_mgf",
     "save_as_msp",
+    "save_spectra"
 ]
diff --git a/matchms/exporting/save_spectra.py b/matchms/exporting/save_spectra.py
new file mode 100644
index 00000000..0c7d76c1
--- /dev/null
+++ b/matchms/exporting/save_spectra.py
@@ -0,0 +1,70 @@
+import logging
+import os
+import pickle
+from typing import List
+from matchms.exporting import save_as_json, save_as_mgf, save_as_msp
+from matchms.Spectrum import Spectrum
+
+
+logger = logging.getLogger("matchms")
+
+
+def save_spectra(spectrums: List[Spectrum],
+                 file: str,
+                 export_style: str = "matchms") -> None:
+    """Saves spectra as the file type specified.
+
+    The following file extensions can be used:
+    "json", "mgf", "msp"
+
+    Args:
+    -----
+    spectrums:
+        The spectra that are saved.
+    file:
+        Path to file containing spectra, with file extension "json", "mgf", "msp"
+    ftype:
+        Optional. Filetype
+    export_style:
+        Converts the keys to the required export style. One of ["matchms", "massbank", "nist", "riken", "gnps"].
+        Default is "matchms"
+    """
+    if os.path.exists(file):
+        raise FileExistsError(f"The specified file: {file} already exists.")
+
+    ftype = os.path.splitext(file)[1].lower()[1:]
+
+    if ftype == "json":
+        save_as_json(spectrums, file, export_style)
+    elif ftype == "mgf":
+        save_as_mgf(spectrums, file, export_style)
+    elif ftype == "msp":
+        save_as_msp(spectrums, file, style=export_style)
+    elif ftype == "pickle":
+        if export_style != "matchms":
+            logger.error("The only available export style for pickle is 'matchms', your export style %s", export_style)
+        save_as_pickled_file(spectrums, file)
+    else:
+        raise TypeError(f"File extension of file: {file} is not recognized")
+
+
+def save_as_pickled_file(spectrums, filename: str) -> None:
+    """Stores spectra as a pickled object
+
+    Args:
+    -----
+    spectrums:
+        The spectra that are saved.
+    filename:
+        Path to file containing spectra, with file extension "json", "mgf", "msp"
+    """
+    if os.path.exists(filename):
+        raise FileExistsError(f"The file '{filename}' already exists.")
+
+    if not isinstance(spectrums, list):
+        raise TypeError("Expected list of spectra")
+    if not isinstance(spectrums[0], Spectrum):
+        raise TypeError("Expected list of spectra")
+
+    with open(filename, "wb") as f:
+        pickle.dump(spectrums, f)
diff --git a/matchms/filtering/SpectrumProcessor.py b/matchms/filtering/SpectrumProcessor.py
index 17df7490..55c43961 100644
--- a/matchms/filtering/SpectrumProcessor.py
+++ b/matchms/filtering/SpectrumProcessor.py
@@ -1,5 +1,6 @@
 import inspect
 import logging
+import os
 from collections import OrderedDict, defaultdict
 from functools import partial
 from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union
@@ -7,6 +8,7 @@ import numpy as np
 import pandas as pd
 from tqdm import tqdm
 from matchms import Spectrum
+from matchms.exporting import save_spectra
 from matchms.filtering.filter_order_and_default_pipelines import (
     ALL_FILTERS, FILTER_FUNCTION_NAMES, PREDEFINED_PIPELINES)
 from matchms.yaml_file_functions import ordered_dump
@@ -166,8 +168,8 @@ class SpectrumProcessor:
         return spectrum_out
 
     def process_spectrums(self, spectrums: list,
-                          create_report: bool = False,
                           progress_bar: bool = True,
+                          cleaned_spectra_file=None
                           ):
         """
         Process a list of spectrums with all filters in the processing pipeline.
@@ -181,16 +183,20 @@ class SpectrumProcessor:
             The report will be returned as pandas DataFrame. Default is set to False.
         progress_bar : bool, optional
             Displays progress bar if set to True. Default is True.
+        cleaned_spectra_file:
+            Path to where the cleaned spectra should be saved.
 
         Returns
         -------
         Spectrums
             List containing the processed spectrums.
+        processing_report
+            A ProcessingReport containing the effect of the filters.
         """
-        if create_report:
-            processing_report = ProcessingReport(self.filters)
-        else:
-            processing_report = None
+        if cleaned_spectra_file is not None:
+            if os.path.exists(cleaned_spectra_file):
+                raise FileExistsError("The specified save references file already exists")
+        processing_report = ProcessingReport(self.filters)
 
         processed_spectrums = []
         for s in tqdm(spectrums, disable=(not progress_bar), desc="Processing spectrums"):
@@ -200,9 +206,10 @@ class SpectrumProcessor:
             if processed_spectrum is not None:
                 processed_spectrums.append(processed_spectrum)
 
-        if create_report:
-            return processed_spectrums, processing_report
-        return processed_spectrums
+        if cleaned_spectra_file is not None:
+            save_spectra(processed_spectrums, cleaned_spectra_file)
+
+        return processed_spectrums, processing_report
 
     @property
     def processing_steps(self):
diff --git a/matchms/importing/load_from_mgf.py b/matchms/importing/load_from_mgf.py
index 54e442b0..a49e14e2 100644
--- a/matchms/importing/load_from_mgf.py
+++ b/matchms/importing/load_from_mgf.py
@@ -5,7 +5,7 @@ from pyteomics.mgf import MGF
 from ..Spectrum import Spectrum
 
 
-def load_from_mgf(source: Union[str, TextIO],
+def load_from_mgf(filename: Union[str, TextIO],
                   metadata_harmonization: bool = True) -> Generator[Spectrum, None, None]:
     """Load spectrum(s) from mgf file.
 
@@ -27,29 +27,29 @@ def load_from_mgf(source: Union[str, TextIO],
 
     Parameters
     ----------
-    source:
+    filename:
         Accepts both filename (with path) for .mgf file or a file-like
         object from a preloaded MGF file.
     metadata_harmonization : bool, optional
         Set to False if metadata harmonization to default keys is not desired.
         The default is True.
     """
-
-    for pyteomics_spectrum in MGF(source, convert_arrays=1):
-
-        metadata = pyteomics_spectrum.get("params", None)
-        mz = pyteomics_spectrum["m/z array"]
-        intensities = pyteomics_spectrum["intensity array"]
-        if "peak_comments" in metadata.keys():
-            metadata["peak_comments"] = ast.literal_eval(str(metadata["peak_comments"]))
-
-        # Sort by mz (if not sorted already)
-        if not np.all(mz[:-1] <= mz[1:]):
-            idx_sorted = np.argsort(mz)
-            mz = mz[idx_sorted]
-            intensities = intensities[idx_sorted]
-
-        yield Spectrum(mz=mz,
-                       intensities=intensities,
-                       metadata=metadata,
-                       metadata_harmonization=metadata_harmonization)
+    with open(filename, 'r', encoding='utf-8') as file:
+        for pyteomics_spectrum in MGF(file, convert_arrays=1):
+
+            metadata = pyteomics_spectrum.get("params", None)
+            mz = pyteomics_spectrum["m/z array"]
+            intensities = pyteomics_spectrum["intensity array"]
+            if "peak_comments" in metadata.keys():
+                metadata["peak_comments"] = ast.literal_eval(str(metadata["peak_comments"]))
+
+            # Sort by mz (if not sorted already)
+            if not np.all(mz[:-1] <= mz[1:]):
+                idx_sorted = np.argsort(mz)
+                mz = mz[idx_sorted]
+                intensities = intensities[idx_sorted]
+
+            yield Spectrum(mz=mz,
+                           intensities=intensities,
+                           metadata=metadata,
+                           metadata_harmonization=metadata_harmonization)
diff --git a/matchms/importing/load_spectra.py b/matchms/importing/load_spectra.py
index bdac5279..5d3cd4df 100644
--- a/matchms/importing/load_spectra.py
+++ b/matchms/importing/load_spectra.py
@@ -1,14 +1,13 @@
 import os
 import pickle
 from typing import Generator, List, Optional, Union
-from matchms import Spectrum
 from matchms.importing import (load_from_json, load_from_mgf, load_from_msp,
                                load_from_mzml, load_from_mzxml, load_from_usi)
-from matchms.typing import SpectrumType
+from matchms.Spectrum import Spectrum
 
 
 def load_spectra(file: str, metadata_harmonization: bool = True,
-                 ftype: Optional[str] = None) -> Union[List[SpectrumType], Generator[SpectrumType, None, None]]:
+                 ftype: Optional[str] = None) -> Union[List[Spectrum], Generator[Spectrum, None, None]]:
     """Loads spectra from your spectrum file into memory as matchms Spectrum object
 
     The following file extensions can be loaded in with this function:
@@ -48,7 +47,7 @@ def load_spectra(file: str, metadata_harmonization: bool = True,
     raise TypeError(f"File extension of file: {file} is not recognized")
 
 
-def load_from_pickle(filename: str, metadata_harmonization: bool) -> List[SpectrumType]:
+def load_from_pickle(filename: str, metadata_harmonization: bool) -> List[Spectrum]:
     """Load spectra stored in pickle
 
     Args:
@@ -60,16 +59,20 @@ def load_from_pickle(filename: str, metadata_harmonization: bool) -> List[Spectr
     with open(filename, 'rb') as file:
         loaded_object = pickle.load(file)
 
-    if not isinstance(loaded_object, list) or not isinstance(loaded_object[0], SpectrumType):
+    if not isinstance(loaded_object, list):
         raise TypeError("Expected list of spectra")
+    for spectrum in loaded_object:
+        if not isinstance(spectrum, Spectrum):
+            raise TypeError("Expected list of spectra")
 
     if metadata_harmonization:
-        loaded_object = [Spectrum(x.peaks.mz, x.peaks.intensisites, x.metadata, metadata_harmonization) for x in loaded_object]
+        loaded_object = [Spectrum(spectrum.mz, spectrum.intensities,
+                                  spectrum.metadata, metadata_harmonization) for spectrum in loaded_object]
     return loaded_object
 
 
 def load_list_of_spectrum_files(spectrum_files: Union[List[str], str]
-                                ) -> Union[List[SpectrumType], Generator[SpectrumType, None, None]]:
+                                ) -> Union[List[Spectrum], Generator[Spectrum, None, None]]:
     """Combines all spectra in multiple files into a list of spectra"""
     # Just load spectra if it is a single file
     if isinstance(spectrum_files, str):


Test patch:
diff --git a/tests/exporting/test_save_spectra.py b/tests/exporting/test_save_spectra.py
new file mode 100644
index 00000000..c1943495
--- /dev/null
+++ b/tests/exporting/test_save_spectra.py
@@ -0,0 +1,43 @@
+import os
+import tempfile
+import pytest
+from matchms.exporting import save_spectra
+from matchms.importing import load_from_mgf, load_spectra
+
+
+def load_test_spectra_file():
+    module_root = os.path.join(os.path.dirname(__file__), "..")
+    spectrums_file = os.path.join(module_root, "testdata", "testdata.mgf")
+    spectra = list(load_from_mgf(spectrums_file))
+    return spectra
+
+
+@pytest.mark.parametrize("file_name",
+                         ["spectra.msp",
+                          "spectra.mgf",
+                          "spectra.json",
+                          "spectra.pickle"])
+def test_spectra(file_name):
+    """ Utility function to save spectra to msp and load them again.
+
+    Params:
+    -------
+    spectra: Spectra objects to store
+
+    Returns:
+    --------
+    reloaded_spectra: Spectra loaded from saved msp file.
+    """
+    spectrum_list = load_test_spectra_file()
+    with tempfile.TemporaryDirectory() as temp_dir:
+        filename = os.path.join(temp_dir, file_name)
+        save_spectra(spectrum_list, filename)
+        assert os.path.exists(filename)
+        reloaded_spectra = list(load_spectra(filename))
+    assert len(reloaded_spectra) == len(spectrum_list)
+    for i, spectrum in enumerate(spectrum_list):
+        reloaded_spectrum = reloaded_spectra[i]
+        # Num_peaks is sometimes added during saving. So to be able to compare it is set to None
+        spectrum.set("num_peaks", None)
+        reloaded_spectrum.set("num_peaks", None)
+        assert spectrum == reloaded_spectrum
diff --git a/tests/filtering/test_spectrum_processor.py b/tests/filtering/test_spectrum_processor.py
index 7d0b9aa5..a8f3e731 100644
--- a/tests/filtering/test_spectrum_processor.py
+++ b/tests/filtering/test_spectrum_processor.py
@@ -1,8 +1,10 @@
+import os
 import numpy as np
 import pytest
 from matchms import SpectrumProcessor
 from matchms.filtering.filter_order_and_default_pipelines import BASIC_FILTERS
 from matchms.filtering.SpectrumProcessor import ProcessingReport
+from matchms.importing.load_spectra import load_spectra
 from ..builder_Spectrum import SpectrumBuilder
 
 
@@ -128,7 +130,7 @@ def test_unknown_keyword():
 
 def test_filter_spectrums(spectrums):
     processor = SpectrumProcessor("minimal")
-    spectrums = processor.process_spectrums(spectrums)
+    spectrums, _ = processor.process_spectrums(spectrums)
     assert len(spectrums) == 3
     actual_masses = [s.get("precursor_mz") for s in spectrums]
     expected_masses = [100, 102, 104]
@@ -139,7 +141,7 @@ def test_filter_spectrums_report(spectrums):
     processor = SpectrumProcessor("minimal")
     processor.add_filter(filter_function=("require_minimum_number_of_peaks", {"n_required": 2}))
     processor.add_filter(filter_function="add_losses")
-    spectrums, report = processor.process_spectrums(spectrums, create_report=True)
+    spectrums, report = processor.process_spectrums(spectrums)
     assert len(spectrums) == 2
     actual_masses = [s.get("precursor_mz") for s in spectrums]
     expected_masses = [100, 102]
@@ -176,7 +178,7 @@ def test_adding_custom_filter(spectrums):
     processor.add_custom_filter(nonsense_inchikey)
     filters = processor.filters
     assert filters[-1].__name__ == "nonsense_inchikey"
-    spectrums, report = processor.process_spectrums(spectrums, create_report=True)
+    spectrums, report = processor.process_spectrums(spectrums)
     assert report.counter_number_processed == 3
     assert report.counter_changed_metadata == {'make_charge_int': 2, 'interpret_pepmass': 3,
                                                'derive_ionmode': 3, 'nonsense_inchikey': 3}
@@ -193,7 +195,7 @@ def test_adding_custom_filter_with_parameters(spectrums):
     processor.add_custom_filter(nonsense_inchikey_multiple, {"number": 2})
     filters = processor.filters
     assert filters[-1].__name__ == "nonsense_inchikey_multiple"
-    spectrums, report = processor.process_spectrums(spectrums, create_report=True)
+    spectrums, report = processor.process_spectrums(spectrums)
     assert report.counter_number_processed == 3
     assert report.counter_changed_metadata == {'make_charge_int': 2, 'interpret_pepmass': 3,
                                                'derive_ionmode': 3, 'nonsense_inchikey_multiple': 3}
@@ -232,7 +234,7 @@ def test_add_filter_with_custom(spectrums):
     filters = processor.filters
 
     assert filters[-1].__name__ == "nonsense_inchikey_multiple"
-    spectrums, _ = processor.process_spectrums(spectrums, create_report=True)
+    spectrums, _ = processor.process_spectrums(spectrums)
     assert spectrums[0].get("inchikey") == "NONSENSENONSENSE", "Custom filter not executed properly"
 
 
@@ -242,7 +244,7 @@ def test_add_filter_with_matchms_filter(spectrums):
                           {"ion_mode_to_keep": "both"}))
     filters = processor.filters
     assert filters[-1].__name__ == "require_correct_ionmode"
-    spectrums, _ = processor.process_spectrums(spectrums, create_report=True)
+    spectrums, _ = processor.process_spectrums(spectrums)
     assert not spectrums, "Expected to be empty list"
 
 
@@ -261,3 +263,19 @@ def test_add_filter_twice():
     processor.add_filter(("derive_adduct_from_name", {"remove_adduct_from_name": False}))
     processor.add_filter("derive_adduct_from_name")
     assert processor.processing_steps == [("derive_adduct_from_name", {"remove_adduct_from_name": True})]
+
+
+def test_save_spectra_spectrum_processor(spectrums, tmp_path):
+    processor = SpectrumProcessor("default")
+    filename = os.path.join(tmp_path, "spectra.msp")
+
+    _, _ = processor.process_spectrums(spectrums, cleaned_spectra_file=str(filename))
+    assert os.path.exists(filename)
+
+    # Reload spectra and compare lengths
+    reloaded_spectra = list(load_spectra(str(filename)))
+    assert len(reloaded_spectra) == len(spectrums)
+
+    # Check that the processed spectra are stored
+    for spectrum in reloaded_spectra:
+        assert spectrum.get("precursor_mz") is not None
diff --git a/tests/importing/test_load_from_mgf.py b/tests/importing/test_load_from_mgf.py
index 921fedee..1ea22f95 100644
--- a/tests/importing/test_load_from_mgf.py
+++ b/tests/importing/test_load_from_mgf.py
@@ -11,14 +11,3 @@ def test_load_from_mgf_using_filepath():
 
     assert len(spectra) > 0
     assert isinstance(spectra[0], Spectrum)
-
-
-def test_load_from_mgf_using_file():
-    module_root = os.path.join(os.path.dirname(__file__), "..")
-    spectra_filepath = os.path.join(module_root, "testdata", "pesticides.mgf")
-
-    with open(spectra_filepath, "r", encoding="utf-8") as spectra_file:
-        spectra = list(load_from_mgf(spectra_file))
-
-        assert len(spectra) > 0
-        assert isinstance(spectra[0], Spectrum)
diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py
index b5a1fae0..3bb66167 100644
--- a/tests/test_pipeline.py
+++ b/tests/test_pipeline.py
@@ -3,6 +3,7 @@ import numpy as np
 import pytest
 from matchms import Pipeline
 from matchms.filtering import select_by_mz
+from matchms.importing.load_spectra import load_spectra
 from matchms.Pipeline import create_workflow
 from matchms.similarity import ModifiedCosine
 from matchms.yaml_file_functions import load_workflow_from_yaml_file
@@ -216,3 +217,16 @@ def test_pipeline_changing_workflow():
     expected = np.array([[1., 0.30384404],
                          [0.30384404, 1.]])
     assert np.allclose(all_scores["ModifiedCosine_score"][3:, 3:], expected)
+
+
+def test_save_spectra_spectrum_processor(tmp_path):
+    workflow = create_workflow(predefined_processing_queries="basic")
+    pipeline = Pipeline(workflow)
+    filename = os.path.join(tmp_path, "spectra.mgf")
+
+    pipeline.run(spectrums_file_msp, cleaned_query_file=str(filename))
+    assert os.path.exists(filename)
+
+    # Reload spectra and compare lengths
+    reloaded_spectra = list(load_spectra(str(filename)))
+    assert len(reloaded_spectra) == len(list(load_spectra(spectrums_file_msp)))