Repository: deepset-ai/haystack

Issue:
`PyPDFToDocument`: remove deprecated `converter_name` and `CONVERTERS_REGISTRY`
`PyPDFToDocument` was refactored in #7362

`converter_name` and `CONVERTERS_REGISTRY` are deprecated and should be removed in 2.3.0.

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 9613098651](https://coveralls.io/builds/68222775)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **6** unchanged lines in **1** file lost coverage.
* Overall coverage decreased (**-0.02%**) to **89.957%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/converters/pypdf.py](https://coveralls.io/builds/68222775/source?filename=components%2Fconverters%2Fpypdf.py#L28) | 6 | 90.0% |
<!-- | **Total:** | **6** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/68222775/badge)](https://coveralls.io/builds/68222775) |
| :-- | --: |
| Change from base [Build 9612234117](https://coveralls.io/builds/68221415): |  -0.02% |
| Covered Lines: | 6700 |
| Relevant Lines: | 7448 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/haystack/components/converters/pypdf.py b/haystack/components/converters/pypdf.py
index ec092503..8f5aa5ce 100644
--- a/haystack/components/converters/pypdf.py
+++ b/haystack/components/converters/pypdf.py
@@ -3,7 +3,6 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import io
-import warnings
 from pathlib import Path
 from typing import Any, Dict, List, Optional, Protocol, Union
 
@@ -56,12 +55,6 @@ class DefaultConverter:
         return default_from_dict(cls, data)
 
 
-# This registry is used to store converters names and instances.
-# It can be used to register custom converters.
-# It is now deprecated and will be removed in Haystack 2.3.0.
-CONVERTERS_REGISTRY: Dict[str, PyPDFConverter] = {"default": DefaultConverter()}
-
-
 @component
 class PyPDFToDocument:
     """
@@ -82,40 +75,16 @@ class PyPDFToDocument:
     ```
     """
 
-    def __init__(self, converter_name: Optional[str] = None, converter: Optional[PyPDFConverter] = None):
+    def __init__(self, converter: Optional[PyPDFConverter] = None):
         """
         Create an PyPDFToDocument component.
 
-        :param converter_name:
-            The name of a PyPDFConverter instance stored in the CONVERTERS_REGISTRY. Deprecated.
         :param converter:
             An instance of a PyPDFConverter compatible class.
         """
         pypdf_import.check()
 
-        self.converter_name = converter_name
-        if converter_name:
-            warnings.warn(
-                "The `converter_name` parameter is deprecated and will be removed in Haystack 2.3.0. "
-                "Please use the `converter` parameter instead.",
-                DeprecationWarning,
-            )
-            try:
-                converter = CONVERTERS_REGISTRY[converter_name]
-            except KeyError:
-                msg = (
-                    f"Invalid converter_name: {converter_name}.\n "
-                    f"Available converters: {list(CONVERTERS_REGISTRY.keys())}"
-                    f"To specify a custom converter, it is recommended to use the `converter` parameter."
-                )
-                raise ValueError(msg) from KeyError
-
-            self.converter = converter
-
-        elif converter:
-            self.converter = converter
-        else:
-            self.converter = DefaultConverter()
+        self.converter = converter or DefaultConverter()
 
     def to_dict(self):
         """
@@ -124,7 +93,7 @@ class PyPDFToDocument:
         :returns:
             Dictionary with serialized data.
         """
-        return default_to_dict(self, converter_name=self.converter_name, converter=self.converter.to_dict())
+        return default_to_dict(self, converter=self.converter.to_dict())
 
     @classmethod
     def from_dict(cls, data):
@@ -137,9 +106,6 @@ class PyPDFToDocument:
         :returns:
             Deserialized component.
         """
-        if data["init_parameters"].get("converter_name"):
-            return default_from_dict(cls, data)
-
         converter_class = deserialize_type(data["init_parameters"]["converter"]["type"])
         data["init_parameters"]["converter"] = converter_class.from_dict(data["init_parameters"]["converter"])
         return default_from_dict(cls, data)
diff --git a/haystack/components/evaluators/sas_evaluator.py b/haystack/components/evaluators/sas_evaluator.py
index 7625b837..0fc5e80d 100644
--- a/haystack/components/evaluators/sas_evaluator.py
+++ b/haystack/components/evaluators/sas_evaluator.py
@@ -157,6 +157,9 @@ class SASEvaluator:
         if len(ground_truth_answers) != len(predicted_answers):
             raise ValueError("The number of predictions and labels must be the same.")
 
+        if any(answer is None for answer in predicted_answers):
+            raise ValueError("Predicted answers must not contain None values.")
+
         if len(predicted_answers) == 0:
             return {"score": 0.0, "individual_scores": [0.0]}
 
diff --git a/haystack/document_stores/in_memory/document_store.py b/haystack/document_stores/in_memory/document_store.py
index 5ff6cb1f..4fd10e1c 100644
--- a/haystack/document_stores/in_memory/document_store.py
+++ b/haystack/document_stores/in_memory/document_store.py
@@ -2,11 +2,13 @@
 #
 # SPDX-License-Identifier: Apache-2.0
 
+import json
 import math
 import re
 import uuid
 from collections import Counter
 from dataclasses import dataclass
+from pathlib import Path
 from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple
 
 import numpy as np
@@ -339,6 +341,42 @@ class InMemoryDocumentStore:
         """
         return default_from_dict(cls, data)
 
+    def save_to_disk(self, path: str) -> None:
+        """
+        Write the database and its' data to disk as a JSON file.
+
+        :param path: The path to the JSON file.
+        """
+        data: Dict[str, Any] = self.to_dict()
+        data["documents"] = [doc.to_dict(flatten=False) for doc in self.storage.values()]
+        with open(path, "w") as f:
+            json.dump(data, f)
+
+    @classmethod
+    def load_from_disk(cls, path: str) -> "InMemoryDocumentStore":
+        """
+        Load the database and its' data from disk as a JSON file.
+
+        :param path: The path to the JSON file.
+        :returns: The loaded InMemoryDocumentStore.
+        """
+        if Path(path).exists():
+            try:
+                with open(path, "r") as f:
+                    data = json.load(f)
+            except Exception as e:
+                raise Exception(f"Error loading InMemoryDocumentStore from disk. error: {e}")
+
+            documents = data.pop("documents")
+            cls_object = default_from_dict(cls, data)
+            cls_object.write_documents(
+                documents=[Document(**doc) for doc in documents], policy=DuplicatePolicy.OVERWRITE
+            )
+            return cls_object
+
+        else:
+            raise FileNotFoundError(f"File {path} not found.")
+
     def count_documents(self) -> int:
         """
         Returns the number of how many documents are present in the DocumentStore.
diff --git a/releasenotes/notes/add-serialization-to-inmemorydocumentstore-2aa4d9ac85b961c5.yaml b/releasenotes/notes/add-serialization-to-inmemorydocumentstore-2aa4d9ac85b961c5.yaml
new file mode 100644
index 00000000..48e3c8e4
--- /dev/null
+++ b/releasenotes/notes/add-serialization-to-inmemorydocumentstore-2aa4d9ac85b961c5.yaml
@@ -0,0 +1,4 @@
+---
+enhancements:
+  - |
+    Added serialization methods save_to_disk and write_to_disk to InMemoryDocumentStore.
diff --git a/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml b/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml
new file mode 100644
index 00000000..d12adfde
--- /dev/null
+++ b/releasenotes/notes/check-for-None-SAS-eval-0b982ccc1491ee83.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - |
+    `SASEvaluator` now raises a `ValueError` if a `None` value is contained in the `predicted_answers` input.
diff --git a/releasenotes/notes/pypdf-rm-deprecated-params-6c4c51489305bb85.yaml b/releasenotes/notes/pypdf-rm-deprecated-params-6c4c51489305bb85.yaml
new file mode 100644
index 00000000..dcd6978e
--- /dev/null
+++ b/releasenotes/notes/pypdf-rm-deprecated-params-6c4c51489305bb85.yaml
@@ -0,0 +1,10 @@
+---
+upgrade:
+  - |
+    The deprecated `converter_name` parameter has been removed from `PyPDFToDocument`.
+
+    To specify a custom converter for `PyPDFToDocument`, use the `converter` initialization parameter and
+    pass an instance of a class that implements the `PyPDFConverter` protocol.
+
+    The `PyPDFConverter` protocol defines the methods `convert`, `to_dict` and `from_dict`.
+    A default implementation of `PyPDFConverter` is provided in the `DefaultConverter` class.


Test patch:
diff --git a/test/components/converters/test_pypdf_to_document.py b/test/components/converters/test_pypdf_to_document.py
index 6bf408b4..00530192 100644
--- a/test/components/converters/test_pypdf_to_document.py
+++ b/test/components/converters/test_pypdf_to_document.py
@@ -7,7 +7,7 @@ from unittest.mock import patch
 import pytest
 
 from haystack import Document, default_from_dict, default_to_dict
-from haystack.components.converters.pypdf import CONVERTERS_REGISTRY, DefaultConverter, PyPDFToDocument
+from haystack.components.converters.pypdf import DefaultConverter, PyPDFToDocument
 from haystack.dataclasses import ByteStream
 
 
@@ -19,19 +19,13 @@ def pypdf_converter():
 class TestPyPDFToDocument:
     def test_init(self, pypdf_converter):
         assert isinstance(pypdf_converter.converter, DefaultConverter)
-        assert pypdf_converter.converter_name is None
-
-    def test_init_fail_nonexisting_converter(self):
-        with pytest.raises(ValueError):
-            PyPDFToDocument(converter_name="non_existing_converter")
 
     def test_to_dict(self, pypdf_converter):
         data = pypdf_converter.to_dict()
         assert data == {
             "type": "haystack.components.converters.pypdf.PyPDFToDocument",
             "init_parameters": {
-                "converter": {"type": "haystack.components.converters.pypdf.DefaultConverter", "init_parameters": {}},
-                "converter_name": None,
+                "converter": {"type": "haystack.components.converters.pypdf.DefaultConverter", "init_parameters": {}}
             },
         }
 
@@ -45,18 +39,6 @@ class TestPyPDFToDocument:
         instance = PyPDFToDocument.from_dict(data)
         assert isinstance(instance, PyPDFToDocument)
         assert isinstance(instance.converter, DefaultConverter)
-        assert instance.converter_name is None
-
-    def test_from_dict_with_converter_name(self):
-        data = {
-            "type": "haystack.components.converters.pypdf.PyPDFToDocument",
-            "init_parameters": {"converter_name": "default"},
-        }
-
-        instance = PyPDFToDocument.from_dict(data)
-        assert isinstance(instance, PyPDFToDocument)
-        assert isinstance(instance.converter, DefaultConverter)
-        assert instance.converter_name == "default"
 
     @pytest.mark.integration
     def test_run(self, test_files_path, pypdf_converter):
@@ -140,22 +122,3 @@ class TestPyPDFToDocument:
         assert len(docs) == 1
         assert "ReAct" not in docs[0].content
         assert "I don't care about converting given pdfs, I always return this" in docs[0].content
-
-    @pytest.mark.integration
-    def test_custom_converter_deprecated(self, test_files_path):
-        from pypdf import PdfReader
-
-        paths = [test_files_path / "pdf" / "sample_pdf_1.pdf"]
-
-        class MyCustomConverter:
-            def convert(self, reader: PdfReader) -> Document:
-                return Document(content="I don't care about converting given pdfs, I always return this")
-
-        CONVERTERS_REGISTRY["custom"] = MyCustomConverter()
-
-        converter = PyPDFToDocument(converter_name="custom")
-        output = converter.run(sources=paths)
-        docs = output["documents"]
-        assert len(docs) == 1
-        assert "ReAct" not in docs[0].content
-        assert "I don't care about converting given pdfs, I always return this" in docs[0].content
diff --git a/test/components/evaluators/test_sas_evaluator.py b/test/components/evaluators/test_sas_evaluator.py
index 4a9ee402..d0e2e9b8 100644
--- a/test/components/evaluators/test_sas_evaluator.py
+++ b/test/components/evaluators/test_sas_evaluator.py
@@ -73,6 +73,21 @@ class TestSASEvaluator:
         with pytest.raises(ValueError):
             evaluator.run(ground_truth_answers=ground_truths, predicted_answers=predictions)
 
+    def test_run_with_none_in_predictions(self):
+        evaluator = SASEvaluator()
+        ground_truths = [
+            "A construction budget of US $2.3 billion",
+            "The Eiffel Tower, completed in 1889, symbolizes Paris's cultural magnificence.",
+            "The Meiji Restoration in 1868 transformed Japan into a modernized world power.",
+        ]
+        predictions = [
+            "A construction budget of US $2.3 billion",
+            None,
+            "The Meiji Restoration in 1868 transformed Japan into a modernized world power.",
+        ]
+        with pytest.raises(ValueError):
+            evaluator.run(ground_truth_answers=ground_truths, predicted_answers=predictions)
+
     def test_run_not_warmed_up(self):
         evaluator = SASEvaluator()
         ground_truths = [
diff --git a/test/document_stores/test_in_memory.py b/test/document_stores/test_in_memory.py
index 2a867950..8b8ed0e5 100644
--- a/test/document_stores/test_in_memory.py
+++ b/test/document_stores/test_in_memory.py
@@ -6,6 +6,7 @@ from unittest.mock import patch
 
 import pandas as pd
 import pytest
+import tempfile
 
 from haystack import Document
 from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
@@ -18,6 +19,11 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
     Test InMemoryDocumentStore's specific features
     """
 
+    @pytest.fixture
+    def tmp_dir(self):
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            yield tmp_dir
+
     @pytest.fixture
     def document_store(self) -> InMemoryDocumentStore:
         return InMemoryDocumentStore(bm25_algorithm="BM25L")
@@ -74,6 +80,18 @@ class TestMemoryDocumentStore(DocumentStoreBaseTests):  # pylint: disable=R0904
         assert store.bm25_parameters == {"key": "value"}
         assert store.index == "my_cool_index"
 
+    def test_save_to_disk_and_load_from_disk(self, tmp_dir: str):
+        docs = [Document(content="Hello world"), Document(content="Haystack supports multiple languages")]
+        document_store = InMemoryDocumentStore()
+        document_store.write_documents(docs)
+        tmp_dir = tmp_dir + "/document_store.json"
+        document_store.save_to_disk(tmp_dir)
+        document_store_loaded = InMemoryDocumentStore.load_from_disk(tmp_dir)
+
+        assert document_store_loaded.count_documents() == 2
+        assert list(document_store_loaded.storage.values()) == docs
+        assert document_store_loaded.to_dict() == document_store.to_dict()
+
     def test_invalid_bm25_algorithm(self):
         with pytest.raises(ValueError, match="BM25 algorithm 'invalid' is not supported"):
             InMemoryDocumentStore(bm25_algorithm="invalid")