diff --git a/.agents/skills/annotate-pybind-adapters/SKILL.md b/.agents/skills/annotate-pybind-adapters/SKILL.md new file mode 100644 index 0000000..5e01730 --- /dev/null +++ b/.agents/skills/annotate-pybind-adapters/SKILL.md @@ -0,0 +1,81 @@ +--- +name: annotate-pybind-adapters +description: Diagnose generated pybind compilation failures caused by wrapper interface signatures that differ from their C++ declarations, then apply the wrap `@pybind_lambda` annotation to only the affected methods, static methods, or global functions. Use for compile-annotate-regenerate migrations with full-signature callable-pointer casts, especially for omitted parameters, value/reference differences, synthetic container accessors, incompatible types, inheritance, or templates. +--- + +# Annotate Pybind Adapters + +Use `@pybind_lambda` as an explicit escape hatch when a wrapper declaration is +an adapter rather than the exact C++ callable signature. Keep full-signature +callable-pointer casts as the default. + +Read the repository instructions and the `Pybind Callable Adapters` section in +`DOCS.md` before editing an interface file. + +## Workflow + +1. Reproduce the generated pybind compilation failure with the narrowest + available build target. +2. Locate the failing binding in generated C++ and map it back to one callable + declaration in the wrapper `.i` file. +3. Inspect the real C++ header. Do not infer signature equivalence from the `.i` + file or from generator heuristics. +4. Confirm that the old forwarding call is valid even though the generated + full-signature `static_cast` does not match the C++ declaration. +5. Add the marker after any `template<...>` prefix and immediately before that + callable: + + ```text + @pybind_lambda + ReturnType method(Arguments...); + + template + @pybind_lambda + T templatedMethod(T value); + ``` + +6. Regenerate the binding and verify the annotated declaration emits a lambda + while nearby unannotated declarations still emit full-signature casts. +7. Re-run the failing compile target, relevant wrapper tests, and the full test + suite prescribed by the repository. +8. Report each annotation and the concrete C++ signature mismatch that requires + it. + +Use the `py312` conda environment for Python commands in this workspace. In the +standalone wrap repository, run focused pytest tests and then: + +```bash +conda run -n py312 python -m pytest tests +``` + +When working in an integrated build that provides the repository-prescribed +target, run its `make -j6 testXXX.run` target with the required permissions. + +## Decision Rules + +Annotate when the wrapper intentionally differs from C++, including: + +- omitted underlying parameters with C++ defaults; +- wrapper values that bind to C++ references; +- synthetic value-returning interfaces such as `at` or `front` over reference + returns; +- incompatible types or inherited/template declarations whose pointer signature + does not match the wrapper spelling. + +Do not annotate merely because a callable is overloaded or templated. A +full-signature cast selects an exact overload even when other overloads appear +only in the C++ header. The generator also retains its existing automatic +template-specialization and adapter cases. + +## Guardrails + +- Annotate one callable declaration at a time; never annotate a class or file in + bulk. +- Do not add symbol-name lists, GTSAM-specific heuristics, or guessed mismatch + detection to wrap. +- Do not change declaration/binding order or type-registration behavior while + fixing an adapter compile failure. +- Preserve Python names, defaults, policies, docstrings, and overload exposure. +- Treat the marker as pybind-only. MATLAB output must remain unchanged. +- Remove no existing automatic lambdas unless the task explicitly changes their + semantics. diff --git a/.agents/skills/annotate-pybind-adapters/agents/openai.yaml b/.agents/skills/annotate-pybind-adapters/agents/openai.yaml new file mode 100644 index 0000000..cc7ffb5 --- /dev/null +++ b/.agents/skills/annotate-pybind-adapters/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Annotate Pybind Adapters" + short_description: "Annotate mismatched pybind callables safely" + default_prompt: "Use $annotate-pybind-adapters to diagnose this generated pybind compile failure and annotate only the mismatched callable." diff --git a/DOCS.md b/DOCS.md index e7457e4..7711139 100644 --- a/DOCS.md +++ b/DOCS.md @@ -117,6 +117,7 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the ```cpp template ``` + - Global variables - Similar to global functions, the wrapper supports global variables as well. - Currently we only support primitive types, such as `double`, `int`, `string`, etc. @@ -225,6 +226,93 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the - Unfortunately, this means that aliases can no longer be used. - Similarly, there can be multiple `preamble.h` and `specializations.h` files. Each of these should match the module file name. +## Pybind Callable Adapters + +The Python generator uses explicit full-signature C++ callable-pointer casts for +ordinary wrapper declarations. For example: + +```cpp +class Example { + int size() const; + static Example Create(); +}; + +int globalFunction(int value); +``` + +generates bindings equivalent to: + +```cpp +static_cast(&Example::size) +static_cast(&Example::Create) +static_cast(&::globalFunction) +``` + +The return type, argument types, class type, and member constness are always +part of the cast. This selects an exact overload even when other overloads exist +in the C++ header but are not listed in the `.i` file. + +A wrapper `.i` declaration does not always reproduce the underlying C++ +signature exactly. Add `@pybind_lambda` to one method, static method, or global +function when its wrapper signature is intentionally an adapter: + +```cpp +class Example { + @pybind_lambda + int lookup(int index) const; + + @pybind_lambda + static Example Load(string filename); + + template + @pybind_lambda + T convert(T value) const; +}; + +@pybind_lambda +int globalFunction(int value); +``` + +Place the annotation after any `template<...>` declaration and immediately +before the callable. The annotated instance method is emitted using the existing +forwarding-lambda path, for example: + +```cpp +.def("lookup", + [](Example* self, int index) { + return self->lookup(index); + }, + py::arg("index")) +``` + +The annotation is useful when the interface intentionally: + +- omits underlying C++ parameters that have defaults; +- accepts values where the C++ function accepts references; +- synthesizes a value-returning container operation such as `at` or `front`; +- uses types that are not equivalent to the callable's declared signature; or +- otherwise adapts an inherited or templated member's signature. + +Do not annotate a callable merely because it is overloaded, including when an +overload exists only in the C++ header. An unannotated `.i` declaration whose +complete function type matches the desired C++ overload uses the generated +full-signature cast. Likewise, existing automatic lambdas for template +specializations, renamed bindings, print/repr, serialization, and synthesized +dunder methods do not need this marker. + +`@pybind_lambda` affects only pybind output. MATLAB generation ignores the +stored marker. Template instantiation preserves the marker on every instantiated +callable. Python-visible names, keyword escaping, default arguments, argument +policies, return-value policies such as `reference_internal`, and generated +docstrings are appended exactly as they are for the normal binding path. + +Wrap does not inspect the C++ AST and cannot determine whether an interface +signature exactly matches the real declaration. Use the annotation only after +comparing the `.i` declaration with the C++ header or after diagnosing a +generated-code compilation error. Unknown annotations and annotations on +constructors, properties, classes, enums, or other unsupported declarations are +parser errors. + ### TODO - Handle `gtsam::Rot3M` conversions to quaternions. - Parse return of const ref arguments. diff --git a/README.md b/README.md index 1dfdda1..e049a9f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ For more information, please follow our [tutorial](https://github.com/borglab/gt ## Documentation -Documentation for wrapping C++ code can be found [here](https://github.com/borglab/wrap/blob/master/DOCS.md). +Documentation for wrapping C++ code can be found [here](https://github.com/borglab/wrap/blob/master/DOCS.md), including the [pybind callable-adapter annotation](https://github.com/borglab/wrap/blob/master/DOCS.md#pybind-callable-adapters). ## Python Wrapper diff --git a/gtwrap/interface_parser/annotations.py b/gtwrap/interface_parser/annotations.py new file mode 100644 index 0000000..f23de16 --- /dev/null +++ b/gtwrap/interface_parser/annotations.py @@ -0,0 +1,36 @@ +"""Pybind-specific annotations supported by wrapper interface files.""" + +from pyparsing import Regex + +from .diagnostics import semantic_error +from .template import Template + + +PYBIND_LAMBDA = Regex(r"@pybind_lambda(?![A-Za-z0-9_])") + + +def _reject_annotation(source, location, tokens): + """Raise a useful error for unknown or misplaced annotations.""" + annotation = tokens[0] + if annotation == "@pybind_lambda": + message = ( + "annotation '@pybind_lambda' can only be applied to a method, " + "static method, or global function" + ) + else: + message = f"malformed or unknown annotation '{annotation}'" + + raise semantic_error( + source, + location, + "callable annotation", + message, + "place '@pybind_lambda' after any template declaration and " + "immediately before the callable declaration", + ) + + +UNSUPPORTED_ANNOTATION = Regex(r"@[^\s;{}()]+|@").set_parse_action( + _reject_annotation) +UNSUPPORTED_TEMPLATED_ANNOTATION = ( + Template.rule.suppress() + UNSUPPORTED_ANNOTATION) diff --git a/gtwrap/interface_parser/classes.py b/gtwrap/interface_parser/classes.py index e040dd7..b65d745 100644 --- a/gtwrap/interface_parser/classes.py +++ b/gtwrap/interface_parser/classes.py @@ -15,6 +15,8 @@ from pyparsing import ZeroOrMore # type: ignore from pyparsing import Literal, Optional, Word, alphas +from .annotations import (PYBIND_LAMBDA, UNSUPPORTED_ANNOTATION, + UNSUPPORTED_TEMPLATED_ANNOTATION) from .enum import Enum from .function import ArgumentList, ReturnType from .template import Template @@ -39,6 +41,7 @@ class Hello { """ rule = ( Optional(Template.rule("template")) # + + Optional(PYBIND_LAMBDA("pybind_lambda")) # + ReturnType.rule("return_type") # + IDENT("name") # + LPAREN # @@ -46,8 +49,14 @@ class Hello { + RPAREN # + Optional(CONST("is_const")) # + SEMI_COLON # BR - ).set_parse_action(lambda t: Method(t.template, t.name, t.return_type, t. - args_list, t.is_const)) + ).set_parse_action(lambda t: Method( + t.template, + t.name, + t.return_type, + t.args_list, + t.is_const, + force_pybind_lambda=bool(t.pybind_lambda), + )) def __init__(self, template: Union[Template, Any], @@ -55,12 +64,14 @@ def __init__(self, return_type: ReturnType, args: ArgumentList, is_const: str, - parent: Union["Class", Any] = ''): + parent: Union["Class", Any] = '', + force_pybind_lambda: bool = False): self.template = template self.name = name self.return_type = return_type self.args = args self.is_const = is_const + self.force_pybind_lambda = force_pybind_lambda self.parent = parent @@ -91,6 +102,7 @@ class Hello { """ rule = ( Optional(Template.rule("template")) # + + Optional(PYBIND_LAMBDA("pybind_lambda")) # + STATIC # + ReturnType.rule("return_type") # + IDENT("name") # @@ -98,19 +110,26 @@ class Hello { + ArgumentList.rule("args_list") # + RPAREN # + SEMI_COLON # BR - ).set_parse_action( - lambda t: StaticMethod(t.name, t.return_type, t.args_list, t.template)) + ).set_parse_action(lambda t: StaticMethod( + t.name, + t.return_type, + t.args_list, + t.template, + force_pybind_lambda=bool(t.pybind_lambda), + )) def __init__(self, name: str, return_type: ReturnType, args: ArgumentList, template: Union[Template, Any] = None, - parent: Union["Class", Any] = ''): + parent: Union["Class", Any] = '', + force_pybind_lambda: bool = False): self.name = name self.return_type = return_type self.args = args self.template = template + self.force_pybind_lambda = force_pybind_lambda self.parent = parent @@ -291,6 +310,8 @@ class Members: ^ Variable.rule # ^ Operator.rule # ^ Enum.rule # + ^ UNSUPPORTED_TEMPLATED_ANNOTATION # + ^ UNSUPPORTED_ANNOTATION # ).set_parse_action(lambda t: Class.Members(t.as_list())) def __init__(self, members: List[Union[Constructor, Method, diff --git a/gtwrap/interface_parser/function.py b/gtwrap/interface_parser/function.py index 6bcc7b4..fd975ea 100644 --- a/gtwrap/interface_parser/function.py +++ b/gtwrap/interface_parser/function.py @@ -14,6 +14,7 @@ from pyparsing import Literal, Optional, ParseResults, DelimitedList +from .annotations import PYBIND_LAMBDA from .template import Template from .tokens import (COMMA, DEFAULT_ARG, EQUAL, IDENT, LOPBRACK, LPAREN, PAIR, ROPBRACK, RPAREN, SEMI_COLON) @@ -156,25 +157,34 @@ class GlobalFunction: Rule to parse functions defined in the global scope. """ rule = ( - Optional(Template.rule("template")) + ReturnType.rule("return_type") # + Optional(Template.rule("template")) # + + Optional(PYBIND_LAMBDA("pybind_lambda")) # + + ReturnType.rule("return_type") # + IDENT("name") # + LPAREN # + ArgumentList.rule("args_list") # + RPAREN # + SEMI_COLON # - ).set_parse_action(lambda t: GlobalFunction(t.name, t.return_type, t. - args_list, t.template)) + ).set_parse_action(lambda t: GlobalFunction( + t.name, + t.return_type, + t.args_list, + t.template, + force_pybind_lambda=bool(t.pybind_lambda), + )) def __init__(self, name: str, return_type: ReturnType, args_list: ArgumentList, template: Template, - parent: Any = ''): + parent: Any = '', + force_pybind_lambda: bool = False): self.name = name self.return_type = return_type self.args = args_list self.template = template + self.force_pybind_lambda = force_pybind_lambda self.parent = parent self.return_type.parent = self diff --git a/gtwrap/interface_parser/module.py b/gtwrap/interface_parser/module.py index 365f2e3..2c0fe63 100644 --- a/gtwrap/interface_parser/module.py +++ b/gtwrap/interface_parser/module.py @@ -15,6 +15,8 @@ from pyparsing import (ParseBaseException, ZeroOrMore, cpp_style_comment, # type: ignore string_end) +from .annotations import (UNSUPPORTED_ANNOTATION, + UNSUPPORTED_TEMPLATED_ANNOTATION) from .classes import Class from .declaration import ForwardDeclaration, Include from .enum import Enum @@ -45,6 +47,8 @@ class Module: ^ Enum.rule # ^ Variable.rule # ^ Namespace.rule # + ^ UNSUPPORTED_TEMPLATED_ANNOTATION # + ^ UNSUPPORTED_ANNOTATION # ).set_parse_action(lambda t: Namespace('', t.as_list())) + string_end) diff --git a/gtwrap/interface_parser/namespace.py b/gtwrap/interface_parser/namespace.py index 90a3d40..741b403 100644 --- a/gtwrap/interface_parser/namespace.py +++ b/gtwrap/interface_parser/namespace.py @@ -16,6 +16,8 @@ from pyparsing import Forward, ParseResults, ZeroOrMore # type: ignore +from .annotations import (UNSUPPORTED_ANNOTATION, + UNSUPPORTED_TEMPLATED_ANNOTATION) from .classes import Class, collect_namespaces from .declaration import ForwardDeclaration, Include from .enum import Enum @@ -72,6 +74,8 @@ class Namespace: ^ Enum.rule # ^ Variable.rule # ^ rule # + ^ UNSUPPORTED_TEMPLATED_ANNOTATION # + ^ UNSUPPORTED_ANNOTATION # )("content") # BR + RBRACE # ).set_parse_action(lambda t: Namespace.from_parse_result(t)) diff --git a/gtwrap/pybind_wrapper.py b/gtwrap/pybind_wrapper.py index d24364d..64fe9d6 100755 --- a/gtwrap/pybind_wrapper.py +++ b/gtwrap/pybind_wrapper.py @@ -111,6 +111,37 @@ def _method_args_signature(self, args: ArgumentList): return ', '.join(types_names) + @staticmethod + def _is_static_method(method): + return isinstance( + method, + (parser.StaticMethod, instantiator.InstantiatedStaticMethod)) + + @staticmethod + def _is_specialized_callable(callable_): + """Return whether the callable is an explicit function-template specialization.""" + instantiated_callables = (instantiator.InstantiatedMethod, + instantiator.InstantiatedStaticMethod, + instantiator.InstantiatedGlobalFunction) + return (isinstance(callable_, instantiated_callables) + and bool(callable_.original.template)) + + @staticmethod + def _full_signature_cast(cpp_target, + return_type, + args, + cpp_class=None, + is_const=False): + """Generate a full function or member-function pointer cast.""" + cpp_args = ', '.join(args.to_cpp()) + if cpp_class is None: + pointer_type = f'{return_type.to_cpp()} (*)({cpp_args})' + else: + const_qualifier = ' const' if is_const else '' + pointer_type = (f'{return_type.to_cpp()} ({cpp_class}::*)' + f'({cpp_args}){const_qualifier}') + return f'static_cast<{pointer_type}>({cpp_target})' + def wrap_ctors(self, my_class): """Wrap the constructors.""" res = "" @@ -270,9 +301,7 @@ def _wrap_method(self, is_method = isinstance( method, (parser.Method, instantiator.InstantiatedMethod)) - is_static = isinstance( - method, - (parser.StaticMethod, instantiator.InstantiatedStaticMethod)) + is_static = self._is_static_method(method) return_void = method.return_type.is_void() return_type = getattr(method.return_type, 'type1', None) return_ref = getattr(return_type, 'is_ref', False) @@ -287,44 +316,62 @@ def _wrap_method(self, lambda_ret = '' ref_policy = '' - caller = cpp_class + "::" if not is_method else "self->" - function_call = ('{opt_return} {caller}{method_name}' - '({args_names});'.format( - opt_return='return' if not return_void else '', - caller=caller, - method_name=cpp_method, - args_names=', '.join(args_names), - )) - - result = ( - '{prefix}.{cdef}("{py_method}",' - '[]({opt_self}{opt_comma}{args_signature_with_names}){lambda_ret}{{' - '{function_call}' - '}}' - '{ref_policy}{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, - lambda_ret=lambda_ret, - function_call=function_call, - ref_policy=ref_policy, - 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 "", - )) + # 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 "") + + requires_lambda = (method.force_pybind_lambda + or self._is_specialized_callable(method) + or bool(method_suffix) or method.name == 'print') + + if requires_lambda: + caller = cpp_class + "::" if not is_method else "self->" + function_call = ('{opt_return} {caller}{method_name}' + '({args_names});'.format( + opt_return='return' + if not return_void else '', + caller=caller, + method_name=cpp_method, + args_names=', '.join(args_names), + )) + callable_binding = ( + '[]({opt_self}{opt_comma}{args_signature_with_names})' + '{lambda_ret}{{{function_call}}}'.format( + opt_self=f'{cpp_class}* self' if is_method else '', + opt_comma=', ' + if is_method and args_signature_with_names else '', + args_signature_with_names=args_signature_with_names, + lambda_ret=lambda_ret, + function_call=function_call, + )) + else: + cpp_target = f'&{cpp_class}::{cpp_method}' + callable_binding = self._full_signature_cast( + cpp_target, + method.return_type, + method.args, + cpp_class=None if is_static else cpp_class, + is_const=is_method + and bool(getattr(method, 'is_const', False)), + ) + + result = ('{prefix}.{cdef}("{py_method}",{callable_binding}' + '{ref_policy}{py_args_names}{docstring}){suffix}'.format( + prefix=prefix, + cdef="def_static" if is_static else "def", + py_method=py_method, + callable_binding=callable_binding, + ref_policy=ref_policy, + py_args_names=py_args_names, + docstring=docstring, + suffix=suffix, + )) # Create __repr__ override # We allow all arguments to .print() and let the compiler handle type mismatches. @@ -518,9 +565,11 @@ def wrap_instantiated_class( class_declaration=class_declaration, wrapped_ctors=self.wrap_ctors(instantiated_class), wrapped_methods=self.wrap_methods( - instantiated_class.methods, cpp_class), + instantiated_class.methods, + cpp_class), wrapped_static_methods=self.wrap_methods( - instantiated_class.static_methods, cpp_class), + instantiated_class.static_methods, + cpp_class), wrapped_dunder_methods=self.wrap_dunder_methods( instantiated_class.dunder_methods, cpp_class), wrapped_properties=self.wrap_properties( @@ -562,10 +611,12 @@ def wrap_stl_class(self, stl_class): (', ' if stl_class.parent_class else ''), module_var=module_var, wrapped_ctors=self.wrap_ctors(stl_class), - wrapped_methods=self.wrap_methods(stl_class.methods, - cpp_class), + wrapped_methods=self.wrap_methods( + stl_class.methods, + cpp_class), wrapped_static_methods=self.wrap_methods( - stl_class.static_methods, cpp_class), + stl_class.static_methods, + cpp_class), wrapped_properties=self.wrap_properties( stl_class.properties, cpp_class), )) @@ -597,25 +648,33 @@ def wrap_functions(self, args_signature = self._method_args_signature(function.args) caller = namespace + "::" - function_call = ('{opt_return} {caller}{function_name}' - '({args_names});'.format( - opt_return='return' - if not return_void else '', - caller=caller, - function_name=cpp_method, - args_names=', '.join(args_names), - )) + if (function.force_pybind_lambda + or self._is_specialized_callable(function)): + function_call = ('{opt_return} {caller}{function_name}' + '({args_names});'.format( + opt_return='return' + if not return_void else '', + caller=caller, + function_name=cpp_method, + args_names=', '.join(args_names), + )) + callable_binding = ('[]({args_signature}){{' + '{function_call}' + '}}'.format( + args_signature=args_signature, + function_call=function_call, + )) + else: + cpp_target = f'&{caller}{cpp_method}' + callable_binding = self._full_signature_cast( + cpp_target, function.return_type, function.args) - ret = ('{prefix}.{cdef}("{function_name}",' - '[]({args_signature}){{' - '{function_call}' - '}}' + ret = ('{prefix}.{cdef}("{function_name}",{callable_binding}' '{py_args_names}){suffix}'.format( prefix=prefix, cdef="def_static" if is_static else "def", function_name=function_name, - args_signature=args_signature, - function_call=function_call, + callable_binding=callable_binding, py_args_names=py_args_names, suffix=suffix)) diff --git a/gtwrap/template_instantiator/function.py b/gtwrap/template_instantiator/function.py index ec7c829..b94c906 100644 --- a/gtwrap/template_instantiator/function.py +++ b/gtwrap/template_instantiator/function.py @@ -50,7 +50,8 @@ def __init__(self, original, instantiations=(), new_name=''): self.return_type, self.args, self.template, - parent=self.parent) + parent=self.parent, + force_pybind_lambda=original.force_pybind_lambda) def to_cpp(self): """Generate the C++ code for wrapping.""" diff --git a/gtwrap/template_instantiator/method.py b/gtwrap/template_instantiator/method.py index cd0a09c..ea199c6 100644 --- a/gtwrap/template_instantiator/method.py +++ b/gtwrap/template_instantiator/method.py @@ -35,7 +35,8 @@ def __init__(self, self.return_type, self.args, self.is_const, - parent=self.parent) + parent=self.parent, + force_pybind_lambda=original.force_pybind_lambda) @classmethod def construct(cls, original, typenames, class_instantiations, @@ -51,6 +52,7 @@ def construct(cls, original, typenames, class_instantiations, args=parser.ArgumentList(instantiated_args), is_const=original.is_const, parent=parent, + force_pybind_lambda=original.force_pybind_lambda, ) return InstantiatedMethod(method, instantiations=method_instantiations) @@ -87,7 +89,8 @@ def __init__(self, self.parent = original.parent super().__init__(self.name, self.return_type, self.args, self.template, - self.parent) + self.parent, + force_pybind_lambda=original.force_pybind_lambda) @classmethod def construct(cls, original, typenames, class_instantiations, @@ -104,6 +107,7 @@ def construct(cls, original, typenames, class_instantiations, args=parser.ArgumentList(instantiated_args), template=original.template, parent=parent, + force_pybind_lambda=original.force_pybind_lambda, ) return InstantiatedStaticMethod(method, instantiations=method_instantiations) diff --git a/tests/expected/python/class_pybind.cpp b/tests/expected/python/class_pybind.cpp index c1844f9..95092ba 100644 --- a/tests/expected/python/class_pybind.cpp +++ b/tests/expected/python/class_pybind.cpp @@ -37,41 +37,41 @@ PYBIND11_MODULE(class_py, m_) { py::class_>(m_, "FunRange") .def(py::init<>()) - .def("range",[](FunRange* self, double d){return self->range(d);}, gtwrap::internal::py_arg("d")) - .def_static("create",[](){return FunRange::create();}); + .def("range",static_cast(&FunRange::range), gtwrap::internal::py_arg("d")) + .def_static("create",static_cast(&FunRange::create)); py::class_, std::shared_ptr>>(m_, "FunDouble") .def("templatedMethodString",[](Fun* self, double d, string t){return self->templatedMethod(d, t);}, gtwrap::internal::py_arg("d"), gtwrap::internal::py_arg("t")) .def("multiTemplatedMethodStringSize_t",[](Fun* self, double d, string t, size_t u){return self->multiTemplatedMethod(d, t, u);}, gtwrap::internal::py_arg("d"), gtwrap::internal::py_arg("t"), gtwrap::internal::py_arg("u")) - .def("sets",[](Fun* self){return self->sets();}) - .def_static("staticMethodWithThis",[](){return Fun::staticMethodWithThis();}) + .def("sets",static_cast::double> (Fun::*)()>(&Fun::sets)) + .def_static("staticMethodWithThis",static_cast (*)()>(&Fun::staticMethodWithThis)) .def_static("templatedStaticMethodInt",[](const int& m){return Fun::templatedStaticMethod(m);}, gtwrap::internal::py_arg("m")); py::class_>(m_, "Test") .def(py::init<>()) .def(py::init(), gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b")) - .def("return_pair",[](Test* self, const gtsam::Vector& v, const gtsam::Matrix& A){return self->return_pair(v, A);}, gtwrap::internal::py_arg("v"), gtwrap::internal::py_arg("A")) - .def("return_pair",[](Test* self, const gtsam::Vector& v){return self->return_pair(v);}, gtwrap::internal::py_arg("v")) - .def("return_bool",[](Test* self, bool value){return self->return_bool(value);}, gtwrap::internal::py_arg("value")) - .def("return_size_t",[](Test* self, size_t value){return self->return_size_t(value);}, gtwrap::internal::py_arg("value")) - .def("return_int",[](Test* self, int value){return self->return_int(value);}, gtwrap::internal::py_arg("value")) - .def("return_double",[](Test* self, double value){return self->return_double(value);}, gtwrap::internal::py_arg("value")) - .def("return_string",[](Test* self, string value){return self->return_string(value);}, gtwrap::internal::py_arg("value")) - .def("return_vector1",[](Test* self, const gtsam::Vector& value){return self->return_vector1(value);}, gtwrap::internal::py_arg("value")) - .def("return_matrix1",[](Test* self, const gtsam::Matrix& value){return self->return_matrix1(value);}, gtwrap::internal::py_arg("value")) - .def("return_vector2",[](Test* self, const gtsam::Vector& value){return self->return_vector2(value);}, gtwrap::internal::py_arg("value")) - .def("return_matrix2",[](Test* self, const gtsam::Matrix& value){return self->return_matrix2(value);}, gtwrap::internal::py_arg("value")) - .def("return_vector2",[](Test* self, const gtsam::Vector& value) -> const auto&{return self->return_vector2(value);}, py::return_value_policy::reference_internal, gtwrap::internal::py_arg("value")) - .def("return_matrix2",[](Test* self, const gtsam::Matrix& value) -> const auto&{return self->return_matrix2(value);}, py::return_value_policy::reference_internal, gtwrap::internal::py_arg("value")) - .def("arg_EigenConstRef",[](Test* self, const gtsam::Matrix& value){ self->arg_EigenConstRef(value);}, gtwrap::internal::py_arg("value")) - .def("push_back",[](Test* self, gtsam::Key key){ self->push_back(key);}, gtwrap::internal::py_arg("key")) - .def("return_field",[](Test* self, const Test& t){return self->return_field(t);}, gtwrap::internal::py_arg("t")) - .def("return_TestPtr",[](Test* self, const std::shared_ptr value){return self->return_TestPtr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_Test",[](Test* self, std::shared_ptr value){return self->return_Test(value);}, gtwrap::internal::py_arg>("value")) - .def("return_Point2Ptr",[](Test* self, bool value){return self->return_Point2Ptr(value);}, gtwrap::internal::py_arg("value")) - .def("create_ptrs",[](Test* self){return self->create_ptrs();}) - .def("create_MixedPtrs",[](Test* self){return self->create_MixedPtrs();}) - .def("return_ptrs",[](Test* self, std::shared_ptr p1, std::shared_ptr p2){return self->return_ptrs(p1, p2);}, gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) + .def("return_pair",static_cast (Test::*)(const gtsam::Vector&, const gtsam::Matrix&) const>(&Test::return_pair), gtwrap::internal::py_arg("v"), gtwrap::internal::py_arg("A")) + .def("return_pair",static_cast (Test::*)(const gtsam::Vector&) const>(&Test::return_pair), gtwrap::internal::py_arg("v")) + .def("return_bool",static_cast(&Test::return_bool), gtwrap::internal::py_arg("value")) + .def("return_size_t",static_cast(&Test::return_size_t), gtwrap::internal::py_arg("value")) + .def("return_int",static_cast(&Test::return_int), gtwrap::internal::py_arg("value")) + .def("return_double",static_cast(&Test::return_double), gtwrap::internal::py_arg("value")) + .def("return_string",static_cast(&Test::return_string), gtwrap::internal::py_arg("value")) + .def("return_vector1",static_cast(&Test::return_vector1), gtwrap::internal::py_arg("value")) + .def("return_matrix1",static_cast(&Test::return_matrix1), gtwrap::internal::py_arg("value")) + .def("return_vector2",static_cast(&Test::return_vector2), gtwrap::internal::py_arg("value")) + .def("return_matrix2",static_cast(&Test::return_matrix2), gtwrap::internal::py_arg("value")) + .def("return_vector2",static_cast(&Test::return_vector2), py::return_value_policy::reference_internal, gtwrap::internal::py_arg("value")) + .def("return_matrix2",static_cast(&Test::return_matrix2), py::return_value_policy::reference_internal, gtwrap::internal::py_arg("value")) + .def("arg_EigenConstRef",static_cast(&Test::arg_EigenConstRef), gtwrap::internal::py_arg("value")) + .def("push_back",static_cast(&Test::push_back), gtwrap::internal::py_arg("key")) + .def("return_field",static_cast(&Test::return_field), gtwrap::internal::py_arg("t")) + .def("return_TestPtr",static_cast (Test::*)(const std::shared_ptr) const>(&Test::return_TestPtr), gtwrap::internal::py_arg>("value")) + .def("return_Test",static_cast) const>(&Test::return_Test), gtwrap::internal::py_arg>("value")) + .def("return_Point2Ptr",static_cast (Test::*)(bool) const>(&Test::return_Point2Ptr), gtwrap::internal::py_arg("value")) + .def("create_ptrs",static_cast,std::shared_ptr> (Test::*)() const>(&Test::create_ptrs)) + .def("create_MixedPtrs",static_cast> (Test::*)() const>(&Test::create_MixedPtrs)) + .def("return_ptrs",static_cast,std::shared_ptr> (Test::*)(std::shared_ptr, std::shared_ptr) const>(&Test::return_ptrs), gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) .def("print",[](Test* self){ py::scoped_ostream_redirect output; self->print();}) .def("__repr__", [](const Test& self){ @@ -79,19 +79,19 @@ PYBIND11_MODULE(class_py, m_) { self.print(); return redirect.str(); }) - .def("lambda_",[](Test* self){ self->lambda();}) - .def("set_container",[](Test* self, std::vector container){ self->set_container(container);}, gtwrap::internal::py_arg>("container")) - .def("set_container",[](Test* self, std::vector> container){ self->set_container(container);}, gtwrap::internal::py_arg>>("container")) - .def("set_container",[](Test* self, std::vector container){ self->set_container(container);}, gtwrap::internal::py_arg>("container")) - .def("get_container",[](Test* self){return self->get_container();}) - .def("_repr_markdown_",[](Test* self, const gtsam::KeyFormatter& keyFormatter){return self->markdown(keyFormatter);}, gtwrap::internal::py_arg("keyFormatter") = gtsam::DefaultKeyFormatter) + .def("lambda_",static_cast(&Test::lambda)) + .def("set_container",static_cast)>(&Test::set_container), gtwrap::internal::py_arg>("container")) + .def("set_container",static_cast>)>(&Test::set_container), gtwrap::internal::py_arg>>("container")) + .def("set_container",static_cast)>(&Test::set_container), gtwrap::internal::py_arg>("container")) + .def("get_container",static_cast> (Test::*)() const>(&Test::get_container)) + .def("_repr_markdown_",static_cast(&Test::markdown), gtwrap::internal::py_arg("keyFormatter") = gtsam::DefaultKeyFormatter) .def_readwrite("model_ptr", &Test::model_ptr) .def_readwrite("value", &Test::value) .def_readwrite("name", &Test::name); py::class_, std::shared_ptr>>(m_, "PrimitiveRefDouble") .def(py::init<>()) - .def_static("Brutal",[](const double& t){return PrimitiveRef::Brutal(t);}, gtwrap::internal::py_arg("t")); + .def_static("Brutal",static_cast (*)(const double&)>(&PrimitiveRef::Brutal), gtwrap::internal::py_arg("t")); py::class_, std::shared_ptr>>(m_, "MyVector3") .def(py::init<>()); @@ -122,7 +122,7 @@ PYBIND11_MODULE(class_py, m_) { .def(py::init&, const std::vector&, double>(), gtwrap::internal::py_arg("js"), gtwrap::internal::py_arg&>("Gs"), gtwrap::internal::py_arg&>("gs"), gtwrap::internal::py_arg("f")); py::class_>, gtsam::SmartProjectionFactor>, std::shared_ptr>>>(m_, "SmartProjectionRigFactorPinholeCameraCal3_S2") - .def("add",[](SmartProjectionRigFactor>* self, const gtsam::PinholeCamera::Measurement& measured, const gtsam::Key& poseKey, const size_t& cameraId){ self->add(measured, poseKey, cameraId);}, gtwrap::internal::py_arg::Measurement&>("measured"), gtwrap::internal::py_arg("poseKey"), gtwrap::internal::py_arg("cameraId") = 0); + .def("add",static_cast>::*)(const gtsam::PinholeCamera::Measurement&, const gtsam::Key&, const size_t&)>(&SmartProjectionRigFactor>::add), gtwrap::internal::py_arg::Measurement&>("measured"), gtwrap::internal::py_arg("poseKey"), gtwrap::internal::py_arg("cameraId") = 0); py::class_, std::shared_ptr>>(m_, "MyFactorPosePoint2") .def(py::init>(), gtwrap::internal::py_arg("key1"), gtwrap::internal::py_arg("key2"), gtwrap::internal::py_arg("measured"), gtwrap::internal::py_arg>("noiseModel")) diff --git a/tests/expected/python/enum_pybind.cpp b/tests/expected/python/enum_pybind.cpp index 398425d..985d1a0 100644 --- a/tests/expected/python/enum_pybind.cpp +++ b/tests/expected/python/enum_pybind.cpp @@ -42,8 +42,8 @@ PYBIND11_MODULE(enum_py, m_) { py::class_> pet(m_, "Pet"); pet .def(py::init(), gtwrap::internal::py_arg("name"), gtwrap::internal::py_arg("type")) - .def("setColor",[](Pet* self, const Color& color){ self->setColor(color);}, gtwrap::internal::py_arg("color")) - .def("getColor",[](Pet* self){return self->getColor();}) + .def("setColor",static_cast(&Pet::setColor), gtwrap::internal::py_arg("color")) + .def("getColor",static_cast(&Pet::getColor)) .def_readwrite("name", &Pet::name) .def_readwrite("type", &Pet::type); @@ -86,9 +86,9 @@ PYBIND11_MODULE(enum_py, m_) { py::class_, std::shared_ptr>> optimizergaussnewtonparams(m_gtsam, "OptimizerGaussNewtonParams"); optimizergaussnewtonparams .def(py::init::Verbosity&>(), gtwrap::internal::py_arg::Verbosity&>("verbosity")) - .def("setVerbosity",[](gtsam::Optimizer* self, const Optimizer::Verbosity value){ self->setVerbosity(value);}, gtwrap::internal::py_arg::Verbosity>("value")) - .def("getVerbosity",[](gtsam::Optimizer* self){return self->getVerbosity();}) - .def("getVerbosity",[](gtsam::Optimizer* self){return self->getVerbosity();}); + .def("setVerbosity",static_cast::*)(const Optimizer::Verbosity)>(>sam::Optimizer::setVerbosity), gtwrap::internal::py_arg::Verbosity>("value")) + .def("getVerbosity",static_cast::*)() const>(>sam::Optimizer::getVerbosity)) + .def("getVerbosity",static_cast::*)() const>(>sam::Optimizer::getVerbosity)); py::enum_::Verbosity>(optimizergaussnewtonparams, "Verbosity", py::arithmetic()) .value("SILENT", gtsam::Optimizer::Verbosity::SILENT) diff --git a/tests/expected/python/functions_pybind.cpp b/tests/expected/python/functions_pybind.cpp index 39ef57c..87d5311 100644 --- a/tests/expected/python/functions_pybind.cpp +++ b/tests/expected/python/functions_pybind.cpp @@ -34,21 +34,21 @@ PYBIND11_MODULE(functions_py, m_) { m_.doc() = "pybind11 wrapper of functions_py"; - m_.def("load2D",[](string filename, std::shared_ptr model, int maxID, bool addNoise, bool smart){return ::load2D(filename, model, maxID, addNoise, smart);}, gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg>("model"), gtwrap::internal::py_arg("maxID"), gtwrap::internal::py_arg("addNoise"), gtwrap::internal::py_arg("smart")); - m_.def("load2D",[](string filename, const std::shared_ptr model, int maxID, bool addNoise, bool smart){return ::load2D(filename, model, maxID, addNoise, smart);}, gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg>("model"), gtwrap::internal::py_arg("maxID"), gtwrap::internal::py_arg("addNoise"), gtwrap::internal::py_arg("smart")); - m_.def("load2D",[](string filename, gtsam::noiseModel::Diagonal* model){return ::load2D(filename, model);}, gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg("model")); - m_.def("aGlobalFunction",[](){return ::aGlobalFunction();}); - m_.def("overloadedGlobalFunction",[](int a){return ::overloadedGlobalFunction(a);}, gtwrap::internal::py_arg("a")); - m_.def("overloadedGlobalFunction",[](int a, double b){return ::overloadedGlobalFunction(a, b);}, gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b")); + m_.def("load2D",static_cast,std::shared_ptr> (*)(string, std::shared_ptr, int, bool, bool)>(&::load2D), gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg>("model"), gtwrap::internal::py_arg("maxID"), gtwrap::internal::py_arg("addNoise"), gtwrap::internal::py_arg("smart")); + m_.def("load2D",static_cast,std::shared_ptr> (*)(string, const std::shared_ptr, int, bool, bool)>(&::load2D), gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg>("model"), gtwrap::internal::py_arg("maxID"), gtwrap::internal::py_arg("addNoise"), gtwrap::internal::py_arg("smart")); + m_.def("load2D",static_cast,std::shared_ptr> (*)(string, gtsam::noiseModel::Diagonal*)>(&::load2D), gtwrap::internal::py_arg("filename"), gtwrap::internal::py_arg("model")); + m_.def("aGlobalFunction",static_cast(&::aGlobalFunction)); + m_.def("overloadedGlobalFunction",static_cast(&::overloadedGlobalFunction), gtwrap::internal::py_arg("a")); + m_.def("overloadedGlobalFunction",static_cast(&::overloadedGlobalFunction), gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b")); m_.def("MultiTemplatedFunctionStringSize_tDouble",[](const string& x, size_t y){return ::MultiTemplatedFunction(x, y);}, gtwrap::internal::py_arg("x"), gtwrap::internal::py_arg("y")); m_.def("MultiTemplatedFunctionDoubleSize_tDouble",[](const double& x, size_t y){return ::MultiTemplatedFunction(x, y);}, gtwrap::internal::py_arg("x"), gtwrap::internal::py_arg("y")); - m_.def("DefaultFuncInt",[](int a, int b){ ::DefaultFuncInt(a, b);}, gtwrap::internal::py_arg("a") = 123, gtwrap::internal::py_arg("b") = 0); - m_.def("DefaultFuncString",[](const string& s, const string& name){ ::DefaultFuncString(s, name);}, gtwrap::internal::py_arg("s") = "hello", gtwrap::internal::py_arg("name") = ""); - m_.def("DefaultFuncObj",[](const gtsam::KeyFormatter& keyFormatter){ ::DefaultFuncObj(keyFormatter);}, gtwrap::internal::py_arg("keyFormatter") = gtsam::DefaultKeyFormatter); - m_.def("DefaultFuncZero",[](int a, int b, double c, int d, bool e){ ::DefaultFuncZero(a, b, c, d, e);}, gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b"), gtwrap::internal::py_arg("c") = 0.0, gtwrap::internal::py_arg("d") = 0, gtwrap::internal::py_arg("e") = false); - m_.def("DefaultFuncVector",[](const std::vector& i, const std::vector& s){ ::DefaultFuncVector(i, s);}, gtwrap::internal::py_arg&>("i") = {1, 2, 3}, gtwrap::internal::py_arg&>("s") = {"borglab", "gtsam"}); - m_.def("setPose",[](const gtsam::Pose3& pose){ ::setPose(pose);}, gtwrap::internal::py_arg("pose") = gtsam::Pose3()); - m_.def("EliminateDiscrete",[](const gtsam::DiscreteFactorGraph& factors, const gtsam::Ordering& frontalKeys){return ::EliminateDiscrete(factors, frontalKeys);}, gtwrap::internal::py_arg("factors"), gtwrap::internal::py_arg("frontalKeys")); + m_.def("DefaultFuncInt",static_cast(&::DefaultFuncInt), gtwrap::internal::py_arg("a") = 123, gtwrap::internal::py_arg("b") = 0); + m_.def("DefaultFuncString",static_cast(&::DefaultFuncString), gtwrap::internal::py_arg("s") = "hello", gtwrap::internal::py_arg("name") = ""); + m_.def("DefaultFuncObj",static_cast(&::DefaultFuncObj), gtwrap::internal::py_arg("keyFormatter") = gtsam::DefaultKeyFormatter); + m_.def("DefaultFuncZero",static_cast(&::DefaultFuncZero), gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b"), gtwrap::internal::py_arg("c") = 0.0, gtwrap::internal::py_arg("d") = 0, gtwrap::internal::py_arg("e") = false); + m_.def("DefaultFuncVector",static_cast&, const std::vector&)>(&::DefaultFuncVector), gtwrap::internal::py_arg&>("i") = {1, 2, 3}, gtwrap::internal::py_arg&>("s") = {"borglab", "gtsam"}); + m_.def("setPose",static_cast(&::setPose), gtwrap::internal::py_arg("pose") = gtsam::Pose3()); + m_.def("EliminateDiscrete",static_cast,std::shared_ptr> (*)(const gtsam::DiscreteFactorGraph&, const gtsam::Ordering&)>(&::EliminateDiscrete), gtwrap::internal::py_arg("factors"), gtwrap::internal::py_arg("frontalKeys")); m_.def("triangulatePoint3Cal3_S2",[](const gtsam::Pose3Vector& poses, std::shared_ptr sharedCal, const gtsam::Point2Vector& measurements, double rank_tol, bool optimize, const gtsam::SharedNoiseModel& model){return ::triangulatePoint3(poses, sharedCal, measurements, rank_tol, optimize, model);}, gtwrap::internal::py_arg("poses"), gtwrap::internal::py_arg>("sharedCal"), gtwrap::internal::py_arg("measurements"), gtwrap::internal::py_arg("rank_tol"), gtwrap::internal::py_arg("optimize"), gtwrap::internal::py_arg("model") = nullptr); m_.def("FindKarcherMeanPoint3",[](const std::vector& elements){return ::FindKarcherMean(elements);}, gtwrap::internal::py_arg&>("elements")); m_.def("FindKarcherMeanSO3",[](const std::vector& elements){return ::FindKarcherMean(elements);}, gtwrap::internal::py_arg&>("elements")); diff --git a/tests/expected/python/geometry_pybind.cpp b/tests/expected/python/geometry_pybind.cpp index c9a43e1..a3a3dea 100644 --- a/tests/expected/python/geometry_pybind.cpp +++ b/tests/expected/python/geometry_pybind.cpp @@ -42,20 +42,20 @@ PYBIND11_MODULE(geometry_py, m_) { py::class_>(m_gtsam, "Point2") .def(py::init<>()) .def(py::init(), gtwrap::internal::py_arg("x"), gtwrap::internal::py_arg("y")) - .def("x",[](gtsam::Point2* self){return self->x();}) - .def("y",[](gtsam::Point2* self){return self->y();}) - .def("dim",[](gtsam::Point2* self){return self->dim();}) - .def("returnChar",[](gtsam::Point2* self){return self->returnChar();}) - .def("argChar",[](gtsam::Point2* self, char a){ self->argChar(a);}, gtwrap::internal::py_arg("a")) - .def("argChar",[](gtsam::Point2* self, std::shared_ptr a){ self->argChar(a);}, gtwrap::internal::py_arg>("a")) - .def("argChar",[](gtsam::Point2* self, char& a){ self->argChar(a);}, gtwrap::internal::py_arg("a")) - .def("argChar",[](gtsam::Point2* self, char* a){ self->argChar(a);}, gtwrap::internal::py_arg("a")) - .def("argChar",[](gtsam::Point2* self, const std::shared_ptr a){ self->argChar(a);}, gtwrap::internal::py_arg>("a")) - .def("argChar",[](gtsam::Point2* self, const char& a){ self->argChar(a);}, gtwrap::internal::py_arg("a")) - .def("argChar",[](gtsam::Point2* self, const char* a){ self->argChar(a);}, gtwrap::internal::py_arg("a")) - .def("argUChar",[](gtsam::Point2* self, unsigned char a){ self->argUChar(a);}, gtwrap::internal::py_arg("a")) - .def("eigenArguments",[](gtsam::Point2* self, const gtsam::Vector& v, const gtsam::Matrix& m){ self->eigenArguments(v, m);}, gtwrap::internal::py_arg("v"), gtwrap::internal::py_arg("m")) - .def("vectorConfusion",[](gtsam::Point2* self){return self->vectorConfusion();}) + .def("x",static_cast(>sam::Point2::x)) + .def("y",static_cast(>sam::Point2::y)) + .def("dim",static_cast(>sam::Point2::dim)) + .def("returnChar",static_cast(>sam::Point2::returnChar)) + .def("argChar",static_cast(>sam::Point2::argChar), gtwrap::internal::py_arg("a")) + .def("argChar",static_cast) const>(>sam::Point2::argChar), gtwrap::internal::py_arg>("a")) + .def("argChar",static_cast(>sam::Point2::argChar), gtwrap::internal::py_arg("a")) + .def("argChar",static_cast(>sam::Point2::argChar), gtwrap::internal::py_arg("a")) + .def("argChar",static_cast) const>(>sam::Point2::argChar), gtwrap::internal::py_arg>("a")) + .def("argChar",static_cast(>sam::Point2::argChar), gtwrap::internal::py_arg("a")) + .def("argChar",static_cast(>sam::Point2::argChar), gtwrap::internal::py_arg("a")) + .def("argUChar",static_cast(>sam::Point2::argUChar), gtwrap::internal::py_arg("a")) + .def("eigenArguments",static_cast(>sam::Point2::eigenArguments), gtwrap::internal::py_arg("v"), gtwrap::internal::py_arg("m")) + .def("vectorConfusion",static_cast(>sam::Point2::vectorConfusion)) .def("serialize", [](gtsam::Point2* self){ return gtsam::serialize(*self); }) .def("deserialize", [](gtsam::Point2* self, string serialized){ gtsam::deserialize(serialized, *self); }, py::arg("serialized")) .def(py::pickle( @@ -64,14 +64,14 @@ PYBIND11_MODULE(geometry_py, m_) { py::class_>(m_gtsam, "Point3") .def(py::init(), gtwrap::internal::py_arg("x"), gtwrap::internal::py_arg("y"), gtwrap::internal::py_arg("z")) - .def("norm",[](gtsam::Point3* self){return self->norm();}) + .def("norm",static_cast(>sam::Point3::norm)) .def("serialize", [](gtsam::Point3* self){ return gtsam::serialize(*self); }) .def("deserialize", [](gtsam::Point3* self, string serialized){ gtsam::deserialize(serialized, *self); }, py::arg("serialized")) .def(py::pickle( [](const gtsam::Point3 &a){ /* __getstate__: Returns a string that encodes the state of the object */ return py::make_tuple(gtsam::serialize(a)); }, [](py::tuple t){ /* __setstate__ */ gtsam::Point3 obj; gtsam::deserialize(t[0].cast(), obj); return obj; })) - .def_static("staticFunction",[](){return gtsam::Point3::staticFunction();}) - .def_static("StaticFunctionRet",[](double z){return gtsam::Point3::StaticFunctionRet(z);}, gtwrap::internal::py_arg("z")); + .def_static("staticFunction",static_cast(>sam::Point3::staticFunction)) + .def_static("StaticFunctionRet",static_cast(>sam::Point3::StaticFunctionRet), gtwrap::internal::py_arg("z")); #include "python/specializations.h" diff --git a/tests/expected/python/inheritance_pybind.cpp b/tests/expected/python/inheritance_pybind.cpp index 29c1cc2..0b522ef 100644 --- a/tests/expected/python/inheritance_pybind.cpp +++ b/tests/expected/python/inheritance_pybind.cpp @@ -42,14 +42,14 @@ PYBIND11_MODULE(inheritance_py, m_) { .def("templatedMethodPoint3",[](MyTemplate* self, const gtsam::Point3& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodVector",[](MyTemplate* self, const gtsam::Vector& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodMatrix",[](MyTemplate* self, const gtsam::Matrix& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) - .def("accept_T",[](MyTemplate* self, const gtsam::Point2& value){ self->accept_T(value);}, gtwrap::internal::py_arg("value")) - .def("accept_Tptr",[](MyTemplate* self, std::shared_ptr value){ self->accept_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_Tptr",[](MyTemplate* self, std::shared_ptr value){return self->return_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_T",[](MyTemplate* self, gtsam::Point2* value){return self->return_T(value);}, gtwrap::internal::py_arg("value")) - .def("create_ptrs",[](MyTemplate* self){return self->create_ptrs();}) - .def("create_MixedPtrs",[](MyTemplate* self){return self->create_MixedPtrs();}) - .def("return_ptrs",[](MyTemplate* self, std::shared_ptr p1, std::shared_ptr p2){return self->return_ptrs(p1, p2);}, gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) - .def_static("Level",[](const gtsam::Point2& K){return MyTemplate::Level(K);}, gtwrap::internal::py_arg("K")); + .def("accept_T",static_cast::*)(const gtsam::Point2&) const>(&MyTemplate::accept_T), gtwrap::internal::py_arg("value")) + .def("accept_Tptr",static_cast::*)(std::shared_ptr) const>(&MyTemplate::accept_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_Tptr",static_cast (MyTemplate::*)(std::shared_ptr) const>(&MyTemplate::return_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_T",static_cast::*)(gtsam::Point2*) const>(&MyTemplate::return_T), gtwrap::internal::py_arg("value")) + .def("create_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)() const>(&MyTemplate::create_ptrs)) + .def("create_MixedPtrs",static_cast> (MyTemplate::*)() const>(&MyTemplate::create_MixedPtrs)) + .def("return_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)(std::shared_ptr, std::shared_ptr) const>(&MyTemplate::return_ptrs), gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) + .def_static("Level",static_cast (*)(const gtsam::Point2&)>(&MyTemplate::Level), gtwrap::internal::py_arg("K")); py::class_, MyBase, std::shared_ptr>>(m_, "MyTemplateMatrix") .def(py::init<>()) @@ -57,14 +57,14 @@ PYBIND11_MODULE(inheritance_py, m_) { .def("templatedMethodPoint3",[](MyTemplate* self, const gtsam::Point3& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodVector",[](MyTemplate* self, const gtsam::Vector& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodMatrix",[](MyTemplate* self, const gtsam::Matrix& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) - .def("accept_T",[](MyTemplate* self, const gtsam::Matrix& value){ self->accept_T(value);}, gtwrap::internal::py_arg("value")) - .def("accept_Tptr",[](MyTemplate* self, std::shared_ptr value){ self->accept_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_Tptr",[](MyTemplate* self, std::shared_ptr value){return self->return_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_T",[](MyTemplate* self, gtsam::Matrix* value){return self->return_T(value);}, gtwrap::internal::py_arg("value")) - .def("create_ptrs",[](MyTemplate* self){return self->create_ptrs();}) - .def("create_MixedPtrs",[](MyTemplate* self){return self->create_MixedPtrs();}) - .def("return_ptrs",[](MyTemplate* self, std::shared_ptr p1, std::shared_ptr p2){return self->return_ptrs(p1, p2);}, gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) - .def_static("Level",[](const gtsam::Matrix& K){return MyTemplate::Level(K);}, gtwrap::internal::py_arg("K")); + .def("accept_T",static_cast::*)(const gtsam::Matrix&) const>(&MyTemplate::accept_T), gtwrap::internal::py_arg("value")) + .def("accept_Tptr",static_cast::*)(std::shared_ptr) const>(&MyTemplate::accept_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_Tptr",static_cast (MyTemplate::*)(std::shared_ptr) const>(&MyTemplate::return_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_T",static_cast::*)(gtsam::Matrix*) const>(&MyTemplate::return_T), gtwrap::internal::py_arg("value")) + .def("create_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)() const>(&MyTemplate::create_ptrs)) + .def("create_MixedPtrs",static_cast> (MyTemplate::*)() const>(&MyTemplate::create_MixedPtrs)) + .def("return_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)(std::shared_ptr, std::shared_ptr) const>(&MyTemplate::return_ptrs), gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) + .def_static("Level",static_cast (*)(const gtsam::Matrix&)>(&MyTemplate::Level), gtwrap::internal::py_arg("K")); py::class_, MyBase, std::shared_ptr>>(m_, "MyTemplateA") .def(py::init<>()) @@ -72,21 +72,21 @@ PYBIND11_MODULE(inheritance_py, m_) { .def("templatedMethodPoint3",[](MyTemplate* self, const gtsam::Point3& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodVector",[](MyTemplate* self, const gtsam::Vector& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) .def("templatedMethodMatrix",[](MyTemplate* self, const gtsam::Matrix& t){return self->templatedMethod(t);}, gtwrap::internal::py_arg("t")) - .def("accept_T",[](MyTemplate* self, const A& value){ self->accept_T(value);}, gtwrap::internal::py_arg("value")) - .def("accept_Tptr",[](MyTemplate* self, std::shared_ptr value){ self->accept_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_Tptr",[](MyTemplate* self, std::shared_ptr value){return self->return_Tptr(value);}, gtwrap::internal::py_arg>("value")) - .def("return_T",[](MyTemplate* self, A* value){return self->return_T(value);}, gtwrap::internal::py_arg("value")) - .def("create_ptrs",[](MyTemplate* self){return self->create_ptrs();}) - .def("create_MixedPtrs",[](MyTemplate* self){return self->create_MixedPtrs();}) - .def("return_ptrs",[](MyTemplate* self, std::shared_ptr p1, std::shared_ptr p2){return self->return_ptrs(p1, p2);}, gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) - .def_static("Level",[](const A& K){return MyTemplate::Level(K);}, gtwrap::internal::py_arg("K")); + .def("accept_T",static_cast::*)(const A&) const>(&MyTemplate::accept_T), gtwrap::internal::py_arg("value")) + .def("accept_Tptr",static_cast::*)(std::shared_ptr) const>(&MyTemplate::accept_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_Tptr",static_cast (MyTemplate::*)(std::shared_ptr) const>(&MyTemplate::return_Tptr), gtwrap::internal::py_arg>("value")) + .def("return_T",static_cast::*)(A*) const>(&MyTemplate::return_T), gtwrap::internal::py_arg("value")) + .def("create_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)() const>(&MyTemplate::create_ptrs)) + .def("create_MixedPtrs",static_cast> (MyTemplate::*)() const>(&MyTemplate::create_MixedPtrs)) + .def("return_ptrs",static_cast,std::shared_ptr> (MyTemplate::*)(std::shared_ptr, std::shared_ptr) const>(&MyTemplate::return_ptrs), gtwrap::internal::py_arg>("p1"), gtwrap::internal::py_arg>("p2")) + .def_static("Level",static_cast (*)(const A&)>(&MyTemplate::Level), gtwrap::internal::py_arg("K")); py::class_, std::shared_ptr>(m_, "ForwardKinematicsFactor"); py::class_, MyTemplate, std::shared_ptr>>(m_, "ParentHasTemplateDouble"); py::class_>(m_, "Base") - .def_static("Create",[](double x){return Base::Create(x);}, gtwrap::internal::py_arg("x")); + .def_static("Create",static_cast (*)(double)>(&Base::Create), gtwrap::internal::py_arg("x")); py::class_>(m_, "Derived"); diff --git a/tests/expected/python/namespaces_pybind.cpp b/tests/expected/python/namespaces_pybind.cpp index 398b8de..dd9a00e 100644 --- a/tests/expected/python/namespaces_pybind.cpp +++ b/tests/expected/python/namespaces_pybind.cpp @@ -47,14 +47,14 @@ PYBIND11_MODULE(namespaces_py, m_) { py::class_>(m_ns1, "ClassB") .def(py::init<>()); - m_ns1.def("aGlobalFunction",[](){return ns1::aGlobalFunction();}); pybind11::module m_ns2 = m_.def_submodule("ns2", "ns2 submodule"); + m_ns1.def("aGlobalFunction",static_cast(&ns1::aGlobalFunction)); pybind11::module m_ns2 = m_.def_submodule("ns2", "ns2 submodule"); py::class_>(m_ns2, "ClassA") .def(py::init<>()) - .def("memberFunction",[](ns2::ClassA* self){return self->memberFunction();}) - .def("nsArg",[](ns2::ClassA* self, const ns1::ClassB& arg){return self->nsArg(arg);}, gtwrap::internal::py_arg("arg")) - .def("nsReturn",[](ns2::ClassA* self, double q){return self->nsReturn(q);}, gtwrap::internal::py_arg("q")) - .def_static("afunction",[](){return ns2::ClassA::afunction();}); + .def("memberFunction",static_cast(&ns2::ClassA::memberFunction)) + .def("nsArg",static_cast(&ns2::ClassA::nsArg), gtwrap::internal::py_arg("arg")) + .def("nsReturn",static_cast(&ns2::ClassA::nsReturn), gtwrap::internal::py_arg("q")) + .def_static("afunction",static_cast(&ns2::ClassA::afunction)); pybind11::module m_ns2_ns3 = m_ns2.def_submodule("ns3", "ns3 submodule"); py::class_>(m_ns2_ns3, "ClassB") @@ -64,9 +64,9 @@ PYBIND11_MODULE(namespaces_py, m_) { .def(py::init<>()); m_ns2.attr("aNs2Var") = ns2::aNs2Var; - m_ns2.def("aGlobalFunction",[](){return ns2::aGlobalFunction();}); - m_ns2.def("overloadedGlobalFunction",[](const ns1::ClassA& a){return ns2::overloadedGlobalFunction(a);}, gtwrap::internal::py_arg("a")); - m_ns2.def("overloadedGlobalFunction",[](const ns1::ClassA& a, double b){return ns2::overloadedGlobalFunction(a, b);}, gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b")); + m_ns2.def("aGlobalFunction",static_cast(&ns2::aGlobalFunction)); + m_ns2.def("overloadedGlobalFunction",static_cast(&ns2::overloadedGlobalFunction), gtwrap::internal::py_arg("a")); + m_ns2.def("overloadedGlobalFunction",static_cast(&ns2::overloadedGlobalFunction), gtwrap::internal::py_arg("a"), gtwrap::internal::py_arg("b")); py::class_>(m_, "ClassD") .def(py::init<>()); @@ -76,9 +76,9 @@ PYBIND11_MODULE(namespaces_py, m_) { .def(py::init<>()) .def(py::init(), gtwrap::internal::py_arg("other")) .def("insert_vector",[](gtsam::Values* self, size_t j, const gtsam::Vector& vector){ self->insert(j, vector);}, gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("vector")) - .def("insert",[](gtsam::Values* self, size_t j, const gtsam::Vector& vector){ self->insert(j, vector);}, gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("vector")) + .def("insert",static_cast(>sam::Values::insert), gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("vector")) .def("insert_matrix",[](gtsam::Values* self, size_t j, const gtsam::Matrix& matrix){ self->insert(j, matrix);}, gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("matrix")) - .def("insert",[](gtsam::Values* self, size_t j, const gtsam::Matrix& matrix){ self->insert(j, matrix);}, gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("matrix")); + .def("insert",static_cast(>sam::Values::insert), gtwrap::internal::py_arg("j"), gtwrap::internal::py_arg("matrix")); #include "python/specializations.h" diff --git a/tests/expected/python/pybind_lambda_adapters_pybind.cpp b/tests/expected/python/pybind_lambda_adapters_pybind.cpp new file mode 100644 index 0000000..683a010 --- /dev/null +++ b/tests/expected/python/pybind_lambda_adapters_pybind.cpp @@ -0,0 +1,57 @@ +#include + +#include "pybind_lambda_adapters.h" + +#include + +namespace gtwrap { +namespace internal { + +template +struct PyArgPolicy { + static pybind11::arg make(const char* name) { return pybind11::arg(name); } +}; + +template +pybind11::arg py_arg(const char* name) { + return PyArgPolicy::type>::make(name); +} + +} // namespace internal +} // namespace gtwrap + + +using namespace std; +namespace py = pybind11; + +PYBIND11_MODULE(pybind_lambda_adapters_py, m_) { + pybind11::module m_adapters = m_.def_submodule("adapters", "adapters submodule"); + + py::class_>(m_adapters, "BaseAdapter") + .def(py::init<>()) + .def("inherited",[](adapters::BaseAdapter* self, int value){return self->inherited(value);}, gtwrap::internal::py_arg("value")); + + py::class_, adapters::BaseAdapter, std::shared_ptr>>(m_adapters, "AdapterInt") + .def(py::init<>()) + .def("exact",static_cast::*)(int)>(&adapters::Adapter::exact), gtwrap::internal::py_arg("value")) + .def("exactConst",static_cast::*)(int) const>(&adapters::Adapter::exactConst), gtwrap::internal::py_arg("value")) + .def("omittedDefault",[](adapters::Adapter* self, int value){return self->omittedDefault(value);}, gtwrap::internal::py_arg("value")) + .def("referenceArgument",[](adapters::Adapter* self, int value){return self->referenceArgument(value);}, gtwrap::internal::py_arg("value")) + .def("hiddenOverload",static_cast::*)(int) const>(&adapters::Adapter::hiddenOverload), gtwrap::internal::py_arg("value")) + .def("declaredOverload",static_cast::*)(int) const>(&adapters::Adapter::declaredOverload), gtwrap::internal::py_arg("value")) + .def("declaredOverload",static_cast::*)(double) const>(&adapters::Adapter::declaredOverload), gtwrap::internal::py_arg("value")) + .def("at",[](adapters::Adapter* self, size_t index){return self->at(index);}, gtwrap::internal::py_arg("index")) + .def("front",[](adapters::Adapter* self){return self->front();}) + .def("alias",[](adapters::Adapter* self, int index){return self->alias(index);}, gtwrap::internal::py_arg("index")) + .def("templatedDouble",[](adapters::Adapter* self, double value){return self->templated(value);}, gtwrap::internal::py_arg("value")) + .def("lambda_",[](adapters::Adapter* self, const string& value) -> const auto&{return self->lambda(value);}, py::return_value_policy::reference_internal, gtwrap::internal::py_arg("value") = "fallback") + .def_static("exactStatic",static_cast(&adapters::Adapter::exactStatic), gtwrap::internal::py_arg("value")) + .def_static("staticOmitted",[](int value){return adapters::Adapter::staticOmitted(value);}, gtwrap::internal::py_arg("value")); + + m_adapters.def("exactGlobal",static_cast(&adapters::exactGlobal), gtwrap::internal::py_arg("value")); + m_adapters.def("globalOmitted",[](int value){return adapters::globalOmitted(value);}, gtwrap::internal::py_arg("value")); + m_adapters.def("globalHidden",static_cast(&adapters::globalHidden), gtwrap::internal::py_arg("value")); + m_adapters.def("globalOverload",static_cast(&adapters::globalOverload), gtwrap::internal::py_arg("value")); + m_adapters.def("globalOverload",static_cast(&adapters::globalOverload), gtwrap::internal::py_arg("value")); + m_adapters.def("globalTemplatedInt",[](int value){return adapters::globalTemplated(value);}, gtwrap::internal::py_arg("value")); +} diff --git a/tests/fixtures/pybind_lambda_adapters.h b/tests/fixtures/pybind_lambda_adapters.h new file mode 100644 index 0000000..301f333 --- /dev/null +++ b/tests/fixtures/pybind_lambda_adapters.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +namespace adapters { + +using Index = std::size_t; + +class BaseAdapter { + public: + BaseAdapter() = default; + + int inherited(int value, bool verbose = false) const { + return value + (verbose ? 1 : 0); + } +}; + +template +class Adapter : public BaseAdapter { + public: + Adapter() = default; + + int exact(int value) { return value; } + int exactConst(int value) const { return value; } + static int exactStatic(int value) { return value; } + + int omittedDefault(int value, bool verbose = false) const { + return value + (verbose ? 1 : 0); + } + + int referenceArgument(int& value) const { return ++value; } + + int hiddenOverload(int value) const { return value; } + int hiddenOverload(double value) const { return static_cast(value); } + + int declaredOverload(int value) const { return value; } + double declaredOverload(double value) const { return value; } + + const T& at(std::size_t) const { return value_; } + const T& front() const { return value_; } + + int alias(Index index) const { return static_cast(index); } + + static int staticOmitted(int value, bool verbose = false) { + return value + (verbose ? 1 : 0); + } + + template + U templated(U value, bool verbose = false) const { + return value + (verbose ? U{1} : U{0}); + } + + const std::string& lambda(const std::string& value = "fallback", + bool verbose = false) const { + text_ = value + (verbose ? "!" : ""); + return text_; + } + + private: + T value_{}; + mutable std::string text_; +}; + +inline int exactGlobal(int value) { return value; } + +inline int globalOmitted(int value, bool verbose = false) { + return value + (verbose ? 1 : 0); +} + +inline int globalHidden(int value) { return value; } +inline int globalHidden(double value) { return static_cast(value); } + +inline int globalOverload(int value) { return value; } +inline double globalOverload(double value) { return value; } + +template +T globalTemplated(T value, bool verbose = false) { + return value + (verbose ? T{1} : T{0}); +} + +} // namespace adapters diff --git a/tests/fixtures/pybind_lambda_adapters.i b/tests/fixtures/pybind_lambda_adapters.i new file mode 100644 index 0000000..a66b5db --- /dev/null +++ b/tests/fixtures/pybind_lambda_adapters.i @@ -0,0 +1,63 @@ +#include + +namespace adapters { + +class BaseAdapter { + BaseAdapter(); + + @pybind_lambda + int inherited(int value) const; +}; + +template +class Adapter: adapters::BaseAdapter { + Adapter(); + + int exact(int value); + int exactConst(int value) const; + static int exactStatic(int value); + + @pybind_lambda + int omittedDefault(int value) const; + + @pybind_lambda + int referenceArgument(int value) const; + + int hiddenOverload(int value) const; + + int declaredOverload(int value) const; + double declaredOverload(double value) const; + + @pybind_lambda + T at(size_t index) const; + + @pybind_lambda + T front() const; + + @pybind_lambda + int alias(int index) const; + + @pybind_lambda + static int staticOmitted(int value); + + template + U templated(U value) const; + + @pybind_lambda + const string& lambda(const string& value = "fallback") const; +}; + +int exactGlobal(int value); + +@pybind_lambda +int globalOmitted(int value); + +int globalHidden(int value); + +int globalOverload(int value); +double globalOverload(double value); + +template +T globalTemplated(T value); + +} diff --git a/tests/test_interface_parser.py b/tests/test_interface_parser.py index ba02ccc..c024083 100644 --- a/tests/test_interface_parser.py +++ b/tests/test_interface_parser.py @@ -277,11 +277,25 @@ def test_method(self): self.assertEqual("f", ret.name) self.assertEqual(0, len(ret.args)) self.assertTrue(not ret.is_const) + self.assertFalse(ret.force_pybind_lambda) - ret = Method.rule.parse_string("int f() const;")[0] + ret = Method.rule.parse_string( + "@pybind_lambda int f() const;")[0] self.assertEqual("f", ret.name) self.assertEqual(0, len(ret.args)) self.assertTrue(ret.is_const) + self.assertTrue(ret.force_pybind_lambda) + + ret = Method.rule.parse_string(""" + template + @pybind_lambda + T convert(T value) const; + """)[0] + self.assertTrue(ret.force_pybind_lambda) + self.assertEqual("convert", ret.name) + self.assertEqual("", repr(ret.template)) + self.assertEqual("double", + ret.template.instantiations[0][0].name) ret = Method.rule.parse_string( "int f(const int x, const Class& c, Class* t) const;")[0] @@ -300,11 +314,13 @@ def test_static_method(self): ret = StaticMethod.rule.parse_string("static int f();")[0] self.assertEqual("f", ret.name) self.assertEqual(0, len(ret.args)) + self.assertFalse(ret.force_pybind_lambda) ret = StaticMethod.rule.parse_string( - "static int f(const int x, const Class& c, Class* t);")[0] + "@pybind_lambda static int f(const int x, const Class& c, Class* t);")[0] self.assertEqual("f", ret.name) self.assertEqual(3, len(ret.args)) + self.assertTrue(ret.force_pybind_lambda) def test_constructor(self): """Test for class constructor.""" @@ -553,6 +569,15 @@ def test_function(self): self.assertEqual("localToWorld", func.name) self.assertEqual("Values", func.return_type.type1.typename.name) self.assertEqual(3, len(func.args)) + self.assertFalse(func.force_pybind_lambda) + + func = GlobalFunction.rule.parse_string(""" + template + @pybind_lambda + T adapt(T value); + """)[0] + self.assertEqual("adapt", func.name) + self.assertTrue(func.force_pybind_lambda) def test_global_variable(self): """Test for global variable.""" diff --git a/tests/test_matlab_wrapper.py b/tests/test_matlab_wrapper.py index 202cb3b..a2f459c 100644 --- a/tests/test_matlab_wrapper.py +++ b/tests/test_matlab_wrapper.py @@ -8,7 +8,10 @@ import filecmp import os import os.path as osp +from pathlib import Path +import re import sys +import tempfile import unittest sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) @@ -119,6 +122,47 @@ def test_matrix_view_arguments(self): self.assertIn('Eigen::Index m', header_content) self.assertIn('Stride(m, 1)', header_content) + def test_pybind_lambda_annotation_is_ignored(self): + """Pybind-only annotations do not alter generated MATLAB files.""" + source = Path(self.INTERFACE_DIR) / 'pybind_lambda_adapters.i' + annotated = source.read_text(encoding='UTF-8') + unannotated = re.sub(r'^[ \t]*@pybind_lambda[ \t]*\n', + '', + annotated, + flags=re.MULTILINE) + + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + annotated_source = root / 'annotated.i' + unannotated_source = root / 'unannotated.i' + annotated_source.write_text(annotated, encoding='UTF-8') + unannotated_source.write_text(unannotated, encoding='UTF-8') + annotated_output = root / 'annotated' + unannotated_output = root / 'unannotated' + annotated_output.mkdir() + unannotated_output.mkdir() + + MatlabWrapper( + module_name='pybind_lambda_adapters', + top_module_namespace=['adapters'], + ignore_classes=[''], + ).wrap([str(annotated_source)], path=str(annotated_output)) + MatlabWrapper( + module_name='pybind_lambda_adapters', + top_module_namespace=['adapters'], + ignore_classes=[''], + ).wrap([str(unannotated_source)], path=str(unannotated_output)) + + annotated_files = { + path.relative_to(annotated_output): path.read_bytes() + for path in annotated_output.rglob('*') if path.is_file() + } + unannotated_files = { + path.relative_to(unannotated_output): path.read_bytes() + for path in unannotated_output.rglob('*') if path.is_file() + } + self.assertEqual(annotated_files, unannotated_files) + def test_eigen_ref_jacobians(self): """Test that Eigen::Ref args are treated as Jacobian outputs. diff --git a/tests/test_parser_diagnostics.py b/tests/test_parser_diagnostics.py index a09eb67..30d4838 100644 --- a/tests/test_parser_diagnostics.py +++ b/tests/test_parser_diagnostics.py @@ -92,6 +92,50 @@ def test_unexpected_access_specifier(self): ) self.assertIn("without an access label", error.hint) + def test_unknown_callable_annotation(self): + error = self.assert_parse_error( + "@pybind_adapter int function();", + line=1, + column=1, + context="callable annotation", + expected="malformed or unknown annotation '@pybind_adapter'", + ) + self.assertIn("@pybind_lambda", error.hint) + + def test_malformed_callable_annotation(self): + self.assert_parse_error( + "@pybind-lambda int function();", + line=1, + column=1, + context="callable annotation", + expected="malformed or unknown annotation '@pybind-lambda'", + ) + + def test_misplaced_callable_annotation(self): + error = self.assert_parse_error( + "class Foo { @pybind_lambda Foo(); };", + line=1, + column=13, + context="callable annotation", + expected=( + "annotation '@pybind_lambda' can only be applied to a method, " + "static method, or global function" + ), + ) + self.assertIn("immediately before the callable", error.hint) + + def test_misplaced_annotation_after_template(self): + self.assert_parse_error( + "class Foo { template @pybind_lambda Foo(T value); };", + line=1, + column=25, + context="callable annotation", + expected=( + "annotation '@pybind_lambda' can only be applied to a method, " + "static method, or global function" + ), + ) + def test_nested_namespace_failure(self): self.assert_parse_error( "namespace ns { class Foo { void bar(???); }; }", diff --git a/tests/test_pybind_wrapper.py b/tests/test_pybind_wrapper.py index 1341706..b4eb7a0 100644 --- a/tests/test_pybind_wrapper.py +++ b/tests/test_pybind_wrapper.py @@ -9,7 +9,11 @@ import filecmp import os import os.path as osp +import shlex +import shutil +import subprocess import sys +import sysconfig import unittest sys.path.append(osp.dirname(osp.dirname(osp.abspath(__file__)))) @@ -31,6 +35,18 @@ class TestWrap(unittest.TestCase): # Create the `actual/python` directory os.makedirs(PYTHON_ACTUAL_DIR, exist_ok=True) + MINIMAL_MODULE_TEMPLATE = """#include + +{includes} + +using namespace std; +namespace py = pybind11; + +PYBIND11_MODULE({module_name}, m_) {{ +{wrapped_namespace} +}} +""" + def wrap_content(self, sources, module_name, @@ -68,7 +84,7 @@ def compare_and_diff(self, file, actual): and assert if diff is zero. """ expected = osp.join(self.PYTHON_TEST_DIR, file) - success = filecmp.cmp(actual, expected) + success = filecmp.cmp(actual, expected, shallow=False) if not success: os.system(f"diff {actual} {expected}") @@ -193,6 +209,280 @@ def test_argument_policy_hook(self): 'gtwrap::internal::py_arg("values")', content) self.assertIn('gtwrap::internal::py_arg("count") = 1', content) + self.assertIn( + 'static_cast' + '(&testing::ArgPolicyFixture::noConvert)', content) + + def test_full_signature_callable_bindings(self): + """Test full-signature pointer casts for ordinary callables.""" + source = osp.join(self.INTERFACE_DIR, 'class.i') + output = self.wrap_content([source], 'class_py', + self.PYTHON_ACTUAL_DIR) + + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + + # Const/non-const methods and static methods use explicit pointer types. + self.assertIn( + 'static_cast(&Test::return_bool)', + content) + self.assertIn( + 'static_cast(&Test::push_back)', + content) + self.assertIn( + 'static_cast(&FunRange::create)', content) + + # Python-only renaming does not require an adapter lambda. + self.assertIn( + '.def("lambda_",static_cast' + '(&Test::lambda)', content) + self.assertIn( + '.def("_repr_markdown_",static_cast(&Test::markdown)', content) + + # Ordinary const overloads include return type and const qualification. + self.assertIn( + 'static_cast (Test::*)(' + 'const gtsam::Vector&, const gtsam::Matrix&) const>' + '(&Test::return_pair)', content) + + source = osp.join(self.INTERFACE_DIR, 'geometry.i') + output = self.wrap_content([source], + 'geometry_py', + self.PYTHON_ACTUAL_DIR, + use_boost_serialization=True) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn( + '.def_static("staticFunction",static_cast' + '(>sam::Point3::staticFunction))', + content) + + source = osp.join(self.INTERFACE_DIR, 'functions.i') + output = self.wrap_content([source], 'functions_py', + self.PYTHON_ACTUAL_DIR) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn( + 'm_.def("aGlobalFunction",static_cast' + '(&::aGlobalFunction))', content) + self.assertIn( + 'static_cast' + '(&::overloadedGlobalFunction)', content) + + def test_mutable_output_and_static_overloads(self): + """Test pointer bindings for mutable output args and static overloads.""" + source = osp.join(self.INTERFACE_DIR, 'eigen_ref.i') + output = self.wrap_content([source], 'eigen_ref_py', + self.PYTHON_ACTUAL_DIR) + + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + + self.assertIn( + 'static_cast, ' + 'Eigen::Ref) const>' + '(>sam::Pose3::transformFrom)', content) + self.assertIn( + 'static_cast)>(>sam::Pose3::Expmap)', content) + self.assertNotIn('[](', content) + + def test_member_static_name_collision(self): + """Test member/static collisions use distinct full pointer types.""" + with open(osp.join(self.TEST_DIR, "pybind_wrapper.tpl"), + encoding="UTF-8") as template_file: + module_template = template_file.read() + + wrapper = PybindWrapper(module_name='mixed_py', + top_module_namespaces=[''], + module_template=module_template) + content = wrapper.wrap_file( + 'class Mixed { int call(int value); ' + 'static int call(double value); };', + module_name='mixed_py') + + self.assertIn( + 'static_cast(&Mixed::call)', content) + self.assertIn( + 'static_cast(&Mixed::call)', content) + self.assertNotIn('[](', content) + + def test_lambda_adapters_are_preserved(self): + """Test that non-forwarding and specialized bindings retain lambdas.""" + source = osp.join(self.INTERFACE_DIR, 'class.i') + output = self.wrap_content([source], 'class_py', + self.PYTHON_ACTUAL_DIR) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + + self.assertIn( + '.def("templatedMethodString",[](Fun* self', content) + self.assertIn('.def("print",[](Test* self)', content) + self.assertIn('.def("__repr__",\n [](', content) + self.assertIn('.def("__len__",[](FastSet* self)', content) + + source = osp.join(self.INTERFACE_DIR, 'geometry.i') + output = self.wrap_content([source], + 'geometry_py', + self.PYTHON_ACTUAL_DIR, + use_boost_serialization=True) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn('.def("serialize", [](gtsam::Point2* self)', content) + self.assertIn('.def(py::pickle(', content) + + source = osp.join(self.INTERFACE_DIR, 'namespaces.i') + output = self.wrap_content([source], 'namespaces_py', + self.PYTHON_ACTUAL_DIR) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn('.def("insert_vector",[](gtsam::Values* self', content) + + source = osp.join(self.INTERFACE_DIR, 'functions.i') + output = self.wrap_content([source], 'functions_py', + self.PYTHON_ACTUAL_DIR) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn( + 'm_.def("MultiTemplatedFunctionStringSize_tDouble",[](', content) + + source = osp.join(self.INTERFACE_DIR, 'special_cases.i') + output = self.wrap_content([source], 'special_cases_py', + self.PYTHON_ACTUAL_DIR) + with open(output, 'r', encoding='UTF-8') as f: + content = f.read() + self.assertIn( + '.def("addPriorPinholeCameraCal3Bundler",[](', content) + + def test_full_signature_cast_preserves_docstring(self): + """Test that member-pointer casts retain generated docstrings.""" + with open(osp.join(self.TEST_DIR, "pybind_wrapper.tpl"), + encoding="UTF-8") as template_file: + module_template = template_file.read() + + wrapper = PybindWrapper(module_name='docstring_py', + top_module_namespaces=[''], + module_template=module_template, + xml_source='unused') + wrapper.xml_parser.extract_docstring = lambda *args: 'A docstring.' + content = wrapper.wrap_file('class DocClass { int value() const; };', + module_name='docstring_py') + + self.assertIn( + '.def("value",static_cast' + '(&DocClass::value), "A docstring.")', content) + + def test_pybind_lambda_annotation(self): + """Annotated adapters use the old lambda path and compile.""" + source = osp.join(self.INTERFACE_DIR, 'pybind_lambda_adapters.i') + output = self.wrap_content( + [source], + 'pybind_lambda_adapters_py', + self.PYTHON_ACTUAL_DIR, + module_template=self.MINIMAL_MODULE_TEMPLATE, + ) + self.compare_and_diff('pybind_lambda_adapters_pybind.cpp', output) + + with open(output, 'r', encoding='UTF-8') as generated: + content = generated.read() + + # Unannotated callables use complete function-pointer types. + self.assertIn( + 'static_cast::*)(int)>' + '(&adapters::Adapter::exact)', content) + self.assertIn( + 'static_cast::*)(int) const>' + '(&adapters::Adapter::exactConst)', content) + self.assertIn( + 'static_cast' + '(&adapters::Adapter::exactStatic)', content) + self.assertIn( + 'static_cast(&adapters::exactGlobal)', content) + + # Hidden and declared exact overloads are selected without annotations. + self.assertIn( + 'static_cast::*)(int) const>' + '(&adapters::Adapter::hiddenOverload)', content) + self.assertIn( + 'static_cast::*)(int) const>' + '(&adapters::Adapter::declaredOverload)', content) + self.assertNotIn( + '[](adapters::Adapter* self, int value)' + '{return self->hiddenOverload(value);}', content) + self.assertIn( + 'static_cast::*)(double) const>' + '(&adapters::Adapter::declaredOverload)', content) + self.assertIn( + 'static_cast(&adapters::globalHidden)', content) + self.assertIn( + 'static_cast(&adapters::globalOverload)', + content) + + # Intentional signature adapters retain the shared lambda implementation. + self.assertIn( + '.def("omittedDefault",[](adapters::Adapter* self, int value)', + content) + self.assertIn( + '.def_static("staticOmitted",[](int value)', content) + self.assertIn( + 'm_adapters.def("globalOmitted",[](int value)', content) + + # Automatic template specialization, defaults, keyword renaming, and + # return policy remain on the same lambda-emission path. + self.assertIn( + '.def("templatedDouble",[](adapters::Adapter* self, ' + 'double value)', content) + self.assertIn( + '.def("lambda_",[](adapters::Adapter* self, ' + 'const string& value) -> const auto&', content) + self.assertIn('py::return_value_policy::reference_internal', content) + self.assertIn( + 'gtwrap::internal::py_arg("value") = "fallback"', + content) + + compiler = shlex.split(os.environ.get('CXX', 'c++')) + compiler_path = shutil.which(compiler[0]) + self.assertIsNotNone(compiler_path, + f"C++ compiler not found: {compiler[0]}") + command = [ + compiler_path, + *compiler[1:], + '-std=c++14', + '-fsyntax-only', + output, + '-I', + osp.join(self.TEST_DIR, '..', 'pybind11', 'include'), + '-I', + sysconfig.get_paths()['include'], + '-I', + self.INTERFACE_DIR, + ] + result = subprocess.run(command, + capture_output=True, + text=True, + check=False) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_annotated_lambda_preserves_docstring(self): + """The forced lambda path retains metadata appended by the wrapper.""" + wrapper = PybindWrapper(module_name='docstring_py', + top_module_namespaces=[''], + module_template=self.MINIMAL_MODULE_TEMPLATE, + xml_source='unused') + wrapper.xml_parser.extract_docstring = lambda *args: 'An adapter.' + content = wrapper.wrap_file( + 'class DocClass { @pybind_lambda const int& lambda(' + 'int value = 1) const; };', + module_name='docstring_py') + + self.assertIn('.def("lambda_",[](DocClass* self, int value) ' + '-> const auto&{return self->lambda(value);}, ' + 'py::return_value_policy::reference_internal, ' + 'gtwrap::internal::py_arg("value") = 1, ' + '"An adapter.")', content) def test_const_ref_return_policy(self): """Test that methods returning const T& emit reference_internal policy. @@ -200,10 +490,8 @@ def test_const_ref_return_policy(self): Without this policy, pybind11 defaults to copying the returned reference. With the policy, the binding keeps the reference alive via the parent object. - Expected emitted code difference: - Before: [](Cls* self, ...){return self->method(...);}, py::arg(...)) - After: [](Cls* self, ...) -> const auto&{return self->method(...);}, - py::return_value_policy::reference_internal, py::arg(...)) + Full pointer casts preserve the C++ reference return type, while + reference_internal keeps the returned reference tied to its parent. """ source = osp.join(self.INTERFACE_DIR, 'class.i') output = self.wrap_content([source], 'class_py', @@ -212,22 +500,28 @@ def test_const_ref_return_policy(self): with open(output, 'r') as f: content = f.read() - # const Vector& return_vector2 should have reference_internal - self.assertIn('-> const auto&{return self->return_vector2', content) - self.assertIn('py::return_value_policy::reference_internal', content) - - # const Matrix& return_matrix2 should also have reference_internal - self.assertIn('-> const auto&{return self->return_matrix2', content) + self.assertIn( + 'static_cast(&Test::return_vector2), ' + 'py::return_value_policy::reference_internal', content) + self.assertIn( + 'static_cast(&Test::return_matrix2), ' + 'py::return_value_policy::reference_internal', content) # Non-ref returns (e.g. return_vector1 which returns by value) should NOT lines = content.split('\n') for line in lines: if 'return_vector1' in line: self.assertNotIn('reference_internal', line) - self.assertNotIn('-> const auto&', line) + self.assertIn( + 'static_cast', line) if 'return_matrix1' in line: self.assertNotIn('reference_internal', line) - self.assertNotIn('-> const auto&', line) + self.assertIn( + 'static_cast', line) source = osp.join(self.INTERFACE_DIR, 'return_policies.i') output = self.wrap_content([source], 'return_policies_py', @@ -239,10 +533,14 @@ def test_const_ref_return_policy(self): for line in content.split('\n'): if 'return_const_ref' in line: self.assertIn('reference_internal', line) - self.assertIn('-> const auto&', line) + self.assertIn( + 'static_cast const auto&', line) + self.assertIn('static_cast<', line) + self.assertNotIn('[](', line) if __name__ == '__main__': diff --git a/tests/test_template_instantiator.py b/tests/test_template_instantiator.py index af7630b..8d8bf8f 100644 --- a/tests/test_template_instantiator.py +++ b/tests/test_template_instantiator.py @@ -21,7 +21,7 @@ from gtwrap.interface_parser import (Argument, ArgumentList, Class, Constructor, ForwardDeclaration, GlobalFunction, Include, Method, - Namespace, ReturnType, StaticMethod, + Module, Namespace, ReturnType, StaticMethod, Typename) from gtwrap.template_instantiator import (InstantiatedClass, InstantiatedConstructor, @@ -126,6 +126,7 @@ class TestInstantiatedGlobalFunction(unittest.TestCase): def setUp(self): original = GlobalFunction.rule.parse_string(""" template + @pybind_lambda R function(const T& x); """)[0] instantiations = [ @@ -142,6 +143,7 @@ def test_constructor(self): self.assertEqual(len(self.func.args.list()), 1) self.assertEqual(self.func.args.list()[0].ctype.get_typename(), "int") self.assertEqual(self.func.return_type.type1.get_typename(), "double") + self.assertTrue(self.func.force_pybind_lambda) def test_to_cpp(self): """Test to_cpp method.""" @@ -210,6 +212,7 @@ class TestInstantiatedMethod(unittest.TestCase): def setUp(self): method = Method.rule.parse_string(""" template + @pybind_lambda double method(const U& param); """)[0] instantiations = [Typename.rule.parse_string("double")[0]] @@ -220,11 +223,13 @@ def test_constructor(self): self.assertIsInstance(self.method, InstantiatedMethod) self.assertIsInstance(self.method.original, Method) self.assertEqual(self.method.name, "methodDouble") + self.assertTrue(self.method.force_pybind_lambda) def test_construct(self): """Test the construct classmethod.""" method = Method.rule.parse_string(""" template + @pybind_lambda T method(U& param); """)[0] method_instantiations = [Typename.rule.parse_string("double")[0]] @@ -249,6 +254,7 @@ class Class {}; instantiated_method.args.list()[0].ctype.get_typename(), "double") self.assertEqual(instantiated_method.return_type.type1.get_typename(), "string") + self.assertTrue(instantiated_method.force_pybind_lambda) def test_to_cpp(self): """Test the to_cpp method.""" @@ -262,6 +268,7 @@ class TestInstantiatedStaticMethod(unittest.TestCase): def setUp(self): static_method = StaticMethod.rule.parse_string(""" template + @pybind_lambda static T staticMethod(const U& param); """)[0] instantiations = [Typename.rule.parse_string("double")[0]] @@ -273,11 +280,13 @@ def test_constructor(self): self.assertIsInstance(self.static_method, InstantiatedStaticMethod) self.assertIsInstance(self.static_method.original, StaticMethod) self.assertEqual(self.static_method.name, "staticMethodDouble") + self.assertTrue(self.static_method.force_pybind_lambda) def test_construct(self): """Test the construct classmethod.""" static_method = StaticMethod.rule.parse_string(""" template + @pybind_lambda static T staticMethod(U& param); """)[0] method_instantiations = [Typename.rule.parse_string("double")[0]] @@ -304,6 +313,7 @@ class Class {}; self.assertEqual( instantiated_static_method.return_type.type1.get_typename(), "string") + self.assertTrue(instantiated_static_method.force_pybind_lambda) def test_to_cpp(self): """Test the to_cpp method.""" @@ -615,6 +625,26 @@ class Values { instantiated_namespace.content[1].static_methods[0].name, "staticMethodDouble") + def test_annotation_survives_typedef_instantiation(self): + """An annotation on a templated class member survives a typedef.""" + module = Module.parse_string(""" + namespace adapters { + template + class Adapter { + @pybind_lambda + T value(T input) const; + }; + typedef adapters::Adapter IntAdapter; + } + """) + + instantiated = instantiate_namespace(module) + namespace = instantiated.content[0] + typedef_class = next( + item for item in namespace.content + if isinstance(item, Class) and item.name == "IntAdapter") + self.assertTrue(typedef_class.methods[0].force_pybind_lambda) + if __name__ == '__main__': unittest.main()