SEBA-198 Allow clients to determine if a field is set to None
Change-Id: I9c8eca7cbb52fd6fffd1a2a1ff59f441d1fa1240
diff --git a/xos/xos_client/xosapi/fake_stub.py b/xos/xos_client/xosapi/fake_stub.py
index 0970b39..ac35805 100644
--- a/xos/xos_client/xosapi/fake_stub.py
+++ b/xos/xos_client/xosapi/fake_stub.py
@@ -53,6 +53,25 @@
self.is_set[name] = True
super(FakeObj, self).__setattr__(name, value)
+ def HasField(self, name):
+ """ Return True if the field is set in the protobuf. """
+
+ # gRPC throws a valueerror if the field doesn't exist in the schema
+ if name not in self.fields:
+ raise ValueError("Field %s does not exist in schema" % name)
+
+ # Fields that are always set
+ if name in ["leaf_model_name"]:
+ return True
+
+ field = self.DESCRIPTOR.fields_by_name[name].field_decl
+
+ # Reverse foreign keys lists are never None, they are an empty list
+ if field.get("fk_reverse", None):
+ return True
+
+ return self.is_set.get(name, False)
+
@property
def self_content_type_id(self):
return "xos.%s" % self.__class__.__name__.lower()
@@ -79,6 +98,8 @@
def __init__(self, field):
extensions = {}
+ self.field_decl = field
+
fk_model = field.get("fk_model", None)
if fk_model:
reverseFieldName = field.get("fk_reverseFieldName", None)
@@ -268,6 +289,16 @@
DESCRIPTOR = FakeDescriptor("Tag")
+class TestModel(FakeObj):
+ FIELDS = ( {"name": "id", "default": 0},
+ {"name": "intfield", "default": 0} )
+
+ def __init__(self, **kwargs):
+ return super(TestModel, self).__init__(self.FIELDS, **kwargs)
+
+ DESCRIPTOR = FakeDescriptor("TestModel")
+
+
class ID(FakeObj):
pass
@@ -300,7 +331,9 @@
def __init__(self):
self.id_counter = 1
self.objs = {}
- for name in ["Controller", "Deployment", "Slice", "Site", "Tag", "Service", "ServiceInstance", "ONOSService", "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice"]:
+ for name in ["Controller", "Deployment", "Slice", "Site", "Tag", "Service", "ServiceInstance", "ONOSService",
+ "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice",
+ "TestModel"]:
setattr(self, "Get%s" % name, functools.partial(self.get, name))
setattr(self, "List%s" % name, functools.partial(self.list, name))
setattr(self, "Create%s" % name, functools.partial(self.create, name))
@@ -371,14 +404,18 @@
class FakeProtos(object):
def __init__(self):
- for name in ["Controller", "Deployment", "Slice", "Site", "ID", "Tag", "Service", "ServiceInstance", "ONOSService", "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice"]:
+ for name in ["Controller", "Deployment", "Slice", "Site", "ID", "Tag", "Service", "ServiceInstance",
+ "ONOSService", "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice",
+ "TestModel"]:
setattr(self, name, globals()[name])
self.common__pb2 = FakeCommonProtos()
class FakeSymDb(object):
def __init__(self):
self._classes = {}
- for name in ["Controller", "Deployment", "Slice", "Site", "ID", "Tag", "Service", "ServiceInstance", "ONOSService", "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice"]:
+ for name in ["Controller", "Deployment", "Slice", "Site", "ID", "Tag", "Service", "ServiceInstance",
+ "ONOSService", "User", "Network", "NetworkTemplate", "ControllerNetwork", "NetworkSlice",
+ "TestModel"]:
self._classes["xos.%s" % name] = globals()[name]
diff --git a/xos/xos_client/xosapi/orm.py b/xos/xos_client/xosapi/orm.py
index d0a2a31..7d6d263 100644
--- a/xos/xos_client/xosapi/orm.py
+++ b/xos/xos_client/xosapi/orm.py
@@ -232,6 +232,16 @@
if name in self._reverse_fkmap.keys():
return self.reverse_fk_resolve(name)
+ try:
+ # When sending a reply, XOS will leave the field unset if it is None in the data model. If
+ # HasField(<fieldname>)==False for an existing object, then the caller can infer that field was set to
+ # None.
+ if (not self.is_new) and (not self._wrapped_class.HasField(name)):
+ return None
+ except ValueError:
+ # ValueError is thrown if the field does not exist. We will handle that case in the getattr() below.
+ pass
+
return getattr(self._wrapped_class, name, *args, **kwargs)
def __setattr__(self, name, value):
@@ -239,6 +249,11 @@
self.fk_set(name, value)
elif name in self.__dict__:
super(ORMWrapper,self).__setattr__(name, value)
+ elif value is None:
+ # When handling requests, XOS interprets gRPC HasField(<fieldname>)==False to indicate that the caller
+ # has not set the field and wants it to continue to use its existing (or default) value. That leaves us
+ # with no easy way to support setting a field to None.
+ raise ValueError("Setting a non-foreignkey field to None is not supported")
else:
setattr(self._wrapped_class, name, value)
diff --git a/xos/xos_client/xosapi/test_orm.py b/xos/xos_client/xosapi/test_orm.py
index 6c2661c..6006a52 100644
--- a/xos/xos_client/xosapi/test_orm.py
+++ b/xos/xos_client/xosapi/test_orm.py
@@ -416,6 +416,44 @@
self.assertEqual(onos_service_cast.leaf_model_name, "ONOSService")
self.assertEqual(onos_service_cast.id, onos_service.id)
+ def test_field_null(self):
+ """ In a saved object, if a nullable field is left set to None, make sure the ORM returns None """
+
+ orm = self.make_coreapi()
+ tm = orm.TestModel()
+ tm.save()
+
+ tm = orm.TestModel.objects.all()[0]
+ self.assertFalse(tm._wrapped_class.HasField("intfield"))
+ self.assertEqual(tm.intfield, None)
+
+ def test_field_null_new(self):
+ """ For models that haven't been saved yet, reading the field should return the gRPC default """
+
+ orm = self.make_coreapi()
+ tm = orm.TestModel()
+
+ self.assertEqual(tm.intfield, 0)
+
+ def test_field_non_null(self):
+ """ In a saved object, if a nullable field is set to a value, then make sure the ORM returns the value """
+
+ orm = self.make_coreapi()
+ tm = orm.TestModel(intfield=3)
+ tm.save()
+
+ tm = orm.TestModel.objects.all()[0]
+ self.assertEqual(tm.intfield, 3)
+
+ def test_field_set_null(self):
+ """ Setting a field to None is not allowed """
+
+ orm = self.make_coreapi()
+ tm = orm.TestModel()
+ with self.assertRaises(Exception) as e:
+ tm.intfile = None
+ self.assertEqual(e.exception.message, "Setting a non-foreignkey field to None is not supported")
+
def main():
global USE_FAKE_STUB
global xos_grpc_client