-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Adding string constructor for enum #1122
base: master
Are you sure you want to change the base?
Conversation
It's hard to read the diff because the code was moved in the file -- could you fix that so that it's easier to review the PR? |
Oops, sorry, I originally needed to move it to add |
287aa03
to
f73bcd4
Compare
Fixed. I've tried several different error types for the throw, but they all get converted to: > m.test_conversion_enum("NotEnum")
E TypeError: test_conversion_enum(): incompatible function arguments. The following argument types are supported:
E 1. (arg0: pybind11_tests.enums.ConversionEnum) -> str
E
E Invoked with: 'NotEnum' |
c3eeb98
to
b02e440
Compare
def(init([this](std::string value) -> Type {
for (const auto &kv : m_entries) {
std::string key = cast<str>(kv.first);
if(value == key || key == m_name + "::" + value) {
return cast<Type>(kv.second);
}
}
throw value_error("\"" + value + "\" is not a valid value for enum type " + m_name);
})); But still get the error on GCC Py2.7 when I run: python -c "import pybind11_tests as m; print(m.enums.ConversionEnum('Convert1'))" Newer compilers and Pythons seem to be happier. Most irritatingly, LLVM on macOS is happy, so I can't easily attach to python and debug in Xcode. Edit: fixed. It was from using |
904781f
to
7248aed
Compare
I haven't reviewed the PR in detail yet (I'm tied up with other things at the moment--and anyway I'll wait for you to finish it off before I do), but as a first observation I think using One observation: I think you can use |
44b2da3
to
8e279ac
Compare
Suggestion: I already had added something like this in my project. There's however 2 slight differences that might be interesting for this PR:
Especially, if this For reference, this is how my version looks: (Surely, it's not better, but it might contain some interesting things?) template <typename Type>
void make_implicitly_convertible_from_string(pybind11::enum_<Type> &enumType, bool ignoreCase=false)
{
enumType.def(pybind11::init([enumType, ignoreCase](const std::string &value)
{
auto values = enumType.attr("__members__").template cast<pybind11::dict>();
auto strValue = pybind11::str(value);
if (values.contains(strValue)) {
return Type(values[strValue].template cast<Type>());
}
if (ignoreCase) {
auto upperStrValue = strValue.attr("upper")();
for (auto &item : values) {
if (item.first.attr("upper")().attr("__eq__")(upperStrValue).template cast<bool>()) {
return Type(item.second.template cast<Type>());
}
}
}
throw pybind11::value_error("\"" + value + "\" is not a valid value for enum type " + enumType.attr("__name__").template cast<std::string>());
}));
pybind11::implicitly_convertible<std::string, Type>();
}; |
I don't really like the idea of adding case insensitivity to just the string constructor; shouldn't However, to add that as a feature, a new constexpr bool is_case_insensitive = detail::any_of<std::is_same<case_insensitive, Extra>...>::value;
// ... later ...
std::string key = pybind11::cast<pybind11::str>(is_case_insensitive ? kv.first.attr("upper") : kv.first);
if(is_case_insensitive) {
value = pybind11::cast<pybind11::str>(pybind11::str(value).attr("upper")());
} Eventually, one could also add a |
@henryiii Nono, agreed that that's not a good default! But I'm just thinking of this file extension enum I have: all values are capitalized (according to the Python code conventions), but writing a all-lowercase version seems very natural. So for example a flag was what I was thinking of, indeed :-)
That's the problem. I don't think you could do that easily, could you? Since the non-case-sensitive overload is already there and will already accept all calls with a single string argument? (But of course, if no one else would think this feature is worth it, I could find some way around it, or consider dropping my support for it...) By the way: for the case-sensitive implementation you have now, do you need this loop? Wouldn't just accessing the dictionary with this key (~ |
@YannickJadoul , shouldn't .def("func", &func)
.def("func", [](std::string in){... find enum and call func ...}); However, "faking it" by adding a custom string converter for the enum was a nice trick to shorten the syntax, and that seems to be lost with this PR. The problem is that adding a new constructor doesn't precede the old one. Shouldn't the functions be traversed in reverse order, to allow new functions to override previously added ones? Save for a better fix, a tag could be added to allow the string init to be turned on or off. |
@henryiii Yeah, you could be right. That was just an example, of course. I do still think that case-insensitivity could be interesting (99% of all enums do not depend on case sensitivity, I'd guess), in some cases, but ... maybe it's too specific for a generic constructor, yes.
Most of the time, you'd all |
How about the flag idea for case-insensitivity? py::enum_< MyEnum >(m, "MyEnum", py::case_insensitive())
.def("A")
.def("B")
}; Or would it be better to go back to the separate method: py::enum_< MyEnum >(m, "MyEnum")
.def("A")
.def("B")
.add_string_constructor()
}; The later would allow you to add your own string constructor instead. |
Hmm, I like both. I definitely like the flag thing; that would be perfect :-) (I also have a boolean in my quick code) For other users, I don't know; maybe the Another option still would be (EDIT: Thanks for considering, btw! Again, not claiming it nééds to be in there, but maybe others will like the feature, so since you're making a PR anyway, I could as well bring it up.) |
Too many choices! Someone official please inform or vote:
|
I definitely need this for my project so thank you very much. For us, case insensitivity isn't important - to my thinking it's a bit strange in that it's easy to see how "FOO" maps to Enum::FOO but not "fOo" but that's just me. I'm not sure what "doesn't allow new string constructors" means in choice 1) so I wouldn't know how to vote - other than yes, I want this :-) |
@@ -1377,6 +1377,14 @@ template <typename Type> class enum_ : public class_<Type> { | |||
return m; | |||
}, return_value_policy::copy); | |||
def(init([](Scalar i) { return static_cast<Type>(i); })); | |||
def(init([name, m_entries_ptr](std::string value) -> Type { | |||
pybind11::dict values = reinterpret_borrow<pybind11::dict>(m_entries_ptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't your code, but let's clean it up anyway: instead of auto m_entries_ptr = m_entries.inc_ref().ptr();
being a raw, incremented pointer, we can change it to: auto entries = m_entries;
. Then all the lambdas (this, plus the others that do a reinterpret_borrow
) can just capture entries
and can simplify their reinterpret_borrow<dict>(m_entries_ptr)
to just entries
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, much nicer the new way. Thanks!
def test_scoped_enum(): | ||
assert m.test_scoped_enum(m.ScopedEnum.Three) == "ScopedEnum::Three" | ||
with pytest.raises(TypeError): | ||
m.test_scoped_enum("Three") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a check here against the specific exception message, via:
with pytest.raises(TypeError) as excinfo:
# ... the test call
assert str(excinfo.value) == "the expected exception message"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with that is this gives the wrong error message. I need help figuring out why the error message is being swallowed. I've tried stepping through it in Xcode, but finding where exceptions (at least this one) is being caught is tricky. I think it's inside a constructor/destructor somewhere. I think that directly calling the constructor works, but not if it's part of an implicit conversion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests for errors, one that passes (direct construction was okay) and one that fails (implicit construction).
I don't think case insensitivity is worth supporting or worrying about. It obviously isn't supported in C++, but neither is it supported by Python's (3.4+) enums string conversion: import enum
class Color(enum.Enum):
RED = 1
green = 2
print(repr(Color["RED"]))
print(repr(Color["green"]))
print(repr(Color["Green"]))
|
You can define multiple constructors for an object in pybind; they are called in order of definition, and the first for which the given set of arguments can be converted from Python values to match the C++ argument types wins. What @henryiii means is that without his addition, you could always add an extra string-taking constructor that does case-insensitive (or any other sort of arbitrary lookup based on an input string), but if the core functionality unconditionally adds a string-taking constructor, that won't be possible anymore--the core constructor will always be checked first. If this is really important to someone, I'd rather see a more general ability to inject a method definition at the beginning of the overload chain (e.g. |
Fair enough, if I'm completely alone in liking the case insensitivity, let's just forget about it :-) |
An aside that is not immediately related to the change here: I'm wondering if it would make sense to create an abstract pybind11-internal type which is a base class of all enumerations. Many of the functions declared over and over again for each enumeration aren't really specific to that enumeration or could be implemented in a generic way, reducing object code bloat. Projects that use many enumerations have hundreds of redundant versions of these functions, which is a concern. PS: While looking at the patch, it wasn't clear to me why the |
I improved the line I think you are talking about: auto entries = m_entries; a bit: dict& entries = m_entries; But functions that capture still make a copy of the |
The old version read
so it was just using a pointer to the underlying PyObject. |
Doesn't inc_ref add a (leaked) reference? I can change it back tomorrow. |
Yes; but since we don't support type destruction, every type is essentially a leaked reference already. It's not all that "clean", but as long as types aren't unloadable, it's not actually creating any new leak. |
I've reverted the change, a |
def test_conversion_enum_raises_implicit(): | ||
with pytest.raises(ValueError) as excinfo: | ||
m.test_conversion_enum("Convert0") | ||
assert str(excinfo.value) == "\"Convert0\" is not a valid value for enum type ConversionEnum" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test fails, since it gives a generic message and TypeError
.
What is the status of this PR? Was the feature implemented somewhere else already? |
Er, not sure if this approach is "Pythonic" with stdlib Main concern here is the onus of maintaining this functionality if/when we're able to resolve #2332 to some extent, either by replication or as @wjakob mentioned above (comment) via actually using |
This adds the ability to access an enum using a string. So the following works now:
Besides making it a bit easier to access enum values using a variable (vs.
getattr
), this also fixes #483 in a general way (and was based on comments there, combined with newer procedures). Assuming you have a C++ enum and function like this:You can now add to the bindings:
And then this works:
That reuses and elevates a nice feature (
implicitly_convertible
), rather than adding something new toenum_
's syntax. If this looks good, I'll add some docs and remove the WIP flag. The issue pointed out in the PR, which is that the error is swallowed up and converted to a less-useful one also needs to be addressed.