Repository: CORE-GATECH-GROUP/serpent-tools (python)

Issue:
Update settings from config file
Currently, the settings loader has to take settings one at a time. For the sake of consistency and ease, it would be nice to have the `rc` settings loader take a file path argument and load all settings from there.

I think a `yaml` file would be pretty straight forward, since we already use the `pyyaml` module. The loader could be setup to read in a strict, full name manner,
```
depletion.metadataKeys: ['ZAI', 'NAMES']
depletion.materials: ['fuel*', 'bp1']
verbosity: 'error'
...
```
or with nested levels
```
depletion:
    metadataKeys: ['ZAI', 'NAMES']
    materials: ['fuel*', 'bp1']
verbosity: 'error'
...
```
The two implementations should have the same result, just that in the second case there is some reconstructing that has to be done behind the scenes.

Pull request description:
Update the UserSettingsLoader from yaml file
Closes #57 by reading in a yaml file containing potentially nested settings,
```
verbosity: warning
depletion:
  materials: [fuel*]
  metadataKeys: [NAMES, DAYS]
```
Additional unit tests added to test this capability. Also included a `strict` argument that,
if `False`, will alert the user that certain settings could not be loaded due to some error, bad type, 
not a setting, or not in defaults. These bad settings will be skipped and not loaded, with the intent
that the user can determine what settings went wrong.

Included a brief example in `examples/Settings.ipynb` as well.

Patch:
diff --git a/README.rst b/README.rst
index 5ab1e01..c7b11fb 100644
--- a/README.rst
+++ b/README.rst
@@ -44,9 +44,6 @@ References
 ----------
 
 The Annals of Nuclear Energy article should be cited for all work 
