Repository: Ouranosinc/xclim
Pull request #47 (2018-10-01 23:57:50)

implemented runtime attribute assignment in with_attrs
Related to #41 


Patch:
diff --git a/requirements.txt b/requirements.txt
index 8549f8ac..6630f86d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ scipy
 bottleneck
 netCDF4
 pandas
+funcsigs
diff --git a/setup.py b/setup.py
index b74c8dc9..5362b23a 100644
--- a/setup.py
+++ b/setup.py
@@ -11,14 +11,14 @@ with open('README.rst') as readme_file:
 with open('HISTORY.rst') as history_file:
     history = history_file.read()
 
-#with open('requirements.txt') as req_file:
+# with open('requirements.txt') as req_file:
 #    requirements = req_file.read().split('\n')
 
-requirements = ['dask', 'numpy', 'scipy', 'xarray', 'netcdf4', 'pandas', ]
+requirements = ['dask', 'numpy', 'scipy', 'xarray', 'netcdf4', 'pandas', 'funcsigs']
 
 setup_requirements = ['pytest-runner', ]
 
-test_requirements = ['pytest', ]
+test_requirements = ['pytest', 'tox']
 
 docs_requirements = ['Sphinx', 'guzzle-sphinx-theme', ]
 
diff --git a/tox.ini b/tox.ini
index 1c327ee3..f9f97799 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,8 +16,7 @@ python =
 basepython = python
 deps = flake8
 commands =
-  flake8 xclim
-  flake8 tests
+  flake8 xclim tests
 
 [testenv:docs]
 deps =
diff --git a/xclim/indices.py b/xclim/indices.py
index fd225935..25f90c5c 100644
--- a/xclim/indices.py
+++ b/xclim/indices.py
@@ -1,11 +1,14 @@
 # -*- coding: utf-8 -*-
 
-"""Main module.
+"""
+Main module
 """
 from functools import wraps
 
+import six
 import numpy as np
 import xarray as xr
+import re
 
 from . import run_length as rl
 from .checks import valid_daily_mean_temperature, valid_daily_max_min_temperature, valid_daily_min_temperature, \
@@ -15,6 +18,11 @@ from .utils import daily_downsampler as dds
 xr.set_options(enable_cftimeindex=True)  # Set xarray to use cftimeindex
 
 
+if six.PY2:
+    from funcsigs import signature
+elif six.PY3:
+    from inspect import signature
+
 # Frequencies : YS: year start, QS-DEC: seasons starting in december, MS: month start
 # See http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
 K2C = 273.15
@@ -37,6 +45,31 @@ def first_paragraph(txt):
     return txt.split('\n\n')[0]
 
 
+attrs_mapping = {'cell_methods': {'YS': 'years'}, }
+
+
+def format_kwargs(attrs, params):
+    """Update entries in place with argument values.
+
+    Parameters
+    ----------
+    attrs : dict
+      Attributes to be assigned to function output. The values of the attributes in braces will be replaced the
+      the corresponding args values.
+    params : dict
+      A BoundArguments.arguments dictionary storing a function's arguments.
+    """
+    for key, val in attrs.items():
+        m = re.findall("{(\w+)}", val)
+        for name in m:
+            if name in params:
+                v = params.get(name)
+                if v is None:
+                    raise ValueError("{0} is not a valid function argument.".format(name))
+                repl = attrs_mapping[key][v]
+                attrs[key] = re.sub("{%s}" % name, repl, val)
+
+
 def with_attrs(**func_attrs):
     r"""Set attributes in the decorated function at definition time,
     and assign these attributes to the function output at the
@@ -54,6 +87,9 @@ def with_attrs(**func_attrs):
         @wraps(fn)
         def wrapper(*args, **kwargs):
             out = fn(*args, **kwargs)
+            # Bind the arguments
+            ba = signature(fn).bind(*args, **kwargs)
+            format_kwargs(func_attrs, ba.arguments)
             out.attrs.update(func_attrs)
             return out
 
@@ -286,9 +322,8 @@ def consecutive_wet_days(pr, thresh=1.0, freq='YS'):
 #
 #     return group.apply(func)
 
-
-@with_attrs(standard_name='cooling_degree_days', long_name='cooling degree days', units='K*day')
 @valid_daily_mean_temperature
+@with_attrs(standard_name='cooling_degree_days', long_name='cooling degree days', units='K*day')
 def cooling_degree_days(tas, thresh=18, freq='YS'):
     r"""Cooling degree days above threshold."""
 
@@ -828,6 +863,8 @@ def tx_mean(tasmax, freq='YS'):
 
 
 @valid_daily_max_temperature
+@with_attrs(standard_name='tx_min', long_name='Minimum of daily maximum temperature',
+            cell_methods='time: minimum within {freq}')
 def tx_min(tasmax, freq='YS'):
     """Lowest max temperature
 


Test patch:
diff --git a/tests/test_indices.py b/tests/test_indices.py
index 6ebf60c9..cf1d1baa 100644
--- a/tests/test_indices.py
+++ b/tests/test_indices.py
@@ -143,6 +143,20 @@ class Test_prcptotal():
         assert (np.allclose(target, out_std.values))
 
 
+class Test_tx_min():
+    def time_series(self, values):
+        coords = pd.date_range('7/1/2000', periods=len(values), freq=pd.DateOffset(days=1))
+        return xr.DataArray(values, coords=[coords, ], dims='time',
+                            attrs={'standard_name': 'air_temperature',
+                                   'cell_methods': 'time: maximum within days',
+                                   'units': 'K'})
+
+    def test_attrs(self):
+        a = self.time_series(np.array([20, 25, -15, 19]) + K2C)
+        txm = xci.tx_min(a, freq='YS')
+        assert txm.cell_methods == 'time: minimum within years'
+
+
 # I'd like to parametrize some of these tests so we don't have to write individual tests for each indicator.
 @pytest.mark.skip('')
 class TestTG():
diff --git a/tests/test_utils.py b/tests/test_utils.py
index cb2121b3..df1e4833 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -24,6 +24,7 @@ import pandas as pd
 import xarray as xr
 
 from xclim.utils import daily_downsampler
+from xclim.indices import format_kwargs
 
 TESTS_HOME = os.path.abspath(os.path.dirname(__file__))
 TESTS_DATA = os.path.join(TESTS_HOME, 'testdata')
@@ -107,3 +108,11 @@ class Test_daily_downsampler():
             target_season = [30.5] + [(n - 1) * 30 + 15.5 for n in [4, 7, 10, 12]]
             target = {'YS': target_year, 'MS': target_month, 'QS-DEC': target_season}[freq]
             assert (np.allclose(x2.values, target))
+
+
+def test_format_kwargs():
+    attrs = dict(standard_name='tx_min', long_name='Minimum of daily maximum temperature',
+                 cell_methods='time: minimum within {freq}')
+
+    format_kwargs(attrs, {'freq': 'YS'})
+    assert attrs['cell_methods'] == 'time: minimum within years'