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

Issue:
Add more message types
Add some future/depreciated warnings that can either piggy back off the [python builtin warnings](https://docs.python.org/2/library/warnings.html) but also conform to our messaging methodology

Pull request:
Added depreciation and future warning decorators; More debug statements
- Changed info messages to debug messages for DepletionReader
- Added more debug statements for adding materials and adding data to those materials
- Added two decorators to notify users of functionality that may be changed or non-existent on the future

Closes #33 

## Decorators
The two new decorators are `depreciated` and `willChange`. These are to decorate functions in the following manner
```
@depreciated
def foo(*args, **kwargs):
    <do some things>
    <return something or not>

foo(arg, kwarg0=True)
# prints the following warning
# WARNING : serpentTools   : Call to depreciated function foo

@willChange('this function will return None in version 1.0)
def bar(*args, **kwargs):
    return True

bar(None)
# prints the following warning
# WARNING : serpentTools   : this function will return None in version 1.0
```

These should be used if a function/method is to be retired (depreciated) in future versions, or if the functionality will change.

## See Also
-  [The decorators they don't tell you about - Chase Stevens](https://github.com/hchasestevens/posts/blob/master/notebooks/the-decorators-they-wont-tell-you-about.ipynb#blob_contributors_box)
-  [Class decorators with arguments- Bruce Eckel](https://www.artima.com/weblogs/viewpost.jsp?thread=240845)


Patch:
diff --git a/serpentTools/__init__.py b/serpentTools/__init__.py
index 87c984c..bf0e5d3 100644
--- a/serpentTools/__init__.py
+++ b/serpentTools/__init__.py
@@ -5,13 +5,14 @@ ROOT_DIR = os.path.dirname(__file__)
 from serpentTools import settings
 
 # List TODOS/feature requests here for now
-# Compatability
+# Compatibility
 # TODO: Python 2 support
 # TODO: Test compatibility with earlier numpy releases
 # Usage/scripting
 # TODO: Update rc with dictionary
 # TODO: Update rc with yaml file into dictionary
 # TODO: Capture materials with underscores for depletion
+# TODO: Find a way to capture some or all of log messages for testing
 
 from ._version import get_versions
 __version__ = get_versions()['version']
diff --git a/serpentTools/objects/materials.py b/serpentTools/objects/materials.py
index 43e3db8..49e2ff4 100644
--- a/serpentTools/objects/materials.py
+++ b/serpentTools/objects/materials.py
@@ -3,7 +3,7 @@
 import numpy
 from matplotlib import pyplot
 
-
+from serpentTools.settings import messages
 from serpentTools.objects import _NamedObject
 
 
@@ -62,6 +62,7 @@ class DepletedMaterial(_NamedObject):
             List of strings corresponding to the raw data from the file
         """
         newName = self._convertVariableName(variable)
+        messages.debug('Adding {} data to {}'.format(newName, self.name))
         if isinstance(rawData, str):
             scratch = [float(item) for item in rawData.split()]
         else:
diff --git a/serpentTools/parsers/depletion.py b/serpentTools/parsers/depletion.py
index 2f31b49..c053805 100644
--- a/serpentTools/parsers/depletion.py
+++ b/serpentTools/parsers/depletion.py
@@ -37,22 +37,15 @@ class DepletionReader(MaterialReader):
     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::
+        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')
 
-            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', )
-
-        """
+        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."""
@@ -64,7 +57,7 @@ class DepletionReader(MaterialReader):
 
     def read(self):
         """Read through the depletion file and store requested data."""
-        messages.info('Preparing to read {}'.format(self.filePath))
+        messages.debug('Preparing to read {}'.format(self.filePath))
         keys = ['MAT', 'TOT'] if self.settings['processTotal'] else ['MAT']
         keys.extend(self.settings['metadataKeys'])
         separators = ['\n', '];']
@@ -75,7 +68,7 @@ class DepletionReader(MaterialReader):
                 elif (('TOT' in chunk[0] and self.settings['processTotal'])
                       or 'MAT' in chunk[0]):
                     self._addMaterial(chunk)
-        messages.info('Done reading depletion file')
+        messages.debug('Done reading depletion file')
         messages.debug('  found {} materials'.format(len(self.materials)))
 
     def _addMetadata(self, chunk):
@@ -100,13 +93,13 @@ class DepletionReader(MaterialReader):
 
     def _addMaterial(self, chunk):
         """Add data from a MAT chunk."""
-        name, variable = self._getGroupsFromChunk(self.matchMatNVar, 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)
+        variable = self._getGroupsFromChunk(self._matchTotNVar, chunk)
         self._processChunk(chunk, 'total', variable)
 
     def _getGroupsFromChunk(self, regex, chunk):
@@ -120,7 +113,9 @@ class DepletionReader(MaterialReader):
         if variable not in self.settings['materialVariables']:
             pass
         if name not in self.materials:
+            messages.debug('Adding material {}...'.format(name))
             self.materials[name] = DepletedMaterial(self, name)
+            messages.debug('  added')
         if len(chunk) == 1:  # single line values, e.g. volume or burnup
             cleaned = self._cleanSingleLine(chunk)
         else:
diff --git a/serpentTools/settings/messages.py b/serpentTools/settings/messages.py
index a639526..54faeef 100644
--- a/serpentTools/settings/messages.py
+++ b/serpentTools/settings/messages.py
@@ -7,7 +7,8 @@ See Also
 * https://www.python.org/dev/peps/pep-0391/
 """
 
-
+import functools
+import warnings
 import logging
 from logging.config import dictConfig
 
@@ -19,11 +20,10 @@ class SerpentToolsException(Exception):
 
 LOG_OPTS = ['critical', 'error', 'warning', 'info', 'debug']
 
-
 loggingConfig = {
     'version': 1,
     'formatters': {
-        'brief': {'format': '%(levelname)-8s: %(name)-15s: %(message)s'},
+        'brief': {'format': '%(levelname)-8s: %(name)-12s: %(message)s'},
         'precise': {
             'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
         }
@@ -74,8 +74,42 @@ def updateLevel(level):
     """Set the level of the logger."""
     if level.lower() not in LOG_OPTS:
         __logger__.setLevel('INFO')
-        warning('Logger option {} not in options. Set to warning.')
-        return 'warning'
+        warning('Logger option {} not in options. Set to info.'.format(level))
+        return 'info'
     else:
         __logger__.setLevel(level.upper())
         return level
+
+
+def depreciated(f):
+    """Display a warning indicating a function will be depreciated."""
+
+    @functools.wraps(f)
+    def decoratedFunc(*args, **kwargs):
+        msg = 'Call to depreciated function {}'.format(f.__name__)
+        warning(msg)
+        _updateFilterAlert(msg, DeprecationWarning)
+        return f(*args, **kwargs)
+
+    return decoratedFunc
+
+
+def willChange(changeMsg):
+    """Inform the user that some functionality may change."""
+
+    def decorate(f):
+        @functools.wraps(f)
+        def decoratedFunc(*args, **kwargs):
+            warning(changeMsg)
+            _updateFilterAlert(changeMsg, FutureWarning)
+            return f(*args, **kwargs)
+
+        return decoratedFunc
+
+    return decorate
+
+
+def _updateFilterAlert(msg, category):
+    warnings.simplefilter('always', category)
+    warnings.warn(msg, category=category, stacklevel=3)
+    warnings.simplefilter('default', category)


Test patch:
diff --git a/serpentTools/tests/test_settings.py b/serpentTools/tests/test_settings.py
index e094205..f57fe77 100644
--- a/serpentTools/tests/test_settings.py
+++ b/serpentTools/tests/test_settings.py
@@ -2,6 +2,7 @@
 import unittest
 
 from serpentTools import settings
+from serpentTools.settings.messages import depreciated, willChange
 
 
 class DefaultSettingsTester(unittest.TestCase):
@@ -99,5 +100,30 @@ class RCTester(unittest.TestCase):
         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
+
+        self.assertEqual(7, demoFuture(2))
+        self.assertEqual(7, demoFuture(2, 5))
+
+    def test_depreciatedDecorator(self):
+        """Verify that the depreciated decorator doesn't break things"""
+
+        @depreciated
+        def demoFunction(x, val=5):
+            return x + val
+
+        self.assertEqual(7, demoFunction(2))
+        self.assertEqual(7, demoFunction(2, 5))
+
+
 if __name__ == '__main__':
     unittest.main()