Prepare the ground to use enums instead of strings
Makes MappedField understand enum classes instead of a dictionary. Also adds a script to generate enum definitions from DMTF definitions, as well as creating compatibility constants. Change-Id: Ic950a35bb9fc603e5c4b943ac19dba982bdf7212
This commit is contained in:
parent
69b03d8010
commit
4b99f93a11
@ -16,6 +16,7 @@
|
||||
import abc
|
||||
import collections
|
||||
import copy
|
||||
import enum
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
@ -269,18 +270,28 @@ class MappedField(Field):
|
||||
:param field: JSON field to fetch the value from. This can be either
|
||||
a string or a list of string. In the latter case, the value will
|
||||
be fetched from a nested object.
|
||||
:param mapping: a mapping to take values from.
|
||||
:param mapping: a mapping to take values from, a dictionary or
|
||||
an enumeration.
|
||||
:param required: whether this field is required. Missing required,
|
||||
but not defaulted, fields result in MissingAttributeError.
|
||||
:param default: the default value to use when the field is missing.
|
||||
This value is not matched against the mapping.
|
||||
"""
|
||||
if not isinstance(mapping, collections.abc.Mapping):
|
||||
raise TypeError("The mapping argument must be a mapping")
|
||||
if isinstance(mapping, type) and issubclass(mapping, enum.Enum):
|
||||
def adapter(value):
|
||||
try:
|
||||
return mapping(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
elif isinstance(mapping, collections.abc.Mapping):
|
||||
adapter = mapping.get
|
||||
else:
|
||||
raise TypeError("The mapping argument must be a mapping or "
|
||||
"an enumeration")
|
||||
|
||||
super(MappedField, self).__init__(
|
||||
field, required=required, default=default,
|
||||
adapter=mapping.get)
|
||||
field, required=required, default=default, adapter=adapter)
|
||||
|
||||
|
||||
class MappedListField(Field):
|
||||
@ -300,10 +311,20 @@ class MappedListField(Field):
|
||||
but not defaulted, fields result in MissingAttributeError.
|
||||
:param default: the default value to use when the field is missing.
|
||||
"""
|
||||
if not isinstance(mapping, collections.abc.Mapping):
|
||||
raise TypeError("The mapping argument must be a mapping")
|
||||
if isinstance(mapping, type) and issubclass(mapping, enum.Enum):
|
||||
def adapter(value):
|
||||
try:
|
||||
return mapping(value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
self._mapping_adapter = mapping.get
|
||||
elif isinstance(mapping, collections.abc.Mapping):
|
||||
adapter = mapping.get
|
||||
else:
|
||||
raise TypeError("The mapping argument must be a mapping or "
|
||||
"an enumeration")
|
||||
|
||||
self._mapping_adapter = adapter
|
||||
super(MappedListField, self).__init__(
|
||||
field, required=required, default=default,
|
||||
adapter=lambda x: x)
|
||||
|
@ -14,6 +14,7 @@
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import enum
|
||||
from http import client as http_client
|
||||
import io
|
||||
import json
|
||||
@ -332,7 +333,9 @@ TEST_JSON = {
|
||||
'Dictionary': {
|
||||
'key1': {'property_a': 'value1', 'property_b': 'value2'},
|
||||
'key2': {'property_a': 'value3', 'property_b': 'value4'}
|
||||
}
|
||||
},
|
||||
'Enum': 'PROTOCOL_FIELD_2',
|
||||
'EnumList': ['PROTOCOL_FIELD_2', 'PROTOCOL_FIELD_3'],
|
||||
}
|
||||
|
||||
|
||||
@ -343,6 +346,13 @@ MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
class EnumMapping(enum.Enum):
|
||||
|
||||
FIELD1 = "PROTOCOL_FIELD_1"
|
||||
FIELD2 = "PROTOCOL_FIELD_2"
|
||||
FIELD3 = "PROTOCOL_FIELD_3"
|
||||
|
||||
|
||||
class NestedTestField(resource_base.CompositeField):
|
||||
string = resource_base.Field('String', required=True)
|
||||
integer = resource_base.Field('Integer', adapter=int)
|
||||
@ -371,6 +381,10 @@ class ComplexResource(resource_base.ResourceBase):
|
||||
non_existing_nested = NestedTestField('NonExistingNested')
|
||||
non_existing_mapped = resource_base.MappedField('NonExistingMapped',
|
||||
MAPPING)
|
||||
enum_mapped = resource_base.MappedField('Enum', EnumMapping)
|
||||
enum_mapped_list = resource_base.MappedListField('EnumList', EnumMapping)
|
||||
non_existing_enum_mapped = resource_base.MappedField('NonExistingEnum',
|
||||
EnumMapping)
|
||||
|
||||
|
||||
class FieldTestCase(base.TestCase):
|
||||
@ -402,6 +416,10 @@ class FieldTestCase(base.TestCase):
|
||||
self.test_resource.dictionary['key2'].property_b)
|
||||
self.assertIsNone(self.test_resource.non_existing_nested)
|
||||
self.assertIsNone(self.test_resource.non_existing_mapped)
|
||||
self.assertEqual(EnumMapping.FIELD2, self.test_resource.enum_mapped)
|
||||
self.assertEqual([EnumMapping.FIELD2, EnumMapping.FIELD3],
|
||||
self.test_resource.enum_mapped_list)
|
||||
self.assertIsNone(self.test_resource.non_existing_enum_mapped)
|
||||
|
||||
def test_missing_required(self):
|
||||
del self.json['String']
|
||||
@ -437,9 +455,11 @@ class FieldTestCase(base.TestCase):
|
||||
|
||||
def test_mapping_missing(self):
|
||||
self.json['Nested']['Mapped'] = 'banana'
|
||||
self.json['Enum'] = 'banana'
|
||||
self.test_resource.refresh(force=True)
|
||||
|
||||
self.assertIsNone(self.test_resource.nested.mapped)
|
||||
self.assertIsNone(self.test_resource.enum_mapped)
|
||||
|
||||
def test_composite_field_as_mapping(self):
|
||||
field = self.test_resource.nested
|
||||
@ -458,6 +478,11 @@ class FieldTestCase(base.TestCase):
|
||||
self.assertRaisesRegex(KeyError, '_load', lambda: field['_load'])
|
||||
self.assertRaisesRegex(KeyError, '__init__', lambda: field['__init__'])
|
||||
|
||||
def test_invalid_mapping_definiton(self):
|
||||
self.assertRaises(TypeError, resource_base.MappedField, 'Field', 42)
|
||||
self.assertRaises(TypeError, resource_base.MappedListField,
|
||||
'Field', 42)
|
||||
|
||||
|
||||
class PartialKeyResource(resource_base.ResourceBase):
|
||||
string = resource_base.Field(
|
||||
|
83
tools/generate-enum.py
Executable file
83
tools/generate-enum.py
Executable file
@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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 argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def to_underscores(item):
|
||||
words = re.findall(r'[A-Z](?:[a-z0-9]+|[A-Z0-9]*(?=[A-Z]|$))', item)
|
||||
return '_'.join(w.upper() for w in words)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("url", help="URL of a DMTF definition")
|
||||
parser.add_argument("name", help="name of the enumeration")
|
||||
parser.add_argument("--compat", help="generate old-style constants",
|
||||
action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.url.startswith("https://") or args.url.startswith("http://"):
|
||||
resp = requests.get(args.url)
|
||||
resp.raise_for_status()
|
||||
content = resp.json()
|
||||
else:
|
||||
with open(args.url, "t") as fp:
|
||||
content = json.load(fp)
|
||||
|
||||
try:
|
||||
definition = content["definitions"][args.name]
|
||||
except KeyError as exc:
|
||||
sys.exit(f"Key {exc} was not found in definition at {args.url}")
|
||||
|
||||
try:
|
||||
items = definition["enum"]
|
||||
descriptions = definition.get("enumDescriptions", {})
|
||||
except (TypeError, KeyError):
|
||||
sys.exit(f"Definition {args.name} is malformed or not en enumeration")
|
||||
|
||||
items = [(to_underscores(item), item) for item in items]
|
||||
|
||||
print(f"class {args.name}(enum.Enum):")
|
||||
for varname, item in items:
|
||||
print(f" {varname} = '{item}'")
|
||||
|
||||
try:
|
||||
description = descriptions[item]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# 79 (expected) - 4 (indentation) - 2 * 3 (quotes) = 69
|
||||
lines = textwrap.wrap(description, 69)
|
||||
lines[0] = '"""' + lines[0]
|
||||
lines[-1] += '"""'
|
||||
for line in lines:
|
||||
print(f' {line}')
|
||||
print()
|
||||
|
||||
if args.compat:
|
||||
print()
|
||||
print("# Backward compatibility")
|
||||
for varname, item in items:
|
||||
print(f"{to_underscores(args.name)}_{varname} = "
|
||||
f"{args.name}.{varname}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
Loading…
x
Reference in New Issue
Block a user