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

Side by Side Diff: commit-queue/model.py

Issue 135363007: Delete public commit queue to avoid confusion after move to internal repo (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/
Patch Set: Created 6 years, 10 months 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 | « commit-queue/loop.sh ('k') | commit-queue/natsort.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
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
3 # found in the LICENSE file.
4
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.
24 """
25
26 import json
27 import logging
28 import os
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__'
33
34 # Marker to tell the deserializer that we don't know the expected type, used in
35 # composite types.
36 _UNKNOWN = object()
37
38
39 def as_dict(value):
40 """Recursively converts an object into a dictionary.
41
42 Converts tuple,set,frozenset into list and recursively process each items.
43 """
44 if hasattr(value, 'as_dict') and callable(value.as_dict):
45 return value.as_dict()
46 elif isinstance(value, (list, tuple, set, frozenset)):
47 return [as_dict(v) for v in value]
48 elif isinstance(value, dict):
49 return dict((as_dict(k), as_dict(v))
50 for k, v in value.iteritems())
51 elif isinstance(value, (bool, float, int, basestring)) or value is None:
52 return value
53 else:
54 raise AttributeError('Can\'t type %s into a dictionary' % type(value))
55
56
57 def _inner_from_dict(name, value, member_types):
58 """Recursively regenerates an object.
59
60 For each of the allowable types, try to convert it. If None is an allowable
61 type, any data that can't be parsed will be parsed as None and will be
62 silently discarded. Otherwise, an exception will be raise.
63 """
64 logging.debug('_inner_from_dict(%s, %r, %s)', name, value, member_types)
65 result = None
66 if member_types is _UNKNOWN:
67 # Use guesswork a bit more and accept anything.
68 if isinstance(value, dict):
69 if TYPE_FLAG in value:
70 result = PersistentMixIn.from_dict(value, _UNKNOWN)
71 else:
72 # Unserialize it as a raw dict.
73 result = dict(
74 (_inner_from_dict(None, k, _UNKNOWN),
75 _inner_from_dict(None, v, _UNKNOWN))
76 for k, v in value.iteritems())
77 elif isinstance(value, list):
78 # All of these are serialized to list.
79 result = [_inner_from_dict(None, v, _UNKNOWN) for v in value]
80 elif isinstance(value, (bool, float, int, unicode)):
81 result = value
82 else:
83 raise TypeError('No idea how to convert %r' % value)
84 else:
85 for member_type in member_types:
86 # Explicitly leave None out of this loop.
87 if issubclass(member_type, PersistentMixIn):
88 if isinstance(value, dict) and TYPE_FLAG in value:
89 result = PersistentMixIn.from_dict(value, member_type)
90 break
91 elif member_type is dict:
92 if isinstance(value, dict):
93 result = dict(
94 (_inner_from_dict(None, k, _UNKNOWN),
95 _inner_from_dict(None, v, _UNKNOWN))
96 for k, v in value.iteritems())
97 break
98 elif member_type in (list, tuple, set, frozenset):
99 # All of these are serialized to list.
100 if isinstance(value, list):
101 result = member_type(
102 _inner_from_dict(None, v, _UNKNOWN) for v in value)
103 break
104 elif member_type in (bool, float, int, str, unicode):
105 if isinstance(value, member_type):
106 result = member_type(value)
107 break
108 elif member_type is None.__class__ and value is None:
109 result = None
110 break
111 else:
112 logging.info(
113 'Ignored %s: didn\'t fit types %s; %s',
114 name,
115 ', '.join(i.__name__ for i in member_types),
116 repr(value)[:200])
117 _check_type_value(name, result, member_types)
118 return result
119
120
121 def to_yaml(obj):
122 """Converts a PersistentMixIn into a yaml-inspired format.
123
124 Warning: Not unit tested, use at your own risk!
125 """
126 def align(x):
127 y = x.splitlines(True)
128 if len(y) > 1:
129 return ''.join(y[0:1] + [' ' + z for z in y[1:]])
130 return x
131 def align_value(x):
132 if '\n' in x:
133 return '\n ' + align(x)
134 return x
135
136 if hasattr(obj, 'as_dict') and callable(obj.as_dict):
137 out = (to_yaml(obj.as_dict()),)
138 elif isinstance(obj, (bool, float, int, unicode)) or obj is None:
139 out = (align(str(obj)),)
140 elif isinstance(obj, dict):
141 if TYPE_FLAG in obj:
142 out = ['%s:' % obj[TYPE_FLAG]]
143 else:
144 out = []
145 for k, v in obj.iteritems():
146 # Skips many members resolving to bool() == False
147 if k.startswith('__') or v in (None, '', False, 0):
148 continue
149 r = align_value(to_yaml(v))
150 if not r:
151 continue
152 out.append('- %s: %s' % (k, r))
153 elif hasattr(obj, '__iter__') and callable(obj.__iter__):
154 out = ['- %s' % align(to_yaml(x)) for x in obj]
155 else:
156 out = ('%s' % obj.__class__.__name__,)
157 return '\n'.join(out)
158
159
160 def _default_value(member_types):
161 """Returns an instance of the first allowed type. Special case None."""
162 if member_types[0] is None.__class__:
163 return None
164 else:
165 return member_types[0]()
166
167
168 def _check_type_value(name, value, member_types):
169 """Raises a TypeError exception if value is not one of the allowed types in
170 member_types.
171 """
172 if not isinstance(value, member_types):
173 prefix = '%s e' % name if name else 'E'
174 raise TypeError(
175 '%sxpected type(s) %s; got %r' %
176 (prefix, ', '.join(i.__name__ for i in member_types), value))
177
178
179
180 class PersistentMixIn(object):
181 """Class to be used as a base class to persistent data in a simplistic way.
182
183 Persistent class member needs to be set to a tuple containing the instance
184 member variable that needs to be saved or loaded. The first item will be
185 default value, e.g.:
186 foo = (None, str, dict)
187 Will default initialize self.foo to None.
188 """
189 # Cache of all the subclasses of PersistentMixIn.
190 __persistent_classes_cache = None
191
192 _read_only = False
193
194 def __init__(self, **kwargs):
195 """Initializes with the default members."""
196 super(PersistentMixIn, self).__init__()
197 persistent_members = self._persistent_members()
198 for member, member_types in persistent_members.iteritems():
199 if member in kwargs:
200 value = kwargs.pop(member)
201 if isinstance(value, str):
202 # Assume UTF-8 all the time. Note: This is explicitly when the object
203 # is constructed in the code. This code path is never used when
204 # deserializing the object.
205 value = value.decode('utf-8')
206 else:
207 value = _default_value(member_types)
208 _check_type_value(member, value, member_types)
209 setattr(self, member, value)
210 if kwargs:
211 raise AttributeError('Received unexpected initializers: %s' % kwargs)
212
213 def __setattr__(self, name, value):
214 """Enforces immutability if cls._read_only is True."""
215 if self._read_only:
216 raise TypeError()
217 return super(PersistentMixIn, self).__setattr__(name, value)
218
219 @classmethod
220 def _persistent_members(cls):
221 """Returns the persistent items as a dict.
222
223 Each entry value can be a tuple when the member can be assigned different
224 types.
225 """
226 # Note that here, cls is the subclass, not PersistentMixIn.
227 # TODO(maruel): Cache the results. It's tricky because setting
228 # cls.__persistent_members_cache on a class will implicitly set it on its
229 # subclass. So in a class hierarchy with A -> B -> PersistentMixIn, calling
230 # B()._persistent_members() will incorrectly set the cache for A.
231 persistent_members_cache = {}
232 # Enumerate on the subclass, not on an instance.
233 for item in dir(cls):
234 if item.startswith('_'):
235 continue
236 item_value = getattr(cls, item)
237 if isinstance(item_value, type):
238 item_value = (item_value,)
239 if not isinstance(item_value, tuple):
240 continue
241 if not all(i is None or i.__class__ == type for i in item_value):
242 continue
243 if any(i is str for i in item_value):
244 raise TypeError(
245 '%s is type \'str\' which is currently not supported' % item)
246 item_value = tuple(
247 f if f is not None else None.__class__ for f in item_value)
248 persistent_members_cache[item] = item_value
249 return persistent_members_cache
250
251 @staticmethod
252 def _get_subclass(typename):
253 """Returns the PersistentMixIn subclass with the name |typename|."""
254 subclass = None
255 if PersistentMixIn.__persistent_classes_cache is not None:
256 subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
257 if not subclass:
258 # Get the subclasses recursively.
259 PersistentMixIn.__persistent_classes_cache = {}
260 def recurse(c):
261 for s in c.__subclasses__():
262 assert s.__name__ not in PersistentMixIn.__persistent_classes_cache
263 PersistentMixIn.__persistent_classes_cache[s.__name__] = s
264 recurse(s)
265 recurse(PersistentMixIn)
266
267 subclass = PersistentMixIn.__persistent_classes_cache.get(typename)
268 if not subclass:
269 raise KeyError('Couldn\'t find type %s' % typename)
270 return subclass
271
272 def as_dict(self):
273 """Create a dictionary out of this object, i.e. Serialize the object."""
274 out = {}
275 for member, member_types in self._persistent_members().iteritems():
276 value = getattr(self, member)
277 _check_type_value(member, value, member_types)
278 out[member] = as_dict(value)
279 out[TYPE_FLAG] = self.__class__.__name__
280 return out
281
282 @staticmethod
283 def from_dict(data, subclass=_UNKNOWN):
284 """Returns an instance of a class inheriting from PersistentMixIn,
285 initialized with 'data' dict, i.e. Deserialize the object.
286 """
287 logging.debug('from_dict(%r, %s)', data, subclass)
288 if subclass is _UNKNOWN:
289 subclass = PersistentMixIn._get_subclass(data[TYPE_FLAG])
290 # This initializes the instance with the default values.
291
292 # pylint: disable=W0212
293 kwargs = {}
294 for member, member_types in subclass._persistent_members().iteritems():
295 if member in data:
296 try:
297 value = _inner_from_dict(member, data[member], member_types)
298 except TypeError:
299 # pylint: disable=E1103
300 logging.error(
301 'Failed to instantiate %s because of member %s',
302 subclass.__name__, member)
303 raise
304 else:
305 value = _default_value(member_types)
306 _check_type_value(member, value, member_types)
307 kwargs[member] = value
308 try:
309 obj = subclass(**kwargs)
310 except TypeError:
311 # pylint: disable=E1103
312 logging.error('Failed to instantiate %s: %r', subclass.__name__, kwargs)
313 raise
314 assert isinstance(obj, PersistentMixIn) and obj.__class__ != PersistentMixIn
315 return obj
316
317 def __str__(self):
318 return to_yaml(self)
319
320 def __eq__(self, _):
321 raise TypeError()
322
323 # pylint: disable=R0201
324 def __ne__(self, _):
325 raise TypeError()
326
327
328 def is_equivalent(lhs, rhs):
329 """Implements the equivalent of __eq__.
330
331 The reason for not implementing __eq__ is to not encourage bad behavior by
332 implicitly and recursively using __eq__() in a list().remove() call.
333 """
334 # pylint: disable=W0212
335 if lhs._persistent_members() != rhs._persistent_members():
336 return False
337 for i in lhs._persistent_members():
338 if getattr(lhs, i) != getattr(rhs, i):
339 return False
340 return True
341
342
343 def immutable(func):
344 """Member function decorators that convert 'self' to an immutable object.
345
346 Member functions of the object can't be called unless they are immutable too.
347 Properties can be looked up, this function assumes properties do not mutate
348 the object.
349
350 Note: a user can still call classmethod and do mutation on the class, or they
351 can lookup a member object and mutate this one. Don't be silly.
352 """
353 class Immutable(object):
354 def __init__(self, obj):
355 object.__setattr__(self, '__ref', obj)
356
357 def __getattribute__(self, name):
358 ref = object.__getattribute__(self, '__ref')
359 value = getattr(ref, name)
360 if not callable(value):
361 return value
362 if getattr(value, 'is_immutable', None):
363 # It is immutable too.
364 return value
365 if getattr(value, 'im_self', None) == None:
366 # It is static.
367 return value
368 raise TypeError(
369 'Can\'t call mutable member function \'%s\' on an immutable '
370 'instance of %s' % (name, ref.__class__.__name__))
371
372 def __setattr__(self, name, _value):
373 ref = object.__getattribute__(self, '__ref')
374 raise TypeError(
375 'Can\'t change attribute \'%s\' on an immutable instance of \'%s\'' %
376 (name, ref.__class__.__name__))
377
378 def __delattr__(self, name):
379 ref = object.__getattribute__(self, '__ref')
380 raise TypeError(
381 'Can\'t delete attribute \'%s\' on an immutable instance of \'%s\'' %
382 (name, ref.__class__.__name__))
383
384 def hook(self, *args, **kwargs):
385 return func(Immutable(self), *args, **kwargs)
386
387 hook.is_immutable = True
388 return hook
389
390
391 def load_from_json_file(filename):
392 """Loads one object from a JSON file."""
393 with open(filename, 'r') as f:
394 return PersistentMixIn.from_dict(json.load(f))
395
396
397 def save_to_json_file(filename, obj):
398 """Save one object in a JSON file."""
399 try:
400 old = filename + '.old'
401 if os.path.exists(filename):
402 os.rename(filename, old)
403 finally:
404 with open(filename, 'wb') as f:
405 json.dump(obj.as_dict(), f, sort_keys=True, indent=2)
406 f.write('\n')
OLDNEW
« no previous file with comments | « commit-queue/loop.sh ('k') | commit-queue/natsort.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698