From b9e04fd36c4255a1e3bfb66577ecc3447b995da7 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 28 Oct 2026 09:33:26 -0500 Subject: [PATCH 1/2] Support SuiteSparse:GraphBLAS 10.5.2 Behavior fix. SuiteSparse:GraphBLAS 10.5.2 turned GxB_NONBLOCKING_GPU and GxB_BLOCKING_GPU from GrB_Mode values of their own (7099 and 7098) into #defines of GrB_NONBLOCKING and GrB_BLOCKING, and python-suitesparse-graphblas built against it no longer has them. Before, graphblas.ss (gb.ss.about, gb.ss.config, gb.ss.concat, and so on, and `from graphblas import *`) failed to import with that build: AttributeError: cffi library 'suitesparse_graphblas._graphblas' has no function, constant or global variable named 'GxB_NONBLOCKING_GPU' After, it imports, and gb.ss.about["mode"] reads "blocking" or "nonblocking" as before. The GPU modes go into About._modes only when the library has them, with setdefault, so a library that exposes them as aliases cannot turn "nonblocking" into "nonblocking_gpu". test_about now checks the mode against the one graphblas was initialized with. 10.5.2 also checks that imported values are long enough, which older versions got wrong (they compared the byte size of the values with the number of entries). test_ss_import_export imported a 14 x 7 bitmap with values for 7 x 7, which 10.5.2 rejects with InvalidValue and older versions filled from past the end of the values; it now gives values for the whole bitmap. --- graphblas/ss/_core.py | 8 ++++++-- graphblas/tests/test_matrix.py | 3 +++ graphblas/tests/test_ss_utils.py | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/graphblas/ss/_core.py b/graphblas/ss/_core.py index 5e851b7cc..f036b3bb7 100644 --- a/graphblas/ss/_core.py +++ b/graphblas/ss/_core.py @@ -259,9 +259,13 @@ class About(Mapping): _modes = { lib.GrB_NONBLOCKING: "nonblocking", lib.GrB_BLOCKING: "blocking", - lib.GxB_NONBLOCKING_GPU: "nonblocking_gpu", - lib.GxB_BLOCKING_GPU: "blocking_gpu", } + # GxB_NONBLOCKING_GPU and GxB_BLOCKING_GPU were modes of their own in SS:GraphBLAS <=10.5.1. + # In 10.5.2 they became aliases of GrB_NONBLOCKING and GrB_BLOCKING, which python-suitesparse- + # graphblas leaves out; `setdefault` keeps "nonblocking" and "blocking" if they are aliases. + if hasattr(lib, "GxB_NONBLOCKING_GPU"): + _modes.setdefault(lib.GxB_NONBLOCKING_GPU, "nonblocking_gpu") + _modes.setdefault(lib.GxB_BLOCKING_GPU, "blocking_gpu") _mode_options = { "mode": lib.GxB_MODE, } diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 4befea2cf..ea8a29f26 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2084,6 +2084,9 @@ def test_ss_import_export(A, do_iso, methods): assert A5.isequal(A) assert A5.ss.is_iso is do_iso d["bitmap"] = np.concatenate([d["bitmap"], d["bitmap"]], axis=0) + if not do_iso: + # Values for the whole bitmap; GraphBLAS reads them all + d["values"] = np.concatenate([d["values"], d["values"]], axis=0) B5b = Matrix.ss.import_any(**d) if in_method == "import": # B5b == [A, A] (i.e, get 2d shape from bitmap first if possible) diff --git a/graphblas/tests/test_ss_utils.py b/graphblas/tests/test_ss_utils.py index 2c9e01a1a..b1469b7a0 100644 --- a/graphblas/tests/test_ss_utils.py +++ b/graphblas/tests/test_ss_utils.py @@ -223,6 +223,8 @@ def test_about(): with pytest.raises(KeyError): about["badkey"] assert "SuiteSparse" in about["library_name"] + # SS:GraphBLAS 10.5.2 made the GPU modes aliases of these; neither may read as a GPU mode + assert about["mode"] == ("blocking" if gb._init_params["blocking"] else "nonblocking") with pytest.raises(TypeError): del about["library_name"] # pylint: disable=unsupported-delete-operation assert "library_name" in repr(about) From f2404c11d5ef0ab1bfc481c96594d644eaeae7d1 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 22 Oct 2026 11:20:46 -0500 Subject: [PATCH 2/2] Cap an unpinned pandas below 3.1 in CI pandas 3.1.0 came out on 2026-10-22, on PyPI so far, and three test_formatting.py tests fail with it, on main and here: * test_sparse_vector_repr and test_sparse_matrix_repr: the pandas-based repr renderer appends its "..." row with ``df.loc["..."] = ...`` to integer columns, and pandas 3.1 warns ("Pandas4Warning: Setting an item of incompatible dtype is deprecated"), which ``filterwarnings = error`` turns into a failure. * test_autocompute: under pandas' default display.max_columns=0, pandas 3.1 fits one column fewer on each side of a wide repr's "..." than 3.0 did, and the test pins 3.0's output. conda-forge has pandas 3.1.0 now, so every CI draw of the newest pandas hits these. #605 renders reprs without pandas, which makes both moot, so rather than change the renderer it replaces, CI stays below pandas 3.1 until #605 is on main. The pools keep "" for the newest release. A new LATEST_CEILINGS table caps it when the pick is written out, so an unpinned pandas becomes ``pdver='<3.1'`` and the log's summary reads ``pd=latest<3.1``. Pins and rules are unchanged; an unpinned draw now gets the newest 3.0.x. A MAINT note says when to remove the cap, and check_versions.sh points to it. Only test_and_build.yml installs pandas to run the tests. imports.yml (pip, ``.[default]``) and debug.yml (conda) also install the newest pandas, but they only import graphblas, which pandas 3.1 does not affect. Checked: ``--validate`` passes (160000 combos). Dry-run solves with the cap land on pandas 3.0.6 for Python 3.15 on linux-64 and for 3.13 with numpy 2.2 on win-64, and on 2.3.3 for 3.11 with numpy 1.24 on osx-arm64. Locally, the three tests fail with pandas 3.1.0 and pass with 3.0.6. --- scripts/check_versions.sh | 1 + scripts/ci_pick_versions.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/scripts/check_versions.sh b/scripts/check_versions.sh index d9959556d..f702f2f41 100755 --- a/scripts/check_versions.sh +++ b/scripts/check_versions.sh @@ -13,6 +13,7 @@ conda search 'flake8-bugbear[channel=conda-forge]>=26.9.9' conda search 'flake8-simplify[channel=conda-forge]>=0.30.0' conda search 'numpy[channel=conda-forge]>=2.5' +# ci_pick_versions.py caps an unpinned pandas below 3.1 for now (see its MAINT note). conda search 'pandas[channel=conda-forge]>=3.0' conda search 'scipy[channel=conda-forge]>=1.18' conda search 'networkx[channel=conda-forge]>=3.6' diff --git a/scripts/ci_pick_versions.py b/scripts/ci_pick_versions.py index ebd003d96..8c7d4786c 100755 --- a/scripts/ci_pick_versions.py +++ b/scripts/ci_pick_versions.py @@ -83,6 +83,14 @@ "2.x": ["2.2", "2.3", "3.0", ""], } +# Caps on an unpinned ("" = latest) pick, as conda specs. Pins in the pools are not +# affected; a capped package's newest release stays out of CI until the cap goes. +# MAINT: 2026-10-22 pandas 3.1.0 fits fewer columns in a repr's auto-width +# (display.max_columns=0), and warns (Pandas4Warning) when the pandas-based repr renderer +# appends its "..." row to a numeric column; both fail test_formatting.py. Remove the cap +# once #605 (the repr renderer that does not use pandas) is on main. +LATEST_CEILINGS = {"pandas": "<3.1"} + # The "2.x" pool starts at 2.6 because awkward <2.6 still uses numpy.AxisError, # which numpy 2.0 removed. Only >=2.10 pins survive numpy >=2.5; see the awkward # constraints in `apply_constraints`. @@ -557,14 +565,17 @@ def format_output(v): """Format version dict as bash-eval-safe key=value lines. Values use '=X.Y' prefix for conda install (e.g., npver='=2.2'). - Empty means latest, NA means skip. + Empty means latest, NA means skip. Latest is capped by LATEST_CEILINGS + (e.g., pdver='<3.1'). psg already has its prefix baked in. """ lines = [] for key in ("numpy", "scipy", "pandas", "awkward", "networkx", "pyyaml", "sparse", "numba"): var = _VAR_NAMES[key] val = v[key] - if val in ("NA", ""): + if val == "" and key in LATEST_CEILINGS: + lines.append(f"{var}='{LATEST_CEILINGS[key]}'") + elif val in ("NA", ""): lines.append(f"{var}='{val}'") else: lines.append(f"{var}='={val}'") @@ -598,7 +609,7 @@ def format_summary(v): if val == "NA": parts.append(f"{name}=NA") elif val == "": - parts.append(f"{name}=latest") + parts.append(f"{name}=latest{LATEST_CEILINGS.get(key, '')}") else: parts.append(f"{name}={val}") return "versions: " + " ".join(parts)