Skip to content

Treat a missing parent package as "optional module unavailable" - #2545

Open
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:optional-import-missing-parent
Open

Treat a missing parent package as "optional module unavailable"#2545
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:optional-import-missing-parent

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

_optional_cuda_import() decides whether a ModuleNotFoundError means "the optional
module is not installed"
(return None) or "the module is installed but one of its
dependencies is broken"
(re-raise), by comparing err.name against the requested name:

    except ModuleNotFoundError as err:
        if err.name != fully_qualified_modname:
            raise
        return None

That comparison misses the most common case. ModuleNotFoundError.name is the
outermost missing name, so an absent parent package reports the parent:

>>> importlib.import_module("cuda.bindings.nvjitlink").__name__   # cuda-bindings not installed
ModuleNotFoundError: No module named 'cuda.bindings'      # err.name == "cuda.bindings"

Reproduced directly, with only cuda-pathfinder importable:

>>> from cuda.pathfinder._optional_cuda_import import _optional_cuda_import
>>> _optional_cuda_import("cuda.bindings.nvjitlink")
Traceback (most recent call last):
  ...
ModuleNotFoundError: No module named 'cuda.bindings'

The target module is exactly as unavailable as it would be if only the leaf were missing,
but the exception escapes instead of returning None.

Why it matters

Both callers today are in cuda.core, where cuda-bindings is an optional dependency
cuda_core/pyproject.toml lists only cuda-pathfinder and numpy as required, with
cuda-bindings in the cu12/cu13 extras — and a None return selects a documented
fallback:

  • cuda_core/cuda/core/_linker.pyx:687 → falls back to the driver cuLink* APIs
  • cuda_core/cuda/core/_program.pyx:669 → falls back to NVRTC-only compilation

So the escaping exception defeats both fallbacks in precisely the configuration they exist
for: cuda-core installed without the bindings extra.

Fix

Accept a missing ancestor package as "unavailable" as well, via a small helper that
requires a real dotted-path boundary (startswith(missing + ".")), so a sibling that
merely shares a string prefix is still re-raised. The "don't mask a broken dependency"
half of the original intent is untouched.

Tests

Three added to cuda_pathfinder/tests/test_optional_cuda_import.py:

  • ..._returns_none_when_parent_package_missing — stubs the exact ModuleNotFoundError
    CPython raises. Fails on main.
  • ..._returns_none_for_a_really_uninstalled_package — no stub at all, goes through the
    real import machinery with a name that cannot exist. Fails on main.
  • ..._reraises_for_a_string_prefix_that_is_not_an_ancestor — pins the dotted-boundary
    check (cuda.bindings is a string prefix of cuda.bindings_extra, not its parent).
    Passes on main too, on purpose.

The existing ..._reraises_nested_module_not_found test does not cover this case: it uses
an unrelated name (not_a_real_dependency), never an ancestor.

Verified: 2 of the 3 fail against upstream/main, all pass with the change, and the rest
of cuda_pathfinder/tests has the same pass/fail set as main. ruff check,
ruff format --check, and mypy (the pre-commit mypy-pathfinder invocation) are clean.

_optional_cuda_import() decides whether a ModuleNotFoundError means "the
optional module is not installed" (return None) or "the module is installed
but one of its dependencies is broken" (re-raise) by comparing err.name
against the requested name.

That comparison misses the most common case. ModuleNotFoundError.name is the
outermost missing name, so `import cuda.bindings.nvjitlink` in an environment
without cuda-bindings raises with name == "cuda.bindings", not
"cuda.bindings.nvjitlink". The exception is therefore re-raised, even though
the target module is exactly as unavailable as it would be if only the leaf
were missing.

Reproduced with only cuda-pathfinder importable:

    >>> from cuda.pathfinder._optional_cuda_import import _optional_cuda_import
    >>> _optional_cuda_import("cuda.bindings.nvjitlink")
    ModuleNotFoundError: No module named 'cuda.bindings'

Both callers today are in cuda.core, where cuda-bindings is an optional
dependency (only in the cu12/cu13 extras) and a None return selects a
documented fallback: _linker.pyx falls back to the driver cuLink* APIs and
_program.pyx to NVRTC-only compilation. The escaping exception defeats both
fallbacks in precisely the configuration they exist for.

Accept a missing ancestor package as "unavailable" too, via a small helper
that requires a real dotted-path boundary, so a sibling with a common string
prefix is still re-raised. An unrelated missing dependency keeps re-raising,
unchanged.
@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.pathfinder Everything related to the cuda.pathfinder module label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.pathfinder Everything related to the cuda.pathfinder module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant