models: fix a bug from the run_cmd/GitPython refactor
[git-cola.git] / ugit / model.py
blob9b5b347f244027acef13dec56e004b0abed3cb40
1 #!/usr/bin/env python
2 import os
3 import imp
4 from cStringIO import StringIO
5 from types import DictType
6 from types import ListType
7 from types import TupleType
8 from types import StringTypes
9 from types import BooleanType
10 from types import IntType
11 from types import LongType
12 from types import FloatType
13 from types import ComplexType
14 from types import InstanceType
16 class Observable(object):
17 """Handles subject/observer notifications."""
18 def __init__(self,*args,**kwargs):
19 self.__observers = []
20 self.__notify = True
21 def get_notify(self):
22 return self.__notify
23 def set_notify(self, notify=True):
24 self.__notify = notify
25 def add_observer(self, observer):
26 if observer not in self.__observers:
27 self.__observers.append(observer)
28 def remove_observer(self, observer):
29 if observer in self.__observers:
30 self.__observers.remove(observer)
31 def notify_observers(self, *param):
32 if not self.__notify: return
33 for observer in self.__observers:
34 observer.notify(*param)
36 class Model(Observable):
37 """Creates a generic model object with params specified
38 as a name:value dictionary.
40 get_name() and set_name(value) are created automatically
41 for any of the parameters specified in the **kwargs"""
43 def __init__(self, *args, **kwargs):
44 Observable.__init__(self)
45 self.__params = []
46 # For meta-programmability
47 self.from_dict(kwargs)
48 self.init()
50 # for subclasses
51 def init(self): pass
53 def create(self,**kwargs):
54 return self.from_dict(kwargs)
56 def get_param_names(self):
57 return tuple(self.__params)
59 def notify_all(self):
60 self.notify_observers(*self.get_param_names())
62 def clone(self, *args, **kwargs):
63 return self.__class__(*args, **kwargs).from_dict(self.to_dict())
65 def has_param(self,param):
66 return param in self.__params
68 def get_param(self,param):
69 return getattr(self, param)
71 def __getattr__(self, param):
72 """Provides automatic get/set/add/append methods."""
74 # Base case: we actually have this param
75 if param in self.__dict__:
76 return getattr(self, param)
78 # Check for the translated variant of the param
79 realparam = self.__translate(param, sep='')
80 if realparam in self.__dict__:
81 return getattr(self, realparam)
83 if realparam.startswith("get"):
84 param = self.__translate(param, "get")
85 return lambda: getattr(self, param)
87 elif realparam.startswith("set"):
88 param = self.__translate(param, "set")
89 return lambda v: self.set_param(param, v,
90 check_params=True)
92 elif (realparam.startswith("add")
93 or realparam.startswith("append")):
95 if realparam.startswith("add"):
96 param = self.__translate(realparam, "add")
97 else:
98 param = self.__translate(realparam, "append")
100 def array_append(*values):
101 array = getattr(self, param)
102 if array is None:
103 classnm = self.__class__.__name__
104 errmsg = ("%s object has no array named '%s'"
105 %( classnm, param ))
106 raise AttributeError(errmsg)
107 else:
108 array.extend(values)
109 # Cache the function definition
110 setattr(self, realparam, array_append)
111 return array_append
113 errmsg = ("%s object has no parameter '%s'"
114 % (self.__class__.__name__, param))
116 raise AttributeError(errmsg)
118 def set_param(self, param, value, notify=True, check_params=False):
119 """Set param with optional notification and validity checks."""
121 param = param.lower()
122 if check_params and param not in self.__params:
123 raise Exception("Parameter '%s' not available for %s"
124 % (param, self.__class__.__name__))
125 elif param not in self.__params:
126 self.__params.append(param)
128 setattr(self, param, value)
129 if notify: self.notify_observers(param)
131 def copy_params(self, model, params=None):
132 if params is None:
133 params = self.get_param_names()
134 for param in params:
135 self.set_param(param, model.get_param(param))
137 def __translate(self, param, prefix='', sep='_'):
138 """Translates an param name from the external name
139 used in methods to those used internally. The default
140 settings strip off '_' so that both get_foo() and getFoo()
141 are valid incantations."""
142 return param[len(prefix):].lstrip(sep).lower()
144 def save(self, filename):
145 if not try_json():
146 return
147 import simplejson
148 file = open(filename, 'w')
149 simplejson.dump(self.to_dict(), file, indent=4)
150 file.close()
152 def load(self, filename):
153 if not try_json():
154 return
155 import simplejson
156 file = open(filename, 'r')
157 ddict = simplejson.load(file)
158 file.close()
159 if "__class__" in ddict:
160 # load params in-place.
161 del ddict["__class__"]
162 return self.from_dict(ddict)
164 @staticmethod
165 def instance(filename):
166 if not try_json():
167 return
168 import simplejson
169 file = open(filename, 'r')
170 ddict = simplejson.load(file)
171 file.close()
172 if "__class__" in ddict:
173 cls = Model.str_to_class(ddict["__class__"])
174 del ddict["__class__"]
175 return cls().from_dict(ddict)
176 else:
177 return Model().from_dict(ddict)
180 def from_dict(self, source_dict):
181 """Import a complex model from a dictionary.
182 We store class information in the __class__ variable.
183 If it looks like a duck, it's a duck."""
185 if "__class__" in source_dict:
186 clsstr = source_dict["__class__"]
187 del source_dict["__class__"]
188 cls = Model.str_to_class(clsstr)
189 return cls().from_dict(source_dict)
191 # Not initiating a clone: load parameters in-place
192 for param, val in source_dict.iteritems():
193 self.set_param(
194 param,
195 self.__obj_from_value(val),
196 notify=False)
197 self.__params.sort()
198 return self
200 def __obj_from_value(self, val):
201 # Atoms
202 if is_atom(val):
203 return val
205 # Possibly nested lists
206 elif is_list(val):
207 return [ self.__obj_from_value(v) for v in val ]
209 elif is_dict(val):
210 # A param that maps to a Model-object
211 if "__class__" in val:
212 clsstr = val["__class__"]
213 cls = Model.str_to_class(clsstr)
214 del val["__class__"]
215 return cls().from_dict(val)
216 newdict = {}
217 for k, v in val.iteritems():
218 newdict[k] = self.__obj_from_value(v)
219 return newdict
221 # All others
222 return val
225 def to_dict(self):
226 """Exports a model to a dictionary.
227 This simplifies serialization."""
228 params = {"__class__": Model.class_to_str(self)}
229 for param in self.__params:
230 params[param] =\
231 self.__obj_to_value(getattr(self, param))
232 return params
234 def __obj_to_value(self, item):
235 if is_atom(item):
236 return item
238 elif is_list(item):
239 newlist = [ self.__obj_to_value(i) for i in item ]
240 return newlist
242 elif is_dict(item):
243 newdict = {}
244 for k,v in item.iteritems():
245 newdict[k] = self.__obj_to_value(v)
246 return newdict
248 elif is_instance(item):
249 return item.to_dict()
251 else:
252 raise NotImplementedError("Unknown type:" + str(type(item)))
254 __INDENT__ = 0
255 __PREINDENT__ = True
256 __STRSTACK__ = []
258 @staticmethod
259 def INDENT(i=0):
260 Model.__INDENT__ += i
261 return '\t' * Model.__INDENT__
263 def __str__(self):
264 """A convenient, recursively-defined stringification method."""
266 # This avoid infinite recursion on cyclical structures
267 if self in Model.__STRSTACK__:
268 return "REFERENCE"
269 else:
270 Model.__STRSTACK__.append(self)
272 io = StringIO()
274 if Model.__PREINDENT__:
275 io.write(Model.INDENT())
277 io.write(self.__class__.__name__ + '(')
279 Model.INDENT(1)
281 for param in self.__params:
282 if param.startswith('_'): continue
283 io.write('\n')
285 inner = Model.INDENT() + param + " = "
286 value = getattr(self, param)
288 if type(value) == ListType:
289 indent = Model.INDENT(1)
290 io.write(inner + "[\n")
291 for val in value:
292 if is_model(val):
293 io.write(str(val)+'\n')
294 else:
295 io.write(indent)
296 io.write(str(val))
297 io.write(",\n")
299 io.write(Model.INDENT(-1))
300 io.write("],")
301 else:
302 Model.__PREINDENT__ = False
303 io.write(inner)
304 io.write(str(value))
305 io.write(',')
306 Model.__PREINDENT__ = True
308 io.write('\n' + Model.INDENT(-1) + ')')
309 value = io.getvalue()
310 io.close()
312 Model.__STRSTACK__.remove(self)
313 return value
315 @staticmethod
316 def str_to_class(clstr):
317 items = clstr.split('.')
318 modules = items[:-1]
319 classname = items[-1]
320 path = None
321 module = None
322 for mod in modules:
323 search = imp.find_module(mod, path)
324 try:
325 module = imp.load_module(mod, *search)
326 if hasattr(module, "__path__"):
327 path = module.__path__
328 finally:
329 if search and search[0]:
330 search[0].close()
331 if module:
332 return getattr(module, classname)
333 else:
334 raise Exception("No class found for: %s" % clstr)
336 @staticmethod
337 def class_to_str(instance):
338 modname = instance.__module__
339 classname = instance.__class__.__name__
340 return "%s.%s" % (modname, classname)
343 #############################################################################
344 def try_json():
345 try:
346 import simplejson
347 return True
348 except ImportError:
349 print "Unable to import simplejson." % action
350 print "You do not have simplejson installed."
351 print "try: sudo apt-get install simplejson"
352 return False
354 #############################################################################
355 def is_model(item): return issubclass(item.__class__, Model)
356 def is_dict(item):
357 return type(item) is DictType
358 def is_list(item):
359 return type(item) is ListType or type(item) is TupleType
360 def is_atom(item):
361 return(type(item) in StringTypes
362 or type(item) is BooleanType
363 or type(item) is IntType
364 or type(item) is LongType
365 or type(item) is FloatType
366 or type(item) is ComplexType)
367 def is_instance(item):
368 return(is_model(item)
369 or type(item) is InstanceType)