diff --git a/docs/source/main-algorithms/todd-coxeter/to-todd-coxeter.rst b/docs/source/main-algorithms/todd-coxeter/to-todd-coxeter.rst index 69276882..7171c7c9 100644 --- a/docs/source/main-algorithms/todd-coxeter/to-todd-coxeter.rst +++ b/docs/source/main-algorithms/todd-coxeter/to-todd-coxeter.rst @@ -51,6 +51,10 @@ Additionally, specify one of the following for *rtype*: - ``(ToddCoxeter, list[int])`` for constructing a :any:`ToddCoxeter` on words with type ``list[int]``. +The word type must be specified explicitly for this conversion. Using +``rtype=(ToddCoxeter,)`` raises a :any:`TypeError`, since the word type cannot +be inferred from *fpb* and *wg*. + This function converts the :any:`FroidurePin` object *fpb* into a :any:`ToddCoxeter` object using the :any:`WordGraph` *wg* (which should be either the :any:`FroidurePin.left_cayley_graph` or the @@ -107,6 +111,8 @@ Additionally, specify the following for *rtype*: - ``(ToddCoxeter,)`` for constructing a :any:`ToddCoxeter`. +The word type is inferred from *kb*. + This function converts the :any:`KnuthBendix` object *kb* into a :any:`ToddCoxeter` object using the right Cayley graph of the semigroup represented by *kb*. diff --git a/src/libsemigroups_pybind11/to.py b/src/libsemigroups_pybind11/to.py index 30ac41a9..b8b83ee4 100644 --- a/src/libsemigroups_pybind11/to.py +++ b/src/libsemigroups_pybind11/to.py @@ -123,9 +123,13 @@ def _nice_name(types: _Iterator[_Any]) -> str: def to(*args, rtype: tuple): """Convert from one type of |libsemigroups_pybind11| object to another. - This function converts the the arguments specified in *args* to object of + This function converts the arguments specified in *args* to an object of type *rtype*. + If a converter raises :any:`TypeError`, the error message lists the possible + values of *rtype* for the requested output class. Which values can be used + depends on *args*. The original exception is retained as the cause. + :param args: the objects to convert. :param rtype: the type of object to convert to. :type rtype: tuple @@ -180,7 +184,21 @@ def to(*args, rtype: tuple): f"but found: {_nice_name(rtype)}" ) constructor = rtype[0] - return constructor(_RETURN_TYPE_TO_CONVERTER_FUNCTION[rtype](*cxx_args)) + try: + result = _RETURN_TYPE_TO_CONVERTER_FUNCTION[rtype](*cxx_args) + except TypeError as error: + possible_rtypes = ( + _nice_name(types) + for types in _RETURN_TYPE_TO_CONVERTER_FUNCTION + if types[0] is constructor + ) + message = ( + f"could not convert the arguments with rtype={_nice_name(rtype)}; " + f"possible values of rtype for {_nice_name(constructor)} are:" + "\n * " + "\n * ".join(possible_rtypes) + "\n" + ) + raise TypeError(message) from error + return constructor(result) __all__ = ["to"] diff --git a/tests/test_to.py b/tests/test_to.py index 4599ae00..671c53d8 100644 --- a/tests/test_to.py +++ b/tests/test_to.py @@ -35,12 +35,14 @@ PresentationWord, ) from libsemigroups_pybind11 import ( + Alphabet, Bipartition, Congruence, FroidurePin, InversePresentation, Kambites, KnuthBendix, + LibsemigroupsError, Order, Presentation, Stephen, @@ -448,18 +450,40 @@ def test_to_ToddCoxeter_017(): # From FroidurePin -def test_to_ToddCoxeter_018(): +@pytest.mark.parametrize("kind", [congruence_kind.onesided, congruence_kind.twosided]) +def test_to_ToddCoxeter_018(kind): S = FroidurePin(Transf([1, 3, 4, 2, 3]), Transf([3, 2, 1, 3, 3])) - tc = to(congruence_kind.twosided, S, S.right_cayley_graph(), rtype=(ToddCoxeter, str)) + tc = to(kind, S, S.right_cayley_graph(), rtype=(ToddCoxeter, str)) assert tc.current_word_graph().number_of_nodes() == S.size() + 1 assert isinstance(tc, ToddCoxeter) + assert tc.py_template_params == (str,) + assert tc.number_of_classes() == S.size() -def test_to_ToddCoxeter_019(): +@pytest.mark.parametrize("kind", [congruence_kind.onesided, congruence_kind.twosided]) +def test_to_ToddCoxeter_019(kind): S = FroidurePin(Transf([1, 3, 4, 2, 3]), Transf([3, 2, 1, 3, 3])) - tc = to(congruence_kind.twosided, S, S.right_cayley_graph(), rtype=(ToddCoxeter, list[int])) + tc = to(kind, S, S.right_cayley_graph(), rtype=(ToddCoxeter, list[int])) assert tc.current_word_graph().number_of_nodes() == S.size() + 1 assert isinstance(tc, ToddCoxeter) + assert tc.py_template_params == (list[int],) + assert tc.number_of_classes() == S.size() + + +@pytest.mark.parametrize("kind", [congruence_kind.onesided, congruence_kind.twosided]) +@pytest.mark.parametrize("unwrap", [False, True]) +def test_to_todd_coxeter_missing_word_type(kind, unwrap): + # https://github.com/libsemigroups/libsemigroups_pybind11/issues/461 + S = FroidurePin(Transf([1, 3, 4, 2, 3]), Transf([3, 2, 1, 3, 3])) + wg = S.right_cayley_graph() + if unwrap: + S = to_cxx(S) + with pytest.raises(TypeError, match="possible values of rtype for ToddCoxeter") as exc: + to(kind, S, wg, rtype=(ToddCoxeter,)) + assert "(ToddCoxeter, str)" in str(exc.value) + assert "(ToddCoxeter, list[int])" in str(exc.value) + assert isinstance(exc.value.__cause__, TypeError) + assert "to_todd_coxeter(): incompatible function arguments" in str(exc.value.__cause__) ############################################################################### @@ -1106,6 +1130,61 @@ def test_to_invalid_word_type(): assert message.endswith("but found: (Presentation, list[str])") +@pytest.mark.parametrize( + "rtype", + [ + (Alphabet, str), + (Congruence, str), + (FroidurePin,), + (InversePresentation,), + (KnuthBendix,), + (Presentation,), + (ToddCoxeter,), + ], +) +def test_to_conversion_error_suggests_rtypes(rtype): + with pytest.raises(TypeError, match="possible values of rtype") as exc: + to(object(), rtype=rtype) + + name = rtype[0].__name__ + message = str(exc.value) + assert f"possible values of rtype for {name}" in message + suggestions = [line for line in message.splitlines() if line.startswith(" * ")] + assert suggestions + assert all(line.startswith(f" * ({name}") for line in suggestions) + assert isinstance(exc.value.__cause__, TypeError) + assert "incompatible function arguments" in str(exc.value.__cause__) + + +def test_to_presentation_error_suggests_word_types(): + S = FroidurePin(Transf([1, 0])) + with pytest.raises(TypeError, match="possible values of rtype for Presentation") as exc: + to(S, rtype=(Presentation,)) + assert "(Presentation, str)" in str(exc.value) + assert "(Presentation, list[int])" in str(exc.value) + for word_type in (str, list[int]): + assert len(to(S, rtype=(Presentation, word_type)).alphabet()) == 1 + + +def test_to_conversion_error_preserves_callback_cause(): + error = TypeError("invalid letter mapping") + + def bad_mapping(_): + raise error + + with pytest.raises(TypeError, match="possible values of rtype for Presentation") as exc: + to(Presentation("ab"), bad_mapping, rtype=(Presentation, list[int])) + assert exc.value.__cause__ is error + + +@pytest.mark.parametrize("word_type", [str, list[int]]) +def test_to_conversion_error_preserves_libsemigroups_error(word_type): + S = FroidurePin(Transf([1, 0])) + with pytest.raises(LibsemigroupsError, match="expected the 3rd argument") as exc: + to(congruence_kind.twosided, S, WordGraph(0, 0), rtype=(ToddCoxeter, word_type)) + assert exc.value.__cause__ is None + + def test_to_999(): x = 10 with pytest.raises(TypeError):