From 7bd4e6c40c9a5c755b70c9bfcf535fb4d705ba2e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 15:21:55 -0600 Subject: [PATCH 01/20] Begin adding `.ss` to operator namespaces (with deprecations) --- .github/workflows/test_and_build.yml | 8 +- .pre-commit-config.yaml | 2 +- graphblas/{agg.py => agg/__init__.py} | 36 ++++- graphblas/binary/__init__.py | 28 +++- graphblas/binary/ss.py | 3 + graphblas/core/__init__.py | 2 +- graphblas/core/agg.py | 62 ++++++--- graphblas/core/operator.py | 181 +++++++++++++++++++++---- graphblas/monoid/__init__.py | 2 +- graphblas/op/__init__.py | 28 +++- graphblas/op/ss.py | 3 + graphblas/semiring/__init__.py | 28 +++- graphblas/semiring/ss.py | 3 + graphblas/ss/__init__.py | 2 +- graphblas/tests/conftest.py | 8 +- graphblas/tests/pickle1.pkl | Bin 2916 -> 2866 bytes graphblas/tests/test_matrix.py | 119 ++++++++-------- graphblas/tests/test_op.py | 80 +++++++---- graphblas/tests/test_operator_types.py | 32 +++-- graphblas/tests/test_pickle.py | 8 +- graphblas/tests/test_vector.py | 63 +++++---- graphblas/unary/__init__.py | 28 +++- graphblas/unary/ss.py | 3 + pyproject.toml | 2 + scripts/create_pickle.py | 2 +- setup.cfg | 2 + 26 files changed, 541 insertions(+), 194 deletions(-) rename graphblas/{agg.py => agg/__init__.py} (65%) create mode 100644 graphblas/binary/ss.py create mode 100644 graphblas/op/ss.py create mode 100644 graphblas/semiring/ss.py create mode 100644 graphblas/unary/ss.py diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index d22bf8014..bf6dab681 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -174,7 +174,7 @@ jobs: if [[ $H && $normal ]] ; then if [[ $macos ]] ; then echo " $vanilla" ; elif [[ $windows ]] ; then echo " $suitesparse" ; fi ; fi)$( \ if [[ $H && $bizarro ]] ; then if [[ $macos ]] ; then echo " $suitesparse" ; elif [[ $windows ]] ; then echo " $vanilla" ; fi ; fi) echo $args - coverage run -m pytest --randomly -v $args \ + coverage run -m pytest -Werror --randomly -v $args \ ${{ matrix.slowtask == 'pytest_normal' && '--runslow' || '' }} - name: Unit tests (bizarro scalars) run: | @@ -209,7 +209,7 @@ jobs: if [[ $H && $normal ]] ; then if [[ $macos ]] ; then echo " $suitesparse" ; elif [[ $windows ]] ; then echo " $vanilla" ; fi ; fi)$( \ if [[ $H && $bizarro ]] ; then if [[ $macos ]] ; then echo " $vanilla" ; elif [[ $windows ]] ; then echo " $suitesparse" ; fi ; fi) echo $args - coverage run -a -m pytest --randomly -v $args \ + coverage run -a -m pytest -Werror --randomly -v $args \ ${{ matrix.slowtask == 'pytest_bizarro' && '--runslow' || '' }} git checkout . # Undo changes to scalar default - name: Miscellaneous tests @@ -230,8 +230,8 @@ jobs: rm script.py # Tests whose coverage depend on order of tests :/ # TODO: understand why these are order-dependent and try to fix - coverage run -a -m pytest -x --no-mapnumpy -k test_binaryop_attributes_numpy graphblas/tests/test_op.py - # coverage run -a -m pytest -x --no-mapnumpy -k test_npmonoid graphblas/tests/test_numpyops.py --runslow + coverage run -a -m pytest -Werror -x --no-mapnumpy -k test_binaryop_attributes_numpy graphblas/tests/test_op.py + # coverage run -a -m pytest -Werror -x --no-mapnumpy -k test_npmonoid graphblas/tests/test_numpyops.py --runslow - name: Auto-generated code check if: matrix.slowtask == 'pylint' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fc76424c..32c7dcf15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.213 + rev: v0.0.215 hooks: - id: ruff args: [--force-exclude] diff --git a/graphblas/agg.py b/graphblas/agg/__init__.py similarity index 65% rename from graphblas/agg.py rename to graphblas/agg/__init__.py index 92854645c..9ce96e675 100644 --- a/graphblas/agg.py +++ b/graphblas/agg/__init__.py @@ -70,6 +70,40 @@ """ # All items are dynamically added by classes in core/agg.py # This module acts as a container of Aggregator instances -from .core import agg +_deprecated = {} + + +def __dir__(): + return globals().keys() | _deprecated.keys() | {"ss"} + + +def __getattr__(key): + if key in _deprecated: + import warnings + + warnings.warn( + f"`gb.agg.{key}` is deprecated; please use `gb.agg.ss.{key}` instead. " + f"`{key}` is specific to SuiteSparse:GraphBLAS. " + f"`gb.agg.{key}` will be removed in version 2023.9.0 or later." + ) + rv = _deprecated[key] + globals()[key] = rv + return rv + if key == "ss": + from .. import backend + + if backend != "suitesparse": + raise AttributeError( + f'module {__name__!r} only has attribute "ss" when backend is "suitesparse"' + ) + from importlib import import_module + + ss = import_module(".ss", __name__) + globals()["ss"] = ss + return ss + raise AttributeError(f"module {__name__!r} has no attribute {key!r}") + + +from ..core import agg # noqa: E402 isort:skip del agg diff --git a/graphblas/binary/__init__.py b/graphblas/binary/__init__.py index 05e80e868..b551503cd 100644 --- a/graphblas/binary/__init__.py +++ b/graphblas/binary/__init__.py @@ -8,13 +8,25 @@ "rfloordiv": "floordiv", "rpow": "pow", } +_deprecated = {} def __dir__(): - return globals().keys() | _delayed.keys() + return globals().keys() | _delayed.keys() | _deprecated.keys() | {"ss"} def __getattr__(key): + if key in _deprecated: + import warnings + + warnings.warn( + f"`gb.binary.{key}` is deprecated; please use `gb.binary.ss.{key}` instead. " + f"`{key}` is specific to SuiteSparse:GraphBLAS. " + f"`gb.binary.{key}` will be removed in version 2023.9.0 or later." + ) + rv = _deprecated[key] + globals()[key] = rv + return rv if key in _delayed: func, kwargs = _delayed.pop(key) rv = func(**kwargs) @@ -24,10 +36,22 @@ def __getattr__(key): other = globals().get(other_key, other_key) rv._commutes_to = other return rv + if key == "ss": + from .. import backend + + if backend != "suitesparse": + raise AttributeError( + f'module {__name__!r} only has attribute "ss" when backend is "suitesparse"' + ) + from importlib import import_module + + ss = import_module(".ss", __name__) + globals()["ss"] = ss + return ss raise AttributeError(f"module {__name__!r} has no attribute {key!r}") from ..core import operator # noqa: E402 isort:skip -from . import numpy # noqa: E402, F401 isort:skip +from . import numpy # noqa: E402 isort:skip del operator diff --git a/graphblas/binary/ss.py b/graphblas/binary/ss.py new file mode 100644 index 000000000..e45cbcda0 --- /dev/null +++ b/graphblas/binary/ss.py @@ -0,0 +1,3 @@ +from ..core import operator + +del operator diff --git a/graphblas/core/__init__.py b/graphblas/core/__init__.py index 72791c833..c079a6e2f 100644 --- a/graphblas/core/__init__.py +++ b/graphblas/core/__init__.py @@ -1,6 +1,6 @@ def __getattr__(name): if name in {"ffi", "lib", "NULL"}: - from .. import _autoinit # noqa: F401 + from .. import _autoinit return globals()[name] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/graphblas/core/agg.py b/graphblas/core/agg.py index 04c51e681..9e50c8622 100644 --- a/graphblas/core/agg.py +++ b/graphblas/core/agg.py @@ -3,7 +3,7 @@ import numpy as np -from .. import agg, binary, monoid, semiring, unary +from .. import agg, backend, binary, monoid, semiring, unary from ..dtypes import INT64, lookup_dtype from .utils import output_type @@ -95,9 +95,13 @@ def __contains__(self, dtype): return self._any_dtype or dtype in self.types def __repr__(self): + if self.name in agg._deprecated: + return f"agg.ss.{self.name}" return f"agg.{self.name}" def __reduce__(self): + if self.name in agg._deprecated: + return f"agg.ss.{self.name}" return f"agg.{self.name}" def __call__(self, val, *, rowwise=False, columnwise=False): @@ -512,8 +516,8 @@ def _argminmax(agg, updater, expr, opts, *, in_composite, monoid): opts, in_composite=in_composite, monoid=monoid, - row_semiring=semiring.min_firstj, - col_semiring=semiring.min_secondj, + row_semiring=semiring._deprecated["min_firstj"], + col_semiring=semiring._deprecated["min_secondj"], ) return _argminmaxij( agg, @@ -522,8 +526,8 @@ def _argminmax(agg, updater, expr, opts, *, in_composite, monoid): opts, in_composite=in_composite, monoid=monoid, - row_semiring=semiring.min_firsti, - col_semiring=semiring.min_secondi, + row_semiring=semiring._deprecated["min_firsti"], + col_semiring=semiring._deprecated["min_secondi"], ) if expr.cfunc_name.startswith("GrB_Vector_reduce"): return _argminmaxij( @@ -533,8 +537,8 @@ def _argminmax(agg, updater, expr, opts, *, in_composite, monoid): opts, in_composite=in_composite, monoid=monoid, - row_semiring=semiring.min_firsti, - col_semiring=semiring.min_secondi, + row_semiring=semiring._deprecated["min_firsti"], + col_semiring=semiring._deprecated["min_secondi"], ) if expr.cfunc_name.startswith("GrB_Matrix_reduce"): raise ValueError(f"Aggregator {agg.name} may not be used with Matrix.reduce_scalar.") @@ -542,15 +546,15 @@ def _argminmax(agg, updater, expr, opts, *, in_composite, monoid): # These "do the right thing", but don't work with `reduce_scalar` -agg.argmin = Aggregator( +_argmin = Aggregator( "argmin", custom=partial(_argminmax, monoid=monoid.min), - types=[semiring.min_firsti], + types=[semiring._deprecated["min_firsti"]], ) -agg.argmax = Aggregator( +_argmax = Aggregator( "argmax", custom=partial(_argminmax, monoid=monoid.max), - types=[semiring.min_firsti], + types=[semiring._deprecated["min_firsti"]], ) @@ -603,15 +607,15 @@ def _first_last(agg, updater, expr, opts, *, in_composite, semiring_): updater << A[i, j] -agg.first = Aggregator( +_first = Aggregator( "first", - custom=partial(_first_last, semiring_=semiring.min_secondi), + custom=partial(_first_last, semiring_=semiring._deprecated["min_secondi"]), types=[binary.first], any_dtype=True, ) -agg.last = Aggregator( +_last = Aggregator( "last", - custom=partial(_first_last, semiring_=semiring.max_secondi), + custom=partial(_first_last, semiring_=semiring._deprecated["max_secondi"]), types=[binary.second], any_dtype=True, ) @@ -642,18 +646,34 @@ def _first_last_index(agg, updater, expr, opts, *, in_composite, semiring): raise NotImplementedError(f"{agg.name} with {expr.cfunc_name}") -agg.first_index = Aggregator( +_first_index = Aggregator( "first_index", - custom=partial(_first_last_index, semiring=semiring.min_secondi), - types=[semiring.min_secondi], + custom=partial(_first_last_index, semiring=semiring._deprecated["min_secondi"]), + types=[semiring._deprecated["min_secondi"]], any_dtype=INT64, ) -agg.last_index = Aggregator( +_last_index = Aggregator( "last_index", - custom=partial(_first_last_index, semiring=semiring.max_secondi), - types=[semiring.min_secondi], + custom=partial(_first_last_index, semiring=semiring._deprecated["max_secondi"]), + types=[semiring._deprecated["min_secondi"]], any_dtype=INT64, ) +agg._deprecated = { + "argmin": _argmin, + "argmax": _argmax, + "first": _first, + "last": _last, + "first_index": _first_index, + "last_index": _last_index, +} +if backend == "suitesparse": + agg.ss.argmin = _argmin + agg.ss.argmax = _argmax + agg.ss.first = _first + agg.ss.last = _last + agg.ss.first_index = _first_index + agg.ss.last_index = _last_index + agg.Aggregator = Aggregator agg.TypedAggregator = TypedAggregator diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 77b5c7023..48b0c911c 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -11,6 +11,7 @@ from .. import ( _STANDARD_OPERATOR_NAMES, + backend, binary, config, indexunary, @@ -48,9 +49,73 @@ ffi_new = ffi.new UNKNOWN_OPCLASS = "UnknownOpClass" +# These now live as e.g. `gb.unary.ss.positioni` +# Deprecations such as `gb.unary.positioni` will be removed in 2023.9.0 or later. +_SS_OPERATORS = { + # unary + "positioni", + "positioni1", + "positionj", + "positionj1", + # binary + "firsti", + "firsti1", + "firstj", + "firstj1", + "secondi", + "secondi1", + "secondj", + "secondj1", + # semiring + "any_firsti", + "any_firsti1", + "any_firstj", + "any_firstj1", + "any_secondi", + "any_secondi1", + "any_secondj", + "any_secondj1", + "max_firsti", + "max_firsti1", + "max_firstj", + "max_firstj1", + "max_secondi", + "max_secondi1", + "max_secondj", + "max_secondj1", + "min_firsti", + "min_firsti1", + "min_firstj", + "min_firstj1", + "min_secondi", + "min_secondi1", + "min_secondj", + "min_secondj1", + "plus_firsti", + "plus_firsti1", + "plus_firstj", + "plus_firstj1", + "plus_secondi", + "plus_secondi1", + "plus_secondj", + "plus_secondj1", + "times_firsti", + "times_firsti1", + "times_firstj", + "times_firstj1", + "times_secondi", + "times_secondi1", + "times_secondj", + "times_secondj1", +} + def _hasop(module, name): - return name in module.__dict__ or name in module._delayed + return ( + name in module.__dict__ + or name in module._delayed + or name in getattr(module, "_deprecated", ()) + ) class OpPath: @@ -369,12 +434,21 @@ def __call__(self, left, right=None): @property def binaryop(self): - return getattr(binary, self.name.split("_", 1)[1])[self.type] + name = self.name.split("_", 1)[1] + if name in _SS_OPERATORS: + binop = binary._deprecated[name] + else: + binop = getattr(binary, name) + return binop[self.type] @property def monoid(self): monoid_name, binary_name = self.name.split("_", 1) - binop = getattr(binary, binary_name)[self.type] + if binary_name in _SS_OPERATORS: + binop = binary._deprecated[binary_name] + else: + binop = getattr(binary, binary_name) + binop = binop[self.type] val = getattr(monoid, monoid_name) return val[binop.return_type] @@ -866,12 +940,12 @@ def _remove_nesting(cls, funcname, *, module=None, modname=None, strict=True): if modname is None: modname = cls._modname if "." not in funcname: - if strict and hasattr(module, funcname): + if strict and _hasop(module, funcname): raise AttributeError(f"{modname}.{funcname} is already defined") else: path, funcname = funcname.rsplit(".", 1) for folder in path.split("."): - if not hasattr(module, folder): + if not _hasop(module, folder): setattr(module, folder, OpPath(module, folder)) module = getattr(module, folder) modname = f"{modname}.{folder}" @@ -930,15 +1004,30 @@ def _initialize(cls, include_in_ops=True): type_ = None name = "_".join(splitname).lower() # Create object for name unless it already exists - if not hasattr(cls._module, name): + if not _hasop(cls._module, name): + if backend == "suitesparse" and name in _SS_OPERATORS: + fullname = f"ss.{name}" + else: + fullname = name if cls._positional is None: - obj = cls(name) + obj = cls(fullname) + else: + obj = cls(fullname, is_positional=name in cls._positional) + if name in _SS_OPERATORS: + if backend == "suitesparse": + setattr(cls._module.ss, name, obj) + cls._module._deprecated[name] = obj + if include_in_ops and not _hasop(op, name): + op._deprecated[name] = obj + if backend == "suitesparse": + setattr(op.ss, name, obj) else: - obj = cls(name, is_positional=name in cls._positional) - setattr(cls._module, name, obj) - _STANDARD_OPERATOR_NAMES.add(f"{cls._modname}.{name}") - if include_in_ops and not hasattr(op, name): - setattr(op, name, obj) + setattr(cls._module, name, obj) + if include_in_ops and not _hasop(op, name): + setattr(op, name, obj) + _STANDARD_OPERATOR_NAMES.add(f"{cls._modname}.{fullname}") + elif name in _SS_OPERATORS: + obj = cls._module._deprecated[name] else: obj = getattr(cls._module, name) gb_obj = getattr(lib, varname) @@ -1211,7 +1300,10 @@ def _initialize(cls): # fmt: on ]: for name in names: - op = getattr(unary, name) + if name in _SS_OPERATORS: + op = unary._deprecated[name] + else: + op = getattr(unary, name) for input_types, target_type in types: typed_op = op._typed_ops[target_type] output_type = op.types[target_type] @@ -2279,7 +2371,10 @@ def _initialize(cls): ) for names, *types in name_types: for name in names: - cur_op = getattr(binary, name) + if name in _SS_OPERATORS: + cur_op = binary._deprecated[name] + else: + cur_op = getattr(binary, name) for input_types, target_type in types: typed_op = cur_op._typed_ops[target_type] output_type = cur_op.types[target_type] @@ -2293,17 +2388,33 @@ def _initialize(cls): del binary.ldexp[FP64] # Fill in commutes info for left_name, right_name in cls._commutes.items(): - left = getattr(binary, left_name) - left._commutes_to = right_name + if left_name in _SS_OPERATORS: + left = binary._deprecated[left_name] + else: + left = getattr(binary, left_name) if right_name not in binary._delayed: - right = getattr(binary, right_name) + if right_name in _SS_OPERATORS: + right = binary._deprecated[right_name] + else: + right = getattr(binary, right_name) + if backend == "suitesparse" and left_name in _SS_OPERATORS: + left._commutes_to = f"ss.{right_name}" + right._commutes_to = f"ss.{left_name}" + else: + left._commutes_to = right_name right._commutes_to = left_name for name in cls._commutative: cur_op = getattr(binary, name) cur_op._commutes_to = name - for left, right in cls._commutes_to_in_semiring.items(): - left = getattr(binary, left) - right = getattr(binary, right) + for left_name, right_name in cls._commutes_to_in_semiring.items(): + if left_name in _SS_OPERATORS: + left = binary._deprecated[left_name] + else: + left = getattr(binary, left_name) + if right_name in _SS_OPERATORS: + right = binary._deprecated[right_name] + else: + right = getattr(binary, right_name) left._semiring_commutes_to = right right._semiring_commutes_to = left # Allow some functions to work on UDTs @@ -2551,6 +2662,8 @@ def binaryop(self): if self._binaryop is not None: return self._binaryop # Must be builtin + if self.name in _SS_OPERATORS: + return binary._deprecated[self.name] return getattr(binary, self.name) @property @@ -2830,11 +2943,17 @@ def _initialize(cls): for lname in ["any", "eq", "land", "lor", "lxnor", "lxor"]: target_name = f"{lname}_ne" source_name = f"{lname}_lxor" - if not hasattr(semiring, target_name): + if not _hasop(semiring, target_name): continue - target_op = getattr(semiring, target_name) + if target_name in _SS_OPERATORS: + target_op = semiring._deprecated[target_name] + else: + target_op = getattr(semiring, target_name) if BOOL not in target_op.types: # pragma: no branch (safety) - source_op = getattr(semiring, source_name) + if source_name in _SS_OPERATORS: + source_op = semiring._deprecated[source_name] + else: + source_op = getattr(semiring, source_name) typed_op = source_op._typed_ops[BOOL] target_op.types[BOOL] = BOOL target_op._typed_ops[BOOL] = typed_op @@ -2911,9 +3030,12 @@ def _initialize(cls): ]: for left, right in itertools.product(lnames, rnames): name = f"{left}_{right}" - if not hasattr(semiring, name): + if not _hasop(semiring, name): continue - cur_op = getattr(semiring, name) + if name in _SS_OPERATORS: + cur_op = semiring._deprecated[name] + else: + cur_op = getattr(semiring, name) for input_types, target_type in types: typed_op = cur_op._typed_ops[target_type] output_type = cur_op.types[target_type] @@ -2970,7 +3092,10 @@ def binaryop(self): if self._binaryop is not None: return self._binaryop # Must be builtin - return getattr(binary, self.name.split("_")[1]) + name = self.name.split("_")[1] + if name in _SS_OPERATORS: + return binary._deprecated[name] + return getattr(binary, name) @property def monoid(self): @@ -2978,7 +3103,7 @@ def monoid(self): if self._monoid is not None: return self._monoid # Must be builtin - return getattr(monoid, self.name.split("_")[0]) + return getattr(monoid, self.name.split("_")[0].split(".")[-1]) @property def is_positional(self): @@ -3166,6 +3291,8 @@ def get_semiring(monoid, binaryop, name=None): or binary_prefix == ["numpy"] and not monoid_prefix ) + or backend == "suitesparse" + and binary_name in _SS_OPERATORS ): canonical_name = ( ".".join(monoid_prefix or binary_prefix) + f".{monoid_name}_{binary_name}" diff --git a/graphblas/monoid/__init__.py b/graphblas/monoid/__init__.py index 3bb1704fe..007aba416 100644 --- a/graphblas/monoid/__init__.py +++ b/graphblas/monoid/__init__.py @@ -21,6 +21,6 @@ def __getattr__(key): from ..core import operator # noqa: E402 isort:skip -from . import numpy # noqa: E402, F401 isort:skip +from . import numpy # noqa: E402 isort:skip del operator diff --git a/graphblas/op/__init__.py b/graphblas/op/__init__.py index 0ae897cd7..f60186ad8 100644 --- a/graphblas/op/__init__.py +++ b/graphblas/op/__init__.py @@ -1,22 +1,46 @@ # All items are dynamically added by classes in operator.py # This module acts as a container of all UnaryOp, BinaryOp, and Semiring instances _delayed = {} +_deprecated = {} def __dir__(): - return globals().keys() | _delayed.keys() + return globals().keys() | _delayed.keys() | _deprecated.keys() | {"ss"} def __getattr__(key): + if key in _deprecated: + import warnings + + warnings.warn( + f"`gb.op.{key}` is deprecated; please use `gb.op.ss.{key}` instead. " + f"`{key}` is specific to SuiteSparse:GraphBLAS. " + f"`gb.op.{key}` will be removed in version 2023.9.0 or later." + ) + rv = _deprecated[key] + globals()[key] = rv + return rv if key in _delayed: module = _delayed.pop(key) rv = getattr(module, key) globals()[key] = rv return rv + if key == "ss": + from .. import backend + + if backend != "suitesparse": + raise AttributeError( + f'module {__name__!r} only has attribute "ss" when backend is "suitesparse"' + ) + from importlib import import_module + + ss = import_module(".ss", __name__) + globals()["ss"] = ss + return ss raise AttributeError(f"module {__name__!r} has no attribute {key!r}") from ..core import operator # noqa: E402 isort:skip -from . import numpy # noqa: E402, F401 isort:skip +from . import numpy # noqa: E402 isort:skip del operator diff --git a/graphblas/op/ss.py b/graphblas/op/ss.py new file mode 100644 index 000000000..e45cbcda0 --- /dev/null +++ b/graphblas/op/ss.py @@ -0,0 +1,3 @@ +from ..core import operator + +del operator diff --git a/graphblas/semiring/__init__.py b/graphblas/semiring/__init__.py index ef78593b9..56e08ffa3 100644 --- a/graphblas/semiring/__init__.py +++ b/graphblas/semiring/__init__.py @@ -1,13 +1,25 @@ # All items are dynamically added by classes in operator.py # This module acts as a container of Semiring instances _delayed = {} +_deprecated = {} def __dir__(): - return globals().keys() | _delayed.keys() + return globals().keys() | _delayed.keys() | _deprecated.keys() | {"ss"} def __getattr__(key): + if key in _deprecated: + import warnings + + warnings.warn( + f"`gb.semiring.{key}` is deprecated; please use `gb.semiring.ss.{key}` instead. " + f"`{key}` is specific to SuiteSparse:GraphBLAS. " + f"`gb.semiring.{key}` will be removed in version 2023.9.0 or later." + ) + rv = _deprecated[key] + globals()[key] = rv + return rv if key in _delayed: func, kwargs = _delayed.pop(key) if type(kwargs["binaryop"]) is str: @@ -21,10 +33,22 @@ def __getattr__(key): rv = func(**kwargs) globals()[key] = rv return rv + if key == "ss": + from .. import backend + + if backend != "suitesparse": + raise AttributeError( + f'module {__name__!r} only has attribute "ss" when backend is "suitesparse"' + ) + from importlib import import_module + + ss = import_module(".ss", __name__) + globals()["ss"] = ss + return ss raise AttributeError(f"module {__name__!r} has no attribute {key!r}") from ..core import operator # noqa: E402 isort:skip -from . import numpy # noqa: E402, F401 isort:skip +from . import numpy # noqa: E402 isort:skip del operator diff --git a/graphblas/semiring/ss.py b/graphblas/semiring/ss.py new file mode 100644 index 000000000..e45cbcda0 --- /dev/null +++ b/graphblas/semiring/ss.py @@ -0,0 +1,3 @@ +from ..core import operator + +del operator diff --git a/graphblas/ss/__init__.py b/graphblas/ss/__init__.py index 4adfbca95..b36bc1bdc 100644 --- a/graphblas/ss/__init__.py +++ b/graphblas/ss/__init__.py @@ -1 +1 @@ -from ._core import about, concat, config, diag # noqa: F401 +from ._core import about, concat, config, diag diff --git a/graphblas/tests/conftest.py b/graphblas/tests/conftest.py index 818031005..c4947ac37 100644 --- a/graphblas/tests/conftest.py +++ b/graphblas/tests/conftest.py @@ -55,7 +55,9 @@ def save_records(): key for key in dir(gb.semiring) if isinstance( - getattr(gb.semiring, key), + getattr(gb.semiring, key) + if key not in gb.semiring._deprecated + else gb.semiring._deprecated[key], (gb.core.operator.Semiring, gb.core.operator.ParameterizedSemiring), ) ) @@ -63,7 +65,9 @@ def save_records(): key for key in dir(gb.binary) if isinstance( - getattr(gb.binary, key), + getattr(gb.binary, key) + if key not in gb.binary._deprecated + else gb.binary._deprecated[key], (gb.core.operator.BinaryOp, gb.core.operator.ParameterizedBinaryOp), ) ) diff --git a/graphblas/tests/pickle1.pkl b/graphblas/tests/pickle1.pkl index 76aec00d144871f02c1b92863a350088dd8d69c9..78d3a69ae2fe2dbaa4e329a831febe5869e86bf5 100644 GIT binary patch delta 392 zcmaDNwn>bofn}=tMwUcImL>P|LlV)p#7@armc<2>&R7jrz?}qkJKVf? zSu9Y!8IO9nAd)as1!1Og=Yo9%*JSq{t}9CfsK}ck3#1n8US7BtVNU*sP%H^ll_fIy P2-ge_4Ip#!lu|ta+ zO8)|o+9yD@Q!+kgJfCdLsU`uEX7kO@%g;GqD4u{#2B?nZQB{_K#*9;C_AanARQau2YF_{to diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index bfcf5da99..a500caa50 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -1421,50 +1421,51 @@ def test_reduce_agg(A): B.reduce_scalar(agg.vars, allow_empty=False) +@pytest.mark.skipif("not suitesparse") def test_reduce_agg_argminmax(A): # reduce_rowwise expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [1, 6, 5, 0, 5, 2, 4]) - w1b = A.reduce_rowwise(agg.argmin).new() + w1b = A.reduce_rowwise(agg.ss.argmin).new() assert w1b.isequal(expected) - w1c = A.T.reduce_columnwise(agg.argmin).new() + w1c = A.T.reduce_columnwise(agg.ss.argmin).new() assert w1c.isequal(expected) expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 4, 5, 0, 5, 2, 3]) - w2b = A.reduce_rowwise(agg.argmax).new() + w2b = A.reduce_rowwise(agg.ss.argmax).new() assert w2b.isequal(expected) - w2c = A.T.reduce_columnwise(agg.argmax).new() + w2c = A.T.reduce_columnwise(agg.ss.argmax).new() assert w2c.isequal(expected) # reduce_cols expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 0, 5, 0, 6, 2, 1]) - w7b = A.reduce_columnwise(agg.argmin).new() + w7b = A.reduce_columnwise(agg.ss.argmin).new() assert w7b.isequal(expected) - w7c = A.T.reduce_rowwise(agg.argmin).new() + w7c = A.T.reduce_rowwise(agg.ss.argmin).new() assert w7c.isequal(expected) expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 0, 6, 6, 1, 4, 1]) - w8b = A.reduce_columnwise(agg.argmax).new() + w8b = A.reduce_columnwise(agg.ss.argmax).new() assert w8b.isequal(expected) - w8c = A.T.reduce_rowwise(agg.argmax).new() + w8c = A.T.reduce_rowwise(agg.ss.argmax).new() assert w8c.isequal(expected) # reduce_scalar with pytest.raises( ValueError, match="Aggregator argmin may not be used with Matrix.reduce_scalar" ): - A.reduce_scalar(agg.argmin) + A.reduce_scalar(agg.ss.argmin) silly = agg.Aggregator( "silly", - composite=[agg.argmin, agg.argmax], + composite=[agg.ss.argmin, agg.ss.argmax], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.argmin], + types=[agg.ss.argmin], ) - v1 = A.reduce_rowwise(agg.argmin).new() - v2 = A.reduce_rowwise(agg.argmax).new() + v1 = A.reduce_rowwise(agg.ss.argmin).new() + v2 = A.reduce_rowwise(agg.ss.argmax).new() v3 = A.reduce_rowwise(silly).new() assert v3.isequal(binary.plus(v1 & v2).new()) - v1 = A.reduce_columnwise(agg.argmin).new() - v2 = A.reduce_columnwise(agg.argmax).new() + v1 = A.reduce_columnwise(agg.ss.argmin).new() + v2 = A.reduce_columnwise(agg.ss.argmax).new() v3 = A.reduce_columnwise(silly).new() assert v3.isequal(binary.plus(v1 & v2).new()) @@ -1472,100 +1473,101 @@ def test_reduce_agg_argminmax(A): A.reduce_scalar(silly).new() +@pytest.mark.skipif("not suitesparse") def test_reduce_agg_firstlast(A): # reduce_rowwise - w1 = A.reduce_rowwise(agg.first).new() + w1 = A.reduce_rowwise(agg.ss.first).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [2, 8, 1, 3, 7, 1, 5]) assert w1.isequal(expected) - w1b = A.T.reduce_columnwise(agg.first).new() + w1b = A.T.reduce_columnwise(agg.ss.first).new() assert w1b.isequal(expected) - w2 = A.reduce_rowwise(agg.last).new() + w2 = A.reduce_rowwise(agg.ss.last).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 4, 1, 3, 7, 1, 3]) assert w2.isequal(expected) - w2b = A.T.reduce_columnwise(agg.last).new() + w2b = A.T.reduce_columnwise(agg.ss.last).new() assert w2b.isequal(expected) # reduce_columnwise - w3 = A.reduce_columnwise(agg.first).new() + w3 = A.reduce_columnwise(agg.ss.first).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 2, 3, 3, 8, 1, 4]) assert w3.isequal(expected) - w3b = A.T.reduce_rowwise(agg.first).new() + w3b = A.T.reduce_rowwise(agg.ss.first).new() assert w3b.isequal(expected) - w4 = A.reduce_columnwise(agg.last).new() + w4 = A.reduce_columnwise(agg.ss.last).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 2, 5, 7, 3, 7, 4]) assert w4.isequal(expected) - w4b = A.T.reduce_rowwise(agg.last).new() + w4b = A.T.reduce_rowwise(agg.ss.last).new() assert w4b.isequal(expected) # reduce_scalar - w5 = A.reduce_scalar(agg.first).new() + w5 = A.reduce_scalar(agg.ss.first).new() assert w5 == 2 - w6 = A.reduce_scalar(agg.last).new() + w6 = A.reduce_scalar(agg.ss.last).new() assert w6 == 3 B = Matrix(float, nrows=2, ncols=3) - assert B.reduce_scalar(agg.first).new().is_empty - assert B.reduce_scalar(agg.last).new().is_empty - w7 = B.reduce_rowwise(agg.first).new() + assert B.reduce_scalar(agg.ss.first).new().is_empty + assert B.reduce_scalar(agg.ss.last).new().is_empty + w7 = B.reduce_rowwise(agg.ss.first).new() assert w7.isequal(Vector(float, size=B.nrows)) - w8 = B.reduce_columnwise(agg.last).new() + w8 = B.reduce_columnwise(agg.ss.last).new() assert w8.isequal(Vector(float, size=B.ncols)) silly = agg.Aggregator( "silly", - composite=[agg.first, agg.last], + composite=[agg.ss.first, agg.ss.last], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.first], + types=[agg.ss.first], ) - v1 = A.reduce_rowwise(agg.first).new() - v2 = A.reduce_rowwise(agg.last).new() + v1 = A.reduce_rowwise(agg.ss.first).new() + v2 = A.reduce_rowwise(agg.ss.last).new() v3 = A.reduce_rowwise(silly).new() assert v3.isequal(binary.plus(v1 & v2).new()) - s1 = A.reduce_scalar(agg.first).new() - s2 = A.reduce_scalar(agg.last).new() + s1 = A.reduce_scalar(agg.ss.first).new() + s2 = A.reduce_scalar(agg.ss.last).new() s3 = A.reduce_scalar(silly).new() assert s3.isequal(s1.value + s2.value) def test_reduce_agg_firstlast_index(A): # reduce_rowwise - w1 = A.reduce_rowwise(agg.first_index).new() + w1 = A.reduce_rowwise(agg.ss.first_index).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [1, 4, 5, 0, 5, 2, 2]) assert w1.isequal(expected) - w1b = A.T.reduce_columnwise(agg.first_index).new() + w1b = A.T.reduce_columnwise(agg.ss.first_index).new() assert w1b.isequal(expected) - w2 = A.reduce_rowwise(agg.last_index).new() + w2 = A.reduce_rowwise(agg.ss.last_index).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 6, 5, 2, 5, 2, 4]) assert w2.isequal(expected) - w2b = A.T.reduce_columnwise(agg.last_index).new() + w2b = A.T.reduce_columnwise(agg.ss.last_index).new() assert w2b.isequal(expected) # reduce_columnwise - w3 = A.reduce_columnwise(agg.first_index).new() + w3 = A.reduce_columnwise(agg.ss.first_index).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 0, 3, 0, 1, 2, 1]) assert w3.isequal(expected) - w3b = A.T.reduce_rowwise(agg.first_index).new() + w3b = A.T.reduce_rowwise(agg.ss.first_index).new() assert w3b.isequal(expected) - w4 = A.reduce_columnwise(agg.last_index).new() + w4 = A.reduce_columnwise(agg.ss.last_index).new() expected = Vector.from_coo([0, 1, 2, 3, 4, 5, 6], [3, 0, 6, 6, 6, 4, 1]) assert w4.isequal(expected) - w4b = A.T.reduce_rowwise(agg.last_index).new() + w4b = A.T.reduce_rowwise(agg.ss.last_index).new() assert w4b.isequal(expected) # reduce_scalar with pytest.raises(ValueError, match="Aggregator first_index may not"): - A.reduce_scalar(agg.first_index).new() + A.reduce_scalar(agg.ss.first_index).new() with pytest.raises(ValueError, match="Aggregator last_index may not"): - A.reduce_scalar(agg.last_index).new() + A.reduce_scalar(agg.ss.last_index).new() silly = agg.Aggregator( "silly", - composite=[agg.first_index, agg.last_index], + composite=[agg.ss.first_index, agg.ss.last_index], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.first_index], + types=[agg.ss.first_index], ) - v1 = A.reduce_rowwise(agg.first_index).new() - v2 = A.reduce_rowwise(agg.last_index).new() + v1 = A.reduce_rowwise(agg.ss.first_index).new() + v2 = A.reduce_rowwise(agg.ss.last_index).new() v3 = A.reduce_rowwise(silly).new() assert v3.isequal(binary.plus(v1 & v2).new()) @@ -3666,22 +3668,27 @@ def test_udt(): info = A.ss.export("cooc") result = A.ss.import_any(**info) assert result.isequal(A) - result = unary.positioni(A).new() - expected = Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1], [0, 0, 1, 1]) - assert result.isequal(expected) AB = unary.one(select.tril(A).new()).new() BA = select.tril(unary.one(A).new()).new() assert AB.isequal(BA) assert Matrix.from_csc(*A.to_csc()).isequal(A) # Just make sure these work - for aggop in [agg.any_value, agg.first, agg.last, agg.count]: + for aggop in [agg.any_value, agg.count]: A.reduce_rowwise(aggop).new() A.reduce_columnwise(aggop).new() A.reduce_scalar(aggop).new() - for aggop in [agg.first_index, agg.last_index]: - A.reduce_rowwise(aggop).new() - A.reduce_columnwise(aggop).new() + if suitesparse: + result = unary.ss.positioni(A).new() + expected = Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1], [0, 0, 1, 1]) + assert result.isequal(expected) + for aggop in [agg.ss.first, agg.ss.last]: + A.reduce_rowwise(aggop).new() + A.reduce_columnwise(aggop).new() + A.reduce_scalar(aggop).new() + for aggop in [agg.ss.first_index, agg.ss.last_index]: + A.reduce_rowwise(aggop).new() + A.reduce_columnwise(aggop).new() A.clear() A[[0, 1], 1] = [(2, 3), (4, 5)] expected = Matrix.from_coo([0, 1], [1, 1], [(2, 3), (4, 5)], dtype=udt) diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index 9e10efc81..d0aa784ef 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -4,7 +4,7 @@ import pytest import graphblas as gb -from graphblas import agg, binary, dtypes, indexunary, monoid, op, select, semiring, unary +from graphblas import agg, backend, binary, dtypes, indexunary, monoid, op, select, semiring, unary from graphblas.core import lib, operator from graphblas.core.operator import BinaryOp, IndexUnaryOp, Monoid, Semiring, UnaryOp, get_semiring from graphblas.dtypes import ( @@ -27,6 +27,8 @@ from graphblas import Matrix, Vector # isort:skip (for dask-graphblas) +suitesparse = backend == "suitesparse" + def orig_types(op): return op.types.keys() - op.coercions.keys() @@ -49,15 +51,17 @@ def test_op_repr(): def test_unaryop(): assert unary.ainv["INT32"].gb_obj == lib.GrB_AINV_INT32 assert unary.ainv[dtypes.UINT16].gb_obj == lib.GrB_AINV_UINT16 - assert orig_types(unary.positioni) == {INT32, INT64} - assert orig_types(unary.positionj1) == {INT32, INT64} + if suitesparse: + assert orig_types(unary.ss.positioni) == {INT32, INT64} + assert orig_types(unary.ss.positionj1) == {INT32, INT64} def test_binaryop(): assert binary.plus["INT32"].gb_obj == lib.GrB_PLUS_INT32 assert binary.plus[dtypes.UINT16].gb_obj == lib.GrB_PLUS_UINT16 - assert orig_types(binary.firsti) == {INT32, INT64} - assert orig_types(binary.secondj1) == {INT32, INT64} + if suitesparse: + assert orig_types(binary.ss.firsti) == {INT32, INT64} + assert orig_types(binary.ss.secondj1) == {INT32, INT64} def test_monoid(): @@ -68,7 +72,8 @@ def test_monoid(): def test_semiring(): assert semiring.min_plus["INT32"].gb_obj == lib.GrB_MIN_PLUS_SEMIRING_INT32 assert semiring.min_plus[dtypes.UINT16].gb_obj == lib.GrB_MIN_PLUS_SEMIRING_UINT16 - assert orig_types(semiring.min_firsti) == {INT32, INT64} + if suitesparse: + assert orig_types(semiring.ss.min_firsti) == {INT32, INT64} def test_agg(): @@ -910,7 +915,11 @@ def test_create_semiring(): cur_monoid = getattr(monoid, monoid_name) if not isinstance(cur_monoid, Monoid): continue - cur_binary = getattr(binary, binary_name) + cur_binary = ( + getattr(binary, binary_name) + if binary_name not in binary._deprecated + else binary._deprecated[binary_name] + ) if not isinstance(cur_binary, BinaryOp): continue Semiring.register_anonymous(cur_monoid, cur_binary) @@ -931,7 +940,8 @@ def test_commutes(): assert semiring.plus_times.is_commutative assert semiring.any_first.commutes_to is semiring.any_second assert semiring.plus_times.is_commutative - assert semiring.min_secondi.commutes_to is semiring.min_firstj + if suitesparse: + assert semiring.ss.min_secondi.commutes_to is semiring.ss.min_firstj assert semiring.plus_pow.commutes_to is semiring.plus_rpow assert not semiring.plus_pow.is_commutative assert binary.isclose.commutes_to is binary.isclose @@ -957,7 +967,8 @@ def test_commutes(): assert semiring.plus_times[int].is_commutative assert semiring.any_first[int].commutes_to is semiring.any_second[int] assert semiring.plus_times[int].is_commutative - assert semiring.min_secondi[int].commutes_to is semiring.min_firstj[int] + if suitesparse: + assert semiring.ss.min_secondi[int].commutes_to is semiring.ss.min_firstj[int] assert semiring.plus_pow[int].commutes_to is semiring.plus_rpow[int] assert not semiring.plus_pow[int].is_commutative assert binary.isclose(0.1)[int].commutes_to is binary.isclose(0.1)[int] @@ -971,7 +982,10 @@ def test_commutes(): # Stress test (this can create extra semirings) names = dir(semiring) for name in names: - val = getattr(semiring, name) + if name in semiring._deprecated: + val = semiring._deprecated[name] + else: + val = getattr(semiring, name) if not hasattr(val, "commutes_to"): continue assert val.commutes_to is None or isinstance(val.commutes_to, type(val)) @@ -1064,25 +1078,23 @@ def test_lazy_op(): def test_positional(): - assert unary.positioni.is_positional - assert unary.positioni1[int].is_positional - assert unary.positionj1.is_positional - assert unary.positionj[float].is_positional assert not unary.exp.is_positional assert not unary.abs[bool].is_positional - - assert binary.firsti.is_positional - assert binary.secondj1[int].is_positional assert not binary.plus.is_positional assert not binary.minus[float].is_positional - assert not monoid.plus.is_positional assert not monoid.plus[int].is_positional - - assert semiring.any_firsti.is_positional - assert semiring.any_secondj[int].is_positional assert not semiring.any_first.is_positional assert not semiring.any_second[int].is_positional + if suitesparse: + assert unary.ss.positioni.is_positional + assert unary.ss.positioni1[int].is_positional + assert unary.ss.positionj1.is_positional + assert unary.ss.positionj[float].is_positional + assert binary.ss.firsti.is_positional + assert binary.ss.secondj1[int].is_positional + assert semiring.ss.any_firsti.is_positional + assert semiring.ss.any_secondj[int].is_positional def test_udt(): @@ -1166,9 +1178,10 @@ def _udt_first(x, y): expected = Vector(int, size=v.size) expected(result.S) << 1 assert result.isequal(expected) - result = v.apply(gb.unary.positioni).new() - expected = expected.apply(gb.unary.positioni).new() - assert result.isequal(expected) + if suitesparse: + result = v.apply(gb.unary.ss.positioni).new() + expected = expected.apply(gb.unary.ss.positioni).new() + assert result.isequal(expected) result = indexunary.rowindex(v).new() assert result.isequal(Vector.from_coo([0, 1, 2], [0, 1, 2])) @@ -1194,8 +1207,9 @@ def badfunc2(x, y): # pragma: no cover (numba) assert binary.first[udt].return_type is udt assert binary.first[udt].commutes_to is binary.second[udt] - assert semiring.any_firsti[int].commutes_to is semiring.any_secondj[int] - assert semiring.any_firsti[udt].commutes_to is semiring.any_secondj[udt] + if suitesparse: + assert semiring.ss.any_firsti[int].commutes_to is semiring.ss.any_secondj[int] + assert semiring.ss.any_firsti[udt].commutes_to is semiring.ss.any_secondj[udt] assert binary.second[udt].type is udt assert binary.second[udt].type2 is udt @@ -1216,10 +1230,13 @@ def test_dir(): def test_semiring_commute_exists(): from .conftest import orig_semirings - vals = {getattr(semiring, key) for key in orig_semirings} + vals = { + semiring._deprecated[key] if key in semiring._deprecated else getattr(semiring, key) + for key in orig_semirings + } missing = set() for key in orig_semirings: - val = getattr(semiring, key) + val = semiring._deprecated[key] if key in semiring._deprecated else getattr(semiring, key) commutes_to = val.commutes_to if commutes_to is not None and commutes_to not in vals: # pragma: no cover (debug) missing.add(commutes_to.name) @@ -1230,10 +1247,13 @@ def test_semiring_commute_exists(): def test_binaryop_commute_exists(): from .conftest import orig_binaryops - vals = {getattr(binary, key) for key in orig_binaryops} + vals = { + binary._deprecated[key] if key in binary._deprecated else getattr(binary, key) + for key in orig_binaryops + } missing = set() for key in orig_binaryops: - val = getattr(binary, key) + val = binary._deprecated[key] if key in binary._deprecated else getattr(binary, key) commutes_to = val.commutes_to if commutes_to is not None and commutes_to not in vals: # pragma: no cover (debug) missing.add(commutes_to.name) diff --git a/graphblas/tests/test_operator_types.py b/graphblas/tests/test_operator_types.py index a93e1808b..68fde247e 100644 --- a/graphblas/tests/test_operator_types.py +++ b/graphblas/tests/test_operator_types.py @@ -1,7 +1,7 @@ import itertools from collections import defaultdict -from graphblas import binary, dtypes, monoid, semiring, unary +from graphblas import backend, binary, dtypes, monoid, semiring, unary from graphblas.core import operator from graphblas.dtypes import ( BOOL, @@ -23,6 +23,8 @@ FC32 = "FC32" FC64 = "FC64" +suitesparse = backend == "suitesparse" + BOOL = frozenset({BOOL}) UINT = frozenset({UINT8, UINT16, UINT32, UINT64}) INT = frozenset({INT8, INT16, INT32, INT64, UINT8, UINT16, UINT32, UINT64}) @@ -46,7 +48,6 @@ "sin", "sinh", "sqrt", "tan", "tanh", "trunc", }, (ALL, NOFC): {"abs"}, - (ALL, POS): {"positioni", "positioni1", "positionj", "positionj1"}, (FCFP, BOOL): {"isfinite", "isinf", "isnan"}, (FC, FC): {"conj"}, (FC, FP): {"carg", "cimag", "creal"}, @@ -55,6 +56,9 @@ (NOFC, FP): {"cbrt", "erf", "erfc", "lgamma", "tgamma"}, (NOFC, NOFC): {"lnot"}, } +if suitesparse: + UNARY[(ALL, POS)] = {"positioni", "positioni1", "positionj", "positionj1"} + BINARY = { (ALL, ALL): { "any", "cdiv", "first", "iseq", "isne", "minus", "pair", "plus", "pow", "rdiv", @@ -64,9 +68,6 @@ (ALL, FCFP): {"truediv", "rtruediv"}, (ALL, NOBOOL): {"rpow"}, # different for pow, b/c rpow is a UDF (ALL, NOFC): {"absfirst", "abssecond"}, - (ALL, POS): { - "firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1", - }, (BOOLINT, FP): {"ldexp"}, (INT, INT): {"band", "bclr", "bget", "bor", "bset", "bshift", "bxnor", "bxor"}, (INT, INT64): {"binom"}, @@ -76,6 +77,11 @@ (NOFC, FPINT): {"floordiv", "rfloordiv"}, (NOFC, NOFC): {"isge", "isgt", "isle", "islt", "land", "lor", "lxor", "max", "min"}, } +if suitesparse: + BINARY[(ALL, POS)] = { + "firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1", + } + MONOID = { (UINT, UINT): {"band", "bor", "bxnor", "bxor"}, (BOOL, BOOL): {"eq"}, @@ -102,10 +108,6 @@ {"max"}, {"absfirst", "abssecond"}, ], - (ALL, POS): [ # POS, extra->INT64 - {"any", "max", "min", "plus", "times"}, - {"firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1"}, - ], (ALL, BOOL): [ # BOOL, extra->BOOL {"eq", "land", "lor", "lxnor", "lxor"}, {"first", "pair", "second"}, @@ -151,12 +153,17 @@ {"land", "lor", "lxor", "first", "second"}, ], } +if suitesparse: + _SEMIRING1[(ALL, POS)] = [ # POS, extra->INT64 + {"any", "max", "min", "plus", "times"}, + {"firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1"}, + ] # fmt: on _SEMIRING2 = defaultdict(lambda: (set(), set())) # {semiring: [input_types, output_types]} for key, (leftvals, rightvals) in _SEMIRING1.items(): for left, right in itertools.product(leftvals, rightvals): name = f"{left}_{right}" - if not hasattr(semiring, name): + if (not suitesparse or not hasattr(semiring.ss, name)) and not hasattr(semiring, name): continue _SEMIRING2[name][0].update(key[0]) _SEMIRING2[name][1].update(key[1]) @@ -312,7 +319,10 @@ def _run_test(module, typ, expected): expected = d seen = defaultdict(set) for name in dir(module): - val = getattr(module, name) + if name in getattr(module, "_deprecated", ()): + val = module._deprecated[name] + else: + val = getattr(module, name) if not isinstance(val, typ) or name in IGNORE: continue key = (frozenset(val.types.keys()), frozenset(val.types.values())) diff --git a/graphblas/tests/test_pickle.py b/graphblas/tests/test_pickle.py index 8a6684d49..f1dae22fa 100644 --- a/graphblas/tests/test_pickle.py +++ b/graphblas/tests/test_pickle.py @@ -6,6 +6,8 @@ import graphblas as gb +suitesparse = gb.backend == "suitesparse" + def unarypickle(x): return x + 1 # pragma: no cover (numba) @@ -104,7 +106,6 @@ def test_serialize(): "monoid.numpy.logaddexp[float]": gb.monoid.numpy.logaddexp[float], "semiring.numpy.logaddexp2_hypot[float]": gb.semiring.numpy.logaddexp2_hypot[float], "agg.sum": gb.agg.sum, - "agg.first[int]": gb.agg.first[int], "binary.absfirst": gb.binary.absfirst, "binary.absfirst[float]": gb.binary.absfirst[float], "binary.isclose": gb.binary.isclose, @@ -125,6 +126,8 @@ def test_serialize(): "all_indices": gb.core.expr._ALL_INDICES, "replace": gb.replace, } + if suitesparse: + d["agg.ss.first[int]"] = gb.agg.ss.first[int] try: pkl = pickle.dumps(d) except Exception: # pragma: no cover (debug) @@ -171,7 +174,8 @@ def check_values(d): assert d["monoid.numpy.logaddexp[float]"] is gb.monoid.numpy.logaddexp[float] assert d["semiring.numpy.logaddexp2_hypot[float]"] is gb.semiring.numpy.logaddexp2_hypot[float] assert d["agg.sum"] is gb.agg.sum - assert d["agg.first[int]"] is gb.agg.first[int] + if suitesparse and "agg.ss.first[int]" in d: + assert d["agg.ss.first[int]"] is gb.agg.ss.first[int] assert d["binary.absfirst"] is gb.binary.absfirst assert d["binary.absfirst[float]"] is gb.binary.absfirst[float] assert d["binary.isclose"] is gb.binary.isclose diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 3e561c168..e6b339b1d 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -915,47 +915,50 @@ def test_reduce_agg(v): assert s.is_empty +@pytest.mark.skipif("not suitesparse") def test_reduce_agg_argminmax(v): - assert v.reduce(agg.argmin).new() == 6 - assert v.reduce(agg.argmax).new() == 4 + assert v.reduce(agg.ss.argmin).new() == 6 + assert v.reduce(agg.ss.argmax).new() == 4 silly = agg.Aggregator( "silly", - composite=[agg.argmin, agg.argmax], + composite=[agg.ss.argmin, agg.ss.argmax], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.argmin], + types=[agg.ss.argmin], ) s = v.reduce(silly).new() assert s == 10 +@pytest.mark.skipif("not suitesparse") def test_reduce_agg_firstlast(v): empty = Vector(int, size=4) - assert empty.reduce(agg.first).new().is_empty - assert empty.reduce(agg.last).new().is_empty + assert empty.reduce(agg.ss.first).new().is_empty + assert empty.reduce(agg.ss.last).new().is_empty - assert v.reduce(agg.first).new() == 1 - assert v.reduce(agg.last).new() == 0 + assert v.reduce(agg.ss.first).new() == 1 + assert v.reduce(agg.ss.last).new() == 0 silly = agg.Aggregator( "silly", - composite=[agg.first, agg.last], + composite=[agg.ss.first, agg.ss.last], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.first], + types=[agg.ss.first], ) s = v.reduce(silly).new() assert s == 1 +@pytest.mark.skipif("not suitesparse") def test_reduce_agg_firstlast_index(v): - assert v.reduce(agg.first_index).new() == 1 - assert v.reduce(agg.last_index).new() == 6 + assert v.reduce(agg.ss.first_index).new() == 1 + assert v.reduce(agg.ss.last_index).new() == 6 silly = agg.Aggregator( "silly", - composite=[agg.first_index, agg.last_index], + composite=[agg.ss.first_index, agg.ss.last_index], finalize=lambda x, y, opts: binary.plus(x & y), - types=[agg.first_index], + types=[agg.ss.first_index], ) s = v.reduce(silly).new() assert s == 7 @@ -2037,25 +2040,28 @@ def test_udt(): assert v[:].new().isequal(v) indices, values = v.to_coo() assert v.isequal(Vector.from_coo(indices, values)) - result = unary.positioni(v).new() - expected = Vector.from_coo([0, 1, 2], [0, 1, 2]) - assert result.isequal(expected) - result = binary.firsti(v & v).new() - assert result.isequal(expected) - result = semiring.min_firsti(v @ v).new() - assert result == 0 assert v.reduce(agg.any_value).new() == s - assert v.reduce(agg.first).new() == s - assert v.reduce(agg.last).new() == s assert v.reduce(agg.exists).new() == 1 - assert v.reduce(agg.first_index).new() == 0 - assert v.reduce(agg.last_index).new() == 2 assert v.reduce(agg.count).new() == 3 if suitesparse: + result = unary.ss.positioni(v).new() + expected = Vector.from_coo([0, 1, 2], [0, 1, 2]) + assert result.isequal(expected) + assert v.reduce(agg.ss.first).new() == s + assert v.reduce(agg.ss.last).new() == s + assert v.reduce(agg.ss.first_index).new() == 0 + assert v.reduce(agg.ss.last_index).new() == 2 + result = binary.ss.firsti(v & v).new() + assert result.isequal(expected) + result = semiring.ss.min_firsti(v @ v).new() + assert result == 0 info = v.ss.export() result = Vector.ss.import_any(**info) assert result.isequal(v) - for aggop in [agg.any_value, agg.first, agg.last, agg.count, agg.first_index, agg.last_index]: + aggs = [agg.any_value, agg.count] + if suitesparse: + aggs.extend([agg.ss.first, agg.ss.last, agg.ss.first_index, agg.ss.last_index]) + for aggop in aggs: v.reduce(aggop).new() v.clear() v[[0, 1]] = [(2, 3), (4, 5)] @@ -2117,7 +2123,10 @@ def test_udt(): assert s == t assert t == s # Just make sure these work - for aggop in [agg.any_value, agg.first, agg.last, agg.count, agg.first_index, agg.last_index]: + aggs = [agg.any_value, agg.count] + if suitesparse: + aggs.extend([agg.ss.first, agg.ss.last, agg.ss.first_index, agg.ss.last_index]) + for aggop in aggs: v.reduce(aggop).new() v.clear() v[[0, 1]] = [[2, 3, 4], [5, 6, 7]] diff --git a/graphblas/unary/__init__.py b/graphblas/unary/__init__.py index 3fcf2cad5..34bd9bad6 100644 --- a/graphblas/unary/__init__.py +++ b/graphblas/unary/__init__.py @@ -1,22 +1,46 @@ # All items are dynamically added by classes in operator.py # This module acts as a container of UnaryOp instances _delayed = {} +_deprecated = {} def __dir__(): - return globals().keys() | _delayed.keys() + return globals().keys() | _delayed.keys() | _deprecated.keys() | {"ss"} def __getattr__(key): + if key in _deprecated: + import warnings + + warnings.warn( + f"`gb.unary.{key}` is deprecated; please use `gb.unary.ss.{key}` instead. " + f"`{key}` is specific to SuiteSparse:GraphBLAS. " + f"`gb.unary.{key}` will be removed in version 2023.9.0 or later." + ) + rv = _deprecated[key] + globals()[key] = rv + return rv if key in _delayed: func, kwargs = _delayed.pop(key) rv = func(**kwargs) globals()[key] = rv return rv + if key == "ss": + from .. import backend + + if backend != "suitesparse": + raise AttributeError( + f'module {__name__!r} only has attribute "ss" when backend is "suitesparse"' + ) + from importlib import import_module + + ss = import_module(".ss", __name__) + globals()["ss"] = ss + return ss raise AttributeError(f"module {__name__!r} has no attribute {key!r}") from ..core import operator # noqa: E402 isort:skip -from . import numpy # noqa: E402, F401 isort:skip +from . import numpy # noqa: E402 isort:skip del operator diff --git a/graphblas/unary/ss.py b/graphblas/unary/ss.py new file mode 100644 index 000000000..e45cbcda0 --- /dev/null +++ b/graphblas/unary/ss.py @@ -0,0 +1,3 @@ +from ..core import operator + +del operator diff --git a/pyproject.toml b/pyproject.toml index 041033b65..ee2028b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,12 +95,14 @@ ignore = [ "D203", # 1 blank line required before class docstring (Note: conflicts with D211, which is preferred) "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) + # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) ] [tool.ruff.per-file-ignores] "graphblas/core/operator.py" = ["S102"] "graphblas/tests/*py" = ["S101", "T201"] "graphblas/tests/test_dtype.py" = ["UP003"] "graphblas/tests/test_formatting.py" = ["E501"] +"graphblas/**/__init__.py" = ["F401"] "scripts/create_pickle.py" = ["F403", "F405"] [tool.pylint.messages_control] diff --git a/scripts/create_pickle.py b/scripts/create_pickle.py index 4e62ef7fd..9a74a09a1 100755 --- a/scripts/create_pickle.py +++ b/scripts/create_pickle.py @@ -53,7 +53,7 @@ def pickle1(filename): "monoid.numpy.logaddexp[float]": gb.monoid.numpy.logaddexp[float], "semiring.numpy.logaddexp2_hypot[float]": gb.semiring.numpy.logaddexp2_hypot[float], "agg.sum": gb.agg.sum, - "agg.first[int]": gb.agg.first[int], + # "agg.ss.first[int]": gb.agg.ss.first[int], "binary.absfirst": gb.binary.absfirst, "binary.absfirst[float]": gb.binary.absfirst[float], "binary.isclose": gb.binary.isclose, diff --git a/setup.cfg b/setup.cfg index 8d3471de5..210e81e40 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,11 +10,13 @@ exclude = extend-ignore = E203, SIM105, + SIM401, # E203 whitespace before ':' (to be compatible with black) per-file-ignores = scripts/create_pickle.py:F403,F405, graphblas/tests/*.py:T201, graphblas/core/ss/matrix.py:SIM113, + graphblas/**/__init__.py:F401, [build_sphinx] all-files = 1 From 98494718185ad6b86293e4f769b2dc5b4df4e1f1 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 17:32:10 -0600 Subject: [PATCH 02/20] Test deprecations --- graphblas/agg/__init__.py | 3 +- graphblas/binary/__init__.py | 3 +- graphblas/core/operator.py | 23 +++++++++------ graphblas/op/__init__.py | 3 +- graphblas/semiring/__init__.py | 3 +- graphblas/tests/conftest.py | 6 ++-- graphblas/tests/pickle1-vanilla.pkl | Bin 2689 -> 2642 bytes graphblas/tests/pickle1.pkl | Bin 2866 -> 2913 bytes graphblas/tests/pickle3.pkl | Bin 875 -> 875 bytes graphblas/tests/test_matrix.py | 1 + graphblas/tests/test_op.py | 38 ++++++++++++++++++++++++- graphblas/tests/test_operator_types.py | 13 ++++++--- graphblas/unary/__init__.py | 3 +- scripts/create_pickle.py | 4 ++- 14 files changed, 78 insertions(+), 22 deletions(-) diff --git a/graphblas/agg/__init__.py b/graphblas/agg/__init__.py index 9ce96e675..868851709 100644 --- a/graphblas/agg/__init__.py +++ b/graphblas/agg/__init__.py @@ -84,7 +84,8 @@ def __getattr__(key): warnings.warn( f"`gb.agg.{key}` is deprecated; please use `gb.agg.ss.{key}` instead. " f"`{key}` is specific to SuiteSparse:GraphBLAS. " - f"`gb.agg.{key}` will be removed in version 2023.9.0 or later." + f"`gb.agg.{key}` will be removed in version 2023.9.0 or later.", + DeprecationWarning, ) rv = _deprecated[key] globals()[key] = rv diff --git a/graphblas/binary/__init__.py b/graphblas/binary/__init__.py index b551503cd..089869eb7 100644 --- a/graphblas/binary/__init__.py +++ b/graphblas/binary/__init__.py @@ -22,7 +22,8 @@ def __getattr__(key): warnings.warn( f"`gb.binary.{key}` is deprecated; please use `gb.binary.ss.{key}` instead. " f"`{key}` is specific to SuiteSparse:GraphBLAS. " - f"`gb.binary.{key}` will be removed in version 2023.9.0 or later." + f"`gb.binary.{key}` will be removed in version 2023.9.0 or later.", + DeprecationWarning, ) rv = _deprecated[key] globals()[key] = rv diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 48b0c911c..19b19b227 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -961,7 +961,10 @@ def _remove_nesting(cls, funcname, *, module=None, modname=None, strict=True): def _find(cls, funcname): rv = cls._module for attr in funcname.split("."): - rv = getattr(rv, attr, None) + if attr in getattr(rv, "_deprecated", ()): + rv = rv._deprecated[attr] + else: + rv = getattr(rv, attr, None) if rv is None: break return rv @@ -2392,17 +2395,19 @@ def _initialize(cls): left = binary._deprecated[left_name] else: left = getattr(binary, left_name) + if backend == "suitesparse" and right_name in _SS_OPERATORS: + left._commutes_to = f"ss.{right_name}" + else: + left._commutes_to = right_name if right_name not in binary._delayed: if right_name in _SS_OPERATORS: right = binary._deprecated[right_name] else: right = getattr(binary, right_name) - if backend == "suitesparse" and left_name in _SS_OPERATORS: - left._commutes_to = f"ss.{right_name}" - right._commutes_to = f"ss.{left_name}" - else: - left._commutes_to = right_name - right._commutes_to = left_name + if backend == "suitesparse" and left_name in _SS_OPERATORS: + right._commutes_to = f"ss.{left_name}" + else: + right._commutes_to = left_name for name in cls._commutative: cur_op = getattr(binary, name) cur_op._commutes_to = name @@ -3306,14 +3311,14 @@ def get_semiring(monoid, binaryop, name=None): rv = ( getattr(module, funcname) if funcname in module.__dict__ or funcname in module._delayed - else None + else getattr(module, "_deprecated", {}).get(funcname) ) if rv is None and name != canonical_name: module, funcname = Semiring._remove_nesting(name, strict=False) rv = ( getattr(module, funcname) if funcname in module.__dict__ or funcname in module._delayed - else None + else getattr(module, "_deprecated", {}).get(funcname) ) if rv is None: rv = Semiring.register_new(canonical_name, monoid, binaryop) diff --git a/graphblas/op/__init__.py b/graphblas/op/__init__.py index f60186ad8..b02b5228c 100644 --- a/graphblas/op/__init__.py +++ b/graphblas/op/__init__.py @@ -15,7 +15,8 @@ def __getattr__(key): warnings.warn( f"`gb.op.{key}` is deprecated; please use `gb.op.ss.{key}` instead. " f"`{key}` is specific to SuiteSparse:GraphBLAS. " - f"`gb.op.{key}` will be removed in version 2023.9.0 or later." + f"`gb.op.{key}` will be removed in version 2023.9.0 or later.", + DeprecationWarning, ) rv = _deprecated[key] globals()[key] = rv diff --git a/graphblas/semiring/__init__.py b/graphblas/semiring/__init__.py index 56e08ffa3..be181d405 100644 --- a/graphblas/semiring/__init__.py +++ b/graphblas/semiring/__init__.py @@ -15,7 +15,8 @@ def __getattr__(key): warnings.warn( f"`gb.semiring.{key}` is deprecated; please use `gb.semiring.ss.{key}` instead. " f"`{key}` is specific to SuiteSparse:GraphBLAS. " - f"`gb.semiring.{key}` will be removed in version 2023.9.0 or later." + f"`gb.semiring.{key}` will be removed in version 2023.9.0 or later.", + DeprecationWarning, ) rv = _deprecated[key] globals()[key] = rv diff --git a/graphblas/tests/conftest.py b/graphblas/tests/conftest.py index c4947ac37..66a3721f9 100644 --- a/graphblas/tests/conftest.py +++ b/graphblas/tests/conftest.py @@ -54,7 +54,8 @@ def save_records(): orig_semirings.update( key for key in dir(gb.semiring) - if isinstance( + if key != "ss" + and isinstance( getattr(gb.semiring, key) if key not in gb.semiring._deprecated else gb.semiring._deprecated[key], @@ -64,7 +65,8 @@ def save_records(): orig_binaryops.update( key for key in dir(gb.binary) - if isinstance( + if key != "ss" + and isinstance( getattr(gb.binary, key) if key not in gb.binary._deprecated else gb.binary._deprecated[key], diff --git a/graphblas/tests/pickle1-vanilla.pkl b/graphblas/tests/pickle1-vanilla.pkl index 42ff4a7c7101f289ff95c97d756894c21b867591..36ea207603fdcf1d9cc778b666097dfceeeede49 100644 GIT binary patch delta 325 zcmZn^y(GfYz%tc+Ba00CWKVWy#$%Iv*-IGDPG;ipVZ1fjpF>INLPmEFhf`)=Vo{}k z!Ia5UGA_4G37V4eAmiHPZVt72kTjcbeqMfN3PSQ_#tUzTcA&Bz{^Hc!%%aS^^!S3z zS0>$zX4r5;R$k z-Iek5} cD~X+wu`J_k4-c~Vl*v;v*xRNAO)1p_0FWXcK>z>% delta 34 qcmaDTwn>bofn}=tMwUcI<|X&@H)k+1F){n5&)J;Ie3+f7R1W~k1Pc`a diff --git a/graphblas/tests/pickle3.pkl b/graphblas/tests/pickle3.pkl index 38d57dad1332876e1837be1289beabe43daf6bed..d04a53cb9ea47b9513d1c583676e23e3f585c838 100644 GIT binary patch delta 25 fcmaFO_L^INT64 {"any", "max", "min", "plus", "times"}, {"firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1"}, @@ -163,7 +166,7 @@ for key, (leftvals, rightvals) in _SEMIRING1.items(): for left, right in itertools.product(leftvals, rightvals): name = f"{left}_{right}" - if (not suitesparse or not hasattr(semiring.ss, name)) and not hasattr(semiring, name): + if name not in dir(semiring): continue _SEMIRING2[name][0].update(key[0]) _SEMIRING2[name][1].update(key[1]) @@ -319,6 +322,8 @@ def _run_test(module, typ, expected): expected = d seen = defaultdict(set) for name in dir(module): + if name == "ss": + continue if name in getattr(module, "_deprecated", ()): val = module._deprecated[name] else: diff --git a/graphblas/unary/__init__.py b/graphblas/unary/__init__.py index 34bd9bad6..92e508df3 100644 --- a/graphblas/unary/__init__.py +++ b/graphblas/unary/__init__.py @@ -15,7 +15,8 @@ def __getattr__(key): warnings.warn( f"`gb.unary.{key}` is deprecated; please use `gb.unary.ss.{key}` instead. " f"`{key}` is specific to SuiteSparse:GraphBLAS. " - f"`gb.unary.{key}` will be removed in version 2023.9.0 or later." + f"`gb.unary.{key}` will be removed in version 2023.9.0 or later.", + DeprecationWarning, ) rv = _deprecated[key] globals()[key] = rv diff --git a/scripts/create_pickle.py b/scripts/create_pickle.py index 9a74a09a1..0e0c5ceae 100755 --- a/scripts/create_pickle.py +++ b/scripts/create_pickle.py @@ -13,6 +13,7 @@ def pickle1(filename): + suitesparse = gb.backend == "suitesparse" v = gb.Vector.from_coo([1], 2) unary_pickle = gb.core.operator.UnaryOp.register_new("unary_pickle", unarypickle) @@ -53,7 +54,6 @@ def pickle1(filename): "monoid.numpy.logaddexp[float]": gb.monoid.numpy.logaddexp[float], "semiring.numpy.logaddexp2_hypot[float]": gb.semiring.numpy.logaddexp2_hypot[float], "agg.sum": gb.agg.sum, - # "agg.ss.first[int]": gb.agg.ss.first[int], "binary.absfirst": gb.binary.absfirst, "binary.absfirst[float]": gb.binary.absfirst[float], "binary.isclose": gb.binary.isclose, @@ -72,6 +72,8 @@ def pickle1(filename): "all_indices": gb.core.expr._ALL_INDICES, "replace": gb.replace, } + if suitesparse: + d["agg.ss.first[int]"] = gb.agg.ss.first[int] with open(filename, "wb") as f: pickle.dump(d, f) From fac68a9f3b7f6506962f782d3f80777e2764c8f7 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 17:49:37 -0600 Subject: [PATCH 03/20] oops forgot a file --- graphblas/agg/ss.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 graphblas/agg/ss.py diff --git a/graphblas/agg/ss.py b/graphblas/agg/ss.py new file mode 100644 index 000000000..c3f06c0a7 --- /dev/null +++ b/graphblas/agg/ss.py @@ -0,0 +1,3 @@ +from ..core import agg + +del agg From 0e069c5cc74ae927f24a9548b80047e3fc431953 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 17:57:22 -0600 Subject: [PATCH 04/20] Give our test output color in CI --- .github/workflows/test_and_build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index bf6dab681..eca8e0713 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -174,7 +174,7 @@ jobs: if [[ $H && $normal ]] ; then if [[ $macos ]] ; then echo " $vanilla" ; elif [[ $windows ]] ; then echo " $suitesparse" ; fi ; fi)$( \ if [[ $H && $bizarro ]] ; then if [[ $macos ]] ; then echo " $suitesparse" ; elif [[ $windows ]] ; then echo " $vanilla" ; fi ; fi) echo $args - coverage run -m pytest -Werror --randomly -v $args \ + coverage run -m pytest --color=yes -Werror --randomly -v $args \ ${{ matrix.slowtask == 'pytest_normal' && '--runslow' || '' }} - name: Unit tests (bizarro scalars) run: | @@ -209,7 +209,7 @@ jobs: if [[ $H && $normal ]] ; then if [[ $macos ]] ; then echo " $suitesparse" ; elif [[ $windows ]] ; then echo " $vanilla" ; fi ; fi)$( \ if [[ $H && $bizarro ]] ; then if [[ $macos ]] ; then echo " $vanilla" ; elif [[ $windows ]] ; then echo " $suitesparse" ; fi ; fi) echo $args - coverage run -a -m pytest -Werror --randomly -v $args \ + coverage run -a -m pytest --color=yes -Werror --randomly -v $args \ ${{ matrix.slowtask == 'pytest_bizarro' && '--runslow' || '' }} git checkout . # Undo changes to scalar default - name: Miscellaneous tests @@ -230,8 +230,8 @@ jobs: rm script.py # Tests whose coverage depend on order of tests :/ # TODO: understand why these are order-dependent and try to fix - coverage run -a -m pytest -Werror -x --no-mapnumpy -k test_binaryop_attributes_numpy graphblas/tests/test_op.py - # coverage run -a -m pytest -Werror -x --no-mapnumpy -k test_npmonoid graphblas/tests/test_numpyops.py --runslow + coverage run -a -m pytest --color=yes -Werror -x --no-mapnumpy -k test_binaryop_attributes_numpy graphblas/tests/test_op.py + # coverage run -a -m pytest --color=yes -Werror -x --no-mapnumpy -k test_npmonoid graphblas/tests/test_numpyops.py --runslow - name: Auto-generated code check if: matrix.slowtask == 'pylint' run: | From bcf4869bdc025f25382cd78dceda2757b457e824 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 18:20:10 -0600 Subject: [PATCH 05/20] Fix test_op_namespace --- graphblas/tests/test_op.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index a4bfb379e..e2496e837 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -713,11 +713,17 @@ def test_op_namespace(): for key, val in vars(select).items() if isinstance(val, (operator.OpBase, operator.ParameterizedUdf)) } - assert not unarynames - opnames, unarynames - opnames - assert not binarynames - opnames, binarynames - opnames + extra_unary = unarynames - opnames - unary._deprecated.keys() + assert not extra_unary + extra_binary = binarynames - opnames - binary._deprecated.keys() + assert not extra_binary assert not monoidnames - opnames, monoidnames - opnames - assert not semiringnames - opnames, semiringnames - opnames - assert not opnames - (unarynames | binarynames | monoidnames | semiringnames) + extra_semiring = semiringnames - opnames - semiring._deprecated.keys() + assert not extra_semiring + extra_ops = ( + opnames - (unarynames | binarynames | monoidnames | semiringnames) - op._deprecated.keys() + ) + assert not extra_ops # These are not part of the `op` namespace assert indexunarynames - opnames == indexunarynames, indexunarynames - opnames assert selectnames - opnames == selectnames, selectnames - opnames From 7cf2dfa555e99ba14404060b72f7724cbf955d1e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 18:28:17 -0600 Subject: [PATCH 06/20] Some coverage --- graphblas/core/operator.py | 6 +++--- graphblas/tests/test_op.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 19b19b227..cd1878640 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -1020,7 +1020,7 @@ def _initialize(cls, include_in_ops=True): if backend == "suitesparse": setattr(cls._module.ss, name, obj) cls._module._deprecated[name] = obj - if include_in_ops and not _hasop(op, name): + if include_in_ops and not _hasop(op, name): # pragma: no branch op._deprecated[name] = obj if backend == "suitesparse": setattr(op.ss, name, obj) @@ -2414,11 +2414,11 @@ def _initialize(cls): for left_name, right_name in cls._commutes_to_in_semiring.items(): if left_name in _SS_OPERATORS: left = binary._deprecated[left_name] - else: + else: # pragma: no cover (safety) left = getattr(binary, left_name) if right_name in _SS_OPERATORS: right = binary._deprecated[right_name] - else: + else: # pragma: no cover (safety) right = getattr(binary, right_name) left._semiring_commutes_to = right right._semiring_commutes_to = left diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index e2496e837..b3461e392 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -79,6 +79,8 @@ def test_semiring(): def test_agg(): assert repr(agg.count) == "agg.count" assert repr(agg.count["INT32"]) == "agg.count[INT32]" + if suitesparse: + assert repr(agg.ss.first) == "agg.ss.first" assert "INT64" in agg.sum_of_inverses assert agg.sum_of_inverses["INT64"].return_type == FP64 assert "BOOL" not in agg.sum_of_inverses From 67add6fe9062f73d942f31e547278cf3c8824e77 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 20:20:36 -0600 Subject: [PATCH 07/20] Coverage; also, require pylint to pass --- .github/workflows/test_and_build.yml | 10 ++++++++-- graphblas/core/operator.py | 12 ++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index eca8e0713..0e39e2579 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -250,9 +250,15 @@ jobs: coveralls --service=github - name: codecov uses: codecov/codecov-action@v3 - - name: Pylint (informational only; never fails) + - name: Pylint if: matrix.slowtask == 'pylint' - run: pylint --exit-zero graphblas/ + run: | + # If this fails, read the report and try to improve the code. + # You can run `pylint` locally: `pre-commit run --all-files --hook-stage manual` + # If you think we ought to always ignore a specific pylint code, you can add + # it to the `[tool.pylint.messages_control]` section in `pyproject.toml`. + # To ignore a pylint error for a single line, add `# pylint: disable=CODE`. + pylint --exit-zero graphblas/ - name: Notebooks Execution check if: matrix.slowtask == 'notebooks' run: jupyter nbconvert --to notebook --execute notebooks/*ipynb diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index cd1878640..b7ec2f78a 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -2667,8 +2667,6 @@ def binaryop(self): if self._binaryop is not None: return self._binaryop # Must be builtin - if self.name in _SS_OPERATORS: - return binary._deprecated[self.name] return getattr(binary, self.name) @property @@ -2950,15 +2948,9 @@ def _initialize(cls): source_name = f"{lname}_lxor" if not _hasop(semiring, target_name): continue - if target_name in _SS_OPERATORS: - target_op = semiring._deprecated[target_name] - else: - target_op = getattr(semiring, target_name) + target_op = getattr(semiring, target_name) if BOOL not in target_op.types: # pragma: no branch (safety) - if source_name in _SS_OPERATORS: - source_op = semiring._deprecated[source_name] - else: - source_op = getattr(semiring, source_name) + source_op = getattr(semiring, source_name) typed_op = source_op._typed_ops[BOOL] target_op.types[BOOL] = BOOL target_op._typed_ops[BOOL] = typed_op From 3fe1621f75a65cb9f3b896ffb2a99572cd610063 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 8 Jan 2023 20:42:47 -0600 Subject: [PATCH 08/20] More coverage --- .github/workflows/test_and_build.yml | 3 ++- environment.yml | 2 +- graphblas/tests/test_operator_types.py | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 0e39e2579..f30a7b887 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -254,7 +254,8 @@ jobs: if: matrix.slowtask == 'pylint' run: | # If this fails, read the report and try to improve the code. - # You can run `pylint` locally: `pre-commit run --all-files --hook-stage manual` + # You can run `pylint` locally: `pre-commit run --all-files --hook-stage manual`. + # It's faster to run on a single file: `pylint graphblas/path/to/file.py`. # If you think we ought to always ignore a specific pylint code, you can add # it to the `[tool.pylint.messages_control]` section in `pyproject.toml`. # To ignore a pylint error for a single line, add `# pylint: disable=CODE`. diff --git a/environment.yml b/environment.yml index e3260c38c..cba7ecb6a 100644 --- a/environment.yml +++ b/environment.yml @@ -32,6 +32,7 @@ dependencies: - matplotlib # For linting - pre-commit + - pylint # For testing - pytest-cov # For debugging @@ -82,7 +83,6 @@ dependencies: # - pycodestyle # - pydot # - pygraphviz - # - pylint # - pytest-runner # - pytest-xdist # - python-graphviz diff --git a/graphblas/tests/test_operator_types.py b/graphblas/tests/test_operator_types.py index 5946600a8..522b42ad2 100644 --- a/graphblas/tests/test_operator_types.py +++ b/graphblas/tests/test_operator_types.py @@ -57,7 +57,7 @@ (NOFC, NOFC): {"lnot"}, } # Remove "suitesparse-vailla" once these deprecations are removed -if backend in {"suitesparse", "suitesparse-vanilla"}: +if backend in {"suitesparse", "suitesparse-vanilla"}: # pragma: no branch (future) UNARY[(ALL, POS)] = {"positioni", "positioni1", "positionj", "positionj1"} BINARY = { @@ -79,7 +79,7 @@ (NOFC, NOFC): {"isge", "isgt", "isle", "islt", "land", "lor", "lxor", "max", "min"}, } # Remove "suitesparse-vailla" once these deprecations are removed -if backend in {"suitesparse", "suitesparse-vanilla"}: +if backend in {"suitesparse", "suitesparse-vanilla"}: # pragma: no branch (future) BINARY[(ALL, POS)] = { "firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1", } @@ -156,7 +156,7 @@ ], } # Remove "suitesparse-vailla" once these deprecations are removed -if backend in {"suitesparse", "suitesparse-vanilla"}: +if backend in {"suitesparse", "suitesparse-vanilla"}: # pragma: no branch (future) _SEMIRING1[(ALL, POS)] = [ # POS, extra->INT64 {"any", "max", "min", "plus", "times"}, {"firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", "secondj", "secondj1"}, From 5f097e1d401ba1ae0557284099f27b748f4ce18d Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 9 Jan 2023 15:35:09 -0600 Subject: [PATCH 09/20] Update pylint comments --- .github/workflows/test_and_build.yml | 4 ++-- .pre-commit-config.yaml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index f30a7b887..2e670ec35 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -254,8 +254,8 @@ jobs: if: matrix.slowtask == 'pylint' run: | # If this fails, read the report and try to improve the code. - # You can run `pylint` locally: `pre-commit run --all-files --hook-stage manual`. - # It's faster to run on a single file: `pylint graphblas/path/to/file.py`. + # It's best (fastest) to run on a single file at a time: `pylint graphblas/some/file.py`. + # You can also run `pylint` locally via: `pre-commit run --all-files --hook-stage manual`. # If you think we ought to always ignore a specific pylint code, you can add # it to the `[tool.pylint.messages_control]` section in `pyproject.toml`. # To ignore a pylint error for a single line, add `# pylint: disable=CODE`. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32c7dcf15..996f664d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.215 + rev: v0.0.216 hooks: - id: ruff args: [--force-exclude] @@ -81,6 +81,7 @@ repos: - repo: local hooks: # Add `--hook-stage manual` to pre-commit command to run (very slow) + # It's probably better (and faster!) to simply run `pylint graphblas/some/file.py` - id: pylint name: pylint entry: pylint From 02d9a2b0e0dad9d487846db3ee698e71aed194a6 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 10 Jan 2023 21:35:42 -0600 Subject: [PATCH 10/20] tweaks --- .github/workflows/test_and_build.yml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 2e670ec35..1ea5d6284 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -107,7 +107,7 @@ jobs: source upstream weights: | - 1 + 2 1 1 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 996f664d7..7f3fcb65c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.216 + rev: v0.0.217 hooks: - id: ruff args: [--force-exclude] From 3811452cd171f27e67ffbc5fab2ac325465ef837 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 10 Jan 2023 22:53:52 -0600 Subject: [PATCH 11/20] Deprecation note --- .pre-commit-config.yaml | 5 +++-- graphblas/agg/__init__.py | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f3fcb65c..773f0617d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,8 @@ repos: - repo: https://github.com/MarcoGorelli/auto-walrus rev: v0.2.2 hooks: - - id: auto-walrus + - id: auto-walrus + args: [--line-length, "100"] - repo: https://github.com/psf/black rev: 22.12.0 hooks: @@ -69,7 +70,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.217 + rev: v0.0.218 hooks: - id: ruff args: [--force-exclude] diff --git a/graphblas/agg/__init__.py b/graphblas/agg/__init__.py index 868851709..1447dd932 100644 --- a/graphblas/agg/__init__.py +++ b/graphblas/agg/__init__.py @@ -49,14 +49,19 @@ - harmonic_mean - root_mean_square -Custom recipes: - - first - - last +Custom recipes (specific to SuiteSparse:GraphBLAS): + - ss.first + - ss.last # These don't work with Matrix.reduce_scalar - - first_index - - last_index - - argmin - - argmax + - ss.first_index + - ss.last_index + - ss.argmin + - ss.argmax + +.. deprecated:: 2023.1.0 + Aggregators `first`, `last`, `first_index`, `last_index`, `argmin`, and `argmax` are + deprecated in the `agg` namespace such as `agg.first`. Use them from `agg.ss` namespace + instead such as `agg.ss.first`. Will be removed in version 2023.9.0 or later. # Possible aggregators: # - absolute_deviation, sum(abs(x - mean(x))), sum_absminus(x, mean(x)) From 930db215b17189d710e9b16624f0e078b9dad9e8 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 12 Jan 2023 14:43:25 -0600 Subject: [PATCH 12/20] ruff --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 773f0617d..2c4722246 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,7 +70,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.218 + rev: v0.0.219 hooks: - id: ruff args: [--force-exclude] From 35c0dee5281a3e5cebd622a4f756137448330f48 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 14 Jan 2023 13:19:38 -0600 Subject: [PATCH 13/20] ruff --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c4722246..bc2803c78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,7 +70,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.219 + rev: v0.0.221 hooks: - id: ruff args: [--force-exclude] diff --git a/pyproject.toml b/pyproject.toml index ee2028b07..b777f161c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ ignore = [ "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) + "PLR2004", # Magic number used in comparison, consider replacing magic with a constant variable ] [tool.ruff.per-file-ignores] "graphblas/core/operator.py" = ["S102"] From 3cdcd03b4339294310ad7b2e99af286ccf9e645d Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 14 Jan 2023 21:07:47 -0600 Subject: [PATCH 14/20] Update to flake8-bugbear==23.1.14 --- .pre-commit-config.yaml | 2 +- graphblas/core/formatting.py | 2 +- graphblas/tests/conftest.py | 2 +- graphblas/tests/test_dtype.py | 2 +- graphblas/tests/test_op.py | 2 +- graphblas/tests/test_vector.py | 2 +- pyproject.toml | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc2803c78..dbd55e76d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: # These versions need updated manually - flake8==6.0.0 - flake8-comprehensions==3.10.1 - - flake8-bugbear==22.12.6 + - flake8-bugbear==23.1.14 - flake8-simplify==0.19.3 - repo: https://github.com/asottile/yesqa rev: v1.4.0 diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index 305df05ae..ad1f7cc2c 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -626,7 +626,7 @@ def format_scalar_expression(expr): def create_header(type_name, keys, vals, *, lower_border=False, name="", quote=True): vals = [str(x) for x in vals] if name and quote: - name = f'"{name}"' + name = f'"{name}"' # noqa: B028 key_text = [] val_text = [] for key, val in zip(keys, vals): diff --git a/graphblas/tests/conftest.py b/graphblas/tests/conftest.py index 66a3721f9..3a5b850a2 100644 --- a/graphblas/tests/conftest.py +++ b/graphblas/tests/conftest.py @@ -38,7 +38,7 @@ def pytest_configure(config): gb.init(backend, blocking=blocking) print( - f'Running tests with "{backend}" backend, blocking={blocking}, ' + f"Running tests with {backend!r} backend, blocking={blocking}, " f"record={record}, mapnumpy={mapnumpy}, runslow={runslow}" ) if record: diff --git a/graphblas/tests/test_dtype.py b/graphblas/tests/test_dtype.py index 90fac5618..a54d08819 100644 --- a/graphblas/tests/test_dtype.py +++ b/graphblas/tests/test_dtype.py @@ -228,7 +228,7 @@ def test_dtype_to_from_string(): try: dtype2 = dtypes._string_to_dtype(s) except Exception: - with pytest.raises(Exception): + with pytest.raises(ValueError): lookup_dtype(dtype) else: assert dtype == dtype2 diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index b3461e392..5143558a7 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -137,7 +137,7 @@ def test_get_typed_op(): ) with pytest.raises(ValueError, match="Unknown binary or aggregator"): operator.get_typed_op("bad_op_name", dtypes.INT64, kind="binary|aggregator") - with pytest.raises(Exception): + with pytest.raises(AttributeError): # get_typed_op expects dtypes to already be dtypes operator.get_typed_op(binary.plus, dtypes.INT64, "bad dtype") diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index e6b339b1d..bb9e29460 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -2149,7 +2149,7 @@ def test_udt(): if suitesparse: vv = Vector.ss.deserialize(v.ss.serialize(), dtype=long_udt) assert v.isequal(vv, check_dtype=True) - with pytest.raises(Exception): + with pytest.raises(SyntaxError): # The size of the UDT name is limited Vector.ss.deserialize(v.ss.serialize()) # May be able to look up non-anonymous dtypes by name if their names are too long diff --git a/pyproject.toml b/pyproject.toml index b777f161c..90d7d79a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,7 @@ select = [ "RUF", # ruff-specific rules ] ignore = [ + "RUF100", # Unused `noqa` directive (unknown: `code`) (Note: I like requiring `noqa` to have codes, but ruff does not know all codes) "D203", # 1 blank line required before class docstring (Note: conflicts with D211, which is preferred) "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) From 98aedb783a769d078083448ef27edbf47a2d5355 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 14 Jan 2023 22:21:48 -0600 Subject: [PATCH 15/20] aha, that's how ruff does it --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 90d7d79a1..b7ad5eda7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,8 +91,11 @@ select = [ "PIE", # flake8-pie "RUF", # ruff-specific rules ] +external = [ + # noqa codes that ruff doesn't know about: https://github.com/charliermarsh/ruff#external + "B028", +] ignore = [ - "RUF100", # Unused `noqa` directive (unknown: `code`) (Note: I like requiring `noqa` to have codes, but ruff does not know all codes) "D203", # 1 blank line required before class docstring (Note: conflicts with D211, which is preferred) "SIM105", # Use contextlib.suppress(...) instead of try-except-pass (Note: try-except-pass is much faster) "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) From 292b317ed0aa1783cf43392c64c092722869cbd0 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 17 Jan 2023 17:42:54 -0600 Subject: [PATCH 16/20] Also deprecate erf, erfc, frexpe, frexpx, lgamma, tgamma --- .pre-commit-config.yaml | 31 ++++++++++++++++++++++++++----- docs/user_guide/operations.rst | 2 +- graphblas/core/operator.py | 10 +++++++++- graphblas/tests/test_op.py | 2 ++ graphblas/tests/test_vector.py | 2 +- notebooks/HPEC2022.ipynb | 2 +- notebooks/Louvain.ipynb | 4 +--- pyproject.toml | 2 ++ 8 files changed, 43 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dbd55e76d..d74afd229 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.10.1 + rev: v0.11 hooks: - id: validate-pyproject name: Validate pyproject.toml @@ -47,6 +47,7 @@ repos: rev: 22.12.0 hooks: - id: black + - id: black-jupyter - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 hooks: @@ -70,15 +71,15 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.221 + rev: v0.0.224 hooks: - id: ruff args: [--force-exclude] - repo: https://github.com/sphinx-contrib/sphinx-lint rev: v0.6.7 hooks: - - id: sphinx-lint - args: [--enable, all, "--disable=line-too-long,leaked-markup"] + - id: sphinx-lint + args: [--enable, all, "--disable=line-too-long,leaked-markup"] - repo: local hooks: # Add `--hook-stage manual` to pre-commit command to run (very slow) @@ -95,4 +96,24 @@ repos: rev: v4.4.0 hooks: - id: no-commit-to-branch # no commit directly to main -# Maybe: black-jupyter, blacken-docs, blackdoc mypy, pydocstyle, rstcheck, pydocstringformatter, velin +# +# Maybe: +# +# - repo: https://github.com/rstcheck/rstcheck +# rev: v6.1.1 +# hooks: +# - id: rstcheck +# additional_dependencies: [nbsphinx] +# +# - repo: https://github.com/PyCQA/pydocstyle +# rev: 6.3.0 +# hooks: +# - id: pydocstyle +# additional_dependencies: [tomli] +# +# - repo: https://github.com/PyCQA/bandit +# rev: 1.7.4 +# hooks: +# - id: bandit +# +# blacken-docs, blackdoc mypy, pydocstringformatter, velin, flynt, yamllint diff --git a/docs/user_guide/operations.rst b/docs/user_guide/operations.rst index d06c79a83..41e4fc2c6 100644 --- a/docs/user_guide/operations.rst +++ b/docs/user_guide/operations.rst @@ -376,7 +376,7 @@ Matrix-Matrix Assignment Example: A = gb.Matrix.from_coo([0, 0, 1, 1, 2, 2], [1, 2, 0, 1, 0, 2], [2.0, 5.0, 1.5, 4.0, 0.5, -7.0]) - B = gb.Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1] + B = gb.Matrix.from_coo([0, 0, 1, 1], [0, 1, 0, 1], [-99., -98., -97., -96.]) A[::2, ::2] << B diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index b7ec2f78a..46e1028ec 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -52,6 +52,14 @@ # These now live as e.g. `gb.unary.ss.positioni` # Deprecations such as `gb.unary.positioni` will be removed in 2023.9.0 or later. _SS_OPERATORS = { + # unary + "erf", # scipy.special.erf + "erfc", # scipy.special.erfc + "frexpe", # np.frexp[1] + "frexpx", # np.frexp[0] + "lgamma", # scipy.special.loggamma + "tgamma", # scipy.special.gamma + # Positional # unary "positioni", "positioni1", @@ -2346,7 +2354,7 @@ def _initialize(cls): ( ( "firsti", "firsti1", "firstj", "firstj1", "secondi", "secondi1", - "secondj", "secondj1",), + "secondj", "secondj1"), ( position_dtypes, INT64, diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index 5143558a7..408c56bf7 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -1312,6 +1312,8 @@ def test_op_ss(): def test_deprecated(): + with pytest.warns(DeprecationWarning, match="please use"): + gb.unary.erf with pytest.warns(DeprecationWarning, match="please use"): gb.unary.positioni with pytest.warns(DeprecationWarning, match="please use"): diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index bb9e29460..ad133f05c 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -1311,7 +1311,7 @@ def import_func(x, import_name, **kwargs): w(w.S) << 1 w_orig = w.dup() format = "full" - for (raw, import_format, give_ownership, take_ownership, import_name,) in itertools.product( + for (raw, import_format, give_ownership, take_ownership, import_name) in itertools.product( [False, True], [format, None], [False, True], diff --git a/notebooks/HPEC2022.ipynb b/notebooks/HPEC2022.ipynb index a305ee52f..850ae7c2d 100644 --- a/notebooks/HPEC2022.ipynb +++ b/notebooks/HPEC2022.ipynb @@ -4898,7 +4898,7 @@ "metadata": {}, "outputs": [], "source": [ - "A.ss.config?" + "?A.ss.config" ] }, { diff --git a/notebooks/Louvain.ipynb b/notebooks/Louvain.ipynb index 308ab0d63..f1b042a49 100644 --- a/notebooks/Louvain.ipynb +++ b/notebooks/Louvain.ipynb @@ -68,9 +68,7 @@ " # Build the identity matrix, assigning each node to its own community\n", " if self._ident_comms is None:\n", " nn, nc = self.nn, self.nc\n", - " self._ident_comms = Matrix.from_coo(\n", - " range(nn), range(nn), [1] * nn, nrows=nc, ncols=nn\n", - " )\n", + " self._ident_comms = Matrix.from_coo(range(nn), range(nn), [1] * nn, nrows=nc, ncols=nn)\n", " return self._ident_comms\n", "\n", " def compute_modularity(self, comms=None):\n", diff --git a/pyproject.toml b/pyproject.toml index b7ad5eda7..2f10d65f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,6 +90,7 @@ select = [ "PLW", # pylint Warning "PIE", # flake8-pie "RUF", # ruff-specific rules + "COM", # flake8-commas ] external = [ # noqa codes that ruff doesn't know about: https://github.com/charliermarsh/ruff#external @@ -101,6 +102,7 @@ ignore = [ "SIM108", # Use ternary operator ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) # "SIM401", # Use dict.get ... instead of if-else-block (Note: if-else better for coverage and sometimes clearer) "PLR2004", # Magic number used in comparison, consider replacing magic with a constant variable + "COM812", # Trailing comma missing ] [tool.ruff.per-file-ignores] "graphblas/core/operator.py" = ["S102"] From 1e4c2e00618f54b53879b509ee44e45549dd0db1 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 17 Jan 2023 17:54:50 -0600 Subject: [PATCH 17/20] Only test imports for one (random) Python version per CI --- .github/workflows/debug.yml | 1 + .github/workflows/imports.yml | 21 +++++++++++++++++---- .github/workflows/test_and_build.yml | 2 +- scripts/check_versions.sh | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml index 6066b2d90..794746f77 100644 --- a/.github/workflows/debug.yml +++ b/.github/workflows/debug.yml @@ -1,3 +1,4 @@ +# This file is outdated name: Debug CI Jobs on: diff --git a/.github/workflows/imports.yml b/.github/workflows/imports.yml index 79aae4c71..a9863a213 100644 --- a/.github/workflows/imports.yml +++ b/.github/workflows/imports.yml @@ -9,14 +9,27 @@ on: jobs: test_imports: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10"] + # strategy: + # matrix: + # python-version: ["3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v3 + - name: RNG for Python version + uses: ddradar/choose-random-action@v2.0.2 + id: pyver + with: + contents: | + 3.8 + 3.9 + 3.10 + weights: | + 1 + 1 + 1 - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ steps.pyver.outputs.selected }} + # python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip - run: pip install -e . - run: ./scripts/test_imports.sh diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 0e2a6121f..e99af9eb3 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -161,7 +161,7 @@ jobs: npver=$(python -c 'import random ; print(random.choice(["=1.23", ""]))') spver=$(python -c 'import random ; print(random.choice(["=1.9", "=1.10", ""]))') pdver=$(python -c 'import random ; print(random.choice(["=1.5", ""]))') - akver=$(python -c 'import random ; print(random.choice(["=1.10", "=2.0", ""]))') + akver=$(python -c 'import random ; print(random.choice(["=1.10", "=2.0.5", "=2.0.6", ""]))') fi if [[ ${{ steps.sourcetype.outputs.selected }} == "source" || ${{ steps.sourcetype.outputs.selected }} == "upstream" ]]; then # TODO: there are currently issues with some numpy versions when diff --git a/scripts/check_versions.sh b/scripts/check_versions.sh index 8a9af5a3b..e9697338b 100755 --- a/scripts/check_versions.sh +++ b/scripts/check_versions.sh @@ -7,6 +7,6 @@ conda search 'numpy[channel=conda-forge]>=1.24.1' conda search 'pandas[channel=conda-forge]>=1.5.2' conda search 'scipy[channel=conda-forge]>=1.10.0' conda search 'networkx[channel=conda-forge]>=3.0' -conda search 'awkward[channel=conda-forge]>=2.0.5' +conda search 'awkward[channel=conda-forge]>=2.0.6' conda search 'numba[channel=conda-forge]>=0.56.4' conda search 'pyyaml[channel=conda-forge]>=6.0' From 357cee430fe70dd2887f09a961d29162dc5a5476 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 18 Jan 2023 16:19:26 -0600 Subject: [PATCH 18/20] Move setuptools config to pyproject.toml --- .github/workflows/publish_pypi.yml | 16 ++++- .pre-commit-config.yaml | 2 +- environment.yml | 2 + pyproject.toml | 106 ++++++++++++++++++++++++++++- setup.py | 57 +--------------- 5 files changed, 123 insertions(+), 60 deletions(-) diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml index a9ec73e47..cf2b917b3 100644 --- a/.github/workflows/publish_pypi.yml +++ b/.github/workflows/publish_pypi.yml @@ -8,6 +8,7 @@ on: jobs: build_and_deploy: runs-on: ubuntu-latest + if: github.repository == 'python-graphblas/python-graphblas' defaults: run: shell: bash -l {0} @@ -21,9 +22,18 @@ jobs: with: python-version: "3.8" - name: Install build dependencies - run: python -m pip install setuptools wheel - - name: Build wheel - run: python setup.py sdist bdist_wheel + run: | + python -m pip install --upgrade pip + python -m pip install build twine + - name: Build wheel and sdist + run: python -m build --sdist --wheel + - uses: actions/upload-artifact@v3 + with: + name: releases + path: dist + if-no-files-found: error + - name: Check with twine + run: python -m twine check --strict dist/* - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d74afd229..899353275 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.224 + rev: v0.0.225 hooks: - id: ruff args: [--force-exclude] diff --git a/environment.yml b/environment.yml index e3260c38c..71891745e 100644 --- a/environment.yml +++ b/environment.yml @@ -49,6 +49,7 @@ dependencies: # - autoflake # - black # - black-jupyter + # - build # - codespell # - commonmark # - cython @@ -95,6 +96,7 @@ dependencies: # - snakeviz # - sphinx-lint # - sympy + # - twine # - vim # - yesqa # - zarr diff --git a/pyproject.toml b/pyproject.toml index 2f10d65f5..7f0191f22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,110 @@ [build-system] -requires = ["setuptools>=41", "wheel", "setuptools-git-versioning<2"] build-backend = "setuptools.build_meta" +requires = [ + "setuptools >=61", + "setuptools-git-versioning", +] + +[project] +name = "python-graphblas" +dynamic = ["version"] +description = "Python library for GraphBLAS: high-performance sparse linear algebra for scalable graph analytics" +readme = "README.md" +requires-python = ">=3.8" +license = {file = "LICENSE"} +authors = [ + {name = "Erik Welch"}, + {name = "Jim Kitchen"}, +] +maintainers = [ + {name = "Erik Welch", email = "erik.n.welch@gmail.com"}, + {name = "Jim Kitchen", email = "jim22k@gmail.com"}, +] +keywords = [ + "graphblas", + "graph", + "sparse", + "matrix", + "lagraph", + "suitesparse", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Apache Software License", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", +] +dependencies = [ + "suitesparse-graphblas >=7.4.0.0, <7.5", + "numpy >=1.21", + "numba >=0.55", + "donfig >=0.6", + "pyyaml >=5.4", +] + +[project.urls] +homepage = "https://github.com/python-graphblas/python-graphblas" +documentation = "https://python-graphblas.readthedocs.io" +repository = "https://github.com/python-graphblas/python-graphblas" +changelog = "https://github.com/python-graphblas/python-graphblas/releases" + +[project.optional-dependencies] +repr = [ + "pandas >=1.2", +] +io = [ + "networkx >=2.8", + "scipy >=1.8", + "awkward >=1.9", +] +viz = [ + "matplotlib", +] +test = [ + "pytest", + "pandas >=1.2", + "scipy >=1.8", +] +complete = [ + "pandas >=1.2", + "networkx >=2.8", + "scipy >=1.8", + "awkward >=1.9", + "matplotlib", + "pytest", +] + +[tool.setuptools] +# Let's be explicit +# $ python -c 'from setuptools import find_packages ; [print(x) for x in sorted(find_packages())]' +# $ find graphblas/ -name __init__.py -print | sort | sed -e 's/\/__init__.py//g' -e 's/\//./g' +# $ python -c 'import tomli ; [print(x) for x in sorted(tomli.load(open("pyproject.toml", "rb"))["tool"]["setuptools"]["packages"])]' +packages = [ + "graphblas", + "graphblas.agg", + "graphblas.binary", + "graphblas.core", + "graphblas.core.ss", + "graphblas.indexunary", + "graphblas.monoid", + "graphblas.op", + "graphblas.semiring", + "graphblas.select", + "graphblas.ss", + "graphblas.tests", + "graphblas.unary", +] [tool.setuptools-git-versioning] enabled = true diff --git a/setup.py b/setup.py index 70dd662c4..606849326 100644 --- a/setup.py +++ b/setup.py @@ -1,56 +1,3 @@ -from setuptools import find_packages, setup +from setuptools import setup -extras_require = { - "repr": ["pandas >=1.2"], - "io": ["networkx >=2.8", "scipy >=1.8", "awkward >=1.9"], - "viz": ["matplotlib"], - "test": ["pytest", "pandas >=1.2", "scipy >=1.8"], -} -extras_require["complete"] = sorted({v for req in extras_require.values() for v in req}) - -with open("README.md") as f: - long_description = f.read() - -setup( - name="python-graphblas", - description=( - "Python library for GraphBLAS: high-performance sparse linear algebra " - "for scalable graph analytics" - ), - long_description=long_description, - long_description_content_type="text/markdown", - author="Erik Welch and Jim Kitchen", - author_email="erik.n.welch@gmail.com,jim22k@gmail.com", - url="https://github.com/python-graphblas/python-graphblas", - packages=find_packages(), - python_requires=">=3.8", - install_requires=[ - "suitesparse-graphblas >=7.4.0.0, <7.5", - "numpy >=1.21", - "numba >=0.55", - "donfig >=0.6", - "pyyaml >=5.4", - ], - extras_require=extras_require, - include_package_data=True, - license="Apache License 2.0", - keywords=["graphblas", "graph", "sparse", "matrix", "lagraph", "suitesparse"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: Apache Software License", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3 :: Only", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - ], - zip_safe=False, -) +setup() From 0cbb767ff77ffe34405e2bc502881b4aea177e7e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 18 Jan 2023 22:32:05 -0600 Subject: [PATCH 19/20] I like these dev/dirty versions better --- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 899353275..2700075b0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.225 + rev: v0.0.226 hooks: - id: ruff args: [--force-exclude] diff --git a/pyproject.toml b/pyproject.toml index 7f0191f22..a9871df69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,11 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", + "Intended Audience :: Other Audience", "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Information Analysis", ] dependencies = [ "suitesparse-graphblas >=7.4.0.0, <7.5", @@ -108,6 +110,8 @@ packages = [ [tool.setuptools-git-versioning] enabled = true +dev_template = "{tag}+{ccount}.g{sha}" +dirty_template = "{tag}+{ccount}.g{sha}.dirty" [tool.black] line-length = 100 From 50e3d4276ad5aa2902a18b1be31bee07ac326e05 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 20 Jan 2023 09:47:17 -0600 Subject: [PATCH 20/20] Use psg 7.4.2.0 --- .github/workflows/test_and_build.yml | 2 +- .pre-commit-config.yaml | 4 ++-- graphblas/core/formatting.py | 2 +- graphblas/core/matrix.py | 2 +- graphblas/tests/test_matrix.py | 2 +- graphblas/tests/test_scalar.py | 3 ++- pyproject.toml | 5 +++-- scripts/check_versions.sh | 2 +- 8 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index e99af9eb3..4751e4e39 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -174,7 +174,7 @@ jobs: # That is, we don't need to support versions of it that are two years old. # But, it's still useful for us to test with different versions! if [[ ${{ steps.sourcetype.outputs.selected}} == "conda-forge" ]] ; then - psgver=$(python -c 'import random ; print(random.choice(["=7.4.0", "=7.4.1", ""]))') + psgver=$(python -c 'import random ; print(random.choice(["=7.4.0", "=7.4.1", "=7.4.2", ""]))') else psgver="" fi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2700075b0..e9f4e9d72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: # These versions need updated manually - flake8==6.0.0 - flake8-comprehensions==3.10.1 - - flake8-bugbear==23.1.14 + - flake8-bugbear==23.1.17 - flake8-simplify==0.19.3 - repo: https://github.com/asottile/yesqa rev: v1.4.0 @@ -71,7 +71,7 @@ repos: additional_dependencies: [tomli] files: ^(graphblas|docs)/ - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.226 + rev: v0.0.227 hooks: - id: ruff args: [--force-exclude] diff --git a/graphblas/core/formatting.py b/graphblas/core/formatting.py index ad1f7cc2c..305df05ae 100644 --- a/graphblas/core/formatting.py +++ b/graphblas/core/formatting.py @@ -626,7 +626,7 @@ def format_scalar_expression(expr): def create_header(type_name, keys, vals, *, lower_border=False, name="", quote=True): vals = [str(x) for x in vals] if name and quote: - name = f'"{name}"' # noqa: B028 + name = f'"{name}"' key_text = [] val_text = [] for key, val in zip(keys, vals): diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index f8b4f5291..c7483aaf4 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -1229,7 +1229,7 @@ def _from_dense(cls, values, dtype=None, *, name=None): rows = rows.ravel() cols = cols.ravel() if values.ndim > 2: - values = values.reshape([nrows * ncols] + rest) + values = values.reshape([nrows * ncols, *rest]) else: values = values.ravel() return cls.from_coo(rows, cols, values, dtype, nrows=nrows, ncols=ncols, name=name) diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 6e28fdfe5..fc7f7ea08 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -3045,7 +3045,7 @@ def test_ss_flatten(A): with pytest.raises(ValueError, match="cannot reshape"): v.ss.reshape(100, 100) with pytest.raises(ValueError): - v.ss.reshape(A.shape + (1,)) + v.ss.reshape((*A.shape, 1)) @pytest.mark.skipif("not suitesparse") diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 5f856b8d4..91da4847c 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -71,7 +71,8 @@ def test_dup(s): ("INT32", -2), ("BOOL", True), ("FP32", -2.5), - ] + uint_data: + *uint_data, + ]: s5 = s4.dup(dtype=dtype, name="s5") assert s5.dtype == dtype and s5.value == val s6 = s_empty.dup(dtype=dtype, name="s6") diff --git a/pyproject.toml b/pyproject.toml index a9871df69..206fa9534 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,6 +179,7 @@ select = [ # "A", # flake8-builtins "C4", # flake8-comprehensions "T10", # flake8-debugger + # "EM", # flake8-errmsg "ISC", # flake8-implicit-str-concat # "ICN", # flake8-import-conventions "T20", # flake8-print @@ -197,12 +198,12 @@ select = [ "PLR", # pylint Refactor "PLW", # pylint Warning "PIE", # flake8-pie - "RUF", # ruff-specific rules "COM", # flake8-commas + # "INP", # flake8-no-pep420 + "RUF", # ruff-specific rules ] external = [ # noqa codes that ruff doesn't know about: https://github.com/charliermarsh/ruff#external - "B028", ] ignore = [ "D203", # 1 blank line required before class docstring (Note: conflicts with D211, which is preferred) diff --git a/scripts/check_versions.sh b/scripts/check_versions.sh index e9697338b..fb637aa51 100755 --- a/scripts/check_versions.sh +++ b/scripts/check_versions.sh @@ -4,7 +4,7 @@ # This may be helpful when updating dependency versions in CI. # Tip: add `--json` for more information. conda search 'numpy[channel=conda-forge]>=1.24.1' -conda search 'pandas[channel=conda-forge]>=1.5.2' +conda search 'pandas[channel=conda-forge]>=1.5.3' conda search 'scipy[channel=conda-forge]>=1.10.0' conda search 'networkx[channel=conda-forge]>=3.0' conda search 'awkward[channel=conda-forge]>=2.0.6'