Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ The python wrapper supports keyword arguments for functions/methods. Hence, the
template<T, U> class Class2 { ... };
typedef Class2<Type1, Type2> MyInstantiatedClass;
```
- Serialization can be enabled for one typedef without marking every
specialization of the template serializable:

```cpp
template<T> class Class3 { ... };
@serializable
typedef Class3<Type1> SerializableClass3;
typedef Class3<Type2> PlainClass3;
```

`@serializable` is wrapper metadata. It generates the same Python pickle
and MATLAB save/load support as a `void serialize() const;` marker on a
concrete wrapper class, but applies only to the annotated typedef.
- Templates can also be defined for constructors, methods, properties and static methods.
- In the class definition, appearances of the template argument(s) will be replaced with their
instantiated types, e.g. `void setValue(const T& value);`.
Expand Down
23 changes: 19 additions & 4 deletions gtwrap/interface_parser/annotations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Pybind-specific annotations supported by wrapper interface files."""
"""Annotations supported by wrapper interface files."""

from pyparsing import Regex

Expand All @@ -12,21 +12,36 @@
def _reject_annotation(source, location, tokens):
"""Raise a useful error for unknown or misplaced annotations."""
annotation = tokens[0]
context = "callable annotation"
if annotation == "@pybind_lambda":
message = (
"annotation '@pybind_lambda' can only be applied to a method, "
"static method, or global function"
)
hint = (
"place '@pybind_lambda' after any template declaration and "
"immediately before the callable declaration"
)
elif annotation == "@serializable":
context = "typedef annotation"
message = (
"annotation '@serializable' can only be applied to a template "
"typedef"
)
hint = "place '@serializable' immediately before the typedef declaration"
else:
message = f"malformed or unknown annotation '{annotation}'"
hint = (
"use a supported annotation such as '@pybind_lambda' or "
"'@serializable' in its documented position"
)

raise semantic_error(
source,
location,
"callable annotation",
context,
message,
"place '@pybind_lambda' after any template declaration and "
"immediately before the callable declaration",
hint,
)


Expand Down
12 changes: 9 additions & 3 deletions gtwrap/interface_parser/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@

from typing import List

from pyparsing import Optional, ParseResults, DelimitedList # type: ignore
from pyparsing import DelimitedList, Optional, ParseResults, Regex # type: ignore

from .tokens import (EQUAL, IDENT, LBRACE, LOPBRACK, RBRACE, ROPBRACK,
SEMI_COLON, TEMPLATE, TYPEDEF)
from .type import TemplatedType, Typename


SERIALIZABLE = Regex(r"@serializable(?![A-Za-z0-9_])")


class Template:
"""
Rule to parse templated values in the interface file.
Expand Down Expand Up @@ -83,17 +86,20 @@ class TypedefTemplateInstantiation:
typedef SuperComplexName<Arg1, Arg2, Arg3> EasierName;
```
"""
rule = (TYPEDEF + TemplatedType.rule("templated_type") +
rule = (Optional(SERIALIZABLE("serializable")) +
TYPEDEF + TemplatedType.rule("templated_type") +
IDENT("new_name") +
SEMI_COLON).set_parse_action(lambda t: TypedefTemplateInstantiation(
t.templated_type[0], t.new_name))
t.templated_type[0], t.new_name, bool(t.serializable)))

def __init__(self,
templated_type: TemplatedType,
new_name: str,
serializable: bool = False,
parent: str = ''):
self.typename = templated_type.typename
self.new_name = new_name
self.serializable = serializable
self.parent = parent

def __repr__(self):
Expand Down
13 changes: 12 additions & 1 deletion gtwrap/template_instantiator/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ class InstantiatedClass(parser.Class):
Instantiate the class defined in the interface file.
"""

def __init__(self, original: parser.Class, instantiations=(), new_name=''):
def __init__(self,
original: parser.Class,
instantiations=(),
new_name='',
serializable=False):
"""
Template <T, U>
Instantiations: [T1, U1]
"""
self.original = original
self.instantiations = instantiations
self.serializable = serializable

self.template = None
self.is_virtual = original.is_virtual
Expand Down Expand Up @@ -58,6 +63,12 @@ def __init__(self, original: parser.Class, instantiations=(), new_name=''):

# Instantiate all instance methods
self.methods = self.instantiate_methods(typenames)
if serializable and not any(
method.name in ('serialize', 'serializable')
for method in self.methods):
self.methods.append(
parser.Method.rule.parse_string(
"void serialize() const;")[0])

self.dunder_methods = original.dunder_methods

