Skip to content
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ configure_package_config_file(
INSTALL_INCLUDE_DIR)

# Set all the install paths
set(GTWRAP_CMAKE_INSTALL_DIR $${INSTALL_CMAKE_DIR})
set(GTWRAP_CMAKE_INSTALL_DIR ${INSTALL_CMAKE_DIR})
set(GTWRAP_LIB_INSTALL_DIR ${INSTALL_LIB_DIR})
set(GTWRAP_BIN_INSTALL_DIR ${INSTALL_BIN_DIR})
set(GTWRAP_INCLUDE_INSTALL_DIR ${INSTALL_INCLUDE_DIR})
Expand Down
4 changes: 1 addition & 3 deletions gtwrap/interface_parser/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ def cpp_typename(self):
Return a Typename with the namespaces and cpp name of this
class.
"""
namespaces_name = self.namespaces()
namespaces_name.append(self.name)
return Typename(namespaces_name)
return Typename(self.name, self.namespaces())

def __repr__(self):
return "Enum: {0}".format(self.name)
3 changes: 1 addition & 2 deletions gtwrap/interface_parser/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@

from typing import Any, Iterable, List, Union

from pyparsing import (Literal, Optional, ParseResults, # type: ignore
delimitedList)
from pyparsing import Literal, Optional, ParseResults, delimitedList

from .template import Template
from .tokens import (COMMA, DEFAULT_ARG, EQUAL, IDENT, LOPBRACK, LPAREN, PAIR,
Expand Down
89 changes: 62 additions & 27 deletions gtwrap/interface_parser/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ class Typename:
namespaces_name_rule = delimitedList(IDENT, "::")
rule = (
namespaces_name_rule("namespaces_and_name") #
).setParseAction(lambda t: Typename(t))
).setParseAction(lambda t: Typename.from_parse_result(t))
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
).setParseAction(lambda t: Typename.from_parse_result(t))
).setParseAction(Typename.from_parse_result)

Copilot uses AI. Check for mistakes.

def __init__(self,
t: ParseResults,
name: str,
namespaces: list[str],
instantiations: Sequence[ParseResults] = ()):
self.name = t[-1] # the name is the last element in this list
self.namespaces = t[:-1]
self.name = name
self.namespaces = namespaces

# If the first namespace is empty string, just get rid of it.
if self.namespaces and self.namespaces[0] == '':
Expand All @@ -63,12 +64,38 @@ def __init__(self,
self.instantiations = []

@staticmethod
def from_parse_result(parse_result: Union[str, list]):
def from_parse_result(parse_result: list):
"""Unpack the parsed result to get the Typename instance."""
return parse_result[0]
name = parse_result[-1] # the name is the last element in this list
namespaces = parse_result[:-1]
return Typename(name, namespaces)

def __repr__(self) -> str:
return self.to_cpp()
if self.get_template_args():
templates = f"<{self.get_template_args()}>"
else:
templates = ""

if len(self.namespaces) > 0:
namespaces = "::".join(self.namespaces) + "::"
else:
namespaces = ""

return f"{namespaces}{self.name}{templates}"

def get_template_args(self) -> str:
"""Return the template args as a string, e.g. <double, gtsam::Pose3>."""
return ", ".join([inst.to_cpp() for inst in self.instantiations])

def templated_name(self) -> str:
"""Return the name without namespace and with the template instantiations if any."""
if self.instantiations:
templates = self.get_template_args()
name = f"{self.name}<{templates}>"
else:
name = self.name

return name

def instantiated_name(self) -> str:
"""Get the instantiated name of the type."""
Expand All @@ -84,8 +111,7 @@ def qualified_name(self):
def to_cpp(self) -> str:
"""Generate the C++ code for wrapping."""
if self.instantiations:
cpp_name = self.name + "<{}>".format(", ".join(
[inst.to_cpp() for inst in self.instantiations]))
cpp_name = self.name + f"<{self.get_template_args()}>"
else:
cpp_name = self.name
return '{}{}{}'.format(
Expand Down Expand Up @@ -129,7 +155,7 @@ class BasicType:
rule = (Or(BASIC_TYPES)("typename")).setParseAction(lambda t: BasicType(t))
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
rule = (Or(BASIC_TYPES)("typename")).setParseAction(lambda t: BasicType(t))
rule = (Or(BASIC_TYPES)("typename")).setParseAction(BasicType)

Copilot uses AI. Check for mistakes.

def __init__(self, t: ParseResults):
self.typename = Typename(t)
self.typename = Typename.from_parse_result(t)


class CustomType:
Expand All @@ -148,7 +174,7 @@ class CustomType:
rule = (Typename.rule("typename")).setParseAction(lambda t: CustomType(t))
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

This 'lambda' is just a simple wrapper around a callable object. Use that object directly.

Suggested change
rule = (Typename.rule("typename")).setParseAction(lambda t: CustomType(t))
rule = (Typename.rule("typename")).setParseAction(CustomType)

Copilot uses AI. Check for mistakes.

def __init__(self, t: ParseResults):
self.typename = Typename(t)
self.typename = Typename.from_parse_result(t)


class Type:
Expand Down Expand Up @@ -226,18 +252,16 @@ def to_cpp(self) -> str:
"""

