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:
40
openstack/format.py
Normal file
40
openstack/format.py
Normal 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)
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
41
openstack/tests/test_format.py
Normal file
41
openstack/tests/test_format.py
Normal 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)))
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user