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
4 changes: 3 additions & 1 deletion graphblas/agg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ def __getattr__(key):
ss = import_module(".ss", __name__)
globals()["ss"] = ss
return ss
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
4 changes: 3 additions & 1 deletion graphblas/binary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def __getattr__(key):
f"module {__name__!r} unable to compile UDF for {key!r}; "
"install numba for UDF support"
)
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
17 changes: 17 additions & 0 deletions graphblas/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ def inner(func_wo_doc):
return inner


def _module_attr_error(module_name, key, names):
"""Build the AttributeError raised by an operator namespace's ``__getattr__``.

``names`` should be the module's ``__dir__()`` so lazily-registered operators
are offered as "did you mean" suggestions without forcing them to build.
"""
import difflib

msg = f"module {module_name!r} has no attribute {key!r}"
candidates = [name for name in names if not name.startswith("_")]
matches = difflib.get_close_matches(key, candidates, n=3)
if matches:
hint = " or ".join(repr(match) for match in matches)
msg = f"{msg}. Did you mean {hint}?"
return AttributeError(msg)


# Include most common types (even mistakes)
_output_types = {
int: int,
Expand Down
4 changes: 3 additions & 1 deletion graphblas/indexunary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def __getattr__(key):
ss = import_module(".ss", __name__)
globals()["ss"] = ss
return ss
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
4 changes: 3 additions & 1 deletion graphblas/monoid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def __getattr__(key):
ss = import_module(".ss", __name__)
globals()["ss"] = ss
return ss
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
4 changes: 3 additions & 1 deletion graphblas/op/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def __getattr__(key):
f"module {__name__!r} unable to compile UDF for {key!r}; "
"install numba for UDF support"
)
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator, _supports_udfs # noqa: E402 isort:skip
Expand Down
4 changes: 3 additions & 1 deletion graphblas/select/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def __getattr__(key):
ss = import_module(".ss", __name__)
globals()["ss"] = ss
return ss
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


def _resolve_expr(expr, callname, opname):
Expand Down
4 changes: 3 additions & 1 deletion graphblas/semiring/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ def __getattr__(key):
f"module {__name__!r} unable to compile UDF for {key!r}; "
"install numba for UDF support"
)
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
34 changes: 34 additions & 0 deletions graphblas/tests/test_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -3725,3 +3725,37 @@ def test_compile_codegen_helper():
assert "Source:" in msg
assert bad_src in msg
assert isinstance(exc_info.value.__cause__, SyntaxError)


def test_operator_namespace_typo_suggestions():
# A typo in an operator namespace should suggest close matches (via difflib),
# drawn from __dir__() so lazily-registered operators are offered without
# forcing them to build.
with pytest.raises(AttributeError, match="has no attribute 'pluss'.*Did you mean 'plus'"):
binary.pluss
with pytest.raises(AttributeError, match="Did you mean 'plus'"):
monoid.pluss
with pytest.raises(AttributeError, match="plus_times"):
semiring.plus_time
with pytest.raises(AttributeError, match="Did you mean 'sum'"):
agg.summ
with pytest.raises(AttributeError, match="Did you mean"):
unary.expp
with pytest.raises(AttributeError, match="rowindex"):
indexunary.rowindexx
with pytest.raises(AttributeError, match="triu"):
select.triu_typo
with pytest.raises(AttributeError, match="Did you mean 'plus'"):
op.pluss

# No close match -> plain message, no suggestion appended
with pytest.raises(AttributeError) as exc_info:
binary.zzzzzz
assert "has no attribute 'zzzzzz'" in str(exc_info.value)
assert "Did you mean" not in str(exc_info.value)

# Building suggestions must not force lazy operators to compile
before = set(binary._delayed)
with pytest.raises(AttributeError):
binary.pluss
assert set(binary._delayed) == before
4 changes: 3 additions & 1 deletion graphblas/unary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def __getattr__(key):
ss = import_module(".ss", __name__)
globals()["ss"] = ss
return ss
raise AttributeError(f"module {__name__!r} has no attribute {key!r}")
from ..core.utils import _module_attr_error

raise _module_attr_error(__name__, key, __dir__())


from ..core import operator # noqa: E402 isort:skip
Expand Down
Loading