diff options
Diffstat (limited to 'meta/jsonobject/base.py')
| -rw-r--r-- | meta/jsonobject/base.py | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/meta/jsonobject/base.py b/meta/jsonobject/base.py new file mode 100644 index 0000000000..90a5f44c6a --- /dev/null +++ b/meta/jsonobject/base.py @@ -0,0 +1,394 @@ +from __future__ import absolute_import +from collections import namedtuple, OrderedDict +import copy +import six +import inspect +from .exceptions import ( + DeleteNotAllowed, + WrappingAttributeError, +) +from .base_properties import JsonProperty, DefaultProperty +from .utils import check_type + +JsonObjectClassSettings = namedtuple('JsonObjectClassSettings', ['type_config']) + +CLASS_SETTINGS_ATTR = '_$_class_settings' + + +def get_settings(cls): + return getattr(cls, CLASS_SETTINGS_ATTR, + JsonObjectClassSettings(type_config=TypeConfig())) + + +def set_settings(cls, settings): + setattr(cls, CLASS_SETTINGS_ATTR, settings) + + +class TypeConfig(object): + """ + This class allows the user to configure dynamic + type handlers and string conversions for their JsonObject. + + properties is a map from python types to JsonProperty subclasses + string_conversions is a list or tuple of (regex, python type)-tuples + + This class is used to store the configuration but is not part of the API. + To configure: + + class Foo(JsonObject): + # property definitions go here + # ... + + class Meta(object): + update_properties = { + datetime.datetime: MySpecialDateTimeProperty + } + # this is already set by default + # but you can override with your own modifications + string_conversions = ((date_re, datetime.date), + (datetime_re, datetime.datetime), + (time_re, datetime.time), + (decimal_re, decimal.Decimal)) + + If you now do + + foo = Foo() + foo.timestamp = datetime.datetime(1988, 7, 7, 11, 8, 0) + + timestamp will be governed by a MySpecialDateTimeProperty + instead of the default. + + """ + + def __init__(self, properties=None, string_conversions=None): + self._properties = properties if properties is not None else {} + + self._string_conversions = ( + OrderedDict(string_conversions) if string_conversions is not None + else OrderedDict() + ) + # cache this + self.string_conversions = self._get_string_conversions() + self.properties = self._properties + + def replace(self, properties=None, string_conversions=None): + return TypeConfig( + properties=(properties if properties is not None + else self._properties), + string_conversions=(string_conversions if string_conversions is not None + else self._string_conversions) + ) + + def updated(self, properties=None, string_conversions=None): + """ + update properties and string_conversions with the paramenters + keeping all non-mentioned items the same as before + returns a new TypeConfig with these changes + (does not modify original) + + """ + _properties = self._properties.copy() + _string_conversions = self.string_conversions[:] + if properties: + _properties.update(properties) + if string_conversions: + _string_conversions.extend(string_conversions) + return TypeConfig( + properties=_properties, + string_conversions=_string_conversions, + ) + + def _get_string_conversions(self): + result = [] + for pattern, conversion in self._string_conversions.items(): + conversion = ( + conversion if conversion not in self._properties + else self._properties[conversion](type_config=self).to_python + ) + result.append((pattern, conversion)) + return result + + +META_ATTRS = ('properties', 'string_conversions', 'update_properties') + + +class JsonObjectMeta(type): + class Meta(object): + pass + + def __new__(mcs, name, bases, dct): + cls = super(JsonObjectMeta, mcs).__new__(mcs, name, bases, dct) + + cls.__configure(**{key: value + for key, value in cls.Meta.__dict__.items() + if key in META_ATTRS}) + cls_settings = get_settings(cls) + + properties = {} + properties_by_name = {} + for key, value in dct.items(): + if isinstance(value, JsonProperty): + properties[key] = value + elif key.startswith('_'): + continue + elif type(value) in cls_settings.type_config.properties: + property_ = cls_settings.type_config.properties[type(value)](default=value) + properties[key] = dct[key] = property_ + setattr(cls, key, property_) + + for key, property_ in properties.items(): + property_.init_property(default_name=key, + type_config=cls_settings.type_config) + assert property_.name is not None, property_ + assert property_.name not in properties_by_name, \ + 'You can only have one property named {0}'.format( + property_.name) + properties_by_name[property_.name] = property_ + + for base in bases: + if getattr(base, '_properties_by_attr', None): + for key, value in base._properties_by_attr.items(): + if key not in properties: + properties[key] = value + properties_by_name[value.name] = value + + cls._properties_by_attr = properties + cls._properties_by_key = properties_by_name + return cls + + def __configure(cls, properties=None, string_conversions=None, + update_properties=None): + super_settings = get_settings(super(cls, cls)) + assert not properties or not update_properties, \ + "{} {}".format(properties, update_properties) + type_config = super_settings.type_config + if update_properties is not None: + type_config = type_config.updated(properties=update_properties) + elif properties is not None: + type_config = type_config.replace(properties=properties) + if string_conversions is not None: + type_config = type_config.replace( + string_conversions=string_conversions) + set_settings(cls, super_settings._replace(type_config=type_config)) + return cls + + +class _JsonObjectPrivateInstanceVariables(object): + + def __init__(self, dynamic_properties=None): + self.dynamic_properties = dynamic_properties or {} + + +@six.add_metaclass(JsonObjectMeta) +class JsonObjectBase(object): + _allow_dynamic_properties = False + _validate_required_lazily = False + + _properties_by_attr = None + _properties_by_key = None + + _string_conversions = () + + def __init__(self, _obj=None, **kwargs): + setattr(self, '_$', _JsonObjectPrivateInstanceVariables()) + + self._obj = check_type(_obj, dict, + 'JsonObject must wrap a dict or None') + self._wrapped = {} + + for key, value in self._obj.items(): + try: + self.set_raw_value(key, value) + except AttributeError: + raise WrappingAttributeError( + "can't set attribute corresponding to {key!r} " + "on a {cls} while wrapping {data!r}".format( + cls=self.__class__, + key=key, + data=_obj, + ) + ) + + for attr, value in kwargs.items(): + try: + setattr(self, attr, value) + except AttributeError: + raise WrappingAttributeError( + "can't set attribute {key!r} " + "on a {cls} while wrapping {data!r}".format( + cls=self.__class__, + key=attr, + data=_obj, + ) + ) + + for key, value in self._properties_by_key.items(): + if key not in self._obj: + try: + d = value.default() + except TypeError: + d = value.default(self) + self[key] = d + + def set_raw_value(self, key, value): + wrapped = self.__wrap(key, value) + if key in self._properties_by_key: + self[key] = wrapped + else: + setattr(self, key, wrapped) + + @classmethod + def properties(cls): + return cls._properties_by_attr.copy() + + @property + def __dynamic_properties(self): + return getattr(self, '_$').dynamic_properties + + @classmethod + def wrap(cls, obj): + self = cls(obj) + return self + + def validate(self, required=True): + for key, value in self._wrapped.items(): + self.__get_property(key).validate(value, required=required) + + def to_json(self): + self.validate() + return copy.deepcopy(self._obj) + + def __get_property(self, key): + try: + return self._properties_by_key[key] + except KeyError: + return DefaultProperty(type_config=get_settings(self).type_config) + + def __wrap(self, key, value): + property_ = self.__get_property(key) + + if value is None: + return None + + return property_.wrap(value) + + def __unwrap(self, key, value): + property_ = self.__get_property(key) + try: + property_.validate( + value, + required=not self._validate_required_lazily, + recursive=False, + ) + except TypeError: + property_.validate( + value, + required=not self._validate_required_lazily, + ) + if value is None: + return None, None + + return property_.unwrap(value) + + def __setitem__(self, key, value): + wrapped, unwrapped = self.__unwrap(key, value) + self._wrapped[key] = wrapped + if self.__get_property(key).exclude(unwrapped): + self._obj.pop(key, None) + else: + self._obj[key] = unwrapped + if key not in self._properties_by_key: + assert key not in self._properties_by_attr + self.__dynamic_properties[key] = wrapped + super(JsonObjectBase, self).__setattr__(key, wrapped) + + def __is_dynamic_property(self, name): + return ( + name not in self._properties_by_attr and + not name.startswith('_') and + not inspect.isdatadescriptor(getattr(self.__class__, name, None)) + ) + + def __setattr__(self, name, value): + if self.__is_dynamic_property(name): + if self._allow_dynamic_properties: + self[name] = value + else: + raise AttributeError( + "{0!r} is not defined in schema " + "(not a valid property)".format(name) + ) + else: + super(JsonObjectBase, self).__setattr__(name, value) + + def __delitem__(self, key): + if key in self._properties_by_key: + raise DeleteNotAllowed(key) + else: + if not self.__is_dynamic_property(key): + raise KeyError(key) + del self._obj[key] + del self._wrapped[key] + del self.__dynamic_properties[key] + super(JsonObjectBase, self).__delattr__(key) + + def __delattr__(self, name): + if name in self._properties_by_attr: + raise DeleteNotAllowed(name) + elif self.__is_dynamic_property(name): + del self[name] + else: + super(JsonObjectBase, self).__delattr__(name) + + def __repr__(self): + name = self.__class__.__name__ + predefined_properties = self._properties_by_attr.keys() + predefined_property_keys = set(self._properties_by_attr[p].name + for p in predefined_properties) + dynamic_properties = (set(self._wrapped.keys()) + - predefined_property_keys) + properties = sorted(predefined_properties) + sorted(dynamic_properties) + return u'{name}({keyword_args})'.format( + name=name, + keyword_args=', '.join('{key}={value!r}'.format( + key=key, + value=getattr(self, key) + ) for key in properties), + ) + + +class _LimitedDictInterfaceMixin(object): + """ + mindlessly farms selected dict methods out to an internal dict + + really only a separate class from JsonObject + to keep this mindlessness separate from the methods + that need to be more carefully understood + + """ + _wrapped = None + + def keys(self): + return self._wrapped.keys() + + def items(self): + return self._wrapped.items() + + def iteritems(self): + return self._wrapped.iteritems() + + def __contains__(self, item): + return item in self._wrapped + + def __getitem__(self, item): + return self._wrapped[item] + + def __iter__(self): + return iter(self._wrapped) + + def __len__(self): + return len(self._wrapped) + + +def get_dynamic_properties(obj): + return getattr(obj, '_$').dynamic_properties.copy() |
