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') |