"""Wrapper code writer for C++ classes."""
import logging
import os
from typing import TYPE_CHECKING
from pygccxml.declarations import type_traits_classes
from pygccxml.declarations.matchers import access_type_matcher_t
from cppwg.utils.constants import (
CPPWG_CLASS_OVERRIDE_SUFFIX,
CPPWG_EXT,
CPPWG_HEADER_COLLECTION_FILENAME,
)
from cppwg.utils.utils import (
call_generator_hook,
canonicalize_type_whitespace,
ensure_trailing_newline,
type_string_matches,
write_file_if_changed,
)
from cppwg.writers.base_writer import CppBaseWrapperWriter
from cppwg.writers.constructor_writer import CppConstructorWrapperWriter
from cppwg.writers.method_writer import CppMethodWrapperWriter
if TYPE_CHECKING:
from string import Template
from pygccxml.declarations.calldef_members import member_function_t
from pygccxml.declarations.class_declaration import class_t
from cppwg.info.class_info import CppClassInfo
[docs]
class CppClassWrapperWriter(CppBaseWrapperWriter):
"""
Writer to generate wrapper code for C++ classes.
Attributes
----------
class_info : CppClassInfo
The class information
wrapper_templates : dict[str, Template]
Templates with placeholders for generating wrapper code
module_classes : dict[pygccxml.declarations.class_t, str]
A dictionary of decls and names for all classes in the module
package_classes : set[pygccxml.declarations.class_t]
Decls for every class wrapped anywhere in the package (all modules).
Used to detect base classes wrapped in another module of this package.
package_class_infos : dict[pygccxml.declarations.class_t, CppClassInfo]
Maps each wrapped decl to its class_info. Used by the inherited-override
skip to consult a base class's excluded_methods.
overwrite : bool
Force rewrite of the class wrapper files, even if unchanged
hpp_string : str
The hpp wrapper code
cpp_string : str
The cpp wrapper code
"""
def __init__(
self,
class_info: "CppClassInfo",
wrapper_templates: dict[str, "Template"],
module_classes: dict["class_t", str],
package_classes: set["class_t"] = None,
overwrite: bool = False,
package_class_infos: dict["class_t", "CppClassInfo"] = None,
) -> None:
logger = logging.getLogger()
super().__init__(wrapper_templates)
self.class_info = class_info
if len(self.class_info.cpp_names) != len(self.class_info.py_names):
logger.error("C++ and Python class name lists should be the same length")
raise AssertionError()
self.module_classes = module_classes
self.package_classes = package_classes if package_classes is not None else set()
self.package_class_infos = (
package_class_infos if package_class_infos is not None else {}
)
self.overwrite = overwrite
self.hpp_string: str = ""
self.cpp_string: str = ""
# Type-caster headers to include in this class's wrapper .cpp, detected
# from the generated registration text in write(). Empty until then.
self.typecaster_includes: list[str] = []
[docs]
def prefix_block(self) -> str:
"""
Return the prefix text block for the top of a wrapper file.
Returns
-------
str
The prefix text followed by a newline, or an empty string.
"""
prefix_text = self.class_info.hierarchy_attribute("prefix_text")
return f"{prefix_text}\n" if prefix_text else ""
@staticmethod
def _format_include(entry: str) -> str:
"""
Format one `#include` line for a header entry, or "" to skip it.
An angle-bracket entry (e.g. `<string>` or `<petsc/caster.h>`) becomes
`#include <...>`; anything else becomes a quoted `#include "..."`. A
non-string or empty entry (e.g. a mis-typed yaml scalar) is skipped. Used
for both `source_includes` and auto-detected type-caster headers so the
two accept the same `<...>` / `"..."` spellings.
Parameters
----------
entry : str
A header entry, e.g. `Foo.hpp` or `<string>`.
Returns
-------
str
The formatted include line (with trailing newline), or "".
"""
if not isinstance(entry, str) or not entry:
return ""
if entry.startswith("<"):
# e.g. #include <string>
return f"#include {entry}\n"
# e.g. #include "Foo.hpp"
return f'#include "{entry}"\n'
[docs]
def includes_block(self) -> str:
"""
Return the `#include` block for a class wrapper cpp file.
Returns
-------
str
The include directives, one per line.
"""
# Build an order-preserving, de-duplicated list of include lines. A
# header could legitimately reach this block from more than one source
# e.g. a type-caster header that is auto-detected *and* listed
# manually under source_includes.
seen: set[str] = set()
lines: list[str] = []
def add(line: str) -> None:
if line and line not in seen:
seen.add(line)
lines.append(line)
common_include = self.class_info.hierarchy_attribute("common_include_file")
if common_include:
add(f'#include "{CPPWG_HEADER_COLLECTION_FILENAME}"\n')
# Auto-detected type-caster headers, added to the wrappers whose
# signatures use the caster's types. A caster header may be spelled with
# angle brackets (e.g. `<petsc/caster.h>`) or quoted, like source_includes.
for header in self.typecaster_includes:
add(self._format_include(header))
# Headers a custom generator's emitted code needs. A generator can name
# types in its get_class_cpp_def_code() output that never appear in the
# parsed signatures (e.g. AddCellWriter<CellAgesWriter>), so cppwg cannot
# auto-detect them; it declares them via the optional get_source_includes()
# hook. These are emitted before the common-include early return: they may
# be <...> system headers or otherwise absent from the wrapper header
# collection, so the header collection alone would not pull them in.
for header in call_generator_hook(
self.class_info.custom_generator_instance, "get_source_includes", []
):
add(self._format_include(header))
if common_include:
return "".join(lines)
# Auto-resolved headers for the project types this class's wrapped
# signatures use (empty unless auto_includes is enabled). Emitted with the
# class's other includes; the dedup above drops any that a caster or a
# manual source_includes entry already covers.
for header in self.class_info.auto_include_headers:
add(self._format_include(header))
# Non-common: the class's explicit source_includes, then its own header.
# Caster headers already lead the block, so a caster duplicated here is
# skipped rather than emitted a second time.
for source_include in self.class_info.hierarchy_attribute_gather_flat(
"source_includes"
):
add(self._format_include(source_include))
source_file = self.class_info.source_file
if not source_file:
source_file = os.path.basename(self.class_info.decls[0].location.file_name)
add(f'#include "{source_file}"\n')
return "".join(lines)
[docs]
def smart_ptr_handle(self) -> str:
"""
Return the smart pointer holder declaration, or an empty string.
Returns
-------
str
e.g. "PYBIND11_DECLARE_HOLDER_TYPE(T, boost::shared_ptr<T>)".
"""
smart_ptr_type = self.class_info.hierarchy_attribute("smart_ptr_type")
if not smart_ptr_type:
return ""
return self.wrapper_templates["smart_pointer_holder"].substitute(
holder=smart_ptr_type
)
[docs]
def prefix_code(self) -> str:
"""Return any custom prefix code lines for the class."""
return "".join(f"{code_line}\n" for code_line in self.class_info.prefix_code)
[docs]
def suffix_code(self) -> str:
"""Return any custom suffix code lines for the class."""
return "".join(f"{code_line}\n" for code_line in self.class_info.suffix_code)
[docs]
def virtual_overrides(
self, template_idx: int
) -> tuple[str, str, list["member_function_t"]]:
"""
Build the virtual "trampoline" override block for the class.
Identify any methods needing overrides (i.e. any that are virtual in the
current class or in a base class), and build the trampoline override
class that forwards them to Python.
Parameters
----------
template_idx : int
The index of the template in the class info
Returns
-------
tuple[str, str, list[pygccxml.declarations.member_function_t]]
The return-type typedef block, the override class block (empty if
the class has no virtual methods), and the list of methods needing
an override.
"""
methods_needing_override: list["member_function_t"] = []
return_types: list[str] = [] # e.g. ["void", "unsigned int", "::Bar<2> *"]
# Collect all virtual methods and their return types
class_decl = self.class_info.decls[template_idx]
for member_function in class_decl.member_functions(allow_empty=True):
is_pure_virtual = member_function.virtuality == "pure virtual"
is_virtual = member_function.virtuality == "virtual"
if is_pure_virtual or is_virtual:
methods_needing_override.append(member_function)
return_types.append(member_function.return_type.decl_string)
# Add typedefs for return types with special characters
# e.g. typedef ::Bar<2> * _Bar_lt_2_gt_Ptr;
return_typedefs = ""
for return_type in return_types:
if return_type != self.tidy_name(return_type):
return_typedefs += "typedef {class_cpp_name} {tidy_name};\n".format(
class_cpp_name=return_type,
tidy_name=self.tidy_name(return_type),
)
# Override virtual methods
override_class = ""
if methods_needing_override:
# Add virtual override class, e.g.:
# class Foo_Overrides : public Foo {
# public:
# using Foo::Foo;
class_py_name = self.class_info.py_names[template_idx]
override_class += self.wrapper_templates[
"class_virtual_override_header"
].substitute(
class_py_name=class_py_name,
class_base_name=self.class_info.name,
)
# Override each method, e.g.:
# void bar(double d) const override {
# PYBIND11_OVERRIDE_PURE(
# bar,
# Foo_2_2,
# bar,
# d);
# }
for method in methods_needing_override:
method_writer = CppMethodWrapperWriter(
self.class_info,
template_idx,
method,
self.wrapper_templates,
)
override_class += method_writer.generate_virtual_override_wrapper()
override_class += "};\n\n"
return return_typedefs, override_class, methods_needing_override
[docs]
def bases_block(self, class_decl: "class_t") -> str:
"""
Return the base-class list appended to the ``py::class_`` declaration.
Cross-module inheritance is opted into per module via `imports`. When
set, a base class that is not wrapped in this module may still be
referenced, but only if it is known to be registered elsewhere: either
it is wrapped in another module of this package, or the user has listed
it under `external_bases` (for bases wrapped in an imported package).
This avoids emitting unregistered bases (e.g. framework/utility bases),
which would fail at import.
Parameters
----------
class_decl : pygccxml.declarations.class_t
The class declaration whose bases to inspect.
Returns
-------
str
e.g. ", AbstractFoo, InterfaceFoo".
"""
bases = ""
allow_external_bases = bool(self.class_info.hierarchy_attribute("imports"))
external_bases = self.class_info.hierarchy_attribute("external_bases")
if isinstance(external_bases, str):
# A scalar (e.g. `external_bases: AbstractFoo`) is a single base
# name; wrap it so the membership test below is exact rather than a
# substring match (e.g. "Foo" in "AbstractFoo").
external_bases = [external_bases]
elif not isinstance(external_bases, (list, tuple, set)):
# Unset (None) or any other mis-typed scalar -> no external bases.
external_bases = []
# Compare on unqualified names: the base's pygccxml name is unqualified,
# so accept either a qualified or unqualified config entry (e.g. both
# "foo::AbstractBar" and "AbstractBar" match a base named AbstractBar).
external_bases = {str(name).split("::")[-1] for name in external_bases}
for base in class_decl.bases: # type(base) -> hierarchy_info_t
# Check that the base class is not private
if base.access_type == "private":
continue
related_class = base.related_class
if related_class in self.module_classes:
# Base class is wrapped in this module: refer to it by its
# Python wrapper name.
bases += f", {self.module_classes[related_class]}"
elif (
allow_external_bases
and related_class is not None
and (
related_class in self.package_classes
or related_class.name.split("<", 1)[0] in external_bases
)
):
# Base class is wrapped in another module - either elsewhere in
# this package, or in an imported package (listed under
# `external_bases`). Refer to it by its C++ type so that pybind11
# links the inheritance at runtime. The module that registers the
# base must be listed under `imports` so that it is imported
# before this class is registered.
bases += f", {related_class.decl_string}"
return bases
[docs]
def build_hpp(self, register_py_names: list[str]) -> str:
"""
Build the class wrapper hpp file contents.
One hpp is emitted per class, forward-declaring the register function for
every instantiation that is written.
Parameters
----------
register_py_names : list[str]
The Python names of the instantiations that have a register function,
e.g. ["Foo_2_2", "Foo_3_3"].
Returns
-------
str
The hpp wrapper code.
"""
decl_template = self.wrapper_templates["class_hpp_register_declaration"]
register_declarations = "".join(
decl_template.substitute(class_py_name=class_py_name)
for class_py_name in register_py_names
)
return self.wrapper_templates["class_hpp"].substitute(
prefix_text=self.prefix_block(),
class_hpp_name=self.class_info.py_name_base(),
register_declarations=register_declarations,
)
def _overrides_wrapped_base_virtual(
self, class_decl: "class_t", method_decl: "member_function_t"
) -> bool:
"""
Return True if a method overrides a virtual wrapped on a wrapped base.
The method qualifies if it is virtual or pure virtual and some base class,
wrapped in this package and not class-excluded, declares its own member
function of the same name, argument types and const-ness that is itself
virtual (the return type is intentionally not compared, so a
covariant-return override still matches), and that base does not itself
exclude the method from wrapping - by name, return type or arg type, the
same rules as CppMethodWrapperWriter.method_is_excluded (otherwise the base
emits no binding, so this override is the only one). The base must also be
reachable from
the derived py::class_ via an emitted pybind11 base link: a same-module
base is always linked, but a base wrapped in another module of the package
is only linked when cross-module inheritance is enabled (`imports` set) -
see bases_block. Without that link the base's binding is not inherited, so
the override would become unreachable if skipped. This is the per-method
test used by _is_inherited_override; it does not consider sibling
overloads.
Parameters
----------
class_decl : pygccxml.declarations.class_t
The class declaration owning the method.
method_decl : pygccxml.declarations.member_function_t
The candidate member function.
Returns
-------
bool
True if the method overrides a wrapped base virtual.
"""
if method_decl.virtuality not in ("virtual", "pure virtual"):
return False
# Cross-module inheritance is only linked into the derived py::class_ when
# the module opts in via `imports` (see bases_block). Without it, a base
# wrapped in another module contributes no inherited binding, so an
# override of it here is the sole binding and must not be skipped.
allow_external_bases = bool(self.class_info.hierarchy_attribute("imports"))
name = method_decl.name
arg_types = [
canonicalize_type_whitespace(t.decl_string)
for t in method_decl.argument_types
]
for hierarchy_info in class_decl.recursive_bases:
base_decl = hierarchy_info.related_class
# Skip bases pygccxml could not resolve, and bases not wrapped in this
# package (an unwrapped base provides no binding to inherit).
if base_decl is None or base_decl not in self.package_classes:
continue
# A base wrapped in another module only provides an inherited binding
# when its pybind base link is emitted (imports enabled). A same-module
# base (in module_classes) is always linked.
if base_decl not in self.module_classes and not allow_external_bases:
continue
for base_method in base_decl.member_functions(name, allow_empty=True):
# Only public methods are bound (build_class_register filters on
# public access), so a protected/private base virtual is not
# wrapped on the base and cannot make this override redundant.
if base_method.access_type != "public":
continue
if base_method.virtuality not in ("virtual", "pure virtual"):
continue
if base_method.has_const != method_decl.has_const:
continue
base_arg_types = [
canonicalize_type_whitespace(t.decl_string)
for t in base_method.argument_types
]
if base_arg_types != arg_types:
continue
# The base declares a matching virtual. Keep the override only if
# the base does not actually wrap it: a base method excluded from
# wrapping (by name, return type or arg type - the same rules as
# CppMethodWrapperWriter) emits no binding, so this override is the
# sole binding and must not be skipped.
base_info = self.package_class_infos.get(base_decl)
if base_info is not None and CppMethodWrapperWriter.method_is_excluded(
base_info, base_decl, base_method
):
continue
return True
return False
def _is_inherited_override(
self, class_decl: "class_t", method_decl: "member_function_t"
) -> bool:
"""
Return True if a method's binding is a redundant inherited override.
With ``exclude_inherited_overrides`` set, a method that overrides a virtual
method already wrapped on a wrapped base class need not be bound again: pybind11
inheritance exposes the base binding and virtual dispatch routes the call
to this override. Only the ``.def`` is skipped - the virtual trampoline
(see virtual_overrides) is unaffected, so Python subclasses can still
override the method.
The method must itself override a wrapped base virtual
(_overrides_wrapped_base_virtual) AND *every other public overload of the
same name that will actually be wrapped* must too. This overload guard
matters because pybind11 resolves overloads by name: a derived binding of
a name shadows the inherited base binding for that name. So if the class
still binds some other overload of this name, that surviving binding would
hide the base's binding of this one - making it uncallable from Python.
Skipping is safe only when the class binds none of that name and thus
inherits the base's full overload set. A sibling overload that is itself
excluded from wrapping (excluded_methods / return_type_excludes /
arg_type_excludes, i.e. CppMethodWrapperWriter.method_is_excluded) emits
no binding, so it cannot shadow and does not block skipping.
Parameters
----------
class_decl : pygccxml.declarations.class_t
The class declaration owning the method.
method_decl : pygccxml.declarations.member_function_t
The candidate member function.
Returns
-------
bool
True if the binding should be skipped as a redundant override.
"""
if not self.class_info.hierarchy_attribute("exclude_inherited_overrides"):
return False
if not self._overrides_wrapped_base_virtual(class_decl, method_decl):
return False
# Overload-shadowing guard: skip only if no other public overload of the
# same name survives as a binding (i.e. every same-name overload is also a
# skippable override). Otherwise the surviving overload shadows the base.
query = access_type_matcher_t("public")
for sibling in class_decl.member_functions(
method_decl.name, function=query, allow_empty=True
):
if sibling is method_decl:
continue
# A sibling that will not be wrapped (excluded by name/type) emits no
# binding, so it cannot shadow the base and does not force keeping.
if CppMethodWrapperWriter.method_is_excluded(
self.class_info, class_decl, sibling
):
continue
if not self._overrides_wrapped_base_virtual(class_decl, sibling):
return False
return True
[docs]
def build_class_register(self, template_idx: int) -> tuple[str, str]:
"""
Build the registration block for one template instantiation.
Parameters
----------
template_idx : int
The index of the template instantiation in the class info.
Returns
-------
tuple[str, str]
The registration block and its trampoline return typedefs (the
latter are hoisted into the shared preamble and deduplicated).
"""
class_cpp_name = self.class_info.cpp_names[template_idx]
class_py_name = self.class_info.py_names[template_idx]
class_decl = self.class_info.decls[template_idx]
generator = self.class_info.custom_generator_instance
# Find and define virtual function "trampoline" overrides
(
return_typedefs,
override_class,
methods_needing_override,
) = self.virtual_overrides(template_idx)
# Add the trampoline override class to the class definition if needed
# e.g. py::class_<Foo, Foo_Overrides>(m, "Foo")
overrides_string = ""
if methods_needing_override:
overrides_string = f", {class_py_name}{CPPWG_CLASS_OVERRIDE_SUFFIX}"
# Add smart pointer support to the wrapper class definition if needed
# e.g. py::class_<Foo, boost::shared_ptr<Foo>>(m, "Foo")
ptr_support = ""
smart_ptr_type = self.class_info.hierarchy_attribute("smart_ptr_type")
if smart_ptr_type:
ptr_support = f", {smart_ptr_type}<{class_py_name}>"
# Add public constructors
query = access_type_matcher_t("public")
constructors = "".join(
CppConstructorWrapperWriter(
self.class_info,
template_idx,
constructor,
self.wrapper_templates,
).generate_wrapper()
for constructor in class_decl.constructors(function=query, allow_empty=True)
)
# Add public member functions. A method that redundantly overrides a
# virtual already wrapped on a wrapped base class is skipped when
# exclude_inherited_overrides is set (its trampoline, built separately in
# virtual_overrides, is kept).
methods = "".join(
CppMethodWrapperWriter(
self.class_info,
template_idx,
member_function,
self.wrapper_templates,
).generate_wrapper()
for member_function in class_decl.member_functions(
function=query, allow_empty=True
)
if not self._is_inherited_override(class_decl, member_function)
)
block = self.wrapper_templates["class_cpp_register"].substitute(
generator_pre_code=call_generator_hook(
generator, "get_class_cpp_pre_code", "", class_py_name
),
class_py_name=class_py_name,
class_cpp_name=class_cpp_name,
override_class=override_class,
overrides_string=overrides_string,
ptr_support=ptr_support,
bases=self.bases_block(class_decl),
constructors=constructors,
methods=methods,
# Normalise the generator snippet to end with a newline: it is spliced
# into the .def() chain ahead of suffix_code and the closing `;`, so a
# missing newline (or a trailing // comment) could swallow the
# statement terminator. See ensure_trailing_newline.
generator_def_code=ensure_trailing_newline(
call_generator_hook(
generator, "get_class_cpp_def_code", "", class_py_name
)
),
suffix_code=self.suffix_code(),
)
return block, return_typedefs
[docs]
def build_struct_enum_register(self, template_idx: int) -> str:
"""
Build the registration block for a struct-enum instantiation.
Handles a struct that wraps a single nested enum, for example::
struct Foo {
enum Value {A, B, C};
};
Parameters
----------
template_idx : int
The index of the template instantiation in the class info.
Returns
-------
str
The registration block.
"""
class_cpp_name = self.class_info.cpp_names[template_idx]
class_py_name = self.class_info.py_names[template_idx]
class_decl = self.class_info.decls[template_idx]
generator = self.class_info.custom_generator_instance
enum_decl = class_decl.enumerations(allow_empty=True)[0]
# Refer to the class through the wrapper alias (class_py_name), which is
# typedef'd to the C++ type, so the registration function name matches
# the hpp declaration and the module's register_..._class call even when
# class_py_name differs from the C++ decl name (templates, name overrides).
enum_values = "".join(
' .value("{val}", {class_py_name}::{enum_name}::{val})\n'.format(
val=value[0],
class_py_name=class_py_name,
enum_name=enum_decl.name,
)
for value in enum_decl.values
)
return self.wrapper_templates["struct_enum_register"].substitute(
generator_pre_code=call_generator_hook(
generator, "get_class_cpp_pre_code", "", class_py_name
),
class_py_name=class_py_name,
class_cpp_name=class_cpp_name,
enum_name=enum_decl.name,
enum_values=enum_values,
)
[docs]
def write(self, work_dir: str) -> None:
"""
Write the hpp and cpp wrapper code to file.
All of a class's template instantiations share a single
`{class}.cppwg.hpp` / `.cpp` pair. The cpp holds one shared preamble
followed by one registration block per instantiation.
Parameters
----------
work_dir : str
The directory to write the files to
"""
logger = logging.getLogger()
# decls, cpp_names and py_names are parallel, one entry per instantiation,
# and the loop below indexes all three by the same position. Validate they
# are the same length up-front so a mismatch fails deterministically here
# rather than as an IndexError (or a mislabelled wrapper) mid-loop.
n_decls = len(self.class_info.decls)
n_cpp = len(self.class_info.cpp_names)
n_py = len(self.class_info.py_names)
if not n_decls == n_cpp == n_py:
message = (
f"Class {self.class_info.name} has mismatched instantiation lists "
f"({n_decls} decls, {n_cpp} cpp_names, {n_py} py_names); they must "
"be kept in lockstep."
)
logger.error(message)
raise AssertionError(message)
register_blocks: list[str] = []
register_py_names: list[str] = []
class_typedefs: list[str] = []
deduped_typedefs: list[str] = []
seen_typedefs: set[str] = set()
for idx, class_decl in enumerate(self.class_info.decls):
class_py_name = self.class_info.py_names[idx]
# The alias typedef (typedef <cpp> <py>;) for this instantiation, kept
# for the preamble so every block can refer to it (and every other
# instantiation) by its alias regardless of block order.
alias_typedef = "typedef {cpp_name} {py_name};\n".format(
cpp_name=self.class_info.cpp_names[idx],
py_name=class_py_name,
)
# Check for the struct-enum pattern, e.g.:
# struct Foo { enum Value {A, B, C}; };
if type_traits_classes.is_struct(class_decl):
enums = class_decl.enumerations(allow_empty=True)
if len(enums) == 1:
register_blocks.append(self.build_struct_enum_register(idx))
register_py_names.append(class_py_name)
class_typedefs.append(alias_typedef)
continue
block, return_typedefs = self.build_class_register(idx)
register_blocks.append(block)
register_py_names.append(class_py_name)
class_typedefs.append(alias_typedef)
# Hoist trampoline return typedefs into the shared preamble,
# deduplicated so a type shared by several instantiations (e.g.
# ::std::string) is not redefined.
for line in return_typedefs.splitlines(keepends=True):
if line not in seen_typedefs:
seen_typedefs.add(line)
deduped_typedefs.append(line)
# Nothing to register (e.g. only structs that are not the single-enum
# pattern) - write no files, matching the previous behaviour.
if not register_blocks:
return
# Assemble the preamble typedef blocks and the register section once; each
# feeds both the type-caster scan and the emitted cpp, so join a single
# time rather than repeating the work. register_section carries the exact
# separator (a leading newline) that goes between the preamble and the
# register blocks in the emitted file, so it is a single source of truth
# for that boundary.
class_typedefs_block = "".join(class_typedefs)
return_typedefs_block = "".join(deduped_typedefs)
register_section = "\n" + "\n".join(register_blocks)
# Detect which type-caster headers this class needs by scanning the
# generated registration text (the alias/trampoline typedefs and the
# register blocks) for the configured caster types. Scanning the emitted
# code means only types that actually survived into the wrapper count, so
# exclusions are honoured automatically. scan_text is assembled from the
# same pieces (and the same separator) as the emitted cpp below, so it is
# exactly the tail of the wrapper text from the typedefs onward - the scan
# never diverges from what is written. Must run before build_cpp_header,
# which emits the includes via includes_block().
scan_text = class_typedefs_block + return_typedefs_block + register_section
self.typecaster_includes = self._detect_typecasters(scan_text)
self.hpp_string = self.build_hpp(register_py_names)
self.cpp_string = self.build_cpp_header(
class_typedefs_block, return_typedefs_block
)
self.cpp_string += register_section
self.write_files(work_dir, self.class_info.py_name_base())
def _detect_typecasters(self, scan_text: str) -> list[str]:
"""
Return the type-caster headers whose types appear in the wrapper code.
Iterates the package's already-validated typecasters
(`PackageInfo.parsed_typecasters`, reached via the info tree) and
includes an entry's header if any of its type names occurs as a whole
token in `scan_text` (the generated registration code). The result
preserves config order and is de-duplicated, so a header is emitted once
even if it matches several types or several instantiations.
The entries are validated (and any malformed-entry warnings emitted) once
on PackageInfo, not per class wrapper, so a bad entry does not warn once
per class.
Parameters
----------
scan_text : str
The generated registration text to search (typedefs + register
blocks).
Returns
-------
list[str]
The caster header filenames to include, in config order.
"""
entries = self.class_info.hierarchy_attribute("parsed_typecasters")
if not entries:
return []
headers: list[str] = []
for header, types in entries:
if header in headers:
continue
if any(type_string_matches(scan_text, type_name) for type_name in types):
headers.append(header)
return headers
[docs]
def write_files(self, work_dir: str, file_stem: str) -> None:
"""
Write the hpp and cpp wrapper code to file.
Parameters
----------
work_dir : str
The directory to write the files to
file_stem : str
The wrapper file stem shared by all of the class's
instantiations, e.g. Foo (for Foo.cppwg.hpp / Foo.cppwg.cpp).
"""
hpp_filepath = os.path.join(work_dir, f"{file_stem}.{CPPWG_EXT}.hpp")
cpp_filepath = os.path.join(work_dir, f"{file_stem}.{CPPWG_EXT}.cpp")
write_file_if_changed(hpp_filepath, self.hpp_string, self.overwrite)
write_file_if_changed(cpp_filepath, self.cpp_string, self.overwrite)