From 1bf84137c78e1a0b9f4b5e998569b9e4073ff62e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 20 Oct 2026 12:01:35 +0200 Subject: [PATCH] fix: return a uint8 tuple for scalar input with bytes=True Co-Authored-By: Claude Opus 5.5 --- src/cmap/_colormap.py | 30 ++++++++++++++++++++++-------- tests/test_colormap.py | 11 +++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/cmap/_colormap.py b/src/cmap/_colormap.py index 4dab625fe..401bc6da2 100644 --- a/src/cmap/_colormap.py +++ b/src/cmap/_colormap.py @@ -37,6 +37,7 @@ from ._catalog import CatalogItem LutCacheKey = tuple[int, float, bool] + RGBABytes = tuple[np.uint8, np.uint8, np.uint8, np.uint8] Interpolation = Literal["linear", "nearest"] class MPLSegmentData(TypedDict, total=False): @@ -407,8 +408,16 @@ def __call__( *, N: int = 256, gamma: float = 1, - bytes: bool = False, + bytes: Literal[False] = False, ) -> Color: ... + @overload + def __call__( + self, x: float, *, N: int = 256, gamma: float = 1, bytes: Literal[True] + ) -> RGBABytes: ... + @overload + def __call__( + self, x: float, *, N: int = 256, gamma: float = 1, bytes: bool + ) -> Color | RGBABytes: ... def __call__( self, x: float | NDArray | Iterable[float], @@ -416,7 +425,7 @@ def __call__( N: int = 256, gamma: float = 1, bytes: bool = False, - ) -> Color | NDArray[np.float64]: + ) -> Color | RGBABytes | NDArray[np.float64]: r"""Map scalar values in X to an RGBA array. This is the primary API for "using" a `cmap.Colormap` to map scalar values to @@ -460,14 +469,15 @@ def __call__( bytes : bool If False (default), the returned RGBA values will be floats in the interval ``[0, 1]`` otherwise they will be `numpy.uint8`\s in the - interval ``[0, 255]``. + interval ``[0, 255]``. For scalar `x`, `bytes=True` returns a tuple of + four `numpy.uint8` (as matplotlib does) rather than a `Color`. Returns ------- - color : Color | NDArray - If X is a float, a single RGBA color will be returned. If x is an - array-like, an array of RGBA colors will be returned with shape - `x.shape + (4,)` + color : Color | tuple | NDArray + If X is a float, a single `Color` will be returned (or a 4-tuple of + `numpy.uint8` if `bytes` is True). If x is an array-like, an array of + RGBA colors will be returned with shape `x.shape + (4,)` Examples -------- @@ -532,7 +542,11 @@ def __call__( xa[mask_masked] = N + 2 rgba = lut.take(xa, axis=0, mode="clip") - return rgba if np.iterable(x) else Color(rgba) + if np.iterable(x): + return rgba + if bytes: + return tuple(rgba) + return Color(rgba) def _with_exceptional_colors(self, lut: np.ndarray) -> np.ndarray: """Return `lut` with three rows appended, one per exceptional value class. diff --git a/tests/test_colormap.py b/tests/test_colormap.py index 8c98bc753..d8cad569b 100644 --- a/tests/test_colormap.py +++ b/tests/test_colormap.py @@ -591,3 +591,14 @@ def test_invalid_function_colormap_with_cmap_kwargs() -> None: # providing cmap_kwargs to a non-callable colormap should raise TypeError with pytest.raises(TypeError, match=r"Cannot apply cmap_kwargs to colormap"): Colormap("viridis", cmap_kwargs={"param": 1.0}) + + +def test_scalar_bytes() -> None: + cm = Colormap("viridis") + expect = cm(np.array([0.5]), bytes=True)[0] + for x in (0.5, np.float64(0.5), np.array(0.5)): + rgba = cm(x, bytes=True) + assert isinstance(rgba, tuple) + assert rgba == tuple(expect) + assert all(isinstance(v, np.uint8) for v in rgba) + assert isinstance(cm(0.5), Color)