Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions graphblas/core/operator/monoid.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,22 @@
def _is_builtin_binaryop(binaryop):
"""True if ``binaryop`` wraps built-in GraphBLAS operators rather than a UDF.

Built-ins are the only binary ops constructed with neither a Python func nor
a numba func. That covers ``binary.numpy.float_power`` too, which is
assembled out of ``binary.pow``'s typed ops.
Most built-ins are constructed with neither a Python func nor a numba func.
That covers ``binary.numpy.float_power`` too, which is assembled out of
``binary.pow``'s typed ops. A few built-ins (``any``, ``first``, ``pair``,
``second``) carry a Python ``orig_func`` as their UDT fallback, so the func
test alone misses them; their typed ops are still ``TypedBuiltinBinaryOp``,
which is the authoritative signal. A UDF's typed ops are never
``TypedBuiltinBinaryOp``. Under ``mapnumpy=True`` the mapped numpy binary
ops hold the built-in's typed ops and no funcs, so both tests agree they
are built-in; the numpy monoids themselves are mapped without passing
through ``Monoid._build``.
"""
return binaryop.orig_func is None and binaryop._numba_func is None
return (
binaryop.orig_func is None
and binaryop._numba_func is None
or any(type(typed) is TypedBuiltinBinaryOp for typed in binaryop._typed_ops.values())
)


def _scalar_identity(monoid_name, scalar_dtype):
Expand Down
18 changes: 18 additions & 0 deletions graphblas/tests/test_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,24 @@ def test_monoid_rejects_builtin_binaryop():
assert not hasattr(gb.monoid, "_bad_builtin_monoid")


def test_monoid_rejects_builtin_binaryop_with_udt_fallback():
"""binary.any, first, pair, and second are built-ins that carry a Python
func as their UDT fallback, so a func-based probe alone misses them. They
must be rejected like the other built-ins: their typed ops have no _monoid
slot, and a failed registration used to leave binary.<name>._monoid
pointing at the half-built monoid.
"""
for binop in [binary.any, binary.first, binary.pair, binary.second]:
with pytest.raises(TypeError, match="must be a user-defined BinaryOp"):
Monoid.register_anonymous(binop, 0)
# State is untouched: any's built-in monoid association survives, and the
# ops without a built-in monoid still have none.
assert binary.any.monoid is monoid.any
assert binary.first.monoid is None
assert binary.pair.monoid is None
assert binary.second.monoid is None


@pytest.mark.skipif("not supports_udfs")
@pytest.mark.slow
def test_semiring_parameterized():
Expand Down