import inspect
from .yamlizing_error import YamlizingError
class NODEFAULT:
def __new__(cls):
raise NotImplementedError
def __init__(self):
raise NotImplementedError
class _Attribute(object):
__slots__ = ()
def __repr__(self):
rep = '<{}'.format(self.__class__.__name__)
for attr_name in self.__class__.__slots__:
attr = getattr(self, attr_name)
if inspect.isclass(attr):
attr = attr.__name__
rep += ' {}:{}'.format(attr_name, attr)
return rep + '>'
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
for attr_name in self.__class__.__slots__:
if getattr(self, attr_name) != getattr(other, attr_name):
return False
return True
def __hash__(self):
return sum(hash(getattr(self, attr_name))
for attr_name in self.__class__.__slots__)
def has_default(self, obj):
raise NotImplementedError
@property
def is_required(self):
raise NotImplementedError
def ensure_type(self, data, node):
raise NotImplementedError
def to_yaml(self, obj, dumper, node_items):
raise NotImplementedError
def get_value(self, obj):
raise NotImplementedError
def set_value(self, obj, value):
raise NotImplementedError
class Attribute(_Attribute):
"""
Represents an attribute of a Python class, and a key/value pair in YAML.
Attributes
----------
name : str
name of the attribute within the Python class
key : str
name of the attribute within the YAML representation
type : type or ANY
type of the attribute within the Python class. When ``ANY``, the type
is a pass-through and whatever YAML determines it should be will be
applied.
default : value or NODEFAULT
default value if not supplied in YAML. If ``default=NODEFAULT``, then
the attribute must be supplied.
storage_name : str
``'_yamlized_' + name``, stored as a separate attribute for speed.
"""
__slots__ = ('_name', 'storage_name', 'key', 'type', 'default', 'fvalidator', 'doc')
def __init__(self, name=None, key=None, type=NODEFAULT, default=NODEFAULT, validator=None,
doc=None):
from yamlize.yamlizable import Dynamic, Typed
# initialize _name for .name assignment
self._name = None
self.storage_name = None
self.key = key
self.name = name # sets storage_name and key if applicable
self.default = default
self.fvalidator = validator
self.doc = doc
if type == NODEFAULT:
self.type = Dynamic
else:
self.type = Typed(type)
@property
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
if name is not None:
self.storage_name = '_yamlized_' + name
if self.key is None:
self.key = name
def has_default(self, obj):
return not hasattr(obj, self.storage_name)
@property
def is_required(self):
return self.default is NODEFAULT
def ensure_type(self, data, node=None):
if isinstance(data, self.type) or data == self.default:
return data
try:
new_value = self.type(data)
except BaseException:
raise YamlizingError('Failed to coerce value `{}` to type `{}`'
.format(data, self.type), node)
if new_value != data:
raise YamlizingError('Coerced `{}` to `{}`, but the new value `{}`'
' is not equal to old `{}`.'
.format(type(data), type(new_value), new_value, data),
node)
return new_value
def from_yaml(self, obj, loader, node, round_trip_data):
try:
# it is possible that we attempted to coerce None -> int, when None was the default
value = self.type.from_yaml(loader, node, round_trip_data)
except YamlizingError:
if self.is_required:
raise
else:
value = loader.construct_object(node, deep=True)
if value != self.default:
raise
try:
self.set_value(obj, value)
except Exception as ee:
raise YamlizingError('Failed to assign attribute `{}` to `{}`, '
'got: {}'
.format(self.name, value, ee), node)
def to_yaml(self, obj, dumper, node_items, round_trip_data):
if self.has_default(obj):
# short circuit, don't write out default data
return
data = self.get_value(obj)
try:
val_node = self.type.to_yaml(dumper, data, round_trip_data)
except YamlizingError:
if data == self.default:
val_node = dumper.represent_data(data)
else:
raise
key_node = dumper.represent_data(self.key)
node_items.append((key_node, val_node))
def get_value(self, obj):
return self.__get__(obj)
def set_value(self, obj, value):
self.__set__(obj, value)
def __get__(self, obj, owner=None):
if obj is None:
return self
result = getattr(obj, self.storage_name, self.default)
if result is NODEFAULT:
raise YamlizingError('Attribute `{}` was not defined on `{}`'
.format(self.name, obj))
return result
def __set__(self, obj, value):
value = self.ensure_type(value)
if self.fvalidator is not None:
if self.fvalidator(obj, value) is False:
raise ValueError('Cannot set `{}.{}` to invalid value `{}`'
.format(obj.__class__.__name__, self.name, value))
setattr(obj, self.storage_name, value)
def __delete__(self, obj):
delattr(obj, self.storage_name)
def validator(self, fvalidator):
return type(self)(self.name, self.key, self.type, self.default, fvalidator, self.doc)
class MapItem(_Attribute):
"""
Represents a key of a dictionary, and a key/value pair in YAML.
This should only be used temporarily.
"""
__slots__ = ('key', 'key_type', 'val_type')
def __init__(self, key, key_type, val_type):
self.key = key
self.key_type = key_type
self.val_type = val_type
def has_default(self, obj):
return False
@property
def is_required(self):
return False
def to_yaml(self, obj, dumper, node_items, round_trip_data):
data = self.get_value(obj)
val_node = self.val_type.to_yaml(dumper, data, round_trip_data)
key_node = self.key_type.to_yaml(dumper, self.key, round_trip_data)
node_items.append((key_node, val_node))
def get_value(self, obj):
return obj[self.key]
def set_value(self, obj, value):
obj[self.key] = value
class KeyedListItem(_Attribute):
"""
Represents a key of a dictionary, and a key/value pair in YAML.
This should only be used temporarily.
"""
__slots__ = ('key_attr', 'item_type', 'item_key')
def __init__(self, key_attr, item_type, item_key):
# HACK: key_attr is a descriptor, make it a tuple to trick python
self.key_attr = (key_attr,)
self.item_type = item_type
self.item_key = item_key
def has_default(self, obj):
return False
@property
def is_required(self):
return False
def to_yaml(self, obj, dumper, node_items, round_trip_data):
value = self.get_value(obj)
# HACK: key_attr is a tuple of the Attribute descriptor
key_node, val_node = self.item_type.to_yaml_key_val(
dumper, value, self.key_attr[0], round_trip_data)
node_items.append((key_node, val_node))
def get_value(self, obj):
return obj[self.item_key]
def set_value(self, obj, value):
obj[self.item_key] = value