Handle booleans that are strings in APIs

Coerces string values to a boolean only when the prop is given a
type=BoolStr. BoolStr only accepts string words, no shortened values,
synonyms or integer values. When no acceptable value is found a
TypeError or ValueError is raised. Setters and Constructor calls
validate the attribute value as a fail-fast behavior.

Change-Id: I382426325522227a83ec5234c5116da97e71cfa2
Closes-Bug: #1365724
This commit is contained in:
Steve Lewis
2014-09-25 15:21:04 -07:00
parent c7262b0865
commit 049a2a09d5
8 changed files with 129 additions and 10 deletions

40
openstack/format.py Normal file
View File

@@ -0,0 +1,40 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class BoolStr(object):
"""A custom boolean/string hybrid type for resource.props.
Translates a given value to the desired type.
"""
def __init__(self, given):
"""A boolean parser.
Interprets the given value as a boolean, ignoring whitespace and case.
A TypeError is raised when interpreted as neither True nor False.
"""
expr = str(given).lower()
if 'true' == expr:
self.parsed = True
elif 'false' == expr:
self.parsed = False
else:
msg = 'Invalid as boolean: %s' % given
raise ValueError(msg)
def __bool__(self):
return self.parsed
__nonzero__ = __bool__
def __str__(self):
return str(self.parsed)

View File

@@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from openstack import format
from openstack.identity import identity_service
from openstack import resource
@@ -30,4 +31,4 @@ class Role(resource.Resource):
# Properties
description = resource.prop('description')
name = resource.prop('name')
enabled = resource.prop('enabled') # API implements as string
enabled = resource.prop('enabled', type=format.BoolStr)

View File

@@ -66,13 +66,24 @@ class prop(object):
def __get__(self, instance, owner):
try:
return instance._attrs[self.name]
value = instance._attrs[self.name]
except KeyError:
raise AttributeError('Unset property: %s' % self.name)
if self.type and not isinstance(value, self.type):
value = self.type(value)
attr = getattr(value, 'parsed', None)
if attr is not None:
value = attr
return value
def __set__(self, instance, value):
if self.type and not isinstance(value, self.type):
raise TypeError('Invalid type for attr %s' % self.name)
try:
value = str(self.type(value)) # validate to fail fast
except AttributeError:
raise TypeError('Invalid type for attr: %s' % self.name)
instance._attrs[self.name] = value
@@ -127,6 +138,11 @@ class Resource(collections.MutableMapping):
attrs = {}
self._attrs = attrs
# ensure setters are called for type coercion
for k, v in attrs.items():
if k != 'id': # id property is read only
setattr(self, k, v)
self._dirty = set() if loaded else set(attrs.keys())
self._loaded = loaded
if not self.resource_name:

View File

@@ -39,7 +39,7 @@ class TestRole(testtools.TestCase):
def test_make_it(self):
sot = role.Role(EXAMPLE)
self.assertEqual(EXAMPLE['enabled'], sot.enabled)
self.assertEqual(True, sot.enabled)
self.assertEqual(EXAMPLE['description'], sot.description)
self.assertEqual(EXAMPLE['id'], sot.id)
self.assertEqual(EXAMPLE['name'], sot.name)

View File

