Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ r = Rectangle(4, 5)
- By default, cppwg only rewrites wrapper files whose content has changed, leaving
unchanged files untouched so build systems skip recompiling them. Pass
`--overwrite` to force a full rewrite of all wrapper files.
- A templated class's instantiations share a single wrapper file: `Foo<2>` and
`Foo<3>` are wrapped in one `Foo.cppwg.hpp` / `Foo.cppwg.cpp` pair (declaring
and defining a `register_Foo_2_class` and `register_Foo_3_class` each) rather
than a separate file pair per instantiation. This reduces the number of
generated files, and the translation units the build has to compile.
- To pass extra flags to the castxml clang frontend (e.g. to silence a
diagnostic), use `--castxml_cflags`. Values starting with `-` must use `=`,
e.g. `--castxml_cflags="-Wno-deprecated"`.
Expand Down
7 changes: 4 additions & 3 deletions cppwg/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,10 @@ def discover_template_instantiations(self) -> None:
instantiation_map: dict[str, list[list[str]]] = {}
macro_only_files: list[str] = []
for filepath in self.package_info.source_cpp_files:
has_instantiations, file_map = (
utils.find_template_instantiations_in_source_file(filepath)
)
(
has_instantiations,
file_map,
) = utils.find_template_instantiations_in_source_file(filepath)
if not has_instantiations:
continue

Expand Down
46 changes: 26 additions & 20 deletions cppwg/info/class_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,29 @@ def update_from_source(self, source_file_paths: list[str]) -> None:
# Update the C++ and Python class names
self.update_names()

def py_name_base(self) -> str:
"""
Return the shared Python-name base for the class, before template args.

This is the common prefix of every instantiation's Python name (e.g.
"Foo" for py_names ["Foo_2_2", "Foo_3_3"]), and is used to name the
single wrapper file shared by all of a class's instantiations. For an
untemplated class it is the (override) name unchanged; for a templated
class it is cleaned the same way as the instantiation names.
"""
base = self.name_override or self.name
if not self.template_arg_lists:
return base

for name, replacement in self.name_replacements.items():
base = base.replace(name, replacement)
base = base.translate(
str.maketrans({"<": None, ">": None, ",": None, " ": None})
)
if len(base) > 1:
base = base[0].capitalize() + base[1:]
return base

def update_py_names(self) -> None:
"""
Set the Python names for the class, accounting for template args.
Expand All @@ -524,34 +547,17 @@ def update_py_names(self) -> None:
class instantiation. For example, class "Foo" with template arguments
[[2, 2], [3, 3]] will have a Python name list ["Foo_2_2", "Foo_3_3"].
"""
class_name = self.py_name_base()

# Handles untemplated classes
if not self.template_arg_lists:
if self.name_override:
self.py_names.append(self.name_override)
else:
self.py_names.append(self.name)
self.py_names.append(class_name)
return

# Table of special characters for removal
rm_chars = {"<": None, ">": None, ",": None, " ": None}
rm_table = str.maketrans(rm_chars)

# Clean the class name
class_name = self.name
if self.name_override:
class_name = self.name_override

# Do standard name replacements e.g. "unsigned int" -> "Unsigned"
for name, replacement in self.name_replacements.items():
class_name = class_name.replace(name, replacement)

# Remove special characters
class_name = class_name.translate(rm_table)

# Capitalize the first letter e.g. "foo" -> "Foo"
if len(class_name) > 1:
class_name = class_name[0].capitalize() + class_name[1:]

# Create a string of template args separated by "_" e.g. 2_2
for template_arg_list in self.template_arg_lists:
# Example template_arg_list : [2, 2]
Expand Down
8 changes: 8 additions & 0 deletions cppwg/templates/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ def get_class_cpp_def_code(self, *args, **kwargs) -> str:

return ""

def get_module_pre_code(self) -> str:
"""
Return a string of C++ code to be inserted before the module
definition.
"""

return ""

