From 2bc3a13ed2bfd1c4954740e345df78b7e2e59264 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:18:17 +0200 Subject: [PATCH 01/13] Added to_numpy() and to_cupy() --- README.md | 6 ++++++ src/cunumpy/__init__.py | 3 ++- src/cunumpy/__init__.pyi | 6 ++++++ src/cunumpy/xp.py | 23 ++++++++++++++++++++++- tests/unit/test_app.py | 31 ++++++++++++++++++++++++++++++- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index debfdf1..f4d0a99 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ arr = xp.array([1,2]) print(type(arr)) print(xp.__version__) + +# Convert to NumPy +arr_np = xp.to_numpy(arr) + +# Convert to CuPy (requires CuPy installed) +# arr_cp = xp.to_cupy(arr) ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index 32c5da8..fa79607 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,7 +1,8 @@ # cunumpy/__init__.py from . import xp +from .xp import to_cupy, to_numpy -__all__ = ["xp"] +__all__ = ["xp", "to_numpy", "to_cupy"] def __getattr__(name: str): diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index b5fe885..50bf9b8 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -1,7 +1,13 @@ # Stub file for Pylance/mypy: exposes all numpy symbols so that # `import cunumpy as xp` followed by `xp.` shows numpy completions. # At runtime the real __init__.py dispatches to numpy or cupy via __getattr__. +from typing import Any + +import numpy as np from numpy import * from numpy import __config__, __version__ from . import xp + +def to_numpy(array: Any) -> np.ndarray: ... +def to_cupy(array: Any) -> Any: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index 6438575..4dbff5d 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -1,6 +1,8 @@ import os from types import ModuleType -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal + +import numpy as np BackendType = Literal["numpy", "cupy"] @@ -56,6 +58,25 @@ def xp(self) -> ModuleType: verbose=False, ) + +def to_numpy(array: Any) -> np.ndarray: + """Convert an array to a NumPy array.""" + if hasattr(array, "get"): + return array.get() + + return np.asarray(array) + + +def to_cupy(array: Any) -> Any: + """Convert an array to a CuPy array.""" + try: + import cupy as cp + + return cp.asarray(array) + except ImportError: + raise ImportError("CuPy is not available.") + + # 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: diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index ce6b9d0..c4cee6c 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -1,4 +1,5 @@ import numpy as np +import pytest import cunumpy as xp @@ -17,10 +18,38 @@ def test_numpy_symbols_accessible(): This validates the runtime behaviour that the stub file (__init__.pyi) declares to Pylance so that `xp.` shows numpy completions in VS Code. """ - missing = [name for name in np.__all__ if not hasattr(xp, name)] + # Exclude our custom methods from the numpy check + custom_methods = ["to_numpy", "to_cupy", "xp"] + missing = [ + name + for name in np.__all__ + if not hasattr(xp, name) and name not in custom_methods + ] assert missing == [], f"Symbols not accessible via cunumpy: {missing}" +def test_to_numpy(): + arr = xp.array([1, 2, 3]) + # Even if it's already numpy, to_numpy should work + arr_np = xp.to_numpy(arr) + assert isinstance(arr_np, np.ndarray) + assert np.array_equal(arr_np, [1, 2, 3]) + + +def test_to_cupy_not_available(): + try: + import cupy + + pytest.skip("CuPy is installed, cannot test missing cupy error") + except ImportError: + pass + + arr = np.array([1, 2, 3]) + + with pytest.raises(ImportError): + xp.to_cupy(arr) + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From c18894b693c8cb91798282e4b98ebfb4d72bee7d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:23:15 +0200 Subject: [PATCH 02/13] Added to_cunumpy(), get_backend(), and is_gpu() --- README.md | 8 ++++++-- src/cunumpy/__init__.py | 4 ++-- src/cunumpy/__init__.pyi | 3 +++ src/cunumpy/xp.py | 18 ++++++++++++++++++ tests/unit/test_app.py | 22 +++++++++++++++++++++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f4d0a99..098fa4c 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,12 @@ print(xp.__version__) # Convert to NumPy arr_np = xp.to_numpy(arr) -# Convert to CuPy (requires CuPy installed) -# arr_cp = xp.to_cupy(arr) +# Convert to active backend +arr_xp = xp.to_cunumpy(arr) + +# Inspect backend +print(xp.get_backend(arr)) +print(xp.is_gpu(arr)) ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index fa79607..bc1c070 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,8 +1,8 @@ # cunumpy/__init__.py from . import xp -from .xp import to_cupy, to_numpy +from .xp import get_backend, is_gpu, to_cunumpy, to_cupy, to_numpy -__all__ = ["xp", "to_numpy", "to_cupy"] +__all__ = ["xp", "to_numpy", "to_cupy", "to_cunumpy", "get_backend", "is_gpu"] def __getattr__(name: str): diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index 50bf9b8..422295e 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -11,3 +11,6 @@ from . import xp def to_numpy(array: Any) -> np.ndarray: ... def to_cupy(array: Any) -> Any: ... +def to_cunumpy(array: Any) -> Any: ... +def get_backend(array: Any) -> str: ... +def is_gpu(array: Any) -> bool: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index 4dbff5d..d1ae69d 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -77,6 +77,24 @@ def to_cupy(array: Any) -> Any: raise ImportError("CuPy is not available.") +def to_cunumpy(array: Any) -> Any: + """Convert an array to the currently active backend.""" + if array_backend.backend == "cupy": + return to_cupy(array) + return to_numpy(array) + + +def get_backend(array: Any) -> BackendType: + """Return 'cupy' or 'numpy' depending on the array type.""" + module = getattr(type(array), "__module__", "") + return "cupy" if "cupy" in module else "numpy" + + +def is_gpu(array: Any) -> bool: + """Check if the array is stored on a GPU (CuPy).""" + return get_backend(array) == "cupy" + + # 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: diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index c4cee6c..10a8366 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -19,7 +19,14 @@ def test_numpy_symbols_accessible(): declares to Pylance so that `xp.` shows numpy completions in VS Code. """ # Exclude our custom methods from the numpy check - custom_methods = ["to_numpy", "to_cupy", "xp"] + custom_methods = [ + "to_numpy", + "to_cupy", + "to_cunumpy", + "get_backend", + "is_gpu", + "xp", + ] missing = [ name for name in np.__all__ @@ -50,6 +57,19 @@ def test_to_cupy_not_available(): xp.to_cupy(arr) +def test_to_cunumpy(): + arr = np.array([1, 2, 3]) + arr_xp = xp.to_cunumpy(arr) + # Backend is numpy in tests usually + assert isinstance(arr_xp, (np.ndarray, xp.ndarray)) + + +def test_get_backend_and_is_gpu(): + arr = np.array([1, 2, 3]) + assert xp.get_backend(arr) == "numpy" + assert xp.is_gpu(arr) is False + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From f67ff87f5654728b22f2b214f4926640cd5134ab Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:24:16 +0200 Subject: [PATCH 03/13] Added is_cpu() --- README.md | 1 + src/cunumpy/__init__.py | 12 ++++++++++-- src/cunumpy/__init__.pyi | 1 + src/cunumpy/xp.py | 5 +++++ tests/unit/test_app.py | 4 +++- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 098fa4c..9aeed8d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ arr_xp = xp.to_cunumpy(arr) # Inspect backend print(xp.get_backend(arr)) print(xp.is_gpu(arr)) +print(xp.is_cpu(arr)) ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index bc1c070..52e97de 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,8 +1,16 @@ # cunumpy/__init__.py from . import xp -from .xp import get_backend, is_gpu, to_cunumpy, to_cupy, to_numpy +from .xp import get_backend, is_cpu, is_gpu, to_cunumpy, to_cupy, to_numpy -__all__ = ["xp", "to_numpy", "to_cupy", "to_cunumpy", "get_backend", "is_gpu"] +__all__ = [ + "xp", + "to_numpy", + "to_cupy", + "to_cunumpy", + "get_backend", + "is_gpu", + "is_cpu", +] def __getattr__(name: str): diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index 422295e..d2ba054 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -14,3 +14,4 @@ def to_cupy(array: Any) -> Any: ... def to_cunumpy(array: Any) -> Any: ... def get_backend(array: Any) -> str: ... def is_gpu(array: Any) -> bool: ... +def is_cpu(array: Any) -> bool: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index d1ae69d..c5f42a1 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -95,6 +95,11 @@ def is_gpu(array: Any) -> bool: return get_backend(array) == "cupy" +def is_cpu(array: Any) -> bool: + """Check if the array is stored on a CPU (NumPy).""" + return get_backend(array) == "numpy" + + # 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: diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 10a8366..4ae8bef 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -25,6 +25,7 @@ def test_numpy_symbols_accessible(): "to_cunumpy", "get_backend", "is_gpu", + "is_cpu", "xp", ] missing = [ @@ -64,10 +65,11 @@ def test_to_cunumpy(): assert isinstance(arr_xp, (np.ndarray, xp.ndarray)) -def test_get_backend_and_is_gpu(): +def test_get_backend_and_is_gpu_cpu(): arr = np.array([1, 2, 3]) assert xp.get_backend(arr) == "numpy" assert xp.is_gpu(arr) is False + assert xp.is_cpu(arr) is True if __name__ == "__main__": From 665263645bb5cafabe94d0430b947aa17da19242 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:28:11 +0200 Subject: [PATCH 04/13] Added with xp.use_backend(numpy/cupy): --- README.md | 5 ++++ src/cunumpy/__init__.py | 11 ++++++++- src/cunumpy/__init__.pyi | 6 +++-- src/cunumpy/xp.py | 49 +++++++++++++++++++++++++++++++--------- tests/unit/test_app.py | 13 +++++++++++ 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9aeed8d..2d0e3c5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ arr_xp = xp.to_cunumpy(arr) print(xp.get_backend(arr)) print(xp.is_gpu(arr)) print(xp.is_cpu(arr)) + +# Temporarily switch backend +with xp.use_backend("numpy"): + # This code runs on CPU even if ARRAY_BACKEND=cupy + arr_cpu = xp.zeros(100) ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index 52e97de..2223218 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,6 +1,14 @@ # cunumpy/__init__.py from . import xp -from .xp import get_backend, is_cpu, is_gpu, to_cunumpy, to_cupy, to_numpy +from .xp import ( + get_backend, + is_cpu, + is_gpu, + to_cunumpy, + to_cupy, + to_numpy, + use_backend, +) __all__ = [ "xp", @@ -10,6 +18,7 @@ "get_backend", "is_gpu", "is_cpu", + "use_backend", ] diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index d2ba054..c52115f 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -1,11 +1,11 @@ # Stub file for Pylance/mypy: exposes all numpy symbols so that # `import cunumpy as xp` followed by `xp.` shows numpy completions. # At runtime the real __init__.py dispatches to numpy or cupy via __getattr__. -from typing import Any +from contextlib import contextmanager +from typing import Any, Generator import numpy as np from numpy import * -from numpy import __config__, __version__ from . import xp @@ -15,3 +15,5 @@ def to_cunumpy(array: Any) -> Any: ... 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]: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index c5f42a1..c2591f2 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -1,6 +1,7 @@ import os +from contextlib import contextmanager from types import ModuleType -from typing import TYPE_CHECKING, Any, Literal +from typing import TYPE_CHECKING, Any, Generator, Literal import numpy as np @@ -19,27 +20,31 @@ def __init__( ], "Array backend must be either 'numpy' or 'cupy'." self._backend: BackendType = "cupy" if backend.lower() == "cupy" else "numpy" + self._xp: ModuleType = np # Placeholder # Import numpy/cupy - if self.backend == "cupy": + self._xp = self._load_backend(self._backend, verbose) + + def _load_backend(self, backend: BackendType, verbose: bool = False) -> ModuleType: + if backend == "cupy": try: import cupy as cp - self._xp = cp + return cp except ImportError: if verbose: print("CuPy not available.") - self._backend = "numpy" - - if self.backend == "numpy": - import numpy as np + return np + import numpy as np_mod - self._xp = np - - assert isinstance(self.xp, ModuleType) + 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.") + print(f"Using {self._xp.__name__} backend.") @property def backend(self) -> BackendType: @@ -49,6 +54,21 @@ def backend(self) -> BackendType: def xp(self) -> ModuleType: return self._xp + @contextmanager + def use_backend(self, backend: BackendType) -> Generator[None, None, None]: + """Temporarily change the backend.""" + old_backend = self._backend + old_xp = self._xp + + self._backend = backend + self._xp = self._load_backend(backend) + + try: + yield + finally: + self._backend = old_backend + self._xp = old_xp + # TODO: Make this configurable via environment variable or config file. array_backend = ArrayBackend( @@ -57,6 +77,13 @@ def xp(self) -> ModuleType: ), 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]: + """Temporarily change the backend.""" + return array_backend.use_backend(backend) def to_numpy(array: Any) -> np.ndarray: diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 4ae8bef..6abcb31 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -26,6 +26,7 @@ def test_numpy_symbols_accessible(): "get_backend", "is_gpu", "is_cpu", + "use_backend", "xp", ] missing = [ @@ -72,6 +73,18 @@ def test_get_backend_and_is_gpu_cpu(): assert xp.is_cpu(arr) is True +def test_use_backend(): + # Initial backend should be numpy (default) in this test environment + assert "numpy" in xp.xp.__name__ + + with xp.use_backend("numpy"): + assert "numpy" in xp.xp.__name__ + arr = xp.zeros(10) + assert isinstance(arr, np.ndarray) + + assert "numpy" in xp.xp.__name__ + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From 7d27c65f835f131d93a81ec7ef711507de078653 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:30:21 +0200 Subject: [PATCH 05/13] Update version number to 0.1.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c519bfd..de9c369 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ] [project] name = "cunumpy" -version = "0.1.1" +version = "0.1.2" description = "Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cunumpy as xp`." readme = "README.md" keywords = [ "python" ] From 02b722459e054fb66393bc1851f7680df05454ee Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:33:16 +0200 Subject: [PATCH 06/13] Added xp.set_backend() --- README.md | 3 +++ src/cunumpy/__init__.py | 12 +++--------- src/cunumpy/__init__.pyi | 1 + src/cunumpy/xp.py | 12 +++++++++++- tests/unit/test_app.py | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2d0e3c5..ee5cb1d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ print(xp.is_cpu(arr)) with xp.use_backend("numpy"): # This code runs on CPU even if ARRAY_BACKEND=cupy arr_cpu = xp.zeros(100) + +# Set backend globally +xp.set_backend("cupy") ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index 2223218..ae3e2db 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,14 +1,7 @@ # cunumpy/__init__.py from . import xp -from .xp import ( - get_backend, - is_cpu, - is_gpu, - to_cunumpy, - to_cupy, - to_numpy, - use_backend, -) +from .xp import (get_backend, is_cpu, is_gpu, set_backend, to_cunumpy, to_cupy, + to_numpy, use_backend) __all__ = [ "xp", @@ -19,6 +12,7 @@ "is_gpu", "is_cpu", "use_backend", + "set_backend", ] diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index c52115f..6279d76 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -17,3 +17,4 @@ def is_gpu(array: Any) -> bool: ... def is_cpu(array: Any) -> bool: ... @contextmanager def use_backend(backend: str) -> Generator[None, None, None]: ... +def set_backend(backend: str) -> None: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index c2591f2..db67f11 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -86,6 +86,12 @@ def use_backend(backend: BackendType) -> Generator[None, None, None]: return array_backend.use_backend(backend) +def set_backend(backend: BackendType) -> None: + """Set the backend globally.""" + array_backend._backend = backend + array_backend._xp = array_backend._load_backend(backend) + + def to_numpy(array: Any) -> np.ndarray: """Convert an array to a NumPy array.""" if hasattr(array, "get"): @@ -132,4 +138,8 @@ def is_cpu(array: Any) -> bool: if TYPE_CHECKING: import numpy as xp else: - xp = array_backend.xp + # Use module-level __getattr__ for dynamic xp (Python 3.7+) + def __getattr__(name): + if name == "xp": + return array_backend.xp + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 6abcb31..c4a893c 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -27,6 +27,7 @@ def test_numpy_symbols_accessible(): "is_gpu", "is_cpu", "use_backend", + "set_backend", "xp", ] missing = [ @@ -75,6 +76,7 @@ def test_get_backend_and_is_gpu_cpu(): def test_use_backend(): # Initial backend should be numpy (default) in this test environment + # Accessing xp.xp triggers the dynamic __getattr__ in xp.py assert "numpy" in xp.xp.__name__ with xp.use_backend("numpy"): @@ -85,6 +87,21 @@ def test_use_backend(): assert "numpy" in xp.xp.__name__ +def test_set_backend(): + # Set to numpy + xp.set_backend("numpy") + assert "numpy" in xp.xp.__name__ + arr = xp.array([1]) + assert isinstance(arr, np.ndarray) + + # Set to cupy (falls back to numpy if not available) + xp.set_backend("cupy") + # If cupy is not installed, xp.xp will be numpy module + # We just verify it doesn't crash and we can still call things + arr2 = xp.array([2]) + assert arr2 is not None + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From 023e0c0aa2f559718f4d4d7c5c093ed61f0b2078 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:34:36 +0200 Subject: [PATCH 07/13] Added xp.synchronize() --- README.md | 3 +++ src/cunumpy/__init__.py | 5 +++-- src/cunumpy/__init__.pyi | 1 + src/cunumpy/xp.py | 11 +++++++++++ tests/unit/test_app.py | 12 ++++++++++++ 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ee5cb1d..2e595be 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ with xp.use_backend("numpy"): # Set backend globally xp.set_backend("cupy") + +# Synchronize GPU operations (no-op on CPU) +xp.synchronize() ``` # Build docs diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index ae3e2db..9f4fb78 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,7 +1,7 @@ # cunumpy/__init__.py from . import xp -from .xp import (get_backend, is_cpu, is_gpu, set_backend, to_cunumpy, to_cupy, - to_numpy, use_backend) +from .xp import (get_backend, is_cpu, is_gpu, set_backend, synchronize, + to_cunumpy, to_cupy, to_numpy, use_backend) __all__ = [ "xp", @@ -13,6 +13,7 @@ "is_cpu", "use_backend", "set_backend", + "synchronize", ] diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index 6279d76..4cc8271 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -18,3 +18,4 @@ def is_cpu(array: Any) -> bool: ... @contextmanager def use_backend(backend: str) -> Generator[None, None, None]: ... def set_backend(backend: str) -> None: ... +def synchronize() -> None: ... diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index db67f11..76edb4c 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -92,6 +92,17 @@ def set_backend(backend: BackendType) -> None: array_backend._xp = array_backend._load_backend(backend) +def synchronize() -> None: + """Wait for all kernels in all streams on current device to complete.""" + if array_backend.backend == "cupy": + try: + import cupy as cp + + cp.cuda.Device().synchronize() + except (ImportError, AttributeError): + pass + + def to_numpy(array: Any) -> np.ndarray: """Convert an array to a NumPy array.""" if hasattr(array, "get"): diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index c4a893c..d262754 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -28,6 +28,7 @@ def test_numpy_symbols_accessible(): "is_cpu", "use_backend", "set_backend", + "synchronize", "xp", ] missing = [ @@ -102,6 +103,17 @@ def test_set_backend(): assert arr2 is not None +def test_synchronize(): + # Should not crash on any backend + xp.synchronize() + + with xp.use_backend("numpy"): + xp.synchronize() + + with xp.use_backend("cupy"): + xp.synchronize() + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From d48b368dcc54d00b84399db52a291e68c010204f Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 08:50:43 +0200 Subject: [PATCH 08/13] black+isort compatibility --- pyproject.toml | 3 +++ src/cunumpy/__init__.py | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de9c369..e5c7bfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,3 +51,6 @@ where = [ "src" ] [tool.setuptools.package-data] cunumpy = [ "py.typed", "*.pyi" ] + +[tool.isort] +profile = "black" diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index 9f4fb78..e85851c 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -1,7 +1,16 @@ # cunumpy/__init__.py from . import xp -from .xp import (get_backend, is_cpu, is_gpu, set_backend, synchronize, - to_cunumpy, to_cupy, to_numpy, use_backend) +from .xp import ( + get_backend, + is_cpu, + is_gpu, + set_backend, + synchronize, + to_cunumpy, + to_cupy, + to_numpy, + use_backend, +) __all__ = [ "xp", From 10f43fff388956396d785bb2243deec76e835d3e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:02:07 +0200 Subject: [PATCH 09/13] Only publish docs from devel --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c59b8ea..5c9d160 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Deploy docs to GitHub Pages on: push: - branches: ["devel", "main"] # TODO: Set to main only after release + branches: ["devel"] workflow_dispatch: permissions: From fc1537f6284702eef03d0a4a21ca196b47873ec6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:02:39 +0200 Subject: [PATCH 10/13] Updated docs --- README.md | 14 ++------ docs/source/api.md | 42 ++++++++++++++++++++++-- docs/source/index.md | 29 +++++++++++++---- docs/source/quickstart.md | 68 ++++++++++++++++++++++++++------------- 4 files changed, 109 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 2e595be..0fe4acd 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,8 @@ Simple wrapper for numpy and cupy. Replace `import numpy as np` with `import cun # Install -Create and activate python environment - -``` -python -m venv env -source env/bin/activate -pip install --upgrade pip -``` - -Install the code and requirements with pip - -``` -pip install -e . +```bash +pip install cunumpy ``` Example usage: diff --git a/docs/source/api.md b/docs/source/api.md index a1c08b6..d92ddfb 100644 --- a/docs/source/api.md +++ b/docs/source/api.md @@ -1,6 +1,42 @@ # API -Description of the API +The `cunumpy` package exposes the following helper functions in addition to the standard NumPy/CuPy API. -```{toctree} -:maxdepth: 1 \ No newline at end of file +## Backend Management + +### `to_numpy(array)` +Converts an array to a NumPy array on the CPU. + +### `to_cupy(array)` +Converts an array to a CuPy array on the GPU. Raises `ImportError` if CuPy is not available. + +### `to_cunumpy(array)` +Converts an array to the currently active backend. + +### `get_backend(array)` +Returns the name of the backend (`"numpy"` or `"cupy"`) for the given array. + +### `is_gpu(array)` +Returns `True` if the array is stored on a GPU (CuPy). + +### `is_cpu(array)` +Returns `True` if the array is stored on a CPU (NumPy). + +## Global Configuration + +### `set_backend(backend_name)` +Globally sets the active backend for all `cunumpy` operations. `backend_name` should be `"numpy"` or `"cupy"`. + +### `use_backend(backend_name)` +A context manager that temporarily sets the active backend. + +```python +with xp.use_backend("numpy"): + # Operations here use NumPy + pass +``` + +## Hardware Control + +### `synchronize()` +Blocks until all preceding GPU operations are complete. This is a no-op when using the NumPy backend. diff --git a/docs/source/index.md b/docs/source/index.md index d082474..0be983f 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,11 +1,28 @@ -# Welcome +# CuNumpy -Write the documentation of your python package here +Simple wrapper for NumPy and CuPy. + +## Quick Start + +```python +import cunumpy as xp + +# Use it exactly like NumPy +arr = xp.array([1, 2, 3]) +print(type(arr)) +``` ```{toctree} :maxdepth: 2 -:caption: Contents +:caption: Contents: + +quickstart +tutorials +api +``` + +## Installation -quickstart.md -tutorials.md -api.md +```bash +pip install cunumpy +``` diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md index f42c8bd..773224c 100644 --- a/docs/source/quickstart.md +++ b/docs/source/quickstart.md @@ -1,46 +1,68 @@ # Quickstart -Clone the repo +## Installation -``` -git clone ... +Install `cunumpy` directly from PyPI: + +```bash +pip install cunumpy ``` -# Install +## Basic Usage -Create and activate python environment +Replace `import numpy as np` with `import cunumpy as xp`. By default, it will use NumPy if CuPy is not installed or configured. -``` -python -m venv env -source env/bin/activate -pip install --upgrade pip -``` +```python +import cunumpy as xp -Install the code and requirements with pip +# Create an array (automatically uses the active backend) +arr = xp.array([1, 2, 3]) -``` -pip install -e . +print(f"Array type: {type(arr)}") +print(f"Active backend: {xp.get_backend(arr)}") ``` -Example usage: +## Backend Control -``` +You can control the backend using environment variables: + +```bash export ARRAY_BACKEND=cupy ``` +Or programmatically: + ```python import cunumpy as xp -arr = xp.array([1,2]) -print(type(arr)) -print(xp.__version__) +# Globally set backend +xp.set_backend("cupy") + +# Scoped backend switching +with xp.use_backend("numpy"): + arr_cpu = xp.zeros(10) + print(xp.is_cpu(arr_cpu)) # True + +# Explicit conversion +arr_np = xp.to_numpy(arr) +arr_cp = xp.to_cupy(arr) ``` -# Build docs +## Synchronization +When using the GPU, it's important to synchronize for accurate timing: -``` -make html -cd ../ -open docs/_build/html/index.html +```python +import time +import cunumpy as xp + +xp.set_backend("cupy") +a = xp.random.rand(1000, 1000) + +start = time.time() +b = xp.dot(a, a) +xp.synchronize() # Wait for GPU +end = time.time() + +print(f"Elapsed: {end - start:.4f}s") ``` From 24a300e349eba6d0a83c958f79d740f8d56e42fe Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:03:30 +0200 Subject: [PATCH 11/13] Added CHANGELOG.md --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41c9a08 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + +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.2] - 2026-05-27 + +### Added +- **Backend Management Helpers**: + - `xp.to_numpy(arr)`: Explicitly move an array to CPU (NumPy). + - `xp.to_cupy(arr)`: Explicitly move an array to GPU (CuPy). + - `xp.to_cunumpy(arr)`: Normalize an array to the currently active backend. + - `xp.get_backend(arr)`: Identify if an array is `"numpy"` or `"cupy"`. + - `xp.is_gpu(arr)` & `xp.is_cpu(arr)`: Quick boolean checks for array residence. +- **Global Backend Control**: + - `xp.set_backend(name)`: Globally switch the active backend at runtime. + - `xp.use_backend(name)`: Context manager for temporary, scoped backend switching. +- **Synchronization**: + - `xp.synchronize()`: Blocks until GPU operations are complete (no-op on CPU). Essential for accurate benchmarking. +- **Developer Experience**: + - Added `isort` configuration to `pyproject.toml` with `black` profile compatibility. + +### Changed +- **Dynamic Dispatch Architecture**: Refactored `src/cunumpy/xp.py` to use module-level `__getattr__`. This ensures that `cunumpy.` calls always resolve to the currently active backend module, enabling seamless runtime switching via `set_backend`. +- **Type Safety**: Updated `src/cunumpy/__init__.pyi` stubs to provide full IDE autocompletion and type-checking for all new API methods. +- **Documentation**: Enhanced `README.md` with usage examples for the new backend control and synchronization features. + +### Fixed +- Improved `ArrayBackend` initialization to fallback gracefully to NumPy if CuPy is requested but not installed. +- Fixed symbol accessibility tests to correctly handle custom helper methods in the `cunumpy` namespace. + +## [0.1.1] - 2026-05-27 + +### Added +- **Type Inspection Support**: Added `.pyi` stub files to enable proper symbol inspection and autocompletion for Pylance/VS Code. +- **CI/CD**: Added GitHub Action for automated publishing to PyPI. + +### Changed +- Improved import structure in `src/cunumpy/__init__.py` to allow direct access to `xp`. + +## [0.1.0] - 2026-05-27 + +### Added +- **Initial Core Functionality**: + - `ArrayBackend` class for dispatching between NumPy and CuPy. + - Environment variable support (`ARRAY_BACKEND`) for selecting the backend. + - Basic module structure and `src/cunumpy/xp.py`. +- **Project Scaffold**: + - Documentation setup with Sphinx and nbsphinx. + - Initial unit tests and testing infrastructure. + - Basic tutorial notebook and README instructions. +- **Metadata**: Initial configuration of `pyproject.toml` and project metadata. From 0bc2b1a8ff1e5c62f5690351d3d21fab4aef6ae9 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:16:27 +0200 Subject: [PATCH 12/13] Added xp.cupy_backend and xp.numpy_backend --- docs/source/api.md | 6 ++++++ src/cunumpy/__init__.py | 6 ++++++ src/cunumpy/__init__.pyi | 3 +++ src/cunumpy/xp.py | 14 ++++++++++++++ tests/unit/test_app.py | 12 ++++++++++++ 5 files changed, 41 insertions(+) diff --git a/docs/source/api.md b/docs/source/api.md index d92ddfb..232518d 100644 --- a/docs/source/api.md +++ b/docs/source/api.md @@ -24,6 +24,12 @@ Returns `True` if the array is stored on a CPU (NumPy). ## Global Configuration +### `numpy_backend` +Boolean property that returns `True` if the currently active global backend is NumPy. + +### `cupy_backend` +Boolean property that returns `True` if the currently active global backend is CuPy. + ### `set_backend(backend_name)` Globally sets the active backend for all `cunumpy` operations. `backend_name` should be `"numpy"` or `"cupy"`. diff --git a/src/cunumpy/__init__.py b/src/cunumpy/__init__.py index e85851c..c6592bd 100644 --- a/src/cunumpy/__init__.py +++ b/src/cunumpy/__init__.py @@ -23,9 +23,15 @@ "use_backend", "set_backend", "synchronize", + "numpy_backend", + "cupy_backend", ] def __getattr__(name: str): """Set cunumpy. to cunumpy.xp. (NumPy/CuPy).""" + if name == "numpy_backend": + return xp.numpy_backend + if name == "cupy_backend": + return xp.cupy_backend return getattr(xp.xp, name) diff --git a/src/cunumpy/__init__.pyi b/src/cunumpy/__init__.pyi index 4cc8271..2a4fb33 100644 --- a/src/cunumpy/__init__.pyi +++ b/src/cunumpy/__init__.pyi @@ -19,3 +19,6 @@ def is_cpu(array: Any) -> bool: ... def use_backend(backend: str) -> Generator[None, None, None]: ... def set_backend(backend: str) -> None: ... def synchronize() -> None: ... + +numpy_backend: bool +cupy_backend: bool diff --git a/src/cunumpy/xp.py b/src/cunumpy/xp.py index 76edb4c..36d3b9c 100644 --- a/src/cunumpy/xp.py +++ b/src/cunumpy/xp.py @@ -92,6 +92,16 @@ def set_backend(backend: BackendType) -> None: array_backend._xp = array_backend._load_backend(backend) +def _cupy_backend() -> bool: + """Check if the active global backend is CuPy.""" + return array_backend.backend == "cupy" + + +def _numpy_backend() -> bool: + """Check if the active global backend is NumPy.""" + return array_backend.backend == "numpy" + + def synchronize() -> None: """Wait for all kernels in all streams on current device to complete.""" if array_backend.backend == "cupy": @@ -153,4 +163,8 @@ def is_cpu(array: Any) -> bool: def __getattr__(name): if name == "xp": return array_backend.xp + if name == "numpy_backend": + return _numpy_backend() + if name == "cupy_backend": + return _cupy_backend() raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index d262754..53e095e 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -29,6 +29,8 @@ def test_numpy_symbols_accessible(): "use_backend", "set_backend", "synchronize", + "numpy_backend", + "cupy_backend", "xp", ] missing = [ @@ -114,6 +116,16 @@ def test_synchronize(): xp.synchronize() +def test_backend_bools(): + with xp.use_backend("numpy"): + assert xp.numpy_backend is True + assert xp.cupy_backend is False + + # Note: in test env without cupy, cupy_backend might be false + # even inside use_backend('cupy') if fallback occurs. + # Our implementation of use_backend calls _load_backend which returns np if cp missing. + + if __name__ == "__main__": test_xp_array() test_numpy_symbols_accessible() From 34db71329a0628e82c63afc1687e3ce6b4f6067d Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 28 May 2026 09:17:36 +0200 Subject: [PATCH 13/13] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c9a08..3fae537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Global Backend Control**: - `xp.set_backend(name)`: Globally switch the active backend at runtime. - `xp.use_backend(name)`: Context manager for temporary, scoped backend switching. + - `xp.numpy_backend` & `xp.cupy_backend`: Boolean properties to check the globally active backend. - **Synchronization**: - `xp.synchronize()`: Blocks until GPU operations are complete (no-op on CPU). Essential for accurate benchmarking. - **Developer Experience**: @@ -25,7 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - **Dynamic Dispatch Architecture**: Refactored `src/cunumpy/xp.py` to use module-level `__getattr__`. This ensures that `cunumpy.` calls always resolve to the currently active backend module, enabling seamless runtime switching via `set_backend`. - **Type Safety**: Updated `src/cunumpy/__init__.pyi` stubs to provide full IDE autocompletion and type-checking for all new API methods. -- **Documentation**: Enhanced `README.md` with usage examples for the new backend control and synchronization features. +- **Documentation**: + - Simplified `README.md` and documentation to exclusively focus on PyPI installation (`pip install cunumpy`). + - Enhanced `quickstart.md` and `api.md` with usage examples for the new backend control and synchronization features. +- **CI/CD**: Restricted GitHub Pages documentation deployment to the `devel` branch only. ### Fixed - Improved `ArrayBackend` initialization to fallback gracefully to NumPy if CuPy is requested but not installed.