Uh oh!
There was an error while loading. Please reload this page.
WIP: Adding string constructor for enum - #1122
Conversation
wjakob
commented
Sep 29, 2017
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? |
henryiii
commented
Sep 29, 2017
Oops, sorry, I originally needed to move it to add |
287aa03 to
f73bcd4Comparehenryiii
commented
Sep 29, 2017
Fixed. I've tried several different error types for the throw, but they all get converted to: >m.test_conversion_enum("NotEnum")
ETypeError: test_conversion_enum(): incompatiblefunctionarguments. Thefollowingargumenttypesaresupported:
E1. (arg0: pybind11_tests.enums.ConversionEnum) ->strEEInvokedwith: 'NotEnum' |
c3eeb98 to
b02e440Compare
def(init([this](std::string value) -> Type {
for (constauto &kv : m_entries) {
std::string key = cast<str>(kv.first);
if(value == key || key == m_name + "::" + value) {
return cast<Type>(kv.second);
}
}
throwvalue_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 |
jagerman
commented
Sep 29, 2017
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
8e279acCompareYannickJadoul
commented
Oct 1, 2017
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>
voidmake_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__").templatecast<pybind11::dict>();
auto strValue = pybind11::str(value);
if (values.contains(strValue)) {
returnType(values[strValue].templatecast<Type>());
}
if (ignoreCase) {
auto upperStrValue = strValue.attr("upper")();
for (auto &item : values) {
if (item.first.attr("upper")().attr("__eq__")(upperStrValue).templatecast<bool>()) {
returnType(item.second.templatecast<Type>());
}
}
}
throwpybind11::value_error("\"" + value + "\" is not a valid value for enum type " + enumType.attr("__name__").templatecast<std::string>());
}));
pybind11::implicitly_convertible<std::string, Type>();
}; |
henryiii
commented
Oct 1, 2017
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 constexprbool 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 (~ |
henryiii
commented
Oct 2, 2017
@YannickJadoul , shouldn't .def("func", &func)
.def("func", [](std::string in){... find enumand 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. |
YannickJadoul
commented
Oct 2, 2017
@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 |
henryiii
commented
Oct 2, 2017
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:
|
TD22057
commented
Oct 3, 2017
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 :-) |
| }, 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.
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.
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.
Add a check here against the specific exception message, via:
withpytest.raises(TypeError) asexcinfo:
# ... the test callassertstr(excinfo.value) =="the expected exception message"There was a problem hiding this comment.
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.
Added tests for errors, one that passes (direct construction was okay) and one that fails (implicit construction).
jagerman
commented
Oct 6, 2017
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: importenumclassColor(enum.Enum):
RED=1green=2print(repr(Color["RED"])) print(repr(Color["green"])) print(repr(Color["Green"])) |
jagerman
commented
Oct 6, 2017
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. |
YannickJadoul
commented
Oct 9, 2017
Fair enough, if I'm completely alone in liking the case insensitivity, let's just forget about it :-) |
wjakob
commented
Oct 9, 2017
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 |
henryiii
commented
Oct 10, 2017
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 |
wjakob
commented
Oct 10, 2017
The old version read so it was just using a pointer to the underlying PyObject. |
henryiii
commented
Oct 11, 2017
Doesn't inc_ref add a (leaked) reference? I can change it back tomorrow. |
jagerman
commented
Oct 11, 2017
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.
This test fails, since it gives a generic message and TypeError.
zeroset
commented
Feb 25, 2021
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.