From c4536d5261498e159f9ba32ddd7e4464fe6cc9ac Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:04:11 +0200 Subject: [PATCH 01/10] Set self._backend to cupy correctly and improved check_backend --- src/cunumpy/xp.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index 36d3b9c..2241a39 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -30,13 +30,16 @@ def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleTy try: import cupy as cp + self._backend = "cupy" return cp except ImportError: if verbose: - print("CuPy not available.") + print("CuPy not available. Falling back to NumPy.") + self._backend = "numpy" return np import numpy as np_mod + self._backend = "numpy" return np_mod def __init_post__(self, verbose: bool = False) -> None: @@ -115,7 +118,7 @@ def synchronize() -> None: def to_numpy(array: Any) -> np.ndarray: """Convert an array to a NumPy array.""" - if hasattr(array, "get"): + if get_backend(array) == "cupy": return array.get() return np.asarray(array) From 383ef876bcb4a329519c6ac4ff68aac40729be2c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:05:02 +0200 Subject: [PATCH 02/10] Added __version__ --- src/cunumpy/__init__.py | 8 ++++++++ src/cunumpy/__init__.pyi | 1 + 2 files changed, 9 insertions(+) diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index c6592bd..15c4636 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,4 +1,6 @@ # cunumpy/__init__.py +from importlib.metadata import PackageNotFoundError, version + from . import xp from .xp import ( get_backend, @@ -12,8 +14,14 @@ use_backend, ) +try: + __version__ = version("cunumpy") +except PackageNotFoundError: + __version__ = "0.0.0+unknown" + __all__ = [ "xp", + "__version__", "to_numpy", "to_cupy", "to_cunumpy", diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index 2a4fb33..b30a97b 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -22,3 +22,4 @@ def synchronize() -> None: ... numpy_backend: bool cupy_backend: bool +__version__: str From 3a6e908ae916fc30d9de8df13b362ab928cb0a32 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:05:12 +0200 Subject: [PATCH 03/10] Added tests/unit/test_cunumpy.py --- tests/unit/test_cunumpy.py | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/unit/test_cunumpy.py b/tests/unit/test_cunumpy.py index 8f07ed5..6413af7 100644 --- a/tests/unit/test_cunumpy.py +++ b/tests/unit/test_cunumpy.py @@ -2,6 +2,7 @@ import pytest import cunumpy as xp +import cunumpy.xp as cxp # the internal submodule, to inspect array_backend directly def test_to_numpy(): @@ -56,3 +57,70 @@ def test_backend_bools(): with xp.use_backend("numpy"): assert xp.numpy_backend is True assert xp.cupy_backend is False + + +def test_version_is_own_package_version(): + # __version__ must resolve to cunumpy's own version, not be silently + # proxied to the active backend module's __version__ via __getattr__. + assert isinstance(xp.__version__, str) + assert xp.__version__ != "" + assert xp.__version__ != np.__version__ + + +def test_set_backend_cupy_fallback_reports_effective_backend(): + """array_backend.backend must reflect what actually loaded, not what was requested.""" + try: + import cupy # noqa: F401 + + cupy_installed = True + except ImportError: + cupy_installed = False + + xp.set_backend("cupy") + + if cupy_installed: + # Only true in CI on MPCDF, where CuPy is actually available. + assert cxp.array_backend.backend == "cupy" + assert xp.cupy_backend is True + else: + # No silent lie: requesting cupy without it installed must fall + # back to numpy *and* report "numpy", not "cupy". + assert cxp.array_backend.backend == "numpy" + assert xp.numpy_backend is True + assert xp.cupy_backend is False + + xp.set_backend("numpy") + + +def test_use_backend_cupy_fallback_restores_correctly(): + try: + import cupy # noqa: F401 + + pytest.skip("CuPy is installed; fallback behaviour is not exercised here") + except ImportError: + pass + + assert cxp.array_backend.backend == "numpy" + + with xp.use_backend("cupy"): + # Falls back to numpy since CuPy isn't available here. + assert cxp.array_backend.backend == "numpy" + + # And the previous state is restored afterwards. + assert cxp.array_backend.backend == "numpy" + + +def test_to_numpy_does_not_misdetect_get_method_as_gpu_array(): + """Objects exposing a `.get` method (e.g. dict-like objects) must not be + mistaken for CuPy arrays just because they happen to have a `.get`.""" + + class MappingLike: + def get(self, key, default=None): + raise AssertionError(".get() should not be called for non-cupy objects") + + def __array__(self): + return np.array([1, 2, 3]) + + arr = xp.to_numpy(MappingLike()) + assert isinstance(arr, np.ndarray) + assert np.array_equal(arr, [1, 2, 3]) From daef0a5231104481f75ac7f03c465ecd62f61e11 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:14:40 +0200 Subject: [PATCH 04/10] Removed unneccesary post_init --- src/cunumpy/xp.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index 2241a39..c399f37 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -42,13 +42,6 @@ def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleTy self._backend = "numpy" return np_mod - def __init_post__(self, verbose: bool = False) -> None: - # This is now redundant but kept for compatibility if called - self._xp = self._load_backend(self._backend, verbose) - assert isinstance(self._xp, ModuleType) - if verbose: - print(f"Using {self._xp.__name__} backend.") - @property def backend(self) -> BackendType: return self._backend @@ -73,15 +66,12 @@ def use_backend(self, backend: BackendType) -> Generator[None, None, None]: self._xp = old_xp -# TODO: Make this configurable via environment variable or config file. array_backend = ArrayBackend( backend=( "cupy" if os.getenv("ARRAY_BACKEND", "numpy").lower() == "cupy" else "numpy" ), verbose=False, ) -# Re-run initialization logic properly after backend selection -array_backend.__init_post__(verbose=False) def use_backend(backend: BackendType) -> Generator[None, None, None]: From 1c052ecd6dcc716ee0e760f7f011d31dee4cd7f5 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:43:31 +0200 Subject: [PATCH 05/10] assert --> Raise, added docstring --- src/cunumpy/__init__.py | 18 ++++++++++-------- src/cunumpy/__init__.pyi | 1 + src/cunumpy/xp.py | 22 ++++++++++++++++++---- tests/unit/test_cunumpy.py | 22 ++++++++++++++++++++++ 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index 15c4636..81875ad 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -7,6 +7,7 @@ is_cpu, is_gpu, set_backend, + set_device, synchronize, to_cunumpy, to_cupy, @@ -20,19 +21,20 @@ __version__ = "0.0.0+unknown" __all__ = [ - "xp", "__version__", - "to_numpy", - "to_cupy", - "to_cunumpy", + "cupy_backend", "get_backend", - "is_gpu", "is_cpu", - "use_backend", + "is_gpu", + "numpy_backend", "set_backend", + "set_device", "synchronize", - "numpy_backend", - "cupy_backend", + "to_cunumpy", + "to_cupy", + "to_numpy", + "use_backend", + "xp", ] diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index b30a97b..b945d93 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -18,6 +18,7 @@ def is_cpu(array: Any) -> bool: ... @contextmanager def use_backend(backend: str) -> Generator[None, None, None]: ... def set_backend(backend: str) -> None: ... +def set_device(device_id: int) -> None: ... def synchronize() -> None: ... numpy_backend: bool diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index c399f37..26dc845 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -9,15 +9,21 @@ class ArrayBackend: + """Holds the process-wide active backend (NumPy or CuPy). + + Not thread-safe: `set_backend`/`use_backend` mutate this single shared + instance in place, so concurrent code (threads, async tasks) switching + backends independently will race. Safe for the typical single-threaded + script/notebook usage this library targets. + """ + def __init__( self, backend: BackendType = "numpy", verbose: bool = False, ) -> None: - assert backend.lower() in [ - "numpy", - "cupy", - ], "Array backend must be either 'numpy' or 'cupy'." + if backend.lower() not in ("numpy", "cupy"): + raise ValueError("Array backend must be either 'numpy' or 'cupy'.") self._backend: BackendType = "cupy" if backend.lower() == "cupy" else "numpy" self._xp: ModuleType = np # Placeholder @@ -95,6 +101,14 @@ def _numpy_backend() -> bool: return array_backend.backend == "numpy" +def set_device(device_id: int) -> None: + """Select the active CUDA device for the current process (no-op on NumPy).""" + if array_backend.backend == "cupy": + import cupy as cp + + cp.cuda.Device(device_id).use() + + def synchronize() -> None: """Wait for all kernels in all streams on current device to complete.""" if array_backend.backend == "cupy": diff --git a/tests/unit/test_cunumpy.py b/tests/unit/test_cunumpy.py index 6413af7..7b89298 100644 --- a/tests/unit/test_cunumpy.py +++ b/tests/unit/test_cunumpy.py @@ -124,3 +124,25 @@ def __array__(self): arr = xp.to_numpy(MappingLike()) assert isinstance(arr, np.ndarray) assert np.array_equal(arr, [1, 2, 3]) + + +def test_invalid_backend_raises_value_error(): + with pytest.raises(ValueError): + cxp.ArrayBackend(backend="tensorflow") + + +def test_set_device_is_noop_on_numpy(): + with xp.use_backend("numpy"): + # Must not raise even though there's no GPU to select on the CPU backend. + xp.set_device(0) + + +def test_set_device_selects_cuda_device(): + try: + import cupy as cp + except ImportError: + pytest.skip("CuPy not installed") + + with xp.use_backend("cupy"): + xp.set_device(0) + assert cp.cuda.Device().id == 0 From 365cc657a8dd83e600efff70c248e1d2b893aaf3 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:44:31 +0200 Subject: [PATCH 06/10] Updated version number --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e5c7bfa..fea3ee8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ] [project] name = "cunumpy" -version = "0.1.2" +version = "0.1.3" description = "Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`." readme = "README.md" keywords = [ "python" ] From c137c41616bce69f40d5a7fbabe6dcfd1d33bac7 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:54:30 +0200 Subject: [PATCH 07/10] Updated CI and changelog + small fixes --- .github/workflows/static_analysis.yml | 3 +-- .github/workflows/testing.yml | 11 +++++++--- CHANGELOG.md | 16 ++++++++++++++ src/cunumpy/__init__.pyi | 4 ++-- src/cunumpy/xp.py | 21 +++++++++++++++---- tests/unit/test_cunumpy.py | 30 +++++++++++++++++++++++++++ tests/unit/test_numpy.py | 1 - 7 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 61b3cb6..151b4bb 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -78,7 +78,6 @@ jobs: ruff: runs-on: ubuntu-latest - continue-on-error: true steps: - name: Checkout the code uses: actions/checkout@v4 @@ -86,7 +85,7 @@ jobs: - name: Linting with ruff run: | pip install ruff - ruff check src/ || true + ruff check src/ pylint: runs-on: ubuntu-latest diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0c86b54..2b15a25 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -14,6 +14,11 @@ jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.10", "3.13"] + steps: # Checkout the repository - name: Checkout code @@ -23,16 +28,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10' # Adjust as needed + python-version: ${{ matrix.python-version }} # Cache pip dependencies - name: Cache pip uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }} restore-keys: | - ${{ runner.os }}-pip- + ${{ runner.os }}-pip-${{ matrix.python-version }}- - name: Install dependencies run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index ec7876c..4634055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to the `cunumpy` library are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.3] - 2026-07-31 + +### Added +- `xp.set_device(device_id)`: Select the active CUDA device for the current process (no-op on the NumPy backend). +- `xp.__version__`: Reports the installed `cunumpy` package version. + +### Fixed +- `ArrayBackend` no longer silently reports `"cupy"` as the active backend when CuPy was requested but is unavailable; it now correctly falls back to reporting `"numpy"` so `xp.cupy_backend`/`xp.numpy_backend` reflect what actually loaded. +- `to_numpy()` no longer misdetects CPU objects that merely expose a `.get` method (e.g. dict-like objects) as CuPy arrays; it now checks `get_backend()` instead of `hasattr(array, "get")`. +- Invalid backend names now raise `ValueError` instead of relying on a bare `assert`, which was previously stripped under `python -O`. + +### Changed +- Removed the redundant `ArrayBackend.__init_post__` double-initialization path. +- `ArrayBackend` documents that it is not thread-safe (global mutable backend state). +- CI now runs the test suite across a Python 3.8/3.10/3.13 matrix instead of only 3.10, and `ruff` is now an enforced check rather than advisory. + ## [0.1.2] - 2026-05-27 ### Added diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index ac50708..8379680 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -7,7 +7,7 @@ from typing import Any, Generator import numpy as np from numpy import * -from . import xp +from . import xp as xp def to_numpy(array: Any) -> np.ndarray: ... def to_cupy(array: Any) -> Any: ... @@ -17,7 +17,7 @@ def get_backend(array: Any) -> str: ... def is_gpu(array: Any) -> bool: ... def is_cpu(array: Any) -> bool: ... @contextmanager -def use_backend(backend: str) -> Generator[None, None, None]: ... +def use_backend(backend: str) -> Generator[None]: ... def set_backend(backend: str) -> None: ... def set_device(device_id: int) -> None: ... def synchronize() -> None: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index fcc4eef..718cea1 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -1,4 +1,5 @@ import os +import warnings from contextlib import contextmanager from types import ModuleType from typing import TYPE_CHECKING, Any, Generator, Literal @@ -23,7 +24,7 @@ def cupy_available() -> bool: # Check if a GPU is available _CUPY_AVAILABLE_CACHE = cp.is_available() return _CUPY_AVAILABLE_CACHE - except (ImportError, Exception): + except Exception: # noqa: BLE001 - tolerate any driver/runtime failure _CUPY_AVAILABLE_CACHE = False return False @@ -60,7 +61,9 @@ def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleTy return cp else: if verbose: - print("CuPy not available or not functional. Falling back to NumPy.") + print( + "CuPy not available or not functional. Falling back to NumPy." + ) self._backend = "numpy" return np import numpy as np_mod @@ -68,6 +71,9 @@ def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleTy self._backend = "numpy" return np_mod + def __repr__(self) -> str: + return f"ArrayBackend(backend={self._backend!r}, module={self._xp.__name__!r})" + @property def backend(self) -> BackendType: return self._backend @@ -136,8 +142,15 @@ def synchronize() -> None: import cupy as cp cp.cuda.Device().synchronize() - except (ImportError, AttributeError): + except ImportError: pass + except AttributeError as e: + warnings.warn( + f"CuPy synchronize() failed unexpectedly, this may indicate a " + f"CuPy API mismatch: {e}", + RuntimeWarning, + stacklevel=2, + ) def to_numpy(array: Any) -> np.ndarray: @@ -184,7 +197,7 @@ def is_cpu(array: Any) -> bool: # TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime. # This allows us to use autocompletion for xp (i.e., numpy/cupy) as if numpy was imported. if TYPE_CHECKING: - import numpy as xp + import numpy as xp # noqa: F401 - type-checker-only alias for autocompletion else: # Use module-level __getattr__ for dynamic xp (Python 3.7+) def __getattr__(name): diff --git a/tests/unit/test_cunumpy.py b/tests/unit/test_cunumpy.py index 7b89298..4c3a8b1 100644 --- a/tests/unit/test_cunumpy.py +++ b/tests/unit/test_cunumpy.py @@ -1,3 +1,5 @@ +import sys + import numpy as np import pytest @@ -146,3 +148,31 @@ def test_set_device_selects_cuda_device(): with xp.use_backend("cupy"): xp.set_device(0) assert cp.cuda.Device().id == 0 + + +def test_array_backend_repr_reports_active_backend(): + with xp.use_backend("numpy"): + assert ( + repr(cxp.array_backend) == "ArrayBackend(backend='numpy', module='numpy')" + ) + + +def test_synchronize_warns_on_attribute_error(monkeypatch): + """An AttributeError from CuPy's synchronize call (e.g. API mismatch) + must surface as a warning, not be swallowed silently.""" + + class BrokenDevice: + def synchronize(self): + raise AttributeError("simulated CuPy API mismatch") + + class FakeCupy: + cuda = type("cuda", (), {"Device": staticmethod(lambda: BrokenDevice())}) + + monkeypatch.setitem(sys.modules, "cupy", FakeCupy) + monkeypatch.setattr(cxp.array_backend, "_backend", "cupy") + + try: + with pytest.warns(RuntimeWarning, match="CuPy API mismatch"): + xp.synchronize() + finally: + monkeypatch.setattr(cxp.array_backend, "_backend", "numpy") diff --git a/tests/unit/test_numpy.py b/tests/unit/test_numpy.py index 30f00d7..2247f10 100644 --- a/tests/unit/test_numpy.py +++ b/tests/unit/test_numpy.py @@ -1,5 +1,4 @@ import numpy as np -import pytest import cunumpy as xp From aa09be310034c760e99acf8f002a6b4d3420473e Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:54:49 +0200 Subject: [PATCH 08/10] Formatting --- README.md | 3 ++- docs/source/quickstart.md | 2 +- tests/unit/test_benchmarks.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0fe4acd..9fd85b7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ export ARRAY_BACKEND=cupy ```python import cunumpy as xp -arr = xp.array([1,2]) + +arr = xp.array([1, 2]) print(type(arr)) print(xp.__version__) diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md index 773224c..99970f8 100644 --- a/docs/source/quickstart.md +++ b/docs/source/quickstart.md @@ -41,7 +41,7 @@ xp.set_backend("cupy") # Scoped backend switching with xp.use_backend("numpy"): arr_cpu = xp.zeros(10) - print(xp.is_cpu(arr_cpu)) # True + print(xp.is_cpu(arr_cpu)) # True # Explicit conversion arr_np = xp.to_numpy(arr) diff --git a/tests/unit/test_benchmarks.py b/tests/unit/test_benchmarks.py index f9a9e35..bf856fb 100644 --- a/tests/unit/test_benchmarks.py +++ b/tests/unit/test_benchmarks.py @@ -42,7 +42,7 @@ def test_benchmark_matmul(): print(f"\n[Benchmark] Size: {size}x{size}") print(f"NumPy time: {t_np:.4f}s") print(f"CuPy time: {t_cp:.4f}s") - print(f"Speedup: {t_np/t_cp:.2f}x") + print(f"Speedup: {t_np / t_cp:.2f}x") # On a real GPU (A100/A30), CuPy should be significantly faster # We use a conservative threshold of 1.5x for the test to pass on various hardware @@ -76,5 +76,5 @@ def test_benchmark_fft(): print(f"\n[Benchmark] FFT Size: {size}") print(f"NumPy time: {t_np:.4f}s") print(f"CuPy time: {t_cp:.4f}s") - print(f"Speedup: {t_np/t_cp:.2f}x") + print(f"Speedup: {t_np / t_cp:.2f}x") assert t_cp < t_np From b61fc82df6ef537c10623b4c12db12ab3acb4e5a Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 08:57:10 +0200 Subject: [PATCH 09/10] ruff check fixes --- tests/unit/test_benchmarks.py | 5 ++--- tests/unit/test_integration.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_benchmarks.py b/tests/unit/test_benchmarks.py index bf856fb..4f4e165 100644 --- a/tests/unit/test_benchmarks.py +++ b/tests/unit/test_benchmarks.py @@ -1,6 +1,5 @@ import time -import numpy as np import pytest import cunumpy as xp @@ -19,7 +18,7 @@ def test_benchmark_matmul(): b_np = xp.random.rand(size, size).astype(xp.float32) start_np = time.perf_counter() - c_np = a_np @ b_np + _ = a_np @ b_np # No sync needed for NumPy as it is synchronous end_np = time.perf_counter() t_np = end_np - start_np @@ -34,7 +33,7 @@ def test_benchmark_matmul(): xp.synchronize() start_cp = time.perf_counter() - c_cp = a_cp @ b_cp + _ = a_cp @ b_cp xp.synchronize() # CRITICAL for benchmarking GPU end_cp = time.perf_counter() t_cp = end_cp - start_cp diff --git a/tests/unit/test_integration.py b/tests/unit/test_integration.py index d13764f..2e1078c 100644 --- a/tests/unit/test_integration.py +++ b/tests/unit/test_integration.py @@ -38,6 +38,7 @@ def test_synchronize_logic(): a = xp.random.rand(100) xp.synchronize() assert xp.is_gpu(a) + assert isinstance(a, cp.ndarray) def test_fft_interop(): @@ -67,8 +68,9 @@ def test_mixed_backend_errors(): a_cpu = np.array([1, 2, 3]) a_gpu = xp.to_cupy(a_cpu) - # This should fail because you can't add CPU and GPU arrays directly - with pytest.raises(Exception): + # This should fail because you can't add CPU and GPU arrays directly. + # The exact exception type is NumPy/CuPy-version dependent, hence the broad catch. + with pytest.raises(Exception): # noqa: B017 _ = a_cpu + a_gpu # But to_cunumpy should fix it From 7bfe1ea1e5e00996a13fe19ce6322e8579ccdf37 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 31 Jul 2026 09:02:41 +0200 Subject: [PATCH 10/10] Fix cupy test, backend remained from old test --- tests/unit/test_integration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_integration.py b/tests/unit/test_integration.py index 2e1078c..9cb4486 100644 --- a/tests/unit/test_integration.py +++ b/tests/unit/test_integration.py @@ -73,8 +73,10 @@ def test_mixed_backend_errors(): with pytest.raises(Exception): # noqa: B017 _ = a_cpu + a_gpu - # But to_cunumpy should fix it - a_gpu_fixed = xp.to_cunumpy(a_cpu) + # But to_cunumpy should fix it: it must run inside the cupy backend context + # so it actually converts a_cpu to a CuPy array, not whatever the global + # backend happened to be left as by an earlier test. with xp.use_backend("cupy"): + a_gpu_fixed = xp.to_cunumpy(a_cpu) res = a_gpu + a_gpu_fixed assert xp.is_gpu(res)