Repository: matchms/matchms
Pull request #509 (2023-08-25 14:03:15)

Add pubchem lookup
Previously in #427 this was implemented in a style that did not match the matchms style, so it better fitted in matchmsextras. 
However, when working with Pipeline, I noticed this was not user friendly. 
The problem is that the pubchem annotation needs to be done, after the first metadata cleaning, but before the final require filters. This would mean switching from Pipeline, to matchmsextras to a final Pipeline, which is unnecesary complex. 

The previous version required a file that already contained compound annotations. Now this is optional, it can be run with and without csv file. If you run it without csv file it will cache previously looked up compound_names reducing the number of times pubchem lookup has to be performed (since this is an expensive step). If a csv file is supplied, lookup will still be performed if the csv file does not contain the compound name and this will be added to the csv file, which will make future pipeline cleaning runs faster, since this csv file can be used as starting point. 

This means pubchempy has to be added as a dependency. However, I don't think this is a limitation, since it only depends on pandas, which is already a requirement of matchms. 

I tried specifying version of pubhempy to 1.0.4 in the poetry file, but I did not yet get this to work. Does one of you know how to get this to work? 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9fbf9ffb..dbce4de4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 
 - Logger warning for rdkit molecule conversion [#507](https://github.com/matchms/matchms/pull/507)
+- Repair_smiles_from_compound_name, now works without matchmsextras [#509](https://github.com/matchms/matchms/pull/509/)
+  - pubchempy was added as dependency
 
 ### Fixed
 
diff --git a/matchms/filtering/__init__.py b/matchms/filtering/__init__.py
index c7a459f3..66dd856f 100644
--- a/matchms/filtering/__init__.py
+++ b/matchms/filtering/__init__.py
@@ -93,6 +93,8 @@ from .metadata_processing.derive_inchikey_from_inchi import \
 from .metadata_processing.derive_ionmode import derive_ionmode
 from .metadata_processing.derive_smiles_from_inchi import \
     derive_smiles_from_inchi
+from .metadata_processing.derive_smiles_from_pubchem_compound_name_search import \
+    derive_smiles_from_pubchem_compound_name_search
 from .metadata_processing.harmonize_undefined_inchi import \
     harmonize_undefined_inchi
 from .metadata_processing.harmonize_undefined_inchikey import \
@@ -113,8 +115,6 @@ from .metadata_processing.repair_parent_mass_match_smiles_wrapper import \
     repair_parent_mass_match_smiles_wrapper
 from .metadata_processing.repair_precursor_is_parent_mass import \
     repair_precursor_is_parent_mass
-from .metadata_processing.repair_smiles_from_compound_name import \
-    repair_smiles_from_compound_name
 from .metadata_processing.repair_smiles_of_salts import repair_smiles_of_salts
 from .metadata_processing.require_correct_ionmode import \
     require_correct_ionmode
@@ -178,7 +178,7 @@ __all__ = [
     "repair_parent_mass_match_smiles_wrapper",
     "repair_precursor_is_parent_mass",
     "repair_smiles_of_salts",
-    "repair_smiles_from_compound_name",
+    "derive_smiles_from_pubchem_compound_name_search",
     "repair_not_matching_annotation",
     "require_valid_annotation",
     "require_parent_mass_match_smiles",
diff --git a/matchms/filtering/filter_order_and_default_pipelines.py b/matchms/filtering/filter_order_and_default_pipelines.py
index 361f7850..175f018e 100644
--- a/matchms/filtering/filter_order_and_default_pipelines.py
+++ b/matchms/filtering/filter_order_and_default_pipelines.py
@@ -16,16 +16,16 @@ ALL_FILTERS = [msfilters.make_charge_int,
                # msfilters.derive_adduct_from_name,  # run again? Or improve those filters?
                # msfilters.derive_formula_from_name,  # run again? Or improve those filters?
                msfilters.require_precursor_mz,
-               msfilters.add_parent_mass,
                msfilters.harmonize_undefined_inchikey,
                msfilters.harmonize_undefined_inchi,
                msfilters.harmonize_undefined_smiles,
                msfilters.repair_inchi_inchikey_smiles,
                msfilters.derive_smiles_from_inchi,
-               msfilters.repair_smiles_from_compound_name,
                msfilters.derive_inchi_from_smiles,
                msfilters.derive_inchikey_from_inchi,
                msfilters.clean_adduct,
+               msfilters.add_parent_mass,
+               msfilters.derive_smiles_from_pubchem_compound_name_search,
                msfilters.repair_smiles_of_salts,
                msfilters.repair_precursor_is_parent_mass,
                msfilters.repair_parent_mass_is_mol_wt,
diff --git a/matchms/filtering/metadata_processing/derive_smiles_from_pubchem_compound_name_search.py b/matchms/filtering/metadata_processing/derive_smiles_from_pubchem_compound_name_search.py
new file mode 100644
index 00000000..2ba3bb7b
--- /dev/null
+++ b/matchms/filtering/metadata_processing/derive_smiles_from_pubchem_compound_name_search.py
@@ -0,0 +1,143 @@
+import csv
+import logging
+import os
+from functools import lru_cache
+from typing import List, Optional
+import numpy as np
+import pandas as pd
+import pubchempy
+from matchms import Spectrum
+from matchms.filtering.filter_utils.smile_inchi_inchikey_conversions import \
+    _check_fully_annotated
+
+
+logger = logging.getLogger("matchms")
+
+
+def derive_smiles_from_pubchem_compound_name_search(spectrum_in: Spectrum,
+                                                    annotated_compound_names_file: Optional[str] = None,
+                                                    mass_tolerance: float = 0.1):
+    """Adds smiles, inchi, inchikey based on compound name by searching pubchem
+
+    The smiles, inchi and inchikey are repaired if the found smiles is close enough to the parent mass.
+
+    Parameters
+    ----------
+    spectrum_in:
+        The input spectrum.
+    annotated_compound_names_file: Optional[str]
+        Any compound name that was searched for on pubchem will be added to this file. If a compound name is already
+        in this file it will be used instead of looking up at pubchem. This file can be reused for future runs, speeding
+        up the process.
+        If None. The compound names found will still be cached for this run, but won't be reusable for future runs.
+        The csv file should contain the columns ["compound_name", "smiles", "inchi", "inchikey", "monoisotopic_mass"]
+    mass_tolerance:
+        Acceptable mass difference between query compound and pubchem result.
+    """
+    if spectrum_in is None:
+        return None
+    spectrum = spectrum_in.clone()
+    if _check_fully_annotated(spectrum):
+        return spectrum
+    compound_name = spectrum.get("compound_name")
+    parent_mass = spectrum.get('parent_mass')
+
+    if _is_plausible_name(compound_name) and parent_mass is not None:
+
+        compound_name_annotations = _get_pubchem_compound_name_annotation(compound_name, annotated_compound_names_file)
+        if len(compound_name_annotations) > 0:
+            compound_name_annotation_df = pd.DataFrame(compound_name_annotations)
+            mass_differences = np.abs(compound_name_annotation_df["monoisotopic_mass"] - parent_mass)
+            within_mass_tolerance = compound_name_annotation_df[mass_differences < mass_tolerance]
+            if within_mass_tolerance.shape[0] > 0:
+                # Select the match with the most
+                best_match = within_mass_tolerance.loc[within_mass_tolerance["monoisotopic_mass"].idxmin()]
+                spectrum.set("smiles", best_match["smiles"])
+                spectrum.set("inchi", best_match["inchi"])
+                spectrum.set("inchikey", best_match["inchikey"])
+                logger.info("Added smiles %s based on the compound name %s", best_match["smiles"], compound_name)
+                return spectrum
+    return spectrum
+
+
+@lru_cache(maxsize=None)
+def _get_pubchem_compound_name_annotation(compound_name, csv_file=None) -> List[dict]:
+    """Loads compound name annotation from file or gets it from pubchem any new annotation is added to the file
+
+    functools.cache, makes sure that previously loaded or calculated compound names do not have to be reloaded.
+    This reduces the number of time the csv file has to be loaded from memory.
+    """
+    if csv_file is None:
+        return _pubchem_name_search(compound_name)
+
+    # Search in previously annotated compound names
+    annotated_compound_names = _load_compound_name_annotations(csv_file, compound_name)
+    if not annotated_compound_names:
+        annotated_compound_names = _pubchem_name_search(compound_name)
+        if not annotated_compound_names:
+            _write_compound_name_annotations(csv_file, [{"compound_name": compound_name, "smiles": None, "inchi": None,
+                                                         "inchikey": None,  "monoisotopic_mass": None}])
+        _write_compound_name_annotations(csv_file, annotated_compound_names)
+    return annotated_compound_names
+
+
+def _pubchem_name_search(compound_name: str, name_search_depth=10,) -> List[dict]:
+    """Search pubmed for compound name"""
+    try:
+        results_pubchem = pubchempy.get_compounds(compound_name,
+                                                  'name',
+                                                  listkey_count=name_search_depth)
+        if len(results_pubchem) == 0 and "_" in compound_name:
+            results_pubchem = pubchempy.get_compounds(compound_name.replace("_", " "),
+                                                      'name',
+                                                      listkey_count=name_search_depth)
+    except (pubchempy.ServerError, ConnectionError, ConnectionAbortedError, pubchempy.PubChemHTTPError):
+        print("Connection error, trying again")
+        return _pubchem_name_search(compound_name, name_search_depth=name_search_depth)
+
+    extracted_results = []
+    # extract the needed information:
+    for result in results_pubchem:
+        smiles_pubchem = result.isomeric_smiles
+        if smiles_pubchem is None:
+            smiles_pubchem = result.canonical_smiles
+        extracted_results.append({"compound_name": compound_name,
+                                  "smiles": smiles_pubchem,
+                                  "inchi": result.inchi,
+                                  "inchikey": result.inchikey,
+                                  "monoisotopic_mass": float(result.monoisotopic_mass),})
+    return extracted_results
+
+
+def _load_compound_name_annotations(annotated_compound_names_csv, compound_name: str):
+    """Loads in the annotated compound names and checks format"""
+    if not os.path.exists(annotated_compound_names_csv):
+        return []
+    annotated_compound_names = pd.read_csv(annotated_compound_names_csv)
+    assert list(annotated_compound_names.columns) == ["compound_name", "smiles", "inchi",
+                                                      "inchikey", "monoisotopic_mass"], \
+        "The annotated_compound_names_csv file does not have the columns " \
+        "compound_name, smiles, inchi, inchikey, monoisotopic_mass"
+
+    matches = annotated_compound_names[annotated_compound_names["compound_name"] == compound_name]
+    matches.replace(np.nan, None, inplace=True)
+    return matches.to_dict("records")
+
+
+def _write_compound_name_annotations(annotated_compound_names_csv, compound_name_annotations: List[dict]):
+    if not os.path.exists(annotated_compound_names_csv):
+        with open(annotated_compound_names_csv, "w", encoding="utf8") as f:
+            f.write("compound_name,smiles,inchi,inchikey,monoisotopic_mass\n")
+    with open(annotated_compound_names_csv, "a", encoding="utf-8") as csv_file:
+        writer = csv.writer(csv_file)
+        for compound_name_annotation in compound_name_annotations:
+            writer.writerow([compound_name_annotation["compound_name"],
+                             compound_name_annotation["smiles"],
+                             compound_name_annotation["inchi"],
+                             compound_name_annotation["inchikey"],
+                             compound_name_annotation["monoisotopic_mass"],])
+
+
+def _is_plausible_name(compound_name):
+    """Simple check if it can be a compound name."""
+    return isinstance(compound_name, str) and len(compound_name) > 4
diff --git a/matchms/filtering/metadata_processing/repair_smiles_from_compound_name.py b/matchms/filtering/metadata_processing/repair_smiles_from_compound_name.py
deleted file mode 100644
index 574fab3b..00000000
--- a/matchms/filtering/metadata_processing/repair_smiles_from_compound_name.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import logging
-import numpy as np
-import pandas as pd
-from matchms import Spectrum
-from matchms.filtering.filter_utils.smile_inchi_inchikey_conversions import \
-    _check_fully_annotated
-
-
-logger = logging.getLogger("matchms")
-
-
-def repair_smiles_from_compound_name(spectrum_in: Spectrum,
-                                     annotated_compound_names_file,
-                                     mass_tolerance=0.1):
-    """Adds annotations (smiles, inchi, inchikey) based on compound name
-
-    Based on a table of compound names and smiles matches (stored in a csv file) this function
-    adds the new annotations to the input spectrums if the smiles seem consistent with the available
-    spectrum metadata (e.g., parent mass).
-    This function can be used with csv files that are returned by the pubchem_lookup.py
-    from matchmextras.
-
-    Parameters
-    ----------
-    spectrum_in:
-        The input spectrum.
-    annotated_compound_names_file: str
-        A csv file containing the compound names and matching smiles, inchi, inchikey
-        and monoisotopic_mass. This can be created using the the pubchem_lookup.py from matchmextras.
-    mass_tolerance.
-        Acceptable mass difference between query compound and pubchem result.
-    """
-    annotated_compound_names = _load_compound_name_annotations(annotated_compound_names_file)
-    if spectrum_in is None:
-        return None
-    spectrum = spectrum_in.clone()
-    if _check_fully_annotated(spectrum):
-        return spectrum
-    compound_name = spectrum.get("compound_name")
-    parent_mass = spectrum.get('parent_mass')
-
-    if _is_plausible_name(compound_name) and parent_mass is not None:
-        matching_compound_name = annotated_compound_names[annotated_compound_names["compound_name"] == compound_name]
-        mass_differences = np.abs(matching_compound_name["monoisotopic_mass"]-parent_mass)
-        within_mass_tolerance = matching_compound_name[mass_differences < mass_tolerance]
-        if within_mass_tolerance.shape[0] > 0:
-            # Select the match with the most
-            best_match = within_mass_tolerance.loc[within_mass_tolerance["monoisotopic_mass"].idxmin()]
-            spectrum.set("smiles", best_match["smiles"])
-            spectrum.set("inchi", best_match["inchi"])
-            spectrum.set("inchikey", best_match["inchikey"])
-            logger.info("Added smiles %s based on the compound name %s", best_match["smiles"], compound_name)
-            return spectrum
-    return spectrum
-
-
-def _load_compound_name_annotations(annotated_compound_names_file):
-    """Loads in the annotated compound names and checks format"""
-    annotated_compound_names = pd.read_csv(annotated_compound_names_file)
-    assert list(annotated_compound_names.columns) == ["compound_name", "smiles", "inchi",
-                                                      "inchikey", "monoisotopic_mass"], \
-        "Choose a different annotated compound names file with columns compound_name, smiles, inchi, inchikey, monoisotopic_mass"
-    return annotated_compound_names
-
-
-def _is_plausible_name(compound_name):
-    """Simple check if it can be a compound name."""
-    return isinstance(compound_name, str) and len(compound_name) > 4
diff --git a/pyproject.toml b/pyproject.toml
index 3c9469de..336802cf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,87 +1,88 @@
-[tool.poetry]
-name = "matchms"
-version = "0.22.0"
-description = "Python library for large-scale comparisons and processing of tandem mass spectral data"
-authors = ["matchms developer team <florian.huber@hs-duesseldorf.de>"]
-
-license = "Apache-2.0"
-readme = "README.rst"
-repository = "https://github.com/matchms/matchms"
-documentation = "https://matchms.readthedocs.io/en/latest/"
-keywords=[ "similarity measures",
-           "mass spectrometry",
-           "fuzzy matching",
-           "fuzzy search" ]
-classifiers = [
-    "Development Status :: 4 - Beta",
-    "Intended Audience :: Education",
-    "Intended Audience :: Science/Research",
-    "Intended Audience :: Developers",
-    "License :: OSI Approved :: Apache Software License",
-    "Natural Language :: English",
-    "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.9"
-]
-
-[tool.poetry.dependencies]
-python = ">=3.8,<3.13"
-numpy = "<1.25"
-scipy = "<1.11"
-pandas = "^2.0.3"
-pillow = "!=9.4.0"
-lxml = "^4.9.3"
-matplotlib = "^3.7.2"
-networkx = "^3.1"
-numba = "^0.57.1"
-pickydict = "^0.4.0"
-pyteomics = "^4.6"
-requests = "^2.31.0"
-sparsestack = "^0.4.1"
-tqdm = "^4.65.0"
-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"
-pylint = "^2.17.5"
-prospector = {extras = ["with-pyroma"], version = "^1.10.2"}
-pytest = "^7.4.0"
-pytest-cov = "^4.1.0"
-yapf = "^0.40.1"
-testfixtures = "^7.1.0"
-twine = "^4.0.2"
-black = "^23.7.0"
-poetry-bumpversion = "^0.3.1"
-
-[tool.poetry.group.docs.dependencies]
-sphinxcontrib-apidoc = "^0.3.0"
-sphinx-rtd-theme = "^1.2.2"
-sphinx = "^7"
-
-
-[build-system]
-requires = ["poetry-core"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.poetry_bumpversion.file."matchms/__version__.py"]
-
-[tool.isort]
-sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
-no_lines_before = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
-lines_after_imports = 2
-known_first_party = "matchms"
-skip = "readthedocs/conf.py"
-
-[tool.pytest.ini_options]
-testpaths = [
-    "tests",
-    "integration-tests",
-]
-
-[build_sphinx]
-source-dir = "docs"
-build-dir = "docs/_build"
-all_files = 1
-builder = "html"
+[tool.poetry]
+name = "matchms"
+version = "0.22.0"
+description = "Python library for large-scale comparisons and processing of tandem mass spectral data"
+authors = ["matchms developer team <florian.huber@hs-duesseldorf.de>"]
+
+license = "Apache-2.0"
+readme = "README.rst"
+repository = "https://github.com/matchms/matchms"
+documentation = "https://matchms.readthedocs.io/en/latest/"
+keywords=[ "similarity measures",
+           "mass spectrometry",
+           "fuzzy matching",
+           "fuzzy search" ]
+classifiers = [
+    "Development Status :: 4 - Beta",
+    "Intended Audience :: Education",
+    "Intended Audience :: Science/Research",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.9"
+]
+
+[tool.poetry.dependencies]
+python = ">=3.8,<3.13"
+numpy = "<1.25"
+scipy = "<1.11"
+pandas = "^2.0.3"
+pillow = "!=9.4.0"
+lxml = "^4.9.3"
+matplotlib = "^3.7.2"
+networkx = "^3.1"
+numba = "^0.57.1"
+pickydict = "^0.4.0"
+pyteomics = "^4.6"
+requests = "^2.31.0"
+sparsestack = "^0.4.1"
+tqdm = "^4.65.0"
+rdkit = "^2023.3.2"
+pyyaml = "^6.0.1"
+deprecated = "^1.2.14"
+pubchempy = "*"
+
+[tool.poetry.group.dev.dependencies]
+decorator = "^5.1.1"
+isort = "^5.12.0"
+pylint = "^2.17.5"
+prospector = {extras = ["with-pyroma"], version = "^1.10.2"}
+pytest = "^7.4.0"
+pytest-cov = "^4.1.0"
+yapf = "^0.40.1"
+testfixtures = "^7.1.0"
+twine = "^4.0.2"
+black = "^23.7.0"
+poetry-bumpversion = "^0.3.1"
+
+[tool.poetry.group.docs.dependencies]
+sphinxcontrib-apidoc = "^0.3.0"
+sphinx-rtd-theme = "^1.2.2"
+sphinx = "<7"
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry_bumpversion.file."matchms/__version__.py"]
+
+[tool.isort]
+sections = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
+no_lines_before = "FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER"
+lines_after_imports = 2
+known_first_party = "matchms"
+skip = "readthedocs/conf.py"
+
+[tool.pytest.ini_options]
+testpaths = [
+    "tests",
+    "integration-tests",
+]
+
+[build_sphinx]
+source-dir = "docs"
+build-dir = "docs/_build"
+all_files = 1
+builder = "html"


Test patch:
diff --git a/tests/filtering/test_all_filter_order.py b/tests/filtering/test_all_filter_order.py
index 28a6dc6e..13e62c19 100644
--- a/tests/filtering/test_all_filter_order.py
+++ b/tests/filtering/test_all_filter_order.py
@@ -15,15 +15,21 @@ REPAIR_PARENT_MASS_SMILES_FILTERS = \
      msfilters.repair_parent_mass_is_mol_wt, msfilters.repair_adduct_based_on_smiles,
      msfilters.repair_parent_mass_match_smiles_wrapper, ]
 ANNOTATION_REPARATIONS = [msfilters.repair_inchi_inchikey_smiles, msfilters.derive_smiles_from_inchi,
-                          msfilters.repair_smiles_from_compound_name, msfilters.derive_inchi_from_smiles,
-                          msfilters.derive_inchikey_from_inchi, ]
+                          msfilters.derive_inchi_from_smiles, msfilters.derive_inchikey_from_inchi, ]
 
 
 @pytest.mark.parametrize("early_filters, later_filters", [
     [[msfilters.repair_not_matching_annotation], [msfilters.require_valid_annotation]],
     [REPAIR_PARENT_MASS_SMILES_FILTERS, [msfilters.require_parent_mass_match_smiles]],
-    [ANNOTATION_REPARATIONS + [msfilters.clean_adduct], REPAIR_PARENT_MASS_SMILES_FILTERS],
+    [ANNOTATION_REPARATIONS + [msfilters.clean_adduct, msfilters.derive_smiles_from_pubchem_compound_name_search],
+     REPAIR_PARENT_MASS_SMILES_FILTERS],
     [[msfilters.add_precursor_mz,], [msfilters.require_precursor_mz,]],
+    # Since pubchem lookup checks if annotation is complete.
+    # So deriving inchi and inchikey from smiles, should happen first.
+    [ANNOTATION_REPARATIONS + [msfilters.add_parent_mass],
+     [msfilters.derive_smiles_from_pubchem_compound_name_search, ]],
+    # The parent mass is based on the adduct, so adduct filters should be performed first
+    [[msfilters.clean_adduct, msfilters.derive_adduct_from_name], [msfilters.add_parent_mass]]
 ])
 def test_all_filter_order(early_filters: List[Callable], later_filters: List[Callable]):
     """Tests if early_filter is run before later_filter"""
diff --git a/tests/filtering/test_repair_smiles_from_compound_name.py b/tests/filtering/test_repair_smiles_from_compound_name.py
index 413b1ee5..ba6bb809 100644
--- a/tests/filtering/test_repair_smiles_from_compound_name.py
+++ b/tests/filtering/test_repair_smiles_from_compound_name.py
@@ -1,7 +1,10 @@
 import csv
 import os
 import pytest
-from matchms.filtering import repair_smiles_from_compound_name
+from matchms.filtering import derive_smiles_from_pubchem_compound_name_search
+from matchms.filtering.metadata_processing.derive_smiles_from_pubchem_compound_name_search import (
+    _get_pubchem_compound_name_annotation, _load_compound_name_annotations,
+    _pubchem_name_search, _write_compound_name_annotations)
 from ..builder_Spectrum import SpectrumBuilder
 
 
@@ -17,29 +20,116 @@ def csv_file_annotated_compound_names(tmp_path):
     return csv_file_name
 
 
-@pytest.mark.parametrize("compound_name, smile, inchi, inchikey, parent_mass, expected_smiles", [
-    ("compound_1", "input_smile_1", "inchi_1", "inchikey_1", 100.01, 'smile_1'),
-    ("compound_2", "input_smile_2", "inchi_2", "inchikey_2", 100.01, 'input_smile_2'),
-    ("compound_1", "input_smile_1", "inchi_1", "inchikey_1", 200.01, 'input_smile_1'),
-    ("compound_3", "input_smile_1", "inchi_1", "inchikey_1", 200.01, 'input_smile_1'),
-    # test that correct annotations are skipped
-    ("compound_1", "C1CSSC1CCCCC(=O)O",
-     "InChI=1S/C8H14O2S2/c9-8(10)4-2-1-3-7-5-6-11-12-7/h7H,1-6H2,(H,9,10)", "AGBQKNBQESQNJD-UHFFFAOYSA-N", 100.01,
-     'C1CSSC1CCCCC(=O)O'),
-    ("compound_1", "C1CSSC1CCCCC(=O)O",
-     "InChI=1S/C8H14O2S2/c9-8(10)4-2-1-3-7-5-6-11-12-7/h7H,1-6H2,(H,9,10)", "AGBQKNBQESQNJD-UHFFFAOYSA_bla_bla", 100.01,
-     "smile_1"),
+@pytest.fixture()
+def csv_file_with_real_compound_names(tmp_path):
+    csv_file_name = os.path.join(tmp_path, "expected_compound_annotation.csv")
+    expected_result = r"""compound_name,smiles,inchi,inchikey,monoisotopic_mass
+    PC(18:0/20:4),CCCCCCCCCCCCCCCCCC(=O)O[C@H](COC(=O)CCC/C=C\C/C=C\C/C=C\C/C=C\CCCCC)COP(=O)([O-])OCC[N+](C)(C)C,"InChI=1S/C46H84NO8P/c1-6-8-10-12-14-16-18-20-22-23-25-26-28-30-32-34-36-38-45(48)52-42-44(43-54-56(50,51)53-41-40-47(3,4)5)55-46(49)39-37-35-33-31-29-27-24-21-19-17-15-13-11-9-7-2/h14,16,20,22,25-26,30,32,44H,6-13,15,17-19,21,23-24,27-29,31,33-43H2,1-5H3/b16-14-,22-20-,26-25-,32-30-/t44-/m1/s1",DNYKSJQVBCVGOF-LCKGXUDJSA-N,809.59345564
+    fructose,C,"InChI=1S/C6H12O6/c7-2-6(11)5(10)4(9)3(8)1-12-6/h3-5,7-11H,1-2H2/t3-,4-,5+,6?/m1/s1",LKDRXBCSQODPBY-VRPWFDPXSA-N,180.0633881
+    glucose,C([C@@H]1[C@H]([C@@H]([C@H](C(O1)O)O)O)O)O,"InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1",WQZGKKKJIJFFOK-GASJEMHNSA-N,180.0633881
+    this compound does not exist,,,,
+    galactose,C([C@@H]1[C@@H]([C@@H]([C@H](C(O1)O)O)O)O)O,"InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3+,4+,5-,6?/m1/s1",WQZGKKKJIJFFOK-SVZMEOIVSA-N,180.0633881
+    """
+    with open(csv_file_name, "w", encoding="utf8") as f:
+        f.write(expected_result)
+    return csv_file_name
+
+
+def test_repair_smiles_from_compound_name_skip_already_correct():
+    """Tests if already correct annotations are not repaired (even if the compound name does not match)"""
+    builder = SpectrumBuilder()
+    spectrum_in = builder.with_metadata({"compound_name": "compound_1",
+                                         "parent_mass": 100.01,
+                                         "smiles": "C1CSSC1CCCCC(=O)O",
+                                         "inchi": "InChI=1S/C8H14O2S2/c9-8(10)4-2-1-3-7-5-6-11-12-7/h7H,1-6H2,(H,9,10)",
+                                         "inchikey": "AGBQKNBQESQNJD-UHFFFAOYSA-N"}).build()
+    spectrum = derive_smiles_from_pubchem_compound_name_search(spectrum_in, mass_tolerance=0.1)
+    assert spectrum_in == spectrum
+
+
+@pytest.mark.parametrize("compound_name, smiles, parent_mass, expected_smiles", [
+    ("PC(18:0/20:4)", "wrong_smiles",
+     809.593, r"CCCCCCCCCCCCCCCCCC(=O)O[C@H](COC(=O)CCC/C=C\C/C=C\C/C=C\C/C=C\CCCCC)COP(=O)([O-])OCC[N+](C)(C)C"),
+    ("glucose", "input_smile_1", 180.0633881, 'C([C@@H]1[C@H]([C@@H]([C@H](C(O1)O)O)O)O)O'),
+    ("this compound does not exist", None, 200.01, None),
+    ("also_does_not_exist_and_not_in_csv", None, 100.01, None),
 ])
-def test_clean_compound_name(compound_name, parent_mass, smile, inchi, inchikey,
-                             expected_smiles,
-                             csv_file_annotated_compound_names):
-    # pylint: disable=too-many-arguments
-    pytest.importorskip("rdkit")
+def test_repair_smiles_from_compound_name(compound_name, parent_mass, smiles,
+                                          expected_smiles,
+                                          csv_file_with_real_compound_names, tmp_path):
+    #pylint: disable=too-many-arguments
     builder = SpectrumBuilder()
     spectrum_in = builder.with_metadata({"compound_name": compound_name,
                                          "parent_mass": parent_mass,
-                                         "smiles": smile,
-                                         "inchi": inchi,
-                                         "inchikey": inchikey}).build()
-    spectrum = repair_smiles_from_compound_name(spectrum_in, csv_file_annotated_compound_names, mass_tolerance=0.1)
+                                         "smiles": smiles}).build()
+    spectrum = derive_smiles_from_pubchem_compound_name_search(spectrum_in, csv_file_with_real_compound_names,
+                                                               mass_tolerance=0.1)
+    assert spectrum.get("smiles") == expected_smiles, "Expected different smiles."
+    # Run without csv file
+    spectrum = derive_smiles_from_pubchem_compound_name_search(spectrum_in, mass_tolerance=0.1)
+    assert spectrum.get("smiles") == expected_smiles, "Expected different smiles."
+    # Run with empty csv file
+    empty_csv_file = os.path.join(tmp_path, "test.csv")
+    spectrum = derive_smiles_from_pubchem_compound_name_search(spectrum_in, empty_csv_file, mass_tolerance=0.1)
     assert spectrum.get("smiles") == expected_smiles, "Expected different smiles."
+    stored_in_csv_file = _load_compound_name_annotations(empty_csv_file, compound_name)
+    assert len(stored_in_csv_file) == 1
+    assert stored_in_csv_file[0]["smiles"] == expected_smiles
+    assert stored_in_csv_file[0]["compound_name"] == compound_name
+
+
+def test_write_compound_names_to_file(tmp_path):
+    csv_file_name = os.path.join(tmp_path, "compound_annotation.csv")
+    annotation_1 = [{'compound_name': 'glucose', 'smiles': 'C([C@@H]1[C@H]([C@@H]([C@H](C(O1)O)O)O)O)O',
+                  'inchi': 'InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1',
+                  'inchikey': 'WQZGKKKJIJFFOK-GASJEMHNSA-N', 'monoisotopic_mass': 180.06338810},
+                    {'compound_name': 'glucose', 'smiles': 'test_smiles',
+                     'inchi': 'test_inchi',
+                     'inchikey': 'test_inchikey', 'monoisotopic_mass': 10}
+                    ]
+    _write_compound_name_annotations(csv_file_name, annotation_1)
+    # Run a second time to make sure an alrady existing file can be reused
+    annotation_2 = [{'compound_name': 'compound_2', 'smiles': None,
+                     'inchi': None, 'inchikey': None, 'monoisotopic_mass': None}]
+    _write_compound_name_annotations(csv_file_name,
+                                     annotation_2)
+    assert _load_compound_name_annotations(csv_file_name, "glucose") == annotation_1
+    assert _load_compound_name_annotations(csv_file_name, "compound_2") == annotation_2
+
+
+@pytest.mark.parametrize("compound_name, expected_output", [
+    ("compound_1", [{'compound_name': 'compound_1', 'smiles': 'smile_1',
+                     'inchi': 'inchi_1',
+                     'inchikey': 'inchikey_1', 'monoisotopic_mass': 100}]),
+    ("compound_2", [{'compound_name': 'compound_2', 'smiles': None,
+                     'inchi': None,
+                     'inchikey': None, 'monoisotopic_mass': None}]), ])
+def test_load_compound_name_annotation(compound_name, expected_output, csv_file_annotated_compound_names):
+    result = _load_compound_name_annotations(csv_file_annotated_compound_names, compound_name)
+    assert result == expected_output
+
+
+@pytest.mark.parametrize("compound_name, expected_output", [
+    ("glucose", [{'compound_name': 'glucose', 'smiles': 'C([C@@H]1[C@H]([C@@H]([C@H](C(O1)O)O)O)O)O',
+                  'inchi': 'InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1',
+                  'inchikey': 'WQZGKKKJIJFFOK-GASJEMHNSA-N', 'monoisotopic_mass': 180.06338810}]),
+    ("does_not_exist", [])])
+def test_pubchem_name_search(compound_name, expected_output):
+    result = _pubchem_name_search(compound_name)
+    assert result == expected_output
+
+
+@pytest.mark.parametrize("compound_name, expected_output", [
+    ("compound_1", [{'compound_name': 'compound_1', 'smiles': 'smile_1',
+                     'inchi': 'inchi_1',
+                     'inchikey': 'inchikey_1', 'monoisotopic_mass': 100}]),
+    ("compound_2", [{'compound_name': 'compound_2', 'smiles': None,
+                     'inchi': None,
+                     'inchikey': None, 'monoisotopic_mass': None}]),
+    ("glucose", [{'compound_name': 'glucose', 'smiles': 'C([C@@H]1[C@H]([C@@H]([C@H](C(O1)O)O)O)O)O',
+                  'inchi': 'InChI=1S/C6H12O6/c7-1-2-3(8)4(9)5(10)6(11)12-2/h2-11H,1H2/t2-,3-,4+,5-,6?/m1/s1',
+                  'inchikey': 'WQZGKKKJIJFFOK-GASJEMHNSA-N', 'monoisotopic_mass': 180.06338810}]),
+    ("does_not_exist", [])])
+def test_get_compound_name_annotation(compound_name, expected_output, csv_file_annotated_compound_names):
+    result = _get_pubchem_compound_name_annotation(compound_name, csv_file_annotated_compound_names)
+    assert result == expected_output
diff --git a/tests/filtering/test_spectrum_processor.py b/tests/filtering/test_spectrum_processor.py
index 1d9c5168..25b39d39 100644
--- a/tests/filtering/test_spectrum_processor.py
+++ b/tests/filtering/test_spectrum_processor.py
@@ -22,6 +22,28 @@ 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',
+        'harmonize_undefined_inchikey',
+        'harmonize_undefined_inchi',
+        'harmonize_undefined_smiles',
+        'repair_inchi_inchikey_smiles',
+        'add_parent_mass',
+        '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}),
@@ -32,12 +54,11 @@ def test_filter_sorting_and_output():
                         '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',
+                        ('add_parent_mass', {'estimate_from_adduct': True, 'overwrite_existing_entry': False}),
                         'normalize_intensities']
     assert processing.processing_steps == expected_filters