From e9f422ea2fcef04cb147790bb66ec0942ada6564 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 9 Sep 2026 13:09:39 +0100 Subject: [PATCH 1/2] Clarify missing word type in ToddCoxeter conversions AI disclosure: OpenAI Codex prepared the initial implementation, tests, documentation, and commit message. Co-authored-by: Codex --- .../todd-coxeter/to-todd-coxeter.rst | 6 ++++ src/libsemigroups_pybind11/to.py | 14 +++++++++ tests/test_to.py | 30 ++++++++++++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) 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..6d3a6578 100644 --- a/src/libsemigroups_pybind11/to.py +++ b/src/libsemigroups_pybind11/to.py @@ -12,7 +12,10 @@ from typing import Any as _Any, get_origin as _get_origin from _libsemigroups_pybind11 import ( + FroidurePinBase as _FroidurePinBase, Order as _Order, + WordGraph as _WordGraph, + congruence_kind as _congruence_kind, to_alphabet_string as _to_alphabet_string, to_alphabet_word as _to_alphabet_word, to_congruence_string as _to_congruence_string, @@ -179,6 +182,17 @@ def to(*args, rtype: tuple): f"{_VALID_TYPES_STRING}" f"but found: {_nice_name(rtype)}" ) + if ( + rtype == (_ToddCoxeter,) + and len(cxx_args) == 3 + and isinstance(cxx_args[0], _congruence_kind) + and isinstance(cxx_args[1], _FroidurePinBase) + and isinstance(cxx_args[2], _WordGraph) + ): + raise TypeError( + "converting a FroidurePin and WordGraph to ToddCoxeter requires a word type; " + "use rtype=(ToddCoxeter, str) or rtype=(ToddCoxeter, list[int])" + ) constructor = rtype[0] return constructor(_RETURN_TYPE_TO_CONVERTER_FUNCTION[rtype](*cxx_args)) diff --git a/tests/test_to.py b/tests/test_to.py index 4599ae00..4ddd6c56 100644 --- a/tests/test_to.py +++ b/tests/test_to.py @@ -448,18 +448,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=r"requires a word type; use rtype=\(ToddCoxeter, str\) " + r"or rtype=\(ToddCoxeter, list\[int\]\)", + ): + to(kind, S, wg, rtype=(ToddCoxeter,)) ############################################################################### From 5c59b3d97a093324b19c424a0d6425c9c94ded8c Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 12 Sep 2026 13:21:36 +0100 Subject: [PATCH 2/2] Generalize return-type suggestions for failed conversions List the registered rtype choices for the requested output class when a converter raises TypeError, preserving the original exception as its cause. Cover all output classes, callback errors, and non-TypeError exceptions. AI disclosure: OpenAI Codex prepared the code, tests, documentation, and commit message, and ran the validation. Co-authored-by: Codex --- src/libsemigroups_pybind11/to.py | 36 +++++++++-------- tests/test_to.py | 67 +++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/src/libsemigroups_pybind11/to.py b/src/libsemigroups_pybind11/to.py index 6d3a6578..b8b83ee4 100644 --- a/src/libsemigroups_pybind11/to.py +++ b/src/libsemigroups_pybind11/to.py @@ -12,10 +12,7 @@ from typing import Any as _Any, get_origin as _get_origin from _libsemigroups_pybind11 import ( - FroidurePinBase as _FroidurePinBase, Order as _Order, - WordGraph as _WordGraph, - congruence_kind as _congruence_kind, to_alphabet_string as _to_alphabet_string, to_alphabet_word as _to_alphabet_word, to_congruence_string as _to_congruence_string, @@ -126,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 @@ -182,19 +183,22 @@ def to(*args, rtype: tuple): f"{_VALID_TYPES_STRING}" f"but found: {_nice_name(rtype)}" ) - if ( - rtype == (_ToddCoxeter,) - and len(cxx_args) == 3 - and isinstance(cxx_args[0], _congruence_kind) - and isinstance(cxx_args[1], _FroidurePinBase) - and isinstance(cxx_args[2], _WordGraph) - ): - raise TypeError( - "converting a FroidurePin and WordGraph to ToddCoxeter requires a word type; " - "use rtype=(ToddCoxeter, str) or rtype=(ToddCoxeter, list[int])" - ) 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 4ddd6c56..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, @@ -476,12 +478,12 @@ def test_to_todd_coxeter_missing_word_type(kind, unwrap): wg = S.right_cayley_graph() if unwrap: S = to_cxx(S) - with pytest.raises( - TypeError, - match=r"requires a word type; use rtype=\(ToddCoxeter, str\) " - r"or rtype=\(ToddCoxeter, list\[int\]\)", - ): + 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__) ############################################################################### @@ -1128,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):