Thank you for the detailed reply!
First, it looks like the tests are failing even with the current state. In Python, when you define an equality method but not a hash method, the object automatically becomes unhashable. So we'd need to define __hash__ for sensor_t and data_t, e.g.
.def("__hash__", [](const sensor_t& a) { return std::hash<colmap::sensor_t>()(a); });
As for the MakeDataclass, which classes do not have an equality operator? We could have a fallback that checks equality of the python dict (slower but ok):
#include <type_traits> template<typename T, typename = void> struct has_equality_operator : std::false_type {}; template<typename T> struct has_equality_operator<T, std::void_t<decltype(std::declval<T&>() == std::declval<T&>())>> : std::true_type {}; template<typename T, typename = void> struct has_less_than_operator : std::false_type {}; template<typename T> struct has_less_than_operator<T, std::void_t<decltype(std::declval<T&>() < std::declval<T&>())>> : std::true_type {}; template<typename T, typename = void> struct is_hashable : std::false_type {}; template<typename T> struct is_hashable<T, std::void_t<decltype(std::hash<T>{}(std::declval<T>()))>> : std::true_type {}; template <typename T, typename... options> void MakeDataclass(py::classh<T, options...> cls, const std::vector<std::string>& attributes = {}) { // ... if constexpr (has_equality_operator<T>::value) { cls.def(py::self == py::self); if constexpr (is_hashable<T>::value) { cls.def("__hash__", [](const T& self) { return std::hash<T>()(self); }); } else { cls.attr("__hash__") = py::none(); } } else { cls.def("__eq__", [attributes](const T& self, const py::object& other) { if (!py::isinstance<T>(other)) { return false; } py::dict self_dict = ConvertToDict(self, attributes, true); py::dict other_dict = ConvertToDict(other.cast<T>(), attributes, true); return self_dict.equal(other_dict); }); cls.attr("__hash__") = py::none(); } if constexpr (has_less_than_operator<T>::value) { cls.def(py::self < py::self); } }
(not tested)