Expand Down
3 changes: 2 additions & 1 deletion gtwrap/template_instantiator/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ def instantiate_namespace(namespace):
typedef_content.append(
InstantiatedClass(original_element,
typedef_inst.typename.instantiations,
typedef_inst.new_name))
typedef_inst.new_name,
typedef_inst.serializable))
elif isinstance(original_element, parser.GlobalFunction):
typedef_content.append(
InstantiatedGlobalFunction(
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/serializable_typedef.i
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace gtsam {

template<T>
class SerializableTypedefFixture {
SerializableTypedefFixture();
};

@serializable
typedef gtsam::SerializableTypedefFixture<int> SerializableFixture;
typedef gtsam::SerializableTypedefFixture<double> PlainFixture;

} // namespace gtsam
8 changes: 8 additions & 0 deletions tests/test_interface_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,14 @@ def test_typedef_template_instantiation(self):
self.assertEqual("BearingFactor", typedef.typename.name)
self.assertEqual(["gtsam"], typedef.typename.namespaces)
self.assertEqual(3, len(typedef.typename.instantiations))
self.assertFalse(typedef.serializable)

serializable = TypedefTemplateInstantiation.rule.parse_string("""
@serializable
typedef gtsam::BearingFactor<gtsam::Pose2, gtsam::Point2,
gtsam::Rot2> SerializableBearingFactor2D;
""")[0]
self.assertTrue(serializable.serializable)

def test_base_class(self):
"""Test a base class."""
Expand Down
30 changes: 30 additions & 0 deletions tests/test_matlab_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ def test_geometry(self):
actual = osp.join(self.MATLAB_ACTUAL_DIR, file)
self.compare_and_diff(file, actual)

def test_serializable_template_typedef(self):
"""Serialization metadata applies to one MATLAB typedef only."""
source = osp.join(self.INTERFACE_DIR, 'serializable_typedef.i')
wrapper = MatlabWrapper(module_name='serializable_typedef',
top_module_namespace=['gtsam'],
ignore_classes=[''],
use_boost_serialization=True)
wrapper.wrap([source], path=self.MATLAB_ACTUAL_DIR)

with open(osp.join(self.MATLAB_ACTUAL_DIR, '+gtsam',
'SerializableFixture.m'),
'r', encoding='UTF-8') as generated:
serializable = generated.read()
with open(osp.join(self.MATLAB_ACTUAL_DIR, '+gtsam', 'PlainFixture.m'),
'r', encoding='UTF-8') as generated:
plain = generated.read()
with open(osp.join(self.MATLAB_ACTUAL_DIR,
'serializable_typedef_wrapper.cpp'),
'r', encoding='UTF-8') as generated:
cpp = generated.read()

self.assertIn('string_serialize', serializable)
self.assertIn('string_deserialize', serializable)
self.assertNotIn('string_serialize', plain)
self.assertNotIn('string_deserialize', plain)
self.assertIn(
'BOOST_CLASS_EXPORT_GUID(SerializableFixture, '
'"gtsamSerializableFixture")', cpp)
self.assertNotIn('BOOST_CLASS_EXPORT_GUID(PlainFixture', cpp)

def test_matrix_view_arguments(self):
"""Test that matrix view arguments use MATLAB double arrays directly."""
file = osp.join(self.INTERFACE_DIR, 'matrix_views.i')
Expand Down
14 changes: 14 additions & 0 deletions tests/test_parser_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ def test_misplaced_callable_annotation(self):
)
self.assertIn("immediately before the callable", error.hint)

def test_misplaced_serializable_annotation(self):
"""The serialization annotation applies only to template typedefs."""
error = self.assert_parse_error(
"@serializable class Foo {};",
line=1,
column=1,
context="typedef annotation",
expected=(
"annotation '@serializable' can only be applied to a template "
"typedef"
),
)
self.assertIn("immediately before the typedef", error.hint)

def test_misplaced_annotation_after_template(self):
self.assert_parse_error(
"class Foo { template<T> @pybind_lambda Foo(T value); };",
Expand Down
20 changes: 20 additions & 0 deletions tests/test_pybind_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ def test_geometry(self):

self.compare_and_diff('geometry_pybind.cpp', output)

def test_serializable_template_typedef(self):
"""Serialization metadata applies to one template typedef only."""
source = osp.join(self.INTERFACE_DIR, 'serializable_typedef.i')
output = self.wrap_content([source],
'serializable_typedef_py',
self.PYTHON_ACTUAL_DIR,
use_boost_serialization=True)
with open(output, 'r', encoding='UTF-8') as generated:
content = generated.read()

self.assertEqual(1, content.count('.def("serialize"'))
self.assertEqual(1, content.count('.def("deserialize"'))
self.assertEqual(1, content.count('.def(py::pickle('))
self.assertIn(
'BOOST_CLASS_EXPORT(gtsam::SerializableTypedefFixture<int>)',
content)
self.assertNotIn(
'BOOST_CLASS_EXPORT(gtsam::SerializableTypedefFixture<double>)',
content)

def test_functions(self):
"""Test interface file with function info."""
source = osp.join(self.INTERFACE_DIR, 'functions.i')
Expand Down
29 changes: 29 additions & 0 deletions tests/test_template_instantiator.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,35 @@ class Adapter {
if isinstance(item, Class) and item.name == "IntAdapter")
self.assertTrue(typedef_class.methods[0].force_pybind_lambda)

def test_serializable_typedef_adds_only_alias_marker(self):
"""Serialization metadata applies only to the annotated typedef."""
module = Module.parse_string("""
namespace adapters {
template<T>
class Adapter {
Adapter();
};
@serializable
typedef adapters::Adapter<int> SerializableAdapter;
typedef adapters::Adapter<double> PlainAdapter;
}
""")

instantiated = instantiate_namespace(module)
namespace = instantiated.content[0]
serializable = next(
item for item in namespace.content
if isinstance(item, Class) and item.name == "SerializableAdapter")
plain = next(
item for item in namespace.content
if isinstance(item, Class) and item.name == "PlainAdapter")

self.assertTrue(serializable.serializable)
self.assertEqual(["serialize"],
[method.name for method in serializable.methods])
self.assertFalse(plain.serializable)
self.assertEqual([], plain.methods)


if __name__ == '__main__':
unittest.main()
Loading