diff --git a/src/caoscrawler/__init__.py b/src/caoscrawler/__init__.py index 3c71caed3362e50b5c4af947e0e3b1fa7805f8d5..41b96323b1106d8ce28caadc4a2da012f3dc22ea 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 c62f44eeaa75ca42579aa3d6ead437e901cd38ff..096fde9b573f4ff60995498144cad3589ce7dbb2 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 5a80ab9b230db4540d741bf8fa4f9d11b5158aab..dfb79c8b6b10909952174cf24c3aa9198f3b7743 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"