Skip to content
Merged
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: 4 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
bibtex_bibfiles = ["libsemigroups.bib"]

autosummary_generate = True
# We set this to True here, but remove "libsemigroups_pybind11\..*" from the doc
# everywhere. This is done so we still get the submodule names, but not the
# global module name. A nicer, but more involved solution, could use some Sphinx
# magic as done in https://stackoverflow.com/a/72658470/15278419.
add_module_names = True

templates_path = ["_templates"]
Expand Down
18 changes: 12 additions & 6 deletions docs/source/data-structures/misc/libsemigroups-error.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ Exceptions
==========

This page describes the custom error type used in ``libsemigroups_pybind11``,
namely :any:`LibsemigroupsError`. Other built-in exceptions, such as
:any:`ValueError` and :any:`TypeError`, may also be raised in this project. See
the `Python documentation <https://docs.python.org/3/library/exceptions.html>`_
namely :any:`LibsemigroupsError`, and related functions. Other built-in
exceptions, such as :any:`ValueError` and :any:`TypeError`, may also be raised
in this project. See the
`Python documentation <https://docs.python.org/3/library/exceptions.html>`_
for further details.

Full API
--------

.. autoexception:: LibsemigroupsError
:show-inheritance:

Expand All @@ -27,14 +31,14 @@ for further details.

>>> from libsemigroups_pybind11 import FroidurePin, Perm
>>> gens = [Perm([3, 0, 1, 2]), Perm([1, 2, 0, 3]), Perm([2, 1, 0, 3])]
>>> S = FroidurePin(gens[0])
>>> S.add_generators(gens[1:])
>>> S = FroidurePin(gens)
>>> S
<partially enumerated FroidurePin with 3 generators, 3 elements, Cayley graph ⌀ 1, & 0 rules>

>>> S.generator(3) # Bad: there are only three generators
Traceback (most recent call last):
...
_libsemigroups_pybind11.LibsemigroupsError: generator index out of bounds, expected value in [0, 3), got 3
LibsemigroupsError: generator index out of bounds, expected value in [0, 3), got 3

.. note::

Expand All @@ -43,3 +47,5 @@ for further details.
but you think it should, please let us known by opening an issue on the
`issue tracker
<https://github.com/libsemigroups/libsemigroups_pybind11/issues>`_.

.. autofunction:: error_message_with_prefix
57 changes: 57 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,63 @@ See the installation instructions:
install
changelog

The structure of the module
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Python module ``libsemigroups_pybind11`` was designed to mirror the C++
library ``libsemigroups`` as closely as possible, whilst navigating the
fundamental difference between Python and C++. This is done with the help of the
tool `pybind11 <https://pybind11.readthedocs.io/en/stable/>`_.

