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

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: More doc, minor fixes Created 8 years, 1 month 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') | verification/tree_status.py » ('J')
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 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
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()
OLDNEW
« no previous file with comments | « no previous file | pending_manager.py » ('j') | verification/tree_status.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698