From 0a94476aa231259a6bde27b4cd10aa17eeb0f163 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Sat, 8 Aug 2026 17:59:06 -0700 Subject: [PATCH] fix(core): don't crash `import cuda.core` on a non-integer opt-out value `cuda/core/__init__.py` reads `CUDA_CORE_DONT_FIX_TAB_COMPLETION` with a bare `int(os.environ.get(..., "0"))` at import time. `int()` raises for any value that is not a base-10 integer, and `os.environ.get` returns the empty string (not the `"0"` default) when the variable is set but empty, so: export CUDA_CORE_DONT_FIX_TAB_COMPLETION= python -c "import cuda.core" ValueError: invalid literal for int() with base 10: '' Clearing a variable with `export VAR=` is the usual way to neutralize it in a shell profile, a Dockerfile, or a CI job spec, and `=true` / `=yes` are the obvious guesses for a boolean-looking opt-out. All of them make the whole package unimportable, which is a hard failure for a knob whose only purpose is to skip an optional `rlcompleter` patch. Parse the value leniently instead. Integer values keep their existing meaning (non-zero opts out, so `0` and `00` still install the patch), while a non-integer, non-empty value is honored as an opt-out rather than being silently ignored. Unset and empty/whitespace-only both mean "not set". Also document the variable, which was not listed on the environment variables page, and drop the stale "only installed in interactive mode" comment: the interactivity gate was intentionally removed in #2055 ("Always install the monkeypatch"), so the patch has been unconditional since then. The new parametrized test asserts the resulting behavior for eight values; four of them ("", " ", "true", "yes") fail on main because the subprocess exits non-zero with the ValueError above. --- cuda_core/cuda/core/__init__.py | 13 +++-- .../docs/source/environment_variables.rst | 7 +++ cuda_core/tests/test_rlcompleter_patch.py | 58 +++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/cuda_core/cuda/core/__init__.py b/cuda_core/cuda/core/__init__.py index b9a36e3dee7..7864ae794ca 100644 --- a/cuda_core/cuda/core/__init__.py +++ b/cuda_core/cuda/core/__init__.py @@ -36,12 +36,17 @@ def _patch_rlcompleter_for_cython_properties() -> None: # which rlcompleter's narrow isinstance(..., property) check misses; the # fallback getattr() then invokes the descriptor and any non-AttributeError # it raises kills tab completion. Extend that isinstance check to also - # match getset_descriptor / member_descriptor. Only installed in - # interactive mode so library users running scripts see no global - # rlcompleter side effect. + # match getset_descriptor / member_descriptor. Installed unconditionally + # (the patch is scoped to the rlcompleter module, so non-interactive users + # only pay for the import). import os - if int(os.environ.get("CUDA_CORE_DONT_FIX_TAB_COMPLETION", "0")): + raw_opt_out = os.environ.get("CUDA_CORE_DONT_FIX_TAB_COMPLETION", "").strip() + try: + opt_out = int(raw_opt_out) != 0 + except ValueError: + opt_out = raw_opt_out != "" + if opt_out: # Explicit opt-out for users who don't want the global rlcompleter # side effect, even in an interactive session. return diff --git a/cuda_core/docs/source/environment_variables.rst b/cuda_core/docs/source/environment_variables.rst index b9201abc505..b7e4418bb58 100644 --- a/cuda_core/docs/source/environment_variables.rst +++ b/cuda_core/docs/source/environment_variables.rst @@ -24,3 +24,10 @@ Runtime Environment Variables warnings about CUDA major version mismatches between ``cuda-bindings`` and the installed driver. This warning occurs when ``cuda-bindings`` was built for a newer CUDA major version than the installed driver supports. + +- ``CUDA_CORE_DONT_FIX_TAB_COMPLETION`` : When set to 1, ``import cuda.core`` + does not patch the standard library's :mod:`rlcompleter` module. The patch + works around a CPython bug (fixed in Python 3.13.13, 3.14.6 and 3.15) that + makes tab completion fail on Cython properties, and it changes global + interpreter state; set this variable to opt out. Unset, empty, and ``0`` + leave the patch enabled; any other value disables it. diff --git a/cuda_core/tests/test_rlcompleter_patch.py b/cuda_core/tests/test_rlcompleter_patch.py index 68bd7b6e4f7..50283e62a31 100644 --- a/cuda_core/tests/test_rlcompleter_patch.py +++ b/cuda_core/tests/test_rlcompleter_patch.py @@ -107,3 +107,61 @@ def test_opt_out_env_var_disables_patch_even_when_interactive(): result = _run_probe(pythoninspect=True, opt_out=True) assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}" assert "crash: RuntimeError" in result.stdout, result.stdout + + +# Imports cuda.core and reports whether the rlcompleter patch was installed. +# No CUDA device is needed: the opt-out is evaluated at import time. The +# stdlib rlcompleter module has no `property` attribute of its own, so its +# presence is exactly the signal that the patch ran. +_OPT_OUT_PROBE_SCRIPT = textwrap.dedent(""" + import rlcompleter + + import cuda.core # noqa: F401 + + print(f"patched: {hasattr(rlcompleter, 'property')}") +""") + + +@pytest.mark.agent_authored(model="claude-opus-5") +@pytest.mark.parametrize( + ("value", "expect_patched"), + [ + # Empty / whitespace-only means "not set": `export VAR=` is the usual + # way to neutralize a variable in a shell profile or container spec. + ("", True), + (" ", True), + # Integer values keep their long-standing meaning. + ("0", True), + ("00", True), + ("1", False), + ("2", False), + # Non-integer values are honored as an opt-out. + ("true", False), + ("yes", False), + ], +) +def test_opt_out_env_var_values(value, expect_patched): + """`CUDA_CORE_DONT_FIX_TAB_COMPLETION` must never break `import cuda.core`. + + The opt-out used to be read with a bare `int(...)` at import time, so any + value that is not a base-10 integer -- including the empty string -- raised + `ValueError: invalid literal for int() with base 10: ''` out of + `cuda/core/__init__.py` and made the package unimportable. + """ + env = os.environ.copy() + env.pop("PYTHONPATH", None) + env["CUDA_CORE_DONT_FIX_TAB_COMPLETION"] = value + # Run from a neutral directory so a source tree next to the test run + # cannot shadow the installed package (see _run_probe). + with tempfile.TemporaryDirectory() as tmpdir: + result = subprocess.run( # noqa: S603 + [sys.executable, "-c", _OPT_OUT_PROBE_SCRIPT], + capture_output=True, + text=True, + env=env, + check=False, + stdin=subprocess.DEVNULL, + cwd=tmpdir, + ) + assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}" + assert result.stdout.strip() == f"patched: {expect_patched}", result.stdout