For various implementational reasons (mostly related to Python's lack of an
analogue for C++'s templating system), the Python code exposed by ``pybind11``
is less streamlined and concise than the C++ library. To try and address this,
the authors of ``libsemigroups_pybind11`` have further wrapped the Python code
produced by ``pybind11`` to make the Python and C++ user experience as similar
as possible.

The Python bindings of the ``libsemigroups`` code produced by ``pybind11``
reside in an intermediate module called ``_libsemigroups_pybind11`` (note the
leading underscore), and the public-facing nicely wrapped code resides in this
module — ``libsemigroups_pybind11``.

Should this impact the way you, the user, use ``libsemigroups_pybind11``? For
the most part, no. It should be possible to use ``libsemigroups_pybind11`` in
the ways documented on this site without the knowledge that
``_libsemigroups_pybind11`` even exists. The notable exceptions to this relate
to type names and error messages. A lot of the code in
``libsemigroups_pybind11`` has been imported from ``_libsemigroups_pybind11``,
and this is visible in qualified type names. For example:

.. doctest:: python

>>> from libsemigroups_pybind11 import WordGraph
>>> WordGraph
<class '_libsemigroups_pybind11.WordGraph'>

Additionally, some functions or classes in the ``_libsemigroups_pybind11``
module have additional prefixes and suffixes relative to their
``libsemigroups_pybind11`` counterpart. These usually relate to the module that
the function or class is contained within or a type the function or class is
defined upon. These may appear in error messages. For example:

.. doctest:: python

>>> from libsemigroups_pybind11 import aho_corasick, AhoCorasick
>>> ac = AhoCorasick()
>>> aho_corasick.add_word(ac, False)
Traceback (most recent call last):
...
TypeError: aho_corasick_add_word(): incompatible function arguments. The following argument types are supported:
1. (ac: _libsemigroups_pybind11.AhoCorasick, w: list[int]) -> int
2. (ac: _libsemigroups_pybind11.AhoCorasick, w: str) -> int
<BLANKLINE>
Invoked with: <AhoCorasick with 1 node>, False

The authors of ``libsemigroups_pybind11`` have gone to a lot of effort to
try and make error messages clear, specific and intelligible; however, if there
you encounter any errors with unclear messages, please raise this on the
`issue tracker <https://github.com/libsemigroups/libsemigroups_pybind11/issues>`_.

.. toctree::
:caption: Data Structures
:hidden:
Expand Down
3 changes: 1 addition & 2 deletions etc/replace-strings-in-doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
args = parser.parse_args()

replacements = {
r"_libsemigroups_pybind11.": "",
r"libsemigroups_pybind11\.": "",
r'(?<=<span class="sig-prename descclassname"><span class="pre">)_?libsemigroups_pybind11\.': "",
}

html_glob = "docs/_build/html/**/*.html"
Expand Down
2 changes: 1 addition & 1 deletion src/action.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Increase the capacity to a value that is greater or equal to *val*.
:param val: new capacity of an action instance.
:type val: int

:raises ValueError: if ``val`` is too large.
:raises MemoryError: if ``val`` is too large.

:complexity:
At most linear in the :any:`size()` of the :py:class:`Action`.
Expand Down
4 changes: 2 additions & 2 deletions src/adapters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ This call operator returns the image of *pt* acted on by *x*.
:rtype: Point

:raises TypeError:
If the wrapped C++ type of the sample objects passed via *element* and
If the wrapped C++ type of the sample objects passed via *x* and
*point* are not the same as the wrapped types of the arguments in any
invocation of the call operator. For example, if *point* is ``PPerm([], [],
256)``, then the underlying C++ type uses 8-bit integers to store image
Expand Down Expand Up @@ -171,7 +171,7 @@ This call operator returns the image of *pt* acted on by *x*.
:rtype: Point

:raises TypeError:
If the wrapped C++ type of the sample objects passed via *element* and
If the wrapped C++ type of the sample objects passed via *x* and
*point* are not the same as the wrapped types of the arguments in any
invocation of the call operator. For example, if *point* is ``PPerm([], [],
256)``, then the underlying C++ type uses 8-bit integers to store image
Expand Down
9 changes: 3 additions & 6 deletions src/dot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@ render it with the `Graphviz <https://www.graphviz.org>`_
installation on your system.
)pbdoc");
dot.def("__repr__", py::overload_cast<Dot const&>(&to_human_readable_repr));
dot.def_property(
dot.def_property_readonly(
"colors",
[](Dot const& self) { return Dot::colors; },
[]() {}, // TODO raise exception
R"pbdoc(
An array of default HTML/hex colours.
)pbdoc");
Expand Down Expand Up @@ -357,10 +356,9 @@ This nested class represents a node in the represented graph.
)pbdoc");
n.def("__repr__",
py::overload_cast<Dot::Node const&>(&to_human_readable_repr));
n.def_property(
n.def_property_readonly(
"attrs",
[](Dot::Node& self) { return self.attrs; },
[]() {},
R"pbdoc(
Read-only dictionary containing the attributes of the node.
)pbdoc");
Expand Down Expand Up @@ -397,10 +395,9 @@ Instances of this nested class represents an edge in the represented graph.
)pbdoc");
e.def("__repr__",
py::overload_cast<Dot::Edge const&>(&to_human_readable_repr));
e.def_property(
e.def_property_readonly(
"attrs",
[](Dot::Edge& self) { return self.attrs; },
[]() {},
R"pbdoc(
Read-only dictionary containing containing the attributes of the :any:`Edge`.
)pbdoc");
Expand Down
52 changes: 40 additions & 12 deletions src/errors.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,55 @@ namespace libsemigroups {
}

void init_error(py::module& m) {
// TODO this doesn't seem to properly catch all LibsemigroupsExceptions,
// particularly on macOS. This may have been resolved in pybind11 2.12.0
static py::exception<LibsemigroupsException> exc(
m, "LibsemigroupsError", PyExc_RuntimeError);
// Using the GIL safe call below rather than simply having a static
// py::exception is recommended in the pybind11 doc.
PYBIND11_CONSTINIT static py::gil_safe_call_once_and_store<py::object>
exc_storage;
exc_storage.call_once_and_store_result([&]() {
return py::exception<LibsemigroupsException>(
m, "LibsemigroupsError", PyExc_RuntimeError);
});
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) {
std::rethrow_exception(p);
}
} catch (LibsemigroupsException const& e) {
exc(formatted_error_message(e).c_str());
} catch (py::stop_iteration const& e) {
throw e;
} catch (std::runtime_error const& e) {
exc(formatted_error_message(e).c_str());
py::set_error(exc_storage.get_stored(),
formatted_error_message(e).c_str());
}
});
// TODO: Doc

