Fixed ordering issue

This commit is contained in:
Pedro Algarvio 2015-07-09 20:57:23 +01:00
parent cf071eaffa
commit 3adb731abc
2 changed files with 201 additions and 33 deletions

View file

@ -407,17 +407,22 @@ class ConfigurationMeta(six.with_metaclass(Prepareable, type)):
attrs['__flatten__'] = False
# Let's record the configuration items/sections
items = OrderedDict()
sections = OrderedDict()
items = {}
sections = {}
order = []
# items from parent classes
for base in reversed(bases):
if hasattr(base, '_items'):
items.update(base._items)
if hasattr(base, '_sections'):
sections.update(base._sections)
if hasattr(base, '_order'):
order.extend(base._order)
# Iterate through attrs to discover items/config sections
for key, value in six.iteritems(attrs):
if not hasattr(value, '__item__') and not hasattr(value, '__config__'):
continue
if hasattr(value, '__item__'):
# the value is an item instance
if hasattr(value, 'title') and value.title is None:
@ -426,13 +431,10 @@ class ConfigurationMeta(six.with_metaclass(Prepareable, type)):
value.title = key
items[key] = value
if hasattr(value, '__config__'):
if value.__flatten__ is True:
# Should not be considered as a section
items[key] = value
else:
# the value is a configuration section
sections[key] = value
sections[key] = value
order.append(key)
attrs['_order'] = order
attrs['_items'] = items
attrs['_sections'] = sections
return type.__new__(mcs, name, bases, attrs)
@ -544,34 +546,42 @@ class Configuration(six.with_metaclass(ConfigurationMeta, object)):
ordering = []
serialized['type'] = 'object'
properties = OrderedDict()
for name, section in cls._sections.items():
serialized_section = section.serialize(None if section.__flatten__ is True else name)
if section.__flatten__ is True:
# Flatten the configuration section into the parent
# configuration
properties.update(serialized_section['properties'])
if 'x-ordering' in serialized_section:
ordering.extend(serialized_section['x-ordering'])
if 'required' in serialized:
required.extend(serialized_section['required'])
else:
# Store it as a configuration section
properties[name] = serialized_section
# Handle the configuration items defined in the class instance
after_items_update = OrderedDict()
for name, config in cls._items.items():
if config.__flatten__ is True:
after_items_update.update(config.serialize())
else:
properties[name] = config.serialize()
for name in cls._order:
skip_order = False
if name in cls._sections:
section = cls._sections[name]
serialized_section = section.serialize(None if section.__flatten__ is True else name)
if section.__flatten__ is True:
# Flatten the configuration section into the parent
# configuration
properties.update(serialized_section['properties'])
if 'x-ordering' in serialized_section:
ordering.extend(serialized_section['x-ordering'])
if 'required' in serialized:
required.extend(serialized_section['required'])
else:
# Store it as a configuration section
properties[name] = serialized_section
# Store the order of the item
ordering.append(name)
if name in cls._items:
config = cls._items[name]
# Handle the configuration items defined in the class instance
if config.__flatten__ is True:
after_items_update.update(config.serialize())
skip_order = True
else:
properties[name] = config.serialize()
if config.required:
# If it's a required item, add it to the required list
required.append(name)
if skip_order is False:
# Store the order of the item
if name not in ordering:
ordering.append(name)
if isinstance(config, BaseItem) and config.required:
# If it's a required item, add it to the required list
required.append(name)
serialized['properties'] = properties
# Update the serialized object with any items to include after properties

View file

