Fix custom __str__ for enum_ (#6078)
* fix: prioritize custom enum __str__
* test: strengthen custom enum __str__ regression
* refactor: move enum __str__ handling into enum_
Keep class_::def() generic and let enum_ own the enum-specific
behavior for custom __str__ overloads. This avoids using the private
__entries attribute as a runtime sentinel for py::enum_ while preserving
the prepend behavior that lets user-defined enum __str__ methods take
precedence over the generated default.
One caveat is that this applies to normal py::enum_ API usage. Code that
intentionally upcasts an enum_ binding to class_& and then calls
class_::def("__str__", ...) will bypass this enum_ override and keep the
generic class_ behavior.
* fix: avoid enum def ambiguity on MinGW
Remove the inherited class_::def overload set from enum_ and add an explicit forwarding overload for non-string def() calls. GCC 15 on MinGW otherwise sees the duplicate dependent-base def(const char *, ...) template as ambiguous with enum_::def(const char *, ...), breaking the C++11 build.
---------
Co-authored-by: ctmd1234567 <ctmd1234567@users.noreply.github.com>
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index 9cc45bd..ccd7bae 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -3171,7 +3171,6 @@
public:
using Base = class_<Type>;
using Base::attr;
- using Base::def;
using Base::def_property_readonly;
using Base::def_property_readonly_static;
using Underlying = typename std::underlying_type<Type>::type;
@@ -3275,6 +3274,28 @@
pos_only());
}
+ template <typename Func, typename... Extra>
+ enum_ &def(const char *name_, Func &&f, const Extra &...extra) {
+ if (std::strcmp(name_, "__str__") == 0) {
+ Base::def(name_, std::forward<Func>(f), prepend{}, extra...);
+ } else {
+ Base::def(name_, std::forward<Func>(f), extra...);
+ }
+ return *this;
+ }
+
+ // Avoid using Base::def here: GCC 15/MinGW sees the duplicate dependent-base
+ // def(const char *, ...) template as ambiguous with enum_::def(const char *, ...).
+ template <typename T,
+ typename... Extra,
+ detail::enable_if_t<
+ !std::is_convertible<typename std::decay<T>::type, const char *>::value,
+ int> = 0>
+ enum_ &def(T &&op, const Extra &...extra) {
+ Base::def(std::forward<T>(op), extra...);
+ return *this;
+ }
+
/// Export enumeration entries into the parent scope
enum_ &export_values() {
m_base.export_values();
diff --git a/tests/test_enum.cpp b/tests/test_enum.cpp
index 4ec0af7..2a7fa0f 100644
--- a/tests/test_enum.cpp
+++ b/tests/test_enum.cpp
@@ -24,6 +24,15 @@
.value("Two", ScopedEnum::Two)
.value("Three", ScopedEnum::Three);
+ // test_enum_custom_str
+ enum class CustomStrEnum { A = 1, B = 2 };
+ py::enum_<CustomStrEnum>(m, "CustomStrEnum")
+ .value("A", CustomStrEnum::A)
+ .value("B", CustomStrEnum::B)
+ .def("__str__", [](CustomStrEnum value) {
+ return "CustomStrEnum value " + std::to_string(static_cast<int>(value));
+ });
+
m.def("test_scoped_enum", [](ScopedEnum z) {
return "ScopedEnum::" + std::string(z == ScopedEnum::Two ? "Two" : "Three");
});
diff --git a/tests/test_enum.py b/tests/test_enum.py
index 52083ca..969c30b 100644
--- a/tests/test_enum.py
+++ b/tests/test_enum.py
@@ -277,6 +277,15 @@
assert enum_type.__str__.__doc__.startswith("__str__")
+def test_enum_custom_str_keeps_name_property():
+ assert str(m.CustomStrEnum.A) == "CustomStrEnum value 1"
+ assert str(m.CustomStrEnum.B) == "CustomStrEnum value 2"
+ assert m.CustomStrEnum.A.name == "A"
+ assert m.CustomStrEnum.A.value == 1
+ assert m.CustomStrEnum.B.name == "B"
+ assert m.CustomStrEnum.B.value == 2
+
+
def test_generated_dunder_methods_pos_only():
for enum_type in [m.ScopedEnum, m.UnscopedEnum]:
for binary_op in [