m.def("error_message_with_prefix",
py::overload_cast<>(&error_message_with_prefix));
py::overload_cast<>(&error_message_with_prefix),
R"pbdoc(
Return whether :any:`LibsemigroupsError` messages have a C++ prefix.

Since ``libsemigroups_pybind11`` is built on top of the C++ library
``libsemigroups``, many of the errors thrown in ``libsemigroups_pybind11``
emanate from C++. This function returns whether :any:`LibsemigroupsError`
messages contain a prefix that indicates which C++ function raised the
exception.

:return: Whether :any:`LibsemigroupsError` messages contain a prefix about the
C++ function that raised the exception.
:rtype: bool
)pbdoc");
m.def("error_message_with_prefix",
py::overload_cast<bool>(&error_message_with_prefix));
py::overload_cast<bool>(&error_message_with_prefix),
py::arg("val"),
R"pbdoc(
Specify whether :any:`LibsemigroupsError` messages have a C++ prefix.

Since ``libsemigroups_pybind11`` is built on top of the C++ library
``libsemigroups``, many of the errors thrown in ``libsemigroups_pybind11``
emanate from C++. This function specifies whether :any:`LibsemigroupsError`
messages should contain a prefix that indicates which C++ function raised the
exception. By default, this information is not included.

:param val: Whether :any:`LibsemigroupsError` messages should contain a prefix
about the C++ function that raised the exception.
:type val: bool
)pbdoc");
}
} // namespace libsemigroups
8 changes: 6 additions & 2 deletions src/froidure-pin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ Copy a :any:`FroidurePin` object.
:returns: A copy.
:rtype: FroidurePin
)pbdoc");
// This function should really throw a ValueError if the degree of x is
// incompatible with the existing degree, but this doesn't get detected at
// the Python level, so a LibsemigroupsError is thrown instead. It would
// be possible to intercept this, but it probably isn't worth the effort.
thing.def("add_generator",
&FroidurePin_::add_generator,
py::arg("x"),
Expand Down Expand Up @@ -166,7 +170,7 @@ elements than before (whether it is fully enumerating or not).
:returns: ``self``.
:rtype: FroidurePin

:raises ValueError:
:raises LibsemigroupsError:
if the degree of *x* is incompatible with the existing degree (if any).

:raises TypeError:
Expand Down Expand Up @@ -199,7 +203,7 @@ See :any:`add_generator` for a detailed description.
:raises TypeError:
if any item in *gens* is not of the same type as the existing generators (if any).

:raises ValueError:
:raises LibsemigroupsError:
if the degree of any item in *gens* is incompatible with the existing degree (if any).
)pbdoc");