@ -94,6 +94,164 @@ class ConfigTestCase(TestCase):
self.assertEqual(Final.serialize()['x-ordering'], ['one', 'two', 'three'])
def test_optional_requirements_config(self):
class BaseRequirements(config.Configuration):
driver = config.StringConfig(default='digital_ocean', format='hidden')
class SSHKeyFileConfiguration(config.Configuration):
ssh_key_file = config.StringConfig(
title='SSH Private Key',
description='The path to an SSH private key which will be used '
'to authenticate on the deployed VMs',
required=True)
class SSHKeyNamesConfiguration(config.Configuration):
ssh_key_names = config.StringConfig(
title='SSH Key Names',
description='The names of an SSH key being managed on '
'Digital Ocean account which will be used to '
'authenticate on the deployed VMs',
required=True)
class Requirements(BaseRequirements):
title = 'Digital Ocean'
description = 'Digital Ocean Cloud VM configuration requirements.'
personal_access_token = config.StringConfig(
title='Personal Access Token',
description='This is the API access token which can be generated '
'under the API/Application on your account',
required=True)
requirements_definition = config.AnyOfConfig(
items=(
SSHKeyFileConfiguration.as_requirements_item(),
SSHKeyNamesConfiguration.as_requirements_item()
),
)(flatten=True)
ssh_key_file = SSHKeyFileConfiguration(flatten=True)
ssh_key_names = SSHKeyNamesConfiguration(flatten=True)
self.maxDiff = None
expexcted = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Digital Ocean",
"description": "Digital Ocean Cloud VM configuration requirements.",
"type": "object",
"properties": {
"driver": {
"default": "digital_ocean",
"format": "hidden",
"type": "string",
"title": "driver"
},
"personal_access_token": {
"type": "string",
"description": "This is the API access token which can be "
"generated under the API/Application on your account",
"title": "Personal Access Token"
},
"ssh_key_file": {
"type": "string",
"description": "The path to an SSH private key which will "
"be used to authenticate on the deployed VMs",
"title": "SSH Private Key"
},
"ssh_key_names": {
"type": "string",
"description": "The names of an SSH key being managed on Digital "
"Ocean account which will be used to authenticate "
"on the deployed VMs",
"title": "SSH Key Names"
}
},
"anyOf": [
{"required": ["ssh_key_file"]},
{"required": ["ssh_key_names"]}
],
"required": [
"personal_access_token"
],
"x-ordering": [
"driver",
"personal_access_token",
"ssh_key_file",
"ssh_key_names",
],
"additionalProperties": False
}
self.assertDictEqual(expexcted, Requirements.serialize())
@skipIf(HAS_JSONSCHEMA is False, 'The \'jsonschema\' library is missing')
def test_optional_requirements_config_validation(self):
class BaseRequirements(config.Configuration):
driver = config.StringConfig(default='digital_ocean', format='hidden')
class SSHKeyFileConfiguration(config.Configuration):
ssh_key_file = config.StringConfig(
title='SSH Private Key',
description='The path to an SSH private key which will be used '
'to authenticate on the deployed VMs',
required=True)
class SSHKeyNamesConfiguration(config.Configuration):
ssh_key_names = config.StringConfig(
title='SSH Key Names',
description='The names of an SSH key being managed on '
'Digial Ocean account which will be used to '
'authenticate on the deployed VMs',
required=True)
class Requirements(BaseRequirements):
title = 'Digital Ocean'
description = 'Digital Ocean Cloud VM configuration requirements.'
personal_access_token = config.StringConfig(
title='Personal Access Token',
description='This is the API access token which can be generated '
'under the API/Application on your account',
required=True)
requirements_definition = config.AnyOfConfig(
items=(
SSHKeyFileConfiguration.as_requirements_item(),
SSHKeyNamesConfiguration.as_requirements_item()
),
)(flatten=True)
ssh_key_file = SSHKeyFileConfiguration(flatten=True)
ssh_key_names = SSHKeyNamesConfiguration(flatten=True)
try:
jsonschema.validate(
{"personal_access_token": "foo", "ssh_key_names": "bar", "ssh_key_file": "test"},
Requirements.serialize()
)
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
try:
jsonschema.validate(
{"personal_access_token": "foo", "ssh_key_names": "bar"},
Requirements.serialize()
)
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
try:
jsonschema.validate(
{"personal_access_token": "foo", "ssh_key_file": "test"},
Requirements.serialize()
)
except jsonschema.exceptions.ValidationError as exc:
self.fail('ValidationError raised: {0}'.format(exc))
with self.assertRaises(jsonschema.exceptions.ValidationError) as excinfo:
jsonschema.validate(
{"personal_access_token": "foo"},
Requirements.serialize()
)
self.assertIn('is not valid under any of the given schemas', excinfo.exception.message)
def test_boolean_config(self):
item = config.BooleanConfig(title='Hungry', description='Are you hungry?')
self.assertDictEqual(