Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 """Defines a utility class to easily convert classes from and to dict for | 4 |
| 5 serialization. | 5 """Defines the PersistentMixIn utility class to easily convert classes from and |
| 6 to dict for serialization. | |
|
Peter Mayo
2012/11/23 18:33:39
"to and from" is usual phrase, unless you mean to
M-A Ruel
2012/11/23 18:44:06
done
| |
| 7 | |
| 8 This class is aimed at json-compatible serialization, so it supports the limited | |
| 9 set of structures supported by json; strings, numbers as int or float, list and | |
| 10 dictionaries. | |
| 11 | |
| 12 PersistentMixIn._persistent_members() returns a dict of each member with the | |
| 13 tuple of expected types. Each member can be decoded in multiple types, for | |
| 14 example, a subversion revision number could have (None, int, str), meaning that | |
| 15 the revision could be None, when not known, an int or the int as a string | |
| 16 representation. The tuple is listed in the prefered order of conversions. | |
| 17 | |
| 18 Composites types that cannot be represented exactly in json like tuple, set and | |
| 19 frozenset are converted from and back to list automatically. Any class instance | |
| 20 that has been serialized can be unserialized in the same class instance or into | |
| 21 a bare dict. | |
| 22 | |
| 23 See tests/model_tests.py for examples. | |
| 6 """ | 24 """ |
| 7 | 25 |
| 8 import json | 26 import json |
| 9 import sys | 27 import logging |
| 10 import os | 28 import os |
| 11 | 29 |
| 30 # Set in the output dict to be able to know which class was serialized to help | |
| 31 # deserialization. | |
| 32 TYPE_FLAG = '__persistent_type__' | |
| 12 | 33 |
| 13 TYPE_FLAG = '__persistent_type__' | 34 # Marker to tell the deserializer that we don't know the expected type, used in |
| 14 MODULE_FLAG = '__persistent_module__' | 35 # composite types. |
| 36 _UNKNOWN = object() | |
| 15 | 37 |
| 16 | 38 |
| 17 def as_dict(value): | 39 def as_dict(value): |
| 18 """Recursively converts an object into a dictionary. | 40 """Recursively converts an object into a dictionary. |
| 19 | 41 |
| 20 Converts tuple into list and recursively process each items. | 42 Converts tuple,set,frozenset into list and recursively process each items. |
| 21 """ | 43 """ |
| 22 if hasattr(value, 'as_dict') and callable(value.as_dict): | 44 if hasattr(value, 'as_dict') and callable(value.as_dict): |
| 23 return value.as_dict() | 45 return value.as_dict() |
| 24 elif isinstance(value, (list, tuple)): | 46 elif isinstance(value, (list, tuple, set, frozenset)): |
| 25 return [as_dict(v) for v in value] | 47 return [as_dict(v) for v in value] |
| 26 elif isinstance(value, dict): | 48 elif isinstance(value, dict): |
| 27 return dict((as_dict(k), as_dict(v)) | 49 return dict((as_dict(k), as_dict(v)) |
| 28 for k, v in value.iteritems()) | 50 for k, v in value.iteritems()) |
| 29 elif isinstance(value, (float, int, basestring)) or value is None: | 51 elif isinstance(value, (float, int, basestring)) or value is None: |
| 30 return value | 52 return value |
| 31 else: | 53 else: |
| 32 raise AttributeError('Can\'t type %s into a dictionary' % type(value)) | 54 raise AttributeError('Can\'t type %s into a dictionary' % type(value)) |
| 33 | 55 |
| 34 | 56 |
| 35 def _inner_from_dict(value): | 57 def _inner_from_dict(name, value, member_types): |
| 36 """Recursively regenerates an object.""" | 58 """Recursively regenerates an object. |
| 37 if isinstance(value, dict): | 59 |
| 38 if TYPE_FLAG in value: | 60 For each of the allowable types, try to convert it. If None is an allowable |
| 39 return PersistentMixIn.from_dict(value) | 61 type, any data that can't be parsed will be parsed as None and will be |
| 40 return dict((_inner_from_dict(k), _inner_from_dict(v)) | 62 silently discarded. Otherwise, an exception will be raise. |
| 41 for k, v in value.iteritems()) | 63 """ |
| 42 elif isinstance(value, list): | 64 logging.debug('_inner_from_dict(%s, %r, %s)', name, value, member_types) |
| 43 return [_inner_from_dict(v) for v in value] | 65 result = None |
| 44 elif isinstance(value, (float, int, basestring)) or value is None: | 66 if member_types is _UNKNOWN: |
| 45 return value | 67 # Use guesswork a bit more and accept anything. |
| 68 if isinstance(value, dict) and TYPE_FLAG in value: | |
| 69 result = PersistentMixIn.from_dict(value, _UNKNOWN) | |
| 70 elif isinstance(value, list): | |
| 71 # All of these are serialized to list. | |
| 72 result = [_inner_from_dict(None, v, _UNKNOWN) for v in value] | |
| 73 elif isinstance(value, (float, int, basestring)): | |
| 74 result = value | |
| 75 else: | |
| 76 raise TypeError('No idea how to convert %r' % value) | |
| 46 else: | 77 else: |
| 47 raise AttributeError('Can\'t load type %s' % type(value)) | 78 for member_type in member_types: |
| 79 # Explicitly leave None out of this loop. | |
| 80 if issubclass(member_type, PersistentMixIn): | |
|
csharp
2012/11/23 18:31:58
Link all these high level "if member_type somethin
M-A Ruel
2012/11/23 18:44:06
done and used elif to make it clearer.
| |
| 81 if isinstance(value, dict) and TYPE_FLAG in value: | |
| 82 result = PersistentMixIn.from_dict(value, member_type) | |
| 83 break | |
| 84 | |
| 85 if member_type is dict: | |
| 86 if isinstance(value, dict): | |
| 87 result = dict( | |
| 88 (_inner_from_dict(None, k, _UNKNOWN), | |
| 89 _inner_from_dict(None, v, _UNKNOWN)) | |
| 90 for k, v in value.iteritems()) | |
| 91 break | |
| 92 | |
| 93 if member_type in (list, tuple, set, frozenset): | |
| 94 # All of these are serialized to list. | |
| 95 if isinstance(value, list): | |
| 96 result = member_type( | |
| 97 _inner_from_dict(None, v, _UNKNOWN) for v in value) | |
| 98 break | |
| 99 | |
| 100 if member_type in (float, int, str, unicode): | |
| 101 if isinstance(value, member_type): | |
| 102 result = member_type(value) | |
| 103 break | |
| 104 else: | |
| 105 logging.info( | |
| 106 'Ignored data %r; didn\'t fit types %s', | |
| 107 value, | |
| 108 ', '.join(i.__name__ for i in member_types)) | |
| 109 _check_type_value(name, result, member_types) | |
| 110 return result | |
| 48 | 111 |
| 49 | 112 |
| 50 def to_yaml(obj): | 113 def to_yaml(obj): |
| 51 """Converts a PersisntetMixIn into a yaml-inspired format.""" | 114 """Converts a PersistentMixIn into a yaml-inspired format. |
| 115 | |
| 116 Warning: Not unit tested, use at your own risk! | |
|
csharp
2012/11/23 18:31:58
Why not :)
M-A Ruel
2012/11/23 18:44:06
I only use it for debug output. If it becomes need
| |
| 117 """ | |
| 52 def align(x): | 118 def align(x): |
| 53 y = x.splitlines(True) | 119 y = x.splitlines(True) |
| 54 if len(y) > 1: | 120 if len(y) > 1: |
| 55 return ''.join(y[0:1] + [' ' + z for z in y[1:]]) | 121 return ''.join(y[0:1] + [' ' + z for z in y[1:]]) |
| 56 return x | 122 return x |
| 57 def align_value(x): | 123 def align_value(x): |
| 58 if '\n' in x: | 124 if '\n' in x: |
| 59 return '\n ' + align(x) | 125 return '\n ' + align(x) |
| 60 return x | 126 return x |
| 61 | 127 |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 76 if not r: | 142 if not r: |
| 77 continue | 143 continue |
| 78 out.append('- %s: %s' % (k, r)) | 144 out.append('- %s: %s' % (k, r)) |
| 79 elif hasattr(obj, '__iter__') and callable(obj.__iter__): | 145 elif hasattr(obj, '__iter__') and callable(obj.__iter__): |
| 80 out = ['- %s' % align(to_yaml(x)) for x in obj] | 146 out = ['- %s' % align(to_yaml(x)) for x in obj] |
| 81 else: | 147 else: |
| 82 out = ('%s' % obj.__class__.__name__,) | 148 out = ('%s' % obj.__class__.__name__,) |
| 83 return '\n'.join(out) | 149 return '\n'.join(out) |
| 84 | 150 |
| 85 | 151 |
| 152 def _default_value(member_types): | |
| 153 """Returns an instance of the first allowed type. Special case None.""" | |
| 154 if member_types[0] is None.__class__: | |
| 155 return None | |
| 156 else: | |
| 157 return member_types[0]() | |
| 158 | |
| 159 | |
| 160 def _check_type_value(name, value, member_types): | |
| 161 """Raises a TypeError exception if value is not one of the allowed types in | |
| 162 member_types. | |
| 163 """ | |
| 164 if not isinstance(value, member_types): | |
| 165 prefix = '%s e' % name if name else 'E' | |
| 166 raise TypeError( | |
| 167 '%sxpected type(s) %s; got %r' % | |
| 168 (prefix, ', '.join(i.__name__ for i in member_types), value)) | |
| 169 | |
| 170 | |
| 171 | |
| 86 class PersistentMixIn(object): | 172 class PersistentMixIn(object): |
| 87 """Class to be used as a base class to persistent data in a simplistic way. | 173 """Class to be used as a base class to persistent data in a simplistic way. |
| 88 | 174 |
| 89 persistent class member needs to be set to a tuple containing the instance | 175 Persistent class member needs to be set to a tuple containing the instance |
| 90 member variable that needs to be saved or loaded. | 176 member variable that needs to be saved or loaded. The first item will be |
| 177 default value, e.g.: | |
| 178 foo = (None, str, dict) | |
| 179 Will default initialize self.foo to None. | |
| 180 """ | |
| 181 # Cache of all the subclasses of PersistentMixIn. | |
| 182 __persistent_classes_cache = None | |
| 91 | 183 |
| 92 TODO(maruel): Use __reduce__! | 184 def __init__(self, **kwargs): |
| 93 """ | 185 """Initializes with the default members.""" |
| 94 persistent = None | 186 super(PersistentMixIn, self).__init__() |
| 187 persistent_members = self._persistent_members() | |
| 188 for member, member_types in persistent_members.iteritems(): | |
| 189 if member in kwargs: | |
| 190 value = kwargs.pop(member) | |
| 191 else: | |
| 192 value = _default_value(member_types) | |
| 193 _check_type_value(member, value, member_types) | |
| 194 setattr(self, member, value) | |
| 195 if kwargs: | |
| 196 raise AttributeError('Received unexpected initializers: %s' % kwargs) | |
| 95 | 197 |
| 96 def __new__(cls, *args, **kwargs): | 198 @classmethod |
| 97 """Override __new__() to be able to instantiate derived classes without | 199 def _persistent_members(cls): |
| 98 calling their __init__() function. This is useful when objects are created | 200 """Returns the persistent items as a dict. |
| 99 from a dict. | 201 |
| 202 Each entry value can be a tuple when the member can be assigned different | |
| 203 types. | |
| 100 """ | 204 """ |
| 101 result = super(PersistentMixIn, cls).__new__(cls) | 205 # Note that here, cls is the subclass, not PersistentMixIn. |
| 102 if args or kwargs: | 206 # TODO(maruel): Cache the results. It's tricky because setting |
| 103 result.__init__(*args, **kwargs) | 207 # cls.__persisten_members_cache on a class will implicitly set it on its |
|
Peter Mayo
2012/11/23 18:33:39
__persisten*t*_members_cache
M-A Ruel
2012/11/23 18:44:06
done
| |
| 104 return result | 208 # subclass. So in a class hierarchy with A -> B -> PersistentMixIn, calling |
| 209 # B()._persistent_members() will incorrectly set the cache for A. | |
| 210 persistent_members_cache = {} | |
| 211 # Enumerate on the subclass, not on an instance. | |
| 212 for item in dir(cls): | |
| 213 if item.startswith('_'): | |
| 214 continue | |
| 215 item_value = getattr(cls, item) | |
| 216 if isinstance(item_value, type): | |
| 217 item_value = (item_value,) | |
| 218 if not isinstance(item_value, tuple): | |
| 219 continue | |
| 220 if not all(i is None or i.__class__ == type for i in item_value): | |
| 221 continue | |
| 222 item_value = tuple( | |
| 223 f if f is not None else None.__class__ for f in item_value) | |
| 224 persistent_members_cache[item] = item_value | |
| 225 return persistent_members_cache | |
| 226 | |
| 227 @staticmethod | |
| 228 def _get_subclass(typename): | |
| 229 """Returns the PersistentMixIn subclass with the name |typename|.""" | |
| 230 subclass = None | |
| 231 if PersistentMixIn.__persistent_classes_cache is not None: | |
| 232 subclass = PersistentMixIn.__persistent_classes_cache.get(typename) | |
| 233 if not subclass: | |
| 234 # Get the subclasses recursively. | |
| 235 PersistentMixIn.__persistent_classes_cache = {} | |
| 236 def recurse(c): | |
| 237 for s in c.__subclasses__(): | |
| 238 assert s.__name__ not in PersistentMixIn.__persistent_classes_cache | |
| 239 PersistentMixIn.__persistent_classes_cache[s.__name__] = s | |
| 240 recurse(s) | |
| 241 recurse(PersistentMixIn) | |
| 242 | |
| 243 subclass = PersistentMixIn.__persistent_classes_cache.get(typename) | |
| 244 if not subclass: | |
| 245 raise KeyError('Couldn\'t find type %s' % typename) | |
| 246 return subclass | |
| 105 | 247 |
| 106 def as_dict(self): | 248 def as_dict(self): |
| 107 """Create a dictionary out of this object.""" | 249 """Create a dictionary out of this object, e.g. Serialize the object.""" |
|
Peter Mayo
2012/11/23 18:33:39
e.g. -> i.e. ?
M-A Ruel
2012/11/23 18:44:06
Right, done
| |
| 108 assert isinstance(self.persistent, (list, tuple)) | |
| 109 out = {} | 250 out = {} |
| 110 for member in self.persistent: | 251 for member, member_types in self._persistent_members().iteritems(): |
| 111 assert isinstance(member, str) | 252 value = getattr(self, member) |
| 112 out[member] = as_dict(getattr(self, member)) | 253 _check_type_value(member, value, member_types) |
| 254 out[member] = as_dict(value) | |
| 113 out[TYPE_FLAG] = self.__class__.__name__ | 255 out[TYPE_FLAG] = self.__class__.__name__ |
| 114 out[MODULE_FLAG] = self.__class__.__module__ | |
| 115 return out | 256 return out |
| 116 | 257 |
| 117 @staticmethod | 258 @staticmethod |
| 118 def from_dict(data): | 259 def from_dict(data, subclass=_UNKNOWN): |
| 119 """Returns an instance of a class inheriting from PersistentMixIn, | 260 """Returns an instance of a class inheriting from PersistentMixIn, |
| 120 initialized with 'data' dict.""" | 261 initialized with 'data' dict, e.g. Deserialize the object. |
| 121 datatype = data[TYPE_FLAG] | 262 """ |
| 122 if MODULE_FLAG in data and data[MODULE_FLAG] in sys.modules: | 263 logging.debug('from_dict(%r, %s)', data, subclass) |
| 123 objtype = getattr(sys.modules[data[MODULE_FLAG]], datatype) | 264 if subclass is _UNKNOWN: |
| 124 else: | 265 subclass = PersistentMixIn._get_subclass(data[TYPE_FLAG]) |
| 125 # Fallback to search for the type in the loaded modules. | 266 # This initializes the instance with the default values. |
| 126 for module in sys.modules.itervalues(): | 267 obj = subclass() |
| 127 objtype = getattr(module, datatype, None) | 268 assert isinstance(obj, PersistentMixIn) and obj.__class__ != PersistentMixIn |
| 128 if objtype: | 269 # pylint: disable=W0212 |
| 129 break | 270 for member, member_types in obj._persistent_members().iteritems(): |
| 271 if member in data: | |
| 272 value = _inner_from_dict(member, data[member], member_types) | |
| 130 else: | 273 else: |
| 131 raise KeyError('Couldn\'t find type %s' % datatype) | 274 value = _default_value(member_types) |
| 132 obj = PersistentMixIn.__new__(objtype) | 275 _check_type_value(member, value, member_types) |
| 133 assert isinstance(obj, PersistentMixIn) | 276 setattr(obj, member, value) |
| 134 for member in obj.persistent: | |
| 135 setattr(obj, member, _inner_from_dict(data.get(member, None))) | |
| 136 return obj | 277 return obj |
| 137 | 278 |
| 138 def __str__(self): | 279 def __str__(self): |
| 139 return to_yaml(self) | 280 return to_yaml(self) |
| 140 | 281 |
| 141 | 282 |
| 142 def load_from_json_file(filename): | 283 def load_from_json_file(filename): |
| 143 """Loads one object from a JSON file.""" | 284 """Loads one object from a JSON file.""" |
| 144 try: | 285 try: |
| 145 f = open(filename, 'r') | 286 f = open(filename, 'r') |
| 146 return PersistentMixIn.from_dict(json.load(f)) | 287 return PersistentMixIn.from_dict(json.load(f)) |
| 147 finally: | 288 finally: |
| 148 f.close() | 289 f.close() |
| 149 | 290 |
| 150 | 291 |
| 151 def save_to_json_file(filename, obj): | 292 def save_to_json_file(filename, obj): |
| 152 """Save one object in a JSON file.""" | 293 """Save one object in a JSON file.""" |
| 153 try: | 294 try: |
| 154 old = filename + '.old' | 295 old = filename + '.old' |
| 155 if os.path.exists(filename): | 296 if os.path.exists(filename): |
| 156 os.rename(filename, old) | 297 os.rename(filename, old) |
| 157 finally: | 298 finally: |
| 158 try: | 299 try: |
| 159 f = open(filename, 'w') | 300 f = open(filename, 'w') |
| 160 json.dump(obj.as_dict(), f, sort_keys=True, indent=2) | 301 json.dump(obj.as_dict(), f, sort_keys=True, indent=2) |
| 161 f.write('\n') | 302 f.write('\n') |
| 162 finally: | 303 finally: |
| 163 f.close() | 304 f.close() |
| OLD | NEW |