Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fb3c163
#113 Support enums as a first-class entity
kwabenantim Aug 3, 2026
108305b
#113 Don't report `enum class` declarations as unknown classes
kwabenantim Aug 3, 2026
b9cf2f6
#113 Demonstrate enum wrapping in the shapes example
kwabenantim Aug 3, 2026
bfae486
#113 Only emit .export_values() for unscoped enums
kwabenantim Aug 3, 2026
4579d53
#113 Add an inheritable export_values config option for enums
kwabenantim Aug 3, 2026
09b73a2
#113 Use a non-reserved include guard in ShapeKind.hpp
kwabenantim Aug 3, 2026
2992181
#113 Use non-reserved include guards in the example headers
kwabenantim Aug 3, 2026
b5e9c65
#113 Refresh enum docs and comments
kwabenantim Aug 3, 2026
fed0ec2
#113 Cover enum info update_from_ns and the enum header include
kwabenantim Aug 3, 2026
91e0194
#113 Cover use_all_enums discovery and the pathless-enum include branch
kwabenantim Aug 3, 2026
4606811
#113 Clarify source_file vs source_file_path in the reference
kwabenantim Aug 3, 2026
e051181
#113 Allow source_file for free functions and enums
kwabenantim Aug 3, 2026
5e4d305
#113 Test that discovered enums inherit export_values
kwabenantim Aug 3, 2026
7c3f744
#113 Use the add_* helpers in the discovery loops
kwabenantim Aug 3, 2026
a8ea39e
#113 Correct the not-found hint for enums and free functions
kwabenantim Aug 3, 2026
fdfa131
#113 Skip class-nested enums in use_all_enums discovery
kwabenantim Aug 3, 2026
75f1d2a
#113 Document that struct-wrapped enums go under classes
kwabenantim Aug 3, 2026
dfeffa1
#113 Replace Chaste-specific examples with cppwg ones
kwabenantim Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cppwg/info/base_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ class BaseInfo(ABC):
----------
arg_type_excludes : list[str]
Exclude any method, constructor or free function with an argument of one
of these types. Patterns match a type as a whole token (so `Node` does
not match `AbstractNode`).
of these types. Patterns match a type as a whole token (so `Shape` does
not match `AbstractShape`).
auto_includes : bool | None
Automatically add `#include`s for the project types a class's wrapped
method/constructor signatures use, when the class's own header only
Expand Down Expand Up @@ -136,6 +136,11 @@ def __init__(self, name: str, info_config: dict[str, Any] | None = None) -> None
self.excluded_methods: list[str] = []
self.excluded_variables: list[str] = []
self.return_type_excludes: list[str] = []
# Tri-state (None inherits): whether a wrapped enum exports its
# enumerators into the module scope (pybind11's .export_values()). Only
# meaningful for enums, but inheritable so a package/module setting
# applies to all enums below it. See CppEnumInfo.should_export_values.
self.export_values: bool | None = None

# Pointers
self.pointer_call_policy: str = ""
Expand Down Expand Up @@ -181,6 +186,7 @@ def __init__(self, name: str, info_config: dict[str, Any] | None = None) -> None
"excluded",
"excluded_methods",
"excluded_variables",
"export_values",
"name_replacements",
"pointer_call_policy",
"prefix_code",
Expand Down
78 changes: 78 additions & 0 deletions cppwg/info/enum_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Enum information structure."""

import logging
from typing import TYPE_CHECKING, Any

from cppwg.info.cpp_entity_info import CppEntityInfo
from cppwg.utils import utils

if TYPE_CHECKING:
from pygccxml.declarations.namespace import namespace_t


class CppEnumInfo(CppEntityInfo):
"""
An information structure for individual enums to be wrapped.

Attributes
----------
scoped : bool
Whether the enum is a scoped enum (`enum class`/`enum struct`). Scoped
enums do not export their enumerators into the enclosing scope, so
pybind11's `.export_values()` is omitted for them by default.
Note
----
The inheritable `export_values` config option (defined on BaseInfo) overrides
whether `.export_values()` is emitted; see should_export_values.
"""

def __init__(self, name: str, enum_config: dict[str, Any] | None = None):
super().__init__(name, enum_config)

self.scoped: bool = False

def should_export_values(self) -> bool:
"""
Return whether to emit pybind11's `.export_values()` for this enum.

