Repository: google/openhtf

Issue:
Enforce use of Unit objects.
Right now, people can do things like call `WithUnits()` with a string instead of a `Unit`, and OpenHTF is cool with it. We should convert to `Unit` or throw an error. The multi-dimensional declaration probably needs a check too.



Discussion:
Should we just take a string then and always convert to a `units.UOM` value?

That's not a bad idea.


Resolving patch:
diff --git a/openhtf/bin/units_from_xls.py b/openhtf/bin/units_from_xls.py
--- a/openhtf/bin/units_from_xls.py
+++ b/openhtf/bin/units_from_xls.py
@@ -82,10 +82,10 @@
 
     from openhtf.util import units
 
-    # The following three lines are equivalent:
-    @measures(units.METRE_PER_SECOND)
-    @measures(units.Unit('m/s'))
-    @measures(units.Unit('metre per second'))
+    # The following three expressions are equivalent:
+    units.METRE_PER_SECOND
+    units.Unit('m/s')
+    units.Unit('metre per second')
 
 OpenHTF uses UNECE unit codes internally because they are relatively complete
 and modern, and because they are recognized internationally. For full details
@@ -136,6 +136,7 @@ def __call__(self, name_or_suffix):
 UNITS_BY_ALL = {}
 UNITS_BY_ALL.update(UNITS_BY_NAME)
 UNITS_BY_ALL.update(UNITS_BY_SUFFIX)
+UNITS_BY_ALL[None] = NONE
 
 Unit = UnitLookup(UNITS_BY_ALL)
 '''
diff --git a/openhtf/util/measurements.py b/openhtf/util/measurements.py
--- a/openhtf/util/measurements.py
+++ b/openhtf/util/measurements.py
@@ -71,6 +71,7 @@ def WidgetTestPhase(test):
 
 import openhtf
 from openhtf.util import validators
+from openhtf.util import units
 
 _LOG = logging.getLogger(__name__)
 
@@ -126,14 +127,24 @@ def Doc(self, docstring):
     self.docstring = docstring
     return self
 
-  def WithUnits(self, units):
+  def _maybe_make_unit_desc(self, unit_desc):
+    """Return the UnitDescriptor or convert a string to one."""
+    if isinstance(unit_desc, str) or unit_desc is None:
+      unit_desc = units.Unit(unit_desc)
+    if not isinstance(unit_desc, units.UnitDescriptor):
+      raise TypeError('Invalid units for measurement %s: %s' % (self.name,
+                                                                unit_desc))
+    return unit_desc
+
+  def WithUnits(self, unit_desc):
     """Declare the units for this Measurement, returns self for chaining."""
-    self.units = units
+    self.units = self._maybe_make_unit_desc(unit_desc)
     return self
 
   def WithDimensions(self, *dimensions):
     """Declare dimensions for this Measurement, returns self for chaining."""
-    self.dimensions = dimensions
+    self.dimensions = tuple(
+        self._maybe_make_unit_desc(dim) for dim in dimensions)
     return self
 
   def WithValidator(self, validator):
diff --git a/openhtf/util/units.py b/openhtf/util/units.py
--- a/openhtf/util/units.py
+++ b/openhtf/util/units.py
@@ -24,10 +24,10 @@
 
     from openhtf.util import units
 
-    # The following three lines are equivalent:
-    @measures(units.METRE_PER_SECOND)
-    @measures(units.Unit('m/s'))
-    @measures(units.Unit('metre per second'))
+    # The following three expressions are equivalent:
+    units.METRE_PER_SECOND
+    units.Unit('m/s')
+    units.Unit('metre per second')
 
 OpenHTF uses UNECE unit codes internally because they are relatively complete
 and modern, and because they are recognized internationally. For full details
@@ -4254,5 +4254,6 @@ def __call__(self, name_or_suffix):
 UNITS_BY_ALL = {}
 UNITS_BY_ALL.update(UNITS_BY_NAME)
 UNITS_BY_ALL.update(UNITS_BY_SUFFIX)
+UNITS_BY_ALL[None] = NONE
 
 Unit = UnitLookup(UNITS_BY_ALL)


Test patch:
diff --git a/test/util/measurements_test.py b/test/util/measurements_test.py
--- a/test/util/measurements_test.py
+++ b/test/util/measurements_test.py
@@ -91,6 +91,10 @@ def setUpClass(cls):
       with open(RECORD_FILENAME, 'rb') as picklefile:
         cls.record = pickle.load(picklefile)
 
+  def testUnitEnforcement(self):
+    """Creating a measurement with invalid units should raise."""
+    self.assertRaises(TypeError, Measurement('bad_units').WithUnits, 1701)
+
   def testMeasurements(self):
     result = util.NonLocalResult() 
     def _SaveResult(test_record):