Beginning to rewrite Hooke to decouple from GUI.
authorW. Trevor King <wking@drexel.edu>
Thu, 6 May 2010 11:27:59 +0000 (07:27 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 6 May 2010 11:27:59 +0000 (07:27 -0400)
This commit:
  * Started documenting with reStructuredText (for later use by Sphinx)
  * Added hooke.config module to handle configure files (via ConfigParser)
  * Added hooke.compat module to hold compatibility code
    (e.g. workarounds to allow Hooke to work on older Python versions).
  * Added config input/output to hooke.hooke.Hooke.
  * Stubbed out driver/plugin initialization in Hooke

hooke/compat/odict.py [new file with mode: 0644]
hooke/config.py
hooke/hooke.py

diff --git a/hooke/compat/odict.py b/hooke/compat/odict.py
new file mode 100644 (file)
index 0000000..22de85c
--- /dev/null
@@ -0,0 +1,336 @@
+# -*- coding: utf-8 -*-
+
+# An ordered dictionary implementation.  OrderedDict entered the
+# Python Standard Library in Python 2.7 and 3.1.  See `PEP 372`_ for
+# details.  Code_ by Armin Ronacher and the PEP 273 authors.
+# 
+# .. _PEP 372: http://www.python.org/dev/peps/pep-0372/
+# .. _Code: http://dev.pocoo.org/hg/sandbox/raw-file/tip/odict.py
+
+"""
+    odict
+    ~~~~~
+
+    This module is an example implementation of an ordered dict for the
+    collections module.  It's not written for performance (it actually
+    performs pretty bad) but to show how the API works.
+
+
+    Questions and Answers
+    =====================
+
+    Why would anyone need ordered dicts?
+
+        Dicts in python are unordered which means that the order of items when
+        iterating over dicts is undefined.  As a matter of fact it is most of
+        the time useless and differs from implementation to implementation.
+
+        Many developers stumble upon that problem sooner or later when
+        comparing the output of doctests which often does not match the order
+        the developer thought it would.
+
+        Also XML systems such as Genshi have their problems with unordered
+        dicts as the input and output ordering of tag attributes is often
+        mixed up because the ordering is lost when converting the data into
+        a dict.  Switching to lists is often not possible because the
+        complexity of a lookup is too high.
+
+        Another very common case is metaprogramming.  The default namespace
+        of a class in python is a dict.  With Python 3 it becomes possible
+        to replace it with a different object which could be an ordered dict.
+        Django is already doing something similar with a hack that assigns
+        numbers to some descriptors initialized in the class body of a
+        specific subclass to restore the ordering after class creation.
+
+        When porting code from programming languages such as PHP and Ruby
+        where the item-order in a dict is guaranteed it's also a great help
+        to have an equivalent data structure in Python to ease the transition.
+
+    Where are new keys added?
+
+        At the end.  This behavior is consistent with Ruby 1.9 Hashmaps
+        and PHP Arrays.  It also matches what common ordered dict
+        implementations do currently.
+
+    What happens if an existing key is reassigned?
+
+        The key is *not* moved.  This is consitent with existing
+        implementations and can be changed by a subclass very easily::
+
+            class movingodict(odict):
+                def __setitem__(self, key, value):
+                    self.pop(key, None)
+                    odict.__setitem__(self, key, value)
+
+        Moving keys to the end of a ordered dict on reassignment is not
+        very useful for most applications.
+
+    Does it mean the dict keys are sorted by a sort expression?
+
+        That's not the case.  The odict only guarantees that there is an order
+        and that newly inserted keys are inserted at the end of the dict.  If
+        you want to sort it you can do so, but newly added keys are again added
+        at the end of the dict.
+
+    I initializes the odict with a dict literal but the keys are not
+    ordered like they should!
+
+        Dict literals in Python generate dict objects and as such the order of
+        their items is not guaranteed.  Before they are passed to the odict
+        constructor they are already unordered.
+
+    What happens if keys appear multiple times in the list passed to the
+    constructor?
+
+        The same as for the dict.  The latter item overrides the former.  This
+        has the side-effect that the position of the first key is used because
+        the key is actually overwritten:
+
+        >>> odict([('a', 1), ('b', 2), ('a', 3)])
+        odict.odict([('a', 3), ('b', 2)])
+
+        This behavor is consistent with existing implementation in Python
+        and the PHP array and the hashmap in Ruby 1.9.
+
+    This odict doesn't scale!
+
+        Yes it doesn't.  The delitem operation is O(n).  This is file is a
+        mockup of a real odict that could be implemented for collections
+        based on an linked list.
+
+    Why is there no .insert()?
+
+        There are few situations where you really want to insert a key at
+        an specified index.  To now make the API too complex the proposed
+        solution for this situation is creating a list of items, manipulating
+        that and converting it back into an odict:
+
+        >>> d = odict([('a', 42), ('b', 23), ('c', 19)])
+        >>> l = d.items()
+        >>> l.insert(1, ('x', 0))
+        >>> odict(l)
+        odict.odict([('a', 42), ('x', 0), ('b', 23), ('c', 19)])
+
+    :copyright: (c) 2008 by Armin Ronacher and PEP 273 authors.
+    :license: modified BSD license.
+"""
+from itertools import izip, imap
+from copy import deepcopy
+
+missing = object()
+
+
+class odict(dict):
+    """
+    Ordered dict example implementation.
+
+    This is the proposed interface for a an ordered dict as proposed on the
+    Python mailinglist (proposal_).
+
+    It's a dict subclass and provides some list functions.  The implementation
+    of this class is inspired by the implementation of Babel but incorporates
+    some ideas from the `ordereddict`_ and Django's ordered dict.
+
+    The constructor and `update()` both accept iterables of tuples as well as
+    mappings:
+
+    >>> d = odict([('a', 'b'), ('c', 'd')])
+    >>> d.update({'foo': 'bar'})
+    >>> d
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
+    
+    Keep in mind that when updating from dict-literals the order is not
+    preserved as these dicts are unsorted!
+
+    You can copy an odict like a dict by using the constructor, `copy.copy`
+    or the `copy` method and make deep copies with `copy.deepcopy`:
+
+    >>> from copy import copy, deepcopy
+    >>> copy(d)
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
+    >>> d.copy()
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
+    >>> odict(d)
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar')])
+    >>> d['spam'] = []
+    >>> d2 = deepcopy(d)
+    >>> d2['spam'].append('eggs')
+    >>> d
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])])
+    >>> d2
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', ['eggs'])])
+
+    All iteration methods as well as `keys`, `values` and `items` return
+    the values ordered by the the time the key-value pair is inserted:
+
+    >>> d.keys()
+    ['a', 'c', 'foo', 'spam']
+    >>> d.values()
+    ['b', 'd', 'bar', []]
+    >>> d.items()
+    [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]
+    >>> list(d.iterkeys())
+    ['a', 'c', 'foo', 'spam']
+    >>> list(d.itervalues())
+    ['b', 'd', 'bar', []]
+    >>> list(d.iteritems())
+    [('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])]
+
+    Index based lookup is supported too by `byindex` which returns the
+    key/value pair for an index:
+
+    >>> d.byindex(2)
+    ('foo', 'bar')
+
+    You can reverse the odict as well:
+
+    >>> d.reverse()
+    >>> d
+    odict.odict([('spam', []), ('foo', 'bar'), ('c', 'd'), ('a', 'b')])
+    
+    And sort it like a list:
+
+    >>> d.sort(key=lambda x: x[0].lower())
+    >>> d
+    odict.odict([('a', 'b'), ('c', 'd'), ('foo', 'bar'), ('spam', [])])
+
+    .. _proposal: http://thread.gmane.org/gmane.comp.python.devel/95316
+    .. _ordereddict: http://www.xs4all.nl/~anthon/Python/ordereddict/
+    """
+
+    def __init__(self, *args, **kwargs):
+        dict.__init__(self)
+        self._keys = []
+        self.update(*args, **kwargs)
+
+    def __delitem__(self, key):
+        dict.__delitem__(self, key)
+        self._keys.remove(key)
+
+    def __setitem__(self, key, item):
+        if key not in self:
+            self._keys.append(key)
+        dict.__setitem__(self, key, item)
+
+    def __deepcopy__(self, memo=None):
+        if memo is None:
+            memo = {}
+        d = memo.get(id(self), missing)
+        if d is not missing:
+            return d
+        memo[id(self)] = d = self.__class__()
+        dict.__init__(d, deepcopy(self.items(), memo))
+        d._keys = self._keys[:]
+        return d
+
+    def __getstate__(self):
+        return {'items': dict(self), 'keys': self._keys}
+
+    def __setstate__(self, d):
+        self._keys = d['keys']
+        dict.update(d['items'])
+
+    def __reversed__(self):
+        return reversed(self._keys)
+
+    def __eq__(self, other):
+        if isinstance(other, odict):
+            if not dict.__eq__(self, other):
+                return False
+            return self.items() == other.items()
+        return dict.__eq__(self, other)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __cmp__(self, other):
+        if isinstance(other, odict):
+            return cmp(self.items(), other.items())
+        elif isinstance(other, dict):
+            return dict.__cmp__(self, other)
+        return NotImplemented
+
+    @classmethod
+    def fromkeys(cls, iterable, default=None):
+        return cls((key, default) for key in iterable)
+
+    def clear(self):
+        del self._keys[:]
+        dict.clear(self)
+
+    def copy(self):
+        return self.__class__(self)
+
+    def items(self):
+        return zip(self._keys, self.values())
+
+    def iteritems(self):
+        return izip(self._keys, self.itervalues())
+
+    def keys(self):
+        return self._keys[:]
+
+    def iterkeys(self):
+        return iter(self._keys)
+
+    def pop(self, key, default=missing):
+        if default is missing:
+            return dict.pop(self, key)
+        elif key not in self:
+            return default
+        self._keys.remove(key)
+        return dict.pop(self, key, default)
+
+    def popitem(self, key):
+        self._keys.remove(key)
+        return dict.popitem(key)
+
+    def setdefault(self, key, default=None):
+        if key not in self:
+            self._keys.append(key)
+        dict.setdefault(self, key, default)
+
+    def update(self, *args, **kwargs):
+        sources = []
+        if len(args) == 1:
+            if hasattr(args[0], 'iteritems'):
+                sources.append(args[0].iteritems())
+            else:
+                sources.append(iter(args[0]))
+        elif args:
+            raise TypeError('expected at most one positional argument')
+        if kwargs:
+            sources.append(kwargs.iteritems())
+        for iterable in sources:
+            for key, val in iterable:
+                self[key] = val
+
+    def values(self):
+        return map(self.get, self._keys)
+
+    def itervalues(self):
+        return imap(self.get, self._keys)
+
+    def index(self, item):
+        return self._keys.index(item)
+
+    def byindex(self, item):
+        key = self._keys[item]
+        return (key, dict.__getitem__(self, key))
+
+    def reverse(self):
+        self._keys.reverse()
+
+    def sort(self, *args, **kwargs):
+        self._keys.sort(*args, **kwargs)
+
+    def __repr__(self):
+        return 'odict.odict(%r)' % self.items()
+
+    __copy__ = copy
+    __iter__ = iterkeys
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
index 75ea680cce416c8b412941fa83bb15369ef00993..7f10c1527ad557b16550abc69e3a21e8c9a12b90 100644 (file)
 # -*- coding: utf-8 -*-
 
 '''
-libhooke.py
+config.py
 
-General library of internal objects and utilities for Hooke.
+Configuration defaults, read/write, and template file creation for Hooke.
 
-Copyright (C) 2006 Massimo Sandal (University of Bologna, Italy).
-With algorithms contributed by Francesco Musiani (University of Bologna, Italy)
-
-This program is released under the GNU General Public License version 2.
+COPYRIGHT
 '''
 
-import scipy
-import numpy
-import xml.dom.minidom
-import os
+import ConfigParser as configparser
 import os.path
-import string
-
-#import ConfigParser
-
-def config_file_path(filename, config_dir=None):
-    if config_dir == None:
-        config_dir = os.path.abspath(
-            os.path.join(os.path.dirname(os.path.dirname(__file__)), 'conf'))
-    return os.path.join(config_dir, filename)
-
-class HookeConfig(object):
-    '''
-    Handling of Hooke configuration file
-
-    Mostly based on the simple-yet-useful examples of the Python Library Reference
-    about xml.dom.minidom
-
-    FIXME: starting to look a mess, should require refactoring
-    '''
-
-    def __init__(self, config_dir=None):
-        self.config={}
-        self.config['install']={}
-        self.config['plugins']=[]
-        self.config['drivers']=[]
-        self.config['plotmanips']=[]
-        self.config_dir = config_dir
-
-    def load_config(self, filename):
-        myconfig=file(config_file_path(filename, config_dir=self.config_dir))
-
-        #the following 3 lines are needed to strip newlines. otherwise, since newlines
-        #are XML elements too, the parser would read them (and re-save them, multiplying
-        #newlines...)
-        #yes, I'm an XML n00b
-        the_file=myconfig.read()
-        the_file_lines=the_file.split('\n')
-        the_file=''.join(the_file_lines)
-
-        self.config_tree=xml.dom.minidom.parseString(the_file)
-
-        def getText(nodelist):
-            #take the text from a nodelist
-            #from Python Library Reference 13.7.2
-            rc = ''
-            for node in nodelist:
-                if node.nodeType == node.TEXT_NODE:
-                    rc += node.data
-            return rc
-
-        def handleConfig(config):
-            install_elements=config.getElementsByTagName("install")
-            display_elements=config.getElementsByTagName("display")
-            plugins_elements=config.getElementsByTagName("plugins")
-            drivers_elements=config.getElementsByTagName("drivers")
-            defaultlist_elements=config.getElementsByTagName("defaultlist")
-            plotmanip_elements=config.getElementsByTagName("plotmanips")
-            handleInstall(install_elements)
-            handleDisplay(display_elements)
-            handlePlugins(plugins_elements)
-            handleDrivers(drivers_elements)
-            handleDefaultlist(defaultlist_elements)
-            handlePlotmanip(plotmanip_elements)
-
-        def handleInstall(install_elements):
-            for install in install_elements:
-                for node in install.childNodes:
-                    if node.nodeType == node.TEXT_NODE:
-                        continue
-                    path = os.path.abspath(getText(node.childNodes).strip())
-                    self.config['install'][str(node.tagName)] = path
-
-        def handleDisplay(display_elements):
-            for element in display_elements:
-                for attribute in element.attributes.keys():
-                    self.config[attribute]=element.getAttribute(attribute)
-
-        def handlePlugins(plugins):
-            for plugin in plugins[0].childNodes:
-                try:
-                    self.config['plugins'].append(str(plugin.tagName))
-                except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
-                    pass
-        #FIXME: code duplication
-        def handleDrivers(drivers):
-            for driver in drivers[0].childNodes:
-                try:
-                    self.config['drivers'].append(str(driver.tagName))
-                except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
-                    pass
-
-        def handlePlotmanip(plotmanips):
-            for plotmanip in plotmanips[0].childNodes:
-                try:
-                    self.config['plotmanips'].append(str(plotmanip.tagName))
-                except: #if we allow fancy formatting of xml, there is a text node, so tagName fails for it...
-                    pass
-
-        def handleDefaultlist(defaultlist):
-            '''
-            default playlist
-            '''
-            dflist=getText(defaultlist[0].childNodes)
-            self.config['defaultlist']=dflist.strip()
-
-        handleConfig(self.config_tree)
-        #making items in the dictionary more machine-readable
-        for item in self.config.keys():
-            try:
-                self.config[item]=float(self.config[item])
-            except TypeError: #we are dealing with a list, probably. keep it this way.
-                try:
-                    self.config[item]=eval(self.config[item])
-                except: #not a list, not a tuple, probably a string?
-                    pass
-            except ValueError: #if we can't get it to a number, it must be None or a string
-                if string.lower(self.config[item])=='none':
-                    self.config[item]=None
-                else:
-                    pass
-
-        return self.config
-
-
-    def save_config(self, config_filename):
-        print 'Not Implemented.'
-        pass
-
-
-def debug():
-    '''
-    debug stuff from latest rewrite of hooke_playlist.py
-    should be removed sooner or later (or substituted with new debug code!)
-    '''
-    confo=HookeConfig()
-    print confo.load_config('hooke.conf')
+import textwrap
+
+from .compat.odict import odict as OrderedDict
+
+DEFAULT_PATHS = [
+    '/usr/share/hooke/hooke.cfg',
+    '/etc/hooke/hooke.cfg',
+    '~/.hooke.cfg',
+    ]
+"""We start with the system files, and work our way to the more
+specific user files, so the user can override the sysadmin who
+in turn overrides the developer defaults.
+"""
+
+class Setting (object):
+    """An entry (section or option) in HookeConfigParser.
+    """
+    def __init__(self, section, option=None, value=None, help=None, wrap=True):
+        self.section = section
+        self.option = option
+        self.value = value
+        self.help = help
+        self.wrap = True
+
+    def is_section(self):
+        return self.option == None
+
+    def is_option(self):
+        return not self.is_section()
+
+    def write(self, fp, value=None, comments=True, wrapper=None):
+        if comments == True and self.help != None:
+            text = self.help
+            if self.wrap == True:
+                if wrapper == None:
+                    wrapper = textwrap.TextWrapper(
+                        initial_indent="# ",
+                        subsequent_indent="# ")
+                text = wrapper.fill(text)
+            fp.write(text.rstrip()+'\n')
+        if self.is_section():
+            fp.write("[%s]\n" % self.section)
+        else:
+            fp.write("%s = %s\n" % (self.option,
+                                    str(value).replace('\n', '\n\t')))
+
+DEFAULT_SETTINGS = [
+    Setting('display', help='Control display appearance: colour, ???, etc.'),
+    Setting('display', 'colour_ext', 'None', help=None),
+    Setting('display', 'colour_ret', 'None', help=None),
+    Setting('display', 'ext', '1', help=None),
+    Setting('display', 'ret', '1', help=None),
+
+    Setting('display', 'correct', '1', help=None),
+    Setting('display', 'colout_correct', 'None', help=None),
+    Setting('display', 'contact_point', '0', help=None),
+    Setting('display', 'medfilt', '0', help=None),
+
+    Setting('display', 'xaxes', '0', help=None),
+    Setting('display', 'yaxes', '0', help=None),
+    Setting('display', 'flatten', '1', help=None),
+
+    Setting('conditions', 'temperature', '301', help=None),
+
+    Setting('fitting', 'auto_fit_points', '50', help=None),
+    Setting('fitting', 'auto_slope_span', '20', help=None),
+    Setting('fitting', 'auto_delta_force', '1-', help=None),
+    Setting('fitting', 'auto_fit_nm', '5', help=None),
+    Setting('fitting', 'auto_min_p', '0.005', help=None),
+    Setting('fitting', 'auto_max_p', '10', help=None),
+
+    Setting('?', 'baseline_clicks', '0', help=None),
+    Setting('fitting', 'auto_left_baseline', '20', help=None),
+    Setting('fitting', 'auto_right_baseline', '20', help=None),
+    Setting('fitting', 'force_multiplier', '1', help=None),
+    
+    Setting('display', 'fc_showphase', '0', help=None),
+    Setting('display', 'fc_showimposed', '0', help=None),
+    Setting('display', 'fc_interesting', '0', help=None),
+    Setting('?', 'tccd_threshold', '0', help=None),
+    Setting('?', 'tccd_coincident', '0', help=None),
+    Setting('display', '', '', help=None),
+    Setting('display', '', '', help=None),
+
+    Setting('filesystem', 'filterindex', '0', help=None),
+    Setting('filesystem', 'filters',
+            "Playlist files (*.hkp)|*.hkp|Text files (*.txt)|*.txt|All files (*.*)|*.*')",
+            help=None),
+    Setting('filesystem', 'workdir', 'test',
+            help='\n'.join(['# Substitute your work directory',
+                            '#workdir = D:\hooke']),
+            wrap=False),
+    Setting('filesystem', 'playlist', 'test.hkp', help=None),
+    ]
+
+def get_setting(settings, match):
+    """Return the first Setting object matching both match.section and
+    match.option.
+    """
+    for s in settings:
+        if s.section == match.section and s.option == match.option:
+            return s
+    return None
+
+class HookeConfigParser (configparser.SafeConfigParser):
+    """A wrapper around configparser.SafeConfigParser.
+
+    You will probably only need .read and .write.
+
+    Examples
+    --------
+
+    >>> import sys
+    >>> c = HookeConfigParser(paths=DEFAULT_PATHS,
+    ...                       default_settings=DEFAULT_SETTINGS)
+    >>> c.write(sys.stdout) # doctest: +ELLIPSIS
+    # Control display appearance: colour, ???, etc.
+    [display]
+    colour_ext = None
+    colour_ret = None
+    ...
+    """
+    def __init__(self, paths=None, default_settings=None, defaults=None,
+                 dict_type=OrderedDict, indent='# ', **kwargs):
+        # Can't use super() because SafeConfigParser is a classic class
+        #super(HookeConfigParser, self).__init__(defaults, dict_type)
+        configparser.SafeConfigParser.__init__(self, defaults, dict_type)
+        if paths == None:
+            paths = []
+        self._config_paths = paths
+        if default_settings == None:
+            default_settings = []
+        self._default_settings = default_settings
+        for key in ['initial_indent', 'subsequent_indent']:
+            if key not in kwargs:
+                kwargs[key] = indent
+        self._comment_textwrap = textwrap.TextWrapper(**kwargs)
+        self._setup_default_settings()
+
+    def _setup_default_settings(self):
+        for setting in self._default_settings: #reversed(self._default_settings):
+            # reversed cause: http://docs.python.org/library/configparser.html
+            # "When adding sections or items, add them in the reverse order of
+            # how you want them to be displayed in the actual file."
+            if setting.section not in self.sections():
+                self.add_section(setting.section)
+            if setting.option != None:
+                self.set(setting.section, setting.option, setting.value)
+
+    def read(self, filenames=None):
+        """Read and parse a filename or a list of filenames.
+
+        If filenames is None, it defaults to ._config_paths.  If a
+        filename is not in ._config_paths, it gets appended to the
+        list.  We also run os.path.expanduser() on the input filenames
+        internally so you don't have to worry about it.
+
+        Files that cannot be opened are silently ignored; this is
+        designed so that you can specify a list of potential
+        configuration file locations (e.g. current directory, user's
+        home directory, systemwide directory), and all existing
+        configuration files in the list will be read.  A single
+        filename may also be given.
+
+        Return list of successfully read files.
+        """
+        if filenames == None:
+            filenames = [os.path.expanduser(p) for p in self._config_paths]
+        else:
+            if isinstance(filenames, basestring):
+                filenames = [filenames]
+            paths = [os.path.abspath(os.path.expanduser(p))
+                     for p in self._config_paths]
+            for filename in filenames:
+                if os.path.abspath(os.path.expanduser(filename)) not in paths:
+                    self._config_paths.append(filename)
+        # Can't use super() because SafeConfigParser is a classic class
+        #return super(HookeConfigParser, self).read(filenames)
+        return configparser.SafeConfigParser.read(self, filenames)
+
+    def _write_setting(self, fp, section=None, option=None, value=None,
+                       **kwargs):
+        """Print, if known, a nicely wrapped comment form of a
+        setting's .help.
+        """
+        match = get_setting(self._default_settings, Setting(section, option))
+        if match == None:
+            match = Setting(section, option, value, **kwargs)
+        match.write(fp, value=value, wrapper=self._comment_textwrap)
+
+    def write(self, fp=None, comments=True):
+        """Write an .ini-format representation of the configuration state.
+
+        This expands on RawConfigParser.write() by optionally adding
+        comments when they are known (i.e. for ._default_settings).
+        However, comments are not read in during a read, so .read(x)
+        .write(x) may `not preserve comments`_.
+
+        .. _not preserve comments: http://bugs.python.org/issue1410680
+
+        Examples
+        --------
+
+        >>> import sys, StringIO
+        >>> c = HookeConfigParser()
+        >>> instring = '''
+        ... # Some comment
+        ... [section]
+        ... option = value
+        ...
+        ... '''
+        >>> c._read(StringIO.StringIO(instring), 'example.cfg')
+        >>> c.write(sys.stdout)
+        [section]
+        option = value
+        <BLANKLINE>
+        """
+        local_fp = fp == None
+        if local_fp:
+            fp = open(self._config_paths[-1], 'w')
+        if self._defaults:
+            print self._defaults
+            self._write_setting(fp, configparser.DEFAULTSECT,
+                                help="Miscellaneous options")
+            for (key, value) in self._defaults.items():
+                self._write_setting(fp, configparser.DEFAULTSECT, key, value)
+            fp.write("\n")
+        for section in self._sections:
+            self._write_setting(fp, section)
+            for (key, value) in self._sections[section].items():
+                if key != "__name__":
+                    self._write_setting(fp, section, key, value)
+            fp.write("\n")
+        if local_fp:
+            fp.close()
index 547d7cc874b1341f3ed5a557f988d10da7bddbd6..3d137f3c822955e3e5ea0cbe4c7732ed956bf12f 100644 (file)
@@ -1,18 +1,19 @@
 #!/usr/bin/env python
 
 '''
-HOOKE - A force spectroscopy review & analysis tool
+Hooke - A force spectroscopy review & analysis tool.
 
-Copyright (C) 2008-2010 Massimo Sandal (University of Bologna, Italy).
-                        Rolf Schmidt (Concordia University, Canada).
-
-This program is released under the GNU General Public License version 2.
+COPYRIGHT
 '''
 
-from libhooke import WX_GOOD
+import Queue as queue
+import multiprocessing
+
+from . import config as config_mod
+from .plugin import PLUGINS
+from .driver import DRIVERS
+
 
-import wxversion
-wxversion.select(WX_GOOD)
 import copy
 import cStringIO
 import os
@@ -21,119 +22,44 @@ import sys
 import glob
 import time
 
-import imp
-import wx
-import wx.html
-import wx.aui
-import wxmpl
-import wx.lib.agw.aui as aui
-import wx.propgrid as wxpg
-
-import libhooke as lh
-from config import config
-import drivers
-import plugins
-import hookecommands
-import hookeplaylist
-import hookepropertyeditor
-import hookeresults
-import playlist
-
-global __version__
-
-__version__ = lh.HOOKE_VERSION[0]
-__release_name__ = lh.HOOKE_VERSION[1]
-
-#TODO: order menu items, get rid of all unused IDs
-ID_ExportText = wx.NewId()
-ID_ExportImage = wx.NewId()
-ID_Config = wx.NewId()
-ID_About = wx.NewId()
-ID_Next = wx.NewId()
-ID_Previous = wx.NewId()
-
-ID_ViewAssistant = wx.NewId()
-ID_ViewCommands = wx.NewId()
-ID_ViewFolders = wx.NewId()
-ID_ViewOutput = wx.NewId()
-ID_ViewPlaylists = wx.NewId()
-ID_ViewProperties = wx.NewId()
-ID_ViewResults = wx.NewId()
-
-ID_CommandsList = wx.NewId()
-ID_CommandsListBox = wx.NewId()
-
-ID_TextContent = wx.NewId()
-ID_TreeContent = wx.NewId()
-ID_HTMLContent = wx.NewId()
-ID_SizeReportContent = wx.NewId()
-ID_DeletePerspective = wx.NewId()
-ID_SavePerspective = wx.NewId()
-
-ID_FirstPerspective = ID_SavePerspective + 1000
-#I hope we'll never have more than 1000 perspectives
-ID_FirstPlot = ID_SavePerspective + 2000
-
-class Hooke(wx.App):
-
-    def OnInit(self):
-        self.SetAppName('Hooke')
-        self.SetVendorName('')
-
-        #set the Hooke directory
-        lh.hookeDir = os.path.abspath(os.path.dirname(__file__))
-
-        windowPosition = (config['main']['left'], config['main']['top'])
-        windowSize = (config['main']['width'], config['main']['height'])
-
-        #setup the splashscreen
-        if config['splashscreen']['show']:
-            filename = lh.get_file_path('hooke.jpg', ['resources'])
-            if os.path.isfile(filename):
-                bitmap = wx.Image(filename).ConvertToBitmap()
-                splashStyle = wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT
-                splashDuration = config['splashscreen']['duration']
-                wx.SplashScreen(bitmap, splashStyle, splashDuration, None, -1)
-                wx.Yield()
-                '''
-                we need for the splash screen to disappear
-                for whatever reason splashDuration and sleep do not correspond to each other
-                at least not on Windows
-                maybe it's because duration is in milliseconds and sleep in seconds
-                thus we need to increase the sleep time a bit
-                a factor of 1.2 seems to work quite well
-                '''
-                sleepFactor = 1.2
-                time.sleep(sleepFactor * splashDuration / 1000)
-
-        plugin_objects = []
-        for plugin in config['plugins']:
-            if config['plugins'][plugin]:
-                filename = ''.join([plugin, '.py'])
-                path = lh.get_file_path(filename, ['plugins'])
-                if os.path.isfile(path):
-                    #get the corresponding filename and path
-                    plugin_name = ''.join(['plugins.', plugin])
-                    module = __import__(plugin_name)
-                    #get the file that contains the plugin
-                    class_file = getattr(plugins, plugin)
-                    #get the class that contains the commands
-                    class_object = getattr(class_file, plugin + 'Commands')
-                    plugin_objects.append(class_object)
+#import libhooke as lh
+#import drivers
+#import plugins
+#import hookecommands
+#import hookeplaylist
+#import hookepropertyeditor
+#import hookeresults
+#import playlist
+
+class Hooke(object):
+    def __init__(self, config=None, debug=0):
+        self.debug = debug
+        config_mod.DEFAULTS.extend(self.plugin_default_settings())
+        config_mod.DEFAULTS.extend(self.driver_default_settings())
+        if config == None:
+            config = config_mod.HookeConfigParser(
+                paths=config_mod.DEFAULT_PATHS,
+                default_settings=config_mod.DEFAULT_SETTINGS)
+            config.read_configs()
+        self.config = config
+        self.load_plugins()
+        self.load_drivers()
+
+    def plugin_default_settings(self):
+        pass
 
-        def make_command_class(*bases):
-            #create metaclass with plugins and plotmanipulators
-            return type(HookeFrame)("HookeFramePlugged", bases + (HookeFrame,), {})
-        frame = make_command_class(*plugin_objects)(parent=None, id=wx.ID_ANY, title='Hooke', pos=windowPosition, size=windowSize)
-        frame.Show(True)
-        self.SetTopWindow(frame)
+    def driver_default_settings(self):
+        pass
 
-        return True
+    def load_plugins(self):
+        pass
 
-    def OnExit(self):
-        #TODO: write values to ini file if necessary
-        return True
+    def load_drivers(self)
+        pass
 
+    def close(self):
+        if self.config.changed:
+            self.config.write() # Does not preserve original comments
 
 class HookeFrame(wx.Frame):
 
@@ -1229,34 +1155,8 @@ class HookeFrame(wx.Frame):
             self.AppendToOutput('Not able to plot.')
 
 
-ID_PaneBorderSize = wx.ID_HIGHEST + 1
-ID_SashSize = ID_PaneBorderSize + 1
-ID_CaptionSize = ID_PaneBorderSize + 2
-ID_BackgroundColor = ID_PaneBorderSize + 3
-ID_SashColor = ID_PaneBorderSize + 4
-ID_InactiveCaptionColor =  ID_PaneBorderSize + 5
-ID_InactiveCaptionGradientColor = ID_PaneBorderSize + 6
-ID_InactiveCaptionTextColor = ID_PaneBorderSize + 7
-ID_ActiveCaptionColor = ID_PaneBorderSize + 8
-ID_ActiveCaptionGradientColor = ID_PaneBorderSize + 9
-ID_ActiveCaptionTextColor = ID_PaneBorderSize + 10
-ID_BorderColor = ID_PaneBorderSize + 11
-ID_GripperColor = ID_PaneBorderSize + 12
-
-
-#----------------------------------------------------------------------
-
 if __name__ == '__main__':
-
-    ## now, silence a deprecation warning for py2.3
-    #import warnings
-    #warnings.filterwarnings("ignore", "integer", DeprecationWarning, "wxPython.gdi")
-
-    redirect=True
-    if __debug__:
-        redirect=False
-    app = Hooke(redirect=redirect)
-
+    app = Hooke(debug=__debug__)
     app.MainLoop()