From 128ddd5821192621e8048b9de7a1796a5e5bde19 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 31 Dec 2022 13:10:41 -0600 Subject: [PATCH 01/24] Computation between scalars --- graphblas/core/automethods.py | 33 +++-- graphblas/core/base.py | 15 +- graphblas/core/expr.py | 28 ++-- graphblas/core/infix.py | 171 +++++++++++++++++++---- graphblas/core/infixmethods.py | 38 +++++- graphblas/core/matrix.py | 21 ++- graphblas/core/operator.py | 39 +++++- graphblas/core/scalar.py | 243 ++++++++++++++++++++++++++++++--- graphblas/core/vector.py | 15 ++ graphblas/tests/test_infix.py | 29 ++-- graphblas/tests/test_matrix.py | 40 ++++++ graphblas/tests/test_scalar.py | 76 ++++++++++- graphblas/tests/test_vector.py | 40 ++++++ 13 files changed, 668 insertions(+), 120 deletions(-) diff --git a/graphblas/core/automethods.py b/graphblas/core/automethods.py index 57dbb87a3..4fcbbe041 100644 --- a/graphblas/core/automethods.py +++ b/graphblas/core/automethods.py @@ -105,6 +105,10 @@ def __matmul__(self): return self._get_value("__matmul__") +def __ne__(self): + return self._get_value("__ne__") + + def __neg__(self): return self._get_value("__neg__") @@ -349,6 +353,17 @@ def _main(): "name", "nvals", "wait", + # For infix + "__and__", + "__or__", + "__rand__", + "__ror__", + # Delayed methods + "apply", + "ewise_add", + "ewise_mult", + "ewise_union", + "select", } scalar = { "__array__", @@ -359,6 +374,7 @@ def _main(): "__index__", "__int__", "__invert__", + "__ne__", "__neg__", "_as_matrix", "_as_vector", @@ -369,23 +385,14 @@ def _main(): vector_matrix = { "S", "V", - "__and__", "__contains__", "__getitem__", "__iter__", "__matmul__", - "__or__", - "__rand__", "__rmatmul__", - "__ror__", "_carg", - "apply", "diag", - "ewise_add", - "ewise_mult", - "ewise_union", "reposition", - "select", "ss", "to_coo", "to_values", @@ -415,12 +422,8 @@ def _main(): } common_raises = set() scalar_raises = { - "__and__", "__matmul__", - "__or__", - "__rand__", "__rmatmul__", - "__ror__", } vector_matrix_raises = { "__array__", @@ -469,6 +472,10 @@ def _main(): lines.append(" # These raise exceptions") for name in sorted(common_raises | scalar_raises): lines.append(f" {name} = Scalar.{name}") + for name in sorted(bad_sugar): + if name == "__imatmul__": + continue + lines.append(f" {name} = automethods.{name}") thisdir = os.path.dirname(__file__) infix_exclude = {"_get_value"} diff --git a/graphblas/core/base.py b/graphblas/core/base.py index 6ad9b0175..afe6b1dc2 100644 --- a/graphblas/core/base.py +++ b/graphblas/core/base.py @@ -262,29 +262,21 @@ def __call__( ) def __or__(self, other): - if self._is_scalar: - return NotImplemented from .infix import _ewise_infix_expr return _ewise_infix_expr(self, other, method="ewise_add", within="__or__") def __ror__(self, other): - if self._is_scalar: - return NotImplemented from .infix import _ewise_infix_expr return _ewise_infix_expr(other, self, method="ewise_add", within="__ror__") def __and__(self, other): - if self._is_scalar: - return NotImplemented from .infix import _ewise_infix_expr return _ewise_infix_expr(self, other, method="ewise_mult", within="__and__") def __rand__(self, other): - if self._is_scalar: - return NotImplemented from .infix import _ewise_infix_expr return _ewise_infix_expr(other, self, method="ewise_mult", within="__rand__") @@ -469,8 +461,7 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, * **opts, ) if self._is_scalar: - scalar_as_vector = expr.method_name == "inner" - if scalar_as_vector: + if expr._scalar_as_vector: fake_self = self._as_vector() cfunc_name = expr.cfunc_name args = [fake_self, mask, accum] @@ -498,7 +489,7 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, * # Make the GraphBLAS call call(cfunc_name, args) if self._is_scalar: - if scalar_as_vector: + if expr._scalar_as_vector: if self._is_cscalar or backend != "suitesparse": self.value = fake_self[0].new(is_cscalar=True, name="") # SS: this assumes GrB_Scalar was cast to Vector @@ -611,8 +602,6 @@ def _new(self, dtype, mask, name, is_cscalar=None, **opts): output(mask=mask, **opts).update(self) return output - dup = new - def _format_expr(self): args = self.args[:-2] if self.cfunc_name is None else self.args return self.expr_repr.format(*args, method_name=self.method_name, op=self.op) diff --git a/graphblas/core/expr.py b/graphblas/core/expr.py index 4c656840b..d6c4c89b1 100644 --- a/graphblas/core/expr.py +++ b/graphblas/core/expr.py @@ -270,13 +270,13 @@ def __init__(self, updater, resolved_indexes, *, is_submask): if updater.kwargs.get("input_mask") is not None: raise TypeError("`input_mask` argument may only be used for extract") - def update(self, obj): - # Occurs when user calls `C[index](...).update(obj)` or `C(...)[index].update(obj)` - self.updater._setitem(self.resolved_indexes, obj, is_submask=self.is_submask) + def update(self, expr): + # Occurs when user calls `C[index](...).update(expr)` or `C(...)[index].update(expr)` + self.updater._setitem(self.resolved_indexes, expr, is_submask=self.is_submask) - def __lshift__(self, obj): - # Occurs when user calls `C[index](...) << obj` or `C(...)[index] << obj` - self.updater._setitem(self.resolved_indexes, obj, is_submask=self.is_submask) + def __lshift__(self, expr): + # Occurs when user calls `C[index](...) << expr` or `C(...)[index] << expr` + self.updater._setitem(self.resolved_indexes, expr, is_submask=self.is_submask) def __eq__(self, other): raise TypeError(f"__eq__ not defined for objects of type {type(self)}.") @@ -301,15 +301,15 @@ def __call__(self, *args, **kwargs): is_submask = updater.kwargs.get("mask") is not None return Assigner(updater, self.resolved_indexes, is_submask=is_submask) - def __lshift__(self, obj, **opts): - # Occurs when user calls `C[index] << obj` - self.update(obj, **opts) + def __lshift__(self, expr, **opts): + # Occurs when user calls `C[index] << expr` + self.update(expr, **opts) - def update(self, obj, **opts): - # Occurs when user calls `C[index].update(obj)` + def update(self, expr, **opts): + # Occurs when user calls `C[index].update(expr)` if getattr(self.parent, "_is_transposed", False): raise TypeError("'TransposedMatrix' object does not support item assignment") - Updater(self.parent, opts=opts)._setitem(self.resolved_indexes, obj, is_submask=False) + Updater(self.parent, opts=opts)._setitem(self.resolved_indexes, expr, is_submask=False) def new(self, dtype=None, *, mask=None, input_mask=None, name=None, **opts): """ @@ -326,8 +326,6 @@ def new(self, dtype=None, *, mask=None, input_mask=None, name=None, **opts): delayed_extractor = self.parent._prep_for_extract(self.resolved_indexes) return delayed_extractor.new(dtype, mask=mask, name=name, **opts) - dup = new - def _extract_delayed(self): """Return an Expression object, treating this as an extract call""" return self.parent._prep_for_extract(self.resolved_indexes) @@ -502,8 +500,6 @@ def new(self, dtype=None, *, mask=None, name=None, **opts): expr = self._to_expr() return expr.new(dtype, mask=mask, name=name, **opts) - dup = new - def _to_expr(self): if self._value is None: # Rely on the default operator for `x @ y` diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 67c4a83d5..1367091b6 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -44,6 +44,100 @@ def _ewise_mult_to_expr(self): ) +class ScalarInfixExpr(InfixExprBase): + __slots__ = () + ndim = 0 + shape = () + output_type = ScalarExpression + _is_scalar = True + _is_cscalar = False + is_cscalar = False + is_grbscalar = True + + def new(self, dtype=None, *, is_cscalar=False, name=None, **opts): + expr = self._to_expr() + return expr.new(dtype, is_cscalar=is_cscalar, name=name, **opts) + + def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): + if dtype is None: + dtype = self.dtype + if clear: + return Scalar(dtype, is_cscalar=is_cscalar, name=name) + return self.new(dtype, is_cscalar=is_cscalar, name=name, **opts) + + # Begin auto-generated code: Scalar + __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) + __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) + __bool__ = wrapdoc(Scalar.__bool__)(property(automethods.__bool__)) + __complex__ = wrapdoc(Scalar.__complex__)(property(automethods.__complex__)) + __eq__ = wrapdoc(Scalar.__eq__)(property(automethods.__eq__)) + __float__ = wrapdoc(Scalar.__float__)(property(automethods.__float__)) + __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) + __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) + __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) + __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) + __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) + __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) + __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) + __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) + _as_matrix = wrapdoc(Scalar._as_matrix)(property(automethods._as_matrix)) + _as_vector = wrapdoc(Scalar._as_vector)(property(automethods._as_vector)) + _is_empty = wrapdoc(Scalar._is_empty)(property(automethods._is_empty)) + _name_html = wrapdoc(Scalar._name_html)(property(automethods._name_html)) + _nvals = wrapdoc(Scalar._nvals)(property(automethods._nvals)) + apply = wrapdoc(Scalar.apply)(property(automethods.apply)) + ewise_add = wrapdoc(Scalar.ewise_add)(property(automethods.ewise_add)) + ewise_mult = wrapdoc(Scalar.ewise_mult)(property(automethods.ewise_mult)) + ewise_union = wrapdoc(Scalar.ewise_union)(property(automethods.ewise_union)) + gb_obj = wrapdoc(Scalar.gb_obj)(property(automethods.gb_obj)) + get = wrapdoc(Scalar.get)(property(automethods.get)) + is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) + isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) + isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) + name = wrapdoc(Scalar.name)(property(automethods.name)) + name = name.setter(automethods._set_name) + nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) + select = wrapdoc(Scalar.select)(property(automethods.select)) + value = wrapdoc(Scalar.value)(property(automethods.value)) + wait = wrapdoc(Scalar.wait)(property(automethods.wait)) + # These raise exceptions + __matmul__ = Scalar.__matmul__ + __rmatmul__ = Scalar.__rmatmul__ + __iadd__ = automethods.__iadd__ + __iand__ = automethods.__iand__ + __ifloordiv__ = automethods.__ifloordiv__ + __imod__ = automethods.__imod__ + __imul__ = automethods.__imul__ + __ior__ = automethods.__ior__ + __ipow__ = automethods.__ipow__ + __isub__ = automethods.__isub__ + __itruediv__ = automethods.__itruediv__ + __ixor__ = automethods.__ixor__ + # End auto-generated code: Scalar + + +class ScalarEwiseAddExpr(ScalarInfixExpr): + __slots__ = () + method_name = "ewise_add" + _example_op = "plus" + _infix = "|" + + _to_expr = _ewise_add_to_expr + + +class ScalarEwiseMultExpr(ScalarInfixExpr): + __slots__ = () + method_name = "ewise_mult" + _example_op = "times" + _infix = "&" + + _to_expr = _ewise_mult_to_expr + + +utils._output_types[ScalarEwiseAddExpr] = Scalar +utils._output_types[ScalarEwiseMultExpr] = Scalar + + class VectorInfixExpr(InfixExprBase): __slots__ = "_size" ndim = 1 @@ -61,6 +155,13 @@ def size(self): def shape(self): return (self._size,) + @wrapdoc(Vector.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if clear: + expr = self._to_expr() + return expr.dup(dtype, clear=clear, name=name, **opts) + return self.new(dtype, mask=mask, name=name, **opts) + # Begin auto-generated code: Vector S = wrapdoc(Vector.S)(property(automethods.S)) V = wrapdoc(Vector.V)(property(automethods.V)) @@ -180,6 +281,13 @@ def ncols(self): def shape(self): return (self._nrows, self._ncols) + @wrapdoc(Matrix.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if clear: + expr = self._to_expr() + return expr.dup(dtype, clear=clear, name=name, **opts) + return self.new(dtype, mask=mask, name=name, **opts) + # Begin auto-generated code: Matrix S = wrapdoc(Matrix.S)(property(automethods.S)) T = wrapdoc(Matrix.T)(property(automethods.T)) @@ -279,38 +387,14 @@ def __init__(self, left, right, *, nrows, ncols): utils._output_types[MatrixMatMulExpr] = Matrix -class ScalarMatMulExpr(InfixExprBase): +class ScalarMatMulExpr(ScalarInfixExpr): __slots__ = () method_name = "inner" - ndim = 0 - output_type = ScalarExpression - shape = () _example_op = "plus_times" _infix = "@" - _is_scalar = True - - def new(self, dtype=None, *, is_cscalar=None, name=None, **opts): - # Rely on the default operator for the method - expr = getattr(self.left, self.method_name)(self.right) - return expr.new(dtype, is_cscalar=is_cscalar, name=name, **opts) - - dup = new - - @property - def is_cscalar(self): - if self._value is not None: - return self._value._is_cscalar - return self._to_expr()._is_cscalar - - _is_cscalar = is_cscalar - - @property - def is_grbscalar(self): - if self._value is not None: - return not self._value._is_cscalar - return not self._to_expr()._is_cscalar # Begin auto-generated code: Scalar + __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) __bool__ = wrapdoc(Scalar.__bool__)(property(automethods.__bool__)) __complex__ = wrapdoc(Scalar.__complex__)(property(automethods.__complex__)) @@ -319,12 +403,20 @@ def is_grbscalar(self): __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) + __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) + __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) + __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) + __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) _as_matrix = wrapdoc(Scalar._as_matrix)(property(automethods._as_matrix)) _as_vector = wrapdoc(Scalar._as_vector)(property(automethods._as_vector)) _is_empty = wrapdoc(Scalar._is_empty)(property(automethods._is_empty)) _name_html = wrapdoc(Scalar._name_html)(property(automethods._name_html)) _nvals = wrapdoc(Scalar._nvals)(property(automethods._nvals)) + apply = wrapdoc(Scalar.apply)(property(automethods.apply)) + ewise_add = wrapdoc(Scalar.ewise_add)(property(automethods.ewise_add)) + ewise_mult = wrapdoc(Scalar.ewise_mult)(property(automethods.ewise_mult)) + ewise_union = wrapdoc(Scalar.ewise_union)(property(automethods.ewise_union)) gb_obj = wrapdoc(Scalar.gb_obj)(property(automethods.gb_obj)) get = wrapdoc(Scalar.get)(property(automethods.get)) is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) @@ -333,15 +425,22 @@ def is_grbscalar(self): name = wrapdoc(Scalar.name)(property(automethods.name)) name = name.setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) + select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) wait = wrapdoc(Scalar.wait)(property(automethods.wait)) # These raise exceptions - __and__ = Scalar.__and__ __matmul__ = Scalar.__matmul__ - __or__ = Scalar.__or__ - __rand__ = Scalar.__rand__ __rmatmul__ = Scalar.__rmatmul__ - __ror__ = Scalar.__ror__ + __iadd__ = automethods.__iadd__ + __iand__ = automethods.__iand__ + __ifloordiv__ = automethods.__ifloordiv__ + __imod__ = automethods.__imod__ + __imul__ = automethods.__imul__ + __ior__ = automethods.__ior__ + __ipow__ = automethods.__ipow__ + __isub__ = automethods.__isub__ + __itruediv__ = automethods.__itruediv__ + __ixor__ = automethods.__ixor__ # End auto-generated code: Scalar @@ -371,6 +470,18 @@ def _ewise_infix_expr(left, right, *, method, within): left._expect_type(right, tuple(types), within=within, argname="right") elif right_type in types: right._expect_type(left, tuple(types), within=within, argname="left") + elif left_type is Scalar: + # Create dummy expression to check compatibility of dimensions, etc. + expr = getattr(left, method)(right, binary.any) + if method == "ewise_mult": + return ScalarEwiseMultExpr(left, right) + return ScalarEwiseAddExpr(left, right) + elif right_type is Scalar: + # Create dummy expression to check compatibility of dimensions, etc. + expr = getattr(right, method)(left, binary.any) + if method == "ewise_mult": + return ScalarEwiseMultExpr(right, left) + return ScalarEwiseAddExpr(right, left) else: # pragma: no cover (sanity) raise TypeError(f"Bad types for ewise infix: {type(left).__name__}, {type(right).__name__}") diff --git a/graphblas/core/infixmethods.py b/graphblas/core/infixmethods.py index 3abc90c3f..bcef56b99 100644 --- a/graphblas/core/infixmethods.py +++ b/graphblas/core/infixmethods.py @@ -1,7 +1,8 @@ from .. import binary, unary from ..dtypes import BOOL -from .infix import MatrixInfixExpr, VectorInfixExpr +from .infix import MatrixInfixExpr, ScalarInfixExpr, VectorInfixExpr from .matrix import Matrix, MatrixExpression, MatrixIndexExpr, TransposedMatrix +from .scalar import Scalar, ScalarExpression, ScalarIndexExpr from .utils import output_type from .vector import Vector, VectorExpression, VectorIndexExpr @@ -10,13 +11,28 @@ def call_op(self, other, op, *, outer=False, union=False): type1 = output_type(self) type2 = output_type(other) types = {Matrix, TransposedMatrix, Vector} - if type1 in types and type2 in types: - if outer: + if type1 in types: + if type2 in types: + if outer: + return self.ewise_add(other, op) + if union: + return self.ewise_union(other, op, False, False) + return self.ewise_mult(other, op) + return op(self, other) + if type2 in types: + return op(self, other) + # Scalars + if outer: + if type1 is Scalar: return self.ewise_add(other, op) - if union: + return other.ewise_add(self, op.commutes_to) + if union: + if type1 is Scalar: return self.ewise_union(other, op, False, False) + return other.ewise_union(self, op.commutes_to, False, False) + if type1 is Scalar: return self.ewise_mult(other, op) - return op(self, other) + return other.ewise_mult(self, op.commutes_to) def __divmod__(self, other): @@ -282,6 +298,12 @@ def __itruediv__(self, other): "__xor__", ]: val = d[name] + if name not in {"__bool__", "__eq__", "__invert__", "__ne__", "__neg__"}: + setattr(Scalar, name, val) + if not name.startswith("__i"): + setattr(ScalarExpression, name, val) + setattr(ScalarInfixExpr, name, val) + setattr(ScalarIndexExpr, name, val) setattr(Vector, name, val) setattr(Matrix, name, val) if not name.startswith("__i") or name == "__invert__": @@ -383,6 +405,12 @@ def _main(): "d = globals()\n" f"for name in {methods}:\n" " val = d[name]\n" + " if name not in {'__bool__', '__eq__', '__invert__', '__ne__', '__neg__'}:\n" + " setattr(Scalar, name, val)\n" + " if not name.startswith('__i'):\n" + " setattr(ScalarExpression, name, val)\n" + " setattr(ScalarInfixExpr, name, val)\n" + " setattr(ScalarIndexExpr, name, val)\n" " setattr(Vector, name, val)\n" " setattr(Matrix, name, val)\n" " if not name.startswith('__i') or name == '__invert__':\n" diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 5e17434f3..5adb2648d 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -3061,6 +3061,14 @@ def nrows(self): def shape(self): return (self._nrows, self._ncols) + @wrapdoc(Matrix.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if dtype is None: + dtype = self.dtype + if clear: + return Matrix(dtype, self._nrows, self._ncols, name=name) + return self._new(dtype, mask, name, **opts) + # Begin auto-generated code: Matrix _get_value = automethods._get_value S = wrapdoc(Matrix.S)(property(automethods.S)) @@ -3150,6 +3158,14 @@ def nrows(self): def shape(self): return (self._nrows, self._ncols) + @wrapdoc(Matrix.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if clear: + if dtype is None: + dtype = self.dtype + return self.output_type(dtype, *self.shape, name=name) + return self.new(dtype, mask=mask, name=name, **opts) + # Begin auto-generated code: Matrix _get_value = automethods._get_value S = wrapdoc(Matrix.S)(property(automethods.S)) @@ -3244,6 +3260,7 @@ def new(self, dtype=None, *, mask=None, name=None, **opts): output(mask=mask, **opts) << self return output + @wrapdoc(Matrix.dup) def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): if dtype is None: dtype = self.dtype @@ -3301,11 +3318,11 @@ def to_csr(self, dtype=None): def to_csc(self, dtype=None): return self._matrix.to_csr(dtype) - @wrapdoc(Matrix.to_csr) + @wrapdoc(Matrix.to_dcsr) def to_dcsr(self, dtype=None): return self._matrix.to_dcsc(dtype) - @wrapdoc(Matrix.to_csc) + @wrapdoc(Matrix.to_dcsc) def to_dcsc(self, dtype=None): return self._matrix.to_dcsr(dtype) diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index c9274f0fb..d094545c9 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -89,12 +89,33 @@ def _call_op(op, left, right=None, thunk=None, **kwargs): from .matrix import Matrix, TransposedMatrix from .vector import Vector - if output_type(left) in {Vector, Matrix, TransposedMatrix}: + left_type = output_type(left) + if left_type in {Vector, Matrix, TransposedMatrix}: if thunk is not None: return left.select(op, thunk=thunk, **kwargs) return left.apply(op, right=right, **kwargs) - if output_type(right) in {Vector, Matrix, TransposedMatrix}: + right_type = output_type(right) + if right_type in {Vector, Matrix, TransposedMatrix}: return right.apply(op, left=left, **kwargs) + + from .scalar import Scalar, _as_scalar + + if left_type is Scalar: + if thunk is not None: + 1 / 0 # XXX TODO + return left.select(op, thunk=thunk, **kwargs) + return left.apply(op, right=right, **kwargs) + if right_type is Scalar: + return right.apply(op, left=left, **kwargs) + try: + left_scalar = _as_scalar(left, is_cscalar=False) + except Exception: + pass + else: + if thunk is not None: + 1 / 0 # XXX TODO + return left_scalar.select(op, thunk=thunk, **kwargs) + return left_scalar.apply(op, right=right, **kwargs) raise TypeError( f"Bad types when calling {op!r}. Got types: {type(left)}, {type(right)}.\n" "Expected an infix expression or an apply with a Vector or Matrix and a scalar:\n" @@ -183,11 +204,21 @@ def __call__(self, val): from .matrix import Matrix, TransposedMatrix from .vector import Vector - if output_type(val) in {Vector, Matrix, TransposedMatrix}: + if (typ := output_type(val)) in {Vector, Matrix, TransposedMatrix}: + return val.apply(self) + from .scalar import Scalar, _as_scalar + + if typ is Scalar: return val.apply(self) + try: + scalar = _as_scalar(val, is_cscalar=False) + except Exception: + pass + else: + return scalar.apply(self) raise TypeError( f"Bad type when calling {self!r}.\n" - " - Expected type: Vector, Matrix, TransposedMatrix.\n" + " - Expected type: Scalar, Vector, Matrix, TransposedMatrix.\n" f" - Got: {type(val)}.\n" "Calling a UnaryOp is syntactic sugar for calling apply. " f"For example, `A.apply({self!r})` is the same as `{self!r}(A)`." diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index c1bf996b4..270fa0e68 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -3,9 +3,9 @@ import numpy as np -from .. import backend, config +from .. import backend, binary, config, monoid from ..binary import isclose -from ..dtypes import _INDEX, BOOL, FP64, lookup_dtype +from ..dtypes import _INDEX, BOOL, FP64, lookup_dtype, unify from ..exceptions import EmptyObject, check_status from . import automethods, ffi, lib, utils from .base import BaseExpression, BaseType, call @@ -121,6 +121,9 @@ def __eq__(self, other): """ return self.isequal(other) + def __ne__(self, other): + return not self.isequal(other) + def __bool__(self): """Truthiness check. @@ -531,7 +534,7 @@ def from_value(cls, value, dtype=None, *, is_cscalar=False, name=None): if typ is Scalar and type(value) is not Scalar: if config.get("autocompute"): return value.new(dtype=dtype, is_cscalar=is_cscalar, name=name) - cls._expect_type( + cls()._expect_type( value, Scalar, within="from_value", @@ -590,17 +593,169 @@ def _as_matrix(self, *, name=None): rv[0, 0] = self return rv + ######################################################### + # Delayed methods + # + # These return a delayed expression object which must be passed + # to update to trigger a call to GraphBLAS + ######################################################### + + def ewise_add(self, other, op=monoid.plus): + method_name = "ewise_add" + if type(other) is not Scalar: + dtype = self.dtype if self.dtype._is_udt else None + try: + other = Scalar.from_value(other, dtype, is_cscalar=False, name="") + except TypeError: + other = self._expect_type( + other, + Scalar, + within=method_name, + keyword_name="other", + extra_message="Literal scalars also accepted.", + op=op, + ) + op = get_typed_op(op, self.dtype, other.dtype, kind="binary") + self._expect_op(op, ("BinaryOp", "Monoid"), within=method_name, argname="op") + return ScalarExpression( + method_name, + f"GrB_Vector_eWiseAdd_{op.opclass}", + [self._as_vector(), other._as_vector()], + op=op, + is_cscalar=False, + scalar_as_vector=True, + ) + + def ewise_mult(self, other, op=binary.times): + method_name = "ewise_mult" + if type(other) is not Scalar: + dtype = self.dtype if self.dtype._is_udt else None + try: + other = Scalar.from_value(other, dtype, is_cscalar=False, name="") + except TypeError: + other = self._expect_type( + other, + Scalar, + within=method_name, + keyword_name="other", + extra_message="Literal scalars also accepted.", + op=op, + ) + op = get_typed_op(op, self.dtype, other.dtype, kind="binary") + self._expect_op(op, ("BinaryOp", "Monoid"), within=method_name, argname="op") + return ScalarExpression( + method_name, + f"GrB_Vector_eWiseMult_{op.opclass}", + [self._as_vector(), other._as_vector()], + op=op, + is_cscalar=False, + scalar_as_vector=True, + ) + + def ewise_union(self, other, op, left_default, right_default): + method_name = "ewise_union" + dtype = self.dtype if self.dtype._is_udt else None + if type(other) is not Scalar: + try: + other = Scalar.from_value(other, dtype, is_cscalar=False, name="") + except TypeError: + other = self._expect_type( + other, + Scalar, + within=method_name, + keyword_name="other", + extra_message="Literal scalars also accepted.", + op=op, + ) + if type(left_default) is not Scalar: + try: + left = Scalar.from_value( + left_default, dtype, is_cscalar=False, name="" # pragma: is_grbscalar + ) + except TypeError: + left = self._expect_type( + left_default, + Scalar, + within=method_name, + keyword_name="left_default", + extra_message="Literal scalars also accepted.", + op=op, + ) + else: + left = _as_scalar(left_default, dtype, is_cscalar=False) # pragma: is_grbscalar + if type(right_default) is not Scalar: + try: + right = Scalar.from_value( + right_default, dtype, is_cscalar=False, name="" # pragma: is_grbscalar + ) + except TypeError: + right = self._expect_type( + right_default, + Scalar, + within=method_name, + keyword_name="right_default", + extra_message="Literal scalars also accepted.", + op=op, + ) + else: + right = _as_scalar(right_default, dtype, is_cscalar=False) # pragma: is_grbscalar + defaults_dtype = unify(left.dtype, right.dtype) + args_dtype = unify(self.dtype, other.dtype) + op = get_typed_op(op, defaults_dtype, args_dtype, kind="binary") + self._expect_op(op, ("BinaryOp", "Monoid"), within=method_name, argname="op") + if op.opclass == "Monoid": + op = op.binaryop + expr_repr = "{0.name}.{method_name}({2.name}, {op}, {1._expr_name}, {3._expr_name})" + if backend == "suitesparse": + expr = ScalarExpression( + method_name, + "GxB_Vector_eWiseUnion", + [self._as_vector(), left, other._as_vector(), right], + op=op, + expr_repr=expr_repr, + is_cscalar=False, + scalar_as_vector=True, + ) + return expr + + def apply(self, op, right=None, *, left=None): + expr = self._as_vector().apply(op, right, left=left) + return ScalarExpression( + expr.method_name, + expr.cfunc_name, + expr.args, + op=expr.op, + dtype=expr.dtype, + expr_repr=expr.expr_repr, + is_cscalar=False, + scalar_as_vector=True, + ) + + def select(self, op, thunk=None): + expr = self._as_vector().select(op, thunk) + return ScalarExpression( + expr.method_name, + expr.cfunc_name, + expr.args, + op=expr.op, + dtype=expr.dtype, + expr_repr=expr.expr_repr, + is_cscalar=False, + scalar_as_vector=True, + ) + class ScalarExpression(BaseExpression): - __slots__ = "_is_cscalar" + __slots__ = "_is_cscalar", "_scalar_as_vector" output_type = Scalar ndim = 0 shape = () _is_scalar = True - def __init__(self, *args, is_cscalar, **kwargs): + def __init__(self, *args, is_cscalar, scalar_as_vector=False, **kwargs): super().__init__(*args, **kwargs) self._is_cscalar = is_cscalar + self._scalar_as_vector = scalar_as_vector def construct_output(self, dtype=None, *, is_cscalar=None, name=None): if dtype is None: @@ -614,7 +769,15 @@ def new(self, dtype=None, *, is_cscalar=None, name=None, **opts): is_cscalar = self._is_cscalar return super()._new(dtype, None, name, is_cscalar=is_cscalar, **opts) - dup = new + @wrapdoc(Scalar.dup) + def dup(self, dtype=None, *, clear=False, is_cscalar=None, name=None, **opts): + if dtype is None: + dtype = self.dtype + if is_cscalar is None: + is_cscalar = self._is_cscalar + if clear: + return Scalar(dtype, is_cscalar=is_cscalar, name=name) + return self.new(dtype, is_cscalar=is_cscalar, name=name, **opts) def __repr__(self): from .formatting import format_scalar_expression @@ -631,6 +794,7 @@ def _repr_html_(self): # Begin auto-generated code: Scalar _get_value = automethods._get_value + __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) __bool__ = wrapdoc(Scalar.__bool__)(property(automethods.__bool__)) __complex__ = wrapdoc(Scalar.__complex__)(property(automethods.__complex__)) @@ -639,12 +803,20 @@ def _repr_html_(self): __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) + __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) + __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) + __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) + __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) _as_matrix = wrapdoc(Scalar._as_matrix)(property(automethods._as_matrix)) _as_vector = wrapdoc(Scalar._as_vector)(property(automethods._as_vector)) _is_empty = wrapdoc(Scalar._is_empty)(property(automethods._is_empty)) _name_html = wrapdoc(Scalar._name_html)(property(automethods._name_html)) _nvals = wrapdoc(Scalar._nvals)(property(automethods._nvals)) + apply = wrapdoc(Scalar.apply)(property(automethods.apply)) + ewise_add = wrapdoc(Scalar.ewise_add)(property(automethods.ewise_add)) + ewise_mult = wrapdoc(Scalar.ewise_mult)(property(automethods.ewise_mult)) + ewise_union = wrapdoc(Scalar.ewise_union)(property(automethods.ewise_union)) gb_obj = wrapdoc(Scalar.gb_obj)(property(automethods.gb_obj)) get = wrapdoc(Scalar.get)(property(automethods.get)) is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) @@ -653,15 +825,22 @@ def _repr_html_(self): name = wrapdoc(Scalar.name)(property(automethods.name)) name = name.setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) + select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) wait = wrapdoc(Scalar.wait)(property(automethods.wait)) # These raise exceptions - __and__ = Scalar.__and__ __matmul__ = Scalar.__matmul__ - __or__ = Scalar.__or__ - __rand__ = Scalar.__rand__ __rmatmul__ = Scalar.__rmatmul__ - __ror__ = Scalar.__ror__ + __iadd__ = automethods.__iadd__ + __iand__ = automethods.__iand__ + __ifloordiv__ = automethods.__ifloordiv__ + __imod__ = automethods.__imod__ + __imul__ = automethods.__imul__ + __ior__ = automethods.__ior__ + __ipow__ = automethods.__ipow__ + __isub__ = automethods.__isub__ + __itruediv__ = automethods.__itruediv__ + __ixor__ = automethods.__ixor__ # End auto-generated code: Scalar @@ -679,18 +858,20 @@ def new(self, dtype=None, *, is_cscalar=None, name=None, **opts): self.resolved_indexes, dtype, opts, is_cscalar=is_cscalar, name=name ) - dup = new - - @property - def is_cscalar(self): - return self._is_cscalar + @wrapdoc(Scalar.dup) + def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): + if dtype is None: + dtype = self.dtype + if clear: + return Scalar(dtype, is_cscalar=is_cscalar, name=name) + return self.new(dtype, is_cscalar=is_cscalar, name=name, **opts) - @property - def is_grbscalar(self): - return not self._is_cscalar + is_cscalar = Scalar.is_cscalar + is_grbscalar = Scalar.is_grbscalar # Begin auto-generated code: Scalar _get_value = automethods._get_value + __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) __bool__ = wrapdoc(Scalar.__bool__)(property(automethods.__bool__)) __complex__ = wrapdoc(Scalar.__complex__)(property(automethods.__complex__)) @@ -699,12 +880,20 @@ def is_grbscalar(self): __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) + __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) + __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) + __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) + __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) _as_matrix = wrapdoc(Scalar._as_matrix)(property(automethods._as_matrix)) _as_vector = wrapdoc(Scalar._as_vector)(property(automethods._as_vector)) _is_empty = wrapdoc(Scalar._is_empty)(property(automethods._is_empty)) _name_html = wrapdoc(Scalar._name_html)(property(automethods._name_html)) _nvals = wrapdoc(Scalar._nvals)(property(automethods._nvals)) + apply = wrapdoc(Scalar.apply)(property(automethods.apply)) + ewise_add = wrapdoc(Scalar.ewise_add)(property(automethods.ewise_add)) + ewise_mult = wrapdoc(Scalar.ewise_mult)(property(automethods.ewise_mult)) + ewise_union = wrapdoc(Scalar.ewise_union)(property(automethods.ewise_union)) gb_obj = wrapdoc(Scalar.gb_obj)(property(automethods.gb_obj)) get = wrapdoc(Scalar.get)(property(automethods.get)) is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) @@ -713,15 +902,22 @@ def is_grbscalar(self): name = wrapdoc(Scalar.name)(property(automethods.name)) name = name.setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) + select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) wait = wrapdoc(Scalar.wait)(property(automethods.wait)) # These raise exceptions - __and__ = Scalar.__and__ __matmul__ = Scalar.__matmul__ - __or__ = Scalar.__or__ - __rand__ = Scalar.__rand__ __rmatmul__ = Scalar.__rmatmul__ - __ror__ = Scalar.__ror__ + __iadd__ = automethods.__iadd__ + __iand__ = automethods.__iand__ + __ifloordiv__ = automethods.__ifloordiv__ + __imod__ = automethods.__imod__ + __imul__ = automethods.__imul__ + __ior__ = automethods.__ior__ + __ipow__ = automethods.__ipow__ + __isub__ = automethods.__isub__ + __itruediv__ = automethods.__itruediv__ + __ixor__ = automethods.__ixor__ # End auto-generated code: Scalar @@ -751,3 +947,6 @@ def _dict_to_record(np_type, d): utils._output_types[Scalar] = Scalar utils._output_types[ScalarIndexExpr] = Scalar utils._output_types[ScalarExpression] = Scalar + +# Import vector to import matrix to import infix to import infixmethods, which has side effects +from . import vector # noqa: E402, F401 isort:skip diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index 0995aa78b..8cc271b13 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -1417,6 +1417,7 @@ def inner(self, other, op=semiring.plus_times): [self, other._as_matrix()], op=op, is_cscalar=False, + scalar_as_vector=True, ) if self._size != other._size: expr.new(name="") # incompatible shape; raise now @@ -1851,6 +1852,12 @@ def size(self): def shape(self): return (self._size,) + @wrapdoc(Vector.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if clear: + return Vector(self.dtype if dtype is None else dtype, self.size, name=name) + return self._new(dtype, mask, name) + # Begin auto-generated code: Vector _get_value = automethods._get_value S = wrapdoc(Vector.S)(property(automethods.S)) @@ -1926,6 +1933,14 @@ def size(self): def shape(self): return (self._size,) + @wrapdoc(Vector.dup) + def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): + if clear: + if dtype is None: + dtype = self.dtype + return self.output_type(dtype, *self.shape, name=name) + return self.new(dtype, mask=mask, name=name, **opts) + # Begin auto-generated code: Vector _get_value = automethods._get_value S = wrapdoc(Vector.S)(property(automethods.S)) diff --git a/graphblas/tests/test_infix.py b/graphblas/tests/test_infix.py index d7e27d550..826ae03c4 100644 --- a/graphblas/tests/test_infix.py +++ b/graphblas/tests/test_infix.py @@ -164,14 +164,15 @@ def test_bad_ewise(s1, v1, A1, A2): with raises(DimensionMismatch): A1.T & A1 - with raises(TypeError): - s1 | 1 - with raises(TypeError): - 1 | s1 - with raises(TypeError): - s1 & 1 - with raises(TypeError): - 1 & s1 + # These are okay now + # with raises(TypeError): + s1 | 1 + # with raises(TypeError): + 1 | s1 + # with raises(TypeError): + s1 & 1 + # with raises(TypeError): + 1 & s1 with raises(TypeError, match="not supported for FP64"): v1 |= v1 @@ -254,10 +255,10 @@ def test_apply_unary_bad(s1, v1): op.exp(v1, 1) with raises(TypeError, match="__call__"): op.exp(1, v1) - with raises(TypeError, match="Bad type when calling unary.exp"): - op.exp(s1) - with raises(TypeError, match="Bad type when calling unary.exp"): - op.exp(1) + # with raises(TypeError, match="Bad type when calling unary.exp"): + op.exp(s1) # Okay now + # with raises(TypeError, match="Bad type when calling unary.exp"): + op.exp(1) # Okay now with raises(TypeError, match="Bad dtype"): op.exp(v1 | v1) @@ -282,8 +283,8 @@ def test_apply_binary(v1, A1): def test_apply_binary_bad(v1): - with raises(TypeError, match="Bad types when calling binary.plus"): - op.plus(1, 1) + # with raises(TypeError, match="Bad types when calling binary.plus"): + op.plus(1, 1) # Okay now with raises(TypeError, match="Bad type when calling binary.plus"): op.plus(v1) with raises(TypeError, match="Bad type for keyword argument `right="): diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 9a9bf526d..f8cadcb45 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2,6 +2,7 @@ import itertools import pickle import sys +import types import weakref import numpy as np @@ -2908,6 +2909,16 @@ def test_expr_is_like_matrix(A): "_prep_for_extract", "_extract_element", } + # Make sure signatures actually match + skip = {"__init__", "__repr__", "_repr_html_", "new"} + for expr in [binary.times(B & B), B & B, B.T]: + print(type(expr).__name__) + for attr, val in inspect.getmembers(expr): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(B, attr): + continue + val2 = getattr(B, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ @autocompute @@ -2950,6 +2961,35 @@ def test_index_expr_is_like_matrix(A): "and then run `python -m graphblas.core.automethods`. If you're messing with infix " "methods, then you may need to run `python -m graphblas.core.infixmethods`." ) + # Make sure signatures actually match. `update` has different docstring. + skip = {"__call__", "__init__", "__repr__", "_repr_html_", "new", "update"} + for attr, val in inspect.getmembers(B[[0, 1], [0, 1]]): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(B, attr): + continue + val2 = getattr(B, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ + + +@autocompute +def test_dup_expr(A): + result = (A + A).dup() + assert result.isequal(2 * A) + result = (A + A).dup(clear=True) + assert result.isequal(A.dup(clear=True)) + result = (A * A).dup(mask=A.V) + assert result.isequal((A**2).new(mask=A.V)) + result = A[:, :].dup() + assert result.isequal(A) + result = A[:, :].dup(clear=True) + assert result.isequal(A.dup(clear=True), check_dtype=True) + result = A[:, :].dup(float, clear=True) + assert result.isequal(A.dup(float, clear=True), check_dtype=True) + B = A.dup(bool) + result = (B | B).dup() + assert result.isequal(B) + result = (B | B).dup(clear=True) + assert result.isequal(B.dup(clear=True)) @pytest.mark.skipif("not suitesparse") diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 87e2b32b2..72b0ba758 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -2,13 +2,14 @@ import pickle import random import sys +import types import weakref import numpy as np import pytest import graphblas as gb -from graphblas import backend, binary, dtypes, replace +from graphblas import backend, binary, dtypes, replace, unary from .conftest import autocompute, compute @@ -321,9 +322,11 @@ def test_wait(s): @autocompute def test_expr_is_like_scalar(s): v = Vector.from_coo([1], [2]) + t = s.dup(bool) attrs = {attr for attr, val in inspect.getmembers(s)} expr_attrs = {attr for attr, val in inspect.getmembers(v.inner(v))} infix_attrs = {attr for attr, val in inspect.getmembers(v @ v)} + scalar_infix_attrs = {attr for attr, val in inspect.getmembers(t & t)} # Should we make any of these raise informative errors? expected = { "__call__", @@ -350,6 +353,17 @@ def test_expr_is_like_scalar(s): "methods, then you may need to run `python -m graphblas.core.infixmethods`." ) assert attrs - infix_attrs == expected + assert attrs - scalar_infix_attrs == expected + # Make sure signatures actually match. `expr.dup` has `**opts` + skip = {"__init__", "__repr__", "_repr_html_", "new", "dup"} + for expr in [v.inner(v), v @ v, t & t]: + print(type(expr).__name__) + for attr, val in inspect.getmembers(expr): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(s, attr): + continue + val2 = getattr(s, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ @autocompute @@ -379,6 +393,43 @@ def test_index_expr_is_like_scalar(s): "and then run `python -m graphblas.core.automethods`. If you're messing with infix " "methods, then you may need to run `python -m graphblas.core.infixmethods`." ) + # Make sure signatures actually match. `update` has different docstring. + skip = {"__call__", "__init__", "__repr__", "_repr_html_", "new", "update", "dup"} + for attr, val in inspect.getmembers(v[0]): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(s, attr): + continue + val2 = getattr(s, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ + + +@autocompute +def test_dup_expr(s): + v = Vector.from_coo([1], [2]) + result = (s + s).dup() + assert result.isequal(2 * s) + result = (s + s).dup(is_cscalar=not s._is_cscalar) + assert result.isequal(2 * s) + assert result._is_cscalar != s._is_cscalar + result = (s + s).dup(dtype=float) + assert result.isequal(10.0, check_dtype=True) + result = (s + s).dup(clear=True) + assert result.isequal(s.dup(clear=True)) + b = s.dup(bool) + result = (b | b).dup() + assert result.isequal(b, check_dtype=True) + result = (b | b).dup(clear=True) + assert result.isequal(b.dup(clear=True), check_dtype=True) + result = (b | b).dup(float) + assert result.isequal(b.dup(float), check_dtype=True) + result = (v @ v).dup() + assert result.isequal(4) + result = v[1].dup() + assert result.isequal(2, check_dtype=True) + result = v[1].dup(float) + assert result.isequal(2.0, check_dtype=True) + result = v[1].dup(clear=True) + assert result.isequal(v[0]) def test_ndim(s): @@ -499,3 +550,26 @@ def test_ss_descriptors(s): v[0].new(nthreads=4) with pytest.raises(ValueError, match="escriptor"): s(nthreads=4) << 1 + + +@autocompute +def test_scalar_operators(s): + assert -s == -5 + assert s + 1 == 6 + assert 1 + s == 6 + assert s - 1 == 4 + assert 1 - s == -4 + assert s * 2 == 10 + assert 2 * s == 10 + assert s * s == 25 + assert s**2 == 25 + assert unary.cos(0) == 1 + assert binary.plus(s | 2) == 7 + assert binary.plus(s, 2) == 7 + assert binary.plus(5, 2) == 7 + assert binary.plus(2, s) == 7 + assert (-s).apply(unary.abs) == 5 + with pytest.raises(TypeError): + unary.sin(object()) + with pytest.raises(TypeError): + binary.plus(object(), object()) diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 93f78a87b..058334c5f 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -2,6 +2,7 @@ import itertools import pickle import sys +import types import weakref import numpy as np @@ -1610,6 +1611,16 @@ def test_expr_is_like_vector(v): "methods, then you may need to run `python -m graphblas.core.infixmethods`." ) assert attrs - infix_attrs == expected + # Make sure signatures actually match + skip = {"__init__", "__repr__", "_repr_html_", "new"} + for expr in [binary.times(w & w), w & w]: + print(type(expr).__name__) + for attr, val in inspect.getmembers(expr): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(w, attr): + continue + val2 = getattr(w, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ @autocompute @@ -1644,6 +1655,35 @@ def test_index_expr_is_like_vector(v): "and then run `python -m graphblas.core.automethods`. If you're messing with infix " "methods, then you may need to run `python -m graphblas.core.infixmethods`." ) + # Make sure signatures actually match. `update` has different docstring. + skip = {"__call__", "__init__", "__repr__", "_repr_html_", "new", "update"} + for attr, val in inspect.getmembers(w[[0, 1]]): + if attr in skip or not isinstance(val, types.MethodType) or not hasattr(w, attr): + continue + val2 = getattr(w, attr) + assert inspect.signature(val) == inspect.signature(val2), attr + assert val.__doc__ == val2.__doc__ + + +@autocompute +def test_dup_expr(v): + result = (v + v).dup() + assert result.isequal(2 * v) + result = (v + v).dup(clear=True) + assert result.isequal(v.dup(clear=True)) + result = (v * v).dup(mask=v.V) + assert result.isequal((v**2).new(mask=v.V)) + result = v[:].dup() + assert result.isequal(v) + result = v[:].dup(clear=True) + assert result.isequal(v.dup(clear=True), check_dtype=True) + result = v[:].dup(float, clear=True) + assert result.isequal(v.dup(float, clear=True), check_dtype=True) + b = v.dup(bool) + result = (b | b).dup() + assert result.isequal(b) + result = (b | b).dup(clear=True) + assert result.isequal(b.dup(clear=True)) @pytest.mark.skipif("not suitesparse") From aec1973b95c9213c7e74cad34d00d57a41828e13 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 31 Dec 2022 14:57:18 -0600 Subject: [PATCH 02/24] Vanilla ewise_union between scalars --- graphblas/core/scalar.py | 22 ++++++++++++++++++++++ graphblas/tests/test_scalar.py | 15 +++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index 270fa0e68..005934434 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -27,6 +27,17 @@ def _scalar_index(name): return self +def _s_union_s(updater, left, right, left_default, right_default, op, dtype): + opts = updater.opts + new_left = left.dup(dtype, clear=True) + new_left(**opts) << binary.second(right, left_default) + new_left(**opts) << binary.first(left | new_left) + new_right = right.dup(dtype, clear=True) + new_right(**opts) << binary.second(left, right_default) + new_right(**opts) << binary.first(right | new_right) + updater << op(new_left & new_right) + + class Scalar(BaseType): """Create a new GraphBLAS Sparse Scalar. @@ -716,6 +727,17 @@ def ewise_union(self, other, op, left_default, right_default): is_cscalar=False, scalar_as_vector=True, ) + else: + dtype = unify(defaults_dtype, args_dtype) + expr = ScalarExpression( + method_name, + None, + [self, left, other, right, _s_union_s, (self, other, left, right, op, dtype)], + op=op, + expr_repr=expr_repr, + is_cscalar=False, + scalar_as_vector=True, + ) return expr def apply(self, op, right=None, *, left=None): diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 72b0ba758..18fea23d8 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -10,6 +10,7 @@ import graphblas as gb from graphblas import backend, binary, dtypes, replace, unary +from graphblas.exceptions import EmptyObject from .conftest import autocompute, compute @@ -500,6 +501,20 @@ def test_sizeof(s): sys.getsizeof(s) +def test_ewise_union(s): + t = Scalar(int) + result = s.ewise_union(t, binary.plus, 10, 20).new() + assert result == 25 + with pytest.raises(EmptyObject): + s.ewise_union(t, binary.plus, 10, t).new() + result = s.ewise_union(s, binary.plus, 10, 20).new() + assert result == 10 + result = t.ewise_union(t, binary.plus, 10, 20).new() + assert result.is_empty + with pytest.raises(EmptyObject): + t.ewise_union(t, binary.plus, t, t).new() + + @pytest.mark.skipif("not suitesparse") def test_ss_concat(s): empty = Scalar(int) From 8a75e41e6b933b7343797aecf4d6fbeceb1e3937 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sat, 31 Dec 2022 16:36:48 -0600 Subject: [PATCH 03/24] Fix bizarro scalars --- graphblas/core/infix.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 1367091b6..832260c81 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -52,7 +52,6 @@ class ScalarInfixExpr(InfixExprBase): _is_scalar = True _is_cscalar = False is_cscalar = False - is_grbscalar = True def new(self, dtype=None, *, is_cscalar=False, name=None, **opts): expr = self._to_expr() @@ -65,6 +64,10 @@ def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): return Scalar(dtype, is_cscalar=is_cscalar, name=name) return self.new(dtype, is_cscalar=is_cscalar, name=name, **opts) + @property + def is_grbscalar(self): + return not self.is_cscalar + # Begin auto-generated code: Scalar __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) From 70d600c626698ef4424ff256e23f6e3ee486ed32 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 2 Jan 2023 10:26:46 -0600 Subject: [PATCH 04/24] Coverage --- .pre-commit-config.yaml | 2 +- graphblas/core/matrix.py | 2 +- graphblas/core/operator.py | 2 -- graphblas/core/vector.py | 2 +- graphblas/select/__init__.py | 13 ++++++++----- graphblas/tests/test_matrix.py | 4 +++- graphblas/tests/test_scalar.py | 18 ++++++++++++++++-- 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index df5683491..9f1b99838 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: files: ^(graphblas|docs)/ args: ["--ignore-words-list=coo,ba"] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.205 + rev: v0.0.206 hooks: - id: ruff args: ["--force-exclude"] diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 5adb2648d..0dfb4f73f 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2741,7 +2741,7 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o vals = Vector.from_coo( np.arange(shape[0]), values, dtype, size=shape[0] ) - except Exception: + except Exception: # pragma: no cover (safety) vals = None else: if dtype.np_type.subdtype is not None: diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index d094545c9..569916ad3 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -102,7 +102,6 @@ def _call_op(op, left, right=None, thunk=None, **kwargs): if left_type is Scalar: if thunk is not None: - 1 / 0 # XXX TODO return left.select(op, thunk=thunk, **kwargs) return left.apply(op, right=right, **kwargs) if right_type is Scalar: @@ -113,7 +112,6 @@ def _call_op(op, left, right=None, thunk=None, **kwargs): pass else: if thunk is not None: - 1 / 0 # XXX TODO return left_scalar.select(op, thunk=thunk, **kwargs) return left_scalar.apply(op, right=right, **kwargs) raise TypeError( diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index 8cc271b13..98434d22d 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -1642,7 +1642,7 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o vals = Vector.from_coo( np.arange(shape[0]), values, dtype, size=shape[0] ) - except Exception: + except Exception: # pragma: no cover (safety) vals = None else: if dtype.np_type.subdtype is not None: diff --git a/graphblas/select/__init__.py b/graphblas/select/__init__.py index f7166b036..14dcc6596 100644 --- a/graphblas/select/__init__.py +++ b/graphblas/select/__init__.py @@ -21,13 +21,12 @@ def __getattr__(key): def _resolve_expr(expr, callname, opname): - from ..core.matrix import MatrixExpression - from ..core.vector import VectorExpression + from ..core.base import BaseExpression - if not isinstance(expr, (VectorExpression, MatrixExpression)): + if not isinstance(expr, BaseExpression): raise TypeError( - f"Expected VectorExpression or MatrixExpression; found {type(expr)}\n" - f"Typical usage: select.{callname}(x <= 5)" + f"Expected ScalarExpression, VectorExpression, or MatrixExpression; " + f"found {type(expr)}\nTypical usage: select.{callname}(x <= 5)" ) tensor = expr.args[0] thunk = expr.args[1] @@ -46,6 +45,10 @@ def _resolve_expr(expr, callname, opname): thunk -= 1 if method not in globals(): raise ValueError(f"Unknown or unregistered select method: {method}") + if tensor._parent is not None: + # Handle ScalarExpressions that change their arguments to Vector + tensor = tensor._parent + thunk = thunk._parent return globals()[method](tensor, thunk) diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index f8cadcb45..ce764f51a 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2976,7 +2976,9 @@ def test_dup_expr(A): result = (A + A).dup() assert result.isequal(2 * A) result = (A + A).dup(clear=True) - assert result.isequal(A.dup(clear=True)) + assert result.isequal(A.dup(clear=True), check_dtype=True) + result = (A + A).dup(float, clear=True) + assert result.isequal(A.dup(float, clear=True), check_dtype=True) result = (A * A).dup(mask=A.V) assert result.isequal((A**2).new(mask=A.V)) result = A[:, :].dup() diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 18fea23d8..a72125af1 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -9,7 +9,7 @@ import pytest import graphblas as gb -from graphblas import backend, binary, dtypes, replace, unary +from graphblas import backend, binary, dtypes, monoid, replace, select, unary from graphblas.exceptions import EmptyObject from .conftest import autocompute, compute @@ -507,12 +507,26 @@ def test_ewise_union(s): assert result == 25 with pytest.raises(EmptyObject): s.ewise_union(t, binary.plus, 10, t).new() - result = s.ewise_union(s, binary.plus, 10, 20).new() + result = s.ewise_union(s, monoid.plus, 10, 20).new() assert result == 10 result = t.ewise_union(t, binary.plus, 10, 20).new() assert result.is_empty with pytest.raises(EmptyObject): t.ewise_union(t, binary.plus, t, t).new() + v = Vector(int, 2) + with pytest.raises(TypeError, match="Literal scalars also"): + s.ewise_union(v, binary.plus, 10, 20) + with pytest.raises(TypeError, match="Literal scalars also"): + s.ewise_union(t, binary.plus, v, 20) + with pytest.raises(TypeError, match="Literal scalars also"): + s.ewise_union(t, binary.plus, 10, v) + + +def test_select(s): + assert select.value(s < 10).new() == s + assert select.value(s > 10).new().is_empty + assert select.valueeq(s, 5).new() == s + assert select.valuene(5, s).new().is_empty @pytest.mark.skipif("not suitesparse") From 4add8fe5c1a63dc2e979ae9dc80a90fd7c612e9a Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 2 Jan 2023 10:38:34 -0600 Subject: [PATCH 05/24] Handle select with scalars for suitesparse-vanilla backend --- .github/workflows/test_and_build.yml | 6 +++--- graphblas/select/__init__.py | 10 +++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index a54084e34..e9d1ca0d1 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -117,8 +117,8 @@ jobs: coveralls --service=github - name: codecov uses: codecov/codecov-action@v3 - # Randomly test notebooks 1/3 of the time - - name: random number + # Randomly test notebooks 1/4 of the time + - name: RNG for running notebooks uses: ddradar/choose-random-action@v2.0.2 id: rand with: @@ -127,7 +127,7 @@ jobs: false weights: | 1 - 2 + 3 - name: Notebooks Execution check if: ${{ steps.rand.outputs.selected == 'true' }} run: | diff --git a/graphblas/select/__init__.py b/graphblas/select/__init__.py index 14dcc6596..73dcd43a7 100644 --- a/graphblas/select/__init__.py +++ b/graphblas/select/__init__.py @@ -45,10 +45,14 @@ def _resolve_expr(expr, callname, opname): thunk -= 1 if method not in globals(): raise ValueError(f"Unknown or unregistered select method: {method}") - if tensor._parent is not None: + if expr._is_scalar: # Handle ScalarExpressions that change their arguments to Vector - tensor = tensor._parent - thunk = thunk._parent + if tensor._parent is not None: # e.g., suitesparse + tensor = tensor._parent + thunk = thunk._parent + else: # e.g., suitesparse-vanilla + tensor = tensor[0].new() + thunk = thunk[0].new() return globals()[method](tensor, thunk) From d4b017ce89adf37d5015fbb9be0598d17f14d4d1 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 2 Jan 2023 11:30:38 -0600 Subject: [PATCH 06/24] More coverage --- graphblas/select/__init__.py | 2 +- graphblas/tests/test_scalar.py | 12 ++++++++++++ graphblas/tests/test_vector.py | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/graphblas/select/__init__.py b/graphblas/select/__init__.py index 73dcd43a7..4f9376ee3 100644 --- a/graphblas/select/__init__.py +++ b/graphblas/select/__init__.py @@ -43,7 +43,7 @@ def _resolve_expr(expr, callname, opname): elif expr.op.name == "ge": method = f"{opname}gt" thunk -= 1 - if method not in globals(): + if method not in globals(): # pragma: no cover (sanity) raise ValueError(f"Unknown or unregistered select method: {method}") if expr._is_scalar: # Handle ScalarExpressions that change their arguments to Vector diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index a72125af1..0158ac2a3 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -522,11 +522,23 @@ def test_ewise_union(s): s.ewise_union(t, binary.plus, 10, v) +def test_ewise_mult_add(s): + assert s.ewise_add(s).new() == 10 + assert s.ewise_mult(s).new() == 25 + v = Vector(int, 2) + with pytest.raises(TypeError, match="Literal scalars also"): + s.ewise_add(v) + with pytest.raises(TypeError, match="Literal scalars also"): + s.ewise_mult(v) + + def test_select(s): assert select.value(s < 10).new() == s assert select.value(s > 10).new().is_empty assert select.valueeq(s, 5).new() == s assert select.valuene(5, s).new().is_empty + with pytest.raises(TypeError): + select.value(s | s) @pytest.mark.skipif("not suitesparse") diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 058334c5f..8e6109bdc 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -732,6 +732,9 @@ def test_select(v): w7 = v.select("==").new() expected = Vector.from_coo([6], 0) assert w7.isequal(expected) + w8 = select.index(v >= 4).new() + expected = Vector.from_coo([4, 6], [2, 0], size=7) + assert w8.isequal(expected) @autocompute From 34121be5668674dce1404e40d17193b6add5cd3c Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Mon, 2 Jan 2023 12:22:43 -0600 Subject: [PATCH 07/24] Missed a docstring for `dup` --- graphblas/core/infix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 832260c81..589c028c0 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -57,6 +57,7 @@ def new(self, dtype=None, *, is_cscalar=False, name=None, **opts): expr = self._to_expr() return expr.new(dtype, is_cscalar=is_cscalar, name=name, **opts) + @wrapdoc(Scalar.dup) def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): if dtype is None: dtype = self.dtype From 61b9d20d7f569c79d2215edbaa394d25bb8e995e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 3 Jan 2023 12:18:16 -0600 Subject: [PATCH 08/24] Add `ruff` ISC (flake8-implicit-str-concat), T20 (flake8-print), and PIE (flake8-pie) --- .pre-commit-config.yaml | 2 +- graphblas/core/automethods.py | 28 ++++++++++++++++++++++------ graphblas/core/infix.py | 12 ++++-------- graphblas/core/matrix.py | 10 +++++----- graphblas/core/operator.py | 4 +--- graphblas/core/scalar.py | 6 ++---- graphblas/core/vector.py | 18 ++++++++++++------ graphblas/tests/test_formatting.py | 6 +++--- graphblas/tests/test_matrix.py | 4 +--- graphblas/viz.py | 9 ++------- pyproject.toml | 22 +++++++++++++++++++++- 11 files changed, 74 insertions(+), 47 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f1b99838..2e806d324 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: files: ^(graphblas|docs)/ args: ["--ignore-words-list=coo,ba"] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.206 + rev: v0.0.209 hooks: - id: ruff args: ["--force-exclude"] diff --git a/graphblas/core/automethods.py b/graphblas/core/automethods.py index 4fcbbe041..dbcd9b237 100644 --- a/graphblas/core/automethods.py +++ b/graphblas/core/automethods.py @@ -466,9 +466,13 @@ def _main(): lines = [] lines.append(" _get_value = automethods._get_value") for name in sorted(common | scalar): - lines.append(f" {name} = wrapdoc(Scalar.{name})(property(automethods.{name}))") if name == "name": - lines.append(" name = name.setter(automethods._set_name)") + lines.append( + " name = wrapdoc(Scalar.name)(property(automethods.name))" + ".setter(automethods._set_name)" + ) + else: + lines.append(f" {name} = wrapdoc(Scalar.{name})(property(automethods.{name}))") lines.append(" # These raise exceptions") for name in sorted(common_raises | scalar_raises): lines.append(f" {name} = Scalar.{name}") @@ -497,9 +501,15 @@ def get_name(line): indent = " " else: indent = "" - lines.append(f" {indent}{name} = wrapdoc(Vector.{name})(property(automethods.{name}))") if name == "name": - lines.append(" name = name.setter(automethods._set_name)") + lines.append( + " name = wrapdoc(Vector.name)(property(automethods.name))" + ".setter(automethods._set_name)" + ) + else: + lines.append( + f" {indent}{name} = wrapdoc(Vector.{name})(property(automethods.{name}))" + ) lines.append(" # These raise exceptions") for name in sorted(common_raises | vector_matrix_raises): lines.append(f" {name} = Vector.{name}") @@ -520,9 +530,15 @@ def get_name(line): indent = " " else: indent = "" - lines.append(f" {indent}{name} = wrapdoc(Matrix.{name})(property(automethods.{name}))") if name == "name": - lines.append(" name = name.setter(automethods._set_name)") + lines.append( + " name = wrapdoc(Matrix.name)(property(automethods.name))" + ".setter(automethods._set_name)" + ) + else: + lines.append( + f" {indent}{name} = wrapdoc(Matrix.{name})(property(automethods.{name}))" + ) lines.append(" # These raise exceptions") for name in sorted(common_raises | vector_matrix_raises): lines.append(f" {name} = Matrix.{name}") diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 589c028c0..9b876d756 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -98,8 +98,7 @@ def is_grbscalar(self): is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) - name = wrapdoc(Scalar.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Scalar.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) @@ -192,8 +191,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): inner = wrapdoc(Vector.inner)(property(automethods.inner)) isclose = wrapdoc(Vector.isclose)(property(automethods.isclose)) isequal = wrapdoc(Vector.isequal)(property(automethods.isequal)) - name = wrapdoc(Vector.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Vector.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Vector.nvals)(property(automethods.nvals)) outer = wrapdoc(Vector.outer)(property(automethods.outer)) reduce = wrapdoc(Vector.reduce)(property(automethods.reduce)) @@ -321,8 +319,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): kronecker = wrapdoc(Matrix.kronecker)(property(automethods.kronecker)) mxm = wrapdoc(Matrix.mxm)(property(automethods.mxm)) mxv = wrapdoc(Matrix.mxv)(property(automethods.mxv)) - name = wrapdoc(Matrix.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Matrix.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Matrix.nvals)(property(automethods.nvals)) reduce_columnwise = wrapdoc(Matrix.reduce_columnwise)(property(automethods.reduce_columnwise)) reduce_rowwise = wrapdoc(Matrix.reduce_rowwise)(property(automethods.reduce_rowwise)) @@ -426,8 +423,7 @@ class ScalarMatMulExpr(ScalarInfixExpr): is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) - name = wrapdoc(Scalar.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Scalar.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 0dfb4f73f..26165a285 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -2685,7 +2685,9 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o if is_submask: # C[I, J](M) << A expr_repr = ( - "[[{2._expr_name} rows], [{4._expr_name} cols]]" f"({mask.name})" " << {0.name}" + "[[{2._expr_name} rows], [{4._expr_name} cols]]" + f"({mask.name})" # fmt: skip + " << {0.name}" ) if backend == "suitesparse": cfunc_name = "GxB_Matrix_subassign" @@ -3099,8 +3101,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): kronecker = wrapdoc(Matrix.kronecker)(property(automethods.kronecker)) mxm = wrapdoc(Matrix.mxm)(property(automethods.mxm)) mxv = wrapdoc(Matrix.mxv)(property(automethods.mxv)) - name = wrapdoc(Matrix.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Matrix.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Matrix.nvals)(property(automethods.nvals)) reduce_columnwise = wrapdoc(Matrix.reduce_columnwise)(property(automethods.reduce_columnwise)) reduce_rowwise = wrapdoc(Matrix.reduce_rowwise)(property(automethods.reduce_rowwise)) @@ -3196,8 +3197,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): kronecker = wrapdoc(Matrix.kronecker)(property(automethods.kronecker)) mxm = wrapdoc(Matrix.mxm)(property(automethods.mxm)) mxv = wrapdoc(Matrix.mxv)(property(automethods.mxv)) - name = wrapdoc(Matrix.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Matrix.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Matrix.nvals)(property(automethods.nvals)) reduce_columnwise = wrapdoc(Matrix.reduce_columnwise)(property(automethods.reduce_columnwise)) reduce_rowwise = wrapdoc(Matrix.reduce_rowwise)(property(automethods.reduce_rowwise)) diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 569916ad3..6170016bd 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -1297,10 +1297,8 @@ class IndexUnaryOp(OpBase): re.compile("^GxB_(VALUEEQ|VALUENE)_(FC32|FC64)$"), ], } - # fmt: off _positional = {"tril", "triu", "diag", "offdiag", "colle", "colgt", "rowle", "rowgt", - "rowindex", "colindex"} - # fmt: on + "rowindex", "colindex"} # fmt: skip @classmethod def _build(cls, name, func, *, is_udt=False, anonymous=False): diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index 005934434..690e4c557 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -844,8 +844,7 @@ def _repr_html_(self): is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) - name = wrapdoc(Scalar.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Scalar.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) @@ -921,8 +920,7 @@ def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) - name = wrapdoc(Scalar.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Scalar.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) select = wrapdoc(Scalar.select)(property(automethods.select)) value = wrapdoc(Scalar.value)(property(automethods.value)) diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index 98434d22d..ff977efe7 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -1607,7 +1607,11 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o else: cfunc_name = "GrB_Vector_assign" mask = _vanilla_subassign_mask(self, mask, idx, replace, opts) - expr_repr = "[[{2._expr_name} elements]]" f"({mask.name})" " = {0.name}" + expr_repr = ( + "[[{2._expr_name} elements]]" + f"({mask.name})" # fmt: skip + " = {0.name}" + ) else: # v(m)[I] << w # v[I] << w @@ -1699,7 +1703,11 @@ def _prep_for_assign(self, resolved_indexes, value, mask, is_submask, replace, o else: cfunc_name = "GrB_Vector_assign_Scalar" mask = _vanilla_subassign_mask(self, mask, idx, replace, opts) - expr_repr = "[[{2._expr_name} elements]]" f"({mask.name})" " = {0._expr_name}" + expr_repr = ( + "[[{2._expr_name} elements]]" + f"({mask.name})" # fmt: skip + " = {0._expr_name}" + ) else: # v(m)[I] << c # v[I] << c @@ -1885,8 +1893,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): inner = wrapdoc(Vector.inner)(property(automethods.inner)) isclose = wrapdoc(Vector.isclose)(property(automethods.isclose)) isequal = wrapdoc(Vector.isequal)(property(automethods.isequal)) - name = wrapdoc(Vector.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Vector.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Vector.nvals)(property(automethods.nvals)) outer = wrapdoc(Vector.outer)(property(automethods.outer)) reduce = wrapdoc(Vector.reduce)(property(automethods.reduce)) @@ -1968,8 +1975,7 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): inner = wrapdoc(Vector.inner)(property(automethods.inner)) isclose = wrapdoc(Vector.isclose)(property(automethods.isclose)) isequal = wrapdoc(Vector.isequal)(property(automethods.isequal)) - name = wrapdoc(Vector.name)(property(automethods.name)) - name = name.setter(automethods._set_name) + name = wrapdoc(Vector.name)(property(automethods.name)).setter(automethods._set_name) nvals = wrapdoc(Vector.nvals)(property(automethods.nvals)) outer = wrapdoc(Vector.outer)(property(automethods.outer)) reduce = wrapdoc(Vector.reduce)(property(automethods.reduce)) diff --git a/graphblas/tests/test_formatting.py b/graphblas/tests/test_formatting.py index 82212d895..3094aea91 100644 --- a/graphblas/tests/test_formatting.py +++ b/graphblas/tests/test_formatting.py @@ -159,7 +159,7 @@ def test_no_pandas_repr(A, C, v, w): ) repr_printer(v, "v", indent=8) assert repr(v) == ( - '"v" nvals size dtype format\n' "gb.Vector 3 5 FP64 bitmap" + '"v" nvals size dtype format\ngb.Vector 3 5 FP64 bitmap' ) repr_printer(~w.V, "~w.V", indent=8) assert repr(~w.V) == ( @@ -503,8 +503,8 @@ def test_vector_mask_repr_large(w): def test_scalar_repr(s, t): repr_printer(s, "s") - assert repr(s) == ('"s_1" value dtype\n' "gb.Scalar 42 INT64") - assert repr(t) == ('"t" value dtype\n' "gb.Scalar None INT64") + assert repr(s) == ('"s_1" value dtype\ngb.Scalar 42 INT64') + assert repr(t) == ('"t" value dtype\ngb.Scalar None INT64') def test_no_pandas_repr_html(A, C, v, w): diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index ce764f51a..dd41b0bd0 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -1799,13 +1799,11 @@ def test_isclose(A, v): [3.0, 2.0, 3.0, 1.0, 5.0, 3.0, 7.0, 8.0, 3.0, 1.0, 7.0, 4.0], ) assert not C4.isclose(A, check_dtype=True), "different datatypes are not equal" - # fmt: off C5 = Matrix.from_coo( [3, 0, 3, 5, 6, 0, 6, 1, 6, 2, 4, 1], - [0, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6], + [0, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 6], # fmt: skip [3.0, 2.0, 3.0, 1.0, 5.0, 3.000000000000000001, 7.0, 8.0, 3.0, 1 - 1e-11, 7.0, 4.0], ) - # fmt: on assert C5.isclose(A) C6 = Matrix.from_coo( [3, 0, 3, 5, 6, 0, 6, 1, 6, 2, 4, 1], diff --git a/graphblas/viz.py b/graphblas/viz.py index 8f1f9fc36..c58a1b81b 100644 --- a/graphblas/viz.py +++ b/graphblas/viz.py @@ -53,15 +53,10 @@ def draw(m): # pragma: no cover .. image:: /_static/img/draw-example.png """ - try: - nx, plt = _get_imports(["nx", "plt"], "draw") - except ImportError as exc: - print(exc.msg) - return + nx, plt = _get_imports(["nx", "plt"], "draw") typ = _output_type(m) if typ is not _Matrix and typ is not _TransposedMatrix: - print(f"Can only draw a Matrix, not {type(m)}") - return + raise TypeError(f"Can only draw a Matrix, not {type(m)}") g = to_networkx(m) pos = nx.spring_layout(g) diff --git a/pyproject.toml b/pyproject.toml index 9e3abcd10..d4be65c62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,35 +6,55 @@ line-length = 100 target-version = ["py38", "py39", "py310", "py311"] [tool.ruff] +# https://github.com/charliermarsh/ruff/ line-length = 100 target-version = "py38" select = [ "F", # pyflakes "E", # pycodestyle Error "W", # pycodestyle Warning + # "C90", # mccabe + # "I", # isort + # "D", # pydocstyle "UP", # pyupgrade + # "N", # pep8-naming "YTT", # flake8-2020 + # "ANN", # flake8-annotations "S", # bandit + # "BLE", # flake8-blind-except + # "FBT", # flake8-boolean-trap "B", # flake8-bugbear + # "A", # flake8-builtins "C4", # flake8-comprehensions "T10", # flake8-debugger + "ISC", # flake8-implicit-str-concat + # "ICN", # flake8-import-conventions + "T20", # flake8-print + # "PT", # flake8-pytest-style "Q", # flake8-quotes + # "RET", # flake8-return "SIM", # flake8-simplify + # "TID", # flake8-tidy-imports + # "ARG", # flake8-unused-arguments "DTZ", # flake8-datetimez + # "ERA", # eradicate + # "PD", # pandas-vet "PGH", # pygrep-hooks "PLC", # pylint Convention "PLE", # pylint Error "PLR", # pylint Refactor "PLW", # pylint Warning + "PIE", # flake8-pie "RUF", # ruff-specific rules ] exclude = [ "versioneer.py", + "graphblas/_version.py", ] [tool.ruff.per-file-ignores] "graphblas/_version.py" = ["S101"] "graphblas/core/operator.py" = ["S102"] -"graphblas/tests/*py" = ["S101"] +"graphblas/tests/*py" = ["S101", "T201"] "graphblas/tests/test_dtype.py" = ["UP003"] "graphblas/tests/test_formatting.py" = ["E501"] "scripts/create_pickle.py" = ["F403", "F405"] From f8018b8503ede8c2937286573c4eb1ec3ad8378e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 15:31:13 -0600 Subject: [PATCH 09/24] Support awkward version 2; also support 11 more pylint checks --- .pre-commit-config.yaml | 2 +- graphblas/binary/numpy.py | 4 ++-- graphblas/core/base.py | 4 ++-- graphblas/core/matrix.py | 3 ++- graphblas/core/operator.py | 4 ++-- graphblas/core/ss/matrix.py | 23 ++++++++++++----------- graphblas/io.py | 18 +++++++++++++----- graphblas/tests/test_matrix.py | 6 +++--- graphblas/tests/test_op.py | 4 ++-- graphblas/tests/test_ss_utils.py | 2 +- graphblas/tests/test_vector.py | 9 ++++----- graphblas/unary/numpy.py | 4 ++-- pyproject.toml | 15 ++------------- 13 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2e806d324..4c259728f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: files: ^(graphblas|docs)/ args: ["--ignore-words-list=coo,ba"] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.209 + rev: v0.0.210 hooks: - id: ruff args: ["--force-exclude"] diff --git a/graphblas/binary/numpy.py b/graphblas/binary/numpy.py index 7d55d83fa..a5855718c 100644 --- a/graphblas/binary/numpy.py +++ b/graphblas/binary/numpy.py @@ -135,8 +135,8 @@ def __dir__(): def __getattr__(name): if name in _delayed: - func, kwargs = _delayed.pop(name) - rv = func(**kwargs) + delayed_func, kwargs = _delayed.pop(name) + rv = delayed_func(**kwargs) globals()[name] = rv return rv if name not in _binary_names: diff --git a/graphblas/core/base.py b/graphblas/core/base.py index afe6b1dc2..d1b42a32c 100644 --- a/graphblas/core/base.py +++ b/graphblas/core/base.py @@ -157,8 +157,8 @@ def _expect_op_message( ) -def _expect_op(self, op, values, **kwargs): - if (message := _expect_op_message(self, op, values, **kwargs)) is not None: +def _expect_op(self, op, values, *, within, **kwargs): + if (message := _expect_op_message(self, op, values, within=within, **kwargs)) is not None: raise TypeError(message) from None diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 34a0b80f0..034ffc53c 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -247,7 +247,8 @@ def __getitem__(self, keys): return ScalarIndexExpr(self, resolved_indexes) if len(shape) == 1: return VectorIndexExpr(self, resolved_indexes, *shape) - return MatrixIndexExpr(self, resolved_indexes, *shape) + nrows, ncols = shape + return MatrixIndexExpr(self, resolved_indexes, nrows, ncols) def __setitem__(self, keys, expr, **opts): """Assign values to a single element, row/column, or submatrix. diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 6170016bd..1b7c22e93 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -650,7 +650,7 @@ def _call(self, *args, **kwargs): binop._monoid = binop # temporary! try: # If this call is successful, then it will set `binop._monoid` - self._monoid(*args, **kwargs) + self._monoid(*args, **kwargs) # pylint: disable=not-callable except Exception: binop._monoid = None # assert binop._monoid is not binop @@ -1807,7 +1807,7 @@ def _get_udt_wrapper(numba_func, return_type, dtype, dtype2=None, *, include_ind f"{zarray}{xarray}{yarray}" f" {zname} = {BL}numba_func({xname}{rcidx}{yname}){BR}\n" ) - exec(text, d) + exec(text, d) # pylint: disable=exec-used return d["wrapper"], wrapper_sig diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index 783248043..6b42b7abf 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -4180,7 +4180,8 @@ def _compactify(self, how, reverse, asindex, nkey, nval, fmt, indices_name, name values = compact_values(orig_indptr, new_indptr, values) if reverse: values = reverse_values(new_indptr, values) - newinfo = dict(info, indptr=new_indptr, values=values) + newinfo = dict(info, indptr=new_indptr) + newinfo["values"] = values newinfo[indices_name] = new_indices newinfo[nkey] = nval return self.import_any( @@ -4360,7 +4361,7 @@ def deserialize(cls, data, dtype=None, *, name=None, **opts): @numba.njit(parallel=True) def argsort_values(indptr, indices, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=np.uint64) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable rv[indptr[i] : indptr[i + 1]] = indices[ np.int64(indptr[i]) + np.argsort(values[indptr[i] : indptr[i + 1]]) ] @@ -4370,7 +4371,7 @@ def argsort_values(indptr, indices, values): # pragma: no cover (numba) @numba.njit(parallel=True) def sort_values(indptr, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=values.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable rv[indptr[i] : indptr[i + 1]] = np.sort(values[indptr[i] : indptr[i + 1]]) return rv @@ -4378,7 +4379,7 @@ def sort_values(indptr, values): # pragma: no cover (numba) @numba.njit(parallel=True) def compact_values(old_indptr, new_indptr, values): # pragma: no cover (numba) rv = np.empty(new_indptr[-1], dtype=values.dtype) - for i in numba.prange(new_indptr.size - 1): + for i in numba.prange(new_indptr.size - 1): # pylint: disable=not-an-iterable start = np.int64(new_indptr[i]) offset = np.int64(old_indptr[i]) - start for j in range(start, new_indptr[i + 1]): @@ -4389,7 +4390,7 @@ def compact_values(old_indptr, new_indptr, values): # pragma: no cover (numba) @numba.njit(parallel=True) def reverse_values(indptr, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=values.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable offset = np.int64(indptr[i]) + np.int64(indptr[i + 1]) - 1 for j in range(indptr[i], indptr[i + 1]): rv[j] = values[offset - j] @@ -4406,7 +4407,7 @@ def compact_indices(indptr, k): # pragma: no cover (numba) indptr = create_indptr(indptr, k) col_indices = np.empty(indptr[-1], dtype=np.uint64) N = np.int64(0) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable start = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - start N = max(N, deg) @@ -4419,7 +4420,7 @@ def compact_indices(indptr, k): # pragma: no cover (numba) def choose_random1(indptr): # pragma: no cover (numba) choices = np.empty(indptr.size - 1, dtype=indptr.dtype) new_indptr = np.arange(indptr.size, dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if deg == 1: @@ -4456,7 +4457,7 @@ def choose_random(indptr, k): # pragma: no cover (numba) # be nice to have them sorted if convenient to do so. new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4537,7 +4538,7 @@ def choose_first(indptr, k): # pragma: no cover (numba) new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4561,7 +4562,7 @@ def choose_last(indptr, k): # pragma: no cover (numba) new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4606,7 +4607,7 @@ def indices_to_indptr(indices, size): # pragma: no cover (numba) @njit(parallel=True) def indptr_to_indices(indptr): # pragma: no cover (numba) indices = np.empty(indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): + for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable for j in range(indptr[i], indptr[i + 1]): indices[j] = i return indices diff --git a/graphblas/io.py b/graphblas/io.py index 51b9bc844..b29c483c5 100644 --- a/graphblas/io.py +++ b/graphblas/io.py @@ -382,10 +382,18 @@ def to_awkward(A, format=None): awkward.Array """ - import awkward._v2 as ak - from awkward._v2.forms.listoffsetform import ListOffsetForm - from awkward._v2.forms.numpyform import NumpyForm - from awkward._v2.forms.recordform import RecordForm + try: + # awkward version 1 + import awkward._v2 as ak + from awkward._v2.forms.listoffsetform import ListOffsetForm + from awkward._v2.forms.numpyform import NumpyForm + from awkward._v2.forms.recordform import RecordForm + except ImportError: + # awkward version 2 + import awkward as ak + from awkward.forms.listoffsetform import ListOffsetForm + from awkward.forms.numpyform import NumpyForm + from awkward.forms.recordform import RecordForm out_type = _output_type(A) if format is None: @@ -442,7 +450,7 @@ def to_awkward(A, format=None): ) if format.startswith("hyper"): global _AwkwardDoublyCompressedMatrix - if _AwkwardDoublyCompressedMatrix is None: + if _AwkwardDoublyCompressedMatrix is None: # pylint: disable=used-before-assignment # Define behaviors to make all fields function at the top-level @ak.behaviors.mixins.mixin_class(ak.behavior) class _AwkwardDoublyCompressedMatrix: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index dd41b0bd0..39a80b6fd 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2440,17 +2440,17 @@ def import_func(x, import_name, **kwargs): d = C2.ss.unpack(format, raw=raw) if in_method == "import": - def import_func(x, import_name, **kwargs): + def import_func2(x, import_name, **kwargs): return getattr(Matrix.ss, f"import_{import_name}")(**kwargs) else: - def import_func(x, import_name, **kwargs): + def import_func2(x, import_name, **kwargs): getattr(x.ss, f"pack_{import_name}")(**kwargs) return x d["format"] = import_format - other = import_func(C2, import_name, take_ownership=take_ownership, **d) + other = import_func2(C2, import_name, take_ownership=take_ownership, **d) if format == "fullc" and raw and import_format is None and import_name == "any": # It's 1d, so we can't tell we're column-oriented w/o format keyword if do_iso: diff --git a/graphblas/tests/test_op.py b/graphblas/tests/test_op.py index 08beb86fa..c3aaec813 100644 --- a/graphblas/tests/test_op.py +++ b/graphblas/tests/test_op.py @@ -1182,10 +1182,10 @@ def badfunc(x): # pragma: no cover (numba) assert udt not in badunary assert int not in badunary - def badfunc(x, y): # pragma: no cover (numba) + def badfunc2(x, y): # pragma: no cover (numba) return BreakCompile(x) - badbinary = BinaryOp.register_anonymous(badfunc, is_udt=True) + badbinary = BinaryOp.register_anonymous(badfunc2, is_udt=True) assert udt not in badbinary assert int not in badbinary diff --git a/graphblas/tests/test_ss_utils.py b/graphblas/tests/test_ss_utils.py index 298a8a53e..dd3b99208 100644 --- a/graphblas/tests/test_ss_utils.py +++ b/graphblas/tests/test_ss_utils.py @@ -194,7 +194,7 @@ def test_about(): about["badkey"] assert "SuiteSparse" in about["library_name"] with pytest.raises(TypeError): - del about["library_name"] + del about["library_name"] # pylint: disable=unsupported-delete-operation assert "library_name" in repr(about) diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 8e6109bdc..52f4f185a 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -1276,7 +1276,6 @@ def test_ss_import_export_auto(v, do_iso, methods): else: d = v2.ss.unpack(format, sort=sort, raw=raw) if in_method == "import": - import_func = getattr(Vector.ss, f"import_{import_name}") def import_func(x, import_name, **kwargs): return getattr(Vector.ss, f"import_{import_name}")(**kwargs) @@ -1317,22 +1316,22 @@ def import_func(x, import_name, **kwargs): d = w2.ss.unpack(format, raw=raw) if in_method == "import": - def import_func(x, import_name, **kwargs): + def import_func2(x, import_name, **kwargs): return getattr(Vector.ss, f"import_{import_name}")(**kwargs) else: - def import_func(x, import_name, **kwargs): + def import_func2(x, import_name, **kwargs): getattr(x.ss, f"pack_{import_name}")(**kwargs) return x d["format"] = import_format - other = import_func(w2, import_name, take_ownership=take_ownership, **d) + other = import_func2(w2, import_name, take_ownership=take_ownership, **d) assert other.isequal(w_orig) assert other.ss.is_iso is do_iso d["format"] = "bad_format" with pytest.raises(ValueError, match="Invalid format"): - import_func(w2, import_name, **d) + import_func2(w2, import_name, **d) assert w.isequal(w_orig) assert w.ss.is_iso is do_iso assert w_orig.ss.is_iso is do_iso diff --git a/graphblas/unary/numpy.py b/graphblas/unary/numpy.py index 46698d5e1..b4cea6ae0 100644 --- a/graphblas/unary/numpy.py +++ b/graphblas/unary/numpy.py @@ -124,8 +124,8 @@ def __dir__(): def __getattr__(name): if name in _delayed: - func, kwargs = _delayed.pop(name) - rv = func(**kwargs) + delayed_func, kwargs = _delayed.pop(name) + rv = delayed_func(**kwargs) globals()[name] = rv return rv if name not in _unary_names: diff --git a/pyproject.toml b/pyproject.toml index d4be65c62..8d6acc6fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,21 +65,13 @@ py-version = "3.8" ignore-paths = ["graphblas/_version.py", "versioneer.py"] disable = [ # Error - "assigning-non-slot", + "assigning-non-slot", # almost passes "assignment-from-no-return", - "function-redefined", - "missing-kwoa", - "no-value-for-parameter", - "not-callable", - "repeated-keyword", - "unexpected-special-method-signature", - "unsupported-delete-operation", - "used-before-assignment", + "unexpected-special-method-signature", # almost passes # Warning "arguments-differ", "arguments-out-of-order", - "exec-used", "expression-not-assigned", "fixme", "global-statement", @@ -93,8 +85,6 @@ disable = [ "unspecified-encoding", "unused-argument", "unused-variable", - "unused-wildcard-import", - "wildcard-import", # Refactor "cyclic-import", @@ -114,7 +104,6 @@ disable = [ "invalid-unary-operand-type", "no-member", "no-name-in-module", - "not-an-iterable", "too-many-function-args", "unexpected-keyword-arg", # warning From 57d5e67b37e53245da468951654c7e45c22c61bd Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 21:28:26 -0600 Subject: [PATCH 10/24] Revert: ignore pylint not-an-iterable --- graphblas/core/ss/matrix.py | 20 ++++++++++---------- pyproject.toml | 1 + 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/graphblas/core/ss/matrix.py b/graphblas/core/ss/matrix.py index 6b42b7abf..f89c61d30 100644 --- a/graphblas/core/ss/matrix.py +++ b/graphblas/core/ss/matrix.py @@ -4361,7 +4361,7 @@ def deserialize(cls, data, dtype=None, *, name=None, **opts): @numba.njit(parallel=True) def argsort_values(indptr, indices, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=np.uint64) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): rv[indptr[i] : indptr[i + 1]] = indices[ np.int64(indptr[i]) + np.argsort(values[indptr[i] : indptr[i + 1]]) ] @@ -4371,7 +4371,7 @@ def argsort_values(indptr, indices, values): # pragma: no cover (numba) @numba.njit(parallel=True) def sort_values(indptr, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=values.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): rv[indptr[i] : indptr[i + 1]] = np.sort(values[indptr[i] : indptr[i + 1]]) return rv @@ -4379,7 +4379,7 @@ def sort_values(indptr, values): # pragma: no cover (numba) @numba.njit(parallel=True) def compact_values(old_indptr, new_indptr, values): # pragma: no cover (numba) rv = np.empty(new_indptr[-1], dtype=values.dtype) - for i in numba.prange(new_indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(new_indptr.size - 1): start = np.int64(new_indptr[i]) offset = np.int64(old_indptr[i]) - start for j in range(start, new_indptr[i + 1]): @@ -4390,7 +4390,7 @@ def compact_values(old_indptr, new_indptr, values): # pragma: no cover (numba) @numba.njit(parallel=True) def reverse_values(indptr, values): # pragma: no cover (numba) rv = np.empty(indptr[-1], dtype=values.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): offset = np.int64(indptr[i]) + np.int64(indptr[i + 1]) - 1 for j in range(indptr[i], indptr[i + 1]): rv[j] = values[offset - j] @@ -4407,7 +4407,7 @@ def compact_indices(indptr, k): # pragma: no cover (numba) indptr = create_indptr(indptr, k) col_indices = np.empty(indptr[-1], dtype=np.uint64) N = np.int64(0) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): start = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - start N = max(N, deg) @@ -4420,7 +4420,7 @@ def compact_indices(indptr, k): # pragma: no cover (numba) def choose_random1(indptr): # pragma: no cover (numba) choices = np.empty(indptr.size - 1, dtype=indptr.dtype) new_indptr = np.arange(indptr.size, dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if deg == 1: @@ -4457,7 +4457,7 @@ def choose_random(indptr, k): # pragma: no cover (numba) # be nice to have them sorted if convenient to do so. new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4538,7 +4538,7 @@ def choose_first(indptr, k): # pragma: no cover (numba) new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4562,7 +4562,7 @@ def choose_last(indptr, k): # pragma: no cover (numba) new_indptr = create_indptr(indptr, k) choices = np.empty(new_indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): idx = np.int64(indptr[i]) deg = np.int64(indptr[i + 1]) - idx if k < deg: @@ -4607,7 +4607,7 @@ def indices_to_indptr(indices, size): # pragma: no cover (numba) @njit(parallel=True) def indptr_to_indices(indptr): # pragma: no cover (numba) indices = np.empty(indptr[-1], dtype=indptr.dtype) - for i in numba.prange(indptr.size - 1): # pylint: disable=not-an-iterable + for i in numba.prange(indptr.size - 1): for j in range(indptr[i], indptr[i + 1]): indices[j] = i return indices diff --git a/pyproject.toml b/pyproject.toml index 8d6acc6fa..79ae44d26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,6 +104,7 @@ disable = [ "invalid-unary-operand-type", "no-member", "no-name-in-module", + "not-an-iterable", "too-many-function-args", "unexpected-keyword-arg", # warning From 93d1afd26765be2dd3cdc5f965d70de23515816e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 22:21:18 -0600 Subject: [PATCH 11/24] Move more config to pyproject.toml; fix awkward tests --- graphblas/tests/test_io.py | 7 +++++-- pyproject.toml | 19 +++++++++++++++++++ setup.cfg | 14 -------------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/graphblas/tests/test_io.py b/graphblas/tests/test_io.py index fe26373fb..3efae6369 100644 --- a/graphblas/tests/test_io.py +++ b/graphblas/tests/test_io.py @@ -17,8 +17,11 @@ try: import awkward._v2 as ak -except ImportError: # pragma: no cover (import) - ak = None +except ImportError: + try: + import awkward as ak + except ImportError: + ak = None suitesparse = gb.backend == "suitesparse" diff --git a/pyproject.toml b/pyproject.toml index 79ae44d26..22aa7a911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,22 @@ requires = ["setuptools", "wheel"] line-length = 100 target-version = ["py38", "py39", "py310", "py311"] +[tool.isort] +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +profile = "black" +skip_gitignore = true +float_to_top = true +default_section = "THIRDPARTY" +known_first_party = "graphblas" +line_length = 100 + +[tool.pytest.ini_options] +testpaths = "graphblas/tests" +xfail_strict = true +markers = [ + "slow: Skipped unless --runslow passed", +] + [tool.ruff] # https://github.com/charliermarsh/ruff/ line-length = 100 @@ -47,6 +63,9 @@ select = [ "PIE", # flake8-pie "RUF", # ruff-specific rules ] +ignore = [ + "D203", # 1 blank line required before class docstring +] exclude = [ "versioneer.py", "graphblas/_version.py", diff --git a/setup.cfg b/setup.cfg index f103bdd3d..8d5fb6b64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,15 +13,6 @@ extend-ignore = # E203 whitespace before ':' (to be compatible with black) per-file-ignores = scripts/create_pickle.py:F403,F405 -[isort] -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -profile = black -skip_gitignore = true -float_to_top = true -default_section = THIRDPARTY -known_first_party = graphblas -line_length = 100 - [versioneer] VCS = git style = pep440 @@ -30,11 +21,6 @@ versionfile_build = graphblas/_version.py tag_prefix= parentdir_prefix=python-graphblas- -[tool:pytest] -testpaths = graphblas/tests -markers: - slow: Skipped unless --runslow passed - [coverage:run] source = graphblas # TODO: test and get coverage for viz.py From 51bde4ba43631af09f95c21ac13cfb62c2f27e9d Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 22:47:52 -0600 Subject: [PATCH 12/24] coverage --- graphblas/io.py | 2 +- graphblas/tests/test_io.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/graphblas/io.py b/graphblas/io.py index b29c483c5..6d98a5969 100644 --- a/graphblas/io.py +++ b/graphblas/io.py @@ -382,7 +382,7 @@ def to_awkward(A, format=None): awkward.Array """ - try: + try: # pragma: no cover (import) # awkward version 1 import awkward._v2 as ak from awkward._v2.forms.listoffsetform import ListOffsetForm diff --git a/graphblas/tests/test_io.py b/graphblas/tests/test_io.py index 3efae6369..b420fa5c5 100644 --- a/graphblas/tests/test_io.py +++ b/graphblas/tests/test_io.py @@ -20,7 +20,7 @@ except ImportError: try: import awkward as ak - except ImportError: + except ImportError: # pragma: no cover (import) ak = None From d136fffedd6bbe2bdfeb9b7b19a289b28f4be759 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 23:15:01 -0600 Subject: [PATCH 13/24] CI: break test section into multiple sections for easier review --- .github/workflows/test_and_build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index e9d1ca0d1..be09b5b9e 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -79,6 +79,8 @@ jobs: - name: Unit tests run: | coverage run --branch -m pytest ${{ matrix.cfg.testopts }} -v + - name: Unit tests (bizarro scalars) + run: | # Run tests again with Scalars being C scalars by default find graphblas -type f -name "*.py" -print0 | xargs -0 sed -i -s \ -e '/# pragma: is_grbscalar/! s/is_cscalar=False/is_cscalar=True/g' \ @@ -87,7 +89,8 @@ jobs: -e '/# pragma: to_grb/ s/is_cscalar = True/is_cscalar = False/g' coverage run -a --branch -m pytest ${{ matrix.cfg.testopts }} -v git checkout . # Undo changes to scalar default - + - name: Miscellaneous tests + run: | # Test (and cover) automatic initialization coverage run -a --branch graphblas/tests/test_auto_init.py coverage run -a --branch graphblas/tests/test_external_init.py From 22b175c21469c2188d21ce3d890838a524c51f7e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 4 Jan 2023 23:51:23 -0600 Subject: [PATCH 14/24] Only test one slow task per CI job --- .github/workflows/test_and_build.yml | 32 ++++++++++++++++------------ 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index be09b5b9e..9bb35ada9 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -76,9 +76,24 @@ jobs: mamba install python-suitesparse-graphblas fi pip install --no-deps -e . + # Randomly test up to one slow task per job + - name: RNG for tests + uses: ddradar/choose-random-action@v2.0.2 + id: rand + with: + contents: | + slow_fast_fast + fast_slow_fast + fast_fast_slow + fast_fast_fast + weights: | + 1 + 1 + 1 + 1 - name: Unit tests run: | - coverage run --branch -m pytest ${{ matrix.cfg.testopts }} -v + coverage run --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'slow_fast_fast' && '--runslow' || '' }} - name: Unit tests (bizarro scalars) run: | # Run tests again with Scalars being C scalars by default @@ -87,7 +102,7 @@ jobs: -e '/# pragma: is_grbscalar/! s/is_cscalar = False/is_cscalar = True/g' \ -e '/# pragma: to_grb/ s/is_cscalar=True/is_cscalar=False/g' \ -e '/# pragma: to_grb/ s/is_cscalar = True/is_cscalar = False/g' - coverage run -a --branch -m pytest ${{ matrix.cfg.testopts }} -v + coverage run -a --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'fast_slow_fast' && '--runslow' || ''}} git checkout . # Undo changes to scalar default - name: Miscellaneous tests run: | @@ -120,19 +135,8 @@ jobs: coveralls --service=github - name: codecov uses: codecov/codecov-action@v3 - # Randomly test notebooks 1/4 of the time - - name: RNG for running notebooks - uses: ddradar/choose-random-action@v2.0.2 - id: rand - with: - contents: | - true - false - weights: | - 1 - 3 - name: Notebooks Execution check - if: ${{ steps.rand.outputs.selected == 'true' }} + if: steps.rand.outputs.selected == 'fast_fast_slow' run: | mamba install matplotlib nbconvert jupyter 'ipython>=7' jupyter nbconvert --to notebook --execute notebooks/*ipynb From c43ac8afd13a98376d4b7cf58b0ac0b8231f7612 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 09:50:56 -0600 Subject: [PATCH 15/24] Better; I knew I would like randomization :) --- .github/workflows/test_and_build.yml | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index 9bb35ada9..ce803ad43 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -56,9 +56,24 @@ jobs: channel-priority: strict activate-environment: graphblas auto-activate-base: false + # Randomly test up to one slow task per job (about 5% chance one group isn't run) + - name: RNG for tests + uses: ddradar/choose-random-action@v2.0.2 + id: rand + with: + contents: | + slow_fast_fast + fast_slow_fast + fast_fast_slow + fast_fast_fast + weights: | + 1 + 1 + 1 + 1 - name: Update env run: | - mamba install pytest coverage coveralls=3.3.1 pytest-randomly black \ + mamba install pytest coverage coveralls=3.3.1 pytest-randomly ${{ steps.rand.outputs.selected == 'fast_fast_fast' && 'black' || '' }} \ pandas numba scipy networkx cffi donfig pyyaml awkward - name: Build extension module run: | @@ -76,21 +91,6 @@ jobs: mamba install python-suitesparse-graphblas fi pip install --no-deps -e . - # Randomly test up to one slow task per job - - name: RNG for tests - uses: ddradar/choose-random-action@v2.0.2 - id: rand - with: - contents: | - slow_fast_fast - fast_slow_fast - fast_fast_slow - fast_fast_fast - weights: | - 1 - 1 - 1 - 1 - name: Unit tests run: | coverage run --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'slow_fast_fast' && '--runslow' || '' }} @@ -105,6 +105,7 @@ jobs: coverage run -a --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'fast_slow_fast' && '--runslow' || ''}} git checkout . # Undo changes to scalar default - name: Miscellaneous tests + if: steps.rand.outputs.selected == 'fast_fast_fast' run: | # Test (and cover) automatic initialization coverage run -a --branch graphblas/tests/test_auto_init.py @@ -120,17 +121,19 @@ jobs: coverage run -a --branch script.py rm script.py - name: Auto-generated code check + if: steps.rand.outputs.selected == 'fast_fast_fast' run: | + # This step uses `black` coverage run -a --branch -m graphblas.core.automethods coverage run -a --branch -m graphblas.core.infixmethods git diff --exit-code - coverage xml - name: Coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_FLAG_NAME: ${{ matrix.cfg.pyver}}/${{ matrix.cfg.testopts }} COVERALLS_PARALLEL: true run: | + coverage xml coverage report --show-missing coveralls --service=github - name: codecov From 1c5ea24c8209cb38d3ed16671f8cbe7e4933896b Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 10:20:20 -0600 Subject: [PATCH 16/24] Update ruff: one cleanup --- .pre-commit-config.yaml | 2 +- graphblas/core/operator.py | 5 ++--- pyproject.toml | 3 ++- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c259728f..62c4ab166 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,7 +68,7 @@ repos: files: ^(graphblas|docs)/ args: ["--ignore-words-list=coo,ba"] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.210 + rev: v0.0.211 hooks: - id: ruff args: ["--force-exclude"] diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 1b7c22e93..465151c9f 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -3008,9 +3008,8 @@ def get_typed_op(op, dtype, dtype2=None, *, is_left_scalar=False, is_right_scala if dtype2 is None: return op[dtype] # Handle special cases such as first and second (may have UDTs) - if op._custom_dtype is not None: - if (rv := op._custom_dtype(op, dtype, dtype2)) is not None: - return rv + if op._custom_dtype is not None and (rv := op._custom_dtype(op, dtype, dtype2)) is not None: + return rv # Generic case: try to unify the two dtypes try: return op[ diff --git a/pyproject.toml b/pyproject.toml index 22aa7a911..1c68fec2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,8 @@ select = [ "RUF", # ruff-specific rules ] ignore = [ - "D203", # 1 blank line required before class docstring + "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) ] exclude = [ "versioneer.py", From 568cd86278f2ca31080fc62098488d28c7d120c0 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 11:13:41 -0600 Subject: [PATCH 17/24] Move more config to pyproject.toml (see #356) --- .github/workflows/test_and_build.yml | 18 +++++++++--------- .pre-commit-config.yaml | 2 +- environment.yml | 1 + pyproject.toml | 23 +++++++++++++++++++++++ setup.cfg | 21 --------------------- 5 files changed, 34 insertions(+), 31 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index ce803ad43..faf40e3a7 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -93,7 +93,7 @@ jobs: pip install --no-deps -e . - name: Unit tests run: | - coverage run --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'slow_fast_fast' && '--runslow' || '' }} + coverage run -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'slow_fast_fast' && '--runslow' || '' }} - name: Unit tests (bizarro scalars) run: | # Run tests again with Scalars being C scalars by default @@ -102,30 +102,30 @@ jobs: -e '/# pragma: is_grbscalar/! s/is_cscalar = False/is_cscalar = True/g' \ -e '/# pragma: to_grb/ s/is_cscalar=True/is_cscalar=False/g' \ -e '/# pragma: to_grb/ s/is_cscalar = True/is_cscalar = False/g' - coverage run -a --branch -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'fast_slow_fast' && '--runslow' || ''}} + coverage run -a -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'fast_slow_fast' && '--runslow' || ''}} git checkout . # Undo changes to scalar default - name: Miscellaneous tests if: steps.rand.outputs.selected == 'fast_fast_fast' run: | # Test (and cover) automatic initialization - coverage run -a --branch graphblas/tests/test_auto_init.py - coverage run -a --branch graphblas/tests/test_external_init.py + coverage run -a graphblas/tests/test_auto_init.py + coverage run -a graphblas/tests/test_external_init.py # Test (and cover) lazy module loader echo "from graphblas.agg import count" > script.py - coverage run -a --branch script.py + coverage run -a script.py echo "from graphblas import agg" > script.py # Does this still cover? echo "from graphblas.core import agg" >> script.py - coverage run -a --branch script.py + coverage run -a script.py # Tests lazy loading of lib, ffi, and NULL in gb.core echo "from graphblas.core import base" > script.py - coverage run -a --branch script.py + coverage run -a script.py rm script.py - name: Auto-generated code check if: steps.rand.outputs.selected == 'fast_fast_fast' run: | # This step uses `black` - coverage run -a --branch -m graphblas.core.automethods - coverage run -a --branch -m graphblas.core.infixmethods + coverage run -a -m graphblas.core.automethods + coverage run -a -m graphblas.core.infixmethods git diff --exit-code - name: Coverage env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62c4ab166..072d0a4ec 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,8 +65,8 @@ repos: hooks: - id: codespell types_or: [python, rst, markdown] + additional_dependencies: [tomli] files: ^(graphblas|docs)/ - args: ["--ignore-words-list=coo,ba"] - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.0.211 hooks: diff --git a/environment.yml b/environment.yml index ff1ba1351..219dc2115 100644 --- a/environment.yml +++ b/environment.yml @@ -90,5 +90,6 @@ dependencies: # - scalene # - snakeviz # - sympy + # - vim # - yesqa # - zarr diff --git a/pyproject.toml b/pyproject.toml index 1c68fec2d..0a423e5ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,29 @@ markers = [ "slow: Skipped unless --runslow passed", ] +[tool.coverage.run] +branch = true +source = ["graphblas"] +omit = [ + "graphblas/_version.py", + "graphblas/viz.py", # TODO: test and get coverage for viz.py +] + +[tool.coverage.report] +ignore_errors = false +precision = 1 +fail_under = 0 +skip_covered = true +skip_empty = true +exclude_lines = [ + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", +] + +[tool.codespell] +ignore-words-list = "coo,ba" + [tool.ruff] # https://github.com/charliermarsh/ruff/ line-length = 100 diff --git a/setup.cfg b/setup.cfg index 8d5fb6b64..6af4743a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,27 +21,6 @@ versionfile_build = graphblas/_version.py tag_prefix= parentdir_prefix=python-graphblas- -[coverage:run] -source = graphblas -# TODO: test and get coverage for viz.py -omit = - graphblas/_version.py, - graphblas/viz.py - -[coverage:report] -# Regexes for lines to exclude from consideration -exclude_lines = - pragma: no cover - - raise AssertionError - raise NotImplementedError - -ignore_errors = True -precision = 1 -fail_under = 0 -skip_covered = True -skip_empty = True - [build_sphinx] all-files = 1 source-dir = docs From 5f659cc24dc9e688b9eebd1c39d99fb1e6988b46 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 11:51:08 -0600 Subject: [PATCH 18/24] flake8-simplify actually seems nice --- .pre-commit-config.yaml | 1 + environment.yml | 3 +++ graphblas/binary/__init__.py | 5 +---- graphblas/tests/test_scalar.py | 4 +++- setup.cfg | 7 ++++++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 072d0a4ec..e62d4329c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,6 +55,7 @@ repos: - flake8==6.0.0 - flake8-comprehensions==3.10.1 - flake8-bugbear==22.12.6 + - flake8-simplify==0.19.3 - repo: https://github.com/asottile/yesqa rev: v1.4.0 hooks: diff --git a/environment.yml b/environment.yml index 219dc2115..5b771641d 100644 --- a/environment.yml +++ b/environment.yml @@ -57,6 +57,9 @@ dependencies: # - flake8 # - flake8-bugbear # - flake8-comprehensions + # - flake8-print + # - flake8-quotes + # - flake8-simplify # - gcc # - gh # - graph-tool diff --git a/graphblas/binary/__init__.py b/graphblas/binary/__init__.py index 726d26764..05e80e868 100644 --- a/graphblas/binary/__init__.py +++ b/graphblas/binary/__init__.py @@ -21,10 +21,7 @@ def __getattr__(key): globals()[key] = rv if key in _delayed_commutes_to: other_key = _delayed_commutes_to[key] - if other_key in globals(): - other = globals()[other_key] - else: - other = other_key + other = globals().get(other_key, other_key) rv._commutes_to = other return rv raise AttributeError(f"module {__name__!r} has no attribute {key!r}") diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 0158ac2a3..92b392f80 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -306,7 +306,9 @@ def test_invert(): s = Scalar.from_value(1, dtype=bool) assert ~s == not_s assert (~s).value == not_s.value - assert not (s.value) == not_s.value + compare = s.value == not_s.value + assert not compare + assert s.value != not_s.value bad = Scalar(int) with pytest.raises(TypeError, match="The invert operator"): ~bad diff --git a/setup.cfg b/setup.cfg index 6af4743a2..9f8b59000 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,12 +6,17 @@ max-line-length = 100 inline-quotes = " exclude = versioneer.py, + graphblas/_version.py, graphblas/tests/test_formatting.py, ssgb, extend-ignore = E203, + SIM105, # E203 whitespace before ':' (to be compatible with black) -per-file-ignores = scripts/create_pickle.py:F403,F405 +per-file-ignores = + scripts/create_pickle.py:F403,F405, + graphblas/tests/*.py:T201, + graphblas/core/ss/matrix.py:SIM113, [versioneer] VCS = git From e7aa1a09bda58dcdb401a65e6a60e4998c5423a4 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 12:31:24 -0600 Subject: [PATCH 19/24] Simplify ScalarMatMulExpr --- graphblas/core/infix.py | 67 +++++------------------------------------ 1 file changed, 8 insertions(+), 59 deletions(-) diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 9b876d756..26e43035c 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -137,8 +137,16 @@ class ScalarEwiseMultExpr(ScalarInfixExpr): _to_expr = _ewise_mult_to_expr +class ScalarMatMulExpr(ScalarInfixExpr): + __slots__ = () + method_name = "inner" + _example_op = "plus_times" + _infix = "@" + + utils._output_types[ScalarEwiseAddExpr] = Scalar utils._output_types[ScalarEwiseMultExpr] = Scalar +utils._output_types[ScalarMatMulExpr] = Scalar class VectorInfixExpr(InfixExprBase): @@ -388,65 +396,6 @@ def __init__(self, left, right, *, nrows, ncols): utils._output_types[MatrixMatMulExpr] = Matrix -class ScalarMatMulExpr(ScalarInfixExpr): - __slots__ = () - method_name = "inner" - _example_op = "plus_times" - _infix = "@" - - # Begin auto-generated code: Scalar - __and__ = wrapdoc(Scalar.__and__)(property(automethods.__and__)) - __array__ = wrapdoc(Scalar.__array__)(property(automethods.__array__)) - __bool__ = wrapdoc(Scalar.__bool__)(property(automethods.__bool__)) - __complex__ = wrapdoc(Scalar.__complex__)(property(automethods.__complex__)) - __eq__ = wrapdoc(Scalar.__eq__)(property(automethods.__eq__)) - __float__ = wrapdoc(Scalar.__float__)(property(automethods.__float__)) - __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) - __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) - __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) - __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) - __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) - __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) - __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) - __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) - _as_matrix = wrapdoc(Scalar._as_matrix)(property(automethods._as_matrix)) - _as_vector = wrapdoc(Scalar._as_vector)(property(automethods._as_vector)) - _is_empty = wrapdoc(Scalar._is_empty)(property(automethods._is_empty)) - _name_html = wrapdoc(Scalar._name_html)(property(automethods._name_html)) - _nvals = wrapdoc(Scalar._nvals)(property(automethods._nvals)) - apply = wrapdoc(Scalar.apply)(property(automethods.apply)) - ewise_add = wrapdoc(Scalar.ewise_add)(property(automethods.ewise_add)) - ewise_mult = wrapdoc(Scalar.ewise_mult)(property(automethods.ewise_mult)) - ewise_union = wrapdoc(Scalar.ewise_union)(property(automethods.ewise_union)) - gb_obj = wrapdoc(Scalar.gb_obj)(property(automethods.gb_obj)) - get = wrapdoc(Scalar.get)(property(automethods.get)) - is_empty = wrapdoc(Scalar.is_empty)(property(automethods.is_empty)) - isclose = wrapdoc(Scalar.isclose)(property(automethods.isclose)) - isequal = wrapdoc(Scalar.isequal)(property(automethods.isequal)) - name = wrapdoc(Scalar.name)(property(automethods.name)).setter(automethods._set_name) - nvals = wrapdoc(Scalar.nvals)(property(automethods.nvals)) - select = wrapdoc(Scalar.select)(property(automethods.select)) - value = wrapdoc(Scalar.value)(property(automethods.value)) - wait = wrapdoc(Scalar.wait)(property(automethods.wait)) - # These raise exceptions - __matmul__ = Scalar.__matmul__ - __rmatmul__ = Scalar.__rmatmul__ - __iadd__ = automethods.__iadd__ - __iand__ = automethods.__iand__ - __ifloordiv__ = automethods.__ifloordiv__ - __imod__ = automethods.__imod__ - __imul__ = automethods.__imul__ - __ior__ = automethods.__ior__ - __ipow__ = automethods.__ipow__ - __isub__ = automethods.__isub__ - __itruediv__ = automethods.__itruediv__ - __ixor__ = automethods.__ixor__ - # End auto-generated code: Scalar - - -utils._output_types[ScalarMatMulExpr] = Scalar - - def _ewise_infix_expr(left, right, *, method, within): left_type = output_type(left) right_type = output_type(right) From 5983a42fb957199de16ffb629f1b314daca2df8d Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 13:54:15 -0600 Subject: [PATCH 20/24] Fix and catch using single back-tic instead of two in RST files --- .pre-commit-config.yaml | 9 +++++++-- docs/getting_started/faq.rst | 6 +++--- docs/getting_started/primer.rst | 2 +- docs/user_guide/init.rst | 2 +- docs/user_guide/operations.rst | 2 +- environment.yml | 1 + 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e62d4329c..4abb29ac2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -72,7 +72,12 @@ repos: rev: v0.0.211 hooks: - id: ruff - args: ["--force-exclude"] + 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"] - repo: local hooks: # Add `--hook-stage manual` to pre-commit command to run (very slow) @@ -82,7 +87,7 @@ repos: language: system types: [python] stages: [manual] - args: ["graphblas/"] + args: [graphblas/] pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 diff --git a/docs/getting_started/faq.rst b/docs/getting_started/faq.rst index ddd69be47..c2cc51646 100644 --- a/docs/getting_started/faq.rst +++ b/docs/getting_started/faq.rst @@ -29,7 +29,7 @@ For simple expressions, the two libraries are very similar. A += 1 For more complex expressions, however, the two libraries diverge significantly. -`pygraphblas` tends towards the `numpy` style of immediate execution and using +``pygraphblas`` tends towards the ``numpy`` style of immediate execution and using keyword arguments to affect the output. .. code-block:: python @@ -37,7 +37,7 @@ keyword arguments to affect the output. # pygraphblas A.mxm(B.transpose(), mask=A, out=C, accum=FP64.PLUS, semiring=FP64.MIN_PLUS) -`python-graphblas` uses delayed expressions and keeps the output-affecting arguments +``python-graphblas`` uses delayed expressions and keeps the output-affecting arguments together with the output. .. code-block:: python @@ -45,7 +45,7 @@ together with the output. # python-graphblas C(A, accum=binary.plus) << semiring.min_plus(A @ B.T) -`python-graphblas` also contains additional features, such as the `Recorder` and advanced aggregators. +``python-graphblas`` also contains additional features, such as the ``Recorder`` and advanced aggregators. What is the performance penalty of writing algorithms with python-graphblas vs writing them directly in C? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/docs/getting_started/primer.rst b/docs/getting_started/primer.rst index d95cde0cc..710dca702 100644 --- a/docs/getting_started/primer.rst +++ b/docs/getting_started/primer.rst @@ -55,7 +55,7 @@ as their start and end point. Common ways to store a graph ---------------------------- -The most common way to store a graph is as an edge list, containing the `(start_node, end_node, weight)`. +The most common way to store a graph is as an edge list, containing the ``(start_node, end_node, weight)``. The following graph and its edge list are shown below. diff --git a/docs/user_guide/init.rst b/docs/user_guide/init.rst index 7e1fdfa00..62f81b50f 100644 --- a/docs/user_guide/init.rst +++ b/docs/user_guide/init.rst @@ -42,7 +42,7 @@ moment. Default Initialization ---------------------- -If submodules are imported before calling `graphblas.init`, a default initialization occurs. +If submodules are imported before calling ``graphblas.init``, a default initialization occurs. The default backend is ``suitesparse`` and the default mode is ``nonblocking``. If ``graphblas.init`` is called after the default initialization with different parameters, diff --git a/docs/user_guide/operations.rst b/docs/user_guide/operations.rst index 1f1c19ed4..d06c79a83 100644 --- a/docs/user_guide/operations.rst +++ b/docs/user_guide/operations.rst @@ -134,7 +134,7 @@ Element-wise Intersection Two identically shaped collections can be intersected element-wise. Locations where only one of the two collections contains a value will be missing in the output. -The GraphBLAS spec calls this operation `eWiseMult` because it has the same behavior as sparse +The GraphBLAS spec calls this operation ``eWiseMult`` because it has the same behavior as sparse multiplication when missing values are treated as zero. As a result, ``binary.times`` is the default operator for element-wise intersection. diff --git a/environment.yml b/environment.yml index 5b771641d..f8e3610dd 100644 --- a/environment.yml +++ b/environment.yml @@ -92,6 +92,7 @@ dependencies: # - ruff # - scalene # - snakeviz + # - sphinx-lint # - sympy # - vim # - yesqa From 612c48a03338296c522ce14d959a304114ae300e Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 16:56:05 -0600 Subject: [PATCH 21/24] Support accum for scalars --- graphblas/core/base.py | 18 ++++++------------ graphblas/core/infixmethods.py | 12 ++++++++---- graphblas/core/operator.py | 6 ++---- graphblas/tests/test_scalar.py | 5 +++-- graphblas/tests/test_vector.py | 18 ++++++++++++------ 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/graphblas/core/base.py b/graphblas/core/base.py index d1b42a32c..b7e363e9c 100644 --- a/graphblas/core/base.py +++ b/graphblas/core/base.py @@ -322,10 +322,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, * if expr._is_scalar and self._is_scalar: # Extract element (s << v[1]) if accum is not None: - raise TypeError( - "Scalar accumulation with extract element" - "--such as `s(accum=accum) << v[0]`--is not supported" - ) + self(**opts) << self.ewise_add(expr, accum) + return expr.parent._extract_element( expr.resolved_indexes, self.dtype, @@ -347,10 +345,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, * # Simple assignment (w << v) if self._is_scalar: if accum is not None: - raise TypeError( - "Scalar update with accumulation--such as `s(accum=accum) << t`" - "--is not supported" - ) + self(**opts) << self.ewise_add(expr, accum) + return if opts: # Ignore opts for now descriptor_lookup(**opts) @@ -371,10 +367,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None, * # s << (v @ v) expr = expr._to_expr() elif accum is not None: - raise TypeError( - "Scalar update with accumulation--such as `s(accum=accum) << t`" - "--is not supported" - ) + self(**opts) << self.ewise_add(expr, accum) + return else: if opts: # Ignore opts for now diff --git a/graphblas/core/infixmethods.py b/graphblas/core/infixmethods.py index bcef56b99..9bc0e7348 100644 --- a/graphblas/core/infixmethods.py +++ b/graphblas/core/infixmethods.py @@ -85,7 +85,8 @@ def __ixor__(self, other): if ( other_type is Vector and self.ndim == 2 - or other_type not in {Vector, Matrix, TransposedMatrix} + or self.ndim != 0 + and other_type not in {Vector, Matrix, TransposedMatrix} ): self << __xor__(self, other) elif other.dtype != BOOL: @@ -103,7 +104,8 @@ def __ior__(self, other): if ( other_type is Vector and self.ndim == 2 - or other_type not in {Vector, Matrix, TransposedMatrix} + or self.ndim != 0 + and other_type not in {Vector, Matrix, TransposedMatrix} ): expr = call_op(self, other, binary.lor, outer=True) if expr.dtype != BOOL: @@ -171,7 +173,8 @@ def __iadd__(self, other): if ( other_type is Vector and self.ndim == 2 - or other_type not in {Vector, Matrix, TransposedMatrix} + or self.ndim != 0 + and other_type not in {Vector, Matrix, TransposedMatrix} ): self << __add__(self, other) else: @@ -385,7 +388,8 @@ def _main(): " if (\n" " other_type is Vector\n" " and self.ndim == 2\n" - " or other_type not in {Vector, Matrix, TransposedMatrix}\n" + " or self.ndim != 0\n" + " and other_type not in {Vector, Matrix, TransposedMatrix}\n" " ):\n" f" self << __{method}__(self, other)\n" " else:\n" diff --git a/graphblas/core/operator.py b/graphblas/core/operator.py index 465151c9f..77b5c7023 100644 --- a/graphblas/core/operator.py +++ b/graphblas/core/operator.py @@ -89,13 +89,11 @@ def _call_op(op, left, right=None, thunk=None, **kwargs): from .matrix import Matrix, TransposedMatrix from .vector import Vector - left_type = output_type(left) - if left_type in {Vector, Matrix, TransposedMatrix}: + if (left_type := output_type(left)) in {Vector, Matrix, TransposedMatrix}: if thunk is not None: return left.select(op, thunk=thunk, **kwargs) return left.apply(op, right=right, **kwargs) - right_type = output_type(right) - if right_type in {Vector, Matrix, TransposedMatrix}: + if (right_type := output_type(right)) in {Vector, Matrix, TransposedMatrix}: return right.apply(op, left=left, **kwargs) from .scalar import Scalar, _as_scalar diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 92b392f80..0ad544443 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -231,8 +231,9 @@ def test_update(s): assert s == 4 s() << 5 assert s == 5 - with pytest.raises(TypeError, match="is not supported"): - s(accum=binary.plus) << 6 + # with pytest.raises(TypeError, match="is not supported"): + s(accum=binary.plus) << 6 # Now okay + assert s == 11 with pytest.raises(TypeError, match="Mask not allowed for Scalars"): s(s) with pytest.raises(TypeError, match="input_mask not allowed for Scalars"): diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index 52f4f185a..f5d5182c5 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -453,10 +453,12 @@ def test_extract_fancy_scalars(v): assert s.dtype == dtypes.FP64 t = Scalar(float) - with pytest.raises(TypeError, match="is not supported"): - t(accum=binary.plus) << s - with pytest.raises(TypeError, match="is not supported"): - t(accum=binary.plus) << 1 + # with pytest.raises(TypeError, match="is not supported"): + t(accum=binary.plus) << s # Now okay + assert t == 1.0 + # with pytest.raises(TypeError, match="is not supported"): + t(accum=binary.plus) << 1 # Now okay + assert t == 2.0 with pytest.raises(TypeError, match="Mask not allowed for Scalars"): t(mask=t) << s @@ -468,8 +470,12 @@ def test_extract_fancy_scalars(v): t = Scalar(float) t() << v[1] assert t.value == 1.0 - with pytest.raises(TypeError, match="Scalar accumulation with extract element"): - t(accum=binary.plus) << v[0] + # with pytest.raises(TypeError, match="Scalar accumulation with extract element"): + with pytest.raises(TypeError, match="autocompute"): + t(accum=binary.plus) << v[1] # Now okay, but autocomputes + with gb.config.set(autocompute=True): + t(accum=binary.plus) << v[1] # Now okay, but autocomputes + assert t == 2.0 def test_extract_negative_indices(v): From 318b540d2ae7c9f76a2d63660bf80ce808a3cd8a Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 18:28:56 -0600 Subject: [PATCH 22/24] `__neg__` and `__invert__` return expressions for scalars too --- graphblas/core/automethods.py | 10 ---------- graphblas/core/infix.py | 2 -- graphblas/core/infixmethods.py | 16 ++++++++-------- graphblas/core/scalar.py | 28 +--------------------------- graphblas/tests/test_matrix.py | 1 + graphblas/tests/test_scalar.py | 14 ++++++++++---- graphblas/tests/test_vector.py | 1 + 7 files changed, 21 insertions(+), 51 deletions(-) diff --git a/graphblas/core/automethods.py b/graphblas/core/automethods.py index dbcd9b237..78a159806 100644 --- a/graphblas/core/automethods.py +++ b/graphblas/core/automethods.py @@ -93,10 +93,6 @@ def __int__(self): return self._get_value("__int__") -def __invert__(self): - return self._get_value("__invert__") - - def __iter__(self): return self._get_value("__iter__") @@ -109,10 +105,6 @@ def __ne__(self): return self._get_value("__ne__") -def __neg__(self): - return self._get_value("__neg__") - - def __or__(self): return self._get_value("__or__") @@ -373,9 +365,7 @@ def _main(): "__float__", "__index__", "__int__", - "__invert__", "__ne__", - "__neg__", "_as_matrix", "_as_vector", "_is_empty", diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 26e43035c..39d60d582 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -78,9 +78,7 @@ def is_grbscalar(self): __float__ = wrapdoc(Scalar.__float__)(property(automethods.__float__)) __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) - __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) - __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) diff --git a/graphblas/core/infixmethods.py b/graphblas/core/infixmethods.py index 9bc0e7348..d3b0dd5e4 100644 --- a/graphblas/core/infixmethods.py +++ b/graphblas/core/infixmethods.py @@ -85,7 +85,7 @@ def __ixor__(self, other): if ( other_type is Vector and self.ndim == 2 - or self.ndim != 0 + or not self._is_scalar and other_type not in {Vector, Matrix, TransposedMatrix} ): self << __xor__(self, other) @@ -104,7 +104,7 @@ def __ior__(self, other): if ( other_type is Vector and self.ndim == 2 - or self.ndim != 0 + or not self._is_scalar and other_type not in {Vector, Matrix, TransposedMatrix} ): expr = call_op(self, other, binary.lor, outer=True) @@ -173,7 +173,7 @@ def __iadd__(self, other): if ( other_type is Vector and self.ndim == 2 - or self.ndim != 0 + or not self._is_scalar and other_type not in {Vector, Matrix, TransposedMatrix} ): self << __add__(self, other) @@ -301,9 +301,9 @@ def __itruediv__(self, other): "__xor__", ]: val = d[name] - if name not in {"__bool__", "__eq__", "__invert__", "__ne__", "__neg__"}: + if name not in {"__eq__", "__ne__"}: setattr(Scalar, name, val) - if not name.startswith("__i"): + if not name.startswith("__i") or name == "__invert__": setattr(ScalarExpression, name, val) setattr(ScalarInfixExpr, name, val) setattr(ScalarIndexExpr, name, val) @@ -388,7 +388,7 @@ def _main(): " if (\n" " other_type is Vector\n" " and self.ndim == 2\n" - " or self.ndim != 0\n" + " or not self._is_scalar\n" " and other_type not in {Vector, Matrix, TransposedMatrix}\n" " ):\n" f" self << __{method}__(self, other)\n" @@ -409,9 +409,9 @@ def _main(): "d = globals()\n" f"for name in {methods}:\n" " val = d[name]\n" - " if name not in {'__bool__', '__eq__', '__invert__', '__ne__', '__neg__'}:\n" + " if name not in {'__eq__', '__ne__'}:\n" " setattr(Scalar, name, val)\n" - " if not name.startswith('__i'):\n" + " if not name.startswith('__i') or name == '__invert__':\n" " setattr(ScalarExpression, name, val)\n" " setattr(ScalarInfixExpr, name, val)\n" " setattr(ScalarIndexExpr, name, val)\n" diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index 690e4c557..5635e11ed 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -5,7 +5,7 @@ from .. import backend, binary, config, monoid from ..binary import isclose -from ..dtypes import _INDEX, BOOL, FP64, lookup_dtype, unify +from ..dtypes import _INDEX, FP64, lookup_dtype, unify from ..exceptions import EmptyObject, check_status from . import automethods, ffi, lib, utils from .base import BaseExpression, BaseType, call @@ -155,28 +155,6 @@ def __int__(self): def __complex__(self): return complex(self.value) - def __neg__(self): - dtype = self.dtype - if dtype.name[0] == "U" or dtype == BOOL or dtype._is_udt: - raise TypeError(f"The negative operator, `-`, is not supported for {dtype.name} dtype") - rv = Scalar(dtype, is_cscalar=self._is_cscalar, name=f"-{self.name or 's_temp'}") - if self._is_empty: - return rv - rv.value = -self.value - return rv - - def __invert__(self): - if self.dtype != BOOL: - raise TypeError( - f"The invert operator, `~`, is not supported for {self.dtype.name} dtype. " - "It is only supported for BOOL dtype." - ) - rv = Scalar(BOOL, is_cscalar=self._is_cscalar, name=f"~{self.name or 's_temp'}") - if self._is_empty: - return rv - rv.value = not self.value - return rv - __index__ = __int__ def __array__(self, dtype=None): @@ -824,9 +802,7 @@ def _repr_html_(self): __float__ = wrapdoc(Scalar.__float__)(property(automethods.__float__)) __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) - __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) - __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) @@ -900,9 +876,7 @@ def dup(self, dtype=None, *, clear=False, is_cscalar=False, name=None, **opts): __float__ = wrapdoc(Scalar.__float__)(property(automethods.__float__)) __index__ = wrapdoc(Scalar.__index__)(property(automethods.__index__)) __int__ = wrapdoc(Scalar.__int__)(property(automethods.__int__)) - __invert__ = wrapdoc(Scalar.__invert__)(property(automethods.__invert__)) __ne__ = wrapdoc(Scalar.__ne__)(property(automethods.__ne__)) - __neg__ = wrapdoc(Scalar.__neg__)(property(automethods.__neg__)) __or__ = wrapdoc(Scalar.__or__)(property(automethods.__or__)) __rand__ = wrapdoc(Scalar.__rand__)(property(automethods.__rand__)) __ror__ = wrapdoc(Scalar.__ror__)(property(automethods.__ror__)) diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 39a80b6fd..bfcf5da99 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2616,6 +2616,7 @@ def test_not_to_array(A): (-10, [], []), ], ) +@autocompute def test_diag(A, params): k, indices, values = params expected = Vector.from_coo(indices, values, dtype=A.dtype, size=max(0, A.nrows - abs(k))) diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 0ad544443..5a003d983 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -278,18 +278,23 @@ def test_scalar_to_numpy(s): assert a.shape == b.shape +@autocompute def test_neg(): for dtype in sorted( - (dtype for dtype in vars(dtypes).values() if isinstance(dtype, dtypes.DataType)), + ( + dtype + for attr, dtype in vars(dtypes).items() + if isinstance(dtype, dtypes.DataType) and attr not in {"_INDEX"} + ), key=lambda x: x.name, reverse=random.choice([False, True]), # used to segfault when False ): s = Scalar.from_value(1, dtype=dtype) empty = Scalar(dtype) - if dtype.name == "BOOL" or dtype.name.startswith("U") or dtype._is_udt: - with pytest.raises(TypeError, match="The negative operator, `-`, is not supported"): + if dtype._is_udt: + with pytest.raises(KeyError, match="ainv does not work with"): -s - with pytest.raises(TypeError, match="The negative operator, `-`, is not supported"): + with pytest.raises(KeyError, match="ainv does not work with"): -empty else: minus_s = Scalar.from_value(-1, dtype=dtype) @@ -299,6 +304,7 @@ def test_neg(): assert compute((-empty).value) is None +@autocompute def test_invert(): empty = Scalar(bool) assert empty == ~empty diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index f5d5182c5..3e561c168 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -1402,6 +1402,7 @@ def test_vector_index_with_scalar(): v[s] +@autocompute def test_diag(v): indices, values = v.to_coo() for k in range(-5, 5): From 5ab686c806530998c9fb79d54cd9575c9e3e6ba9 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Thu, 5 Jan 2023 19:31:14 -0600 Subject: [PATCH 23/24] fix --- graphblas/ss/_core.py | 5 ++--- graphblas/tests/test_scalar.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/graphblas/ss/_core.py b/graphblas/ss/_core.py index 640d7886b..a68222c87 100644 --- a/graphblas/ss/_core.py +++ b/graphblas/ss/_core.py @@ -3,7 +3,7 @@ from ..core import ffi, lib from ..core.base import _expect_type from ..core.matrix import Matrix, TransposedMatrix -from ..core.scalar import Scalar +from ..core.scalar import _as_scalar from ..core.ss.config import BaseConfig from ..core.ss.matrix import _concat_mn from ..core.vector import Vector @@ -47,8 +47,7 @@ def diag(x, k=0, dtype=None, *, name=None, **opts): x = _expect_type( _graphblas_ss, x, (Matrix, TransposedMatrix, Vector), within="diag", argname="x" ) - if type(k) is not Scalar: - k = Scalar.from_value(k, INT64, is_cscalar=True, name="") + k = _as_scalar(k, INT64, is_cscalar=True) if dtype is None: dtype = x.dtype typ = type(x) diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 5a003d983..5f856b8d4 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -297,7 +297,7 @@ def test_neg(): with pytest.raises(KeyError, match="ainv does not work with"): -empty else: - minus_s = Scalar.from_value(-1, dtype=dtype) + minus_s = Scalar.from_value(-1, dtype=dtype, is_cscalar=False) # pragma: is_grbscalar assert s == -minus_s assert (-s).value == minus_s.value assert empty == -empty From 789059a1353cc69b1facbf0eeba81edb5db4a33b Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Fri, 6 Jan 2023 15:44:00 -0600 Subject: [PATCH 24/24] Add docstrings to new Scalar methods; add pylint task to CI --- .github/workflows/test_and_build.yml | 34 ++++--- .pre-commit-config.yaml | 2 +- graphblas/core/automethods.py | 4 + graphblas/core/base.py | 1 + graphblas/core/infix.py | 4 + graphblas/core/matrix.py | 28 +++--- graphblas/core/scalar.py | 131 ++++++++++++++++++++++++++- graphblas/core/utils.py | 2 +- graphblas/core/vector.py | 26 +++--- pyproject.toml | 10 +- 10 files changed, 201 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test_and_build.yml b/.github/workflows/test_and_build.yml index faf40e3a7..f7466a7d4 100644 --- a/.github/workflows/test_and_build.yml +++ b/.github/workflows/test_and_build.yml @@ -56,16 +56,20 @@ jobs: channel-priority: strict activate-environment: graphblas auto-activate-base: false - # Randomly test up to one slow task per job (about 5% chance one group isn't run) + # Randomly test one slow task per job (about 5% chance one group isn't run). + # + # TODO: have build matrix be [slowtask]x[os] and randomly choose [pyver] and [sourcetype]. + # This should ensure we'll have full code coverage (i.e., no chance of getting unlucky). + # If important in the future, we could also randomize dependency versions. - name: RNG for tests uses: ddradar/choose-random-action@v2.0.2 - id: rand + id: slowtask with: contents: | - slow_fast_fast - fast_slow_fast - fast_fast_slow - fast_fast_fast + pytest_normal + pytest_bizarro + pylint + notebooks weights: | 1 1 @@ -73,7 +77,8 @@ jobs: 1 - name: Update env run: | - mamba install pytest coverage coveralls=3.3.1 pytest-randomly ${{ steps.rand.outputs.selected == 'fast_fast_fast' && 'black' || '' }} \ + mamba install pytest coverage coveralls=3.3.1 pytest-randomly \ + ${{ steps.slowtask.outputs.selected == 'pylint' && 'black pylint' || '' }} \ pandas numba scipy networkx cffi donfig pyyaml awkward - name: Build extension module run: | @@ -93,7 +98,8 @@ jobs: pip install --no-deps -e . - name: Unit tests run: | - coverage run -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'slow_fast_fast' && '--runslow' || '' }} + coverage run -m pytest ${{ matrix.cfg.testopts }} -v \ + ${{ steps.slowtask.outputs.selected == 'pytest_normal' && '--runslow' || '' }} - name: Unit tests (bizarro scalars) run: | # Run tests again with Scalars being C scalars by default @@ -102,10 +108,11 @@ jobs: -e '/# pragma: is_grbscalar/! s/is_cscalar = False/is_cscalar = True/g' \ -e '/# pragma: to_grb/ s/is_cscalar=True/is_cscalar=False/g' \ -e '/# pragma: to_grb/ s/is_cscalar = True/is_cscalar = False/g' - coverage run -a -m pytest ${{ matrix.cfg.testopts }} -v ${{ steps.rand.outputs.selected == 'fast_slow_fast' && '--runslow' || ''}} + coverage run -a -m pytest ${{ matrix.cfg.testopts }} -v \ + ${{ steps.slowtask.outputs.selected == 'pytest_bizarro' && '--runslow' || ''}} git checkout . # Undo changes to scalar default - name: Miscellaneous tests - if: steps.rand.outputs.selected == 'fast_fast_fast' + if: steps.slowtask.outputs.selected == 'pylint' run: | # Test (and cover) automatic initialization coverage run -a graphblas/tests/test_auto_init.py @@ -121,12 +128,15 @@ jobs: coverage run -a script.py rm script.py - name: Auto-generated code check - if: steps.rand.outputs.selected == 'fast_fast_fast' + if: steps.slowtask.outputs.selected == 'pylint' run: | # This step uses `black` coverage run -a -m graphblas.core.automethods coverage run -a -m graphblas.core.infixmethods git diff --exit-code + - name: Pylint (informational only; never fails) + if: steps.slowtask.outputs.selected == 'pylint' + run: pylint --exit-zero graphblas/ - name: Coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -139,7 +149,7 @@ jobs: - name: codecov uses: codecov/codecov-action@v3 - name: Notebooks Execution check - if: steps.rand.outputs.selected == 'fast_fast_slow' + if: steps.slowtask.outputs.selected == 'notebooks' run: | mamba install matplotlib nbconvert jupyter 'ipython>=7' jupyter nbconvert --to notebook --execute notebooks/*ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4abb29ac2..80d0218c8 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.211 + rev: v0.0.212 hooks: - id: ruff args: [--force-exclude] diff --git a/graphblas/core/automethods.py b/graphblas/core/automethods.py index 78a159806..f4aea3bdf 100644 --- a/graphblas/core/automethods.py +++ b/graphblas/core/automethods.py @@ -500,6 +500,8 @@ def get_name(line): lines.append( f" {indent}{name} = wrapdoc(Vector.{name})(property(automethods.{name}))" ) + if name == "ss": + lines.append(' else:\n ss = Vector.__dict__["ss"] # raise if used') lines.append(" # These raise exceptions") for name in sorted(common_raises | vector_matrix_raises): lines.append(f" {name} = Vector.{name}") @@ -529,6 +531,8 @@ def get_name(line): lines.append( f" {indent}{name} = wrapdoc(Matrix.{name})(property(automethods.{name}))" ) + if name == "ss": + lines.append(' else:\n ss = Matrix.__dict__["ss"] # raise if used') lines.append(" # These raise exceptions") for name in sorted(common_raises | vector_matrix_raises): lines.append(f" {name} = Matrix.{name}") diff --git a/graphblas/core/base.py b/graphblas/core/base.py index b7e363e9c..a0be99f2d 100644 --- a/graphblas/core/base.py +++ b/graphblas/core/base.py @@ -184,6 +184,7 @@ def _check_mask(mask, output=None): class BaseType: + # pylint: disable=assigning-non-slot __slots__ = "gb_obj", "dtype", "name", "__weakref__" # Flag for operations which depend on scalar vs vector/matrix _is_scalar = False diff --git a/graphblas/core/infix.py b/graphblas/core/infix.py index 39d60d582..464335d42 100644 --- a/graphblas/core/infix.py +++ b/graphblas/core/infix.py @@ -205,6 +205,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Vector.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Vector.ss)(property(automethods.ss)) + else: + ss = Vector.__dict__["ss"] # raise if used to_coo = wrapdoc(Vector.to_coo)(property(automethods.to_coo)) to_dict = wrapdoc(Vector.to_dict)(property(automethods.to_dict)) to_values = wrapdoc(Vector.to_values)(property(automethods.to_values)) @@ -334,6 +336,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Matrix.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Matrix.ss)(property(automethods.ss)) + else: + ss = Matrix.__dict__["ss"] # raise if used to_coo = wrapdoc(Matrix.to_coo)(property(automethods.to_coo)) to_csc = wrapdoc(Matrix.to_csc)(property(automethods.to_csc)) to_csr = wrapdoc(Matrix.to_csr)(property(automethods.to_csr)) diff --git a/graphblas/core/matrix.py b/graphblas/core/matrix.py index 034ffc53c..f8b4f5291 100644 --- a/graphblas/core/matrix.py +++ b/graphblas/core/matrix.py @@ -1562,7 +1562,7 @@ def ewise_add(self, other, op=monoid.plus, *, require_monoid=None): Returns ------- - Matrix with a structure formed as the union of the input structures + MatrixExpression with a structure formed as the union of the input structures Examples -------- @@ -1649,7 +1649,7 @@ def ewise_mult(self, other, op=binary.times): Returns ------- - Matrix with a structure formed as the intersection of the input structures + MatrixExpression with a structure formed as the intersection of the input structures Examples -------- @@ -1718,7 +1718,7 @@ def ewise_union(self, other, op, left_default, right_default): Returns ------- - Matrix with a structure formed as the union of the input structures + MatrixExpression with a structure formed as the union of the input structures Examples -------- @@ -1832,7 +1832,7 @@ def mxv(self, other, op=semiring.plus_times): Returns ------- - :class:`~graphblas.Vector` + VectorExpression Examples -------- @@ -1876,7 +1876,7 @@ def mxm(self, other, op=semiring.plus_times): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -1924,7 +1924,7 @@ def kronecker(self, other, op=binary.times): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -1977,7 +1977,7 @@ def apply(self, op, right=None, *, left=None): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -2126,7 +2126,7 @@ def select(self, op, thunk=None): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -2224,7 +2224,7 @@ def reduce_rowwise(self, op=monoid.plus): Returns ------- - Vector + VectorExpression Examples -------- @@ -2262,7 +2262,7 @@ def reduce_columnwise(self, op=monoid.plus): Returns ------- - :class:`~graphblas.Vector` + VectorExpression Examples -------- @@ -2304,7 +2304,7 @@ def reduce_scalar(self, op=monoid.plus, *, allow_empty=True): Returns ------- - :class:`~graphblas.Scalar` + ScalarExpression Examples -------- @@ -2365,7 +2365,7 @@ def reposition(self, row_offset, column_offset, *, nrows=None, ncols=None): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -3110,6 +3110,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Matrix.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Matrix.ss)(property(automethods.ss)) + else: + ss = Matrix.__dict__["ss"] # raise if used to_coo = wrapdoc(Matrix.to_coo)(property(automethods.to_coo)) to_csc = wrapdoc(Matrix.to_csc)(property(automethods.to_csc)) to_csr = wrapdoc(Matrix.to_csr)(property(automethods.to_csr)) @@ -3206,6 +3208,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Matrix.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Matrix.ss)(property(automethods.ss)) + else: + ss = Matrix.__dict__["ss"] # raise if used to_coo = wrapdoc(Matrix.to_coo)(property(automethods.to_coo)) to_csc = wrapdoc(Matrix.to_csc)(property(automethods.to_csc)) to_csr = wrapdoc(Matrix.to_csr)(property(automethods.to_csr)) diff --git a/graphblas/core/scalar.py b/graphblas/core/scalar.py index 5635e11ed..1e6420f16 100644 --- a/graphblas/core/scalar.py +++ b/graphblas/core/scalar.py @@ -184,7 +184,7 @@ def isequal(self, other, *, check_dtype=False): Returns ------- - Bool + bool See Also -------- @@ -590,6 +590,35 @@ def _as_matrix(self, *, name=None): ######################################################### def ewise_add(self, other, op=monoid.plus): + """Perform element-wise computation on the union of sparse values, similar to how + one expects addition to work for sparse data. + + See the `Element-wise Union <../user_guide/operations.html#element-wise-union>`__ + section in the User Guide for more details, especially about the difference between + ewise_add and :meth:`ewise_union`. + + Parameters + ---------- + other : Scalar + The other scalar in the computation; Python scalars also accepted + op : :class:`~graphblas.core.operator.Monoid` or :class:`~graphblas.core.operator.BinaryOp` + Operator to use on intersecting values + + Returns + ------- + ScalarExpression that will be non-empty if any of the inputs is non-empty + + Examples + -------- + + .. code-block:: python + + # Method syntax + c << a.ewise_add(b, op=monoid.max) + + # Functional syntax + c << monoid.max(a | b) + """ method_name = "ewise_add" if type(other) is not Scalar: dtype = self.dtype if self.dtype._is_udt else None @@ -616,6 +645,35 @@ def ewise_add(self, other, op=monoid.plus): ) def ewise_mult(self, other, op=binary.times): + """Perform element-wise computation on the intersection of sparse values, + similar to how one expects multiplication to work for sparse data. + + See the + `Element-wise Intersection <../user_guide/operations.html#element-wise-intersection>`__ + section in the User Guide for more details. + + Parameters + ---------- + other : Scalar + The other scalar in the computation; Python scalars also accepted + op : :class:`~graphblas.core.operator.Monoid` or :class:`~graphblas.core.operator.BinaryOp` + Operator to use on intersecting values + + Returns + ------- + ScalarExpression that will be empty if any of the inputs is empty + + Examples + -------- + + .. code-block:: python + + # Method syntax + c << a.ewise_mult(b, op=binary.gt) + + # Functional syntax + c << binary.gt(a & b) + """ method_name = "ewise_mult" if type(other) is not Scalar: dtype = self.dtype if self.dtype._is_udt else None @@ -642,6 +700,39 @@ def ewise_mult(self, other, op=binary.times): ) def ewise_union(self, other, op, left_default, right_default): + """Perform element-wise computation on the union of sparse values, + similar to how one expects subtraction to work for sparse data. + + See the `Element-wise Union <../user_guide/operations.html#element-wise-union>`__ + section in the User Guide for more details, especially about the difference between + ewise_union and :meth:`ewise_add`. + + Parameters + ---------- + other : Scalar + The other scalar in the computation; Python scalars also accepted + op : :class:`~graphblas.core.operator.Monoid` or :class:`~graphblas.core.operator.BinaryOp` + Operator to use + left_default : + Scalar value to use when the index on the left is missing + right_default : + Scalar value to use when the index on the right is missing + + Returns + ------- + ScalarExpression with a structure formed as the union of the input structures + + Examples + -------- + + .. code-block:: python + + # Method syntax + c << a.ewise_union(b, op=binary.div, left_default=1, right_default=1) + + # Functional syntax + c << binary.div(a | b, left_default=1, right_default=1) + """ method_name = "ewise_union" dtype = self.dtype if self.dtype._is_udt else None if type(other) is not Scalar: @@ -719,6 +810,44 @@ def ewise_union(self, other, op, left_default, right_default): return expr def apply(self, op, right=None, *, left=None): + """Create a new Scalar by applying ``op`` + + See the `Apply <../user_guide/operations.html#apply>`__ + section in the User Guide for more details. + + Common usage is to pass a :class:`~graphblas.core.operator.UnaryOp`, + in which case ``right`` and ``left`` may not be defined. + + A :class:`~graphblas.core.operator.BinaryOp` can also be used, in + which case a scalar must be passed as ``left`` or ``right``. + + An :class:`~graphblas.core.operator.IndexUnaryOp` can also be used + with the thunk passed in as ``right``. + + Parameters + ---------- + op : UnaryOp or BinaryOp or IndexUnaryOp + Operator to apply + right : + Scalar used with BinaryOp or IndexUnaryOp + left : + Scalar used with BinaryOp + + Returns + ------- + ScalarExpression + + Examples + -------- + + .. code-block:: python + + # Method syntax + b << a.apply(op.abs) + + # Functional syntax + b << op.abs(a) + """ expr = self._as_vector().apply(op, right, left=left) return ScalarExpression( expr.method_name, diff --git a/graphblas/core/utils.py b/graphblas/core/utils.py index 3cb739140..f03fe9ccd 100644 --- a/graphblas/core/utils.py +++ b/graphblas/core/utils.py @@ -254,7 +254,7 @@ def __get__(self, obj, type=None): return self.member_property.__get__(obj, type) @property - def __set__(self): + def __set__(self): # pylint: disable=unexpected-special-method-signature return self.member_property.__set__ diff --git a/graphblas/core/vector.py b/graphblas/core/vector.py index d8171c67d..59c910900 100644 --- a/graphblas/core/vector.py +++ b/graphblas/core/vector.py @@ -773,7 +773,7 @@ def ewise_add(self, other, op=monoid.plus, *, require_monoid=None): Returns ------- - Vector with a structure formed as the union of the input structures + VectorExpression with a structure formed as the union of the input structures Examples -------- @@ -857,7 +857,7 @@ def ewise_mult(self, other, op=binary.times): Returns ------- - Vector with a structure formed as the intersection of the input structures + VectorExpression with a structure formed as the intersection of the input structures Examples -------- @@ -907,7 +907,7 @@ def ewise_mult(self, other, op=binary.times): def ewise_union(self, other, op, left_default, right_default): """Perform element-wise computation on the union of sparse values, - similar to how one expects subtraction to work for sparse matrices. + similar to how one expects subtraction to work for sparse vectors. See the `Element-wise Union <../user_guide/operations.html#element-wise-union>`__ section in the User Guide for more details, especially about the difference between @@ -926,7 +926,7 @@ def ewise_union(self, other, op, left_default, right_default): Returns ------- - Vector with a structure formed as the union of the input structures + VectorExpression with a structure formed as the union of the input structures Examples -------- @@ -1041,7 +1041,7 @@ def vxm(self, other, op=semiring.plus_times): Returns ------- - Vector + VectorExpression Examples -------- @@ -1100,7 +1100,7 @@ def apply(self, op, right=None, *, left=None): Returns ------- - Vector + VectorExpression Examples -------- @@ -1246,7 +1246,7 @@ def select(self, op, thunk=None): Returns ------- - Vector + VectorExpression Examples -------- @@ -1344,7 +1344,7 @@ def reduce(self, op=monoid.plus, *, allow_empty=True): Returns ------- - Scalar + ScalarExpression Examples -------- @@ -1389,7 +1389,7 @@ def inner(self, other, op=semiring.plus_times): Returns ------- - Scalar + ScalarExpression Examples -------- @@ -1434,7 +1434,7 @@ def outer(self, other, op=binary.times): Returns ------- - Matrix + MatrixExpression Examples -------- @@ -1485,7 +1485,7 @@ def reposition(self, offset, *, size=None): Returns ------- - Vector + VectorExpression Examples -------- @@ -1900,6 +1900,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Vector.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Vector.ss)(property(automethods.ss)) + else: + ss = Vector.__dict__["ss"] # raise if used to_coo = wrapdoc(Vector.to_coo)(property(automethods.to_coo)) to_dict = wrapdoc(Vector.to_dict)(property(automethods.to_dict)) to_values = wrapdoc(Vector.to_values)(property(automethods.to_values)) @@ -1982,6 +1984,8 @@ def dup(self, dtype=None, *, clear=False, mask=None, name=None, **opts): select = wrapdoc(Vector.select)(property(automethods.select)) if backend == "suitesparse": ss = wrapdoc(Vector.ss)(property(automethods.ss)) + else: + ss = Vector.__dict__["ss"] # raise if used to_coo = wrapdoc(Vector.to_coo)(property(automethods.to_coo)) to_dict = wrapdoc(Vector.to_dict)(property(automethods.to_dict)) to_values = wrapdoc(Vector.to_values)(property(automethods.to_values)) diff --git a/pyproject.toml b/pyproject.toml index 0a423e5ad..b1701c645 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ ignore-words-list = "coo,ba" line-length = 100 target-version = "py38" select = [ + # Have we enabled too many checks that they'll become a nuisance? We'll see... "F", # pyflakes "E", # pycodestyle Error "W", # pycodestyle Warning @@ -103,14 +104,14 @@ exclude = [ "scripts/create_pickle.py" = ["F403", "F405"] [tool.pylint.messages_control] +# To run a single check, do: pylint graphblas --disable E,W,R,C,I --enable assignment-from-no-return max-line-length = 100 py-version = "3.8" ignore-paths = ["graphblas/_version.py", "versioneer.py"] +enable = ["I"] disable = [ # Error - "assigning-non-slot", # almost passes "assignment-from-no-return", - "unexpected-special-method-signature", # almost passes # Warning "arguments-differ", @@ -119,7 +120,6 @@ disable = [ "fixme", "global-statement", "non-parent-init-called", - "protected-access", "redefined-builtin", "redefined-outer-name", "super-init-not-called", @@ -153,6 +153,7 @@ disable = [ # warning "broad-except", "pointless-statement", + "protected-access", "undefined-loop-variable", "unused-import", # refactor @@ -176,4 +177,7 @@ disable = [ "unnecessary-dunder-call", "wrong-import-order", "wrong-import-position", + # informative + "locally-disabled", + "suppressed-message", ]