Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions cuda_core/cuda/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions cuda_core/docs/source/environment_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
58 changes: 58 additions & 0 deletions cuda_core/tests/test_rlcompleter_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading