From b15647d9734c8a89e1d46c88a22a98c9202d8a7e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 18 Oct 2026 05:01:40 -0500 Subject: [PATCH 1/4] Check that imported values have an element for every entry SuiteSparse compares the byte size of an imported values buffer with the number of entries, not with the bytes they need (GB_import.c sets Ax_memsize_for_non_iso to nvals or vlen * vdim), so any buffer of at least nvals bytes got through and was read past its end: Vector.ss.import_full(values=np.ones(3), size=5) built a Vector whose last two values were heap garbage, and so did values too short for a sub-array dtype, as in Vector.ss.import_sparse(size=3, indices=[0, 1], values=np.ones(2), dtype="FP64[2]") Every ss import and pack (sparse, bitmap and full Vectors; csr, csc, hypercsr, hypercsc, bitmapr, bitmapc, fullr and fullc Matrices, and so from_dcsr and from_dcsc) now raises ValueError: values must have 5 or more elements of dtype FP64, not 3 before handing GraphBLAS the buffer. An iso import needs one element. --- graphblas/core/ss/matrix.py | 11 +++++++ graphblas/core/ss/vector.py | 4 +++ graphblas/core/utils.py | 16 ++++++++++ graphblas/tests/test_matrix.py | 54 ++++++++++++++++++++++++++++++++++ graphblas/tests/test_vector.py | 27 +++++++++++++++++ 5 files changed, 112 insertions(+) diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index 9fc535924..94454c528 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -17,6 +17,7 @@ _distinct_buffers, _MatrixArray, _Pointer, + check_values_size, get_order, get_shape, ints_to_numpy_buffer, @@ -1353,6 +1354,8 @@ def _import_csr( else: mhandle = matrix._carg args = () + n = nrows if method == "import" else matrix._nrows + check_values_size(values, dtype, n, is_iso, indptr=indptr) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_CSR")( mhandle, @@ -1557,6 +1560,8 @@ def _import_csc( else: mhandle = matrix._carg args = () + n = ncols if method == "import" else matrix._ncols + check_values_size(values, dtype, n, is_iso, indptr=indptr) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_CSC")( mhandle, @@ -1793,6 +1798,7 @@ def _import_hypercsr( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nvec, is_iso, indptr=indptr) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_HyperCSR")( mhandle, @@ -2032,6 +2038,7 @@ def _import_hypercsc( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nvec, is_iso, indptr=indptr) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_HyperCSC")( mhandle, @@ -2244,6 +2251,7 @@ def _import_bitmapr( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nrows * ncols, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_BitmapR")( mhandle, @@ -2451,6 +2459,7 @@ def _import_bitmapc( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nrows * ncols, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_BitmapC")( mhandle, @@ -2632,6 +2641,7 @@ def _import_fullr( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nrows * ncols, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_FullR")( mhandle, *args, Ax, values.nbytes, is_iso, NULL if desc is None else desc._carg @@ -2804,6 +2814,7 @@ def _import_fullc( else: mhandle = matrix._carg args = () + check_values_size(values, dtype, nrows * ncols, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_FullC")( mhandle, *args, Ax, values.nbytes, is_iso, NULL if desc is None else desc._carg diff --git a/graphblas/core/ss/vector.py b/graphblas/core/ss/vector.py index 9cd4e22d0..8ee259510 100644 --- a/graphblas/core/ss/vector.py +++ b/graphblas/core/ss/vector.py @@ -16,6 +16,7 @@ _CArray, _distinct_buffers, _MatrixArray, + check_values_size, ints_to_numpy_buffer, normalize_chunks, values_to_numpy_buffer, @@ -1016,6 +1017,7 @@ def _import_sparse( else: vhandle = vector._carg args = () + check_values_size(values, dtype, nvals, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Vector_{method}_CSC")( vhandle, @@ -1218,6 +1220,7 @@ def _import_bitmap( else: vhandle = vector._carg args = () + check_values_size(values, dtype, size, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Vector_{method}_Bitmap")( vhandle, @@ -1393,6 +1396,7 @@ def _import_full( else: vhandle = vector._carg args = () + check_values_size(values, dtype, size, is_iso) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Vector_{method}_Full")( vhandle, diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index 7da0e0645..498e875f3 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -225,6 +225,22 @@ def check_element_shape(values, dtype, ndim): ) +def check_values_size(values, dtype, count, is_iso, *, indptr=None): + """Raise unless ``values`` holds ``count`` elements of ``dtype``, or one if ``is_iso``. + + With ``indptr``, the count is ``indptr[count]``, as GraphBLAS reads it. GraphBLAS + imports compare the byte size of the values with the number of entries, not with + the bytes those entries need, so a short array gets through and is read past its end. + """ + if is_iso: + count = 1 + elif indptr is not None: + # GraphBLAS rejects an indptr this short itself + count = int(indptr[count]) if count < indptr.size else 0 + if (have := values.nbytes // dtype.np_type.itemsize) < count: + raise ValueError(f"values must have {count} or more elements of dtype {dtype}, not {have}") + + def normalize_values(self, values, dtype, shape=None, is_iso=False): """Expand and/or update dtype of values array.""" if dtype is not None: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 30d2a5fa0..8196e88ae 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2178,6 +2178,7 @@ def test_ss_import_export(A, do_iso, methods): assert B5b.nvals == 2 * A.nvals assert B5b.nrows == 2 * A.nrows assert B5b.ncols == A.ncols + assert B5b[7:, :].new().isequal(A) else: A5.ss.pack_any(**d) assert A5.isequal(A) @@ -4538,6 +4539,59 @@ def test_subarray_dtype_values_shape(): Matrix.from_coo([0, 1], [0, 1], np.ones((3, 2)), "FP64[2]") +@pytest.mark.skipif("not suitesparse") +def test_ss_import_values_size(): + # GraphBLAS compares the byte size of imported values with the number of entries, + # not the bytes they need, so it read past the end of each of these + match = "have 3 or more elements of dtype FP64, not 1" + with pytest.raises(ValueError, match=match): + Matrix.ss.import_csr( + nrows=2, ncols=2, indptr=[0, 1, 3], col_indices=[0, 0, 1], values=np.ones(1) + ) + A = Matrix(float, 2, 2) + with pytest.raises(ValueError, match=match): + A.ss.pack_csc(indptr=[0, 1, 3], row_indices=[0, 0, 1], values=np.ones(1)) + with pytest.raises(ValueError, match=match): + Matrix.ss.import_hypercsr( + nrows=3, ncols=3, rows=[0, 2], indptr=[0, 1, 3], col_indices=[0, 0, 1], values=[1.0] + ) + with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 3"): + Matrix.ss.import_fullr(values=np.ones(3), nrows=2, ncols=2) + with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 2"): + Matrix.ss.import_bitmapc(bitmap=np.ones((2, 2), bool), values=np.ones(2), nrows=2, ncols=2) + # The dtype's name is whichever another test gave this anonymous dtype + match = r"have 2 or more elements of dtype \S+, not 1" + with pytest.raises(ValueError, match=match): + Matrix.ss.import_hypercsc( + nrows=3, + ncols=3, + cols=[0, 2], + indptr=[0, 1, 2], + row_indices=[0, 1], + values=np.ones(2), + dtype="FP64[2]", + ) + with pytest.raises(ValueError, match=match): + Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], np.ones(2), "FP64[2]") + with pytest.raises(ValueError, match=r"have 4 or more elements of dtype \S+, not 2"): + Matrix.ss.import_fullc(values=np.ones((2, 2, 1)), dtype="FP64[2]") + # One element is enough for iso + A = Matrix.ss.import_fullr(values=[7.0], nrows=2, ncols=2, is_iso=True) + assert A.ss.is_iso + assert A.to_coo()[2].tolist() == [7.0] * 4 + A = Matrix.ss.import_csr( + nrows=2, + ncols=2, + indptr=[0, 1, 2], + col_indices=[0, 1], + values=[1.0, 2.0], + dtype="FP64[2]", + is_iso=True, + ) + assert A.ss.is_iso + assert A.to_coo()[2].tolist() == [[1.0, 2.0]] * 2 + + def test_power(A): expected = A.dup() for i in range(1, 50): diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index de1d2a53d..5ea52a5b7 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -3109,3 +3109,30 @@ def test_subarray_dtype_values_shape(): # The lengths compared are the number of elements, not of numbers (it said 2 != 6) with pytest.raises(ValueError, match="lengths must match: 2 != 3$"): Vector.from_coo([0, 1], np.ones((3, 2)), "FP64[2]") + + +@pytest.mark.skipif("not suitesparse") +def test_ss_import_values_size(): + # GraphBLAS compares the byte size of imported values with the number of entries, + # not the bytes they need, so it read past the end of each of these + with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 2"): + Vector.ss.import_sparse(size=5, indices=[0, 1, 2, 3], values=np.ones(2), nvals=4) + with pytest.raises(ValueError, match="have 5 or more elements of dtype FP64, not 3"): + Vector.ss.import_full(values=np.ones(3), size=5) + v = Vector(float, 5) + with pytest.raises(ValueError, match="have 5 or more elements of dtype FP64, not 3"): + v.ss.pack_full(values=np.ones(3)) + # The dtype's name is whichever another test gave this anonymous dtype + with pytest.raises(ValueError, match=r"have 2 or more elements of dtype \S+, not 1"): + Vector.ss.import_sparse(size=3, indices=[0, 1], values=np.ones(2), dtype="FP64[2]") + with pytest.raises(ValueError, match=r"have 3 or more elements of dtype \S+, not 1"): + Vector.ss.import_bitmap(bitmap=[True, True, True], values=np.ones(3), dtype="FP64[2]") + # One element is enough for iso + v = Vector.ss.import_full(values=[4.0], size=5, is_iso=True) + assert v.ss.is_iso + assert v.to_coo()[1].tolist() == [4.0] * 5 + v = Vector.ss.import_sparse( + size=3, indices=[0, 1], values=[2.0, 3.0], dtype="FP64[2]", is_iso=True + ) + assert v.ss.is_iso + assert v.to_coo()[1].tolist() == [[2.0, 3.0]] * 2 From a38b02bbf57833663951218f83e021b7469c21ae Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 28 Oct 2026 09:37:52 -0500 Subject: [PATCH 2/4] Note that SuiteSparse:GraphBLAS 10.5.2 checks the size of imported values SuiteSparse:GraphBLAS 10.5.2 fixed the import size check that check_values_size works around: GB_import.c compared the byte size of the values with the number of entries. With check_values_size bypassed, the five short imports tried (import_full, import_sparse of FP64[2], import_bitmapr, and two import_csr) now raise InvalidValue with an empty message on 10.5.2, where 10.5.1 built them from bytes past the end. check_values_size still runs on every version: older ones need it, and its ValueError says what is wrong. Its docstring and the comments of the two test_ss_import_values_size tests now give the versions. Comments only. --- graphblas/core/utils.py | 5 +++-- graphblas/tests/test_matrix.py | 4 ++-- graphblas/tests/test_vector.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index 498e875f3..d3f40c482 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -229,8 +229,9 @@ def check_values_size(values, dtype, count, is_iso, *, indptr=None): """Raise unless ``values`` holds ``count`` elements of ``dtype``, or one if ``is_iso``. With ``indptr``, the count is ``indptr[count]``, as GraphBLAS reads it. GraphBLAS - imports compare the byte size of the values with the number of entries, not with - the bytes those entries need, so a short array gets through and is read past its end. + imports before 10.5.2 compare the byte size of the values with the number of entries, + not with the bytes those entries need, so a short array gets through and is read past + its end. 10.5.2 raises InvalidValue with no message instead, so this check stays. """ if is_iso: count = 1 diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 8196e88ae..2362f4ce4 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4541,8 +4541,8 @@ def test_subarray_dtype_values_shape(): @pytest.mark.skipif("not suitesparse") def test_ss_import_values_size(): - # GraphBLAS compares the byte size of imported values with the number of entries, - # not the bytes they need, so it read past the end of each of these + # GraphBLAS before 10.5.2 compares the byte size of imported values with the number + # of entries, not the bytes they need, so it read past the end of each of these match = "have 3 or more elements of dtype FP64, not 1" with pytest.raises(ValueError, match=match): Matrix.ss.import_csr( diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 5ea52a5b7..3e4c9cf40 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -3113,8 +3113,8 @@ def test_subarray_dtype_values_shape(): @pytest.mark.skipif("not suitesparse") def test_ss_import_values_size(): - # GraphBLAS compares the byte size of imported values with the number of entries, - # not the bytes they need, so it read past the end of each of these + # GraphBLAS before 10.5.2 compares the byte size of imported values with the number + # of entries, not the bytes they need, so it read past the end of each of these with pytest.raises(ValueError, match="have 4 or more elements of dtype FP64, not 2"): Vector.ss.import_sparse(size=5, indices=[0, 1, 2, 3], values=np.ones(2), nvals=4) with pytest.raises(ValueError, match="have 5 or more elements of dtype FP64, not 3"): From 93540927b59d85cf7e09dd7945217ab701b4f9ec Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 3 Nov 2026 11:43:01 -0800 Subject: [PATCH 3/4] Fill one element with a scalar in from_dcsr and from_dcsc on SuiteSparse Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], 1.5, "FP64[2]") gave [[1.5, 1.5], [1.5, 1.5]] without SuiteSparse, and on SuiteSparse raised "values must have 1 or more elements of dtype FP64[2], not 0": its SuiteSparse branch imported the scalar as it was, 8 bytes where one element is 16. It now broadcasts a scalar to one whole element and imports it iso, as from_csr and from_csc do; the same for from_dcsc. check_values_size also says what is wrong in two cases it misreported: - A scalar that does not fill one element of a sub-array dtype, as in Vector.ss.import_full(values=1.5, size=3, dtype="FP64[2]", is_iso=True): "a scalar does not fill one element of dtype FP64[2], of shape (2,)" (it said "not 0"). - Index pointers that give more entries than there are indices, as in import_csr(nrows=1, ncols=3, indptr=[0, 2**62], col_indices=[0], values=[1.0]): "index pointers give 4611686018427387904 entries, more than the number of indices, 1" (it asked for that many values). GraphBLAS rejects such an indptr itself, with an InvalidValue and no message, so the check comes first. --- graphblas/core/matrix.py | 10 ++++++++++ graphblas/core/ss/matrix.py | 8 ++++---- graphblas/core/utils.py | 27 +++++++++++++++++++-------- graphblas/tests/test_matrix.py | 16 ++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 335c1f3e8..e1876c58f 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -1269,11 +1269,16 @@ def from_dcsr( """ if backend == "suitesparse": + values, dtype = values_to_numpy_buffer(values, dtype, subarray_after=1) + is_iso = values.ndim == 0 + if is_iso: + values = np.broadcast_to(values, dtype.np_type.shape) # One whole element return cls.ss.import_hypercsr( rows=compressed_rows, indptr=indptr, col_indices=col_indices, values=values, + is_iso=is_iso, nrows=nrows, ncols=ncols, dtype=dtype, @@ -1354,11 +1359,16 @@ def from_dcsc( """ if backend == "suitesparse": + values, dtype = values_to_numpy_buffer(values, dtype, subarray_after=1) + is_iso = values.ndim == 0 + if is_iso: + values = np.broadcast_to(values, dtype.np_type.shape) # One whole element return cls.ss.import_hypercsc( cols=compressed_cols, indptr=indptr, row_indices=row_indices, values=values, + is_iso=is_iso, nrows=nrows, ncols=ncols, dtype=dtype, diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index 94454c528..443019557 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -1355,7 +1355,7 @@ def _import_csr( mhandle = matrix._carg args = () n = nrows if method == "import" else matrix._nrows - check_values_size(values, dtype, n, is_iso, indptr=indptr) + check_values_size(values, dtype, n, is_iso, indptr=indptr, indices=col_indices) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_CSR")( mhandle, @@ -1561,7 +1561,7 @@ def _import_csc( mhandle = matrix._carg args = () n = ncols if method == "import" else matrix._ncols - check_values_size(values, dtype, n, is_iso, indptr=indptr) + check_values_size(values, dtype, n, is_iso, indptr=indptr, indices=row_indices) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_CSC")( mhandle, @@ -1798,7 +1798,7 @@ def _import_hypercsr( else: mhandle = matrix._carg args = () - check_values_size(values, dtype, nvec, is_iso, indptr=indptr) + check_values_size(values, dtype, nvec, is_iso, indptr=indptr, indices=col_indices) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_HyperCSR")( mhandle, @@ -2038,7 +2038,7 @@ def _import_hypercsc( else: mhandle = matrix._carg args = () - check_values_size(values, dtype, nvec, is_iso, indptr=indptr) + check_values_size(values, dtype, nvec, is_iso, indptr=indptr, indices=row_indices) desc = get_descriptor(secure_import=secure_import, **opts) status = getattr(lib, f"GxB_Matrix_{method}_HyperCSC")( mhandle, diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index d3f40c482..a77016756 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -225,20 +225,31 @@ def check_element_shape(values, dtype, ndim): ) -def check_values_size(values, dtype, count, is_iso, *, indptr=None): +def check_values_size(values, dtype, count, is_iso, *, indptr=None, indices=None): """Raise unless ``values`` holds ``count`` elements of ``dtype``, or one if ``is_iso``. - With ``indptr``, the count is ``indptr[count]``, as GraphBLAS reads it. GraphBLAS - imports before 10.5.2 compare the byte size of the values with the number of entries, - not with the bytes those entries need, so a short array gets through and is read past - its end. 10.5.2 raises InvalidValue with no message instead, so this check stays. + With ``indptr``, the count is ``indptr[count]``, as GraphBLAS reads it, and must not + exceed the number of ``indices``. GraphBLAS imports before 10.5.2 compare the byte + size of the values with the number of entries, not with the bytes those entries need, + so a short array gets through and is read past its end. 10.5.2 raises InvalidValue + with no message instead, so this check stays. """ - if is_iso: - count = 1 - elif indptr is not None: + if indptr is not None: # GraphBLAS rejects an indptr this short itself count = int(indptr[count]) if count < indptr.size else 0 + if indices is not None and indices.size < count: + raise ValueError( + f"index pointers give {count} entries, more than the number of indices, " + f"{indices.size}" + ) + if is_iso: + count = 1 if (have := values.nbytes // dtype.np_type.itemsize) < count: + if values.ndim == 0: + raise ValueError( + f"a scalar does not fill one element of dtype {dtype}, " + f"of shape {dtype.np_type.shape}" + ) raise ValueError(f"values must have {count} or more elements of dtype {dtype}, not {have}") diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 2362f4ce4..35fe4bd26 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4534,6 +4534,11 @@ def test_subarray_dtype_values_shape(): assert A.to_coo()[2].tolist() == expected A = Matrix.from_csr([0, 1, 2], [0, 1], 1.5, "FP64[2]") assert A.to_coo()[2].tolist() == expected + # on both backends (SuiteSparse raised "values must have 1 or more elements ... not 0") + A = Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], 1.5, "FP64[2]") + assert A.to_coo()[2].tolist() == expected + A = Matrix.from_dcsc([0, 1], [0, 1, 2], [0, 1], 1.5, "FP64[2]") + assert A.to_coo()[2].tolist() == expected # The lengths compared are the number of elements, not of numbers (it said 6) with pytest.raises(ValueError, match="lengths must match: 2, 2, 3$"): Matrix.from_coo([0, 1], [0, 1], np.ones((3, 2)), "FP64[2]") @@ -4575,6 +4580,17 @@ def test_ss_import_values_size(): Matrix.from_dcsr([0, 1], [0, 1, 2], [0, 1], np.ones(2), "FP64[2]") with pytest.raises(ValueError, match=r"have 4 or more elements of dtype \S+, not 2"): Matrix.ss.import_fullc(values=np.ones((2, 2, 1)), dtype="FP64[2]") + # A scalar is not a whole element of a sub-array dtype (it said "not 0") + with pytest.raises(ValueError, match=r"a scalar does not fill one element of dtype \S+, of"): + Matrix.ss.import_fullr(values=1.5, nrows=2, ncols=2, dtype="FP64[2]", is_iso=True) + # More entries than indices is an indptr problem; it was reported as too few values + match = "index pointers give 4611686018427387904 entries, more than the number of indices, 1" + with pytest.raises(ValueError, match=match): + Matrix.ss.import_csr(nrows=1, ncols=3, indptr=[0, 2**62], col_indices=[0], values=[1.0]) + with pytest.raises(ValueError, match="index pointers give 3 entries, more than the number"): + Matrix.ss.import_hypercsc( + nrows=3, ncols=3, cols=[0, 2], indptr=[0, 1, 3], row_indices=[0, 1], values=[1.0] + ) # One element is enough for iso A = Matrix.ss.import_fullr(values=[7.0], nrows=2, ncols=2, is_iso=True) assert A.ss.is_iso From e464a6429ce147eb80d5aa80ab3f603e531ea2c9 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 3 Nov 2026 12:08:33 -0800 Subject: [PATCH 4/4] Say a scalar does not fill an element only when it is shorter than one import_fullr(values=1.5, nrows=2, ncols=2) said "a scalar does not fill one element of dtype FP64", though 1.5 is a whole FP64 element and the problem is that four are needed. Keep that message for a scalar shorter than one element of a sub-array dtype. Found by review (r47abc). --- graphblas/core/utils.py | 2 +- graphblas/tests/test_matrix.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index a77016756..0b6b46a28 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -245,7 +245,7 @@ def check_values_size(values, dtype, count, is_iso, *, indptr=None, indices=None if is_iso: count = 1 if (have := values.nbytes // dtype.np_type.itemsize) < count: - if values.ndim == 0: + if values.ndim == 0 and have == 0: raise ValueError( f"a scalar does not fill one element of dtype {dtype}, " f"of shape {dtype.np_type.shape}" diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 35fe4bd26..9d5d43f52 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -4583,6 +4583,9 @@ def test_ss_import_values_size(): # A scalar is not a whole element of a sub-array dtype (it said "not 0") with pytest.raises(ValueError, match=r"a scalar does not fill one element of dtype \S+, of"): Matrix.ss.import_fullr(values=1.5, nrows=2, ncols=2, dtype="FP64[2]", is_iso=True) + # A scalar that is one whole element, where more are needed, is just too few values + with pytest.raises(ValueError, match=r"have 4 or more elements of dtype FP64, not 1"): + Matrix.ss.import_fullr(values=1.5, nrows=2, ncols=2) # More entries than indices is an indptr problem; it was reported as too few values match = "index pointers give 4611686018427387904 entries, more than the number of indices, 1" with pytest.raises(ValueError, match=match):