From 2cbed02d957b32e0ffe22815af8abde4d01e9b21 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Tue, 22 Dec 2020 18:56:31 -0600 Subject: [PATCH 1/4] enum: Add Enum.value property --- include/pybind11/pybind11.h | 24 ++++++++++++++++++++++++ tests/test_enum.py | 23 ++++++++++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 104f32206d..efc1436aef 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -46,6 +46,7 @@ #include "options.h" #include "detail/class.h" #include "detail/init.h" +#include "detail/typeid.h" #include #include @@ -1569,6 +1570,23 @@ inline str enum_name(handle arg) { return "???"; } +template +struct enum_value { + static Scalar run(handle arg) { + Type value = pybind11::cast(arg); + return static_cast(value); + } +}; + +template +struct enum_value { + static Scalar run(handle) { + throw pybind11::cast_error( + "Enum for " + type_id() + " is not convertible to " + + type_id()); + } +}; + struct enum_base { enum_base(handle base, handle parent) : m_base(base), m_parent(parent) { } @@ -1728,8 +1746,14 @@ template class enum_ : public class_ { : class_(scope, name, extra...), m_base(*this, scope) { constexpr bool is_arithmetic = detail::any_of...>::value; constexpr bool is_convertible = std::is_convertible::value; + auto property = handle((PyObject *) &PyProperty_Type); m_base.init(is_arithmetic, is_convertible); + attr("value") = property( + cpp_function( + &detail::enum_value::run, + pybind11::name("value"), is_method(*this))); + def(init([](Scalar i) { return static_cast(i); }), arg("value")); def("__int__", [](Type value) { return (Scalar) value; }); #if PY_MAJOR_VERSION < 3 diff --git a/tests/test_enum.py b/tests/test_enum.py index f3cce8bce5..e9732fa74f 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -13,15 +13,24 @@ def test_unscoped_enum(): # name property assert m.UnscopedEnum.EOne.name == "EOne" + assert m.UnscopedEnum.EOne.value == 1 assert m.UnscopedEnum.ETwo.name == "ETwo" - assert m.EOne.name == "EOne" - # name readonly + assert m.UnscopedEnum.ETwo.value == 2 + assert m.EOne is m.UnscopedEnum.EOne + # name, value readonly with pytest.raises(AttributeError): m.UnscopedEnum.EOne.name = "" - # name returns a copy - foo = m.UnscopedEnum.EOne.name - foo = "bar" + with pytest.raises(AttributeError): + m.UnscopedEnum.EOne.value = 10 + # name, value returns a copy + # TODO: Neither the name nor value tests actually check against aliasing. + # Use a mutable type that has reference semantics. + nonaliased_name = m.UnscopedEnum.EOne.name + nonaliased_name = "bar" # noqa: F841 assert m.UnscopedEnum.EOne.name == "EOne" + nonaliased_value = m.UnscopedEnum.EOne.value + nonaliased_value = 10 # noqa: F841 + assert m.UnscopedEnum.EOne.value == 1 # __members__ property assert m.UnscopedEnum.__members__ == { @@ -33,8 +42,8 @@ def test_unscoped_enum(): with pytest.raises(AttributeError): m.UnscopedEnum.__members__ = {} # __members__ returns a copy - foo = m.UnscopedEnum.__members__ - foo["bar"] = "baz" + nonaliased_members = m.UnscopedEnum.__members__ + nonaliased_members["bar"] = "baz" assert m.UnscopedEnum.__members__ == { "EOne": m.UnscopedEnum.EOne, "ETwo": m.UnscopedEnum.ETwo, From f3f32b6c142cb84b3041de0e9950141a32afecd4 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Tue, 22 Dec 2020 19:27:42 -0600 Subject: [PATCH 2/4] simplify --- include/pybind11/pybind11.h | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index efc1436aef..1fd2642d0e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -46,7 +46,6 @@ #include "options.h" #include "detail/class.h" #include "detail/init.h" -#include "detail/typeid.h" #include #include @@ -1570,23 +1569,6 @@ inline str enum_name(handle arg) { return "???"; } -template -struct enum_value { - static Scalar run(handle arg) { - Type value = pybind11::cast(arg); - return static_cast(value); - } -}; - -template -struct enum_value { - static Scalar run(handle) { - throw pybind11::cast_error( - "Enum for " + type_id() + " is not convertible to " + - type_id()); - } -}; - struct enum_base { enum_base(handle base, handle parent) : m_base(base), m_parent(parent) { } @@ -1737,6 +1719,7 @@ template class enum_ : public class_ { using Base = class_; using Base::def; using Base::attr; + using Base::def_property; using Base::def_property_readonly; using Base::def_property_readonly_static; using Scalar = typename std::underlying_type::type; @@ -1746,15 +1729,10 @@ template class enum_ : public class_ { : class_(scope, name, extra...), m_base(*this, scope) { constexpr bool is_arithmetic = detail::any_of...>::value; constexpr bool is_convertible = std::is_convertible::value; - auto property = handle((PyObject *) &PyProperty_Type); m_base.init(is_arithmetic, is_convertible); - attr("value") = property( - cpp_function( - &detail::enum_value::run, - pybind11::name("value"), is_method(*this))); - def(init([](Scalar i) { return static_cast(i); }), arg("value")); + def_property("value", [](Type value) { return (Scalar) value; }, nullptr); def("__int__", [](Type value) { return (Scalar) value; }); #if PY_MAJOR_VERSION < 3 def("__long__", [](Type value) { return (Scalar) value; }); From c3696b98074aa06ad97ec6dc326bbf0e7f51a3b0 Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Wed, 23 Dec 2020 16:31:59 -0600 Subject: [PATCH 3/4] address review --- include/pybind11/pybind11.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 1fd2642d0e..a75424679e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1719,7 +1719,6 @@ template class enum_ : public class_ { using Base = class_; using Base::def; using Base::attr; - using Base::def_property; using Base::def_property_readonly; using Base::def_property_readonly_static; using Scalar = typename std::underlying_type::type; @@ -1732,7 +1731,7 @@ template class enum_ : public class_ { m_base.init(is_arithmetic, is_convertible); def(init([](Scalar i) { return static_cast(i); }), arg("value")); - def_property("value", [](Type value) { return (Scalar) value; }, nullptr); + def_property_readonly("value", [](Type value) { return (Scalar) value; }); def("__int__", [](Type value) { return (Scalar) value; }); #if PY_MAJOR_VERSION < 3 def("__long__", [](Type value) { return (Scalar) value; }); From 3bdf70c2f6de9c11cb15b2e072b1e62074db394d Mon Sep 17 00:00:00 2001 From: Eric Cousineau Date: Thu, 24 Dec 2020 14:31:32 -0600 Subject: [PATCH 4/4] enum: Add iteration and simple type-based check --- include/pybind11/pybind11.h | 61 ++++++++++++++++++++++++++++++++++++- tests/test_enum.py | 37 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index a75424679e..90e442acca 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1569,6 +1569,59 @@ inline str enum_name(handle arg) { return "???"; } +class enum_meta_info { +public: + static pybind11::object enum_meta_cls() { + return get().enum_meta_cls_; + } + + static pybind11::object enum_base_cls() { + return get().enum_base_cls_; + } + +private: + template + friend T& pybind11::get_or_create_shared_data(const std::string&); + + static const enum_meta_info& get() { + return pybind11::get_or_create_shared_data( + "_pybind11_enum_meta_info"); + } + + enum_meta_info() { + handle copy = pybind11::module::import("copy").attr("copy"); + locals_ = copy(pybind11::globals()); + locals_["pybind11_meta_cls"] = reinterpret_borrow( + reinterpret_cast(get_internals().default_metaclass)); + locals_["pybind11_base_cls"] = reinterpret_borrow( + get_internals().instance_base); + // TODO: Make the base class work. + const char code[] = R"""( +pybind11_enum_base_cls = None + +class pybind11_enum_meta_cls(pybind11_meta_cls): + is_pybind11_enum = True + + def __iter__(cls): + return iter(cls.__members__.values()) + + def __len__(cls): + return len(cls.__members__) +)"""; + PyObject *result = PyRun_String( + code, Py_file_input, locals_.ptr(), locals_.ptr()); + if (result == nullptr) { + throw error_already_set(); + } + enum_meta_cls_ = locals_["pybind11_enum_meta_cls"]; + enum_base_cls_ = locals_["pybind11_enum_base_cls"]; + } + + pybind11::object enum_meta_cls_; + pybind11::object enum_base_cls_; + pybind11::dict locals_; +}; + struct enum_base { enum_base(handle base, handle parent) : m_base(base), m_parent(parent) { } @@ -1725,7 +1778,13 @@ template class enum_ : public class_ { template enum_(const handle &scope, const char *name, const Extra&... extra) - : class_(scope, name, extra...), m_base(*this, scope) { + : class_( + scope, name, + // Can't re-declare base type??? + // detail::enum_meta_info::enum_base_cls(), + pybind11::metaclass(detail::enum_meta_info::enum_meta_cls()), + extra...), + m_base(*this, scope) { constexpr bool is_arithmetic = detail::any_of...>::value; constexpr bool is_convertible = std::is_convertible::value; m_base.init(is_arithmetic, is_convertible); diff --git a/tests/test_enum.py b/tests/test_enum.py index e9732fa74f..080440b5bf 100644 --- a/tests/test_enum.py +++ b/tests/test_enum.py @@ -1,7 +1,44 @@ # -*- coding: utf-8 -*- import pytest + +import env # noqa: F401 + from pybind11_tests import enums as m +if env.PY2: + enum = None +else: + import enum + + +def is_enum(cls): + """Example showing how to recognize a class as pybind11 enum or a + PEP 345 enum.""" + if enum is not None: + if issubclass(cls, enum.Enum): + return True + return getattr(cls, "is_pybind11_enum", False) + + +def test_pep435(): + # See #2332. + cls = m.UnscopedEnum + names = ("EOne", "ETwo", "EThree") + values = (cls.EOne, cls.ETwo, cls.EThree) + raw_values = (1, 2, 3) + + assert len(cls) == len(names) + assert list(cls) == list(values) + assert is_enum(cls) + if enum: + assert not issubclass(cls, enum.Enum) + for name, value, raw_value in zip(names, values, raw_values): + assert isinstance(value, cls) + if enum: + assert not isinstance(value, enum.Enum) + assert value.name == name + assert value.value == raw_value + def test_unscoped_enum(): assert str(m.UnscopedEnum.EOne) == "UnscopedEnum.EOne"