Added option edit/save support via git config.
[ugit.git] / ugitlibs / model.py
blob11f2eb6913b530244f5d9ee9e83d4147735496fb
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 self.__list_params = {}
47 self.__object_params = {}
48 # For meta-programmability
49 self.from_dict(kwargs)
51 def get_param_names(self):
52 return tuple(self.__params)
54 def notify_all(self):
55 self.notify_observers(*self.get_param_names())
57 def create(self,**kwargs):
58 return self.from_dict(kwargs)
60 def clone(self, *args, **kwargs):
61 return self.__class__(*args, **kwargs).from_dict(self.to_dict())
63 def set_list_params(self, **list_params):
64 self.__list_params.update(list_params)
66 def set_object_params(self, **obj_params):
67 self.__object_params.update(obj_params)
69 def has_param(self,param):
70 return param in self.__params
72 def get_param(self,param):
73 return getattr(self, param)
75 def __getattr__(self, param):
76 '''Provides automatic get/set/add/append methods.'''
78 # Base case: we actually have this param
79 if param in self.__dict__:
80 return getattr(self, param)
82 # Check for the translated variant of the param
83 realparam = self.__translate(param, sep='')
84 if realparam in self.__dict__:
85 return getattr(self, realparam)
87 if realparam.startswith('get'):
88 param = self.__translate(param, 'get')
89 return lambda: getattr(self, param)
91 elif realparam.startswith('set'):
92 param = self.__translate(param, 'set')
93 return lambda v: self.set_param(param, v,
94 check_params=True)
96 elif realparam.startswith('add'):
97 self.__array = self.__translate(param, 'add')
98 return self.__append
100 elif realparam.startswith('append'):
101 self.__array = self.__translate(param, 'append')
102 return self.__append
104 errmsg = ("%s object has no parameter '%s'"
105 % (self.__class__.__name__, param))
107 raise AttributeError(errmsg)
109 def set_param(self, param, value, notify=True, check_params=False):
110 '''Set param with optional notification and validity checks.'''
112 param = param.lower()
113 if check_params and param not in self.__params:
114 raise Exception("Parameter '%s' not available for %s"
115 % (param, self.__class__.__name__))
116 elif param not in self.__params:
117 self.__params.append(param)
119 setattr(self, param, value)
120 if notify: self.notify_observers(param)
122 def copy_params(self, model, params=None):
123 if params is None:
124 params = self.get_param_names()
125 for param in params:
126 self.set_param(param, model.get_param(param))
128 def __append(self, *values):
129 '''Appends an arbitrary number of values to
130 an array atribute.'''
131 array = getattr(self, self.__array)
132 if array is None:
133 errmsg = "%s object has no parameter '%s'" \
134 %( self.__class__.__name__, self.__array )
135 raise AttributeError(errmsg)
136 else:
137 array.extend(values)
139 def __translate(self, param, prefix='', sep='_'):
140 '''Translates an param name from the external name
141 used in methods to those used internally. The default
142 settings strip off '_' so that both get_foo() and getFoo()
143 are valid incantations.'''
144 return param.lstrip(prefix).lstrip(sep).lower()
146 def __get_class(self, objspec):
147 '''Loads a class from a module and returns the class.'''
149 # str("module.submodule:ClassName")
150 ( modname, classname ) = objspec.split(':')
151 modfile = imp.find_module(modname)
152 module = imp.load_module(modname,
153 modfile[0], modfile[1], modfile[2])
155 if classname in module.__dict__:
156 cls = module.__dict__[classname]
157 else:
158 cls = Model
159 warning = ('WARNING: %s not found in %s\n'
160 %(modname, classname))
161 sys.stderr.write(warning)
163 modfile[0].close()
164 return cls
166 def save(self, filename):
167 import simplejson
168 file = open(filename, 'w')
169 simplejson.dump(self.to_dict(), file, indent=4)
170 file.close()
172 def load(self, filename):
173 import simplejson
174 file = open(filename, 'r')
175 dict = simplejson.load(file)
176 file.close()
177 self.from_dict(dict)
179 def from_dict(self, source_dict):
180 '''Import a complex model from a dictionary. The import/export
181 is clued as to nested Model-objects by setting the
182 __list_params or __object_params object specifications.'''
183 for param,val in source_dict.iteritems():
184 self.set_param(param, self.__param_from_dict(param, val),
185 notify=False)
186 return self
188 def __param_from_dict(self,param,val):
190 # A list of Model-objects
191 if is_list(val):
192 if param in self.__list_params:
193 # A list of Model-derived objects
194 listparam = []
195 objspec = self.__list_params[param]
196 cls = self.__get_class(objspec)
197 for item in val:
198 listparam.append(cls().from_dict(item))
199 return listparam
201 # A param that maps to a Model-object
202 elif is_dict(val):
203 if param in self.__object_params:
204 # "module.submodule:ClassName"
205 objectspec = self.__object_params[param]
206 cls = self.__get_class(objectspec)
207 return cls().from_dict(val)
209 # Atoms and uninteresting hashes/dictionaries
210 return val
212 def to_dict(self):
213 '''Exports a model to a dictionary.
214 This simplifies serialization.'''
215 params = {}
216 for param in self.__params:
217 params[param] = self.__param_to_dict(param)
218 return params
220 def __param_to_dict(self, param):
221 item = getattr(self, param)
222 return self.__item_to_dict(item)
224 def __item_to_dict(self, item):
226 if is_atom(item): return item
228 elif is_list(item):
229 newlist = []
230 for i in item:
231 newlist.append(self.__item_to_dict(i))
232 return newlist
234 elif is_dict(item):
235 newdict = {}
236 for k,v in item.iteritems():
237 newdict[k] = self.__item_to_dict(v)
238 return newdict
240 elif is_instance(item):
241 return item.to_dict()
243 else:
244 raise NotImplementedError, 'Unknown type:' + str(type(item))
246 __INDENT__ = 0
248 @staticmethod
249 def INDENT(i=None):
250 if i is not None:
251 Model.__INDENT__ += i
252 return '\t' * Model.__INDENT__
254 def __str__(self):
255 '''A convenient, recursively-defined stringification method.'''
257 io = StringIO()
258 io.write(Model.INDENT())
259 io.write(self.__class__.__name__ + '(')
261 Model.INDENT(1)
263 for param in self.__params:
264 if param.startswith('_'): continue
265 io.write(os.linesep)
267 inner = Model.INDENT() + param + " = "
268 value = getattr(self, param)
270 if type(value) == ListType:
271 indent = Model.INDENT(1)
272 io.write(inner + "[" + os.linesep)
273 for val in value:
274 if is_model(val):
275 io.write(str(val))
276 io.write(os.linesep)
277 else:
278 io.write(indent)
279 io.write(str(val))
280 io.write(',')
281 io.write(os.linesep)
283 io.write(Model.INDENT(-1))
284 io.write('],')
285 else:
286 io.write(inner)
287 io.write(str(value))
288 io.write(',')
290 io.write(os.linesep)
291 io.write(Model.INDENT(-1))
292 io.write(')')
294 value = io.getvalue()
295 io.close()
296 return value
298 #############################################################################
300 def is_dict(item):
301 return type(item) is DictType
302 def is_list(item):
303 return type(item) is ListType or type(item) is TupleType
304 def is_atom(item):
305 return(type(item) in StringTypes
306 or type(item) is BooleanType
307 or type(item) is IntType
308 or type(item) is LongType
309 or type(item) is FloatType
310 or type(item) is ComplexType)
311 def is_model(item): return issubclass(item.__class__, Model)
312 def is_instance(item):
313 return(is_model(item)
314 or type(item) is InstanceType)