| Index: commit-queue/model.py
|
| ===================================================================
|
| --- commit-queue/model.py (revision 249146)
|
| +++ commit-queue/model.py (working copy)
|
| @@ -1,406 +0,0 @@
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Defines the PersistentMixIn utility class to easily convert classes to and
|
| -from dict for serialization.
|
| -
|
| -This class is aimed at json-compatible serialization, so it supports the limited
|
| -set of structures supported by json; strings, numbers as int or float, list and
|
| -dictionaries.
|
| -
|
| -PersistentMixIn._persistent_members() returns a dict of each member with the
|
| -tuple of expected types. Each member can be decoded in multiple types, for
|
| -example, a subversion revision number could have (None, int, str), meaning that
|
| -the revision could be None, when not known, an int or the int as a string
|
| -representation. The tuple is listed in the prefered order of conversions.
|
| -
|
| -Composites types that cannot be represented exactly in json like tuple, set and
|
| -frozenset are converted from and back to list automatically. Any class instance
|
| -that has been serialized can be unserialized in the same class instance or into
|
| -a bare dict.
|
| -
|
| -See tests/model_tests.py for examples.
|
| -"""
|
| -
|
| -import json
|
| -import logging
|
| -import os
|
| -
|
| -# Set in the output dict to be able to know which class was serialized to help
|
| -# deserialization.
|
| -TYPE_FLAG = '__persistent_type__'
|
| -
|
| -# Marker to tell the deserializer that we don't know the expected type, used in
|
| -# composite types.
|
| -_UNKNOWN = object()
|
| -
|
| -
|
| -def as_dict(value):
|
| - """Recursively converts an object into a dictionary.
|
| -
|
| - Converts tuple,set,frozenset into list and recursively process each items.
|
| - """
|
| - if hasattr(value, 'as_dict') and callable(value.as_dict):
|
| - return value.as_dict()
|
| - elif isinstance(value, (list, tuple, set, frozenset)):
|
| - return [as_dict(v) for v in value]
|
| - elif isinstance(value, dict):
|
| - return dict((as_dict(k), as_dict(v))
|
| - for k, v in value.iteritems())
|
| - elif isinstance(value, (bool, float, int, basestring)) or value is None:
|
| - return value
|
| - else:
|
| - raise AttributeError('Can\'t type %s into a dictionary' % type(value))
|
| -
|
| -
|
| -def _inner_from_dict(name, value, member_types):
|
| - """Recursively regenerates an object.
|
| -
|
| - For each of the allowable types, try to convert it. If None is an allowable
|
| - type, any data that can't be parsed will be parsed as None and will be
|
| - silently discarded. Otherwise, an exception will be raise.
|
| - """
|
| - logging.debug('_inner_from_dict(%s, %r, %s)', name, value, member_types)
|
| - result = None
|
| - if member_types is _UNKNOWN:
|
| - # Use guesswork a bit more and accept anything.
|
| - if isinstance(value, dict):
|
| - if TYPE_FLAG in value:
|
| - result = PersistentMixIn.from_dict(value, _UNKNOWN)
|
| - else:
|
| - # Unserialize it as a raw dict.
|
| - result = dict(
|
| - (_inner_from_dict(None, k, _UNKNOWN),
|
| - _inner_from_dict(None, v, _UNKNOWN))
|
| - for k, v in value.iteritems())
|
| - elif isinstance(value, list):
|
| - # All of these are serialized to list.
|
| - result = [_inner_from_dict(None, v, _UNKNOWN) for v in value]
|
| - elif isinstance(value, (bool, float, int, unicode)):
|
| - result = value
|
| - else:
|
| - raise TypeError('No idea how to convert %r' % value)
|
| - else:
|
| - for member_type in member_types:
|
| - # Explicitly leave None out of this loop.
|
| - if issubclass(member_type, PersistentMixIn):
|
| - if isinstance(value, dict) and TYPE_FLAG in value:
|
| - result = PersistentMixIn.from_dict(value, member_type)
|
| - break
|
| - elif member_type is dict:
|
| - if isinstance(value, dict):
|
| - result = dict(
|
| - (_inner_from_dict(None, k, _UNKNOWN),
|
| - _inner_from_dict(None, v, _UNKNOWN))
|
| - for k, v in value.iteritems())
|
| - break
|
| - elif member_type in (list, tuple, set, frozenset):
|
| - # All of these are serialized to list.
|
| - if isinstance(value, list):
|
| - result = member_type(
|
| - _inner_from_dict(None, v, _UNKNOWN) for v in value)
|
| - break
|
| - elif member_type in (bool, float, int, str, unicode):
|
| - if isinstance(value, member_type):
|
| - result = member_type(value)
|
| - break
|
| - elif member_type is None.__class__ and value is None:
|
| - result = None
|
| - break
|
| - else:
|
| - logging.info(
|
| - 'Ignored %s: didn\'t fit types %s; %s',
|
| - name,
|
| - ', '.join(i.__name__ for i in member_types),
|
| - repr(value)[:200])
|
| - _check_type_value(name, result, member_types)
|
| - return result
|
| -
|
| -
|
| -def to_yaml(obj):
|
| - """Converts a PersistentMixIn into a yaml-inspired format.
|
| -
|
| - Warning: Not unit tested, use at your own risk!
|
| - """
|
| - def align(x):
|
| - y = x.splitlines(True)
|
| - if len(y) > 1:
|
| - return ''.join(y[0:1] + [' ' + z for z in y[1:]])
|
| - return x
|
| - def align_value(x):
|
| - if '\n' in x:
|
| - return '\n ' + align(x)
|
| - return x
|
| -
|
| - if hasattr(obj, 'as_dict') and callable(obj.as_dict):
|
| - out = (to_yaml(obj.as_dict()),)
|
| - elif isinstance(obj, (bool, float, int, unicode)) or obj is None:
|
| - out = (align(str(obj)),)
|
| - elif isinstance(obj, dict):
|
| - if TYPE_FLAG in obj:
|
| - out = ['%s:' % obj[TYPE_FLAG]]
|
| - else:
|
| - out = []
|
| - for k, v in obj.iteritems():
|
| - # Skips many members resolving to bool() == False
|
| - if k.startswith('__') or v in (None, '', False, 0):
|
| - continue
|
| - r = align_value(to_yaml(v))
|
| - if not r:
|
| - continue
|
| - out.append('- %s: %s' % (k, r))
|
| - elif hasattr(obj, '__iter__') and callable(obj.__iter__):
|
| - out = ['- %s' % align(to_yaml(x)) for x in obj]
|
| - else:
|
| - out = ('%s' % obj.__class__.__name__,)
|
| - return '\n'.join(out)
|
| -
|
| -
|
| -def _default_value(member_types):
|
| - """Returns an instance of the first allowed type. Special case None."""
|
| - if member_types[0] is None.__class__:
|
| - return None
|
| - else:
|
| - return member_types[0]()
|
| -
|
| -
|
| -def _check_type_value(name, value, member_types):
|
| - """Raises a TypeError exception if value is not one of the allowed types in
|
| - member_types.
|
| - """
|
| - if not isinstance(value, member_types):
|
| - prefix = '%s e' % name if name else 'E'
|
| - raise TypeError(
|
| - '%sxpected type(s) %s; got %r' %
|
| - (prefix, ', '.join(i.__name__ for i in member_types), value))
|
| -
|
| -
|
| -
|
| -class PersistentMixIn(object):
|
| - """Class to be used as a base class to persistent data in a simplistic way.
|
| -
|
| - Persistent class member needs to be set to a tuple containing the instance
|
| - member variable that needs to be saved or loaded. The first item will be
|
| - default value, e.g.:
|
| - foo = (None, str, dict)
|
| - Will default initialize self.foo to None.
|
| - """
|
| - # Cache of all the subclasses of PersistentMixIn.
|
| - __persistent_classes_cache = None
|
| -
|
| - _read_only = False
|
| -
|
| - def __init__(self, **kwargs):
|
| - """Initializes with the default members."""
|
| - super(PersistentMixIn, self).__init__()
|
| - persistent_members = self._persistent_members()
|
| - for member, member_types in persistent_members.iteritems():
|
| - if member in kwargs:
|
| - value = kwargs.pop(member)
|
| - if isinstance(value, str):
|
| - # Assume UTF-8 all the time. Note: This is explicitly when the object
|
| - # is constructed in the code. This code path is never used when
|
| - # deserializing the object.
|
| - value = value.decode('utf-8')
|
| - else:
|
| - value = _default_value(member_types)
|
| - _check_type_value(member, value, member_types)
|
| - setattr(self, member, value)
|
| - if kwargs:
|
| - raise AttributeError('Received unexpected initializers: %s' % kwargs)
|
| -
|
| - def __setattr__(self, name, value):
|
| - """Enforces immutability if cls._read_only is True."""
|
| - if self._read_only:
|
| - raise TypeError()
|
| - return super(PersistentMixIn, self).__setattr__(name, value)
|
| -
|
| - @classmethod
|
| - def _persistent_members(cls):
|
| - """Returns the persistent items as a dict.
|
| -
|
| - Each entry value can be a tuple when the member can be assigned different
|
| - types.
|
| - """
|
| - # Note that here, cls is the subclass, not PersistentMixIn.
|
| - # TODO(maruel): Cache the results. It's tricky because setting
|
| - # cls.__persistent_members_cache on a class will implicitly set it on its
|
| - # subclass. So in a class hierarchy with A -> B -> PersistentMixIn, calling
|
| - # B()._persistent_members() will incorrectly set the cache for A.
|
| - persistent_members_cache = {}
|
| - # Enumerate on the subclass, not on an instance.
|
| - for item in dir(cls):
|
| - if item.startswith('_'):
|
| - continue
|
| - item_value = getattr(cls, item)
|
| - if isinstance(item_value, type):
|
| - item_value = (item_value,)
|
| - if not isinstance(item_value, tuple):
|
| - continue
|
| - if not all(i is None or i.__class__ == type for i in item_value):
|
| - continue
|
| - if any(i is str for i in item_value):
|
| - raise TypeError(
|
| - '%s is type \'str\' which is currently not supported' % item)
|
| - item_value = tuple(
|
| - f if f is not None else None.__class__ for f in item_value)
|
| - persistent_members_cache[item] = item_value
|
| - return persistent_members_cache
|
| -
|
| - @staticmethod
|
| - def _get_subclass(typename):
|
| - """Returns the PersistentMixIn subclass with the name |typename|."""
|
| - subclass = None
|
| - if PersistentMixIn.__persistent_classes_cache is not None:
|
| - subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
|
| - if not subclass:
|
| - # Get the subclasses recursively.
|
| - PersistentMixIn.__persistent_classes_cache = {}
|
| - def recurse(c):
|
| - for s in c.__subclasses__():
|
| - assert s.__name__ not in PersistentMixIn.__persistent_classes_cache
|
| - PersistentMixIn.__persistent_classes_cache[s.__name__] = s
|
| - recurse(s)
|
| - recurse(PersistentMixIn)
|
| -
|
| - subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
|
| - if not subclass:
|
| - raise KeyError('Couldn\'t find type %s' % typename)
|
| - return subclass
|
| -
|
| - def as_dict(self):
|
| - """Create a dictionary out of this object, i.e. Serialize the object."""
|
| - out = {}
|
| - for member, member_types in self._persistent_members().iteritems():
|
| - value = getattr(self, member)
|
| - _check_type_value(member, value, member_types)
|
| - out[member] = as_dict(value)
|
| - out[TYPE_FLAG] = self.__class__.__name__
|
| - return out
|
| -
|
| - @staticmethod
|
| - def from_dict(data, subclass=_UNKNOWN):
|
| - """Returns an instance of a class inheriting from PersistentMixIn,
|
| - initialized with 'data' dict, i.e. Deserialize the object.
|
| - """
|
| - logging.debug('from_dict(%r, %s)', data, subclass)
|
| - if subclass is _UNKNOWN:
|
| - subclass = PersistentMixIn._get_subclass(data[TYPE_FLAG])
|
| - # This initializes the instance with the default values.
|
| -
|
| - # pylint: disable=W0212
|
| - kwargs = {}
|
| - for member, member_types in subclass._persistent_members().iteritems():
|
| - if member in data:
|
| - try:
|
| - value = _inner_from_dict(member, data[member], member_types)
|
| - except TypeError:
|
| - # pylint: disable=E1103
|
| - logging.error(
|
| - 'Failed to instantiate %s because of member %s',
|
| - subclass.__name__, member)
|
| - raise
|
| - else:
|
| - value = _default_value(member_types)
|
| - _check_type_value(member, value, member_types)
|
| - kwargs[member] = value
|
| - try:
|
| - obj = subclass(**kwargs)
|
| - except TypeError:
|
| - # pylint: disable=E1103
|
| - logging.error('Failed to instantiate %s: %r', subclass.__name__, kwargs)
|
| - raise
|
| - assert isinstance(obj, PersistentMixIn) and obj.__class__ != PersistentMixIn
|
| - return obj
|
| -
|
| - def __str__(self):
|
| - return to_yaml(self)
|
| -
|
| - def __eq__(self, _):
|
| - raise TypeError()
|
| -
|
| - # pylint: disable=R0201
|
| - def __ne__(self, _):
|
| - raise TypeError()
|
| -
|
| -
|
| -def is_equivalent(lhs, rhs):
|
| - """Implements the equivalent of __eq__.
|
| -
|
| - The reason for not implementing __eq__ is to not encourage bad behavior by
|
| - implicitly and recursively using __eq__() in a list().remove() call.
|
| - """
|
| - # pylint: disable=W0212
|
| - if lhs._persistent_members() != rhs._persistent_members():
|
| - return False
|
| - for i in lhs._persistent_members():
|
| - if getattr(lhs, i) != getattr(rhs, i):
|
| - return False
|
| - return True
|
| -
|
| -
|
| -def immutable(func):
|
| - """Member function decorators that convert 'self' to an immutable object.
|
| -
|
| - Member functions of the object can't be called unless they are immutable too.
|
| - Properties can be looked up, this function assumes properties do not mutate
|
| - the object.
|
| -
|
| - Note: a user can still call classmethod and do mutation on the class, or they
|
| - can lookup a member object and mutate this one. Don't be silly.
|
| - """
|
| - class Immutable(object):
|
| - def __init__(self, obj):
|
| - object.__setattr__(self, '__ref', obj)
|
| -
|
| - def __getattribute__(self, name):
|
| - ref = object.__getattribute__(self, '__ref')
|
| - value = getattr(ref, name)
|
| - if not callable(value):
|
| - return value
|
| - if getattr(value, 'is_immutable', None):
|
| - # It is immutable too.
|
| - return value
|
| - if getattr(value, 'im_self', None) == None:
|
| - # It is static.
|
| - return value
|
| - raise TypeError(
|
| - 'Can\'t call mutable member function \'%s\' on an immutable '
|
| - 'instance of %s' % (name, ref.__class__.__name__))
|
| -
|
| - def __setattr__(self, name, _value):
|
| - ref = object.__getattribute__(self, '__ref')
|
| - raise TypeError(
|
| - 'Can\'t change attribute \'%s\' on an immutable instance of \'%s\'' %
|
| - (name, ref.__class__.__name__))
|
| -
|
| - def __delattr__(self, name):
|
| - ref = object.__getattribute__(self, '__ref')
|
| - raise TypeError(
|
| - 'Can\'t delete attribute \'%s\' on an immutable instance of \'%s\'' %
|
| - (name, ref.__class__.__name__))
|
| -
|
| - def hook(self, *args, **kwargs):
|
| - return func(Immutable(self), *args, **kwargs)
|
| -
|
| - hook.is_immutable = True
|
| - return hook
|
| -
|
| -
|
| -def load_from_json_file(filename):
|
| - """Loads one object from a JSON file."""
|
| - with open(filename, 'r') as f:
|
| - return PersistentMixIn.from_dict(json.load(f))
|
| -
|
| -
|
| -def save_to_json_file(filename, obj):
|
| - """Save one object in a JSON file."""
|
| - try:
|
| - old = filename + '.old'
|
| - if os.path.exists(filename):
|
| - os.rename(filename, old)
|
| - finally:
|
| - with open(filename, 'wb') as f:
|
| - json.dump(obj.as_dict(), f, sort_keys=True, indent=2)
|
| - f.write('\n')
|
|
|