diff --git a/src/wrappers/OsipiBase.py b/src/wrappers/OsipiBase.py index 6ef67db..cdee44e 100644 --- a/src/wrappers/OsipiBase.py +++ b/src/wrappers/OsipiBase.py @@ -145,19 +145,60 @@ def osipi_initiate_algorithm(self, algorithm, **kwargs): Args: algorithm (string): The name of the algorithm, should be the same as the file in the src/standardized folder without the .py extension. + + Raises: + ValueError: If the algorithm name does not correspond to any .py file + in ``src/standardized/``. """ # Import the algorithm root_path = pathlib.Path(__file__).resolve().parents[2] if str(root_path) not in sys.path: - print("Root folder not in PYTHONPATH") - return False - + raise RuntimeError( + "Root folder not found in PYTHONPATH. " + "Please ensure the project root is in sys.path." + ) + + # ------------------------------------------------------------------ + # Validate algorithm name against available modules in src/standardized/ + # ------------------------------------------------------------------ + standardized_dir = root_path / "src" / "standardized" + available_algorithms = sorted( + p.stem + for p in standardized_dir.glob("*.py") + if p.stem != "__init__" + ) + + if algorithm not in available_algorithms: + raise ValueError( + f"Algorithm '{algorithm}' not found. " + f"Available algorithms are:\n " + + "\n ".join(available_algorithms) + ) + import_base_path = "src.standardized" import_path = import_base_path + "." + algorithm - #Algorithm = getattr(importlib.import_module(import_path), algorithm) + + # Secondary safety net: catch import / attribute errors that could + # occur if the file exists but is broken or missing the class. + try: + module = importlib.import_module(import_path) + except ImportError as exc: + raise ImportError( + f"Failed to import module for algorithm '{algorithm}': {exc}" + ) from exc + + try: + algorithm_class = getattr(module, algorithm) + except AttributeError as exc: + raise AttributeError( + f"Module '{import_path}' was imported but does not contain " + f"a class named '{algorithm}'. " + f"Available names: {[n for n in dir(module) if not n.startswith('_')]}" + ) from exc + # Change the class from OsipiBase to the specified algorithm - self.__class__ = getattr(importlib.import_module(import_path), algorithm) + self.__class__ = algorithm_class self.__init__(**kwargs) def initialize(**kwargs): diff --git a/tests/IVIMmodels/unit_tests/test_ivim_fit.py b/tests/IVIMmodels/unit_tests/test_ivim_fit.py index 5b4849d..480476d 100644 --- a/tests/IVIMmodels/unit_tests/test_ivim_fit.py +++ b/tests/IVIMmodels/unit_tests/test_ivim_fit.py @@ -300,3 +300,28 @@ def to_list_if_needed(value): if errors: all_errors = "\n".join(errors) raise AssertionError(f"Some tests failed:\n{all_errors}") + + +def test_invalid_algorithm_name(): + """Regression test for bug_7: invalid algorithm names must raise a clear + ValueError instead of a confusing ModuleNotFoundError / AttributeError. + """ + # A clearly wrong name should raise ValueError + with pytest.raises(ValueError, match="not found"): + OsipiBase(algorithm="IAR_LU_biepx") # typo: 'biepx' instead of 'biexp' + + # The error message should list at least one known valid algorithm + try: + OsipiBase(algorithm="totally_fake_algorithm") + except ValueError as exc: + error_msg = str(exc) + assert "Available algorithms are:" in error_msg + # Check a few known algorithms appear in the suggestion list + assert "IAR_LU_biexp" in error_msg + assert "PV_MUMC_biexp" in error_msg + else: + pytest.fail("ValueError was not raised for a completely invalid algorithm name") + + # A valid algorithm should still work without errors + fit = OsipiBase(algorithm="IAR_LU_biexp") + assert fit is not None