diff --git a/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py b/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py index 0a3cc27b595..f4a5251cb9c 100644 --- a/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py +++ b/cuda_pathfinder/cuda/pathfinder/_optional_cuda_import.py @@ -10,6 +10,21 @@ from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError +def _target_is_unavailable(missing_modname: str | None, fully_qualified_modname: str) -> bool: + """Report whether a ModuleNotFoundError means "the target is not installed". + + ``ModuleNotFoundError.name`` is the module that could not be found, which + for ``import a.b.c`` is the *outermost* missing name: if package ``a.b`` is + not installed at all, ``name`` is ``"a.b"``, not ``"a.b.c"``. A missing + ancestor makes the target just as unavailable as a missing leaf, so both + count. Anything else is a broken dependency of the target and must not be + swallowed. + """ + if missing_modname is None: + return False + return fully_qualified_modname == missing_modname or fully_qualified_modname.startswith(missing_modname + ".") + + def _optional_cuda_import( fully_qualified_modname: str, *, @@ -19,7 +34,8 @@ def _optional_cuda_import( Returns: The imported module if available and the optional probe succeeds, - otherwise ``None`` when the requested module is unavailable. + otherwise ``None`` when the requested module — or a package containing + it — is not installed. Raises: ModuleNotFoundError: If the import fails because a dependency of the @@ -30,7 +46,7 @@ def _optional_cuda_import( try: module = importlib.import_module(fully_qualified_modname) except ModuleNotFoundError as err: - if err.name != fully_qualified_modname: + if not _target_is_unavailable(err.name, fully_qualified_modname): raise return None diff --git a/cuda_pathfinder/tests/test_optional_cuda_import.py b/cuda_pathfinder/tests/test_optional_cuda_import.py index 24c7425a9ce..da8f0a99bda 100644 --- a/cuda_pathfinder/tests/test_optional_cuda_import.py +++ b/cuda_pathfinder/tests/test_optional_cuda_import.py @@ -32,6 +32,42 @@ def fake_import_module(name): assert result is None +@pytest.mark.agent_authored(model="claude-opus-5") +def test__optional_cuda_import_returns_none_when_parent_package_missing(monkeypatch): + def fake_import_module(_name): + # What CPython reports for `import cuda.bindings.nvjitlink` when the + # cuda.bindings package itself is not installed: the outermost missing + # name, not the fully qualified one that was requested. + err = ModuleNotFoundError("No module named 'cuda.bindings'") + err.name = "cuda.bindings" + raise err + + monkeypatch.setattr(optional_import_mod.importlib, "import_module", fake_import_module) + + assert _optional_cuda_import("cuda.bindings.nvjitlink") is None + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test__optional_cuda_import_returns_none_for_a_really_uninstalled_package(): + """Same as above, but through the real import machinery instead of a stub.""" + assert _optional_cuda_import("cuda_pathfinder_no_such_package.sub.mod") is None + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test__optional_cuda_import_reraises_for_a_string_prefix_that_is_not_an_ancestor(monkeypatch): + """``cuda.bindings`` is a string prefix of ``cuda.bindings_extra``, not a parent of it.""" + + def fake_import_module(_name): + err = ModuleNotFoundError("No module named 'cuda.bindings'") + err.name = "cuda.bindings" + raise err + + monkeypatch.setattr(optional_import_mod.importlib, "import_module", fake_import_module) + + with pytest.raises(ModuleNotFoundError, match="cuda.bindings"): + _optional_cuda_import("cuda.bindings_extra.mod") + + def test__optional_cuda_import_reraises_nested_module_not_found(monkeypatch): def fake_import_module(_name): err = ModuleNotFoundError("No module named 'not_a_real_dependency'")