Wrap structs - #117
Conversation
A struct that was not the single-nested-enum special case was silently dropped by the class writer (no wrapper file), yet the module writer still emitted its #include and register_..._class call, producing an opaque missing-header compile error (or the unlinkable-struct mismatch). Route any non-single-enum struct through the normal py::class_ path (its members are public by default), and bind public data members with def_readwrite (or def_readonly for const members) via a new CppClassMemberWrapperWriter. Static and bitfield members, which have no usable address, are skipped; the existing excluded_variables option (previously unused) suppresses a named field. The change applies to all wrapped classes, not just structs. package_info._wrapped_types now also walks public data members (same skips) so a member type drives dependency pruning and auto-includes, keeping it in lockstep with the writers. When a class produces no wrapper code at all, warn instead of silently writing nothing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a plain data struct ShapeMetrics to the primitives module: mutable members bound with def_readwrite, a const member with def_readonly, and a field suppressed via excluded_variables. Regenerate the wrappers and add a test that constructs the struct, round-trips a field, checks the const field is read-only, and confirms the excluded field is absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Note that a plain struct is wrapped as a normal class, that public data members are exposed with def_readwrite/def_readonly, and that excluded_variables suppresses a field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several example headers used run-together include guards (ABSTRACTPOLYGON_HPP_) while others already separated words with underscores (SIMPLE_MATH_FUNCTIONS_HPP_). Standardise both example projects on the word-separated form. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add unit tests for the paths the initial #116 tests missed: a class that produces no register blocks (warns, writes nothing), a plain non-struct class taking the normal member-binding path, and an abstract class with an abstract base whose constructors are not wrapped but whose public data members still drive dependency/auto-include resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #117 +/- ##
===========================================
+ Coverage 99.68% 99.69% +0.01%
===========================================
Files 27 28 +1
Lines 2203 2282 +79
Branches 487 507 +20
===========================================
+ Hits 2196 2275 +79
Misses 6 6
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Fixes #116 by wrapping ordinary structs through the class path and exposing public data members.
Changes:
- Adds read/write and const read-only member bindings with exclusions.
- Includes member types in dependency discovery.
- Adds unit, integration, generated-wrapper, and documentation coverage.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
cppwg/writers/member_variable_writer.py |
Generates public member bindings. |
cppwg/writers/class_writer.py |
Routes ordinary structs through class generation. |
cppwg/templates/pybind11_default.py |
Adds the member-binding template. |
cppwg/info/package_info.py |
Includes member types in dependency analysis. |
tests/test_member_variable_writer.py |
Tests member-binding behavior. |
tests/test_class_writer.py |
Tests struct wrapping and exclusions. |
tests/test_package_info.py |
Tests member dependency discovery. |
doc/reference.md |
Documents struct and variable support. |
doc/basics.md |
Documents variable exclusions. |
examples/shapes/src/cpp/primitives/ShapeMetrics.hpp |
Adds an example data struct. |
examples/shapes/wrapper/package_info.yaml |
Configures the example struct. |
examples/shapes/src/py/tests/test_classes.py |
Tests generated Python behavior. |
examples/shapes/wrapper/primitives/ShapeMetrics.cppwg.hpp |
Adds generated registration declaration. |
examples/shapes/wrapper/primitives/ShapeMetrics.cppwg.cpp |
Adds generated member bindings. |
examples/shapes/wrapper/primitives/_pyshapes_primitives.main.cppwg.cpp |
Registers the example struct. |
examples/shapes/wrapper/wrapper_header_collection.cppwg.hpp |
Includes the example header. |
examples/shapes/src/cpp/primitives/UnitSquare.hpp |
Normalizes the include guard. |
examples/shapes/src/cpp/primitives/ShapeKind.hpp |
Normalizes the include guard. |
examples/shapes/src/cpp/primitives/RegularPolygon.hpp |
Normalizes the include guard. |
examples/shapes/src/cpp/primitives/AreaUnits.hpp |
Normalizes the include guard. |
examples/shapes/src/cpp/primitives/AbstractShape.hpp |
Normalizes the include guard. |
examples/shapes/src/cpp/primitives/AbstractPolygon.hpp |
Normalizes the include guard. |
examples/cells/src/cpp/utils/SimulationException.hpp |
Normalizes the include guard. |
examples/cells/src/cpp/utils/PetscUtils.hpp |
Normalizes the include guard. |
examples/cells/src/cpp/mesh/MacroMesh.hpp |
Normalizes the include guard. |
examples/cells/src/cpp/cell/CellFactory.hpp |
Normalizes the include guard. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A public C-style array member (e.g. `double coords[3]`) would emit
`.def_readwrite("field", &Cls::field)`, but pybind11's generated setter
assigns to the member and C arrays are not assignable, so the wrapper
fails to compile (def_readonly is no better: pybind11 has no caster for a
raw array). Skip array members with a debug log, alongside the existing
bitfield/static skips, and mirror the skip in the package_info member
walk so an array member's type no longer drives a spurious dependency or
auto-include.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.
Suppressed comments (3)
cppwg/info/package_info.py:629
- Keep dependency discovery restricted to direct data members too. pygccxml recurses into nested declarations by default, so nested fields can currently add spurious auto-includes or even prune the outer wrapper for a dependency that is never actually bound.
for variable in decl.variables(function=query, allow_empty=True):
doc/reference.md:110
- The implementation also skips C-style array members, but this new public-facing note lists only static and bitfield members. Document arrays here so users understand why a public array field is absent from Python.
[`excluded_variables`](#common-options) to suppress a field. Static and bitfield
members are skipped. (The only special case is a struct wrapping a single nested
cppwg/info/package_info.py:628
- This walk now yields data-member types for the single-enum struct special case, but
CppClassWrapperWriter.write()routes that declaration tobuild_struct_enum_register()and never binds its members. Consequently, an uninstantiated member type can incorrectly prune an otherwise valid enum wrapper, and auto-includes can be added for code that is not emitted. Detect the struct-enum dispatch before walking methods, constructors, or members so this helper continues to represent only generated bindings.
This issue also appears on line 629 of the same file.
# Public data members are bound with def_readwrite/def_readonly, so their
# types are wrapped too. Mirror the member writer's skips (excluded_variables,
# static, bitfield, array) so a member type reached only through a skipped
# member does not trigger a dependency or auto-include.
excluded_variables = gather("excluded_variables")
Two more member kinds cannot be bound with def_readwrite/def_readonly: - A reference member (`T& field`) has no pointer-to-member, so &Class::field is ill-formed. - The variables() query is recursive, so it also returns fields of nested classes (e.g. an iterator); binding one as &Class::field names a member the outer class does not have. The method/constructor writers already guard this with a parent check; the member writer now does too. Skip both in the member writer and mirror them in the package_info dependency walk. Also short-circuit that walk for the single-nested-enum struct special case: such a struct is registered as a py::enum_ and binds none of its methods/constructors/members, so it introduces no wrapped types. Update the reference.md note to list the full skip set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (1)
cppwg/writers/member_variable_writer.py:146
- Mutable does not imply copy-assignable. pybind11's
def_readwritesetter performsc.*pm = value, so fields such asstd::unique_ptr<T>orstd::atomic<T>instantiate a deleted copy assignment operator and make the generated wrapper fail to compile. Please detect non-copy-assignable member types and either expose them read-only or skip them, and mirror that decision in the dependency walk.
# A const member cannot be written from Python, so bind it read-only.
if declarations.is_const(self.variable_decl.decl_type):
access = "readonly"
else:
access = "readwrite"
pybind11's def_readwrite setter performs obj.*pm = value, so a mutable member whose type is not copy-assignable makes the generated wrapper fail to compile. This bites move-only types (std::unique_ptr), std::atomic (deleted copy assignment) and any class with a user-deleted operator=. Add utils.type_is_copy_assignable(decl_type): fundamentals, pointers and enums are always assignable; a class type is treated as non-assignable if pygccxml reports it noncopyable (catches unique_ptr/atomic, which still expose a public move/value operator= that a public-assign check alone would miss) or as lacking a public assignment operator (catches a plainly deleted copy assignment). The member writer skips such members when they would be bound read-write; const members are read-only (no setter) and are unaffected. Mirror the skip in the package_info dependency walk, and note it in reference.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes #116