Skip to content

Commit d62ff30

Browse files
mdboomleofang
andauthored
Fix tab completion (#2055)
* Fix tab completion * Fix tests * Always install the monkeypatch * Update release note * Apply suggestion from @leofang Co-authored-by: Leo Fang <leof@nvidia.com> * Fix test * Fix tests hanging on Windows --------- Co-authored-by: Leo Fang <leof@nvidia.com>
1 parent 73ab82f commit d62ff30

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

cuda_core/cuda/core/__init__.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,46 @@ def _import_versioned_module():
2828
del _import_versioned_module
2929

3030

31+
def _patch_rlcompleter_for_cython_properties():
32+
# TODO: This can be removed when Python 3.13 is our minimum-supported version:
33+
# https://github.com/python/cpython/pull/149577
34+
35+
# Cython @property on cdef class compiles to a C-level getset_descriptor,
36+
# which rlcompleter's narrow isinstance(..., property) check misses; the
37+
# fallback getattr() then invokes the descriptor and any non-AttributeError
38+
# it raises kills tab completion. Extend that isinstance check to also
39+
# match getset_descriptor / member_descriptor. Only installed in
40+
# interactive mode so library users running scripts see no global
41+
# rlcompleter side effect.
42+
import os
43+
44+
if int(os.environ.get("CUDA_CORE_DONT_FIX_TAB_COMPLETION", "0")):
45+
# Explicit opt-out for users who don't want the global rlcompleter
46+
# side effect, even in an interactive session.
47+
return
48+
49+
import rlcompleter
50+
from types import GetSetDescriptorType, MemberDescriptorType
51+
52+
# This works by overriding the `property` built-in with a custom subclass of
53+
# property, but only in the rlcompleter module. This subclass overrides the
54+
# `__instancecheck__` method to also return True for getset_descriptor and
55+
# member_descriptor types, which are what Cython uses for properties on cdef
56+
# classes.
57+
class _PatchedPropMeta(type):
58+
def __instancecheck__(cls, inst):
59+
return isinstance(inst, (property, GetSetDescriptorType, MemberDescriptorType))
60+
61+
class _PatchedProperty(metaclass=_PatchedPropMeta):
62+
pass
63+
64+
rlcompleter.property = _PatchedProperty
65+
66+
67+
_patch_rlcompleter_for_cython_properties()
68+
del _patch_rlcompleter_for_cython_properties
69+
70+
3171
from cuda.core import checkpoint, system, utils
3272
from cuda.core._context import Context, ContextOptions
3373
from cuda.core._device import Device

cuda_core/docs/source/release/1.0.0-notes.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,12 @@ Fixes and enhancements
357357
package size. Debug builds are now supported via
358358
``--config-settings=debug=true``.
359359
(`#1890 <https://github.com/NVIDIA/cuda-python/pull/1890>`__)
360+
- Fixed tab completion silently breaking in the CPython REPL on some
361+
``cuda.core`` objects (for example, hitting Tab after ``mr.`` for a
362+
``DeviceMemoryResource`` would produce no suggestions due to a CPython
363+
limitation interacting with Cython properties). When ``cuda.core`` is
364+
imported in an interactive session it now applies a small patch to the
365+
standard library REPL completer so tab completion works as expected.
366+
If you would rather not have ``cuda.core`` modify the REPL completer, set
367+
``CUDA_CORE_DONT_FIX_TAB_COMPLETION=1`` to opt out.
368+
(`#2053 <https://github.com/NVIDIA/cuda-python/issues/2053>`__)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""
5+
Tests for the rlcompleter monkeypatch installed by `cuda.core` in interactive
6+
sessions.
7+
8+
These tests reproduce the original bug report (NVIDIA/cuda-python#2053): tab
9+
completion on a non-IPC-enabled DeviceMemoryResource crashes because the
10+
Cython @property `allocation_handle` raises RuntimeError, and rlcompleter's
11+
narrow `isinstance(..., property)` check misses C-level getset_descriptor
12+
types and therefore invokes the descriptor.
13+
14+
The patch only installs in interactive mode, so each scenario is exercised in
15+
a fresh subprocess with a controlled combination of `PYTHONINSPECT` and
16+
`CUDA_CORE_DONT_FIX_TAB_COMPLETION`.
17+
"""
18+
19+
import os
20+
import subprocess
21+
import sys
22+
import tempfile
23+
import textwrap
24+
25+
import pytest
26+
27+
from cuda.core import Device
28+
29+
30+
def _gpu_with_mempool_or_skip():
31+
"""Skip when no GPU or no mempool support — test mirrors the bug repro."""
32+
if len(Device.get_all_devices()) == 0:
33+
pytest.skip("Test requires a CUDA device")
34+
dev = Device(0)
35+
if not dev.properties.memory_pools_supported:
36+
pytest.skip("Device 0 does not support mempool operations")
37+
38+
39+
# Probe script: reproduces the bug-report repro literally, then runs
40+
# rlcompleter against `mr` and reports the outcome.
41+
_PROBE_SCRIPT = textwrap.dedent("""
42+
import rlcompleter
43+
from cuda.core import Device, DeviceMemoryResource
44+
45+
dev = Device(0)
46+
dev.set_current()
47+
mr = DeviceMemoryResource(dev)
48+
assert not mr.is_ipc_enabled, "test setup: mr should not be IPC-enabled"
49+
50+
completer = rlcompleter.Completer({"mr": mr})
51+
try:
52+
matches = completer.attr_matches("mr.")
53+
except Exception as exc:
54+
print(f"crash: {type(exc).__name__}: {exc}")
55+
else:
56+
print(f"ok: {len(matches)} matches")
57+
print(f"allocation_handle: {'mr.allocation_handle' in matches}")
58+
""")
59+
60+
61+
def _run_probe(*, pythoninspect: bool, opt_out: bool = False) -> subprocess.CompletedProcess:
62+
env = os.environ.copy()
63+
# Don't let parent-environment values bleed into the subprocess.
64+
env.pop("CUDA_CORE_DONT_FIX_TAB_COMPLETION", None)
65+
# Drop PYTHONPATH so the subprocess can't find a source-tree cuda.core
66+
# via an inherited path entry; we want it to import the installed wheel.
67+
env.pop("PYTHONPATH", None)
68+
if opt_out:
69+
env["CUDA_CORE_DONT_FIX_TAB_COMPLETION"] = "1"
70+
# `python -c` puts the parent's CWD at the head of sys.path. If pytest is
71+
# run from `cuda_core/` (which contains a `cuda/core/` source tree), that
72+
# source tree shadows the installed package. Run the subprocess from a
73+
# neutral temp dir to avoid this.
74+
with tempfile.TemporaryDirectory() as tmpdir:
75+
return subprocess.run( # noqa: S603
76+
[sys.executable, "-c", _PROBE_SCRIPT],
77+
capture_output=True,
78+
text=True,
79+
env=env,
80+
check=False,
81+
# PYTHONINSPECT keeps the interpreter alive after `-c`; close stdin
82+
# so the implicit REPL exits immediately.
83+
stdin=subprocess.DEVNULL,
84+
cwd=tmpdir,
85+
)
86+
87+
88+
def test_patched_completion_succeeds_on_non_ipc_resource():
89+
"""With the patch installed (PYTHONINSPECT=1), tab completion must not
90+
crash and `mr.allocation_handle` must appear in the matches."""
91+
_gpu_with_mempool_or_skip()
92+
93+
result = _run_probe(pythoninspect=True)
94+
assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}"
95+
assert result.stdout.startswith("ok:"), result.stdout
96+
assert "allocation_handle: True" in result.stdout, result.stdout
97+
98+
99+
def test_opt_out_env_var_disables_patch_even_when_interactive():
100+
"""`CUDA_CORE_DONT_FIX_TAB_COMPLETION=1` must short-circuit before the
101+
interactive check, so the bug reproduces again even under PYTHONINSPECT."""
102+
_gpu_with_mempool_or_skip()
103+
104+
result = _run_probe(pythoninspect=True, opt_out=True)
105+
assert result.returncode == 0, f"stderr: {result.stderr}\nstdout: {result.stdout}"
106+
assert "crash: RuntimeError" in result.stdout, result.stdout

0 commit comments

Comments
 (0)