@@ -17,16 +17,16 @@ from openstack.network.v2 import port
IDENTIFIER = 'IDENTIFIER'
EXAMPLE = {
'admin_state_up': True,
'allowed_address_pairs': '2',
'allowed_address_pairs': {'2': 2},
'binding:host_id': '3',
'binding:profile': '4',
'binding:vif_details': '5',
'binding:vif_details': {'5': 5},
'binding:vif_type': '6',
'binding:vnic_type': '7',
'device_id': '8',
'device_owner': '9',
'extra_dhcp_opts': '10',
'fixed_ips': '11',
'extra_dhcp_opts': {'10': 10},
'fixed_ips': {'11': '12'},
'id': IDENTIFIER,
'mac_address': '13',
'name': '14',

View File

@@ -18,7 +18,7 @@ from openstack.network.v2 import router
IDENTIFIER = 'IDENTIFIER'
EXAMPLE = {
'admin_state_up': True,
'external_gateway_info': '2',
'external_gateway_info': {'2': '3'},
'id': IDENTIFIER,
'name': '4',
'tenant_id': '5',

View File

@@ -0,0 +1,41 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from openstack import format
class TestFormat(testtools.TestCase):
def test_parse_true(self):
self.assertEqual(True, format.BoolStr(True).parsed)
self.assertEqual(True, format.BoolStr('True').parsed)
self.assertEqual(True, format.BoolStr('TRUE').parsed)
self.assertEqual(True, format.BoolStr('true').parsed)
def test_parse_false(self):
self.assertEqual(False, format.BoolStr(False).parsed)
self.assertEqual(False, format.BoolStr('False').parsed)
self.assertEqual(False, format.BoolStr('FALSE').parsed)
self.assertEqual(False, format.BoolStr('false').parsed)
def test_parse_fails(self):
self.assertRaises(ValueError, format.BoolStr, None)
self.assertRaises(ValueError, format.BoolStr, '')
self.assertRaises(ValueError, format.BoolStr, 'INVALID')
def test_to_str_true(self):
self.assertEqual('True', str(format.BoolStr(True)))
self.assertEqual('True', str(format.BoolStr('True')))
def test_to_str_false(self):
self.assertEqual('False', str(format.BoolStr('False')))
self.assertEqual('False', str(format.BoolStr(False)))

View File

@@ -16,6 +16,7 @@ import httpretty
import mock
from openstack import exceptions
from openstack import format
from openstack import resource
from openstack import session
from openstack.tests import base
@@ -34,6 +35,7 @@ fake_base_path = '/fakes/%(name)s/data'
fake_path = '/fakes/rey/data'
fake_data = {'id': fake_id,
'enabled': True,
'name': fake_name,
'attr1': fake_attr1,
'attr2': fake_attr2}
@@ -49,6 +51,7 @@ class FakeResource(resource.Resource):
allow_create = allow_retrieve = allow_update = True
allow_delete = allow_list = allow_head = True
enabled = resource.prop('enabled', type=format.BoolStr)
name = resource.prop('name')
first = resource.prop('attr1')
second = resource.prop('attr2')
@@ -84,6 +87,7 @@ class ResourceTests(base.TestTransportBase):
self.stub_url(httpretty.POST, path=fake_path, json=fake_body)
obj = FakeResource.new(name=fake_name,
enabled=True,
attr1=fake_attr1,
attr2=fake_attr2)
@@ -92,16 +96,19 @@ class ResourceTests(base.TestTransportBase):
last_req = httpretty.last_request().parsed_body[fake_resource]
self.assertEqual(3, len(last_req))
self.assertEqual(4, len(last_req))
self.assertEqual('True', last_req['enabled'])
self.assertEqual(fake_name, last_req['name'])
self.assertEqual(fake_attr1, last_req['attr1'])
self.assertEqual(fake_attr2, last_req['attr2'])
self.assertEqual(fake_id, obj.id)
self.assertEqual('True', obj['enabled'])
self.assertEqual(fake_name, obj['name'])
self.assertEqual(fake_attr1, obj['attr1'])
self.assertEqual(fake_attr2, obj['attr2'])
self.assertEqual(True, obj.enabled)
self.assertEqual(fake_name, obj.name)
self.assertEqual(fake_attr1, obj.first)
self.assertEqual(fake_attr2, obj.second)
@@ -330,3 +337,17 @@ class TestFind(base.TestCase):
self.assertEqual(fake_attr2, faker.id)
faker.id_attribute = 'id'
self.assertEqual(fake_id, faker.id)
def test_boolstr_prop(self):
faker = FakeResource(fake_data)
self.assertEqual(True, faker.enabled)
self.assertEqual('True', faker['enabled'])
faker.enabled = False
self.assertEqual(False, faker.enabled)
self.assertEqual('False', faker['enabled'])
# should fail fast
def set_invalid():
faker.enabled = 'INVALID'
self.assertRaises(ValueError, set_invalid)