if self.is_shared_ptr:
typename = "std::shared_ptr<{typename}>".format(
typename=self.get_typename())
typename = f"std::shared_ptr<{self.get_typename()}>"
elif self.is_ptr:
typename = "{typename}*".format(typename=self.typename.to_cpp())
typename = f"{self.typename.to_cpp()}*"
elif self.is_ref:
typename = typename = "{typename}&".format(
typename=self.get_typename())
typename = f"{self.get_typename()}&"
else:
typename = self.get_typename()

return ("{const}{typename}".format(
const="const " if self.is_const else "", typename=typename))
const = "const " if self.is_const else ""
return f"{const}{typename}"


class TemplatedType:
Expand Down Expand Up @@ -265,7 +289,7 @@ def __init__(self, typename: Typename, template_params: List[Type],
is_const: str, is_shared_ptr: str, is_ptr: str, is_ref: str):
instantiations = [param.typename for param in template_params]
# Recreate the typename but with the template params as instantiations.
self.typename = Typename(typename.namespaces + [typename.name],
self.typename = Typename(typename.name, typename.namespaces,
instantiations)

self.template_params = template_params
Expand All @@ -278,22 +302,33 @@ def __init__(self, typename: Typename, template_params: List[Type],
@staticmethod
def from_parse_result(t: ParseResults):
"""Get the TemplatedType from the parser results."""
return TemplatedType(t.typename, t.template_params, t.is_const,
t.is_shared_ptr, t.is_ptr, t.is_ref)
return TemplatedType(t.typename, t.template_params.as_list(),
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

TemplatedType.from_parse_result calls t.template_params.as_list(), but pyparsing.ParseResults exposes asList() (capital "L") rather than as_list(), so this will raise an AttributeError during parsing of templated types. Convert the parse results using asList() or an equivalent (e.g. list(t.template_params)) so that the list of Type objects is built correctly.

Suggested change
return TemplatedType(t.typename, t.template_params.as_list(),
return TemplatedType(t.typename, list(t.template_params),

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

@varunagrawal I ran copilot review on your last PR here, and it did flag the issue. Gemini recommended to just use the deprecated call asList which is compatible across versions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is not correct. Using a deprecated function call is putting a bandage on a fracture and will come back to bite us.

Updating the requirements in the pyproject.toml is the way to go.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed. Is that already updated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It is.

"pyparsing>=3.2.5",

I don't quite understand why Python/Pip is not respecting the requirement. Will look into it a bit more over the weekend after I finish the Hybrid State Estimation paper. :)

t.is_const, t.is_shared_ptr, t.is_ptr, t.is_ref)

def __repr__(self):
return "TemplatedType({typename.namespaces}::{typename.name})".format(
typename=self.typename)
return "TemplatedType({typename.namespaces}::{typename.name}<{template_params}>)".format(
typename=self.typename, template_params=self.template_params)

def get_template_params(self):
"""
Get the template args for the type as a string.
E.g. for
```
template <T = {double}, U = {string}>
class Random(){};
```
it returns `<double, string>`.

"""
# Use Type.to_cpp to do the heavy lifting for the template parameters.
return ", ".join([t.to_cpp() for t in self.template_params])

def get_typename(self):
"""
Get the typename of this type without any qualifiers.
E.g. for `const std::vector<double>& indices` this will return `std::vector<double>`.
"""
# Use Type.to_cpp to do the heavy lifting for the template parameters.
template_args = ", ".join([t.to_cpp() for t in self.template_params])

return f"{self.typename.qualified_name()}<{template_args}>"
return f"{self.typename.qualified_name()}<{self.get_template_params()}>"

def to_cpp(self):
"""
Expand Down
64 changes: 35 additions & 29 deletions gtwrap/pybind_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@

import gtwrap.interface_parser as parser
import gtwrap.template_instantiator as instantiator

from gtwrap.interface_parser.function import ArgumentList
from gtwrap.xml_parser.xml_parser import XMLDocParser


class PybindWrapper:
"""
Class to generate binding code for Pybind11 specifically.
Expand All @@ -30,7 +31,7 @@ def __init__(self,
module_name,
top_module_namespaces='',
use_boost_serialization=False,
ignore_classes=(),
ignore_classes=(),
module_template="",
xml_source=""):
self.module_name = module_name
Expand Down Expand Up @@ -76,7 +77,7 @@ def _py_args_names(self, args):
else:
return ''

def _method_args_signature(self, args):
def _method_args_signature(self, args: ArgumentList):
"""Generate the argument types and names as per the method signature."""
cpp_types = args.to_cpp()
names = args.names()
Expand Down Expand Up @@ -260,37 +261,42 @@ def _wrap_method(self,
args_names=', '.join(args_names),
))

ret = ('{prefix}.{cdef}("{py_method}",'
'[]({opt_self}{opt_comma}{args_signature_with_names}){{'
'{function_call}'
'}}'
'{py_args_names}{docstring}){suffix}'.format(
prefix=prefix,
cdef="def_static" if is_static else "def",
py_method=py_method,
opt_self="{cpp_class}* self".format(
cpp_class=cpp_class) if is_method else "",
opt_comma=', ' if is_method and args_names else '',
args_signature_with_names=args_signature_with_names,
function_call=function_call,
py_args_names=py_args_names,
suffix=suffix,
# Try to get the function's docstring from the Doxygen XML.
# If extract_docstring errors or fails to find a docstring, it just prints a warning.
# The incantation repr(...)[1:-1].replace('"', r'\"') replaces newlines with \n
# and " with \" so that the docstring can be put into a C++ string on a single line.
docstring=', "' + repr(self.xml_parser.extract_docstring(self.xml_source, cpp_class, cpp_method, method.args.names()))[1:-1].replace('"', r'\"') + '"'
if self.xml_source != "" else "",
))
result = (
'{prefix}.{cdef}("{py_method}",'
'[]({opt_self}{opt_comma}{args_signature_with_names}){{'
'{function_call}'
'}}'
'{py_args_names}{docstring}){suffix}'.format(
prefix=prefix,
cdef="def_static" if is_static else "def",
py_method=py_method,
opt_self="{cpp_class}* self".format(
cpp_class=cpp_class) if is_method else "",
opt_comma=', '
if is_method and args_signature_with_names else '',
args_signature_with_names=args_signature_with_names,
function_call=function_call,
py_args_names=py_args_names,
suffix=suffix,
# Try to get the function's docstring from the Doxygen XML.
# If extract_docstring errors or fails to find a docstring, it just prints a warning.
# The incantation repr(...)[1:-1].replace('"', r'\"') replaces newlines with \n
# and " with \" so that the docstring can be put into a C++ string on a single line.
docstring=', "' + repr(
self.xml_parser.extract_docstring(
self.xml_source, cpp_class, cpp_method,
method.args.names()))[1:-1].replace('"', r'\"') +
'"' if self.xml_source != "" else "",
))

# Create __repr__ override
# We allow all arguments to .print() and let the compiler handle type mismatches.
if method.name == 'print':
ret = self._wrap_print(ret, method, cpp_class, args_names,
args_signature_with_names, py_args_names,
prefix, suffix)
result = self._wrap_print(result, method, cpp_class, args_names,
args_signature_with_names, py_args_names,
prefix, suffix)

return ret
return result

def wrap_dunder_methods(self,
methods,
Expand Down
20 changes: 11 additions & 9 deletions gtwrap/template_instantiator/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import gtwrap.interface_parser as parser
from gtwrap.template_instantiator.constructor import InstantiatedConstructor
from gtwrap.template_instantiator.helpers import (InstantiationHelper,
from gtwrap.template_instantiator.helpers import (InstantiatedMember,
InstantiationHelper,
instantiate_args_list,
instantiate_name,
instantiate_return_type,
Expand Down Expand Up @@ -57,7 +58,7 @@ def __init__(self, original: parser.Class, instantiations=(), new_name=''):

# Instantiate all instance methods
self.methods = self.instantiate_methods(typenames)

self.dunder_methods = original.dunder_methods

super().__init__(
Expand Down Expand Up @@ -99,9 +100,11 @@ def instantiate_parent_class(self, typenames):
"""

if isinstance(self.original.parent_class, parser.type.TemplatedType):
return instantiate_type(
self.original.parent_class, typenames, self.instantiations,
parser.Typename(self.namespaces())).typename
namespaces = self.namespaces()
typename = parser.Typename(name=namespaces[-1],
namespaces=namespaces[:-1])
return instantiate_type(self.original.parent_class, typenames,
Comment on lines 102 to +106
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

instantiate_parent_class checks isinstance(self.original.parent_class, parser.type.TemplatedType), but gtwrap.interface_parser does not define a type attribute, so this branch will never be taken and will likely raise an AttributeError when evaluated. To correctly detect templated parent classes, reference TemplatedType via the public API (e.g. parser.TemplatedType) or import TemplatedType from gtwrap.interface_parser.type explicitly.

Copilot uses AI. Check for mistakes.
self.instantiations, typename).typename
else:
return self.original.parent_class

Expand Down Expand Up @@ -140,7 +143,7 @@ def instantiate_static_methods(self, typenames):

return instantiated_static_methods

def instantiate_methods(self, typenames):
def instantiate_methods(self, typenames) -> list[InstantiatedMember]:
"""
Instantiate regular methods in the class.

Expand Down Expand Up @@ -225,9 +228,8 @@ def cpp_typename(self):
", ".join([inst.to_cpp() for inst in self.instantiations]))
else:
name = self.original.name
namespaces_name = self.namespaces()
namespaces_name.append(name)
return parser.Typename(namespaces_name)

return parser.Typename(name=name, namespaces=self.namespaces())

def to_cpp(self):
"""Generate the C++ code for wrapping."""
Expand Down
4 changes: 1 addition & 3 deletions gtwrap/template_instantiator/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ def to_cpp(self):
]
name = "{}<{}>".format(self.original.name,
",".join(instantiated_names))
namespaces_name = self.namespaces()
namespaces_name.append(name)
# Leverage Typename to generate the fully qualified C++ name
return parser.Typename(namespaces_name).to_cpp()
return parser.Typename(name=name, namespaces=self.namespaces()).to_cpp()

def __repr__(self):
return "Instantiated {}".format(
Expand Down
2 changes: 1 addition & 1 deletion gtwrap/template_instantiator/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ def to_cpp(self):
return ret

def __repr__(self):
return f"Instantiated {super().__repr__}"
return f"Instantiated {super().__repr__()}"
Loading
Loading