/ lib / python / gladevcp / persistence.py
persistence.py
  1  #!/usr/bin/env python
  2  # vim: sts=4 sw=4 et
  3  #    This is a component of EMC
  4  #    util.py Copyright 2010 Michael Haberler
  5  #
  6  #
  7  #    This program is free software; you can redistribute it and/or modify
  8  #    it under the terms of the GNU General Public License as published by
  9  #    the Free Software Foundation; either version 2 of the License, or
 10  #    (at your option) any later version.
 11  #
 12  #    This program is distributed in the hope that it will be useful,
 13  #    but WITHOUT ANY WARRANTY; without even the implied warranty of
 14  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  #    GNU General Public License for more details.
 16  #
 17  #    You should have received a copy of the GNU General Public License
 18  #    along with this program; if not, write to the Free Software
 19  #    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.'''
 20  '''
 21      persistence support for gladevcp widgets
 22      Michael Haberler 11/2010
 23  '''
 24  import os
 25  import sys
 26  import time
 27  import gtk
 28  from configobj import ConfigObj, flatten_errors
 29  from validate import Validator
 30  from hashlib import sha1
 31  from hal_widgets import _HalWidgetBase
 32  from hal_actions import _EMC_ActionBase
 33  from gladevcp.gladebuilder import widget_name
 34  
 35  class UselessIniError(Exception):
 36      pass
 37  
 38  class BadDescriptorDictError(Exception):
 39      pass
 40  
 41  # map python type names to ConfigObj type names
 42  co_map = {'str': 'string','int':'integer', 'str': 'string', 'bool': 'boolean'}
 43  debug = 0
 44  version_number = 1
 45  
 46  
 47  def warn(*args):
 48      print >> sys.stderr,''.join(args)
 49  
 50  
 51  def dbg(level,*args):
 52      global debug
 53      if debug < level: return
 54      print ''.join(args)
 55  
 56  
 57  def set_debug(value):
 58      global debug
 59      debug = value
 60  
 61  
 62  def select_widgets(widgets, hal_only=False,output_only = False):
 63      '''
 64      input: a list of widget instances
 65      output: a list of widget instances, pruned according to the flags:
 66          hal_only = True:  only add HAL widgets which make sense restoring
 67          output_only = True: only add widgets which let users
 68          set a value (scale,button..)
 69      '''
 70      wlist = []
 71      for w in widgets:
 72          # always skip the following types: 
 73          if isinstance(w, (_EMC_ActionBase)):
 74              continue
 75          if hal_only and not isinstance(w, _HalWidgetBase):
 76              continue
 77          if output_only and not isinstance(w, (gtk.Range,
 78                                                gtk.SpinButton,
 79                                                gtk.ComboBox,
 80                                                gtk.CheckButton,
 81                                                gtk.ToggleButton,
 82                                                gtk.RadioButton)):
 83              continue
 84          wlist.append(w)
 85      return wlist
 86  
 87  
 88  def accessors(w):
 89      '''
 90      retrieve getter/setter name of an 'interesting' widget
 91      '''
 92      if isinstance(w, (gtk.Range, gtk.SpinButton)):
 93          return (w.get_value,w.set_value)
 94      if isinstance(w, (gtk.CheckButton, gtk.ToggleButton,gtk.RadioButton,gtk.ComboBox)):
 95          return (w.get_active,w.set_active)
 96      return (None,None)
 97  
 98  def store_value(widget,value):
 99      _,setter = accessors(widget)
