Repository: matchms/matchms

Issue:
Add default filters to yaml file
Any default settings of functions are not shown in the yaml file. This is bad for reproducibility, since changing any default settings in a filter function will currently result in a different pipeline running. 

Suggested change:
Automatically get the default filter settings from each filter and returning them in properties. (this will be used to create yaml files and printing the log file)

Comments:

Patch:
diff --git a/matchms/Pipeline.py b/matchms/Pipeline.py
index baf579bb..32214b4e 100644
--- a/matchms/Pipeline.py
+++ b/matchms/Pipeline.py
@@ -214,24 +214,25 @@ class Pipeline:
         self.__workflow = workflow
         self.check_workflow()
 
-        self.write_to_logfile("--- Processing pipeline: ---")
         self._initialize_spectrum_processor_queries()
         if self.is_symmetric is False:
             self._initialize_spectrum_processor_references()
 
     def _initialize_spectrum_processor_queries(self):
         """Initialize spectrum processing workflow for the query spectra."""
+        self.write_to_logfile("--- Processing pipeline query spectra: ---")
         self.processing_queries = initialize_spectrum_processor(
             None,
             self.__workflow["query_filters"]
             )
-
         self.write_to_logfile(str(self.processing_queries))
         if self.processing_queries.processing_steps != self.__workflow["query_filters"]:
             logger.warning("The order of the filters has been changed compared to the Yaml file.")
 
     def _initialize_spectrum_processor_references(self):
         """Initialize spectrum processing workflow for the reference spectra."""
