Repository: CORE-GATECH-GROUP/serpent-tools
Pull request #1 (2017-09-15 17:16:49)

Depletion reader and project settings
## Project Settings

A `rc` class has been created for handling settings.
This object will be used by readers to relay specific settings, and eventually control verbosity and other settings shared by all readers.

The `rc` object is aware of all acceptable settings, and their default values/data types. 
This behavior is forbidden, and appropriate errors are raised.

## `DepletionReader` 
This class takes a `dep.m` file as an input, and can obtain the depleted materials as requested by the user.
The reader can also store metadata like isotope names [U-235 or 922350 formats] and burnup schedule.

Each depleted material can store variables like burnup, atomic density, toxicities, and more, as requested by the users.


Patch:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..19f06e3
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: python
+python:
+  - "3.6"
+  - "3.5"
+
+# install
+install:
+  - python setup.py install
+
+# run
+script: pytest
diff --git a/LICENSE b/LICENSE.md
similarity index 100%
rename from LICENSE
rename to LICENSE.md
diff --git a/README.md b/README.md
index 5e25120..08a9066 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,4 @@
 # serpent-tools
+[![Build Status](https://travis-ci.org/CORE-GATECH-GROUP/serpent-tools.svg)](https://travis-ci.org/CORE-GATECH-GROUP/serpent-tools)
+
 A suite of parsers designed to make interacting with SERPENT output files simple and flawless
diff --git a/serpentTools/__init__.py b/serpentTools/__init__.py
new file mode 100644
index 0000000..6631e0a
--- /dev/null
+++ b/serpentTools/__init__.py
@@ -0,0 +1,20 @@
+from serpentTools import settings
+from serpentTools import parsers
+
+__version__ = '0.1.0'
+
+# List TODOS/feature requests here for now
+# Messages/Errors
+# TODO: Add verbosity control
+# TODO: Add specific exceptions and warnings
+# TODO: Add logging module to help with warnings/exceptions/info
+# Compatability
+# TODO: Python 2 support
+# TODO: Test compatability with earlier numpy releases
+# Pythonicness/cleanliness
+# TODO: Better rc importing and usage
+# TODO: Remove need to specify all settings as a default setting object
+# Usage/scripting
+# TODO: Update rc with dictionary
+# TODO: Update rc with yaml file into dictionary
+# TODO: Capture materials with underscores for depletion
diff --git a/serpentTools/objects/__init__.py b/serpentTools/objects/__init__.py
new file mode 100644
index 0000000..68d7181
--- /dev/null
+++ b/serpentTools/objects/__init__.py
@@ -0,0 +1,221 @@
+"""Objects used to support the parsing."""
+
+import numpy
+from matplotlib import pyplot
+
+
+class _SupportingObject(object):
+    """
+    Base supporting object.
+
+    Parameters
+    ----------
+    container: Some parser from serpentTools.parsers
+        Container that created this object
+    name: str
+        Name of this specific object, e.g. material name, detector name, etc.
+
+    """
+
+    def __init__(self, container, name):
+        self._container = container
+        self.name = name
+        self._filePath = container.filePath
+
+    def __repr__(self):
+        return '<{} {} from {}>'.format(self.whatAmI(),
+                                        self.name, self._filePath)
+
+    def whatAmI(self):
+        return type(self).__name__
+
+    def __getattr__(self, item):
+        """Search for the item in the containers metadata."""
+        if item in self._container.metadata:
+            return self._container.metadata[item]
+        raise AttributeError('{} has object has no attribute \'{}\''
+                             .format(self, item))
+
+    @staticmethod
+    def _convertVariableName(variable):
+        """Converta a SERPENT variable to camelCase."""
+        lowerSplits = [item.lower() for item in variable.split('_')]
+        if len(lowerSplits) == 1:
+            return lowerSplits[0]
+        else:
+            return lowerSplits[0] + ''.join([item.capitalize()
+                                             for item in lowerSplits[1:]])
+
+
+class DepletedMaterial(_SupportingObject):
+    """Class for storing material data from ``_dep.m`` files.
+
+    Parameters
+    ----------
+    parser: :py:class:`~serpentTools.parsers.depletion.DepletionReader`
+        Parser that found this material.
+        Used to obtain file metadata like isotope names and burnup
+    name: str
+        Name of this material
+
+    Attributes
+    ----------
+    zai: numpy.array
+        Isotope id's
+    names: numpy.array
+        Names of isotopes
+    days: numpy.array
+        Days overwhich the material was depleted
+    adens: numpy.array
+        Atomic density over time for each nuclide
+
+    :note:
+
+        These attributes only exist if the pasers was instructed to
+        read in this data. I.e. if ``readers.depletion.metadataKeys``
+        does not contain ``ZAI``, then this object will not have
+        the ``zai`` data.
+
+    """
+
+    def __init__(self, parser, name):
+        _SupportingObject.__init__(self, parser, name)
+        self._varData = {}
+
+    def __getattr__(self, item):
+        """
+        Allows the user to get items like ``zai`` and ``adens``
+        with ``self.zai`` and ``self.adens``, respectively.
+        """
+        if item in self._varData:
+            return self._varData[item]
+        return _SupportingObject.__getattr__(self, item)
+
+    def __getitem__(self, item):
+        if item not in self._varData:
+            if item not in self._container.metadata:
+                raise KeyError('{} has no item {}'.format(self, item))
+            return self._container.metadata[item]
+        return self._varData[item]
+
+    def addData(self, variable, rawData):
+        """Add data straight from the file onto a variable.
+
+        Parameters
+        ----------
+        variable: str
+            Name of the variable directly from ``SERPENT``
+        rawData: list
+            List of strings corresponding to the raw data from the file
+        """
+        newName = self._convertVariableName(variable)
+        if isinstance(rawData, str):
+            scratch = [float(item) for item in rawData.split()]
+        else:
+            scratch = []
+            for line in rawData:
+                if line:
+                    scratch.append([float(item) for item in line.split()])
+        self._varData[newName] = numpy.array(scratch)
+
+    def getXY(self, xUnits, yUnits, timePoints=None, names=None):
+        """Return x values for given time, and corresponding isotope values.
+
+        Parameters
+        ----------
+        xUnits: str
+            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
+        yUnits: str
+            name of y value to return, e.g. ``'adens'``, ``'burnup'``
+        timePoints: list or None
+            If given, select the time points according to those specified here.
+            Otherwise, select all points
+        names: list or None
+            If given, return y values corresponding to these isotope names.
+            Otherwise, return values for all isotopes.
+
+        Returns
+        -------
+        numpy.array
+            Vector of time points
+        numpy.array
+            Array of values
+
+        Raises
+        ------
+        AttributeError
+            If the names of the isotopes have not been obtained and specific
+            isotopes have been requested
+        """
+        if names and 'names' not in self._container.metadata:
+            raise AttributeError('Parser {} has not stored the isotope names.'
+                                 .format(self._container))
+        xVals, colIndices = self._getXSlice(xUnits, timePoints)
+        rowIndices = self._getIsoID(names)
+        allY = self[yUnits]
+        if allY.shape[0] == 1 or len(allY.shape) == 1:  # vector
+            return xVals, allY[colIndices] if colIndices else allY
+        yVals = numpy.empty((len(rowIndices), len(xVals)), dtype=float)
+        for isoID, rowId in enumerate(rowIndices):
+            yVals[isoID, :] = (allY[rowId][colIndices] if colIndices
+                               else allY[rowId][:])
+        return xVals, yVals
+
+    def _getXSlice(self, xUnits, timePoints):
+        allX = self[xUnits]
+        if timePoints is not None:
+            colIndices = [indx for indx, xx in enumerate(allX)
+                          if xx in timePoints]
+            xVals = allX[colIndices]
+        else:
+            colIndices = None
+            xVals = allX
+        return xVals, colIndices
+
+    def _getIsoID(self, isotopes):
+        """Return the row indices that correspond to specfic isotopes."""
+        # TODO: List comprehension to make rowIDs then return array
+        if not isotopes:
+            return numpy.array(list(range(len(self.names))), dtype=int)
+        isoList = [isotopes] if isinstance(isotopes, (str, int)) else isotopes
+        rowIDs = numpy.empty_like(isoList, dtype=int)
+        for indx, isotope in enumerate(isoList):
+            rowIDs[indx] = self.names.index(isotope)
+        return rowIDs
+
+    def plot(self, xUnits, yUnits, timePoints=None, names=None, ax=None):
+        """Plot some data as a function of time for some or all isotopes.
+
+        Parameters
+        ----------
+        xUnits: str
+            name of x value to obtain, e.g. ``'days'``, ``'burnup'``
+        yUnits: str
+            name of y value to return, e.g. ``'adens'``, ``'burnup'``
+        timePoints: list or None
+            If given, select the time points according to those
+            specified here. Otherwise, select all points
+        names: list or None
+            If given, return y values corresponding to these isotope
+            names. Otherwise, return values for all isotopes.
+        ax: None or ``matplotlib axes``
+            If given, add the data to this plot.
+            Otherwise, create a new plot
+
+        Returns
+        -------
+        ``matplotlib axes``
+            Axes corresponding to the figure that was plotted
+
+        See Also
+        --------
+        getXY
+
+        """
+        xVals, yVals = self.getXY(xUnits, yUnits, timePoints, names)
+        ax = ax or pyplot.subplots(1, 1)[1]
+        labels = names or [None]
+        for row in range(yVals.shape[0]):
+            ax.plot(xVals, yVals[row], label=labels[row])
+
+        return ax
diff --git a/serpentTools/objects/readers.py b/serpentTools/objects/readers.py
new file mode 100644
index 0000000..d133884
--- /dev/null
+++ b/serpentTools/objects/readers.py
@@ -0,0 +1,42 @@
+from serpentTools.settings import rc
+
+
+class BaseReader(object):
+    """Parent class from which all parsers will inherit.
+
+    Parameters
+    ----------
+    filePath: str
+        path pointing towards the file to be read
+    readerSettingsLevel: str
+        type of reader. Determines which settings to obtain
+    """
+
+    def __init__(self, filePath, readerSettingsLevel):
+        self.filePath = filePath
+        self.metadata = {}
+        self.settings = rc.getReaderSettings(readerSettingsLevel)
+
+    def __repr__(self):
+        return '<{} reading {}>'.format(self.__class__.__name__, self.filePath)
+
+    def read(self):
+        """Read the file and store the data.
+
+        :note:
+
+          This must be overwritten by each subclass of parser
+
+        """
+        raise NotImplementedError
+
+
+class MaterialReader(BaseReader):
+    """Parent class for files that store materials."""
+
+    def __init__(self, filePath, readerSettingsLevel):
+        BaseReader.__init__(self, filePath, readerSettingsLevel)
+        self.materials = {}
+
+    def read(self):
+        raise NotImplementedError
diff --git a/serpentTools/parsers/__init__.py b/serpentTools/parsers/__init__.py
index e3e057d..834eaba 100644
--- a/serpentTools/parsers/__init__.py
+++ b/serpentTools/parsers/__init__.py
@@ -1,41 +1,2 @@
 """Package dedicated for reading ``SERPENT`` output files"""
 from serpentTools.parsers.depletion import DepletionReader
-from serpentTools.parsers.detector import DetectorReader
-from serpentTools.parsers.fissionMatrix import FissionMatrixReader
-from serpentTools.parsers.results import ResultsReader
-from serpentTools.parsers.bumat import BumatReader
-from serpentTools.parsers.branching import BranchingReader
-
-__all__ = [DepletionReader, DetectorReader, FissionMatrixReader,
-           ResultsReader, BumatReader, BranchingReader]
-
-
-class _BaseReader(object):
-    """Parent class from which all parsers will inherit"""
-
-    def __init__(self, filePath):
-        self.filePath = filePath
-
-    def read(self):
-        """
-        Read the file and store the data.
-
-        :note:
-
-          This must be overwritten by each subclass of parser
-
-        """
-        raise NotImplementedError
-
-
-class _MaterialReader(_BaseReader):
-    """
-    Parent class for files that store materials, i.e. ``bumat`` and ``dep``.
-    """
-
-    def __init__(self, filePath):
-        _BaseReader.__init__(self, filePath)
-        self.materials = {}
-
-    def read(self):
-        raise NotImplementedError
diff --git a/serpentTools/parsers/branching.py b/serpentTools/parsers/branching.py
index 42f13aa..6817810 100644
--- a/serpentTools/parsers/branching.py
+++ b/serpentTools/parsers/branching.py
@@ -33,4 +33,4 @@ class BranchingReader(_BaseReader):
         Returns
         -------
 
-        """
\ No newline at end of file
+        """
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 735aa6d..8a3b695 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -1,15 +1,129 @@
 """Parser responsible for reading the ``*dep.m`` files"""
+import re
 
-from serpentTools.parsers import _MaterialReader
+import numpy
 
+from drewtils.parsers import KeywordParser
 
-class DepletionReader(_MaterialReader):
-    """
-    Parser responsible for reading and working with depletion files.
+from serpentTools.objects.readers import MaterialReader
+from serpentTools.objects import DepletedMaterial
+
+
+class DepletionReader(MaterialReader):
+    """Parser responsible for reading and working with depletion files.
 
     Parameters
     ----------
     filePath: str
         path to the depletion file
+
+    Attributes
+    ----------
+    materials: dict
+        Dictionary with material names as keys and the corresponding
+        :py:class:`~serpentTools.objects.DepletedMaterial` class
+        for that material as values
+    metadata: dict
+        Dictionary with file-wide data names as keys and the
+        corresponding dataas values, e.g. 'zai': [list of zai numbers]
+    settings: dict
+        names and values of the settings used to control operations
+        of this reader
+
+    :note:
+
+        Does not support depleted materials with underscores,
+        i.e. ``fuel_1`` will not be matched with the current methods
     """
-    pass
+
+    def __init__(self, filePath):
+        MaterialReader.__init__(self, filePath, 'depletion')
+        self._matPatterns = self._makeMaterialRegexs()
+        self.matchMatNVar = r'[A-Z]{3}_([0-9a-zA-Z]*)_([A-Z]*_?[A-Z]*)'
+        """
+        Captures material name and variable from string::
+
+            MAT_fuel1_ADENS --> ('fuel1', 'ADENS')
+            MAT_fUeL1g_ING_TOX --> ('fUeL1', 'ING_TOX')
+
+        """
+        self.matchTotNVar = r'[A-Z]{3}_([A-Z]*_?[A-Z]*)'
+        """
+        Captures variables for total block from string::
+
+            TOT_ADENS --> ('ADENS', )
+            ING_TOX --> ('ING_TOX', )
+
+        """
+
+    def _makeMaterialRegexs(self):
+        """Return the patterns by which to find the requested materials."""
+        patterns = self.settings['materials'] or ['.*']
+        # match all materials if nothing given
+        return [re.compile(mat) for mat in patterns]
+
+    def read(self):
+        """Read through the depletion file and store requested data."""
+        keys = ['MAT', 'TOT'] if self.settings['processTotal'] else ['MAT']
+        keys.extend(self.settings['metadataKeys'])
+        separators = ['\n', '];']
+        with KeywordParser(self.filePath, keys, separators) as parser:
+            for chunk in parser.yieldChunks():
+                if 'MAT' not in chunk[0] and 'TOT' not in chunk[0]:
+                    self._addMetadata(chunk)
+                elif (('TOT' in chunk[0] and self.settings['processTotal'])
+                      or 'MAT' in chunk[0]):
+                    self._addMaterial(chunk)
+
+    def _addMetadata(self, chunk):
+        options = {'ZAI': 'zai', 'NAMES': 'names', 'DAYS': 'days',
+                   'BU': 'burnup'}
+        for varName, metadataKey in options.items():
+            if varName in chunk[0]:
+                if varName in ['ZAI', 'NAMES']:
+                    values = [line.strip() for line in chunk[1:]]
+                    if varName == 'NAMES':
+                        values = [item.split()[0][1:] for item in values]
+                else:
+                    line = self._cleanSingleLine(chunk)
+                    values = numpy.array([float(item)
+                                          for item in line.split()])
+                self.metadata[metadataKey] = values
+                return
+
+    @staticmethod
+    def _cleanSingleLine(chunk):
+        return chunk[0][chunk[0].index('[') + 1:chunk[0].index(']')]
+
+    def _addMaterial(self, chunk):
+        """Add data from a MAT chunk."""
+        name, variable = self._getGroupsFromChunk(self.matchMatNVar, chunk)
+        if any([re.match(pat, name) for pat in self._matPatterns]):
+            self._processChunk(chunk, name, variable)
+
+    def _addTotal(self, chunk):
+        """Add data from a TOT chunk"""
+        variable = self._getGroupsFromChunk(self.matchTotNVar, chunk)
+        self._processChunk(chunk, 'total', variable)
+
+    def _getGroupsFromChunk(self, regex, chunk):
+        match = re.match(regex, chunk[0])
+        if match:
+            return match.groups()
+        raise Exception('{} not determine match from the following chunk:\n'
+                        '{}'.format(self, ''.join(chunk)))
+
+    def _processChunk(self, chunk, name, variable):
+        if variable not in self.settings['materialVariables']:
+            pass
+        if name not in self.materials:
+            self.materials[name] = DepletedMaterial(self, name)
+        if len(chunk) == 1:  # single line values, e.g. volume or burnup
+            cleaned = self._cleanSingleLine(chunk)
+        else:
+            cleaned = []
+            for line in chunk:
+                if '[' in line or ']' in line:
+                    continue
+                cleaned.append(line[:line.index('%')])
+        self.materials[name].addData(variable, cleaned)
diff --git a/serpentTools/settings/__init__.py b/serpentTools/settings/__init__.py
new file mode 100644
index 0000000..2223fc0
--- /dev/null
+++ b/serpentTools/settings/__init__.py
@@ -0,0 +1,209 @@
+"""Settings to yield control to the user."""
+
+defaultSettings = {
+    'depletion.metadataKeys': {
+        'default': ['ZAI', 'NAMES', 'DAYS', 'BU'],
+        'options': 'default',
+        'description': 'Non-material data to store, i.e. zai, isotope names, '
+                       'burnup schedule, etc.',
+        'type': list
+    },
+    'depletion.materialVariables': {
+        'default': ['ADENS', 'MDENS', 'BURNUP'],
+        'description': 'Variables to store for each depleted material.',
+        'type': list
+    },
+    'depletion.materials': {
+        'default': [],
+        'description': 'Names of materials to store. '
+                       'Empty list -> all materials.',
+        'type': list
+    },
+    'depletion.processTotal': {
+        'default': True,
+        'description': 'Option to store the depletion data from the TOT block',
+        'type': bool
+    }
+}
+
+
+class DefaultSetting(object):
+    """Store a single setting."""
+
+    def __init__(self, name, default, varType, description, options):
+        self.name = name
+        self.description = description
+        self.default = default
+        self.type = varType
+        self.options = options
+
+    def __repr__(self):
+        return '<DefaultSetting {}: value: {}>'.format(self.name, self.default)
+
+    def validate(self, value):
+        """Return True if the value matches the default scheme.
+
+        Parameters
+        ----------
+        value:
+            value to be tested
+
+        Returns
+        -------
+        bool
+            if the value can be used
+
+        Raises
+        ------
+        TypeError
+            If the value is of an incorrect type
+        KeyError
+            If the value does not correspond to one of the acceptable options
+        """
+        if not isinstance(value, self.type):
+            raise TypeError('Setting {} should be of type {}, not {}'
+                            .format(self.name, self.type, type(value)))
+        if self.options:
+            listVals = [value] if not isinstance(value, list) else value
+            inOptions = any([val in self.options for val in listVals])
+            if not inOptions:
+                opts = [str(option) for option in self.options]
+                raise KeyError('Setting {} is\n{}\nand not one of the allowed '
+                               'options:\n{}'
+                               .format(self.name, value, opts))
+        return True
+
+
+class DefaultSettingsLoader(dict):
+    """Base class for loading all the default settings."""
+
+    def __init__(self):
+        self.__locked__ = False
+        dict.__init__(self, self._load())
+        self.__locked__ = True
+
+    def _load(self):
+        """Load the default setting objects."""
+        defaults = {}
+        for name, value in defaultSettings.items():
+            if 'options' in value:
+                options = (value['default'] if value['options'] == 'default'
+                           else value['options'])
+            else:
+                options = None
+            settingsOptions = {'name': name,
+                               'default': value['default'],
+                               'varType': value['type'],
+                               'options': options,
+                               'description': value['description']
+                               }
+            defaults[name] = DefaultSetting(**settingsOptions)
+        return defaults
+
+    def __setitem__(self, key, value):
+        if self.__locked__:
+            raise KeyError('Default settings cannot be updated once set.')
+        self[key] = value
+
+    def retrieveDefaults(self):
+        """Return a dictionary with the default settings."""
+        return {key: setting.default for key, setting in self.items()}
+
+    def validateSetting(self, name, value):
+        """Validate the setting.
+
+        Parameters
+        ----------
+        name: str
+            Full name of the setting
+        value: value to be set
+
+        Raises
+        ------
+        KeyError
+            If the value is not one of the allowable options or if the
+            setting does not match an existing setting
+        TypeError
+            If the value is not of the correct type
+        """
+        if name not in self:
+            raise KeyError('Setting {} does not exist'.format(name))
+        self[name].validate(value)
+
+
+class UserSettingsLoader(dict):
+    """Class that stores the active user settings."""
+
+    def __init__(self):
+        self._defaultLoader = DefaultSettingsLoader()
+        dict.__init__(self, self._defaultLoader.retrieveDefaults())
+
+    def __setitem__(self, key, value):
+        self.setValue(key, value)
+
+    def setValue(self, name, value):
+        """Set the value of a specific setting.
+
+        Parameters
+        ----------
+        name: str
+            Full name of the setting
+        value: value to be set
+
+        Raises
+        ------
+        KeyError
+            If the value is not one of the allowable options or if the
+            setting does not match an existing setting
+        TypeError
+            If the value is not of the correct type
+
+        """
+        if name not in self:
+            raise KeyError('Setting {} does not exist'.format(name))
+        self._defaultLoader[name].validate(value)
+        # if we've made it here, then the value is valid
+        dict.__setitem__(self, name, value)
+
+    def getReaderSettings(self, readerName):
+        """Get all module-wide and reader-specific settings.
+
+        Parameters
+        ----------
+        readerLevel: str
+            Name of the specific reader.
+            Will look for settings with lead with ``readerName``, e.g.
+            ``depletion.metadataKeys``
+
+        Returns
+        -------
+        dict
+            Single level dictionary with ``settingName: settingValue`` pairs
+
+        Raises
+        ------
+        KeyError
+            If the reader name is not located in the ``readers`` settings
+            dictionary
+        """
+        settings = {}
+        self._validateReaderName(readerName)
+        for setting, value in self.items():
+            settingPath = setting.split('.')
+            if len(settingPath) == 1:  # high level setting like verbosity
+                name = settingPath[0]
+            else:
+                if settingPath[0] == readerName:
+                    name = settingPath[1]
+                else:
+                    continue
+            settings[name] = value
+        return settings
+
+    def _validateReaderName(self, readerName):
+        """Return True if the reader name has settings."""
+        readers = set([name.split('.')[0] for name in self if '.' in name])
+        return readerName in readers
+
+
+rc = UserSettingsLoader()
diff --git a/setup.py b/setup.py
index b366466..e45cf6f 100644
--- a/setup.py
+++ b/setup.py
@@ -1,26 +1,48 @@
-from setuptools import setup
-
-__version__ = '0.1.0rc1'
-
-_classifiers = [
-    'License :: OSI Approved :: MIT License',
-]
-
-setupArgs = {
-    'name': 'serpentTools',
-    'packages': ['serpentTools'],
-    'version': __version__,
-    'url': 'https://github.com/CORE-GATECH-GROUP/serpent-tools',
-    'description': ('A suite of parsers designed to make interacting with '
-                    'SERPENT output files simple, scriptable, and flawless'),
-    'test_suite': 'serpentTools.tests',
-    'author': 'Andrew Johnson',
-    'author_email': 'ajohnson400@gatech.edu',
-    'maintainer': 'Dan Kotlyar',
-    'maintainer_email': 'dan.kotlyar@me.gatech.edu',
-    'classifiers': _classifiers,
-    'keywords': 'SERPENT file parsers transport',
-    'license': 'MIT'
-}
-
-setup(**setupArgs)
+import os
+from setuptools import setup
+
+
+def _getVersion():
+    with open(os.path.join('serpentTools', '__init__.py')) as initF:
+        line = initF.readline()
+        while line != '':
+            if 'version' in line:
+                break
+            line = initF.readline()
+    return line.split()[-1].replace('\'', '')
+
+version = _getVersion()
+
+classifiers = [
+    'License :: OSI Approved :: MIT License',
+]
+
+installRequires = [
+    'numpy>=1.11.1',
+    'matplotlib>=1.5.0',
+    'drewtils>=0.1.4',  # file parsing tools
+]
+
+pythonRequires = '>=3.5'
+
+setupArgs = {
+    'name': 'serpentTools',
+    'python_requires': pythonRequires,
+    'packages': ['serpentTools', 'serpentTools.parsers',
+                 'serpentTools.objects', 'serpentTools.settings'],
+    'version': version,
+    'url': 'https://github.com/CORE-GATECH-GROUP/serpent-tools',
+    'description': ('A suite of parsers designed to make interacting with '
+                    'SERPENT output files simple, scriptable, and flawless'),
+    'test_suite': 'serpentTools.tests',
+    'author': 'Andrew Johnson',
+    'author_email': 'ajohnson400@gatech.edu',
+    'maintainer': 'Dan Kotlyar',
+    'maintainer_email': 'dan.kotlyar@me.gatech.edu',
+    'classifiers': classifiers,
+    'install_requires': installRequires,
+    'keywords': 'SERPENT file parsers transport',
+    'license': 'MIT'
+}
+
+setup(**setupArgs)


Test patch:
diff --git a/serpentTools/tests/__init__.py b/serpentTools/tests/__init__.py
index e69de29..c4fe123 100644
--- a/serpentTools/tests/__init__.py
+++ b/serpentTools/tests/__init__.py
@@ -0,0 +1,3 @@
+import os
+
+TEST_ROOT = os.path.dirname(__file__)
diff --git a/serpentTools/tests/ref_dep.m b/serpentTools/tests/ref_dep.m
new file mode 100644
index 0000000..7253c10
--- /dev/null
+++ b/serpentTools/tests/ref_dep.m
@@ -0,0 +1,75 @@
+ZAI = [
+621490
+541350
+922350
+942390
+50100
+666
+0
+];
+
+NAMES = [
+'Sm149           '
+'Xe135           '
+'U235            '
+'Pu239           '
+'B10             '
+'lost            '
+'total           '
+];
+
+i621490 = 1; iSm149            = 1;
+i541350 = 2; iXe135            = 2;
+i922350 = 3; iU235             = 3;
+i942390 = 4; iPu239            = 4;
+i50100  = 5; iB10              = 5;
+iLOST    = 6;
+iTOT     = 7;
+
+MAT_fuel_VOLUME = [ 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 2.05324E+03 ];
+
+MAT_fuel_BURNUP = [ 0.00000E+00 1.90317E-02 3.60163E-02 1.03882E-01 1.74880E-01 2.57317E-01 3.45353E-01 6.03404E-01 8.49693E-01 1.25409E+00 1.66071E+00 2.44849E+00 3.16403E+00 4.02364E+00 ];
+
+MAT_fuel_ADENS = [
+0.00000E+00 2.44791E-10 1.07741E-09 7.54422E-09 1.54518E-08 2.45253E-08 3.05523E-08 3.98843E-08 4.28827E-08 4.37783E-08 4.46073E-08 4.58472E-08 4.73590E-08 4.84031E-08 % Sm149
+0.00000E+00 3.92719E-09 5.62744E-09 6.09339E-09 6.14629E-09 6.05726E-09 6.14402E-09 6.13795E-09 6.10821E-09 6.13674E-09 6.18320E-09 6.18233E-09 6.08629E-09 6.35706E-09 % Xe135
+5.58287E-04 5.57764E-04 5.57298E-04 5.55440E-04 5.53500E-04 5.51257E-04 5.48871E-04 5.41946E-04 5.35434E-04 5.24946E-04 5.14643E-04 4.95299E-04 4.78407E-04 4.58932E-04 % U235
+0.00000E+00 1.63219E-08 6.37555E-08 4.71450E-07 1.11725E-06 2.08488E-06 3.13490E-06 6.42737E-06 9.58635E-06 1.44874E-05 1.90933E-05 2.73021E-05 3.42000E-05 4.11589E-05 % Pu239
+0.00000E+00 9.58524E-34 6.02782E-33 6.83433E-32 2.06303E-31 5.02500E-31 1.07151E-30 4.79600E-30 1.75720E-29 1.46446E-28 3.98274E-28 1.04091E-27 3.12542E-27 6.72465E-27 % B10
+0.00000E+00 2.90880E-14 5.57897E-14 1.62569E-13 2.75249E-13 4.06673E-13 5.46031E-13 9.58962E-13 1.35027E-12 1.99694E-12 2.64702E-12 3.90487E-12 5.05218E-12 6.43096E-12 % lost data
+6.88332E-02 6.88337E-02 6.88341E-02 6.88357E-02 6.88374E-02 6.88394E-02 6.88415E-02 6.88476E-02 6.88535E-02 6.88632E-02 6.88729E-02 6.88917E-02 6.89087E-02 6.89291E-02 % total
+];
+
+MAT_fuel_MDENS = [
+0.00000E+00 6.05336E-08 2.66430E-07 1.86559E-06 3.82102E-06 6.06479E-06 7.55518E-06 9.86286E-06 1.06043E-05 1.08258E-05 1.10308E-05 1.13374E-05 1.17113E-05 1.19694E-05 % Sm149
+0.00000E+00 8.79777E-07 1.26067E-06 1.36505E-06 1.37690E-06 1.35696E-06 1.37639E-06 1.37503E-06 1.36837E-06 1.37476E-06 1.38517E-06 1.38498E-06 1.36346E-06 1.42412E-06 % Xe135
+2.17903E-01 2.17699E-01 2.17517E-01 2.16791E-01 2.16035E-01 2.15159E-01 2.14228E-01 2.11525E-01 2.08983E-01 2.04890E-01 2.00868E-01 1.93318E-01 1.86725E-01 1.79124E-01 % U235
+0.00000E+00 6.47918E-06 2.53085E-05 1.87148E-04 4.43507E-04 8.27620E-04 1.24444E-03 2.55142E-03 3.80542E-03 5.75096E-03 7.57933E-03 1.08379E-02 1.35761E-02 1.63385E-02 % Pu239
+0.00000E+00 1.59375E-32 1.00225E-31 1.13635E-30 3.43023E-30 8.35513E-30 1.78162E-29 7.97438E-29 2.92172E-28 2.43498E-27 6.62216E-27 1.73073E-26 5.19669E-26 1.11812E-25 % B10
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % lost data
+1.02975E+01 1.02975E+01 1.02975E+01 1.02975E+01 1.02975E+01 1.02975E+01 1.02974E+01 1.02974E+01 1.02974E+01 1.02974E+01 1.02973E+01 1.02972E+01 1.02972E+01 1.02971E+01 % total
+];
+
+MAT_fuel_ING_TOX = [
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % Sm149
+0.00000E+00 3.56712E+07 5.11147E+07 5.53470E+07 5.58276E+07 5.50189E+07 5.58069E+07 5.57517E+07 5.54817E+07 5.57408E+07 5.61628E+07 5.61549E+07 5.52826E+07 5.77420E+07 % Xe135
+1.68091E+00 1.67934E+00 1.67793E+00 1.67234E+00 1.66650E+00 1.65974E+00 1.65256E+00 1.63171E+00 1.61210E+00 1.58053E+00 1.54951E+00 1.49126E+00 1.44041E+00 1.38177E+00 % U235
+0.00000E+00 7.63264E+00 2.98141E+01 2.20465E+02 5.22464E+02 9.74958E+02 1.46598E+03 3.00564E+03 4.48288E+03 6.77478E+03 8.92865E+03 1.27673E+04 1.59930E+04 1.92472E+04 % Pu239
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % B10
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % lost data
+5.22355E+01 1.16651E+09 1.10827E+09 1.25209E+09 1.44102E+09 1.49595E+09 1.66322E+09 1.80206E+09 1.79453E+09 1.79100E+09 1.80188E+09 1.75346E+09 1.60021E+09 1.89771E+09 % total
+];
+
+MAT_fuel_INH_TOX = [
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % Sm149
+0.00000E+00 1.86849E+07 2.67744E+07 2.89913E+07 2.92430E+07 2.88194E+07 2.92322E+07 2.92033E+07 2.90618E+07 2.91976E+07 2.94186E+07 2.94144E+07 2.89575E+07 3.02458E+07 % Xe135
+3.03994E+02 3.03710E+02 3.03456E+02 3.02444E+02 3.01388E+02 3.00167E+02 2.98867E+02 2.95097E+02 2.91551E+02 2.85840E+02 2.80230E+02 2.69697E+02 2.60499E+02 2.49894E+02 % U235
+0.00000E+00 3.66367E+03 1.43108E+04 1.05823E+05 2.50782E+05 4.67980E+05 7.03670E+05 1.44271E+06 2.15178E+06 3.25190E+06 4.28575E+06 6.12831E+06 7.67665E+06 9.23866E+06 % Pu239
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % B10
+0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 0.00000E+00 % lost data
+9.85909E+03 1.25957E+09 1.16819E+09 1.28652E+09 1.45744E+09 1.48633E+09 1.64503E+09 1.76388E+09 1.75446E+09 1.76261E+09 1.78643E+09 1.76296E+09 1.63292E+09 1.95159E+09 % total
+];
+
+BU = [ 0.00000E+00 1.93360E-02 3.86721E-02 1.16016E-01 1.93360E-01 2.90041E-01 3.86721E-01 6.76762E-01 9.66802E-01 1.45020E+00 1.93360E+00 2.90041E+00 3.86721E+00 4.83401E+00 ];
+
+DAYS = [ 0.00000E+00 5.00000E-01 1.00000E+00 3.00000E+00 5.00000E+00 7.50000E+00 1.00000E+01 1.75000E+01 2.50000E+01 3.75000E+01 5.00000E+01 7.50000E+01 1.00000E+02 1.25000E+02 ];
\ No newline at end of file
diff --git a/serpentTools/tests/test_depletion.py b/serpentTools/tests/test_depletion.py
new file mode 100644
index 0000000..17d2c6d
--- /dev/null
+++ b/serpentTools/tests/test_depletion.py
@@ -0,0 +1,155 @@
+"""Test the depletion file."""
+import os
+import unittest
+
+import numpy
+
+from serpentTools.settings import rc
+from serpentTools.tests import TEST_ROOT
+from serpentTools.parsers.depletion import DepletionReader
+
+
+class _DepletionTestHelper(unittest.TestCase):
+    """Base class to setup the depletion reader and material tests."""
+
+    @classmethod
+    def setUpClass(cls):
+        filePath = os.path.join(TEST_ROOT, 'ref_dep.m')
+        cls.rc = rc
+        cls.rc['depletion.materials'] = ['fuel', ]
+        cls.rc['depletion.materialVariables'] = [
+            'BURNUP', 'ADENS', 'ING_TOX']
+        cls.reader = DepletionReader(filePath)
+        cls.reader.read()
+
+
+class Test_Depletion(_DepletionTestHelper):
+    """Class that tests the functionality of the depletion reader."""
+
+    def test_metadata(self):
+        """Test the metadata storage for the reader."""
+        expectedMetadata = {
+            'zai':
+                ['621490', '541350', '922350', '942390', '50100', '666', '0'],
+            'names':
+                ['Sm149', 'Xe135', 'U235', 'Pu239', 'B10', 'lost', 'total'],
+            'burnup': [0.00000E+00, 1.93360E-02, 3.86721E-02, 1.16016E-01,
+                       1.93360E-01, 2.90041E-01, 3.86721E-01, 6.76762E-01,
+                       9.66802E-01, 1.45020E+00, 1.93360E+00, 2.90041E+00,
+                       3.86721E+00, 4.83401E+00],
+            'days': [0.00000E+00, 5.00000E-01, 1.00000E+00, 3.00000E+00,
+                     5.00000E+00, 7.50000E+00, 1.00000E+01, 1.75000E+01,
+                     2.50000E+01, 3.75000E+01, 5.00000E+01, 7.50000E+01,
+                     1.00000E+02, 1.25000E+02]
+        }
+        for key, expectedValue in expectedMetadata.items():
+            with self.subTest(key=key):
+                self.assertIn(key, self.reader.metadata)
+                numpy.testing.assert_equal(self.reader.metadata[key],
+                                           expectedValue)
+
+    def test_ReadMaterials(self):
+        """Verify the reader stored the correct materials."""
+        expectedMaterials = ['fuel']
+        for material in expectedMaterials:
+            with self.subTest():
+                self.assertIn(material, self.reader.materials)
+
+
+class DepletedMaterialTester(_DepletionTestHelper):
+    """Class that tests the functionality of the DepletedMaterial class"""
+
+    @classmethod
+    def setUpClass(cls):
+        _DepletionTestHelper.setUpClass()
+        cls.material = cls.reader.materials['fuel']
+        cls.requestedDays = [0.0, 0.5, 1.0, 5.0, 10.0, 25.0, 50.0]
+        cls.fuelBU = cls.material.burnup
+
+    def test_materials(self):
+        """Verify the materials are read in properly."""
+        self.assertIn('fuel', self.reader.materials)
+        expectedAdens = numpy.array([
+            [0.00000E+00, 2.44791E-10, 1.07741E-09, 7.54422E-09, 1.54518E-08,
+             2.45253E-08, 3.05523E-08, 3.98843E-08, 4.28827E-08, 4.37783E-08,
+             4.46073E-08, 4.58472E-08, 4.73590E-08, 4.84031E-08],
+            [0.00000E+00, 3.92719E-09, 5.62744E-09, 6.09339E-09, 6.14629E-09,
+             6.05726E-09, 6.14402E-09, 6.13795E-09, 6.10821E-09, 6.13674E-09,
+             6.18320E-09, 6.18233E-09, 6.08629E-09, 6.35706E-09],
+            [5.58287E-04, 5.57764E-04, 5.57298E-04, 5.55440E-04, 5.53500E-04,
+             5.51257E-04, 5.48871E-04, 5.41946E-04, 5.35434E-04, 5.24946E-04,
+             5.14643E-04, 4.95299E-04, 4.78407E-04, 4.58932E-04],
+            [0.00000E+00, 1.63219E-08, 6.37555E-08, 4.71450E-07, 1.11725E-06,
+             2.08488E-06, 3.13490E-06, 6.42737E-06, 9.58635E-06, 1.44874E-05,
+             1.90933E-05, 2.73021E-05, 3.42000E-05, 4.11589E-05],
+            [0.00000E+00, 9.58524E-34, 6.02782E-33, 6.83433E-32, 2.06303E-31,
+             5.02500E-31, 1.07151E-30, 4.79600E-30, 1.75720E-29, 1.46446E-28,
+             3.98274E-28, 1.04091E-27, 3.12542E-27, 6.72465E-27],
+            [0.00000E+00, 2.90880E-14, 5.57897E-14, 1.62569E-13, 2.75249E-13,
+             4.06673E-13, 5.46031E-13, 9.58962E-13, 1.35027E-12, 1.99694E-12,
+             2.64702E-12, 3.90487E-12, 5.05218E-12, 6.43096E-12],
+            [6.88332E-02, 6.88337E-02, 6.88341E-02, 6.88357E-02, 6.88374E-02,
+             6.88394E-02, 6.88415E-02, 6.88476E-02, 6.88535E-02, 6.88632E-02,
+             6.88729E-02, 6.88917E-02, 6.89087E-02, 6.89291E-02],
+        ])
+        expectedIngTox = numpy.array([
+            [0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00],
+            [0.00000E+00, 3.56712E+07, 5.11147E+07, 5.53470E+07, 5.58276E+07,
+             5.50189E+07, 5.58069E+07, 5.57517E+07, 5.54817E+07, 5.57408E+07,
+             5.61628E+07, 5.61549E+07, 5.52826E+07, 5.77420E+07],
+            [1.68091E+00, 1.67934E+00, 1.67793E+00, 1.67234E+00, 1.66650E+00,
+             1.65974E+00, 1.65256E+00, 1.63171E+00, 1.61210E+00, 1.58053E+00,
+             1.54951E+00, 1.49126E+00, 1.44041E+00, 1.38177E+00],
+            [0.00000E+00, 7.63264E+00, 2.98141E+01, 2.20465E+02, 5.22464E+02,
+             9.74958E+02, 1.46598E+03, 3.00564E+03, 4.48288E+03, 6.77478E+03,
+             8.92865E+03, 1.27673E+04, 1.59930E+04, 1.92472E+04],
+            [0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00],
+            [0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00,
+             0.00000E+00, 0.00000E+00, 0.00000E+00, 0.00000E+00],
+            [5.22355E+01, 1.16651E+09, 1.10827E+09, 1.25209E+09, 1.44102E+09,
+             1.49595E+09, 1.66322E+09, 1.80206E+09, 1.79453E+09, 1.79100E+09,
+             1.80188E+09, 1.75346E+09, 1.60021E+09, 1.89771E+09],
+        ])
+        self.assertListEqual(self.material.zai,
+                             self.reader.metadata['zai'])
+        numpy.testing.assert_equal(self.material.adens, expectedAdens)
+        numpy.testing.assert_equal(self.material.ingTox, expectedIngTox)
+
+    def test_getXY_burnup_full(self):
+        """
+        Verify the material can produce the full burnup vector through getXY.
+        """
+        _days, actual = self.material.getXY('days', 'burnup', )
+        numpy.testing.assert_equal(actual, self.fuelBU)
+
+    def test_getXY_burnup_slice(self):
+        """Verify depletedMaterial getXY correctly slices a vector."""
+        _days, actual = self.material.getXY('days', 'burnup',
+                                            self.requestedDays)
+        expected = [0.0E0, 1.90317E-2, 3.60163E-2, 1.74880E-1, 3.45353E-01,
+                    8.49693E-01, 1.66071E0]
+        numpy.testing.assert_equal(actual, expected)
+
+    def test_getXY_adens(self):
+        """Verify depletedMaterial getXY can return a requested subsection."""
+        names = ['Xe135', 'U235', 'lost']
+        expected = numpy.array([
+            [0.00000E+00, 3.92719E-09, 5.62744E-09, 6.14629E-09, 6.14402E-09,
+             6.10821E-09, 6.18320E-09],
+            [5.58287E-04, 5.57764E-04, 5.57298E-04, 5.53500E-04, 5.48871E-04,
+             5.35434E-04, 5.14643E-04],
+            [0.00000E+00, 2.90880E-14, 5.57897E-14, 2.75249E-13, 5.46031E-13,
+             1.35027E-12, 2.64702E-12],
+        ], float)
+        _days, actual = self.material.getXY('days', 'adens', names=names,
+                                            timePoints=self.requestedDays)
+        numpy.testing.assert_allclose(actual, expected, rtol=1E-4)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/serpentTools/tests/test_loaders.py b/serpentTools/tests/test_loaders.py
new file mode 100644
index 0000000..f6895e1
--- /dev/null
+++ b/serpentTools/tests/test_loaders.py
@@ -0,0 +1,83 @@
+"""Tests for the settings loaders."""
+import unittest
+
+from serpentTools.settings import DefaultSettingsLoader, UserSettingsLoader
+
+
+class DefaultSettingsTester(unittest.TestCase):
+    """Class to test the functionality of the master loader."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.defaultLoader = 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 UserSettingsTester(unittest.TestCase):
+    """Class to test the methods of updating the user settings."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.loader = UserSettingsLoader()
+
+    def test_failAtNonexistentSetting(self):
+        """Verify that the loader will not load a nonexistent setting."""
+        with self.assertRaises(KeyError):
+            self.loader['bad setting'] = False
+
+    def test_failAtBadSetting_options(self):
+        """Verify that the loader will raise an error for bad options."""
+        with self.assertRaises(KeyError):
+            self.loader['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.loader['depletion.processTotal'] = 'this should fail'
+
+    def test_returnReaderSettings(self):
+        """Verify the correct reader settings can be retrieved."""
+        readerName = 'depletion'
+        expected = {
+            'metadataKeys': ['ZAI', 'NAMES', 'DAYS', 'BU'],
+            'materialVariables': ['ADENS', 'MDENS', 'BURNUP'],
+            'materials': [],
+            'processTotal': True
+        }
+        actual = self.loader.getReaderSettings(readerName)
+        self.assertDictEqual(expected, actual)
+
+
+class RCTester(unittest.TestCase):
+    """Class to test the funcitonality of the scriptable settings manager."""
+
+    @classmethod
+    def setUpClass(cls):
+        from serpentTools.settings import rc
+        cls.rc = rc
+        cls.rc['depletion.metadataKeys'] = ['ZAI']
+
+    def test_readerWithUpdatedSettings(self):
+        """Verify the settings passed to the reader reflect the update."""
+        from serpentTools.parsers.depletion import DepletionReader
+        reader = DepletionReader('None')
+        self.assertTrue(reader.settings['metadataKeys'] == ['ZAI'])
+
+
+if __name__ == '__main__':
+    unittest.main()