pybind / pybind/pybind11

string to enum conversion regression after enum rewrite?

Open
#2,114 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

Issue description

I have a string-enum converter that works using v2.2.4. This breaks starting from v2.3.0 due to the enum_ rewrite.

In v2.2.4, it was possible to support this conversion via implicitly_convertible. It looks like other people are taking advantage of this feature as well: https://github.com/pybind/pybind11/issues/483 and https://github.com/pybind/pybind11/pull/1122.

From v2.3.0+, it appears that this codepath is not being covered by the new PYBIND11_ENUM_OP_CONV and PYBIND11_ENUM_OP_CONV_LHS implementations. These macros appear to only support integer conversion and fails to invoke the existing pathways enabled through implicitly_convertible.

I am wondering if this is a regression or if only integer conversions will be allowed going forward?

Tested using v2.2.4, v2.3.0, v.2.4.3.

Reproducible example code

// example.cc
#include <pybind11/pybind11.h>                                                  
namespace py = pybind11;                                                        
                                                                                
template <typename T>                                                           
T pyStringToEnum(const py::enum_<T>& enm, const std::string& value) {           
    auto values = enm.attr("__members__").template cast<py::dict>();            
    auto strVal = py::str(value);                                               
    if (values.contains(strVal)) {                                              
        return T(values[strVal].template cast<T>());                            
    }                                                                           
    throw "Invalid string value " + value + " for enum " + std::string(typeid(T).name());
}                                                                               
                                                                                
template <typename T>                                                           
py::str enumToPyString(const py::enum_<T>& enm, const T& value) {               
    auto values = enm.attr("__members__").template cast<py::dict>();            
    for (auto val : values) {                                                   
        if (T(val.second.template cast<T>()) == value) {                        
            return py::str(val.first);                                          
        }                                                                       
    }                                                                           
    throw "Invalid value for enum " + std::string(typeid(T).name());            
}                                                                               
                                                                                
enum class Kind { X = 0, Y = 1, Z = 2 };                                        
struct A {
    A() : kind(Kind::X) {}                                                                      
    Kind kind;                                                                  
};                                                                              
                                                                                
PYBIND11_MODULE(example, m) {                                                   
    py::enum_<Kind> enm(m, "Kind");                                             
    enm                                                                         
        .value("X", Kind::X)                                                    
        .value("Y", Kind::Y)                                                    
        .value("Z", Kind::Z)                                                    
        .def(py::init([enm](const std::string& value) -> Kind {                 
            return pyStringToEnum(enm, py::str(value));                         
        }))                                                                     
        .def("__str__", [enm](Kind e) { return enumToPyString(enm, e); });      
                                                                                
    py::implicitly_convertible<std::string, Kind>();                            
    py::implicitly_convertible<int, Kind>();                                    
                                                                                
    py::class_<A>(m, "A")                                                       
        .def(py::init<>())                                                      
        .def_readwrite("kind", &A::kind);                                       
} 
$ g++ -std=c++11 -fPIC -shared $(python-config --includes) $(python-config --libs) -Ipybind11-${VERSION}/include/ example.cc -o example.so 
# test.py
from example import A                                                              
                                                                                   
a = A()                                                                            
kind = a.kind                                                                      
                                                                                   
s = 'kind == "X"'                                                                  
print '{:15} ->   expected: {:<5} result: {:<5}'.format(s, True, eval(s))          
                                                                                   
s = 'kind == None'                                                                 
print '{:15} ->   expected: {:<5} result: {:<5}'.format(s, False, eval(s))         
                                                                                   
s = 'kind == 0'                                                                    
print '{:15} ->   expected: {:<5} result: {:<5}'.format(s, True, eval(s))          
                                                                                   
s = 'kind == 1'                                                                    
print '{:15} ->   expected: {:<5} result: {:<5}'.format(s, False, eval(s))
# output v2.3.0+
kind == "X"     ->   expected: 1     result: 0    
kind == None    ->   expected: 0     result: 0    
kind == 0       ->   expected: 1     result: 0    
kind == 1       ->   expected: 0     result: 0 
# output v2.2.4
kind == "X"     ->   expected: 1     result: 1    
kind == None    ->   expected: 0     result: 0    
kind == 0       ->   expected: 1     result: 1    
kind == 1       ->   expected: 0     result: 0

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the enum_ rewrite and the PYBIND11_ENUM_OP_CONV and PYBIND11_ENUM_OP_CONV_LHS macros, then run the reproducible example in example.cc with test.py against the reported versions. Compare string conversion through implicitly_convertible with the v2.2.4 and v2.3.0+ outputs; done means the intended string-enum behavior is established and the regression or supported limitation is covered.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.