diff --git a/openstack/resource.py b/openstack/resource.py index 06f66885f..e1643d182 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -23,6 +23,69 @@ class MethodNotSupported(Exception): """The resource does not support this operation type.""" +class prop(object): + """A helper for defining properties on a Resource. + + A Resource.prop defines some known attributes within a resource's values. + For example we know a User resource will have a name: + + >>> class User(Resource): + ... name = prop('name') + ... + >>> u = User() + >>> u.name = 'John Doe' + >>> print u['name'] + John Doe + + User objects can now be accessed via the User().name attribute. The 'name' + value we pass as an attribute is the name of the attribute in the message. + This means that you don't need to use the same name for your attribute as + will be set within the object. For example: + + >>> class User(Resource): + ... name = prop('userName') + ... + >>> u = User() + >>> u.name = 'John Doe' + >>> print u['userName'] + John Doe + + There is limited validation ability in props. + + You can validate the type of values that are set: + + >>> class User(Resource): + ... name = prop('userName') + ... age = prop('age', type=int) + ... + >>> u = User() + >>> u.age = 'thirty' + TypeError: Invalid type for attr age + """ + + def __init__(self, name, type=None): + self.name = name + self.type = type + + def __get__(self, instance, owner): + try: + return instance._attrs[self.name] + except KeyError: + raise AttributeError('Unset property: %s', self.name) + + def __set__(self, instance, value): + if self.type and not isinstance(value, self.type): + raise TypeError('Invalid type for attr %s' % self.name) + + instance._attrs[self.name] = value + + def __delete__(self, instance): + try: + del instance._attrs[self.name] + except KeyError: + raise AttributeError('Unset property: %s', self.name) + + @six.add_metaclass(abc.ABCMeta) class Resource(collections.MutableMapping): """A base class that represents a remote resource. diff --git a/openstack/tests/test_resource.py b/openstack/tests/test_resource.py index 3a2c51870..4bb2e7594 100644 --- a/openstack/tests/test_resource.py +++ b/openstack/tests/test_resource.py @@ -43,6 +43,10 @@ class FakeResource(resource.Resource): allow_create = allow_retrieve = allow_update = True allow_delete = allow_list = True + name = resource.prop('name') + first = resource.prop('attr1') + second = resource.prop('attr2') + class ResourceTests(base.TestTransportBase): @@ -59,7 +63,9 @@ class ResourceTests(base.TestTransportBase): self.stub_url(httpretty.POST, path=fake_path, json=fake_body) obj = FakeResource.new(name=fake_name, - attr1=fake_attr1, attr2=fake_attr2) + attr1=fake_attr1, + attr2=fake_attr2) + obj.create(self.session) self.assertFalse(obj.is_dirty) @@ -75,19 +81,28 @@ class ResourceTests(base.TestTransportBase): self.assertEqual(fake_attr1, obj['attr1']) self.assertEqual(fake_attr2, obj['attr2']) + self.assertEqual(fake_name, obj.name) + self.assertEqual(fake_attr1, obj.first) + self.assertEqual(fake_attr2, obj.second) + @httpretty.activate def test_get(self): self.stub_url(httpretty.GET, path=[fake_path, fake_id], json=fake_body) obj = FakeResource.get_by_id(self.session, fake_id) - self.assertEqual(fake_name, obj['name']) self.assertEqual(fake_id, obj.id) + self.assertEqual(fake_name, obj['name']) self.assertEqual(fake_attr1, obj['attr1']) self.assertEqual(fake_attr2, obj['attr2']) + self.assertEqual(fake_name, obj.name) + self.assertEqual(fake_attr1, obj.first) + self.assertEqual(fake_attr2, obj.second) + @httpretty.activate def test_update(self): new_attr1 = 'attr5' + new_attr2 = 'attr6' fake_body1 = fake_body.copy() fake_body1[fake_resource]['attr1'] = new_attr1 @@ -98,12 +113,13 @@ class ResourceTests(base.TestTransportBase): obj = FakeResource.new(name=fake_name, attr1=new_attr1, - attr2=fake_attr2) + attr2=new_attr2) obj.create(self.session) self.assertFalse(obj.is_dirty) self.assertEqual(new_attr1, obj['attr1']) obj['attr1'] = fake_attr1 + obj.second = fake_attr2 self.assertTrue(obj.is_dirty) obj.update(self.session) @@ -118,6 +134,10 @@ class ResourceTests(base.TestTransportBase): self.assertEqual(fake_attr1, obj['attr1']) self.assertEqual(fake_attr2, obj['attr2']) + self.assertEqual(fake_name, obj.name) + self.assertEqual(fake_attr1, obj.first) + self.assertEqual(fake_attr2, obj.second) + @httpretty.activate def test_delete(self): self.stub_url(httpretty.GET, path=[fake_path, fake_id], json=fake_body) @@ -149,4 +169,22 @@ class ResourceTests(base.TestTransportBase): for obj in objs: self.assertIn(obj.id, range(fake_id, fake_id + 3)) self.assertEqual(fake_name, obj['name']) + self.assertEqual(fake_name, obj.name) self.assertIsInstance(obj, FakeResource) + + def test_attrs(self): + obj = FakeResource() + + try: + obj.name + except AttributeError: + pass + else: + self.fail("Didn't raise attribute error") + + try: + del obj.name + except AttributeError: + pass + else: + self.fail("Didn't raise attribute error")