#!/usr/bin/env python3 # vim: sts=4 sw=4 et # This is a component of EMC # util.py Copyright 2010 Michael Haberler # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.''' ''' persistence support for gladevcp widgets Michael Haberler 11/2010 ''' import os import sys import time import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk from configobj import ConfigObj, flatten_errors from validate import Validator from hashlib import sha1 from .hal_widgets import _HalWidgetBase from .hal_actions import _EMC_ActionBase from gladevcp.gladebuilder import widget_name class UselessIniError(Exception): pass class BadDescriptorDictError(Exception): pass # map python type names to ConfigObj type names co_map = {'str': 'string','int':'integer', 'str': 'string', 'bool': 'boolean'} debug = 0 version_number = 1 def warn(*args): print(''.join(args), file=sys.stderr) def dbg(level,*args): global debug if debug < level: return print(''.join(args)) def set_debug(value): global debug debug = value def select_widgets(widgets, hal_only=False,output_only = False): ''' input: a list of widget instances output: a list of widget instances, pruned according to the flags: hal_only = True: only add HAL widgets which make sense restoring output_only = True: only add widgets which let users set a value (scale,button..) ''' wlist = [] for w in widgets: # always skip the following types: if isinstance(w, (_EMC_ActionBase)): continue if hal_only and not isinstance(w, _HalWidgetBase): continue if output_only and not isinstance(w, (Gtk.Range, Gtk.SpinButton, Gtk.ComboBox, Gtk.CheckButton, Gtk.ToggleButton, Gtk.RadioButton)): continue wlist.append(w) return wlist def accessors(w): ''' retrieve getter/setter name of an 'interesting' widget ''' if isinstance(w, (Gtk.Range, Gtk.SpinButton)): return (w.get_value,w.set_value) if isinstance(w, (Gtk.CheckButton, Gtk.ToggleButton,Gtk.RadioButton,Gtk.ComboBox)): return (w.get_active,w.set_active) return (None,None) def store_value(widget,value): _,setter = accessors(widget) return setter and setter(value) def get_value(widget): getter,_ = accessors(widget) return getter and getter() def widget_defaults(widgets): ''' return a dict name/current value of wvalues which are to be restored ''' wvalues = dict() for w in widgets: k = widget_name(w) try: v = get_value(w) wvalues[k] = v except Exception as msg: warn("widget_defaults:" + msg) continue return wvalues class IniFile(object): # well known section and variable names in INI files vars = 'vars' widgets = 'widgets' ini = 'ini' signature = 'signature' version = 'version' def _gen_spec(self,vdict): ''' given a nested dict of sections,vars and default values, derive a ConfigObj typed spec NB: only single level nesting supported FIXME: add safeguards against broken vdict() ''' spec = '' for section in sorted(vdict.keys()): spec += '[' + section + ']\n' for varname in sorted(vdict[section].keys()): typename = type(vdict[section][varname]).__name__ if typename in co_map: typename = co_map[typename] spec += '\t' + varname + ' = ' + typename + '\n' return spec def restore_state(self,obj): ''' restore attributes from INI file 'vars' section as obj attributes, as well as any widget state in 'widgets' section ''' dbg(1, "restore_state() from %s" % (self.filename)) if not IniFile.ini in self.defaults: raise BadDescriptorDictError("defaults dict lacks 'ini' section") if self.defaults[IniFile.ini][IniFile.signature] != ( self.config[IniFile.ini][IniFile.signature]): warn("signature mismatch in %s - resetting to default" % (self.filename)) dbg(1, "expected: %s, got %s" % (self.defaults[IniFile.ini][IniFile.signature], self.config[IniFile.ini][IniFile.signature])) self.create_default_ini() else: dbg(1,"signature verified OK for %s " % (self.filename)) if IniFile.vars in self.config: for k,v in list(self.defaults[IniFile.vars].items()): setattr(obj,k,self.config[IniFile.vars][k]) if IniFile.widgets in self.config: for k,v in list(self.config[IniFile.widgets].items()): store_value(self.builder.get_object(k),v) def save_state(self, obj): ''' save obj attributes as listed in INI file 'IniFile.vars' section and widget state to 'widgets' section ''' if IniFile.vars in self.defaults: for k,v in list(self.defaults[IniFile.vars].items()): self.config[IniFile.vars][k] = getattr(obj,k,None) if IniFile.widgets in self.config: for k in list(self.defaults[IniFile.widgets].keys()): self.config[IniFile.widgets][k] = get_value(self.builder.get_object(k)) self.config.final_comment = ['last update by %s.save_state() on %s ' % (__name__,time.asctime())] self.write() dbg(1, "save_state() to %s" % (self.filename)) def create_default_ini(self): ''' create a default INI file with defaults derived from configspec ''' self.config = ConfigObj(self.filename, interpolation=False, configspec=self.spec) vdt = Validator() self.config.validate(vdt, copy=True) self.config.update(self.defaults) self.config.filename = self.filename self.config.initial_comment.append('generated by %s.create_default_ini() on %s' % (__name__, time.asctime())) self.config.write() def read_ini(self): ''' make sure current file validates OK, this will also type-convert values recreate default INI file if bad things happen ''' retries = 2 while True: try: self.config = ConfigObj(self.filename, file_error=True, interpolation=False, configspec=self.spec, raise_errors=True) validator = Validator() results = self.config.validate(validator,preserve_errors=True) if results != True: for (section_list, key, error ) in flatten_errors(self.config, results): if key is not None: warn("key '%s' in section '%s' : %s" % (key, ', '.join(section_list), error if error else "missing")) else: warn("section '%s' missing" % ', '.join(section_list)) badfilename = self.filename + '.BAD' os.rename(self.filename,badfilename) retries -= 1 if retries: raise UselessIniError("can not make sense of '%s', renamed to '%s'" % (self.filename, badfilename)) else: raise Exception(error) except (IOError, TypeError,UselessIniError) as msg: warn("%s - creating default" % (msg)) self.create_default_ini() continue except: # pass on ConfigObj's other troubles raise else: # all great break def write(self,filename=None): ''' write out changed config ''' if filename: self.filename = filename self.config.write() def __init__(self,filename,defaults,builder): defaults[IniFile.ini] = { IniFile.signature : 'astring', IniFile.version : version_number } self.defaults = defaults self.filename = filename self.builder = builder spec = self._gen_spec(self.defaults) self.signature = sha1(spec.encode('utf-8')).hexdigest() self.defaults[IniFile.ini][IniFile.signature] = self.signature dbg(2, "auto-generated spec:\n%s\nsignature = %s" % (spec, self.signature)) self.spec = spec.splitlines() self.read_ini()