100      return setter and setter(value)
101  
102  def get_value(widget):
103      getter,_ = accessors(widget)
104      return getter and getter()
105  
106  def widget_defaults(widgets):
107      '''
108      return a dict name/current value of wvalues which are to be restored
109      '''
110      wvalues = dict()
111      for w in widgets:
112          k = widget_name(w)
113          try:
114              v = get_value(w)
115              wvalues[k] = v
116          except Exception,msg:
117              warn("widget_defaults:" + msg)
118              continue
119      return wvalues
120  
121  
122  class IniFile(object):
123  
124      # well known section and variable names in .ini files
125      vars = 'vars'
126      widgets = 'widgets'
127      ini = 'ini'
128      signature = 'signature'
129      version = 'version'
130  
131      def _gen_spec(self,vdict):
132          '''
133          given a nested dict of sections,vars and default values,
134          derive a ConfigObj typed spec
135          NB: only single level nesting supported
136  
137          FIXME: add safeguards against broken vdict()
138          '''
139          spec = ''
140          for section in sorted(vdict.keys()):
141              spec += '[' + section + ']\n'
142              for varname in sorted(vdict[section].keys()):
143                  typename = type(vdict[section][varname]).__name__
144                  if co_map.has_key(typename):
145                      typename = co_map[typename]
146                  spec += '\t' + varname + ' = ' + typename  + '\n'
147          return spec
148  
149      def restore_state(self,obj):
150          '''
151          restore attributes from ini file 'vars' section as obj attributes,
152          as well as any widget state in 'widgets' section
153          '''
154          dbg(1, "restore_state() from %s" % (self.filename))
155  
156          if not self.defaults.has_key(IniFile.ini):
157              raise BadDescriptorDictError("defaults dict lacks 'ini' section")
158  
159          if  self.defaults[IniFile.ini][IniFile.signature] != (
160                  self.config[IniFile.ini][IniFile.signature]):
161              warn("signature mismatch in %s -  resetting to default" %
162                   (self.filename))
163              dbg(1, "expected: %s, got %s" %
164                  (self.defaults[IniFile.ini][IniFile.signature],
165                   self.config[IniFile.ini][IniFile.signature]))
166              self.create_default_ini()
167          else:
168              dbg(1,"signature verified OK for %s " % (self.filename))
169  
170          if self.config.has_key(IniFile.vars):
171              for k,v in self.defaults[IniFile.vars].items():
172                  setattr(obj,k,self.config[IniFile.vars][k])
173  
174          if self.config.has_key(IniFile.widgets):
175              for k,v in self.config[IniFile.widgets].items():
176                  store_value(self.builder.get_object(k),v)
177  
178      def save_state(self, obj):
179          '''
180          save obj attributes as listed in ini file 'IniFile.vars' section and
181          widget state to 'widgets' section
182          '''
183          if self.defaults.has_key(IniFile.vars):
184              for k,v in self.defaults[IniFile.vars].items():
185                  self.config[IniFile.vars][k] = getattr(obj,k,None)
186  
187          if self.config.has_key(IniFile.widgets):
188              for k in self.defaults[IniFile.widgets].keys():
189                  self.config[IniFile.widgets][k] = get_value(self.builder.get_object(k))
190  
191          self.config.final_comment = ['last update  by %s.save_state() on %s ' %
192                                           (__name__,time.asctime())]
193          self.write()
194          dbg(1, "save_state() to %s" % (self.filename))
195  
196  
197      def create_default_ini(self):
198          '''
199          create a default ini file with defaults derived from configspec
200          '''
201          self.config = ConfigObj(self.filename, interpolation=False,
202                                  configspec=self.spec)
203          vdt = Validator()
204          self.config.validate(vdt, copy=True)
205          self.config.update(self.defaults)
206          self.config.filename = self.filename
207          self.config.initial_comment.append('generated by %s.create_default_ini() on %s' %
208                                             (__name__, time.asctime()))
209          self.config.write()
210  
211      def read_ini(self):
212          '''
213          make sure current file validates OK, this will also type-convert values
214          recreate default ini file if bad things happen
215          '''
216          retries = 2
217          while True:
218              try:
219                  self.config = ConfigObj(self.filename,
220                                          file_error=True,
221                                          interpolation=False,
222                                          configspec=self.spec,
223                                          raise_errors=True)
224                  validator = Validator()
225                  results = self.config.validate(validator,preserve_errors=True)
226                  if results != True:
227                      for (section_list, key, error
228                           ) in flatten_errors(self.config, results):
229                          if key is not None:
230                              warn("key '%s' in section '%s' : %s" %
231                                   (key, ', '.join(section_list),
232                                    error if error else "missing"))
233                          else:
234                              warn("section '%s' missing" %
235                                   ', '.join(section_list))
236                      badfilename = self.filename + '.BAD'
237                      os.rename(self.filename,badfilename)
238                      retries -= 1
239                      if retries:
240                          raise UselessIniError("cant make sense of '%s', renamed to '%s'"
241                                                % (self.filename, badfilename))
242                      else:
243                          raise Exception(error)
244  
245              except (IOError, TypeError,UselessIniError),msg:
246                  warn("%s - creating default" % (msg))
247                  self.create_default_ini()
248                  continue
249  
250              except: # pass on ConfigObj's other troubles
251                  raise
252  
253              else: # all great
254                  break
255  
256      def write(self,filename=None):
257          '''
258          write out changed config
259          '''
260          if filename:
261              self.filename = filename
262          self.config.write()
263  
264      def __init__(self,filename,defaults,builder):
265          defaults[IniFile.ini] =   { IniFile.signature : 'astring',
266                                      IniFile.version : version_number }
267          self.defaults = defaults
268          self.filename = filename
269          self.builder = builder
270          spec = self._gen_spec(self.defaults)
271          self.signature = sha1(spec).hexdigest()
272          self.defaults[IniFile.ini][IniFile.signature] = self.signature
273  
274          dbg(2, "auto-generated spec:\n%s\nsignature = %s" %
275              (spec, self.signature))
276          self.spec = spec.splitlines()
277          self.read_ini()