def get_module_code(self) -> str:
"""
Return a string of C++ code to be inserted in the module
Expand Down
80 changes: 49 additions & 31 deletions cppwg/templates/pybind11_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# every entry in template_collection is filled the same way (via .substitute).

class_virtual_override_header = Template(
"class ${class_py_name}" + CPPWG_CLASS_OVERRIDE_SUFFIX + " : public ${class_py_name}\n"
"class ${class_py_name}"
+ CPPWG_CLASS_OVERRIDE_SUFFIX
+ " : public ${class_py_name}\n"
"{\n"
"public:\n"
" using ${class_py_name}::${class_base_name};\n"
Expand Down Expand Up @@ -88,38 +90,64 @@
" PyErr_SetString(PyExc_RuntimeError, ${message_expr});\n"
)

# Skeleton for a class wrapper hpp file.
# Skeleton for a class wrapper hpp file. One hpp is emitted per class (not per
# template instantiation); it forward-declares the register function for every
# instantiation via ${register_declarations}.
class_hpp = Template(
"${prefix_text}"
"#ifndef ${class_py_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
"#define ${class_py_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
"#ifndef ${class_hpp_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
"#define ${class_hpp_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
"\n"
"#include <pybind11/pybind11.h>\n"
"\n"
"${register_declarations}"
"#endif // ${class_hpp_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
)

# A single register-function forward declaration, one per instantiation, joined
# into ${register_declarations} above.
class_hpp_register_declaration = Template(
"void register_${class_py_name}_class(pybind11::module &m);\n"
"#endif // ${class_py_name}_hpp__" + CPPWG_EXT + "_wrapper\n"
)

# Skeleton for a class wrapper cpp file.
class_cpp = Template(
# Preamble for a class wrapper cpp file, emitted once per class. The file-scope
# items that must appear only once when several instantiations share a file live
# here: the includes, the smart-pointer holder declaration, class-level prefix
# code, and the (deduplicated) trampoline return typedefs. It also carries the
# per-instantiation alias typedefs (typedef <cpp> <py>;) for every instantiation
# in the file, gathered into ${class_typedefs} ahead of any registration code so
# each block's generator pre-code, trampoline and registration can refer to any
# instantiation by its alias. Each instantiation's registration follows via one
# class_cpp_register block.
class_cpp_header = Template(
"${prefix_text}"
"#include <pybind11/pybind11.h>\n"
"#include <pybind11/stl.h>\n"
"${includes}"
"\n"
'#include "${class_py_name}.' + CPPWG_EXT + '.hpp"\n'
'#include "${class_hpp_name}.' + CPPWG_EXT + '.hpp"\n'
"\n"
"namespace py = pybind11;\n"
"typedef ${class_cpp_name} ${class_py_name};\n"
"${smart_ptr_handle};\n"
"${prefix_code}"
"${generator_pre_code}"
"${class_typedefs}"
"${return_typedefs}"
)

# Registration block for one template instantiation, appended once per
# instantiation after the class_cpp_header preamble. Refers to the class through
# the wrapper alias (class_py_name) so the trampoline, registration function name
# and Python-visible name all match. The alias typedefs for every instantiation
# are emitted once in the class_cpp_header preamble (${class_typedefs}), so the
# generator pre-code, trampoline override class and registration below can all
# refer to the class by that alias without redefining it here.
class_cpp_register = Template(
"${generator_pre_code}"
"\n"
"${override_class}"
"void register_${class_py_name}_class(py::module &m)\n"
"{\n"
' py::class_<${class_py_name}${overrides_string}${ptr_support}${bases}>'
" py::class_<${class_py_name}${overrides_string}${ptr_support}${bases}>"
'(m, "${class_py_name}")\n'
"${constructors}"
"${methods}"
Expand All @@ -131,25 +159,13 @@

# Skeleton for the struct-enum special case, e.g.:
# struct Foo { enum Value { A, B, C }; };
# The header block is identical to a class cpp file; the registration body wraps
# the single nested enum. Like the class cpp, it refers to the class through the
# wrapper alias (class_py_name, typedef'd to the C++ type) so the registration
# function name matches the hpp declaration and the module's register_..._class
# call, and the Python-visible name is the wrapper name (e.g. for templated
# instantiations or name overrides where it differs from the C++ decl name).
struct_enum_cpp = Template(
"${prefix_text}"
"#include <pybind11/pybind11.h>\n"
"#include <pybind11/stl.h>\n"
"${includes}"
"\n"
'#include "${class_py_name}.' + CPPWG_EXT + '.hpp"\n'
"\n"
"namespace py = pybind11;\n"
"typedef ${class_cpp_name} ${class_py_name};\n"
"${smart_ptr_handle};\n"
"${prefix_code}"
# The registration body wraps the single nested enum. Like class_cpp_register it
# refers to the class through the wrapper alias (class_py_name), whose typedef is
# emitted in the shared class_cpp_header preamble (${class_typedefs}), and follows
# that preamble.
struct_enum_register = Template(
"${generator_pre_code}"
"\n"
"void register_${class_py_name}_class(py::module &m){\n"
' py::class_<${class_py_name}> myclass(m, "${class_py_name}");\n'
Comment thread
kwabenantim marked this conversation as resolved.
' py::enum_<${class_py_name}::${enum_name}>(myclass, "${enum_name}")\n'
Expand Down Expand Up @@ -187,8 +203,10 @@
"module_exception_catch": module_exception_catch,
"header_collection_hpp": header_collection_hpp,
"class_hpp": class_hpp,
"class_cpp": class_cpp,
"struct_enum_cpp": struct_enum_cpp,
"class_hpp_register_declaration": class_hpp_register_declaration,
"class_cpp_header": class_cpp_header,
"class_cpp_register": class_cpp_register,
"struct_enum_register": struct_enum_register,
"free_function": free_function,
"class_method": class_method,
"class_constructor": class_constructor,
Expand Down
27 changes: 27 additions & 0 deletions cppwg/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,33 @@
from pygccxml.declarations.class_declaration import class_t


def ensure_trailing_newline(code: str) -> str:
"""
Return `code` guaranteed to end with a newline (unless it is empty).

Custom generators (subclasses of `cppwg.templates.custom.Custom`) return raw
C++ snippets with no trailing-newline guarantee. When such a snippet is
substituted into a wrapper template immediately ahead of another line, a
missing newline glues the two together and can produce invalid C++ (e.g. a
`#include` directive that no longer starts a line, or a closing `}` swallowed
by a trailing `//` comment). Normalise the snippet here so callers can splice
it safely regardless of how the generator formatted it.

Parameters
----------
code : str
The generator-produced code snippet, possibly empty.

Returns
-------
str
The snippet guaranteed to end with a trailing newline, or "" unchanged.
"""
if code and not code.endswith("\n"):
return code + "\n"
return code


def write_file_if_changed(filepath: str, content: str, overwrite: bool = False) -> bool:
"""
Write content to filepath unless an identical file already exists.
Expand Down
Loading
Loading