Repository: matchms/matchms (python)

Issue:
Pipeline should be able to take in custum filters
SpectrumProcessor is able to handle custom filters, however, pipeline is not. 
This is the case because create_workflow converts the filters to string representations. This works for the matchms filters, but does not work for the custom filters. 

Pull request description:
Return processing report
To fix #626 
Create workflow did not allow for taking in custom filters, since it would convert it to string (instead of just passing the callable).  For matchms filters this doesn't give any issues, since they would be converted back again correctly. 

Patch:
diff --git a/matchms/Pipeline.py b/matchms/Pipeline.py
index 21724810..ef6d6553 100644
--- a/matchms/Pipeline.py
+++ b/matchms/Pipeline.py
@@ -2,11 +2,12 @@ import logging
 import os
 from collections import OrderedDict
 from datetime import datetime
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
+from typing import Callable, Iterable, List, Optional, Union
 import matchms.similarity as mssimilarity
 from matchms import calculate_scores
 from matchms.filtering.filter_order import ALL_FILTERS
-from matchms.filtering.SpectrumProcessor import SpectrumProcessor
+from matchms.filtering.SpectrumProcessor import (FunctionWithParametersType,
+                                                 SpectrumProcessor)
 from matchms.importing.load_spectra import load_list_of_spectrum_files
 from matchms.logging_functions import (add_logging_to_file,
                                        reset_matchms_logger,
@@ -22,8 +23,8 @@ logger = logging.getLogger("matchms")
 
 
 def create_workflow(yaml_file_name: Optional[str] = None,
-                    query_filters: Iterable[Union[str, Tuple[str, Dict[str, Any]]]] = (),
-                    reference_filters: Iterable[Union[str, Tuple[str, Dict[str, Any]]]] = (),
+                    query_filters: Iterable[Union[str, Callable, FunctionWithParametersType]] = (),
+                    reference_filters: Iterable[Union[str, Callable, FunctionWithParametersType]] = (),
                     score_computations: Iterable[Union[str, List[dict]]] = (),
                     ) -> OrderedDict:
     """Creates a workflow that specifies the filters and scores needed to be run by Pipeline
@@ -56,6 +57,8 @@ def create_workflow(yaml_file_name: Optional[str] = None,
             file.write("# Change and adapt fields where necessary \n")
             file.write("# " + 20 * "=" + " \n")
             ordered_dump(workflow, file)
+    workflow["query_filters"] = queries_processor.filters
+    workflow["reference_filters"] = reference_processor.filters
     return workflow
 
 
@@ -122,6 +125,7 @@ class Pipeline:
                                                [Spec2Vec, {"model": "my_spec2vec_model.model"}],
                                        ["filter_by_range", {"name": "Spec2Vec", "low": 0.3}]])
     """
+
     def __init__(self,
                  workflow: OrderedDict,
                  progress_bar=True,
@@ -176,7 +180,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, cleaned_query_file=None, cleaned_reference_file=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.
@@ -243,6 +247,7 @@ class Pipeline:
     def _apply_similarity_measure(self, computation, i):
         """Run score computations for all listed methods and on all loaded and processed spectra.
         """
+
         def get_similarity_measure(computation):
             if isinstance(computation[0], str):
                 if len(computation) > 1:
@@ -253,6 +258,7 @@ class Pipeline:
                     return computation[0](**computation[1])
                 return computation[0]()
             raise TypeError("Unknown similarity measure.")
+
         similarity_measure = get_similarity_measure(computation)
         # If this is the first score computation:
         if i == 0:
diff --git a/matchms/filtering/__init__.py b/matchms/filtering/__init__.py
index ef5811a4..f74c46dd 100644
--- a/matchms/filtering/__init__.py
+++ b/matchms/filtering/__init__.py
@@ -116,12 +116,14 @@ from matchms.filtering.metadata_processing.repair_parent_mass_is_molar_mass impo
     repair_parent_mass_is_molar_mass
 from matchms.filtering.metadata_processing.repair_parent_mass_match_smiles_wrapper import \
     repair_parent_mass_match_smiles_wrapper
-from matchms.filtering.metadata_processing.repair_smiles_of_salts import repair_smiles_of_salts
-from matchms.filtering.metadata_processing.require_compound_name import require_compound_name
+from matchms.filtering.metadata_processing.repair_smiles_of_salts import \
+    repair_smiles_of_salts
+from matchms.filtering.metadata_processing.require_compound_name import \
+    require_compound_name
 from matchms.filtering.metadata_processing.require_correct_ionmode import \
     require_correct_ionmode
-from matchms.filtering.metadata_processing.require_formula import require_formula
-from matchms.filtering.metadata_processing.require_parent_mass_match_smiles import \
+from matchms.filtering.metadata_processing.require_formula import \
+    require_formula
 from matchms.filtering.metadata_processing.require_parent_mass_match_smiles import \
     require_parent_mass_match_smiles
 from matchms.filtering.metadata_processing.require_precursor_below_mz import \
@@ -153,9 +155,6 @@ from matchms.filtering.peak_processing.select_by_mz import select_by_mz
 from matchms.filtering.peak_processing.select_by_relative_intensity import \
     select_by_relative_intensity
 from matchms.filtering.SpeciesString import SpeciesString
-from .metadata_processing.repair_smiles_of_salts import repair_smiles_of_salts
-from .metadata_processing.require_correct_ionmode import \
-    require_correct_ionmode
 
 
 __all__ = [


Test patch:
diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py
index 92603d6b..b5e16c9e 100644
--- a/tests/test_pipeline.py
+++ b/tests/test_pipeline.py
@@ -218,3 +218,36 @@ def test_save_spectra_spectrum_processor(tmp_path):
     # Reload spectra and compare lengths
     reloaded_spectra = list(load_spectra(str(filename)))
     assert len(reloaded_spectra) == len(list(load_spectra(spectrums_file_msp)))
+
+
+def test_add_custom_filter():
+    def select_spectra_containing_fragment(spectrum_in, fragment_of_interest=103.05, tolerance=0.01):
+        for fragment_mz in spectrum_in.peaks.mz:
+            # Check if the fragment is close to the fragment_of_interest
+            if fragment_of_interest - tolerance < fragment_mz < fragment_of_interest + tolerance:
+                return spectrum_in
+        return None
+    workflow = create_workflow(
+        query_filters=[],)
+    pipeline = Pipeline(workflow)
+    pipeline.processing_queries.parse_and_add_filter((select_spectra_containing_fragment,
+                                                      {"fragment_of_interest": 103.05, "tolerance": 0.01}))
+    pipeline.run(spectrums_file_msp)
+    cleaned_spectra = pipeline.spectrums_queries
+    assert len(cleaned_spectra) == 0
+
+
+def test_add_custom_filter_to_query_filters():
+    def select_spectra_containing_fragment(spectrum_in, fragment_of_interest=103.05, tolerance=0.01):
+        for fragment_mz in spectrum_in.peaks.mz:
+            # Check if the fragment is close to the fragment_of_interest
+            if fragment_of_interest - tolerance < fragment_mz < fragment_of_interest + tolerance:
+                return spectrum_in
+        return None
+    workflow = create_workflow(
+        query_filters=[(select_spectra_containing_fragment,
+                                                      {"fragment_of_interest": 103.05, "tolerance": 0.01})],)
+    pipeline = Pipeline(workflow)
+    pipeline.run(spectrums_file_msp)
+    cleaned_spectra = pipeline.spectrums_queries
+    assert len(cleaned_spectra) == 0