From f7fbe5bdb30c684e7ae8aecbb157c865176f926c Mon Sep 17 00:00:00 2001 From: Jason Paryani Date: Thu, 29 Aug 2013 21:03:00 -0700 Subject: [PATCH] Change load to use a global SchemaParser. Make structs settable as field --- capnp/__init__.py | 2 +- capnp/capnp.pyx | 139 +++++++++++++++++++++++++++++++++++----------- test/test_load.py | 36 ++++++++++++ 3 files changed, 143 insertions(+), 34 deletions(-) diff --git a/capnp/__init__.py b/capnp/__init__.py index 8f2c321..d66dcb7 100644 --- a/capnp/__init__.py +++ b/capnp/__init__.py @@ -9,7 +9,7 @@ Example Usage:: # Building message = capnp.MallocMessageBuilder() addressBook = message.initRoot(addressbook.AddressBook) - people = addressBook.init('people', 2) + people = addressBook.init('people', 1) alice = people[0] alice.id = 123 diff --git a/capnp/capnp.pyx b/capnp/capnp.pyx index d7d1125..73ef2c2 100644 --- a/capnp/capnp.pyx +++ b/capnp/capnp.pyx @@ -249,12 +249,27 @@ cdef class _DynamicListBuilder: cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(value) self.thisptr.set(index, temp) + cdef _setattrDynamicStructBuilder(self, index, _DynamicStructBuilder value): + cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(value.thisptr.asReader()) + self.thisptr.set(index, temp) + + cdef _setattrDynamicStructReader(self, index, _DynamicStructReader value): + cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(value.thisptr) + self.thisptr.set(index, temp) + def __setitem__(self, index, value): size = self.thisptr.size() if index >= size: raise IndexError('Out of bounds') index = index % size - self._setitem(index, value) + value_type = type(value) + + if value_type is _DynamicStructBuilder: + self._setattrDynamicStructBuilder(index, value) + elif value_type is _DynamicStructReader: + self._setattrDynamicStructReader(index, value) + else: + self._setitem(index, value) def __len__(self): return self.thisptr.size() @@ -474,8 +489,17 @@ cdef class _DynamicStructBuilder: cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(VOID) self.thisptr.set(field, temp) + cdef _setattrDynamicStructBuilder(self, field, _DynamicStructBuilder value): + cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(value.thisptr.asReader()) + self.thisptr.set(field, temp) + + cdef _setattrDynamicStructReader(self, field, _DynamicStructReader value): + cdef C_DynamicValue.Reader temp = C_DynamicValue.Reader(value.thisptr) + self.thisptr.set(field, temp) + def __setattr__(self, field, value): value_type = type(value) + if value_type is int: self._setattrInt(field, value) elif value_type is float: @@ -486,6 +510,10 @@ cdef class _DynamicStructBuilder: self._setattrString(field, value) elif value is None: self._setattrVoid(field) + elif value_type is _DynamicStructBuilder: + self._setattrDynamicStructBuilder(field, value) + elif value_type is _DynamicStructReader: + self._setattrDynamicStructReader(field, value) else: raise ValueError("Non primitive type") @@ -515,7 +543,7 @@ cdef class _DynamicStructBuilder: cpdef initResizableList(self, field): """Method for initializing fields that are of type list (of structs) - This version of init returns a :class:`_DynamicResizableListBuilder` that allows you to add members one at a time (ie. if you don't know the size for sure). This is only meant for lists of Cap'n Proto objects, since you can just define a normal python and fill it with primitive types like int/float. + This version of init returns a :class:`_DynamicResizableListBuilder` that allows you to add members one at a time (ie. if you don't know the size for sure). This is only meant for lists of Cap'n Proto objects, since for primitive types you can just define a normal python list and fill it yourself. .. warning:: You need to call :meth:`_DynamicResizableListBuilder.finish` on the list object before serializing the Cap'n Proto message. Failure to do so will cause your objects not to be written out as well as leaking orphan structs into your message. @@ -692,7 +720,11 @@ cdef class _ParsedSchema: cpdef getNested(self, name): return _ParsedSchema()._init(self.thisptr.getNested(name)) -cdef class _SchemaParser: +cdef class SchemaParser: + """A class for loading Cap'n Proto schema files. + + Do not use this class unless you're sure you know what you're doing. Use the convenience method :func:`load` instead. + """ cdef C_SchemaParser * thisptr def __cinit__(self): self.thisptr = new C_SchemaParser() @@ -700,7 +732,7 @@ cdef class _SchemaParser: def __dealloc__(self): del self.thisptr - def parseDiskFile(self, displayName, diskPath, imports): + def _parseDiskFile(self, displayName, diskPath, imports): cdef StringPtr * importArray = malloc(sizeof(StringPtr) * len(imports)) for i in range(len(imports)): @@ -715,6 +747,69 @@ cdef class _SchemaParser: return ret + def load(self, file_name, display_name=None, imports=[]): + """Load a Cap'n Proto schema from a file + + You will have to load a schema before you can begin doing anything + meaningful with this library. Loading a schema is much like loading + a Python module (and load even returns a `ModuleType`). Once it's been + loaded, you use it much like any other Module:: + + parser = capnp.SchemaParser() + addressbook = parser.load('addressbook.capnp') + print addressbook.qux # qux is a top level constant + # 123 + message = capnp.MallocMessageBuilder() + person = message.initRoot(addressbook.Person) + + :type file_name: str + :param file_name: A relative or absolute path to a Cap'n Proto schema + + :type display_name: str + :param display_name: The name internally used by the Cap'n Proto library + for the loaded schema. By default, it's just os.path.basename(file_name) + + :type imports: list + :param imports: A list of str directories to add to the import path. + + :rtype: ModuleType + :return: A module corresponding to the loaded schema. You can access + parsed schemas and constants with . syntax + + :Raises: :exc:`exceptions.ValueError` if `file_name` doesn't exist + + """ + def _load(nodeSchema, module): + module._nodeSchema = nodeSchema + nodeProto = nodeSchema.getProto() + module._nodeProto = nodeProto + + for node in nodeProto.nestedNodes: + local_module = _ModuleType(node.name) + module.__dict__[node.name] = local_module + + schema = nodeSchema.getNested(node.name) + proto = schema.getProto() + if proto.isStruct: + local_module.schema = schema.asStruct() + elif proto.isConst: + module.__dict__[node.name] = schema.asConstValue() + + _load(schema, local_module) + + if display_name is None: + display_name = _os.path.basename(file_name) + + module = _ModuleType(display_name) + parser = self + + module._parser = parser + + fileSchema = parser._parseDiskFile(display_name, file_name, imports) + _load(fileSchema, module) + + return module + cdef class MessageBuilder: """An abstract base class for building Cap'n Proto messages @@ -941,11 +1036,13 @@ def writePackedMessageToFd(int fd, MessageBuilder message): from types import ModuleType as _ModuleType import os as _os +_global_schema_parser = None + def load(file_name, display_name=None, imports=[]): """Load a Cap'n Proto schema from a file You will have to load a schema before you can begin doing anything - meaningful with this library. Loading a schema is much like Loading + meaningful with this library. Loading a schema is much like loading a Python module (and load even returns a `ModuleType`). Once it's been loaded, you use it much like any other Module:: @@ -972,32 +1069,8 @@ def load(file_name, display_name=None, imports=[]): :Raises: :exc:`exceptions.ValueError` if `file_name` doesn't exist """ - def _load(nodeSchema, module): - module._nodeSchema = nodeSchema - nodeProto = nodeSchema.getProto() - module._nodeProto = nodeProto + global _global_schema_parser + if _global_schema_parser is None: + _global_schema_parser = SchemaParser() - for node in nodeProto.nestedNodes: - local_module = _ModuleType(node.name) - module.__dict__[node.name] = local_module - - schema = nodeSchema.getNested(node.name) - proto = schema.getProto() - if proto.isStruct: - local_module.schema = schema.asStruct() - elif proto.isConst: - module.__dict__[node.name] = schema.asConstValue() - - _load(schema, local_module) - - if display_name is None: - display_name = _os.path.basename(file_name) - module = _ModuleType(display_name) - parser = _SchemaParser() - - module._parser = parser - - fileSchema = parser.parseDiskFile(display_name, file_name, imports) - _load(fileSchema, module) - - return module + return _global_schema_parser.load(file_name, display_name, imports) diff --git a/test/test_load.py b/test/test_load.py index dae6d0b..4285c07 100644 --- a/test/test_load.py +++ b/test/test_load.py @@ -8,6 +8,14 @@ this_dir = os.path.dirname(__file__) def addressbook(): return capnp.load(os.path.join(this_dir, 'addressbook.capnp')) +@pytest.fixture +def foo(): + return capnp.load(os.path.join(this_dir, 'foo.capnp')) + +@pytest.fixture +def bar(): + return capnp.load(os.path.join(this_dir, 'bar.capnp')) + def test_basic_load(): capnp.load(os.path.join(this_dir, 'addressbook.capnp')) @@ -17,3 +25,31 @@ def test_constants(addressbook): def test_classes(addressbook): assert addressbook.AddressBook assert addressbook.Person + +def test_import(foo, bar): + m = capnp.MallocMessageBuilder() + foo = m.initRoot(foo.Foo) + m2 = capnp.MallocMessageBuilder() + bar = m2.initRoot(bar.Bar) + + foo.name = 'foo' + bar.foo = foo + + assert bar.foo.name == 'foo' + +def test_failed_import(): + s = capnp.SchemaParser() + s2 = capnp.SchemaParser() + + foo = s.load(os.path.join(this_dir, 'foo.capnp')) + bar = s2.load(os.path.join(this_dir, 'bar.capnp')) + + m = capnp.MallocMessageBuilder() + foo = m.initRoot(foo.Foo) + m2 = capnp.MallocMessageBuilder() + bar = m2.initRoot(bar.Bar) + + foo.name = 'foo' + + with pytest.raises(ValueError): + bar.foo = foo