From d54c31cdc598c2c373b66c9c527f5de586a87368 Mon Sep 17 00:00:00 2001
From: Daniel <d.hornung@indiscale.com>
Date: Mon, 17 Jun 2024 15:57:53 +0200
Subject: [PATCH] ENH: Also hande the case where spss is not installed.

---
 src/caoscrawler/__init__.py |  9 +++++++--
 src/caoscrawler/utils.py    | 30 ++++++++++++++++++++++++++++++
 unittests/test_utilities.py | 33 +++++++++++++++++++++++++++++++++
 3 files changed, 70 insertions(+), 2 deletions(-)

diff --git a/src/caoscrawler/__init__.py b/src/caoscrawler/__init__.py
index 3c71caed..41b96323 100644
--- a/src/caoscrawler/__init__.py
+++ b/src/caoscrawler/__init__.py
@@ -1,5 +1,10 @@
-from . import converters
-from .conv_impl.spss import SPSSConverter
+from . import converters, utils
+try:
+    from .conv_impl.spss import SPSSConverter
+except ImportError as err:
+    SPSSConverter: type = utils.MissingImport(
+        name="SPSSConverter", hint="Try installing with the `spss` extra option.",
+        err=err)
 from .crawl import Crawler, SecurityMode
 from .version import CfoodRequiredVersionError, get_caoscrawler_version
 
diff --git a/src/caoscrawler/utils.py b/src/caoscrawler/utils.py
index c62f44ee..096fde9b 100644
--- a/src/caoscrawler/utils.py
+++ b/src/caoscrawler/utils.py
@@ -25,6 +25,9 @@
 
 # Some utility functions, e.g. for extending pylib.
 
+import sys
+from typing import Optional
+
 import linkahead as db
 
 
@@ -39,3 +42,30 @@ def has_parent(entity: db.Entity, name: str):
         if parent.name == name:
             return True
     return False
+
+
+def MissingImport(name: str, hint: str = "", err: Optional[Exception] = None) -> type:
+    """Factory with dummy classes, which may be assigned to variables but never used."""
+    def _error():
+        error_msg = f"This class ({name}) cannot be used, because some libraries are missing."
+        if hint:
+            error_msg += "\n\n" + hint
+
+        if err:
+            print(error_msg, file=sys.stdout)
+            raise RuntimeError(error_msg) from err
+        raise RuntimeError(error_msg)
+
+    class _Meta(type):
+        def __getattribute__(cls, *args, **kwargs):
+            _error()
+
+        def __call__(cls, *args, **kwargs):
+            _error()
+
+    class _DummyClass(metaclass=_Meta):
+        pass
+
+    _DummyClass.__name__ = name
+
+    return _DummyClass
diff --git a/unittests/test_utilities.py b/unittests/test_utilities.py
index 5a80ab9b..dfb79c8b 100644
--- a/unittests/test_utilities.py
+++ b/unittests/test_utilities.py
@@ -19,7 +19,10 @@
 # along with this program. If not, see <https://www.gnu.org/licenses/>.
 #
 
+import pytest
+
 from caoscrawler.crawl import split_restricted_path
+from caoscrawler.utils import MissingImport
 
 
 def test_split_restricted_path():
@@ -33,3 +36,33 @@ def test_split_restricted_path():
     assert split_restricted_path("/test//bla") == ["test", "bla"]
     assert split_restricted_path("//test/bla") == ["test", "bla"]
     assert split_restricted_path("///test//bla////") == ["test", "bla"]
+
+
+def test_dummy_class():
+    Missing = MissingImport(name="Not Important", hint="Do the thing instead.")
+    with pytest.raises(RuntimeError) as err_info_1:
+        print(Missing.__name__)
+    with pytest.raises(RuntimeError) as err_info_2:
+        Missing()
+    with pytest.raises(RuntimeError) as err_info_3:
+        print(Missing.foo)
+
+    for err_info in (err_info_1, err_info_2, err_info_3):
+        msg = str(err_info.value)
+        assert "(Not Important)" in msg
+        assert msg.endswith("Do the thing instead.")
+
+    MissingErr = MissingImport(name="Not Important", hint="Do the thing instead.",
+                               err=ImportError("Old error"))
+    with pytest.raises(RuntimeError) as err_info_1:
+        print(MissingErr.__name__)
+    with pytest.raises(RuntimeError) as err_info_2:
+        MissingErr()
+    with pytest.raises(RuntimeError) as err_info_3:
+        print(MissingErr.foo)
+
+    for err_info in (err_info_1, err_info_2, err_info_3):
+        msg = str(err_info.value)
+        assert "(Not Important)" in msg
+        orig_msg = str(err_info.value.__cause__)
+        assert orig_msg == "Old error"
-- 
GitLab