Repository: Ouranosinc/xclim

Issue:
Solve problem with {} tags in identifier
### Description
In the indicator class, we use the identifier attribute as the unique identifier of a given class. Since this was also used as the output variable name for the created netCDF, we introduced formatting tags in the identifier attribute. The problem is that the same identifier is also used in other contacts (finch) to identify uniquely the process, but before the class is called, we can't format the identifier string. 

We should have two distinct attributes: the unique class identifier, and the output variable name. The identifier should not contain {}, but the output variable name could. 

More generally, we should clarify which attributes need to be clearly defined without arguments tags, and which can include tags. 

Discussion:

Patch:
diff --git a/xclim/atmos/_precip.py b/xclim/atmos/_precip.py
index 8397c951..8fb6396d 100644
--- a/xclim/atmos/_precip.py
+++ b/xclim/atmos/_precip.py
@@ -39,20 +39,21 @@ max_1day_precipitation_amount = Pr(identifier='rx1day',
                                    compute=indices.max_1day_precipitation_amount,
                                    )
 
-max_n_day_precipitation_amount = Pr(identifier='rx{window}day',
+max_n_day_precipitation_amount = Pr(identifier='max_n_day_precipitation_amount',
+                                    var_name='rx{window}day',
                                     units='mm',
                                     standard_name='lwe_thickness_of_precipitation_amount',
                                     long_name='maximum {window}-day total precipitation',
-                                    description="{freq} maximum {window}-day total precipitation",
+                                    description="{freq} maximum {window}-day total precipitation.",
                                     cellmethods='time: sum within days time: maximum over days',
                                     compute=indices.max_n_day_precipitation_amount,
                                     )
 
-wetdays = Pr(identifier='r{thresh}mm',
+wetdays = Pr(identifier='wetdays',
              units='days',
              standard_name='number_of_days_with_lwe_thickness_of_precipitation_amount_at_or_above_threshold',
-             long_name='Number of Wet Days (precip >= {thresh} mm)',
-             description='{freq} number of days with daily precipitation over {thresh} mm',
+             long_name='Number of wet days (precip >= {thresh})',
+             description='{freq} number of days with daily precipitation over {thresh}.',
              cell_methods='time: sum within days time: sum over days',
              compute=indices.wetdays,
              )
@@ -61,9 +62,9 @@ maximum_consecutive_wet_days = Pr(identifier='cwd',
                                   units='days',
                                   standard_name='number_of_days_with_lwe_thickness_of_'
                                                 'precipitation_amount_at_or_above_threshold',
-                                  long_name='Maximum consecutive wet days (Precip >= {thresh}mm)',
+                                  long_name='Maximum consecutive wet days (Precip >= {thresh})',
                                   description='{freq} maximum number of days with daily '
-                                              'precipitation over {thresh} mm',
+                                              'precipitation over {thresh}.',
                                   cell_methods='time: sum within days time: sum over days',
                                   compute=indices.maximum_consecutive_wet_days,
                                   )
@@ -72,9 +73,9 @@ maximum_consecutive_dry_days = Pr(identifier='cdd',
                                   units='days',
                                   standard_name='number_of_days_with_lwe_thickness_of_'
                                                 'precipitation_amount_below_threshold',
-                                  long_name='Maximum consecutive dry days (Precip < {thresh}mm)',
+                                  long_name='Maximum consecutive dry days (Precip < {thresh})',
                                   description='{freq} maximum number of days with daily '
-                                              'precipitation below {thresh} mm',
+                                              'precipitation below {thresh}.',
                                   cell_methods='time: sum within days time: sum over days',
                                   compute=indices.maximum_consecutive_dry_days,
                                   )
@@ -84,7 +85,7 @@ daily_pr_intensity = Pr(identifier='sdii',
                         standard_name='lwe_thickness_of_precipitation_amount',
                         long_name='Average precipitation during Wet Days (SDII)',
                         description="{freq} Simple Daily Intensity Index (SDII) : {freq} average precipitation "
-                                    "for days with daily precipitation over {thresh} mm",
+                                    "for days with daily precipitation over {thresh}.",
                         cell_methods='',
                         compute=indices.daily_pr_intensity,
                         )
@@ -93,7 +94,7 @@ precip_accumulation = Pr(identifier='prcptot',
                          units='mm',
                          standard_name='lwe_thickness_of_precipitation_amount',
                          long_name='Total precipitation',
-                         description='{freq} total precipitation',
+                         description='{freq} total precipitation.',
                          cell_methods='time: sum within days time: sum over days',
                          compute=indices.precip_accumulation
                          )
diff --git a/xclim/atmos/_temperature.py b/xclim/atmos/_temperature.py
index 4c68c93f..20a73973 100644
--- a/xclim/atmos/_temperature.py
+++ b/xclim/atmos/_temperature.py
@@ -66,30 +66,30 @@ class TasminTasmax(Indicator2D):
         """The function computing the indicator."""
 
 
-tn_days_below = Tasmin(identifier='tnlt_{thresh}',
+tn_days_below = Tasmin(identifier='tn_days_below',
                        units='days',
                        standard_name='number_of_days_with_air_temperature_below_threshold',
-                       long_name='Number of days with Tmin < {thresh}C',
-                       description="{freq} number of days where daily minimum temperature is below {thresh}℃",
+                       long_name='Number of days with Tmin < {thresh}',
+                       description="{freq} number of days where daily minimum temperature is below {thresh}",
                        cell_methods='time: minimum within days time: sum over days',
                        compute=indices.tn_days_below,
                        )
 
-tx_days_above = Tasmax(identifier='txgt_{thresh}',
+tx_days_above = Tasmax(identifier='tx_days_above',
                        units='days',
                        standard_name='number_of_days_with_air_temperature_above_threshold',
-                       long_name='Number of days with Tmax > {thresh}C',
-                       description="{freq} number of days where daily maximum temperature exceeds {thresh}℃",
+                       long_name='Number of days with Tmax > {thresh}',
+                       description="{freq} number of days where daily maximum temperature exceeds {thresh}",
                        cell_methods='time: maximum within days time: sum over days',
                        compute=indices.tx_days_above,
                        )
 
-tx_tn_days_above = TasminTasmax(identifier='txgt_{thresh_tasmax}_tngt_{thresh_tasmin}',
+tx_tn_days_above = TasminTasmax(identifier='tx_tn_days_above',
                                 units='days',
                                 standard_name='number_of_days_with_air_temperature_above_threshold',
-                                long_name='Number of days with Tmax > {thresh_tasmax}C and Tmin > {thresh_tasmin}C',
+                                long_name='Number of days with Tmax > {thresh_tasmax} and Tmin > {thresh_tasmin}',
                                 description="{freq} number of days where daily maximum temperature exceeds"
-                                            " {thresh_tasmax}℃ and minimum temperature exceeds {thresh_tasmin}℃",
+                                            " {thresh_tasmax} and minimum temperature exceeds {thresh_tasmin}",
                                 cell_methods='',
                                 compute=indices.tx_tn_days_above,
                                 )
@@ -97,12 +97,12 @@ tx_tn_days_above = TasminTasmax(identifier='txgt_{thresh_tasmax}_tngt_{thresh_ta
 heat_wave_frequency = TasminTasmax(identifier='heat_wave_frequency',
                                    units='',
                                    standard_name='heat_wave_events',
-                                   long_name='Number of heat wave events (Tmin > {thresh_tasmin}℃'
-                                             'and Tmax > {thresh_tasmax}℃ for >= {window} days)',
+                                   long_name='Number of heat wave events (Tmin > {thresh_tasmin}'
+                                             'and Tmax > {thresh_tasmax} for >= {window} days)',
                                    description="{freq} number of heat wave events over a given period. "
                                                "An event occurs when the minimum and maximum daily "
                                                "temperature both exceeds specific thresholds : "
-                                               "(Tmin > {thresh_tasmin}℃ and Tmax > {thresh_tasmax}℃) "
+                                               "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) "
                                                "over a minimum number of days ({window}).",
                                    cell_methods='',
                                    keywords="health,",
@@ -112,24 +112,24 @@ heat_wave_frequency = TasminTasmax(identifier='heat_wave_frequency',
 heat_wave_max_length = TasminTasmax(identifier='heat_wave_max_length',
                                     units='days',
                                     standard_name='spell_length_of_days_with_air_temperature_above_threshold',
-                                    long_name='Maximum length of heat wave events (Tmin > {thresh_tasmin}℃'
-                                              'and Tmax > {thresh_tasmax}℃ for >= {window} days)',
+                                    long_name='Maximum length of heat wave events (Tmin > {thresh_tasmin}'
+                                              'and Tmax > {thresh_tasmax} for >= {window} days)',
                                     description="{freq} maximum length of heat wave events occuring in a given period."
                                                 "An event occurs when the minimum and maximum daily "
                                                 "temperature both exceeds specific thresholds "
-                                                "(Tmin > {thresh_tasmin}℃ and Tmax > {thresh_tasmax}℃) over "
+                                                "(Tmin > {thresh_tasmin} and Tmax > {thresh_tasmax}) over "
                                                 "a minimum number of days ({window}).",
                                     cell_methods='',
                                     keywords="health,",
                                     compute=indices.heat_wave_max_length,
                                     )
 
-heat_wave_index = Tasmax(identifier='hwi_{thresh}',
+heat_wave_index = Tasmax(identifier='heat_wave_index',
                          units='days',
                          standard_name='heat_wave_index',
                          long_name='Number of days that are part of a heatwave',
                          description='{freq} number of days that are part of a heatwave, '
-                                     'defined as five or more consecutive days over {thresh}℃',
+                                     'defined as five or more consecutive days over {thresh}.',
                          cell_methods='',
                          compute=indices.heat_wave_index,
                          )
@@ -228,7 +228,8 @@ extreme_temperature_range = TasminTasmax(identifier='etr',
                                          compute=indices.extreme_temperature_range,
                                          )
 
-cold_spell_duration_index = Tasmin(identifier='csdi_{window}',
+cold_spell_duration_index = Tasmin(identifier='cold_spell_duration_index',
+                                   var_name='csdi_{window}',
                                    units='days',
                                    standard_name='cold_spell_duration_index',
                                    long_name='Cold Spell Duration Index, count of days with at '
@@ -241,13 +242,13 @@ cold_spell_duration_index = Tasmin(identifier='csdi_{window}',
                                    compute=indices.cold_spell_duration_index,
                                    )
 
-cold_spell_days = Tas(identifier='csi_{thresh}',
+cold_spell_days = Tas(identifier='cold_spell_days',
                       units='days',
                       standard_name='cold_spell_days',
                       long_name='cold spell index',
                       description='{freq} number of days that are part of a cold spell, defined as {window} '
                                   'or more consecutive days with mean daily '
-                                  'temperature below  {thresh}°C',
+                                  'temperature below  {thresh}.',
                       cell_methods='',
                       compute=indices.cold_spell_days,
                       )
@@ -257,34 +258,34 @@ daily_freezethaw_cycles = TasminTasmax(identifier='dlyfrzthw',
                                        standard_name='daily_freezethaw_cycles',
                                        long_name='daily freezethaw cycles',
                                        description='{freq} number of days with a diurnal freeze-thaw cycle '
-                                                   ': Tmax > 0℃ and Tmin < 0℃',
+                                                   ': Tmax > 0℃ and Tmin < 0℃.',
                                        cell_methods='',
                                        compute=indices.daily_freezethaw_cycles,
                                        )
 
-cooling_degree_days = Tas(identifier='cddcold_{thresh}',
+cooling_degree_days = Tas(identifier='cooling_degree_days',
                           units='K days',
                           standard_name='integral_of_air_temperature_excess_wrt_time',
-                          long_name='Cooling Degree Days (Tmean > {thresh}C)',
-                          description='{freq} cooling degree days above {thresh}°C',
+                          long_name='Cooling Degree Days (Tmean > {thresh})',
+                          description='{freq} cooling degree days above {thresh}.',
                           cell_methods='time: mean within days time: sum over days',
                           compute=indices.cooling_degree_days,
                           )
 
-heating_degree_days = Tas(identifier='hddheat_{thresh}',
+heating_degree_days = Tas(identifier='heating_degree_days',
                           units='K days',
                           standard_name='integral_of_air_temperature_deficit_wrt_time',
-                          long_name='Heating Degree Days (Tmean < {thresh}C)',
-                          description='{freq} heating degree days below {thresh}°C',
+                          long_name='Heating Degree Days (Tmean < {thresh})',
+                          description='{freq} heating degree days below {thresh}.',
                           cell_methods='time: mean within days time: sum over days',
                           compute=indices.heating_degree_days,
                           )
 
-growing_degree_days = Tas(identifier='gddgrow_{thresh}',
+growing_degree_days = Tas(identifier='growing_degree_days',
                           units='K days',
                           standard_name='integral_of_air_temperature_excess_wrt_time',
                           long_name='growing degree days above {thresh}',
-                          description='{freq} growing degree days above {thresh}°C',
+                          description='{freq} growing degree days above {thresh}',
                           cell_methods='time: mean within days time: sum over days',
                           compute=indices.growing_degree_days,
                           )
@@ -302,7 +303,7 @@ frost_days = Tasmin(identifier='frost_days',
                     standard_name='days_with_air_temperature_below_threshold',
                     long_name='Number of Frost Days (Tmin < 0C)',
                     description='{freq} number of days with minimum daily '
-                                'temperature below 0°C',
+                                'temperature below 0℃.',
                     cell_methods='time: minimum within days time: sum over days',
                     compute=indices.frost_days,
                     )
@@ -310,9 +311,9 @@ frost_days = Tasmin(identifier='frost_days',
 ice_days = Tasmax(identifier='ice_days',
                   standard_name='days_with_air_temperature_below_threshold',
                   units='days',
-                  long_name='Number of Ice Days (Tmax < 0C)',
+                  long_name='Number of Ice Days (Tmax < 0℃)',
                   description='{freq} number of days with maximum daily '
-                              'temperature below 0°C',
+                              'temperature below 0℃',
                   cell_methods='time: maximum within days time: sum over days',
                   compute=indices.ice_days,
                   )
@@ -322,30 +323,30 @@ consecutive_frost_days = Tasmin(identifier='consecutive_frost_days',
                                 standard_name='spell_length_of_days_with_air_temperature_below_threshold',
                                 long_name='Maximum number of consecutive days with Tmin < 0C',
                                 description='{freq} maximum number of consecutive days with '
-                                            'minimum daily temperature below 0°C',
+                                            'minimum daily temperature below 0℃',
                                 cell_methods='time: min within days time: maximum over days',
                                 compute=indices.consecutive_frost_days,
                                 )
 
-growing_season_length = Tas(identifier='gsl_{thresh}',
+growing_season_length = Tas(identifier='growing_season_length',
                             units='days',
                             standard_name='growing_season_length',
-                            long_name='ETCCDI Growing Season Length (Tmean > {thresh}C)',
+                            long_name='ETCCDI Growing Season Length (Tmean > {thresh})',
                             description='{freq} number of days between the first occurrence of at least '
-                                        'six consecutive days with mean daily temperature over {thresh}℃ and '
+                                        'six consecutive days with mean daily temperature over {thresh} and '
                                         'the first occurrence of at least {window} consecutive days with '
-                                        'mean daily temperature below {thresh}℃ after July 1st in the northern '
+                                        'mean daily temperature below {thresh} after July 1st in the northern '
                                         'hemisphere and January 1st in the southern hemisphere',
                             cell_methods='',
                             compute=indices.growing_season_length,
                             )
 
-tropical_nights = Tasmin(identifier='tr_{thresh}',
+tropical_nights = Tasmin(identifier='tropical_nights',
                          units='days',
                          standard_name='number_of_days_with_air_temperature_above_threshold',
-                         long_name='Number of Tropical Nights (Tmin > {thresh}C)',
+                         long_name='Number of Tropical Nights (Tmin > {thresh})',
                          description='{freq} number of Tropical Nights : defined as days with minimum daily temperature'
-                                     ' above {thresh}℃',
+                                     ' above {thresh}',
                          cell_methods='time: minimum within days time: sum over days',
                          compute=indices.tropical_nights,
                          )
@@ -356,7 +357,7 @@ tg90p = Tas(identifier='tg90p',
             long_name='Number of days when Tmean > 90th percentile',
             description='{freq} number of days with mean daily temperature above the 90th percentile.'
                         'The 90th percentile is to be computed for a 5 day window centered on each calendar day '
-                        'for a reference period',
+                        'for a reference period.',
             cell_methods='time: mean within days time: sum over days',
             compute=indices.tg90p,
             )
@@ -367,7 +368,7 @@ tg10p = Tas(identifier='tg10p',
             long_name='Number of days when Tmean < 10th percentile',
             description='{freq} number of days with mean daily temperature below the 10th percentile.'
                         'The 10th percentile is to be computed for a 5 day window centered on each calendar day '
-                        'for a reference period',
+                        'for a reference period.',
             cell_methods='time: mean within days time: sum over days',
             compute=indices.tg10p
             )
@@ -378,7 +379,7 @@ tx90p = Tasmax(identifier='tx90p',
                long_name='Number of days when Tmax > 90th percentile',
                description='{freq} number of days with maximum daily temperature above the 90th percentile.'
                            'The 90th percentile is to be computed for a 5 day window centered on each calendar day '
-                           'for a reference period',
+                           'for a reference period.',
                cell_methods='time: maximum within days time: sum over days',
                compute=indices.tx90p,
                )
@@ -389,7 +390,7 @@ tx10p = Tasmax(identifier='tx10p',
                long_name='Number of days when Tmax < 10th percentile',
                description='{freq} number of days with maximum daily temperature below the 10th percentile.'
                            'The 10th percentile is to be computed for a 5 day window centered on each calendar day '
-                           'for a reference period',
+                           'for a reference period.',
                cell_methods='time: maximum within days time: sum over days',
                compute=indices.tx10p
                )
@@ -400,7 +401,7 @@ tn90p = Tasmin(identifier='tn90p',
                long_name='Number of days when Tmin > 90th percentile',
                description='{freq} number of days with minimum daily temperature above the 90th percentile.'
                            'The 90th percentile is to be computed for a 5 day window centered on each calendar day '
-                           'for a reference period',
+                           'for a reference period.',
                cell_methods='time: minimum within days time: sum over days',
                compute=indices.tn90p,
                )
@@ -411,7 +412,7 @@ tn10p = Tasmin(identifier='tn10p',
                long_name='Number of days when Tmin < 10th percentile',
                description='{freq} number of days with minimum daily temperature below the 10th percentile.'
                            'The 10th percentile is to be computed for a 5 day window centered on each calendar day '
-                           'for a reference period',
+                           'for a reference period.',
                cell_methods='time: minimum within days time: sum over days',
                compute=indices.tn10p
                )
diff --git a/xclim/streamflow.py b/xclim/streamflow.py
index b14b8ff8..29353098 100644
--- a/xclim/streamflow.py
+++ b/xclim/streamflow.py
@@ -47,20 +47,23 @@ base_flow_index = Streamflow(identifier='base_flow_index',
                              compute=base_flow_index)
 
 
-freq_analysis = FA(identifier='q{window}{mode}{indexer}',
+freq_analysis = FA(identifier='freq_analysis',
+                   var_name='q{window}{mode}{indexer}',
                    long_name='N-year return period {mode} {indexer} {window}-day flow',
                    description="Streamflow frequency analysis for the {mode} {indexer} {window}-day flow "
                                "estimated using the {dist} distribution.",
                    compute=generic.frequency_analysis)
 
 
-stats = Stats(identifier='q{indexer}{op}',
+stats = Stats(identifier='stats',
+              var_name='q{indexer}{op}',
               long_name='{freq} {op} of {indexer} daily flow ',
               description="{freq} {op} of {indexer} daily flow",
               compute=generic.select_resample_op)
 
 
-doy_qmax = Streamflow(identifier='q{indexer}_doy_qmax',
+doy_qmax = Streamflow(identifier='doy_qmax',
+                      var_name='q{indexer}_doy_qmax',
                       long_name='Day of the year of the maximum over {indexer}',
                       description='Day of the year of the maximum over {indexer}',
                       units='',
@@ -68,7 +71,8 @@ doy_qmax = Streamflow(identifier='q{indexer}_doy_qmax',
                       compute=wrapped_partial(generic.select_resample_op, op=generic.doymax))
 
 
-doy_qmin = Streamflow(identifier='q{indexer}_doy_qmin',
+doy_qmin = Streamflow(identifier='doy_qmin',
+                      var_name='q{indexer}_doy_qmin',
                       long_name='Day of the year of the minimum over {indexer}',
                       description='Day of the year of the minimum over {indexer}',
                       units='',
diff --git a/xclim/utils.py b/xclim/utils.py
index 2df241ee..5f7dca09 100644
--- a/xclim/utils.py
+++ b/xclim/utils.py
@@ -10,7 +10,7 @@ import datetime as dt
 import functools
 import re
 import warnings
-from collections import defaultdict
+from collections import defaultdict, OrderedDict
 from inspect import signature
 
 import numpy as np
@@ -246,12 +246,14 @@ def _check_units(val, dim):
         tu = 'mmday'
     elif dim == '[discharge]':
         tu = 'cms'
+    elif dim == '[length]':
+        tu = 'm'
     else:
         raise NotImplementedError
 
     try:
         (1 * units2pint(val)).to(tu, 'hydro')
-    except pint.UndefinedUnitError:
+    except (pint.UndefinedUnitError, pint.DimensionalityError):
         raise AttributeError("Value's dimension {} does not match expected units {}.".format(val_dim, expected))
 
 
@@ -567,11 +569,15 @@ def walk_map(d, func):
 
 # This class needs to be subclassed by individual indicator classes defining metadata information, compute and
 # missing functions. It can handle indicators with any number of forcing fields.
-class Indicator(object):
+class Indicator():
     r"""Climate indicator based on xarray
     """
-    # Unique ID for registry. May use tags {<tag>} that will be formatted at runtime.
+    # Unique ID for function registry.
     identifier = ''
+
+    # Output variable name. May use tags {<tag>} that will be formatted at runtime.
+    var_name = ''
+
     _nvar = 1
 
     # CF-Convention metadata to be attributed to the output variable. May use tags {<tag>} formatted at runtime.
@@ -600,7 +606,7 @@ class Indicator(object):
                                     'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'},
                       'description': {'YS': 'Annual', 'MS': 'Monthly', 'QS-DEC': 'Seasonal', 'DJF': 'winter',
                                       'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'},
-                      'identifier': {'DJF': 'winter', 'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'}}
+                      'var_name': {'DJF': 'winter', 'MAM': 'spring', 'JJA': 'summer', 'SON': 'fall'}}
 
     for k, v in _attrs_mapping.items():
         v.update(months)
@@ -613,14 +619,18 @@ class Indicator(object):
 
     def __init__(self, **kwds):
 
+        # Set instance attributes.
         for key, val in kwds.items():
             setattr(self, key, val)
 
-        # Sanity checks
-        required = ['compute', ]
-        for key in required:
-            if not getattr(self, key):
-                raise ValueError("{} needs to be defined during instantiation.".format(key))
+        # Verify that the identifier is a proper slug
+        if not re.match(r'^[-\w]+$', self.identifier):
+            warnings.warn("The identifier contains non-alphanumeric characters. It could make life "
+                          "difficult for downstream software reusing this class.", UserWarning)
+
+        # Default value for `var_name` is the `identifier`.
+        if self.var_name == '':
+            self.var_name = self.identifier
 
         # Extract information from the `compute` function.
         # The signature
@@ -665,8 +675,18 @@ class Indicator(object):
 
         # Update attributes
         out_attrs = self.json(ba.arguments)
-        formatted_id = out_attrs.pop('identifier')
-        attrs['history'] += '[{:%Y-%m-%d %H:%M:%S}] {}{}'.format(dt.datetime.now(), formatted_id, ba.signature)
+        vname = out_attrs.pop('var_name')
+
+        # Update the signature with the values of the actual call.
+        cp = OrderedDict()
+        for (k, v) in ba.signature.parameters.items():
+            if v.default is not None and isinstance(v.default, (float, int, str)):
+                cp[k] = v.replace(default=ba.arguments[k])
+            else:
+                cp[k] = v
+
+        attrs['history'] += '[{:%Y-%m-%d %H:%M:%S}] {}: {}{}'.format(
+            dt.datetime.now(), vname, self.identifier, ba.signature.replace(parameters=cp.values()))
         attrs['cell_methods'] += out_attrs.pop('cell_methods')
         attrs.update(out_attrs)
 
@@ -694,7 +714,7 @@ class Indicator(object):
         mask = self.missing(*mba.args, **mba.kwargs)
         ma_out = out.where(~mask)
 
-        return ma_out.rename(formatted_id)
+        return ma_out.rename(vname)
 
     @property
     def cf_attrs(self):
@@ -711,7 +731,7 @@ class Indicator(object):
         This is meant to be used by a third-party library wanting to wrap this class into another interface.
 
         """
-        names = ['identifier', 'abstract', 'keywords']
+        names = ['identifier', 'var_name', 'abstract', 'keywords']
         out = {key: getattr(self, key) for key in names}
         out.update(self.cf_attrs)
         out = self.format(out, args)
@@ -731,12 +751,20 @@ class Indicator(object):
         Warn of potential issues."""
         return True
 
-    @abc.abstractmethod
     def compute(*args, **kwds):
         """The function computing the indicator."""
+        raise NotImplementedError
 
     def format(self, attrs, args=None):
-        """Format attributes including {} tags with arguments."""
+        """Format attributes including {} tags with arguments.
+
+        Parameters
+        ----------
+        attrs: dict
+          Attributes containing tags to replace with arguments' values.
+        args : dict
+          Function call arguments.
+        """
         if args is None:
             return attrs
 
@@ -753,8 +781,12 @@ class Indicator(object):
                         if dk == 'month':
                             dv = 'm{}'.format(dv)
                         mba[k] = '{{{}}}'.format(dv)
+                elif isinstance(v, units.Quantity):
+                    mba[k] = '{:g~P}'.format(v)
+                elif isinstance(v, (int, float)):
+                    mba[k] = '{:g}'.format(v)
                 else:
-                    mba[k] = int(v) if (isinstance(v, float) and v % 1 == 0) else v
+                    mba[k] = v
 
             out[key] = val.format(**mba).format(**self._attrs_mapping.get(key, {}))
 
@@ -774,12 +806,6 @@ class Indicator(object):
         Raise error if conditions are not met."""
         checks.assert_daily(da)
 
-    @classmethod
-    def factory(cls, attrs):
-        """Create a subclass from the attributes dictionary."""
-        name = attrs['identifier'].capitalize()
-        return type(name, (cls,), attrs)
-
 
 class Indicator2D(Indicator):
     _nvar = 2


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 9176c505..98d89f6a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -31,6 +31,7 @@ from xclim import ensembles
 from xclim import indices
 from xclim import subset
 from xclim import utils
+from xclim import atmos
 from xclim.utils import daily_downsampler, Indicator, format_kwargs, parse_doc, walk_map
 from xclim.utils import infer_doy_max, adjust_doy_calendar, percentile_doy
 from xclim.utils import units, pint2cfunits, units2pint
@@ -179,7 +180,8 @@ class TestDailyDownsampler:
 
 
 class UniIndTemp(Indicator):
-    identifier = 'tmin{thresh}'
+    identifier = 'tmin'
+    var_name = 'tmin{thresh}'
     units = 'K'
     long_name = '{freq} mean surface temperature'
     standard_name = '{freq} mean temperature'
@@ -210,10 +212,11 @@ class TestIndicator:
         import datetime as dt
         a = tas_series(np.arange(360.))
         ind = UniIndTemp()
-        txm = ind(a, freq='YS')
+        txm = ind(a, thresh=5, freq='YS')
         assert txm.cell_methods == 'time: mean within days time: mean within years'
         assert '{:%Y-%m-%d %H}'.format(dt.datetime.now()) in txm.attrs['history']
-        assert txm.name == "tmin0"
+        assert "tmin(da, thresh=5, freq='YS')" in txm.attrs['history']
+        assert txm.name == "tmin5"
 
     def test_temp_unit_conversion(self, tas_series):
         a = tas_series(np.arange(360.))
@@ -229,22 +232,12 @@ class TestIndicator:
         ind = UniIndPr()
         meta = ind.json()
 
-        expected = {'identifier', 'units', 'long_name', 'standard_name', 'cell_methods', 'keywords', 'abstract',
+        expected = {'identifier', 'var_name', 'units', 'long_name', 'standard_name', 'cell_methods', 'keywords',
+                    'abstract',
                     'parameters', 'description', 'history', 'references', 'comment', 'notes'}
 
         assert set(meta.keys()).issubset(expected)
 
-    def test_factory(self, pr_series):
-        attrs = dict(identifier='test', units='days', required_units='[length] / [time]',
-                     long_name='long name',
-                     standard_name='standard name', context='hydro'
-                     )
-        cls = Indicator.factory(attrs)
-
-        assert issubclass(cls, Indicator)
-        da = pr_series(np.arange(365))
-        cls(compute=indices.wetdays)(da)
-
     def test_signature(self):
         from inspect import signature
         ind = UniIndTemp()
@@ -273,6 +266,18 @@ class TestIndicator:
 
         assert isinstance(txc.data, dask.array.core.Array)
 
+    def test_identifier(self):
+
+        with pytest.warns(UserWarning):
+            UniIndPr(identifier='t_{}')
+
+    def test_formatting(self, pr_series):
+        out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.0 * units.mm / units.day)
+        assert out.attrs['long_name'] == 'Number of wet days (precip >= 1 mm/day)'
+
+        out = atmos.wetdays(pr_series(np.arange(366)), thresh=1.5 * units.mm / units.day)
+        assert out.attrs['long_name'] == 'Number of wet days (precip >= 1.5 mm/day)'
+
 
 class TestKwargs:
 
@@ -406,6 +411,21 @@ class TestUnitConversion:
         assert out.units == 'kg m-2'
 
 
+class TestCheckUnits:
+
+    def test_basic(self):
+        utils._check_units('mm/day', '[precipitation]')
+        utils._check_units('mm/s', '[precipitation]')
+        utils._check_units('kg/m2/s', '[precipitation]')
+        utils._check_units('kg/m2', '[length]')
+        utils._check_units('cms', '[discharge]')
+        utils._check_units('m3/s', '[discharge]')
+
+        with pytest.raises(AttributeError):
+            utils._check_units('mm', '[precipitation]')
+            utils._check_units('m3', '[discharge]')
+
+
 class TestSubsetGridPoint:
     nc_poslons = os.path.join(TESTS_DATA, 'cmip3', 'tas.sresb1.giss_model_e_r.run1.atm.da.nc')
     nc_file = os.path.join(TESTS_DATA, 'NRCANdaily', 'nrcan_canada_daily_tasmax_1990.nc')