Expand Down
2 changes: 1 addition & 1 deletion src/libsemigroups_pybind11/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def __init__(
if _to_cxx(self) is not None:
return
if len(args) != 0:
raise ValueError(
raise TypeError(
f"expected 0 positional arguments, but found {len(args)}"
)
if not isinstance(generators, list):
Expand Down
4 changes: 1 addition & 3 deletions src/libsemigroups_pybind11/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ def __init__(self: _Self, *args, point=None, element=None) -> None:
if _to_cxx(self) is not None:
return
if len(args) != 0:
raise ValueError(
f"expected 0 positional arguments, but found {len(args)}"
)
raise TypeError(f"expected 0 positional arguments, but found {len(args)}")
self.py_template_params = (
type(_to_cxx(element)),
type(_to_cxx(point)),
Expand Down
4 changes: 2 additions & 2 deletions src/libsemigroups_pybind11/detail/cxx_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ def __init__(

for kwarg in required_kwargs:
if kwarg not in kwargs:
raise ValueError(
raise TypeError(
f'required keyword argument "{kwarg}" not found, '
f"found {tuple(kwargs.keys())} instead"
)
for kwarg in kwargs:
if kwarg not in required_kwargs and kwarg not in optional_kwargs:
raise ValueError(
raise TypeError(
f'unexpected keyword argument "{kwarg}", '
f"required keyword arguments are {required_kwargs} "
f"and optional keyword arguments are {optional_kwargs}"
Expand Down
2 changes: 1 addition & 1 deletion src/libsemigroups_pybind11/froidure_pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def __init__(self: _Self, *args) -> None:
if _to_cxx(self) is not None:
return
if len(args) == 0:
raise ValueError("expected at least 1 argument, found 0")
raise TypeError("expected at least 1 argument, found 0")

if isinstance(args[0], list) and len(args) == 1:
gens = args[0]
Expand Down
2 changes: 1 addition & 1 deletion src/libsemigroups_pybind11/konieczny.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def __init__(self: Self, *args) -> None:
if _to_cxx(self) is not None:
return
if len(args) == 0:
raise ValueError("expected at least 1 argument, found 0")
raise TypeError("expected at least 1 argument, found 0")

if isinstance(args[0], list) and len(args) == 1:
gens = args[0]
Expand Down
4 changes: 2 additions & 2 deletions src/libsemigroups_pybind11/presentation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def __init__(self: Self, *args, **kwargs) -> None:
or (len(args) == 1 and len(kwargs) > 0)
or len(args) > 1
):
raise ValueError(
raise TypeError(
'expected 1 positional argument or the keyword argument "Word"'
f" but found {len(args)} positional arguments, and keywords arguments "
f"{tuple(kwargs.keys())}"
Expand All @@ -123,7 +123,7 @@ def __init__(self: Self, *args, **kwargs) -> None:
f"but found {type(args[0])}"
)
if isinstance(args[0], list) and not all(isinstance(x, int) for x in args[0]):
raise ValueError("expected the argument to consist of int values")
raise TypeError("expected the argument to consist of int values")
if isinstance(args[0], str):
self.py_template_params = (str,)
if isinstance(args[0], list):
Expand Down
2 changes: 1 addition & 1 deletion src/libsemigroups_pybind11/schreier_sims.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self: _Self, *args) -> None:
if _to_cxx(self) is not None:
return
if len(args) == 0:
raise ValueError("expected at least 1 argument, found 0")
raise TypeError("expected at least 1 argument, found 0")

if isinstance(args[0], list) and len(args) == 1:
gens = args[0]
Expand Down
2 changes: 1 addition & 1 deletion src/libsemigroups_pybind11/stephen.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def __init__(self: _Self, *args, **kwargs) -> None:
return

if len(args) != 1:
raise ValueError(f"expected 1 argument, but got {len(args)}")
raise TypeError(f"expected 1 argument, but got {len(args)}")

if isinstance(args[0], (_Presentation, _InversePresentation)):
self.py_template_params = (type(_to_cxx(args[0])),)
Expand Down
5 changes: 3 additions & 2 deletions src/matrix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@

// libsemigroups....
#include <libsemigroups/adapters.hpp> // for Hash
#include <libsemigroups/constants.hpp> // for MaxPlusTruncMat, MinPlusTruncMat
#include <libsemigroups/constants.hpp> // for PositiveInfinity, NegativeInf...
#include <libsemigroups/exception.hpp> // for LIBSEMIGROUPS_EXCEPTION
#include <libsemigroups/matrix.hpp> // for MaxPlusTruncMat, MinPlusTruncMat

#include <libsemigroups/detail/string.hpp> // for string_format, to_string
Expand Down Expand Up @@ -408,7 +409,7 @@ Construct a matrix from rows.
:param rows: the rows of the matrix.
:type rows: list[list[int | PositiveInfinity | NegativeInfinity]]

:raise RunTimeError: if *kind* is
:raise TypeError: if *kind* is
:py:attr:`MatrixKind.MaxPlusTrunc`,
:py:attr:`MatrixKind.MinPlusTrunc`, or
:py:attr:`MatrixKind.NTP`.
Expand Down
Loading