Consolidated model classes into a single class
[ugit.git] / py / model.py
blobd38cdb352628dfb0355bb096946227d8a9ff8354
1 #!/usr/bin/env python
2 import imp
3 from types import *
5 class Observable(object):
6 '''Handles subject/observer notifications.'''
7 def __init__(self, notify=True):
8 self.__observers = []
9 self.__notify = notify
10 def get_notify(self):
11 return self.__notify
12 def set_notify(self, notify=True):
13 self.__notify = notify
14 def add_observer(self, observer):
15 if observer not in self.__observers:
16 self.__observers.append(observer)
17 def remove_observer(self, observer):
18 if observer in self.__observers:
19 self.__observers.remove (observer)
20 def notify_observers(self, *attr):
21 if not self.__notify: return
22 for observer in self.__observers:
23 observer.notify(*attr)
25 class Model(Observable):
26 '''Creates a generic model object with attributes specified
27 as a name:value dictionary.
29 get_name() and set_name(value) are created automatically
30 for any of the specified attributes.'''
32 def __init__(self, attributes = {}, defaults = {}, notify=True):
33 '''Initializes all attributes and default attribute
34 values. By default we do not call notify unless explicitly
35 told to do so.'''
37 Observable.__init__(self, notify)
39 for attr, value in attributes.iteritems():
40 setattr(self, attr, value)
42 for attr, value in defaults.iteritems():
43 if not hasattr(self, attr):
44 setattr(self, attr, value)
46 # For meta-programmability
47 self.__attributes = list(attributes.keys() + defaults.keys())
48 self.__list_attrs = {}
49 self.__object_attrs = {}
51 def set_list_attrs(self, list_attrs):
52 self.__list_attrs.update(list_attrs)
54 def set_object_attrs(self, obj_attrs):
55 self.__object_attrs.update(obj_attrs)
57 def getattr(self, attr):
58 return getattr(self, attr)
60 def get_attributes(self):
61 return self.__attributes
64 def __getattr__(self, attr):
65 '''Provides automatic get/set/add/append methods.'''
67 # Base case: we actually have this attribute
68 if attr in self.__dict__:
69 return getattr(self, attr)
71 # Check for the translated variant of the attr
72 realattr = self.__translate(attr, sep='')
73 if realattr in self.__dict__:
74 return getattr(self, realattr)
76 if realattr.startswith('get'):
77 realattr = self.__translate(attr, 'get')
78 return lambda: getattr(self, realattr)
80 elif realattr.startswith('set'):
81 realattr = self.__translate(attr, 'set')
82 return lambda(value): self.set_and_notify(realattr, value)
84 elif realattr.startswith('add'):
85 self.__array = self.__translate(attr, 'add')
86 return self.__append
88 elif realattr.startswith('append'):
89 self.__array = self.__translate(attr, 'append')
90 return self.__append
92 errmsg = "%s object has no attribute '%s'" \
93 % ( self.__class__, attr )
95 raise AttributeError, errmsg
97 def set(self, attr, value):
98 '''Sets a model attribute.'''
99 setattr(self, attr, value)
100 if attr not in self.__attributes:
101 self.__attributes.append(attr)
103 def set_and_notify(self, attr, value):
104 '''Sets an attribute and notifies observers.'''
105 self.set(attr, value)
106 self.notify_observers(attr)
108 def __append(self, *values):
109 '''Appends an arbitrary number of values to
110 an array atribute.'''
112 attr = self.__array
113 array = getattr(self, attr)
115 if array is None:
116 errmsg = "%s object has no attribute '%s'" \
117 % ( self.__class__, attr )
118 raise AttributeError, errmsg
120 for value in values:
121 array.append(value)
124 def __translate(self, attr, prefix='', sep='_'):
125 '''Translates an attribute name from the external name
126 used in methods to those used internally. The default
127 settings strip off '_' so that both get_foo() and getFoo()
128 are valid incantations.'''
130 return attr.lstrip(prefix).lstrip(sep).lower()
132 def __get_class(self, objspec):
133 '''Loads a class from a module and returns the class.'''
135 # str("module.submodule:ClassName")
136 ( modname, classname ) = objspec.split(':')
137 modfile = imp.find_module(modname)
138 module = imp.load_module(modname,
139 modfile[0], modfile[1], modfile[2])
141 if classname in module.__dict__:
142 cls = module.__dict__[classname]
143 else:
144 cls = Model
145 warning = 'WARNING: %s not found in %s\n' % (
146 modname, classname )
147 sys.stderr.write(warning)
149 modfile[0].close()
150 return cls
152 def save(self, filename):
153 import simplejson
154 file = open(filename, 'w')
155 simplejson.dump( self.to_dict(), file, indent=4 )
156 file.close()
158 def load(self, filename):
159 import simplejson
160 file = open(filename, 'r')
161 dict = simplejson.load(file)
162 file.close()
163 self.from_dict(dict)
165 def from_dict(self, model):
166 '''Import a complex model from a dictionary. The import/export
167 is clued as to nested Model-objects by setting the
168 __list_attrs or __object_attrs object specifications.'''
170 for attr,val in model.iteritems():
171 setattr(self, attr, self.__attr_from_dict(attr,val))
172 if attr not in self.__attributes:
173 self.__attributes.append(attr)
174 return self
176 def __attr_from_dict(self,attr,val):
178 # A list of Model-objects
179 if is_list(val):
180 if attr in self.__list_attrs:
181 # A list of Model-derived objects
182 listattr = []
183 objspec = self.__list_attrs[attr]
184 cls = self.__get_class(objspec)
185 for item in val:
186 listattr.append(cls().from_dict(item))
187 return listattr
189 # An attribute that maps to a Model-object
190 elif is_dict(val):
191 if attr in self.__object_attrs:
192 # "module.submodule:ClassName"
193 objectspec = self.__object_attrs[attr]
194 cls = self.__get_class(objectspec)
195 return cls().from_dict(val)
197 # Atoms and uninteresting hashes/dictionaries
198 return val
201 def to_dict(self):
202 '''Exports a model to a dictionary.
203 This simplifies serialization.'''
205 attrs = {}
206 for attr in self.__attributes:
207 attrs[attr] = self.__attr_to_dict(attr)
208 return attrs
210 def __attr_to_dict(self, attr):
211 item = getattr(self, attr)
212 return self.__item_to_dict(item)
214 def __item_to_dict(self, item):
216 if is_atom(item): return item
218 elif is_list(item):
219 newlist = []
220 for i in item:
221 newlist.append(self.__item_to_dict(i))
222 return newlist
224 elif is_dict(item):
225 newdict = {}
226 for k,v in item.iteritems():
227 newdict[k] = self.__item_to_dict(v)
228 return newdict
230 elif is_instance(item):
231 return item.to_dict()
233 else:
234 raise NotImplementedError, 'Unknown type:' + str(type(item))
237 __INDENT__ = -4 # Used by __str__
239 def __str__(self):
240 '''A convenient, recursively-defined stringification method.'''
242 Model.__INDENT__ += 4
244 strings = ['']
245 for attr in self.__dict__:
246 if attr.startswith('_'): continue
247 inner = " " * Model.__INDENT__ + attr + ": "
249 value = getattr(self, attr)
251 if type(value) == ListType:
253 indent = " " * (Model.__INDENT__ + 4)
254 strings.append(inner + "[")
255 for val in value:
256 stringval = indent + str(val)
257 strings.append (stringval)
259 indent = " " * Model.__INDENT__
260 strings.append(indent + "]")
262 else:
263 strings.append(inner + str(value))
265 Model.__INDENT__ -= 4
267 return "\n".join(strings)
270 def is_list(item): return type(item) is ListType or type(item) is TupleType
271 def is_dict(item): return type(item) is DictType
272 def is_atom(item):
273 return (
274 type(item) in StringTypes
275 or type(item) is BooleanType
276 or type(item) is IntType
277 or type(item) is LongType
278 or type(item) is FloatType
279 or type(item) is ComplexType
282 def is_instance(item):
283 return ( issubclass(item.__class__, Model)
284 or type(item) is InstanceType )