Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(571)

Side by Side Diff: model.py

Issue 11414143: Change models.py to use typed class members instead of a list of strings. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/commit-queue
Patch Set: Address review comments Created 8 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | pending_manager.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 to and
6 from dict for serialization.
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):
81 if isinstance(value, dict) and TYPE_FLAG in value:
82 result = PersistentMixIn.from_dict(value, member_type)
83 break
84 elif member_type is dict:
85 if isinstance(value, dict):
86 result = dict(
87 (_inner_from_dict(None, k, _UNKNOWN),
88 _inner_from_dict(None, v, _UNKNOWN))
89 for k, v in value.iteritems())
90 break
91 elif member_type in (list, tuple, set, frozenset):
92 # All of these are serialized to list.
93 if isinstance(value, list):
94 result = member_type(
95 _inner_from_dict(None, v, _UNKNOWN) for v in value)
96 break
97 elif member_type in (float, int, str, unicode):
98 if isinstance(value, member_type):
99 result = member_type(value)
100 break
101 else:
102 logging.info(
103 'Ignored data %r; didn\'t fit types %s',
104 value,
105 ', '.join(i.__name__ for i in member_types))
106 _check_type_value(name, result, member_types)
107 return result
48 108
49 109
50 def to_yaml(obj): 110 def to_yaml(obj):
51 """Converts a PersisntetMixIn into a yaml-inspired format.""" 111 """Converts a PersistentMixIn into a yaml-inspired format.
112
113 Warning: Not unit tested, use at your own risk!
114 """
52 def align(x): 115 def align(x):
53 y = x.splitlines(True) 116 y = x.splitlines(True)
54 if len(y) > 1: 117 if len(y) > 1:
55 return ''.join(y[0:1] + [' ' + z for z in y[1:]]) 118 return ''.join(y[0:1] + [' ' + z for z in y[1:]])
56 return x 119 return x
57 def align_value(x): 120 def align_value(x):
58 if '\n' in x: 121 if '\n' in x:
59 return '\n ' + align(x) 122 return '\n ' + align(x)
60 return x 123 return x
61 124
(...skipping 14 matching lines...) Expand all
76 if not r: 139 if not r:
77 continue 140 continue
78 out.append('- %s: %s' % (k, r)) 141 out.append('- %s: %s' % (k, r))
79 elif hasattr(obj, '__iter__') and callable(obj.__iter__): 142 elif hasattr(obj, '__iter__') and callable(obj.__iter__):
80 out = ['- %s' % align(to_yaml(x)) for x in obj] 143 out = ['- %s' % align(to_yaml(x)) for x in obj]
81 else: 144 else:
82 out = ('%s' % obj.__class__.__name__,) 145 out = ('%s' % obj.__class__.__name__,)
83 return '\n'.join(out) 146 return '\n'.join(out)
84 147
85 148
149 def _default_value(member_types):
150 """Returns an instance of the first allowed type. Special case None."""
151 if member_types[0] is None.__class__:
152 return None
153 else:
154 return member_types[0]()
155
156
157 def _check_type_value(name, value, member_types):
158 """Raises a TypeError exception if value is not one of the allowed types in
159 member_types.
160 """
161 if not isinstance(value, member_types):
162 prefix = '%s e' % name if name else 'E'
163 raise TypeError(
164 '%sxpected type(s) %s; got %r' %
165 (prefix, ', '.join(i.__name__ for i in member_types), value))
166
167
168
86 class PersistentMixIn(object): 169 class PersistentMixIn(object):
87 """Class to be used as a base class to persistent data in a simplistic way. 170 """Class to be used as a base class to persistent data in a simplistic way.
88 171
89 persistent class member needs to be set to a tuple containing the instance 172 Persistent class member needs to be set to a tuple containing the instance
90 member variable that needs to be saved or loaded. 173 member variable that needs to be saved or loaded. The first item will be
174 default value, e.g.:
175 foo = (None, str, dict)
176 Will default initialize self.foo to None.
177 """
178 # Cache of all the subclasses of PersistentMixIn.
179 __persistent_classes_cache = None
91 180
92 TODO(maruel): Use __reduce__! 181 def __init__(self, **kwargs):
93 """ 182 """Initializes with the default members."""
94 persistent = None 183 super(PersistentMixIn, self).__init__()
184 persistent_members = self._persistent_members()
185 for member, member_types in persistent_members.iteritems():
186 if member in kwargs:
187 value = kwargs.pop(member)
188 else:
189 value = _default_value(member_types)
190 _check_type_value(member, value, member_types)
191 setattr(self, member, value)
192 if kwargs:
193 raise AttributeError('Received unexpected initializers: %s' % kwargs)
95 194
96 def __new__(cls, *args, **kwargs): 195 @classmethod
97 """Override __new__() to be able to instantiate derived classes without 196 def _persistent_members(cls):
98 calling their __init__() function. This is useful when objects are created 197 """Returns the persistent items as a dict.
99 from a dict. 198
199 Each entry value can be a tuple when the member can be assigned different
200 types.
100 """ 201 """
101 result = super(PersistentMixIn, cls).__new__(cls) 202 # Note that here, cls is the subclass, not PersistentMixIn.
102 if args or kwargs: 203 # TODO(maruel): Cache the results. It's tricky because setting
103 result.__init__(*args, **kwargs) 204 # cls.__persistent_members_cache on a class will implicitly set it on its
104 return result 205 # subclass. So in a class hierarchy with A -> B -> PersistentMixIn, calling
206 # B()._persistent_members() will incorrectly set the cache for A.
207 persistent_members_cache = {}
208 # Enumerate on the subclass, not on an instance.
209 for item in dir(cls):
210 if item.startswith('_'):
211 continue
212 item_value = getattr(cls, item)
213 if isinstance(item_value, type):
214 item_value = (item_value,)
215 if not isinstance(item_value, tuple):
216 continue
217 if not all(i is None or i.__class__ == type for i in item_value):
218 continue
219 item_value = tuple(
220 f if f is not None else None.__class__ for f in item_value)
221 persistent_members_cache[item] = item_value
222 return persistent_members_cache
223
224 @staticmethod
225 def _get_subclass(typename):
226 """Returns the PersistentMixIn subclass with the name |typename|."""
227 subclass = None
228 if PersistentMixIn.__persistent_classes_cache is not None:
229 subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
230 if not subclass:
231 # Get the subclasses recursively.
232 PersistentMixIn.__persistent_classes_cache = {}
233 def recurse(c):
234 for s in c.__subclasses__():
235 assert s.__name__ not in PersistentMixIn.__persistent_classes_cache
236 PersistentMixIn.__persistent_classes_cache[s.__name__] = s
237 recurse(s)
238 recurse(PersistentMixIn)
239
240 subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
241 if not subclass:
242 raise KeyError('Couldn\'t find type %s' % typename)
243 return subclass
105 244
106 def as_dict(self): 245 def as_dict(self):
107 """Create a dictionary out of this object.""" 246 """Create a dictionary out of this object, i.e. Serialize the object."""
108 assert isinstance(self.persistent, (list, tuple))
109 out = {} 247 out = {}
110 for member in self.persistent: 248 for member, member_types in self._persistent_members().iteritems():
111 assert isinstance(member, str) 249 value = getattr(self, member)
112 out[member] = as_dict(getattr(self, member)) 250 _check_type_value(member, value, member_types)
251 out[member] = as_dict(value)
113 out[TYPE_FLAG] = self.__class__.__name__ 252 out[TYPE_FLAG] = self.__class__.__name__
114 out[MODULE_FLAG] = self.__class__.__module__
115 return out 253 return out
116 254
117 @staticmethod 255 @staticmethod
118 def from_dict(data): 256 def from_dict(data, subclass=_UNKNOWN):
119 """Returns an instance of a class inheriting from PersistentMixIn, 257 """Returns an instance of a class inheriting from PersistentMixIn,
120 initialized with 'data' dict.""" 258 initialized with 'data' dict, i.e. Deserialize the object.
121 datatype = data[TYPE_FLAG] 259 """
122 if MODULE_FLAG in data and data[MODULE_FLAG] in sys.modules: 260 logging.debug('from_dict(%r, %s)', data, subclass)
123 objtype = getattr(sys.modules[data[MODULE_FLAG]], datatype) 261 if subclass is _UNKNOWN:
124 else: 262 subclass = PersistentMixIn._get_subclass(data[TYPE_FLAG])
125 # Fallback to search for the type in the loaded modules. 263 # This initializes the instance with the default values.
126 for module in sys.modules.itervalues(): 264 obj = subclass()
127 objtype = getattr(module, datatype, None) 265 assert isinstance(obj, PersistentMixIn) and obj.__class__ != PersistentMixIn
128 if objtype: 266 # pylint: disable=W0212
129 break 267 for member, member_types in obj._persistent_members().iteritems():
268 if member in data:
269 value = _inner_from_dict(member, data[member], member_types)
130 else: 270 else:
131 raise KeyError('Couldn\'t find type %s' % datatype) 271 value = _default_value(member_types)
132 obj = PersistentMixIn.__new__(objtype) 272 _check_type_value(member, value, member_types)
133 assert isinstance(obj, PersistentMixIn) 273 setattr(obj, member, value)
134 for member in obj.persistent:
135 setattr(obj, member, _inner_from_dict(data.get(member, None)))
136 return obj 274 return obj
137 275
138 def __str__(self): 276 def __str__(self):
139 return to_yaml(self) 277 return to_yaml(self)
140 278
141 279
142 def load_from_json_file(filename): 280 def load_from_json_file(filename):
143 """Loads one object from a JSON file.""" 281 """Loads one object from a JSON file."""
144 try: 282 try:
145 f = open(filename, 'r') 283 f = open(filename, 'r')
146 return PersistentMixIn.from_dict(json.load(f)) 284 return PersistentMixIn.from_dict(json.load(f))
147 finally: 285 finally:
148 f.close() 286 f.close()
149 287
150 288
151 def save_to_json_file(filename, obj): 289 def save_to_json_file(filename, obj):
152 """Save one object in a JSON file.""" 290 """Save one object in a JSON file."""
153 try: 291 try:
154 old = filename + '.old' 292 old = filename + '.old'
155 if os.path.exists(filename): 293 if os.path.exists(filename):
156 os.rename(filename, old) 294 os.rename(filename, old)
157 finally: 295 finally:
158 try: 296 try:
159 f = open(filename, 'w') 297 f = open(filename, 'w')
160 json.dump(obj.as_dict(), f, sort_keys=True, indent=2) 298 json.dump(obj.as_dict(), f, sort_keys=True, indent=2)
161 f.write('\n') 299 f.write('\n')
162 finally: 300 finally:
163 f.close() 301 f.close()
OLDNEW
« no previous file with comments | « no previous file | pending_manager.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698