Skip to content

Wrap structs - #117

Merged
kwabenantim merged 8 commits into
developfrom
116-wrap-structs
Aug 10, 2026
Merged

Wrap structs#117
kwabenantim merged 8 commits into
developfrom
116-wrap-structs

Conversation

@kwabenantim

Copy link
Copy Markdown
Member

Fixes #116

kwabenantim and others added 5 commits August 5, 2026 11:16
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

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.69%. Comparing base (5ebccf8) to head (55f9629).

Additional details and impacted files

Impacted file tree graph

@@             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              
Flag Coverage Δ
cells 75.59% <21.25%> (-1.94%) ⬇️
shapes 76.16% <68.75%> (-0.15%) ⬇️
unit 92.41% <100.00%> (+1.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cppwg/info/package_info.py 99.23% <100.00%> (+0.04%) ⬆️
cppwg/templates/pybind11_default.py 100.00% <100.00%> (ø)
cppwg/utils/utils.py 100.00% <100.00%> (ø)
cppwg/writers/class_writer.py 98.77% <100.00%> (+0.01%) ⬆️
cppwg/writers/member_variable_writer.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kwabenantim
kwabenantim requested a balanced review from Copilot August 10, 2026 10:20
@kwabenantim
kwabenantim marked this pull request as ready for review August 10, 2026 10:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cppwg/writers/member_variable_writer.py
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to build_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")

Comment thread cppwg/writers/member_variable_writer.py
Comment thread cppwg/writers/class_writer.py
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_readwrite setter performs c.*pm = value, so fields such as std::unique_ptr<T> or std::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>
@kwabenantim
kwabenantim merged commit 4f3929d into develop Aug 10, 2026
17 checks passed
@kwabenantim
kwabenantim deleted the 116-wrap-structs branch August 10, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-enum structs are silently dropped without a wrapper file

2 participants