The `export_values` config option wins if set at this enum or anywhere up
the info tree (package/module); otherwise mirror the C++ enum kind -
export for an unscoped enum, not for a scoped one.
"""
override = self.hierarchy_attribute("export_values")
if override is not None:
return override
return not self.scoped

def update_from_ns(self, source_ns: "namespace_t") -> None:
"""
Update with information from the source namespace.

Adds the enum declaration and records whether the enum is scoped.

Parameters
----------
source_ns : pygccxml.declarations.namespace_t
The source namespace
"""
enum_decls = source_ns.enumerations(self.name, allow_empty=True)

if not enum_decls:
# The enum's header was not parsed. For an explicitly listed enum,
# set source_file or source_file_path so its header is included -
# unless it is already pulled in by another wrapped entity declared
# in the same header.
logger = logging.getLogger()
logger.error(
f"Could not find enum {self.name}. Set source_file or "
"source_file_path in the config so that its header is included."
)
raise RuntimeError(f"Could not find enum: {self.name}")

self.decls = [enum_decls[0]]

# pygccxml does not expose enum scopedness, so read it from the source
# file the enum was declared in (via the resolved decl's location).
self.scoped = utils.is_scoped_enum_in_source_file(
self.decls[0].location.file_name, self.decls[0].name
)
11 changes: 6 additions & 5 deletions cppwg/info/free_function_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ def update_from_ns(self, source_ns: "namespace_t") -> None:
ff_decls = source_ns.free_functions(self.name, allow_empty=True)

if not ff_decls:
# The function's header was not parsed. For explicitly listed free
# functions, the header is only included when source_file_path is
# set in the config.
# The function's header was not parsed. For an explicitly listed free
# function, set source_file or source_file_path so its header is
# included - unless it is already pulled in by another wrapped entity
# declared in the same header.
logger = logging.getLogger()
logger.error(
f"Could not find free function {self.name}. Set source_file_path "
"in the config so that its header is included."
f"Could not find free function {self.name}. Set source_file or "
"source_file_path in the config so that its header is included."
)
raise RuntimeError(f"Could not find free function: {self.name}")

Expand Down
47 changes: 42 additions & 5 deletions cppwg/info/module_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from pygccxml import declarations

from cppwg.info.base_info import BaseInfo
from cppwg.info.class_info import CppClassInfo
from cppwg.info.enum_info import CppEnumInfo
from cppwg.info.free_function_info import CppFreeFunctionInfo
from cppwg.utils import utils

Expand All @@ -31,7 +34,7 @@ class ModuleInfo(BaseInfo):
base here is required (and is the only way) to inherit from an
externally-package-wrapped base, so that cppwg never emits a base class
it cannot confirm is registered. Names are matched without template
arguments, e.g. `AbstractForce` matches `AbstractForce<2, 2>`.
arguments, e.g. `AbstractSphericalMesh` matches `AbstractSphericalMesh<2, 2>`.
imports : list[str]
Python modules to import at the start of this generated module, e.g. the
compiled module of another package or sibling module whose classes are
Expand All @@ -50,6 +53,8 @@ class wrappers (see CppClassWrapperWriter), so that a class in this module
Use all free functions in the module
use_all_variables : bool
Use all variables in the module
use_all_enums : bool
Use all enums in the module

package_info : PackageInfo
The package info object this module belongs to
Expand All @@ -60,6 +65,8 @@ class wrappers (see CppClassWrapperWriter), so that a class in this module
A list of free function info objects that belong to this module
variable_collection : list[CppVariableInfo]
A list of variable info objects that belong to this module
enum_collection : list[CppEnumInfo]
A list of enum info objects that belong to this module
"""

def __init__(self, name: str, module_config: dict[str, Any] | None = None) -> None:
Expand All @@ -81,12 +88,14 @@ def __init__(self, name: str, module_config: dict[str, Any] | None = None) -> No
self.use_all_classes: bool = False
self.use_all_free_functions: bool = False
self.use_all_variables: bool = False
self.use_all_enums: bool = False

self.package_info: "PackageInfo | None" = None

self.class_collection: list[CppClassInfo] = []
self.free_function_collection: list[CppFreeFunctionInfo] = []
self.variable_collection: list["CppVariableInfo"] = []
self.enum_collection: list[CppEnumInfo] = []

if module_config:
for key in [
Expand All @@ -96,6 +105,7 @@ def __init__(self, name: str, module_config: dict[str, Any] | None = None) -> No
"use_all_classes",
"use_all_free_functions",
"use_all_variables",
"use_all_enums",
]:
if key in module_config:
setattr(self, key, module_config[key])
Expand Down Expand Up @@ -135,6 +145,13 @@ def add_variable(self, variable_info: "CppVariableInfo") -> None:
self.variable_collection.append(variable_info)
variable_info.parent = self

def add_enum(self, enum_info: CppEnumInfo) -> None:
"""
Add an enum info object to the module.
"""
self.enum_collection.append(enum_info)
enum_info.parent = self

def is_decl_in_source_path(self, decl: "declaration_t") -> bool:
"""
Check if the declaration is associated with a file in the specified source paths.
Expand Down Expand Up @@ -248,8 +265,7 @@ def update_from_ns(self, source_ns: "namespace_t") -> None:
if self.is_decl_in_source_path(class_decl):
class_info = CppClassInfo(class_decl.name)
class_info.update_names()
class_info.module_info = self
self.class_collection.append(class_info)
self.add_class(class_info)

# Update classes with information from source namespace.
for class_info in self.class_collection:
Expand All @@ -266,13 +282,33 @@ def update_from_ns(self, source_ns: "namespace_t") -> None:
for free_function in free_functions:
if self.is_decl_in_source_path(free_function):
ff_info = CppFreeFunctionInfo(free_function.name)
ff_info.module_info = self
self.free_function_collection.append(ff_info)
self.add_free_function(ff_info)

# Update free functions with information from source namespace.
for ff_info in self.free_function_collection:
ff_info.update_from_ns(source_ns)

# Add discovered enums: if `use_all_enums` is True, this module has no
# enum info objects. Use enum decls from the source namespace to create
# enum info objects.
if self.use_all_enums:
enum_decls = source_ns.enumerations(allow_empty=True)
for enum_decl in enum_decls:
# Skip enums nested in a class/struct: only namespace-scope enums
# are wrapped standalone. A nested enum (e.g. `Value` in a
# `struct Foo { enum Value {...}; }`) would be emitted with an
# unqualified name that does not compile; the struct-enum special
# case handles the wrapped-struct pattern.
if declarations.is_class(enum_decl.parent):
continue
if self.is_decl_in_source_path(enum_decl):
enum_info = CppEnumInfo(enum_decl.name)
self.add_enum(enum_info)

# Update enums with information from source namespace.
for enum_info in self.enum_collection:
enum_info.update_from_ns(source_ns)

def update_from_source(self, source_file_paths: list[str]) -> None:
"""
Update module with information from the source headers.
Expand All @@ -287,3 +323,4 @@ def update_from_source(self, source_file_paths: list[str]) -> None:

self.class_collection.sort(key=lambda x: x.name)
self.free_function_collection.sort(key=lambda x: x.name)
self.enum_collection.sort(key=lambda x: x.name)
2 changes: 1 addition & 1 deletion cppwg/info/package_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def discover_base_class_instantiations(self, source_ns: "namespace_t") -> None:
A harvested arg list is adopted only when its length matches the class's
template parameter count. This guards against a CastXML version that
collapses a defaulted trailing argument (naming e.g.
``AbstractLinearPde<2, 2>`` as ``AbstractLinearPde<2>``): the collapsed
``MacroMesh<2, 2>`` as ``MacroMesh<2>``): the collapsed
list is rejected and the class is left for template_substitutions.

Parameters
Expand Down
41 changes: 41 additions & 0 deletions cppwg/parsers/package_info_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import yaml

from cppwg.info.class_info import CppClassInfo
from cppwg.info.enum_info import CppEnumInfo
from cppwg.info.free_function_info import CppFreeFunctionInfo
from cppwg.info.module_info import ModuleInfo
from cppwg.info.package_info import PackageInfo
Expand Down Expand Up @@ -68,6 +69,7 @@ def parse(self) -> PackageInfo:
"excluded": False,
"excluded_methods": [],
"excluded_variables": [],
"export_values": None,
"pointer_call_policy": "",
"prefix_code": [],
"prefix_text": "",
Expand Down Expand Up @@ -126,9 +128,11 @@ def parse(self) -> PackageInfo:
"use_all_classes": False,
"use_all_free_functions": False,
"use_all_variables": False,
"use_all_enums": False,
"classes": [],
"free_functions": [],
"variables": [],
"enums": [],
}
module_config.update(base_config)

Expand Down Expand Up @@ -160,6 +164,10 @@ def parse(self) -> PackageInfo:
module_config["variables"]
)

module_config["use_all_enums"] = utils.is_option_ALL(
module_config["enums"]
)

# Create the ModuleInfo object from the module config dict
module_info = ModuleInfo(module_config["name"], module_config)

Expand Down Expand Up @@ -268,6 +276,39 @@ def parse(self) -> PackageInfo:
# Add the variable to the module
module_info.add_variable(variable_info)

# Parse the enum data and create enum info objects.
# Note: if module_config["use_all_enums"] == True, enum info objects
# will be added later after parsing the C++ source code.
if not module_config["use_all_enums"]:
if module_config["enums"]:
for raw_enum_info in module_config["enums"]:
# Get enum config from the raw enum info
enum_config = {
"name_override": "",
"source_file": "",
"source_file_path": "",
}
enum_config.update(base_config)

for key in enum_config.keys():
if key in raw_enum_info:
enum_config[key] = raw_enum_info[key]

# Convert source file path to a full path
enum_config["source_file_path"] = self.full_path(
enum_config["source_file_path"]
)
self.verify_path(enum_config["source_file_path"])

# Convert custom generator path to a full path
self.convert_custom_generator(enum_config)

# Create the CppEnumInfo object from the enum config dict
enum_info = CppEnumInfo(raw_enum_info["name"], enum_config)

# Add the enum to the module
module_info.add_enum(enum_info)

return package_info

# Deprecated config option -> replacement guidance shown in the warning.
Expand Down
14 changes: 14 additions & 0 deletions cppwg/templates/pybind11_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"{\n"
"${imports}"
"${exception_translator}"
"${enums}"
"${free_functions}"
"${register_calls}"
"${module_code}"
Expand Down Expand Up @@ -174,6 +175,18 @@
"}\n"
)

# Skeleton for a plain (namespace-scope) enum, registered directly against the
# module m. Emitted inline in the module main cpp (like a free function), not as a
# separate register_..._class function. ${enum_values} is one .value(...) line per
# enumerator. ${enum_terminator} closes the chain: `.export_values();` for an
# unscoped enum (exports enumerators into the enclosing scope) or just `;` for a
# scoped enum (see CppEnumWrapperWriter).
enum_register = Template(
' py::enum_<${enum_cpp_name}>(m, "${enum_py_name}")\n'
"${enum_values}"
"${enum_terminator}\n"
)
Comment thread
kwabenantim marked this conversation as resolved.

# Skeleton for the header collection hpp file, which includes every header to be
# parsed by CastXML plus the explicit template instantiations and typedefs
# (e.g. typedef Foo<2,2> Foo_2_2) for all classes to be wrapped.
Expand Down Expand Up @@ -207,6 +220,7 @@
"class_cpp_header": class_cpp_header,
"class_cpp_register": class_cpp_register,
"struct_enum_register": struct_enum_register,
"enum_register": enum_register,
"free_function": free_function,
"class_method": class_method,
"class_constructor": class_constructor,
Expand Down
Loading
Loading