From 5e625aca4866d646e498a16261c674b04a2f7930 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Sun, 12 Jun 2022 10:28:39 -0500 Subject: [PATCH] [EXPERIMENTAL] Add "onchange" hook --- graphblas/_ss/matrix.py | 36 ++++++++++++++++++++- graphblas/_ss/vector.py | 16 +++++++++ graphblas/base.py | 11 ++++++- graphblas/expr.py | 4 +++ graphblas/matrix.py | 10 +++++- graphblas/scalar.py | 14 ++++++-- graphblas/tests/test_matrix.py | 57 ++++++++++++++++++++++++++++++++ graphblas/tests/test_scalar.py | 27 ++++++++++++++++ graphblas/tests/test_vector.py | 59 ++++++++++++++++++++++++++++++++++ graphblas/vector.py | 10 +++++- 10 files changed, 237 insertions(+), 7 deletions(-) diff --git a/graphblas/_ss/matrix.py b/graphblas/_ss/matrix.py index a12ea16a1..5d448eb2a 100644 --- a/graphblas/_ss/matrix.py +++ b/graphblas/_ss/matrix.py @@ -442,6 +442,8 @@ def build_diag(self, vector, k=0): vector = self._parent._expect_type( vector, gb.Vector, within="ss.build_diag", argname="vector" ) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call("GxB_Matrix_diag", [self._parent, vector, _as_scalar(k, INT64, is_cscalar=True), None]) def split(self, chunks, *, name=None): @@ -544,6 +546,8 @@ def concat(self, tiles): graphblas.ss.concat """ tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=True) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) self._concat(tiles, m, n) def build_scalar(self, rows, columns, value): @@ -564,6 +568,8 @@ def build_scalar(self, rows, columns, value): f"`rows` and `columns` lengths must match: {rows.size}, {columns.size}" ) scalar = _as_scalar(value, self._parent.dtype, is_cscalar=False) # pragma: is_grbscalar + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call( "GxB_Matrix_build_Scalar", [ @@ -897,8 +903,12 @@ def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, m format = f"{self.format[:-1]}r" elif format == "columnwise": format = f"{self.format[:-1]}c" - if give_ownership or format == "coo": + if format == "coo": parent = self._parent + elif give_ownership: + parent = self._parent + if parent._hooks is not None and "onchange" in parent._hooks: + parent._hooks["onchange"](self) else: parent = self._parent.dup(name=f"M_{method}") dtype = parent.dtype.np_type @@ -1388,6 +1398,8 @@ def pack_csr( See `Matrix.ss.import_csr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_csr( indptr=indptr, values=values, @@ -1561,6 +1573,8 @@ def pack_csc( See `Matrix.ss.import_csc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_csc( indptr=indptr, values=values, @@ -1744,6 +1758,8 @@ def pack_hypercsr( See `Matrix.ss.import_hypercsr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_hypercsr( rows=rows, indptr=indptr, @@ -1938,6 +1954,8 @@ def pack_hypercsc( See `Matrix.ss.import_hypercsc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_hypercsc( cols=cols, indptr=indptr, @@ -2126,6 +2144,8 @@ def pack_bitmapr( See `Matrix.ss.import_bitmapr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmapr( bitmap=bitmap, values=values, @@ -2302,6 +2322,8 @@ def pack_bitmapc( See `Matrix.ss.import_bitmapc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmapc( bitmap=bitmap, values=values, @@ -2467,6 +2489,8 @@ def pack_fullr( See `Matrix.ss.import_fullr` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_fullr( values=values, is_iso=is_iso, @@ -2614,6 +2638,8 @@ def pack_fullc( See `Matrix.ss.import_fullc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_fullc( values=values, is_iso=is_iso, @@ -2765,6 +2791,8 @@ def pack_coo( See `Matrix.ss.import_coo` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_coo( nrows=self._parent._nrows, ncols=self._parent._ncols, @@ -2943,6 +2971,8 @@ def pack_coor( See `Matrix.ss.import_coor` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_coor( rows=rows, cols=cols, @@ -3096,6 +3126,8 @@ def pack_cooc( See `Matrix.ss.import_cooc` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_cooc( ncols=self._parent._ncols, rows=rows, @@ -3284,6 +3316,8 @@ def pack_any( See `Matrix.ss.import_any` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_any( values=values, is_iso=is_iso, diff --git a/graphblas/_ss/vector.py b/graphblas/_ss/vector.py index 346ab4c38..ebe5ab1e2 100644 --- a/graphblas/_ss/vector.py +++ b/graphblas/_ss/vector.py @@ -169,6 +169,8 @@ def build_diag(self, matrix, k=0): # Transpose descriptor doesn't do anything, so use the parent k = -k matrix = matrix._matrix + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call("GxB_Vector_diag", [self._parent, matrix, _as_scalar(k, INT64, is_cscalar=True), None]) def split(self, chunks, *, name=None): @@ -253,6 +255,8 @@ def concat(self, tiles): graphblas.ss.concat """ tiles, m, n, is_matrix = _concat_mn(tiles, is_matrix=False) + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) self._concat(tiles, m) def build_scalar(self, indices, value): @@ -268,6 +272,8 @@ def build_scalar(self, indices, value): """ indices = ints_to_numpy_buffer(indices, np.uint64, name="indices") scalar = _as_scalar(value, self._parent.dtype, is_cscalar=False) # pragma: is_grbscalar + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) call( "GxB_Vector_build_Scalar", [ @@ -464,6 +470,8 @@ def unpack(self, format=None, *, sort=False, raw=False): def _export(self, format=None, *, sort=False, give_ownership=False, raw=False, method): if give_ownership: parent = self._parent + if parent._hooks is not None and "onchange" in parent._hooks: + parent._hooks["onchange"](self) else: parent = self._parent.dup(name=f"v_{method}") dtype = parent.dtype.np_type @@ -680,6 +688,8 @@ def pack_any( See `Vector.ss.import_any` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_any( values=values, is_iso=is_iso, @@ -860,6 +870,8 @@ def pack_sparse( See `Vector.ss.import_sparse` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_sparse( indices=indices, values=values, @@ -1027,6 +1039,8 @@ def pack_bitmap( See `Vector.ss.import_bitmap` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_bitmap( bitmap=bitmap, values=values, @@ -1188,6 +1202,8 @@ def pack_full( See `Vector.ss.import_full` documentation for more details. """ + if self._parent._hooks is not None and "onchange" in self._parent._hooks: + self._parent._hooks["onchange"](self) return self._import_full( values=values, is_iso=is_iso, diff --git a/graphblas/base.py b/graphblas/base.py index 491c9b31f..aa14a6a08 100644 --- a/graphblas/base.py +++ b/graphblas/base.py @@ -329,6 +329,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): "Scalar accumulation with extract element" "--such as `s(accum=accum) << v[0]`--is not supported" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) expr.parent._extract_element( expr.resolved_indexes, self.dtype, is_cscalar=self._is_cscalar, result=self ) @@ -396,8 +398,9 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): if type(expr) is Scalar: scalar = expr else: + dtype = self.dtype if self.dtype._is_udt else None try: - scalar = Scalar.from_value(expr, is_cscalar=None, name="") + scalar = Scalar.from_value(expr, dtype, is_cscalar=None, name="") except TypeError: raise TypeError( "Assignment value must be a valid expression type, not " @@ -422,10 +425,14 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): if input_mask is not None: raise TypeError("`input_mask` argument may only be used for extract") elif expr.op is not None and expr.op.opclass == "Aggregator": + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) updater = self(mask=mask, accum=accum, replace=replace) expr.op._new(updater, expr) return elif expr.cfunc_name is None: # Custom recipe + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) updater = self(mask=mask, accum=accum, replace=replace) expr.args[-2](updater, *expr.args[-1]) return @@ -474,6 +481,8 @@ def _update(self, expr, mask=None, accum=None, replace=False, input_mask=None): args.append(expr.op) args.extend(expr.args) args.append(desc) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) # Make the GraphBLAS call call(cfunc_name, args) if self._is_scalar: diff --git a/graphblas/expr.py b/graphblas/expr.py index c12f97fa1..bdb3ef718 100644 --- a/graphblas/expr.py +++ b/graphblas/expr.py @@ -406,6 +406,8 @@ def __getitem__(self, keys): def _setitem(self, resolved_indexes, obj, *, is_submask): # Occurs when user calls C(params)[index] = expr + if self.parent._hooks is not None and "onchange" in self.parent._hooks: + self.parent._hooks["onchange"](self) if resolved_indexes.is_single_element and not self.kwargs: # Fast path using assignElement self.parent._assign_element(resolved_indexes, obj) @@ -427,6 +429,8 @@ def __delitem__(self, keys): if self.parent._is_scalar: raise TypeError("Indexing not supported for Scalars") resolved_indexes = IndexerResolver(self.parent, keys) + if self.parent._hooks is not None and "onchange" in self.parent._hooks: + self.parent._hooks["onchange"](self) if resolved_indexes.is_single_element: self.parent._delete_element(resolved_indexes) else: diff --git a/graphblas/matrix.py b/graphblas/matrix.py index 9d6595219..6847e1cf1 100644 --- a/graphblas/matrix.py +++ b/graphblas/matrix.py @@ -62,7 +62,7 @@ class Matrix(BaseType): High-level wrapper around GrB_Matrix type """ - __slots__ = "_nrows", "_ncols", "_parent", "ss" + __slots__ = "_nrows", "_ncols", "_parent", "_hooks", "ss" ndim = 2 _is_transposed = False _name_counter = itertools.count() @@ -78,6 +78,7 @@ def __new__(cls, dtype=FP64, nrows=0, ncols=0, *, name=None): self._nrows = nrows.value self._ncols = ncols.value self._parent = None + self._hooks = None self.ss = ss(self) return self @@ -90,6 +91,7 @@ def _from_obj(cls, gb_obj, dtype, nrows, ncols, *, parent=None, name=None): self._nrows = nrows self._ncols = ncols self._parent = parent + self._hooks = None if parent is None else parent._hooks self.ss = ss(self) return self @@ -271,11 +273,15 @@ def T(self): return TransposedMatrix(self) def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Matrix_clear", [self]) def resize(self, nrows, ncols): nrows = _as_scalar(nrows, _INDEX, is_cscalar=True) ncols = _as_scalar(ncols, _INDEX, is_cscalar=True) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Matrix_resize", [self, nrows, ncols]) self._nrows = nrows.value self._ncols = ncols.value @@ -325,6 +331,8 @@ def build(self, rows, columns, values, *, dup_op=None, clear=False, nrows=None, f"`rows` and `columns` and `values` lengths must match: " f"{rows.size}, {columns.size}, {values.size}" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if clear: self.clear() if nrows is not None or ncols is not None: diff --git a/graphblas/scalar.py b/graphblas/scalar.py index 478d5e4f9..1d3f9a373 100644 --- a/graphblas/scalar.py +++ b/graphblas/scalar.py @@ -23,6 +23,7 @@ def _scalar_index(name): self.gb_obj = ffi_new("GrB_Index*") self._is_cscalar = True self._empty = True + self._hooks = None return self @@ -32,7 +33,7 @@ class Scalar(BaseType): Pseudo-object for GraphBLAS functions which accumulate into a scalar type """ - __slots__ = "_empty", "_is_cscalar" + __slots__ = "_empty", "_is_cscalar", "_hooks" ndim = 0 shape = () _is_scalar = True @@ -56,6 +57,7 @@ def __new__(cls, dtype=FP64, *, is_cscalar=False, name=None): else: self.gb_obj = ffi_new("GrB_Scalar*") call("GrB_Scalar_new", [_Pointer(self), dtype]) + self._hooks = None return self @classmethod @@ -65,6 +67,7 @@ def _from_obj(cls, gb_obj, dtype, *, is_cscalar=False, name=None): self.gb_obj = gb_obj self.dtype = dtype self._is_cscalar = is_cscalar + self._hooks = None return self def __del__(self): @@ -167,8 +170,8 @@ def isequal(self, other, *, check_dtype=False): if type(other) is not Scalar: if other is None: return self._is_empty + dtype = self.dtype if self.dtype._is_udt else None try: - dtype = self.dtype if self.dtype._is_udt else None other = Scalar.from_value(other, dtype, is_cscalar=None, name="s_isequal") except TypeError: other = self._expect_type( @@ -204,8 +207,9 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False): if type(other) is not Scalar: if other is None: return self._is_empty + dtype = self.dtype if self.dtype._is_udt else None try: - other = Scalar.from_value(other, is_cscalar=None, name="s_isclose") + other = Scalar.from_value(other, dtype, is_cscalar=None, name="s_isclose") except TypeError: other = self._expect_type( other, @@ -235,6 +239,8 @@ def isclose(self, other, *, rel_tol=1e-7, abs_tol=0.0, check_dtype=False): return isclose_func._numba_func(self.value, other.value) def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if self._is_empty: return if self._is_cscalar: @@ -278,6 +284,8 @@ def value(self): @value.setter def value(self, val): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if val is None or output_type(val) is Scalar and val._is_empty: self.clear() elif self._is_cscalar: diff --git a/graphblas/tests/test_matrix.py b/graphblas/tests/test_matrix.py index 38eb4c3f3..003a0ce10 100644 --- a/graphblas/tests/test_matrix.py +++ b/graphblas/tests/test_matrix.py @@ -2739,6 +2739,7 @@ def test_expr_is_like_matrix(A): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -2781,6 +2782,7 @@ def test_index_expr_is_like_matrix(A): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -3586,3 +3588,58 @@ def test_ss_serialize(A): A.ss.serialize("lz4hc", 0) with pytest.raises(InvalidObject): Matrix.ss.deserialize(a[:-5]) + + +def test_hooks(A): + B = A.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + B._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + B.clear() + with pytest.raises(OnChangeException): + B.resize(100, 100) + with pytest.raises(OnChangeException): + B.build([1, 2], [1, 2], [1, 2], clear=True) + with pytest.raises(OnChangeException): + B[1, 2] = 2 + with pytest.raises(OnChangeException): + del B[1, 1] + with pytest.raises(OnChangeException): + B[:, :] = A + with pytest.raises(OnChangeException): + del B[[1, 2], [1, 2]] + with pytest.raises(OnChangeException): + B << B.reposition(1, 2) + with pytest.raises(OnChangeException): + B << A @ A + v = A.diag() + with pytest.raises(OnChangeException): + B.ss.build_diag(v) + tiles = B.ss.split((3, 3)) + with pytest.raises(OnChangeException): + B.ss.concat(tiles) + with pytest.raises(OnChangeException): + B << B + 1 + with pytest.raises(OnChangeException): + B.ss.build_scalar([1, 2], [1, 2], 1) + with pytest.raises(OnChangeException): + B.ss.export(give_ownership=True) + with pytest.raises(OnChangeException): + B.ss.unpack() + info = B.ss.export() + with pytest.raises(OnChangeException): + B.ss.pack_any(**info) + for fmt in ["csr", "csc", "hypercsr", "hypercsc", "bitmapr", "bitmapc", "coor", "cooc", "coo"]: + info = B.ss.export(fmt) + with pytest.raises(OnChangeException): + getattr(B.ss, f"pack_{fmt}")(**info) + with pytest.raises(OnChangeException): + B.ss.pack_fullr(np.ones(B.shape)) + with pytest.raises(OnChangeException): + B.ss.pack_fullc(np.ones(B.shape)) diff --git a/graphblas/tests/test_scalar.py b/graphblas/tests/test_scalar.py index 8b0b21e1d..6ee688efa 100644 --- a/graphblas/tests/test_scalar.py +++ b/graphblas/tests/test_scalar.py @@ -319,6 +319,7 @@ def test_expr_is_like_scalar(s): "_expr_name", "_expr_name_html", "_from_obj", + "_hooks", "_name_counter", "_update", "clear", @@ -351,6 +352,7 @@ def test_index_expr_is_like_scalar(s): "_expr_name", "_expr_name_html", "_from_obj", + "_hooks", "_name_counter", "_update", "clear", @@ -458,3 +460,28 @@ def test_get(s): s.clear() assert compute(s.get()) is None assert s.get("mittens") == "mittens" + + +def test_hooks(s): + t = s.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + t._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + t.clear() + with pytest.raises(OnChangeException): + t.value = 7 + v = Vector.from_values([1, 2], [3, 4]) + with pytest.raises(OnChangeException): + t << v[1] + with pytest.raises(OnChangeException): + t << v.reduce() + with pytest.raises(OnChangeException): + t << v.reduce(gb.agg.count) + with pytest.raises(OnChangeException): + t << s diff --git a/graphblas/tests/test_vector.py b/graphblas/tests/test_vector.py index bc3d4b9df..626d6bedf 100644 --- a/graphblas/tests/test_vector.py +++ b/graphblas/tests/test_vector.py @@ -1547,6 +1547,7 @@ def test_expr_is_like_vector(v): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -1582,6 +1583,7 @@ def test_index_expr_is_like_vector(v): "_deserialize", "_extract_element", "_from_obj", + "_hooks", "_name_counter", "_parent", "_prep_for_assign", @@ -2249,3 +2251,60 @@ def test_ss_serialize(v): v.ss.serialize("lz4hc", 0) with pytest.raises(InvalidObject): Vector.ss.deserialize(a[:-5]) + + +def test_hooks(v): + w = v.dup() + + class OnChangeException(Exception): + pass + + def onchange(x, **kwargs): + raise OnChangeException() + + w._hooks = {"onchange": onchange} + with pytest.raises(OnChangeException): + w.clear() + with pytest.raises(OnChangeException): + w.resize(100) + with pytest.raises(OnChangeException): + w.build([1, 2], [1, 2], clear=True) + with pytest.raises(OnChangeException): + w[1] = 2 + with pytest.raises(OnChangeException): + del w[1] + with pytest.raises(OnChangeException): + w[: v.size] = v + with pytest.raises(OnChangeException): + del w[[1, 2]] + with pytest.raises(OnChangeException): + w << w.reposition(1) + A = w.outer(w).new() + with pytest.raises(OnChangeException): + w << A.reduce_rowwise(agg.count) + with pytest.raises(OnChangeException): + w.ss.build_diag(A) + tiles = w.ss.split(3) + with pytest.raises(OnChangeException): + w.ss.concat(tiles) + with pytest.raises(OnChangeException): + w << w + w + with pytest.raises(OnChangeException): + w << w + 1 + with pytest.raises(OnChangeException): + w.ss.build_scalar([1, 2, 3], 1) + with pytest.raises(OnChangeException): + w.ss.export(give_ownership=True) + with pytest.raises(OnChangeException): + w.ss.unpack() + info = w.ss.export() + with pytest.raises(OnChangeException): + w.ss.pack_any(**info) + info = w.ss.export("sparse") + with pytest.raises(OnChangeException): + w.ss.pack_sparse(**info) + info = w.ss.export("bitmap") + with pytest.raises(OnChangeException): + w.ss.pack_bitmap(**info) + with pytest.raises(OnChangeException): + w.ss.pack_full(np.arange(w.size)) diff --git a/graphblas/vector.py b/graphblas/vector.py index 5630e83e4..e3f510f02 100644 --- a/graphblas/vector.py +++ b/graphblas/vector.py @@ -77,7 +77,7 @@ class Vector(BaseType): High-level wrapper around GrB_Vector type """ - __slots__ = "_size", "_parent", "ss" + __slots__ = "_size", "_parent", "_hooks", "ss" ndim = 1 _name_counter = itertools.count() @@ -90,6 +90,7 @@ def __new__(cls, dtype=FP64, size=0, *, name=None): call("GrB_Vector_new", [_Pointer(self), self.dtype, size]) self._size = size.value self._parent = None + self._hooks = None self.ss = ss(self) return self @@ -101,6 +102,7 @@ def _from_obj(cls, gb_obj, dtype, size, *, parent=None, name=None): self.dtype = dtype self._size = size self._parent = parent + self._hooks = None if parent is None else parent._hooks self.ss = ss(self) return self @@ -281,10 +283,14 @@ def _nvals(self): return n[0] def clear(self): + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Vector_clear", [self]) def resize(self, size): size = _as_scalar(size, _INDEX, is_cscalar=True) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) call("GrB_Vector_resize", [self, size]) self._size = size.value @@ -325,6 +331,8 @@ def build(self, indices, values, *, dup_op=None, clear=False, size=None): raise ValueError( f"`indices` and `values` lengths must match: {indices.size} != {values.size}" ) + if self._hooks is not None and "onchange" in self._hooks: + self._hooks["onchange"](self) if clear: self.clear() if size is not None: