Resource Properties

Provide a helper method for defining known properties that are a part of
a Resource. This will provide a developer friendly way to access
resource properties without needing to know the format of message.

It provides a basic example of validation via the type= parameter. It is
meant as a reference and may be removed later in favour of a proper
validation framework.

Access to the underlying message is still available by the item
accessors, ie setting obj['prop'] will set 'prop' within the resource
and ignore any validation or attribute safety net.

Change-Id: Iccfa4fad3e2f444aac0392a776f5948cefb13791
This commit is contained in:
Jamie Lennox 2014-04-25 17:26:37 +10:00
parent 54f91a596d
commit 8f5a677921
2 changed files with 104 additions and 3 deletions

View File

@ -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.

View File

@ -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")