-using ``SERPENT``. If you wish to cite this project, please cite as
-
-.. code:: bibtex
 using ``SERPENT``. If you wish to cite this project, please cite as::
 
     url{@serpentTools
diff --git a/docs/about.rst b/docs/about.rst
deleted file mode 100644
index 7887051..0000000
--- a/docs/about.rst
+++ /dev/null
@@ -1,31 +0,0 @@
-
-.. include:: ../README.rst
-
-Installation
-------------
-
-The ``serpentTools`` package can be downloaded either as a git repository or
-as a zipped file. Both can be obtained through the ``Clone or download`` option
-at the 
-`serpent-tools GitHub <https://github.com/CORE-GATECH-GROUP/serpent-tools>`_.
-
-Once the repository has been downloaded or extracted from zip, the package 
-can be installed with::
-
-    cd serpentTools
-    python setup.py install
-    python setup.py test
-
-Installing with `setuptools <https://pypi.python.org/pypi/setuptools/38.2.4>`_
-is preferred over the standard ``distutils`` module. ``setuptools`` can be
-installed with ``pip`` as::
-
-    pip install -U setuptools
-
-Installing in this manner ensures that the supporting packages,
-like ``numpy`` are installed and up to date.
-
-License
--------
-
-.. include:: ../LICENSE.rst
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 2b3c86a..a1e4e1f 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -1,3 +1,4 @@
+.. _api:
 
 API
 ===
diff --git a/docs/conf.py b/docs/conf.py
index 0fcd4ae..12a4177 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -161,4 +161,4 @@ texinfo_documents = [
 ]
 
 # -- Options for auto documentation --------------------------------------
-autodoc_default_flags = ['members', 'inherited-members']
+autodoc_default_flags = ['members']
diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst
index a38d11c..dd142de 100644
--- a/docs/contributing/index.rst
+++ b/docs/contributing/index.rst
@@ -1,3 +1,4 @@
+.. _contributing:
 
 Contributing
 ============
diff --git a/docs/examples/Settings.rst b/docs/examples/Settings.rst
index 5e7ba9d..76ab757 100644
--- a/docs/examples/Settings.rst
+++ b/docs/examples/Settings.rst
@@ -25,9 +25,9 @@ Below are the default values for each setting available
 .. code:: ipython3
 
     >>> for setting in sorted(defaultSettings.keys()):
-    >>>     print(setting)
-    >>>     for key in defaultSettings[setting]:
-    >>>         print('\t', key, '-', defaultSettings[setting][key])
+    ...     print(setting)
+    ...     for key in defaultSettings[setting]:
+    ...         print('\t', key, '-', defaultSettings[setting][key])
     depletion.materialVariables
          default - []
          description - Names of variables to store. Empty list -> all variables.
@@ -83,14 +83,14 @@ correct type, and is an allowable option, if given.
 .. code:: ipython3
 
     >>> try:
-    >>>     rc['depletion.metadataKeys'] = False
-    >>> except TypeError as te:
-    >>>     print(te)
+    ...     rc['depletion.metadataKeys'] = False
+    ... except TypeError as te:
+    ...     print(te)
     Setting depletion.metadataKeys should be of type <class 'list'>, not <class 'bool'>
     >>> try:
-    >>>     rc['serpentVersion'] = '1.2.3'
-    >>> except KeyError as ke:
-    >>>     print(ke)
+    ...     rc['serpentVersion'] = '1.2.3'
+    ... except KeyError as ke:
+    ...     print(ke)
     "Setting serpentVersion is
     1.2.3
     and not one of the allowed options:
@@ -102,7 +102,7 @@ changes.
 .. code:: ipython3
 
     >>> with rc:
-    >>>     rc['depletion.metadataKeys'] = ['ZAI', 'BU']
+    ...     rc['depletion.metadataKeys'] = ['ZAI', 'BU']
     >>>
     >>> rc['depletion.metadataKeys']
     >>> rc['verbosity'] = 'info'
@@ -168,3 +168,28 @@ to obtain all the data present in their respective files.
 
 See the :ref:`branching-ex` example for more information on using these
 settings to control scraped data.
+
+.. _conf-files:
+
+Configuration Files
+-------------------
+
+As of version 0.1.2, the ``rc`` object allows for settings to be updated
+from a yaml configuration file using the
+:py:meth:`~serpentTools.settings.UserSettingsLoader.loadYaml` method.
+The file is structured with the names of settings as keys and the
+desired setting value as the values.
+The loader also attempts to expand nested settings, like reader-specific
+settings, that may be lumped in a second level.
+
+.. code:: yaml
+
+    verbosity: warning
+    xs.getInfXS: False
+    branching:
+        areUncsPresent: False
+        floatVariables: [Fhi, Blo]
+    depletion:
+        materials: [fuel*]
+        materialVariables:
+            [ADENS, MDENS, VOLUME]
diff --git a/docs/welcome/about.rst b/docs/welcome/about.rst
index 4d2c74b..e09c9c5 100644
--- a/docs/welcome/about.rst
+++ b/docs/welcome/about.rst
@@ -1,2 +1,71 @@
+=============
+serpent-tools
+=============
 
-.. include:: ../../README.rst
+.. image:: https://travis-ci.org/CORE-GATECH-GROUP/serpent-tools.svg?branch=master
+    :target: https://travis-ci.org/CORE-GATECH-GROUP/serpent-tools
+
+A suite of parsers designed to make interacting with
+``SERPENT`` [1]_ output files simple and flawless.
+
+The ``SERPENT`` Monte Carlo code
+is developed by VTT Technical Research Centre of Finland, Ltd.
+More information, including distribution and licensing of ``SERPENT`` can be
+found at `<montecarlo.vtt.fi>`_
+
+Installation
+------------
+
+The ``serpentTools`` package can be downloaded either as a git repository or
+as a zipped file. Both can be obtained through the ``Clone or download`` option
+at the 
+`serpent-tools GitHub <https://github.com/CORE-GATECH-GROUP/serpent-tools>`_.
+
+Once the repository has been downloaded or extracted from zip, the package 
+can be installed with::
+
+    cd serpentTools
+    python setup.py install
+    python setup.py test
+
+Installing with `setuptools <https://pypi.python.org/pypi/setuptools/38.2.4>`_
+is preferred over the standard ``distutils`` module. ``setuptools`` can be
+installed with ``pip`` as::
+
+    pip install -U setuptools
+
+Installing in this manner ensures that the supporting packages,
+like ``numpy`` are installed and up to date.
+
+Issues
+------
+
+If you have issues installing the project, find a bug, or want to add a feature,
+the `GitHub issue page <https://github.com/CORE-GATECH-GROUP/serpent-tools/issues>`_
+is the best place to do that.
+
+Contributors
+------------
+
+Here are all the wonderful people that helped make this project happen
+
+* `Andrew Johnson <https://github.com/drewejohnson>`_
+* `Dr. Dan Kotlyar <https://github.com/CORE-GATECH>`_
+* `Stefano Terlizzi <https://github.com/sallustius>`_
+
+References
+----------
+
+The Annals of Nuclear Energy article should be cited for all work
+using ``SERPENT``. If you wish to cite this project, please cite as::
+
+    url{@serpentTools
+        author = {Andrew Johnson and Dan Kotlyar},
+        title = {serpentTools: A suite of parsers designed to make interacting with SERPENT outputs simple and flawless},
+        url = {https://github.com/CORE-GATECH-GROUP/serpent-tools},
+        year = {2017}
+    }
+
+.. [1] Leppanen, J. et al. (2015) "The Serpent Monte Carlo code: Status,
+    development and applications in 2013." Ann. Nucl. Energy, `82 (2015) 142-150
+    <http://www.sciencedirect.com/science/article/pii/S0306454914004095>`_
diff --git a/examples/Settings.ipynb b/examples/Settings.ipynb
index d321137..05064f6 100644
--- a/examples/Settings.ipynb
+++ b/examples/Settings.ipynb
@@ -25,9 +25,17 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 1,
    "metadata": {},
-   "outputs": [],
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "INFO    : serpentTools: Using version 1.0b0+34.gce072dd.dirty\n"
+     ]
+    }
+   ],
    "source": [
     "import serpentTools\n",
     "from serpentTools.settings import rc, defaultSettings"
@@ -42,7 +50,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [
     {
@@ -51,60 +59,60 @@
      "text": [
       "branching.areUncsPresent\n",
       "\t default - False\n",
-      "\t type - <class 'bool'>\n",
       "\t description - True if the values in the .coe file contain uncertainties\n",
+      "\t type - <class 'bool'>\n",
       "branching.floatVariables\n",
       "\t default - []\n",
-      "\t description - Names of state data variables to convert to floats for each branch\n",
       "\t type - <class 'list'>\n",
+      "\t description - Names of state data variables to convert to floats for each branch\n",
       "branching.intVariables\n",
       "\t default - []\n",
-      "\t description - Name of state data variables to convert to integers for each branch\n",
       "\t type - <class 'list'>\n",
+      "\t description - Name of state data variables to convert to integers for each branch\n",
       "depletion.materialVariables\n",
       "\t default - []\n",
-      "\t description - Names of variables to store. Empty list -> all variables.\n",
       "\t type - <class 'list'>\n",
+      "\t description - Names of variables to store. Empty list -> all variables.\n",
       "depletion.materials\n",
       "\t default - []\n",
-      "\t description - Names of materials to store. Empty list -> all materials.\n",
       "\t type - <class 'list'>\n",
+      "\t description - Names of materials to store. Empty list -> all materials.\n",
       "depletion.metadataKeys\n",
-      "\t default - ['ZAI', 'NAMES', 'DAYS', 'BU']\n",
       "\t options - default\n",
-      "\t description - Non-material data to store, i.e. zai, isotope names, burnup schedule, etc.\n",
+      "\t default - ['ZAI', 'NAMES', 'DAYS', 'BU']\n",
       "\t type - <class 'list'>\n",
+      "\t description - Non-material data to store, i.e. zai, isotope names, burnup schedule, etc.\n",
       "depletion.processTotal\n",
       "\t default - True\n",
-      "\t description - Option to store the depletion data from the TOT block\n",
       "\t type - <class 'bool'>\n",
+      "\t description - Option to store the depletion data from the TOT block\n",
       "serpentVersion\n",
-      "\t default - 2.1.29\n",
       "\t options - ['2.1.29']\n",
-      "\t description - Version of SERPENT\n",
+      "\t default - 2.1.29\n",
       "\t type - <class 'str'>\n",
+      "\t description - Version of SERPENT\n",
       "verbosity\n",
-      "\t default - info\n",
       "\t options - ['critical', 'error', 'warning', 'info', 'debug']\n",
-      "\t type - <class 'str'>\n",
+      "\t default - info\n",
+      "\t updater - <function updateLevel at 0x7f32b2e87048>\n",
       "\t description - Set the level of errors to be shown.\n",
-      "\t updater - <function updateLevel at 0x00000251B54FD0D0>\n",
+      "\t type - <class 'str'>\n",
       "xs.getB1XS\n",
       "\t default - True\n",
-      "\t description - If true, store the critical leakage cross sections.\n",
       "\t type - <class 'bool'>\n",
+      "\t description - If true, store the critical leakage cross sections.\n",
       "xs.getInfXS\n",
       "\t default - True\n",
-      "\t description - If true, store the infinite medium cross sections.\n",
       "\t type - <class 'bool'>\n",
+      "\t description - If true, store the infinite medium cross sections.\n",
       "xs.variableExtras\n",
       "\t default - []\n",
-      "\t description - Full SERPENT name of variables to be read\n",
       "\t type - <class 'list'>\n",
+      "\t description - Full SERPENT name of variables to be read\n",
       "xs.variableGroups\n",
       "\t default - []\n",
-      "\t description - Name of variable groups from variables.yaml to be expanded into SERPENT variable to be stored\n",
-      "\t type - <class 'list'>\n"
+      "\t type - <class 'list'>\n",
+      "\t description - Name of variable groups from variables.yaml to be expanded into SERPENT variable to be stored\n"
      ]
     }
    ],
@@ -124,7 +132,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
@@ -148,7 +156,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [
     {
@@ -168,7 +176,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
@@ -195,7 +203,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
@@ -236,7 +244,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [
     {
@@ -245,7 +253,7 @@
        "'2.1.29'"
       ]
      },
-     "execution_count": 16,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -256,7 +264,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [
     {
@@ -283,10 +291,8 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
-   "metadata": {
-    "collapsed": true
-   },
+   "execution_count": 9,
+   "metadata": {},
    "outputs": [],
    "source": [
     "assert 'INF_SCATT3' not in varSet"
@@ -305,6 +311,50 @@
    "source": [
     "See the `BrancingReader` example for more information on using these settings to control scraped data."
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Configuration files\n",
+    "As of version 0.1.2, the `rc` object allows for settings to be updated from a yaml configuration file using the `loadYaml` method. The file contains setting names as keys with the desired variables as values, as\n",
+    "```\n",
+    "verbosity: warning\n",
+    "xs.getInfXS: False\n",
+    "```\n",
+    "However, the loader can also expand a nested dictionary structure, as\n",
+    "```\n",
+    "branching:\n",
+    "  areUncsPresent: False\n",
+    "  floatVariables: [Fhi, Blo]\n",
+    "depletion:\n",
+    "  materials: [fuel*]\n",
+    "  materialVariables:\n",
+    "    [ADENS, MDENS, VOLUME]\n",
+    "```"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "False"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "myConf = 'myConfig.yaml'\n",
+    "rc.loadYaml(myConf)\n",
+    "rc['branching.areUncsPresent']"
+   ]
   }
  ],
  "metadata": {
@@ -323,7 +373,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.6.1"
+   "version": "3.5.2"
   }
  },
  "nbformat": 4,
diff --git a/examples/myConfig.yaml b/examples/myConfig.yaml
new file mode 100644
index 0000000..8953d66
--- /dev/null
+++ b/examples/myConfig.yaml
@@ -0,0 +1,12 @@
+xs.getInfXS: False
+xs.getB1XS: True
+xs.variableGroups: [gc-meta, kinetics, xs]
+branching:
+  areUncsPresent: False
+  floatVariables: [Fhi, Blo]
+depletion:
+  materials: [fuel*]
+  metadataKeys: [NAMES, BU]
+  materialVariables:
+    [ADENS, MDENS, VOLUME]
+serpentVersion: 2.1.29
diff --git a/serpentTools/settings.py b/serpentTools/settings.py
index 0f2ae8f..a8d9ae5 100644
--- a/serpentTools/settings.py
+++ b/serpentTools/settings.py
@@ -1,5 +1,6 @@
 """Settings to yield control to the user."""
 import os
+import six
 
 import yaml
 
@@ -151,7 +152,7 @@ class DefaultSettingsLoader(dict):
         for name, value in defaultSettings.items():
             if 'options' in value:
                 options = (value['default'] if value['options'] == 'default'
-                           else value['options'])
+                else value['options'])
             else:
                 options = None
             settingsOptions = {'name': name,
@@ -272,7 +273,7 @@ class UserSettingsLoader(dict):
         """
         settings = {}
         settingsPreffix = ([settingsPreffix] if isinstance(settingsPreffix, str)
-                           else settingsPreffix)
+        else settingsPreffix)
         for setting, value in self.items():
             settingPath = setting.split('.')
             if settingPath[0] in settingsPreffix:
@@ -314,5 +315,60 @@ class UserSettingsLoader(dict):
                 variables.update(baseGroups[key])
         return variables
 
+    def loadYaml(self, filePath, strict=True):
+        """
+        Update the settings based on the contents of the yaml file
+
+        .. versionadded:: 0.2.0
+
+        Parameters
+        ----------
+        filePath: str
+            Path to config file
+        strict: bool
+            Fail at the first incorrect setting. If false, failed settings
+            will not be loaded and alerts will be raised
+
+        Raises
+        ------
+        KeyError or TypeError
+            If settings found in the config file are not
+            valid
+        FileNotFound or OSError
+            If the file does not exist
+
+        """
+        messages.debug('Attempting to read from {}'.format(filePath))
+        with open(filePath) as yFile:
+            l = yaml.safe_load(yFile)
+        messages.info('Loading settings onto object with strict:{}'
+                      .format(strict))
+
+        for key, value in six.iteritems(l):
+            if isinstance(value, dict):
+                self.__recursiveLoad(value, strict, key)
+            else:
+                self.__safeLoad(key, value, strict)
+        messages.info('Done')
+
+    def __recursiveLoad(self, curLevel, strict, preset):
+        for nextLevelKey, nextLevel in six.iteritems(curLevel):
+            newSettingName = preset + '.' + nextLevelKey
+            if isinstance(nextLevel, dict):
+                self.__recursiveLoad(nextLevel, strict, newSettingName)
+            else:
+                self.__safeLoad(newSettingName, nextLevel, strict)
+
+    def __safeLoad(self, key, value, strict):
+        messages.debug('Attempting to set setting {} to {}'
+                       .format(key, value))
+        try:
+            self.setValue(key, value)
+        except (KeyError, TypeError) as error:
+            if strict:
+                raise error
+            else:
+                messages.error(str(error))
+
 
 rc = UserSettingsLoader()


Test patch:
diff --git a/serpentTools/tests/test_settings.py b/serpentTools/tests/test_settings.py
index de3b43d..b6f8e50 100644
--- a/serpentTools/tests/test_settings.py
+++ b/serpentTools/tests/test_settings.py
@@ -1,133 +1,204 @@
-"""Tests for the settings loaders."""
-import warnings
-import unittest
-
-from serpentTools import settings
-from serpentTools.messages import deprecated, willChange
-
-class DefaultSettingsTester(unittest.TestCase):
-    """Class to test the functionality of the master loader."""
-
-    @classmethod
-    def setUpClass(cls):
-        cls.defaultLoader = settings.DefaultSettingsLoader()
-        cls.testSetting = 'depletion.metadataKeys'
-        cls.testSettingExpected = ['ZAI', 'NAMES', 'DAYS', 'BU']
-        cls.testSettingMethod = cls.assertListEqual
-
-    def test_getDefault(self):
-        """Verify the default settings loader properly retrives defaults."""
-        self.testSettingMethod(self._getLoaderSetting(self.testSetting),
-                               self.testSettingExpected)
-
-    def test_cannotChangeDefaults(self):
-        """Verify the default settings loader is locked after creation."""
-        with self.assertRaises(KeyError):
-            self.defaultLoader['this'] = 'should fail'
-
-    def _getLoaderSetting(self, setting):
-        return self.defaultLoader[setting].default
-
-
-class RCTester(unittest.TestCase):
-    """Class to test the functionality of the scriptable settings manager."""
-
-    @classmethod
-    def setUpClass(cls):
-        cls.rc = settings.UserSettingsLoader()
-        cls.rc['depletion.metadataKeys'] = ['ZAI']
-
-    def test_failAtNonexistentSetting(self):
-        """Verify that the loader will not load a nonexistent setting."""
-        with self.assertRaises(KeyError):
-            self.rc['bad setting'] = False
-
-    def test_failAtBadSetting_options(self):
-        """Verify that the loader will raise an error for bad options."""
-        with self.assertRaises(KeyError):
-            self.rc['depletion.metadata'] = ['this should fail']
-
-    def test_failAtBadSettings_type(self):
-        """Verify that the loader will raise an error for bad type."""
-        with self.assertRaises(TypeError):
-            self.rc['depletion.processTotal'] = 'this should fail'
-
-    def test_returnReaderSettings(self):
-        """Verify the correct reader settings can be retrieved."""
-        readerName = 'depletion'
-        expected = {
-            'metadataKeys': ['ZAI'],
-            'materialVariables': [],
-            'materials': [],
-            'processTotal': True,
-        }
-        actual = self.rc.getReaderSettings(readerName)
-        self.assertDictEqual(expected, actual)
-
-    def test_readerWithUpdatedSettings(self):
-        """Verify the settings passed to the reader reflect the update."""
-        from serpentTools.parsers.depletion import DepletionReader
-        with settings.rc as tempRC:
-            tempRC['depletion.metadataKeys'] = ['ZAI']
-            reader = DepletionReader('None')
-        self.assertTrue(reader.settings['metadataKeys'] == ['ZAI'])
-
-    def test_expandExtras(self):
-        """Verify that a set of extras is given if that is the only argument."""
-        extras = ['hello', 'testing']
-        with self.rc as tempRC:
-            tempRC['xs.variableExtras'] = extras
-            expected = set(extras)
-            actual = tempRC.expandVariables()
-        self.assertSetEqual(expected, actual)
-
-    def test_fullExtend(self):
-        """Verify that the variable expansion return the correct variables"""
-        groupNames = ['times', 'diffusion']
-        extras = ['hello']
-        expected = {'CMM_TRANSPXS', 'CMM_TRANSPXS_X', 'CMM_TRANSPXS_Y',
-                    'CMM_TRANSPXS_Z', 'CMM_DIFFCOEF', 'CMM_DIFFCOEF_X',
-                    'CMM_DIFFCOEF_Y', 'CMM_DIFFCOEF_Z', 'hello',
-                    'TOT_CPU_TIME', 'RUNNING_TIME', 'INIT_TIME', 'PROCESS_TIME',
-                    'TRANSPORT_CYCLE_TIME', 'BURNUP_CYCLE_TIME',
-                    'BATEMAN_SOLUTION_TIME', 'MPI_OVERHEAD_TIME',
-                    'ESTIMATED_RUNNING_TIME', 'CPU_USAGE',
-                    'TRANSPORT_CPU_USAGE', 'OMP_PARALLEL_FRAC'}
-        with self.rc as tempRC:
-            tempRC['xs.variableExtras'] = extras
-            tempRC['xs.variableGroups'] = groupNames
-            actual = tempRC.expandVariables()
-        self.assertSetEqual(expected, actual)
-
-
-class MessagingTester(unittest.TestCase):
-    """Class to test the messaging framework."""
-
-    def test_futureDecorator(self):
-        """Verify that the future decorator doesn't break"""
-
-        @willChange('This function will be updated in the future, '
-                    'but will still exist')
-        def demoFuture(x, val=5):
-            return x + val
-
-        with warnings.catch_warnings(record=True) as record:
-            self.assertEqual(7, demoFuture(2))
-            self.assertEqual(7, demoFuture(2, 5))
-            self.assertEquals(len(record), 2, 'Did not catch two warnings::willChange')
-
-    def test_depreciatedDecorator(self):
-        """Verify that the depreciated decorator doesn't break things"""
-
-        @deprecated('this nonexistent function')
-        def demoFunction(x, val=5):
-            return x + val
-
-        with warnings.catch_warnings(record=True) as record:
-            self.assertEqual(7, demoFunction(2))
-            self.assertEqual(7, demoFunction(2, 5))
-            self.assertEquals(len(record), 2, 'Did not catch two warnings::deprecation')
-
-
-if __name__ == '__main__':
-    unittest.main()
+"""Tests for the settings loaders."""
+from os.path import join
+from os import remove
+import warnings
+import unittest
+
+import yaml
+import six
+
+from serpentTools import settings
+from serpentTools.messages import deprecated, willChange
+from serpentTools.tests import TEST_ROOT
+
+
+class DefaultSettingsTester(unittest.TestCase):
+    """Class to test the functionality of the master loader."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.defaultLoader = settings.DefaultSettingsLoader()
+        cls.testSetting = 'depletion.metadataKeys'
+        cls.testSettingExpected = ['ZAI', 'NAMES', 'DAYS', 'BU']
+        cls.testSettingMethod = cls.assertListEqual
+
+    def test_getDefault(self):
+        """Verify the default settings loader properly retrives defaults."""
+        self.testSettingMethod(self._getLoaderSetting(self.testSetting),
+                               self.testSettingExpected)
+
+    def test_cannotChangeDefaults(self):
+        """Verify the default settings loader is locked after creation."""
+        with self.assertRaises(KeyError):
+            self.defaultLoader['this'] = 'should fail'
+
+    def _getLoaderSetting(self, setting):
+        return self.defaultLoader[setting].default
+
+
+class RCTester(unittest.TestCase):
+    """Class to test the functionality of the scriptable settings manager."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.rc = settings.UserSettingsLoader()
+        cls.rc['depletion.metadataKeys'] = ['ZAI']
+
+    def test_failAtNonexistentSetting(self):
+        """Verify that the loader will not load a nonexistent setting."""
+        with self.assertRaises(KeyError):
+            self.rc['bad setting'] = False
+
+    def test_failAtBadSetting_options(self):
+        """Verify that the loader will raise an error for bad options."""
+        with self.assertRaises(KeyError):
+            self.rc['depletion.metadata'] = ['this should fail']
+
+    def test_failAtBadSettings_type(self):
+        """Verify that the loader will raise an error for bad type."""
+        with self.assertRaises(TypeError):
+            self.rc['depletion.processTotal'] = 'this should fail'
+
+    def test_returnReaderSettings(self):
+        """Verify the correct reader settings can be retrieved."""
+        readerName = 'depletion'
+        expected = {
+            'metadataKeys': ['ZAI'],
+            'materialVariables': [],
+            'materials': [],
+            'processTotal': True,
+        }
+        actual = self.rc.getReaderSettings(readerName)
+        self.assertDictEqual(expected, actual)
+
+    def test_readerWithUpdatedSettings(self):
+        """Verify the settings passed to the reader reflect the update."""
+        from serpentTools.parsers.depletion import DepletionReader
+        with settings.rc as tempRC:
+            tempRC['depletion.metadataKeys'] = ['ZAI']
+            reader = DepletionReader('None')
+        self.assertTrue(reader.settings['metadataKeys'] == ['ZAI'])
+
+    def test_expandExtras(self):
+        """Verify that a set of extras is given if that is the only argument."""
+        extras = ['hello', 'testing']
+        with self.rc as tempRC:
+            tempRC['xs.variableExtras'] = extras
+            expected = set(extras)
+            actual = tempRC.expandVariables()
+        self.assertSetEqual(expected, actual)
+
+    def test_fullExtend(self):
+        """Verify that the variable expansion return the correct variables"""
+        groupNames = ['times', 'diffusion']
+        extras = ['hello']
+        expected = {'CMM_TRANSPXS', 'CMM_TRANSPXS_X', 'CMM_TRANSPXS_Y',
+                    'CMM_TRANSPXS_Z', 'CMM_DIFFCOEF', 'CMM_DIFFCOEF_X',
+                    'CMM_DIFFCOEF_Y', 'CMM_DIFFCOEF_Z', 'hello',
+                    'TOT_CPU_TIME', 'RUNNING_TIME', 'INIT_TIME', 'PROCESS_TIME',
+                    'TRANSPORT_CYCLE_TIME', 'BURNUP_CYCLE_TIME',
+                    'BATEMAN_SOLUTION_TIME', 'MPI_OVERHEAD_TIME',
+                    'ESTIMATED_RUNNING_TIME', 'CPU_USAGE',
+                    'TRANSPORT_CPU_USAGE', 'OMP_PARALLEL_FRAC'}
+        with self.rc as tempRC:
+            tempRC['xs.variableExtras'] = extras
+            tempRC['xs.variableGroups'] = groupNames
+            actual = tempRC.expandVariables()
+        self.assertSetEqual(expected, actual)
+
+
+class ConfigLoaderTester(unittest.TestCase):
+    """Class to test loading multiple setttings at once, i.e. config files"""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.configSettings = {
+            'branching.areUncsPresent': True,
+            'branching.floatVariables': ['Bhi', 'Tlo'],
+            'verbosity': 'warning',
+            'depletion.materials': ['fuel*'],
+            'depletion.materialVariables': ['ADENS', 'MDENS']
+        }
+        cls.nestedSettings = {
+            'branching': {
+                'areUncsPresent':
+                    cls.configSettings['branching.areUncsPresent'],
+                'floatVariables':
+                    cls.configSettings['branching.floatVariables']
+            },
+            'depletion': {
+                'materials': cls.configSettings['depletion.materials'],
+                'materialVariables':
+                    cls.configSettings['depletion.materialVariables']
+            },
+            'verbosity': cls.configSettings['verbosity']
+        }
+        cls.files = {'singleLevel': join(TEST_ROOT, 'singleLevelConf.yaml'),
+                     'nested': join(TEST_ROOT, 'nestedConf.yaml'),
+                     'badNested': join(TEST_ROOT, 'badNestedConf.yaml')}
+        cls.rc = settings.UserSettingsLoader()
+
+    def _writeTestRemoveConfFile(self, settings, filePath, expected, strict):
+        with open(filePath, 'w') as out:
+            yaml.dump(settings, out)
+        with self.rc:
+            self.rc.loadYaml(filePath, strict)
+            for key, value in six.iteritems(expected):
+                if isinstance(value, list):
+                    self.assertListEqual(value, self.rc[key])
+                else:
+                    self.assertEqual(value, self.rc[key])
+        remove(filePath)
+
+    def test_loadSingleLevelConfig(self):
+        """Test loading settings from a non-nested config file"""
+        self._writeTestRemoveConfFile(self.configSettings,
+                                      self.files['singleLevel'],
+self.configSettings, True)
+
+    def test_loadNestedConfig(self):
+        """Test loading settings from a nested config file"""
+        self._writeTestRemoveConfFile(self.nestedSettings, self.files['nested'],
+                                      self.configSettings, True)
+
+    def test_loadNestedNonStrict(self):
+        """Test loading settings with errors but non-strict error handling"""
+        badSettings = {'bad setting': False}
+        badSettings.update(self.nestedSettings)
+        self._writeTestRemoveConfFile(badSettings, self.files['nested'],
+                                      self.configSettings, False)
+
+
+class MessagingTester(unittest.TestCase):
+    """Class to test the messaging framework."""
+
+    def test_futureDecorator(self):
+        """Verify that the future decorator doesn't break"""
+
+        @willChange('This function will be updated in the future, '
+                    'but will still exist')
+        def demoFuture(x, val=5):
+            return x + val
+
+        with warnings.catch_warnings(record=True) as record:
+            self.assertEqual(7, demoFuture(2))
+            self.assertEqual(7, demoFuture(2, 5))
+            self.assertEquals(len(record), 2,
+                              'Did not catch two warnings::willChange')
+
+    def test_depreciatedDecorator(self):
+        """Verify that the depreciated decorator doesn't break things"""
+
+        @deprecated('this nonexistent function')
+        def demoFunction(x, val=5):
+            return x + val
+
+        with warnings.catch_warnings(record=True) as record:
+            self.assertEqual(7, demoFunction(2))
+            self.assertEqual(7, demoFunction(2, 5))
+            self.assertEquals(len(record), 2,
+                              'Did not catch two warnings::deprecation')
+
+
+if __name__ == '__main__':
+    unittest.main()