[lxc-devel] [pylxd/master] Attribute state

rockstar on Github lxc-bot at linuxcontainers.org
Tue Jun 28 18:37:24 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 663 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160628/0c6063d6/attachment.bin>
-------------- next part --------------
From 06d412fc9498c38268bb299b292af340a3fb57cd Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Mon, 27 Jun 2016 23:03:56 -0600
Subject: [PATCH 1/3] Remove __dirty__; opt for attributes keeping track of
 state

---
 pylxd/model.py | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/pylxd/model.py b/pylxd/model.py
index 2d08450..016a8e7 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -25,6 +25,8 @@ def __init__(self, validator=None, readonly=False):
         self.validator = validator
         self.readonly = False
 
+        self.dirty = False
+
 
 class Manager(object):
     """A manager declaration.
@@ -95,14 +97,17 @@ class Model(object):
     the instance is marked as dirty. `save` will save the changes
     to the server.
     """
-    __slots__ = ['client', '__dirty__']
+    __slots__ = ['client']
 
     def __init__(self, client, **kwargs):
         self.client = client
 
         for key, val in kwargs.items():
             setattr(self, key, val)
-        self.__dirty__ = False
+            try:
+                self.__attributes__[key].dirty = False
+            except KeyError:  # Manager or Parent attribute
+                pass
 
     def __getattribute__(self, name):
         try:
@@ -121,12 +126,15 @@ def __setattr__(self, name, value):
             if attribute.validator is not None:
                 if attribute.validator is not type(value):
                     value = attribute.validator(value)
-            self.__dirty__ = True
+            attribute.dirty = True
         return super(Model, self).__setattr__(name, value)
 
     @property
     def dirty(self):
-        return self.__dirty__
+        for name, attr in self.__attributes__.items():
+            if attr.dirty:
+                return True
+        return False
 
     def sync(self):
         """Sync from the server.

From b7b97c0e87f1e69259c67fa162e72273ad69bd1a Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 28 Jun 2016 12:19:59 -0600
Subject: [PATCH 2/3] Keep a list of all dirty attributes

---
 pylxd/model.py            | 29 ++++++++++++++---------------
 pylxd/tests/test_model.py | 31 ++++++++++++++++++++++++++++---
 2 files changed, 42 insertions(+), 18 deletions(-)

diff --git a/pylxd/model.py b/pylxd/model.py
index 016a8e7..4508f0b 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -25,8 +25,6 @@ def __init__(self, validator=None, readonly=False):
         self.validator = validator
         self.readonly = False
 
-        self.dirty = False
-
 
 class Manager(object):
     """A manager declaration.
@@ -97,23 +95,21 @@ class Model(object):
     the instance is marked as dirty. `save` will save the changes
     to the server.
     """
-    __slots__ = ['client']
+    __slots__ = ['client', '__dirty__']
 
     def __init__(self, client, **kwargs):
+        self.__dirty__ = []
         self.client = client
 
         for key, val in kwargs.items():
             setattr(self, key, val)
-            try:
-                self.__attributes__[key].dirty = False
-            except KeyError:  # Manager or Parent attribute
-                pass
+        del self.__dirty__[:]
 
     def __getattribute__(self, name):
         try:
             return super(Model, self).__getattribute__(name)
         except AttributeError:
-            if name in self.__slots__:
+            if name in self.__attributes__:
                 self.sync()
                 return super(Model, self).__getattribute__(name)
             else:
@@ -126,17 +122,14 @@ def __setattr__(self, name, value):
             if attribute.validator is not None:
                 if attribute.validator is not type(value):
                     value = attribute.validator(value)
-            attribute.dirty = True
+            self.__dirty__.append(name)
         return super(Model, self).__setattr__(name, value)
 
     @property
     def dirty(self):
-        for name, attr in self.__attributes__.items():
-            if attr.dirty:
-                return True
-        return False
+        return len(self.__dirty__) > 0
 
-    def sync(self):
+    def sync(self, rollback=False):
         """Sync from the server.
 
         When collections of objects are retrieved from the server, they
@@ -153,9 +146,14 @@ def sync(self):
                 raise exceptions.NotFound()
             raise
         for key, val in response.json()['metadata'].items():
-            setattr(self, key, val)
+            if key not in self.__dirty__ or rollback:
+                setattr(self, key, val)
     fetch = deprecated("fetch is deprecated; please use sync")(sync)
 
+    def rollback(self):
+        """Reset the object from the server."""
+        return self.sync(rollback=True)
+
     def save(self, wait=False):
         """Save data to the server.
 
@@ -169,6 +167,7 @@ def save(self, wait=False):
         if response.json()['type'] == 'async' and wait:
             Operation.wait_for_operation(
                 self.client, response.json()['operation'])
+        del self.__dirty__[:]
     update = deprecated('update is deprecated; please use save')(save)
 
     def delete(self, wait=False):
diff --git a/pylxd/tests/test_model.py b/pylxd/tests/test_model.py
index bfcb4b9..efc61ff 100644
--- a/pylxd/tests/test_model.py
+++ b/pylxd/tests/test_model.py
@@ -21,14 +21,30 @@ class Item(model.Model):
     age = model.Attribute(int)
     data = model.Attribute()
 
-    def sync(self):
-        self.age = 1000
-        self.data = {'key': 'val'}
+    @property
+    def api(self):
+        return self.client.api.items[self.name]
 
 
 class TestModel(testing.PyLXDTestCase):
     """Tests for pylxd.model.Model."""
 
+    def setUp(self):
+        super(TestModel, self).setUp()
+
+        self.add_rule({
+            'json': {
+                'type': 'sync',
+                'metadata': {
+                    'name': 'an-item',
+                    'age': 1000,
+                    'data': {'key': 'val'},
+                }
+            },
+            'method': 'GET',
+            'url': r'^http://pylxd.test/1.0/items/an-item',
+        })
+
     def test_init(self):
         """Initial attributes are set."""
         item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
@@ -62,6 +78,15 @@ def test_unset_attribute_sync(self):
 
         self.assertEqual(1000, item.age)
 
+    def test_rollback(self):
+        """Rollback resets the object from the server."""
+        item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
+
+        item.age = 50
+        item.rollback()
+
+        self.assertEqual(1000, item.age)
+
     def test_int_attribute_validator(self):
         """Age is set properly to be an int."""
         item = Item(self.client)

From d18b62df1d8bf14b13d1e1afddf51383d419bc26 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul.hummer at canonical.com>
Date: Tue, 28 Jun 2016 12:33:31 -0600
Subject: [PATCH 3/3] Add more tests for model (and fix a bug)

---
 pylxd/model.py            |  5 +++-
 pylxd/tests/test_model.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 64 insertions(+), 2 deletions(-)

diff --git a/pylxd/model.py b/pylxd/model.py
index 4508f0b..818c1f8 100644
--- a/pylxd/model.py
+++ b/pylxd/model.py
@@ -23,7 +23,7 @@ class Attribute(object):
 
     def __init__(self, validator=None, readonly=False):
         self.validator = validator
-        self.readonly = False
+        self.readonly = readonly
 
 
 class Manager(object):
@@ -148,6 +148,8 @@ def sync(self, rollback=False):
         for key, val in response.json()['metadata'].items():
             if key not in self.__dirty__ or rollback:
                 setattr(self, key, val)
+        if rollback:
+            del self.__dirty__[:]
     fetch = deprecated("fetch is deprecated; please use sync")(sync)
 
     def rollback(self):
@@ -177,6 +179,7 @@ def delete(self, wait=False):
         if response.json()['type'] == 'async' and wait:
             Operation.wait_for_operation(
                 self.client, response.json()['operation'])
+        self.client = None
 
     def marshall(self):
         """Marshall the object in preparation for updating to the server."""
diff --git a/pylxd/tests/test_model.py b/pylxd/tests/test_model.py
index efc61ff..a76b1fa 100644
--- a/pylxd/tests/test_model.py
+++ b/pylxd/tests/test_model.py
@@ -17,7 +17,7 @@
 
 class Item(model.Model):
     """A fake model."""
-    name = model.Attribute()
+    name = model.Attribute(readonly=True)
     age = model.Attribute(int)
     data = model.Attribute()
 
@@ -44,6 +44,22 @@ def setUp(self):
             'method': 'GET',
             'url': r'^http://pylxd.test/1.0/items/an-item',
         })
+        self.add_rule({
+            'json': {
+                'type': 'sync',
+                'metadata': {}
+            },
+            'method': 'PUT',
+            'url': r'^http://pylxd.test/1.0/items/an-item',
+        })
+        self.add_rule({
+            'json': {
+                'type': 'sync',
+                'metadata': {}
+            },
+            'method': 'DELETE',
+            'url': r'^http://pylxd.test/1.0/items/an-item',
+        })
 
     def test_init(self):
         """Initial attributes are set."""
@@ -78,6 +94,23 @@ def test_unset_attribute_sync(self):
 
         self.assertEqual(1000, item.age)
 
+    def test_sync(self):
+        """A sync will update attributes from the server."""
+        item = Item(self.client, name='an-item')
+
+        item.sync()
+
+        self.assertEqual(1000, item.age)
+
+    def test_sync_dirty(self):
+        """Sync will not overwrite local attribute changes."""
+        item = Item(self.client, name='an-item')
+
+        item.age = 250
+        item.sync()
+
+        self.assertEqual(250, item.age)
+
     def test_rollback(self):
         """Rollback resets the object from the server."""
         item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
@@ -86,6 +119,7 @@ def test_rollback(self):
         item.rollback()
 
         self.assertEqual(1000, item.age)
+        self.assertFalse(item.dirty)
 
     def test_int_attribute_validator(self):
         """Age is set properly to be an int."""
@@ -116,3 +150,28 @@ def test_not_dirty(self):
         item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
 
         self.assertFalse(item.dirty)
+
+    def test_marshall(self):
+        """The object is marshalled into a dict."""
+        item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
+
+        result = item.marshall()
+
+        self.assertEqual({'age': 15, 'data': {'key': 'val'}}, result)
+
+    def test_delete(self):
+        """The object is deleted, and client is unset."""
+        item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
+
+        item.delete()
+
+        self.assertIsNone(item.client)
+
+    def test_save(self):
+        """Attributes are written to the server; object is marked clean."""
+        item = Item(self.client, name='an-item', age=15, data={'key': 'val'})
+
+        item.age = 69
+        item.save()
+
+        self.assertFalse(item.dirty)


More information about the lxc-devel mailing list