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) 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)