From cdf893aca0b45f8a3c9b6e79f9c537b21edf3e04 Mon Sep 17 00:00:00 2001
From: Martijn Vermaat <martijn@vermaat.name>
Date: Mon, 22 Aug 2011 14:20:11 +0000
Subject: [PATCH] Support merging user configuration and system configuration.

mutalyzer/config.py:
- Try to load system configuration and user configuration, in that order, and
  merge them if we find both.

tests/test_website.py:
- Use running webserver to retrieve batch results.

extras/config.user.example:
- Example user configuration, overwriting some values from system
  configuration.


git-svn-id: https://humgenprojects.lumc.nl/svn/mutalyzer/branches/refactor-mutalyzer-branch@336 eb6bd6ab-9ccd-42b9-aceb-e2899b4a52f1
---
 README                      |   4 --
 extras/config.example       |   3 +-
 extras/config.user.example  |  11 ++++
 mutalyzer/config.py         | 114 +++++++++++++++---------------------
 mutalyzer/variantchecker.py |   1 -
 tests/test_website.py       |  28 ++++++---
 6 files changed, 79 insertions(+), 82 deletions(-)
 create mode 100644 extras/config.user.example

diff --git a/README b/README
index dc7d6ad1..5120e879 100644
--- a/README
+++ b/README
@@ -42,10 +42,6 @@ Development notes
 
 Todo list:
 - Improve the web interface design :)
-- Running Mutalyzer as a local user (either bin/mutalyzer-website.wsgi on port
-  8080, or bin/mutalyzer for example) should be able with a partly different
-  configuration. Expecially cache and log file locations, because they are
-  owned by www-data.
 - Test all uses of mkstemp().
 - Use naming conventions for modules Crossmap, Db, File, GenRecord, Mapper,
   Retriever, Scheduler.
diff --git a/extras/config.example b/extras/config.example
index 05e01241..bce2c1e4 100644
--- a/extras/config.example
+++ b/extras/config.example
@@ -1,8 +1,7 @@
 #
 # Mutalyzer config file.
 #
-# Copy this file to /etc/mutalyzer/config or ~/.config/mutalyzer/config and
-# modify to suit your preferences.
+# Copy this file to /etc/mutalyzer/config and modify to suit your preferences.
 
 #
 # These settings are used by the Retriever module.
diff --git a/extras/config.user.example b/extras/config.user.example
new file mode 100644
index 00000000..8abc160f
--- /dev/null
+++ b/extras/config.user.example
@@ -0,0 +1,11 @@
+#
+# Mutalyzer config file.
+#
+# Copy this file to ~/.config/mutalyzer/config to overwrite definitions from
+# /etc/mutalyzer.config.
+
+# The cache directory.
+cache = "/home/user/.cache/mutalyzer"
+
+# Name and location of the log file.
+log = "/tmp/mutalyzer.log"
diff --git a/mutalyzer/config.py b/mutalyzer/config.py
index 4f222c97..1f5b36cd 100644
--- a/mutalyzer/config.py
+++ b/mutalyzer/config.py
@@ -6,10 +6,14 @@ module.
 
 
 import os
-import tempfile
 from configobj import ConfigObj
 
-import mutalyzer
+
+SYSTEM_CONFIGURATION = '/etc/mutalyzer/config'
+USER_CONFIGURATION = os.path.join(
+    os.environ.get('XDG_CONFIG_HOME', None) or \
+    os.path.join(os.path.expanduser('~'), '.config'),
+    'mutalyzer', 'config')
 
 
 class ConfigurationError(Exception):
@@ -36,10 +40,16 @@ class Config():
         hard coded constant is used (the name and path to the configuration
         file).
 
-        The configuration file location is automatically detected, in the
-        following order:
-        1) $XDG_CONFIG_HOME/mutalyzer/config
-        2) /etc/mutalyzer/config
+        Configuration values are read from two locations, in this order:
+        1) /etc/mutalyzer/config
+        2) $XDG_CONFIG_HOME/mutalyzer/config
+
+        If both files exist, values defined in the second overwrite values
+        defined in the first.
+
+        An exception to this system is when the optional {filename} argument
+        is set. In that case, the locations listed above are ignored and the
+        configuration is read from {filename}.
 
         By the DRY-principle, we don't enumerate the configuration variables
         for each class in documentation. Instead, what variables are used by
@@ -55,32 +65,23 @@ class Config():
             - Supplied argument {filename} could not be opened.
             - Configuration file could not be parsed.
             - Not all variables are present in configuration file.
-
-        @todo: Be able to have /etc/mutalyzer/config as 'base' configuration
-            and some additional configuration in ~/.mutalyzer/config to
-            overwrite the base configuration.
-            For example, a normal user could use a different cache directory
-            (writable by the user) than the system wide Mutalyzer config.
-            We could use the 'merge' method from configobj.
         """
-        if filename is None:
-            base = os.environ.get('XDG_CONFIG_HOME', None)
-            if base is None:
-                base = os.path.join(os.path.expanduser('~'), '.config')
-            filename = os.path.join(base, 'mutalyzer', 'config')
-
-            if not os.path.isfile(filename):
-                filename = '/etc/mutalyzer/config'
-                if not os.path.isfile(filename):
-                    raise ConfigurationError('Could not locate configuration.')
-
-        try:
-            config = ConfigObj(filename)
-        except IOError:
-            raise ConfigurationError('Could not open configuration file: %s' \
-                                     % filename)
-        except SyntaxError:
-            raise ConfigurationError('Could not parse configuration file.')
+        config = None
+
+        if filename:
+            config = self._load_config(filename)
+        else:
+            if os.path.isfile(SYSTEM_CONFIGURATION):
+                config = self._load_config(SYSTEM_CONFIGURATION)
+            if os.path.isfile(USER_CONFIGURATION):
+                user_config = self._load_config(USER_CONFIGURATION)
+                if config:
+                    config.merge(user_config)
+                else:
+                    config = user_config
+
+        if not config:
+            raise ConfigurationError('Could not locate configuration.')
 
         try:
 
@@ -135,44 +136,21 @@ class Config():
             self.GenRecord.spliceAlarm = int(config["spliceAlarm"])
             self.GenRecord.spliceWarn = int(config["spliceWarn"])
 
-            # If we are in a testing environment, use a temporary file for
-            # logging and a temporary directory for the cache.
-            # We don't remove these after the tests, since they might be
-            # useful for debugging.
-            if mutalyzer.is_test():
-                # Todo:
-                #
-                # This needs some refactoring. The problem with the temporary
-                # file and dir names is that they will not be used by the
-                # (running) batch daemon, which will thus save its results to
-                # to 'normal' directory.
-                # Furthermore, subsequent web requests from a unit test will
-                # use different configuration instantiations, so might not
-                # see results from previous requests.
-                #
-                # We need a more robust solution for different configurations,
-                # depending of the running user/setting (e.g. unit tests).
-                #
-                # Idea: Don't create a local instance of the website in the
-                # unit tests, but only use running instances of all servers
-                # (website, webservice, batch daemon). They will use their
-                # own 'normal' configuration.
-                # All other parts of the unit tests will use temporary test
-                # configuration values. We might even decorate the tests
-                # needing server access as such and provide the option of
-                # skipping these.
-
-                #handle, filename = tempfile.mkstemp(suffix='.log',
-                #                                    prefix='mutalyzer-tests-')
-                #os.close(handle)
-                #self.Output.log = filename
-                #dirname = tempfile.mkdtemp(suffix='.cache',
-                #                           prefix='mutalyzer-tests-')
-                #self.Retriever.cache = dirname
-                #self.Scheduler.resultsDir = dirname
-                pass
-
         except KeyError as e:
             raise ConfigurationError('Missing configuration value: %s' % e)
     #__init__
+
+    def _load_config(self, filename):
+        """
+        Create a ConfigObj from the configuration in {filename}.
+        """
+        try:
+            return ConfigObj(filename)
+        except IOError:
+            raise ConfigurationError('Could not open configuration file: %s' \
+                                     % filename)
+        except SyntaxError:
+            raise ConfigurationError('Could not parse configuration file: %s' \
+                                     % filename)
+    #_load_config
 #Config
diff --git a/mutalyzer/variantchecker.py b/mutalyzer/variantchecker.py
index 12c4be66..5f4cb61e 100644
--- a/mutalyzer/variantchecker.py
+++ b/mutalyzer/variantchecker.py
@@ -20,7 +20,6 @@ from Bio.Seq import Seq
 from Bio.Alphabet import IUPAC
 
 from mutalyzer import util
-from mutalyzer.config import Config
 from mutalyzer.grammar import Grammar
 from mutalyzer.mutator import Mutator
 from mutalyzer import Retriever
diff --git a/tests/test_website.py b/tests/test_website.py
index 7d01e93d..7b3b0f01 100644
--- a/tests/test_website.py
+++ b/tests/test_website.py
@@ -14,6 +14,7 @@ I just installed webtest by 'easy_install webtest'.
 #import logging; logging.basicConfig()
 import os
 import re
+import urllib2
 import time
 import web
 from nose.tools import *
@@ -24,6 +25,9 @@ from mutalyzer import website
 from mutalyzer.util import slow
 
 
+BATCH_RESULT_URL = 'http://localhost/mutalyzer/Results_{id}.txt'
+
+
 class TestWSGI():
     """
     Test the Mutalyzer WSGI interface.
@@ -255,6 +259,14 @@ class TestWSGI():
 
         @return: The batch result document.
         @rtype: string
+
+        @note: Since the batch files are processed by a running batch daemon
+            process, the result gets written to the directory defined by the
+            system-wide configuration (e.g. /var/mutalyzer/cache), thus
+            inaccessible for the TestApp instance under our current user.
+            The 'solution' for this is to download the results via a running
+            webserver that should be using the same configuration as the batch
+            daemon. Yes, this is a hack.
         """
         r = self.app.get('/batch')
         form = r.forms[0]
@@ -275,17 +287,19 @@ class TestWSGI():
             assert re.match('[0-9]+', r.body)
             time.sleep(2)
         assert_equal(r.body, 'OK')
-        r = self.app.get('/Results_' + id + '.txt')
-        assert_equal(r.content_type, 'text/plain')
-        r.mustcontain(header)
+        # This is a hack to get to the batch results (see @note above).
+        response = urllib2.urlopen(BATCH_RESULT_URL.format(id=id))
+        assert_equal(response.info().getheader('Content-Type'), 'text/plain')
+        result = response.read()
+        assert header in result
         if not lines:
             lines = size
-        if len(r.body.strip().split('\n')) -1  != lines:
+        if len(result.strip().split('\n')) -1  != lines:
             # Heisenbug, whenever it occurs we want to see some info.
             print 'File: /Results_' + id + '.txt'
-            print r.body
-        assert_equal(len(r.body.strip().split('\n')) - 1, lines)
-        return r.body
+            print result
+        assert_equal(len(result.strip().split('\n')) - 1, lines)
+        return result
 
     def test_batch_namechecker(self):
         """
-- 
GitLab