+        self.write_to_logfile("--- Processing pipeline reference spectra: ---")
+
         self.processing_references = initialize_spectrum_processor(
             None,
             self.__workflow["reference_filters"]
diff --git a/matchms/Scores.py b/matchms/Scores.py
index c35bafb8..274983f9 100644
--- a/matchms/Scores.py
+++ b/matchms/Scores.py
@@ -111,6 +111,9 @@ class Scores:
         self._index = 0
         raise StopIteration
 
+    def __repr__(self):
+        return self._scores.__repr__()
+
     def __str__(self):
         return self._scores.__str__()
 
diff --git a/matchms/Spectrum.py b/matchms/Spectrum.py
index cdbb3230..a125d4c7 100644
--- a/matchms/Spectrum.py
+++ b/matchms/Spectrum.py
@@ -27,9 +27,11 @@ class Spectrum:
 
         spectrum = Spectrum(mz=np.array([100, 150, 200.]),
                             intensities=np.array([0.7, 0.2, 0.1]),
-                            metadata={'id': 'spectrum1',
+                            metadata={"id": 'spectrum1',
+                                      "precursor_mz": 222.333,
                                       "peak_comments": {200.: "the peak at 200 m/z"}})
 
+        print(spectrum)
         print(spectrum.peaks.mz[0])
         print(spectrum.peaks.intensities[0])
         print(spectrum.get('id'))
@@ -39,6 +41,7 @@ class Spectrum:
 
     .. testoutput::
 
+        Spectrum(precursor m/z=222.33, 3 fragments between 100.0 and 200.0)
         100.0
         0.7
         spectrum1
@@ -101,6 +104,16 @@ class Spectrum:
         combined_hash = self.metadata_hash() + self.spectrum_hash()
         return int.from_bytes(bytearray(combined_hash, 'utf-8'), 'big')
 
+    def __repr__(self):
+        num_peaks = len(self.peaks)
+        min_mz = min(self.peaks.mz)
+        max_mz = max(self.peaks.mz)
+        precursor_mz = self.get("precursor_mz")
+        return f"Spectrum(precursor m/z={precursor_mz:.2f}, {num_peaks} fragments between {min_mz:.1f} and {max_mz:.1f})"
+
+    def __str__(self):
+        return self.__repr__()
+
     def spectrum_hash(self):
         """Return a (truncated) sha256-based hash which is generated
         based on the spectrum peaks (mz:intensity pairs).
diff --git a/matchms/SpectrumProcessor.py b/matchms/SpectrumProcessor.py
index 51964296..090c3f4b 100644
--- a/matchms/SpectrumProcessor.py
+++ b/matchms/SpectrumProcessor.py
@@ -1,6 +1,7 @@
+import inspect
 from collections import defaultdict
 from functools import partial
-from typing import Optional, Tuple, Union
+from typing import Dict, Optional, Tuple, Union
 import numpy as np
 import pandas as pd
 from tqdm import tqdm
@@ -30,10 +31,10 @@ class SpectrumProcessor:
                 raise ValueError("Predefined pipeline parameter should be a string")
             if predefined_pipeline not in PREDEFINED_PIPELINES:
                 raise ValueError(f"Unknown processing pipeline '{predefined_pipeline}'. Available pipelines: {list(PREDEFINED_PIPELINES.keys())}")
-            for fname in PREDEFINED_PIPELINES[predefined_pipeline]:
-                self.add_matchms_filter(fname)
+            for filter_name in PREDEFINED_PIPELINES[predefined_pipeline]:
+                self.add_matchms_filter(filter_name)
 
-    def add_filter(self, filter_function: Union[Tuple[str], str]):
+    def add_filter(self, filter_function: Union[Tuple[str, Dict[str, any]], str]):
         """Add a filter to the processing pipeline. Takes both matchms filter names (and parameters)
         as well as custom-made functions.
         """
@@ -44,7 +45,7 @@ class SpectrumProcessor:
         else:
             self.add_custom_filter(filter_function[0], filter_function[1])
 
-    def add_matchms_filter(self, filter_spec: Union[Tuple[str], str]):
+    def add_matchms_filter(self, filter_spec: Union[Tuple[str, Dict[str, any]], str]):
         """
         Add a filter to the processing pipeline.
 
@@ -66,7 +67,7 @@ class SpectrumProcessor:
             filter_func.__name__ = FILTER_FUNCTIONS[filter_name].__name__
         else:
             raise TypeError("filter_spec should be a string or a tuple or list")
-
+        check_all_parameters_given(filter_func)
         self.filters.append(filter_func)
         # Sort filters according to their order in self.filter_order
         self.filters.sort(key=lambda f: self.filter_order.index(f.__name__))
@@ -93,12 +94,12 @@ class SpectrumProcessor:
             if filter_func.__name__ in self.filter_order:
                 filter_position = self.filter_order.index(filter_func.__name__)
         self.filter_order.insert(filter_position + 1, filter_function.__name__)
-        if filter_params is None:
-            self.filters.append(filter_function)
-        else:
-            filter_func = partial(filter_function, **filter_params)
-            filter_func.__name__ = filter_function.__name__
-            self.filters.append(filter_func)
+        if filter_params is not None:
+            partial_filter_func = partial(filter_function, **filter_params)
+            partial_filter_func.__name__ = filter_function.__name__
+            filter_function = partial_filter_func
+        check_all_parameters_given(filter_function)
+        self.filters.append(filter_function)
 
     def process_spectrum(self, spectrum,
                          processing_report: Optional["ProcessingReport"] = None):
@@ -171,11 +172,54 @@ class SpectrumProcessor:
 
     @property
     def processing_steps(self):
-        return [x.__name__ for x in self.filters]
+        filter_list = []
+        for filter_step in self.filters:
+            parameter_settings = get_parameter_settings(filter_step)
+            if parameter_settings is not None:
+                filter_list.append((filter_step.__name__, parameter_settings))
+            else:
+                filter_list.append(filter_step.__name__)
+        return filter_list
 
     def __str__(self):
-        summary_string = "SpectrumProcessor\nProcessing steps:\n - "
-        return summary_string + "\n - ".join(self.processing_steps)
+        summary_string = "SpectrumProcessor\nProcessing steps:"
+        for processing_step in self.processing_steps:
+            if isinstance(processing_step, str):
+                summary_string += "\n- " + processing_step
+            elif isinstance(processing_step, tuple):
+                filter_name = processing_step[0]
+                summary_string += "\n- - " + filter_name
+                filter_params = processing_step[1]
+                for filter_param in filter_params:
+                    summary_string += "\n  - " + str(filter_param)
+        return summary_string
+
+
+def check_all_parameters_given(func):
+    """Asserts that all added parameters for a function are given (except spectrum_in)"""
+    signature = inspect.signature(func)
+    parameters_without_value = []
+    for parameter, value in signature.parameters.items():
+        if value.default is inspect.Parameter.empty:
+            parameters_without_value.append(parameter)
+    assert len(parameters_without_value) == 1, \
+        f"More than one parameter of the function {func.__name__} is not specified, " \
+        f"the parameters not specified are {parameters_without_value}"
+
+
+def get_parameter_settings(func):
+    """Returns all parameters and parameter values for a function
+
+    This includes default parameter settings and, but also the settings stored in partial"""
+    signature = inspect.signature(func)
+    parameter_settings = {
+            parameter: value.default
+            for parameter, value in signature.parameters.items()
+            if value.default is not inspect.Parameter.empty
+        }
+    if parameter_settings == {}:
+        return None
+    return parameter_settings
 
 
 # List all filters in a functionally working order
@@ -246,7 +290,6 @@ DEFAULT_FILTERS = BASIC_FILTERS \
        "harmonize_undefined_inchi",
        "harmonize_undefined_smiles",
        "repair_inchi_inchikey_smiles",
-       "repair_parent_mass_match_smiles_wrapper",
        "normalize_intensities",
     ]
 FULLY_ANNOTATED_PROCESSING = DEFAULT_FILTERS \
diff --git a/pyproject.toml b/pyproject.toml
index 07a8d7fc..3c9469de 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,7 +42,6 @@ rdkit = "^2023.3.2"
 pyyaml = "^6.0.1"
 deprecated = "^1.2.14"
 
-
 [tool.poetry.group.dev.dependencies]
 decorator = "^5.1.1"
 isort = "^5.12.0"
@@ -59,7 +58,7 @@ poetry-bumpversion = "^0.3.1"
 [tool.poetry.group.docs.dependencies]
 sphinxcontrib-apidoc = "^0.3.0"
 sphinx-rtd-theme = "^1.2.2"
-sphinx = "<7"
+sphinx = "^7"
 
 
 [build-system]
diff --git a/readthedocs/conf.py b/readthedocs/conf.py
index fcf34b61..46c6e91d 100644
--- a/readthedocs/conf.py
+++ b/readthedocs/conf.py
@@ -23,8 +23,8 @@ import matchms
 # -- Project information -----------------------------------------------------
 
 project = "matchms"
-copyright = "2020, Netherlands eScience Center"
-author = "Netherlands eScience Center"
+copyright = "2023, Düsseldorf University of Applied Sciences & Netherlands eScience Center"
+author = "matchms development team"
 
 
 # -- General configuration ---------------------------------------------------


Test patch:
diff --git a/tests/filtering/test_spectrum_processor.py b/tests/filtering/test_spectrum_processor.py
index 0cad682c..a34253e4 100644
--- a/tests/filtering/test_spectrum_processor.py
+++ b/tests/filtering/test_spectrum_processor.py
@@ -24,35 +24,53 @@ def spectrums():
 
 def test_filter_sorting_and_output():
     processing = SpectrumProcessor("default")
-    expected_filters = [
-        'make_charge_int',
-        'add_compound_name',
-        'derive_adduct_from_name',
-        'derive_formula_from_name',
-        'clean_compound_name',
-        'interpret_pepmass',
-        'add_precursor_mz',
-        'derive_ionmode',
-        'correct_charge',
-        'require_precursor_mz',
-        'add_parent_mass',
-        'harmonize_undefined_inchikey',
-        'harmonize_undefined_inchi',
-        'harmonize_undefined_smiles',
-        'repair_inchi_inchikey_smiles',
-        'repair_parent_mass_match_smiles_wrapper',
-        'normalize_intensities'
-        ]
-    actual_filters = [x.__name__ for x in processing.filters]
-    assert actual_filters == expected_filters
-    # 2nd way to access the filter names via processing_steps attribute:
+    expected_filters = ['make_charge_int',
+                        'add_compound_name',
+                        ('derive_adduct_from_name', {'remove_adduct_from_name': True}),
+                        ('derive_formula_from_name', {'remove_formula_from_name': True}),
+                        'clean_compound_name',
+                        'interpret_pepmass',
+                        'add_precursor_mz',
+                        'derive_ionmode',
+                        'correct_charge',
+                        ('require_precursor_mz', {'minimum_accepted_mz': 10.0}),
+                        ('add_parent_mass',
+                         {'estimate_from_adduct': True, 'overwrite_existing_entry': False}),
+                        ('harmonize_undefined_inchikey', {'aliases': None, 'undefined': ''}),
+                        ('harmonize_undefined_inchi', {'aliases': None, 'undefined': ''}),
+                        ('harmonize_undefined_smiles', {'aliases': None, 'undefined': ''}),
+                        'repair_inchi_inchikey_smiles',
+                        'normalize_intensities']
     assert processing.processing_steps == expected_filters
 
 
+@pytest.mark.parametrize("filter_step, expected", [
+    [("add_parent_mass", {'estimate_from_adduct': False}),
+     ('add_parent_mass', {'estimate_from_adduct': False, 'overwrite_existing_entry': False})],
+    ["derive_adduct_from_name",
+     ('derive_adduct_from_name', {'remove_adduct_from_name': True})],
+    [("require_correct_ionmode", {"ion_mode_to_keep": "both"}),
+     ("require_correct_ionmode", {"ion_mode_to_keep": "both"})],
+])
+def test_overwrite_default_settings(filter_step, expected):
+    """Test if both default settings and set settings are returned in processing steps"""
+    processor = SpectrumProcessor(None)
+    processor.add_filter(filter_step)
+    expected_filters = [expected]
+    assert processor.processing_steps == expected_filters
+
+
+def test_incomplete_parameters():
+    """Test if an error is raised when running an incomplete command"""
+    with pytest.raises(AssertionError):
+        processor = SpectrumProcessor(None)
+        processor.add_filter("require_correct_ionmode")
+
+
 def test_string_output():
     processing = SpectrumProcessor("minimal")
-    expected_str = "SpectrumProcessor\nProcessing steps:\n - make_charge_int\n - interpret_pepmass"\
-        "\n - derive_ionmode\n - correct_charge"
+    expected_str = "SpectrumProcessor\nProcessing steps:\n- make_charge_int\n- interpret_pepmass" \
+                   "\n- derive_ionmode\n- correct_charge"
     assert str(processing) == expected_str
 
 
@@ -65,7 +83,7 @@ def test_add_matchms_filter(metadata, expected):
     spectrum_in = SpectrumBuilder().with_metadata(metadata).build()
     processor = SpectrumProcessor("minimal")
     processor.add_matchms_filter(("require_correct_ionmode",
-                                 {"ion_mode_to_keep": "both"}))
+                                  {"ion_mode_to_keep": "both"}))
     spectrum = processor.process_spectrum(spectrum_in)
     if expected is None:
         assert spectrum is None
@@ -175,7 +193,7 @@ def test_add_filter_with_custom(spectrums):
 def test_add_filter_with_matchms_filter(spectrums):
     processor = SpectrumProcessor("minimal")
     processor.add_filter(("require_correct_ionmode",
-                         {"ion_mode_to_keep": "both"}))
+                          {"ion_mode_to_keep": "both"}))
     filters = processor.filters
     assert filters[-1].__name__ == "require_correct_ionmode"
     spectrums, _ = processor.process_spectrums(spectrums, create_report=True)
@@ -187,6 +205,7 @@ def test_all_filters_is_complete():
 
     This is important, since performing tests in the wrong order can make some filters useless.
     """
+
     def get_functions_from_file(file_path):
         """Gets all python functions in a file"""
         with open(file_path, 'r', encoding="utf-8") as file: