Support enums as a first-class entity
Problem
There is no way to wrap a plain C++ enum. The only enums that reach Python are those that happen to be the sole member of a struct, and even then only as a side effect of the class writer.
Naming a bare enum in config.yaml is the natural thing to try, and it fails:
classes:
- name: SemLatticeType # enum
source_file: SemEnumerations.hpp
pygccxml.declarations.runtime_errors.declaration_not_found_t: Unable to find declaration.
Matcher: [(decl type==class_t) and (name==SemLatticeType)]
pygccxml.declarations.runtime_errors.declaration_not_found_t: Unable to find declaration.
Matcher: [(decl type==typedef_t) and (name==SemLatticeType)]
for
enum SemLatticeType : unsigned
{
SEM_LATTICE_CUBIC,
SEM_LATTICE_CLOSE_PACKED
};
Version 0.4.1 (1244f97).
Why it fails
Three things line up:
- A module config accepts only
classes, free_functions and variables (parsers/package_info_parser.py:107-119), so an enum has to be declared as a class.
CppClassInfo.update_decls() (info/class_info.py:174-188) resolves that name with source_ns.class_(), falling back to source_ns.typedef(). pygccxml exposes enums as a separate enumeration_t, and neither query matches one. parsers/source_parser.py:12-22 lists the declaration types cppwg handles, and enumeration_t is not among them.
- The struct-enum handling in
writers/class_writer.py:276-296 is a special case inside the class writer: having found a class, it notices the class is a struct containing exactly one enum and emits py::enum_ rather than the usual class binding.
if type_traits_classes.is_struct(class_decl):
enums = class_decl.enumerations(allow_empty=True)
if len(enums) == 1:
...
So an enum is only reachable when nested inside something cppwg can already find. This is not a pybind11 limitation: py::enum_ works perfectly well at module scope.
Consequence for callers
The workaround is to restructure the C++ purely to satisfy the generator:
struct SemLatticeType
{
enum Value : unsigned
{
SEM_LATTICE_CUBIC,
SEM_LATTICE_CLOSE_PACKED
};
};
Every use site then has to change: the type spelling becomes SemLatticeType::Value, and the values become SemLatticeType::SEM_LATTICE_CUBIC. In Chaste this was ~50 sites across 9 files, none of which improved the C++ (see Chaste/Chaste#111). RelativeTo in global/src/FileFinder.hpp has the same shape for the same reason.
Nor is skipping the config entry an option when the enum appears in a wrapped signature. writers/constructor_writer.py:180-222 emits default arguments as
py::arg("nodeLattice") = SemLatticeType::SEM_LATTICE_CUBIC
and pybind11 converts default arguments to Python objects at registration time, so an unregistered type raises on module import rather than at call time. Any enum used as a parameter type must be registered.
Proposal
Treat enums like free functions and variables, which already have this shape:
- an
enums: key in the module config, plus use_all_enums, alongside the existing three (parsers/package_info_parser.py:107-119)
info/enum_info.py, mirroring info/variable_info.py, resolving via source_ns.enumeration(name) — scopedef_t.enumerations() is already used at class_writer.py:281, so the query is available
writers/enum_writer.py, mirroring writers/free_function_writer.py, emitting
py::enum_<Foo>(m, "Foo")
.value("A", Foo::A)
.export_values();
- registration into
module_writer.py next to the free-function and variable writers
Scoped enums (enum class) should work through the same path; .export_values() applies only to unscoped ones.
The existing struct-enum special case can stay as it is, so nothing currently wrapped changes behaviour. It would be worth a note in the README that a plain enum is now the recommended form and the struct wrapper is legacy.
Test case
enum Foo { A, B }; at namespace scope, wrapped via an enums: entry, callable from Python as Foo.A, and usable as a default argument of a wrapped function without raising at import.
Support enums as a first-class entity
Problem
There is no way to wrap a plain C++ enum. The only enums that reach Python are those that happen to be the sole member of a struct, and even then only as a side effect of the class writer.
Naming a bare enum in
config.yamlis the natural thing to try, and it fails:for
Version 0.4.1 (
1244f97).Why it fails
Three things line up:
classes,free_functionsandvariables(parsers/package_info_parser.py:107-119), so an enum has to be declared as a class.CppClassInfo.update_decls()(info/class_info.py:174-188) resolves that name withsource_ns.class_(), falling back tosource_ns.typedef(). pygccxml exposes enums as a separateenumeration_t, and neither query matches one.parsers/source_parser.py:12-22lists the declaration types cppwg handles, andenumeration_tis not among them.writers/class_writer.py:276-296is a special case inside the class writer: having found a class, it notices the class is a struct containing exactly one enum and emitspy::enum_rather than the usual class binding.So an enum is only reachable when nested inside something cppwg can already find. This is not a pybind11 limitation:
py::enum_works perfectly well at module scope.Consequence for callers
The workaround is to restructure the C++ purely to satisfy the generator:
Every use site then has to change: the type spelling becomes
SemLatticeType::Value, and the values becomeSemLatticeType::SEM_LATTICE_CUBIC. In Chaste this was ~50 sites across 9 files, none of which improved the C++ (see Chaste/Chaste#111).RelativeToinglobal/src/FileFinder.hpphas the same shape for the same reason.Nor is skipping the config entry an option when the enum appears in a wrapped signature.
writers/constructor_writer.py:180-222emits default arguments asand pybind11 converts default arguments to Python objects at registration time, so an unregistered type raises on module import rather than at call time. Any enum used as a parameter type must be registered.
Proposal
Treat enums like free functions and variables, which already have this shape:
enums:key in the module config, plususe_all_enums, alongside the existing three (parsers/package_info_parser.py:107-119)info/enum_info.py, mirroringinfo/variable_info.py, resolving viasource_ns.enumeration(name)—scopedef_t.enumerations()is already used atclass_writer.py:281, so the query is availablewriters/enum_writer.py, mirroringwriters/free_function_writer.py, emittingmodule_writer.pynext to the free-function and variable writersScoped enums (
enum class) should work through the same path;.export_values()applies only to unscoped ones.The existing struct-enum special case can stay as it is, so nothing currently wrapped changes behaviour. It would be worth a note in the README that a plain enum is now the recommended form and the struct wrapper is legacy.
Test case
enum Foo { A, B };at namespace scope, wrapped via anenums:entry, callable from Python asFoo.A, and usable as a default argument of a wrapped function without raising at import.