Source code for yggdrasil.metaschema.datatypes.MultiMetaschemaType
import copy
from collections import OrderedDict
from yggdrasil.metaschema import MetaschemaTypeError
from yggdrasil.metaschema.datatypes import get_type_class
from yggdrasil.metaschema.datatypes.MetaschemaType import MetaschemaType
from yggdrasil.metaschema.properties.TypeMetaschemaProperty import (
TypeMetaschemaProperty)
[docs]def create_multitype_class(types):
r"""Create a MultiMetaschemaType class that wraps multiple
classes.
Args:
types (list): List of names of types.
Returns:
class: Subclass of MultiMetaschemaType that add classes.
"""
type_classes = OrderedDict()
type_name = '_'.join(types)
class_name = str('MultiMetaschemaType_%s' % type_name)
for t in types:
type_classes[t] = get_type_class(t)
out = type(class_name, (MultiMetaschemaType, ),
{'type_classes': type_classes,
'name': type_name})
return out
[docs]class MultiMetaschemaType(MetaschemaType):
r"""Type class for handling behavior when more than one type is
valid."""
_dont_register = True
inherit_properties = False
type_classes = OrderedDict()
def __init__(self, **typedef):
typedef.setdefault('type', list(self.type_classes.keys()))
self.type_instances = OrderedDict()
super(MultiMetaschemaType, self).__init__(**typedef)
[docs] @classmethod
def get_type_class(cls, typedef=None, obj=None):
r"""Get the type class from the provided typedef.
"""
if (typedef is not None) and isinstance(typedef['type'],
(str, bytes)):
type_name = typedef['type']
else:
type_name = TypeMetaschemaProperty.encode(obj)
if type_name not in cls.type_classes:
raise MetaschemaTypeError(
"Type '%s' not in set of supported types (%s)."
% (type_name, list(cls.type_classes.keys())))
return cls.type_classes[type_name]
[docs] @classmethod
def encode_data(cls, obj, typedef):
r"""Encode an object's data.
Args:
obj (object): Object to encode.
typedef (dict): Type definition that should be used to encode the
object.
Returns:
string: Encoded object.
"""
type_class = cls.get_type_class(typedef=typedef, obj=obj)
return type_class.encode_data(obj, typedef)
[docs] @classmethod
def decode_data(cls, obj, typedef):
r"""Decode an object.
Args:
obj (string): Encoded object to decode.
typedef (dict): Type definition that should be used to decode the
object.
Returns:
object: Decoded object.
"""
type_class = cls.get_type_class(typedef=typedef, obj=obj)
return type_class.decode_data(obj, typedef)
[docs] @classmethod
def transform_type(cls, obj, typedef=None):
r"""Transform an object based on type info.
Args:
obj (object): Object to transform.
typedef (dict): Type definition that should be used to transform the
object.
Returns:
object: Transformed object.
"""
type_class = cls.get_type_class(typedef=typedef, obj=obj)
new_typedef = None
if typedef is not None:
new_typedef = dict(typedef, type=type_class.name)
return type_class.transform_type(obj, typedef=new_typedef)
[docs] @classmethod
def coerce_type(cls, obj, typedef=None, **kwargs):
r"""Coerce objects of specific types to match the data type.
Args:
obj (object): Object to be coerced.
typedef (dict, optional): Type defintion that object should be
coerced to. Defaults to None.
**kwargs: Additional keyword arguments are metadata entries that may
aid in coercing the type.
Returns:
object: Coerced object.
"""
try:
type_class = cls.get_type_class(typedef=typedef,
obj=obj)
except MetaschemaTypeError as e:
raise ValueError(e)
new_typedef = None
if typedef is not None:
new_typedef = dict(typedef, type=type_class.name)
return type_class.coerce_type(obj, typedef=new_typedef, **kwargs)
[docs] @classmethod
def issubtype(cls, t):
r"""Determine if this type is a subclass of the provided type.
Args:
t (str, list): Type name or list of type names to check against.
Returns:
bool: True if this type is a subtype of the specified type t.
"""
if isinstance(t, list):
return (len(set(cls.type_classes.keys()).intersection(t)) > 0)
return (t in cls.type_classes)
[docs] @classmethod
def validate(cls, obj, raise_errors=False):
r"""Validate an object to check if it could be of this type.
Args:
obj (object): Object to validate.
raise_errors (bool, optional): If True, errors will be raised when
the object fails to be validated. Defaults to False.
Returns:
bool: True if the object could be of this type, False otherwise.
"""
for tcls in cls.type_classes.values():
if tcls.validate(obj):
return True
if raise_errors:
raise ValueError(("Object of type '%s' is not one of the accepted "
+ "Python types for the allowed set or types (%s).") %
(type(obj), list(cls.type_classes.keys())))
return False
[docs] @classmethod
def normalize(cls, obj):
r"""Normalize an object, if possible, to conform to this type.
Args:
obj (object): Object to normalize.
Returns:
object: Normalized object.
"""
for tcls in cls.type_classes.values():
obj = tcls.normalize(obj)
return obj
[docs] @classmethod
def encode_type(cls, obj, typedef=None, **kwargs):
r"""Encode an object's type definition.
Args:
obj (object): Object to encode.
typedef (dict, optional): Type properties that should be used to
initialize the encoded type definition in certain cases.
Defaults to None and is ignored.
**kwargs: Additional keyword arguments are treated as additional
schema properties.
Returns:
dict: Encoded type definition.
"""
type_class = cls.get_type_class(typedef=typedef, obj=obj)
new_typedef = None
if typedef is not None:
new_typedef = dict(typedef, type=type_class.name)
return type_class.encode_type(obj, typedef=new_typedef, **kwargs)
[docs] def update_typedef(self, **kwargs):
r"""Update the current typedef with new values.
Args:
**kwargs: All keyword arguments are considered to be new type
definitions. If they are a valid definition property, they
will be copied to the typedef associated with the instance.
Returns:
dict: A dictionary of keyword arguments that were not added to the
type definition.
Raises:
MetaschemaTypeError: If the current type does not match the type being
updated to.
"""
types = kwargs.pop('type', [])
if self.type_instances:
if set(types) != set(self.type_instances.keys()):
raise MetaschemaTypeError(
"New types (%s) do not match old (%s)."
% (set(types), set(self.type_instances.keys())))
for tcls in self.type_instances.values():
tcls.update_typedef(**copy.deepcopy(kwargs))
else:
if set(types) != set(self.type_classes.keys()):
raise MetaschemaTypeError(
"New types (%s) do not match class's (%s)."
% (set(types), set(self.type_classes.keys())))
for t, tcls in self.type_classes.items():
self.type_instances[t] = tcls(**copy.deepcopy(kwargs))
return super(MultiMetaschemaType, self).update_typedef(
type=types, **kwargs)
[docs] @classmethod
def definition_schema(cls):
r"""JSON schema for validating a type definition schema."""
types = list(cls.type_classes.keys())
out = {'title': cls.name,
'description': cls.description,
'type': 'object',
'required': copy.deepcopy(cls.definition_properties),
'properties': {'type': {'oneOf': [
{'enum': types},
{'type': 'array',
'items': {'enum': types}}]}}}
return out
[docs] @classmethod
def metadata_schema(cls):
r"""JSON schema for validating a JSON serialization of the type."""
types = list(cls.type_classes.keys())
out = {'title': cls.name,
'description': cls.description,
'type': 'object',
'required': copy.deepcopy(cls.metadata_properties),
'properties': {'type': {'oneOf': [
{'enum': types},
{'type': 'array',
'items': {'enum': types}}]}}}
return out
@classmethod
def _generate_data(cls, typedef, **kwargs):
r"""Generate mock data for the specified type.
Args:
typedef (dict): Type definition.
Returns:
object: Python object of the specified type.
"""
typedef = copy.deepcopy(typedef)
types = typedef.pop('type', [])
for t in types:
try:
return cls.type_classes[t].generate_data(
dict(typedef, type=t), **kwargs)
except BaseException:
pass
raise NotImplementedError # pragma: debug