The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/151
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === This patch adds the ability for the model to know what changed since the last sync. This would allow us to do things like use PATCH instead of PUT. It also added some transactional elements, like the ability to rollback all your local changes to whatever is on the server. I also found a bug in the marshalling around `Attribute.readonly`. That is now fixed.
From 06d412fc9498c38268bb299b292af340a3fb57cd Mon Sep 17 00:00:00 2001 From: Paul Hummer <paul.hum...@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.hum...@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.hum...@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)
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel