Repository: yt-project/unyt
Pull request #44 (2018-07-31 15:19:37)

Add documentation for the exception classes in unyt
This responds to the suggestion from @ygrange in https://github.com/openjournals/joss-reviews/issues/809#issuecomment-406382226. Thanks so much for pointing this out! For some reason I decided to exclude the exception classes from the code coverage estimates, which was totally unnecessary. Adding docstrings allowed me to catch a few outright bugs or places where we should have been raising custom exceptions but weren't. It also allows me to expand the test coverage even more (cue evil laughter).


Patch:
diff --git a/docs/usage.rst b/docs/usage.rst
index 57254a2..2fc58b6 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -257,8 +257,8 @@ raise an error:
   >>> (1.0*mile).to('lb')  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
   Traceback (most recent call last):
   ...
-  unyt.exceptions.UnitConversionError: Cannot convert between mile (dim
-  (length)) and lb (dim (mass)).
+  unyt.exceptions.UnitConversionError: Cannot convert between 'mile' (dim
+  '(length)') and 'lb' (dim '(mass)').
 
 While we recommend using :meth:`unyt_array.to <unyt.array.unyt_array.to>` in
 most cases to convert arrays or quantities to different units, if you would like
@@ -364,7 +364,7 @@ But converting a more complicated compound unit will raise an error:
   Traceback (most recent call last):
   ...
   unyt.exceptions.UnitsNotReducible: The unit "C*T*V" (dimensions
-  "(length)**2*(mass)**2/((current_mks)*(time)**4)" cannot be reduced to
+  "(length)**2*(mass)**2/((current_mks)*(time)**4)") cannot be reduced to
   an expression within the cgs system of units.
 
 If you need to work with complex expressions involving electromagnetic units, we
diff --git a/setup.cfg b/setup.cfg
index cdfe2a2..f8f5521 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -36,7 +36,6 @@ omit =
     versioneer.py
     unyt/_version.py
     unyt/_on_demand_imports.py
-    unyt/exceptions.py
     unyt/_testing.py
     unyt/tests/test_flake8.py
 
diff --git a/unyt/array.py b/unyt/array.py
index 915a3ba..9e4ef40 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -221,7 +221,7 @@ def _bitop_units(unit1, unit2):
         "Bit-twiddling operators are not defined for unyt_array instances")
 
 
-def _coerce_iterable_units(input_object):
+def _coerce_iterable_units(input_object, registry=None):
     if isinstance(input_object, np.ndarray):
         return input_object
     if _iterable(input_object):
@@ -231,7 +231,7 @@ def _coerce_iterable_units(input_object):
                     for _ in input_object]):
                 raise IterableUnitCoercionError(input_object)
             # This will create a copy of the data in the iterable.
-            return unyt_array(input_object)
+            return unyt_array(np.array(input_object), ff, registry=registry)
     return np.asarray(input_object)
 
 
@@ -522,8 +522,7 @@ class unyt_array(np.ndarray):
             pass
         elif _iterable(input_array) and input_array:
             if isinstance(input_array[0], unyt_array):
-                return unyt_array(np.array(input_array, dtype=dtype),
-                                  input_array[0].units, registry=registry)
+                return _coerce_iterable_units(input_array, registry)
 
         # Input array is an already formed ndarray instance
         # We first cast to be our class type
diff --git a/unyt/exceptions.py b/unyt/exceptions.py
index 6574ab2..c40275a 100644
--- a/unyt/exceptions.py
+++ b/unyt/exceptions.py
@@ -15,6 +15,19 @@ Exception classes defined by unyt
 
 
 class UnitOperationError(ValueError):
+    """An exception that is raised when unit operations are not allowed
+
+    Example
+    -------
+
+    >>> import unyt as u
+    >>> 3*u.g + 4*u.m  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.UnitOperationError: The <ufunc 'add'> operator
+    for unyt_arrays with units "g" (dimensions "(mass)") and
+    "m" (dimensions "(length)") is not well defined.
+    """
     def __init__(self, operation, unit1, unit2=None):
         self.operation = operation
         self.unit1 = unit1
@@ -33,6 +46,19 @@ class UnitOperationError(ValueError):
 
 
 class UnitConversionError(Exception):
+    """An error raised when converting to a unit with different dimensions.
+
+    Example
+    -------
+
+    >>> import unyt as u
+    >>> data = 3*u.g
+    >>> data.to('m')  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.UnitConversionError: Cannot convert between 'g'
+    (dim '(mass)') and 'm' (dim '(length)').
+    """
     def __init__(self, unit1, dimension1, unit2, dimension2):
         self.unit1 = unit1
         self.unit2 = unit2
@@ -41,12 +67,33 @@ class UnitConversionError(Exception):
         Exception.__init__(self)
 
     def __str__(self):
-        err = ("Cannot convert between %s (dim %s) and %s (dim %s)." %
-               (self.unit1, self.dimension1, self.unit2, self.dimension2))
+        err = ("Cannot convert between '%s' (dim '%s') and '%s' "
+               "(dim '%s')." % (self.unit1, self.dimension1, self.unit2,
+                                self.dimension2))
         return err
 
 
 class MissingMKSCurrent(Exception):
+    """Raised when querying a unit system for MKS current dimensions
+
+    Since current is a base dimension for SI or SI-like unit systems but not in
+    CGS or CGS-like unit systems, dimensions that include the MKS current
+    dimension (the dimension of ampere) are not representable in CGS-like unit
+    systems. When a CGS-like unit system is queried for such a dimension, this
+    error is raised.
+
+    Example
+    -------
+
+    >>> from unyt.unit_systems import cgs_unit_system as us
+    >>> from unyt import ampere
+    >>> us[ampere.dimensions]  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.MissingMKSCurrent: The cgs unit system does not
+    have a MKS current base unit
+
+    """
     def __init__(self, unit_system_name):
         self.unit_system_name = unit_system_name
 
@@ -57,41 +104,62 @@ class MissingMKSCurrent(Exception):
 
 
 class MKSCGSConversionError(Exception):
+    """Raised when conversion between MKS and CGS units cannot be performed
+
+    This error is raised and caught internally and will expose itself
+    to the user as part of a chained exception leading to a
+    UnitConversionError.
+    """
     def __init__(self, unit):
         self.unit = unit
 
     def __str__(self):
-        err = ("The %s unit cannot be safely converted." % self.unit)
+        err = ("The '%s' unit cannot be safely converted." % self.unit)
         return err
 
 
 class UnitsNotReducible(Exception):
+    """Raised when a unit cannot be safely represented in a unit system
+
+    Example
+    -------
+
+    >>> from unyt import A, cm
+    >>> data = 12*A/cm
+    >>> data.in_cgs()  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.UnitsNotReducible: The unit "A/cm" (dimensions
+    "(current_mks)/(length)") cannot be reduced to an expression
+    within the cgs system of units.
+    """
     def __init__(self, unit, units_base):
         self.unit = unit
         self.units_base = units_base
         Exception.__init__(self)
 
     def __str__(self):
-        err = ("The unit \"%s\" (dimensions \"%s\" cannot be reduced to an "
+        err = ("The unit \"%s\" (dimensions \"%s\") cannot be reduced to an "
                "expression within the %s system of units." %
                (self.unit, self.unit.dimensions, self.units_base))
         return err
 
 
-class EquivalentDimsError(UnitOperationError):
-    def __init__(self, old_units, new_units, base):
-        self.old_units = old_units
-        self.new_units = new_units
-        self.base = base
-
-    def __str__(self):
-        err = ("It looks like you're trying to convert between \"%s\" and "
-               "\"%s\". Try using \"to_equivalent('%s', '%s')\" instead." %
-               (self.old_units, self.new_units, self.new_units, self.base))
-        return err
-
-
 class IterableUnitCoercionError(Exception):
+    """Raised when an iterable cannot be converted to a unyt_array
+
+    Example
+    -------
+
+    >>> from unyt import km, cm, unyt_array
+    >>> data = [2*cm, 3*km]
+    >>> unyt_array(data)  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.IterableUnitCoercionError: Received a list or
+    tuple of quantities with nonuniform units:
+    [unyt_quantity(2., 'cm'), unyt_quantity(3., 'km')]
+    """
     def __init__(self, quantity_list):
         self.quantity_list = quantity_list
 
@@ -102,6 +170,20 @@ class IterableUnitCoercionError(Exception):
 
 
 class InvalidUnitEquivalence(Exception):
+    """Raised an equivalence does not apply to a unit conversion
+
+    Example
+    -------
+
+    >>> import unyt as u
+    >>> data = 12*u.g
+    >>> data.to('erg', equivalence='thermal')\
+ # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.InvalidUnitEquivalence: The unit equivalence
+    'thermal' does not exist for the units 'g' and 'erg'.
+    """
     def __init__(self, equiv, unit1, unit2):
         self.equiv = equiv
         self.unit1 = unit1
@@ -119,18 +201,73 @@ class InvalidUnitEquivalence(Exception):
 
 
 class InvalidUnitOperation(Exception):
+    """Raised when an operation on a unit object is not allowed
+
+    Example
+    -------
+
+    >>> from unyt import cm, g
+    >>> cm + g  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.InvalidUnitOperation: addition with unit objects
+    is not allowed
+    """
     pass
 
 
 class SymbolNotFoundError(Exception):
+    """Raised when a unit name is not available in a unit registry
+
+    Example
+    -------
+
+    >>> from unyt.unit_registry import default_unit_registry
+    >>> default_unit_registry['made_up_unit']\
+  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.SymbolNotFoundError: The symbol 'made_up_unit'
+    does not exist in this registry.
+    """
     pass
 
 
 class UnitParseError(Exception):
+    """Raised when a string unit name is not parseable as a valid unit
+
+    Example
+    -------
+
+    >>> from unyt import Unit
+    >>> Unit('hello')  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.UnitParseError: Could not find unit symbol
+    'hello' in the provided symbols.
+    """
     pass
 
 
 class IllDefinedUnitSystem(Exception):
+    """Raised when the dimensions of the base units of a unit system are
+    inconsistent.
+
+    Example
+    -------
+
+    >>> from unyt.unit_systems import UnitSystem
+    >>> UnitSystem('atomic', 'nm', 'fs', 'nK', 'rad')\
+  # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
+    Traceback (most recent call last):
+    ...
+    unyt.exceptions.IllDefinedUnitSystem: Cannot create unit system
+    with inconsistent mapping from
+    dimensions to units. Received:
+    OrderedDict([((length), nm), ((mass), fs), ((time), nK),
+                 ((temperature), rad), ((angle), rad),
+                 ((current_mks), A), ((luminous_intensity), cd)])
+    """
     def __init__(self, units_map):
         self.units_map = units_map
 
diff --git a/unyt/unit_object.py b/unyt/unit_object.py
index 5872b73..902a55f 100644
--- a/unyt/unit_object.py
+++ b/unyt/unit_object.py
@@ -248,7 +248,7 @@ class Unit(object):
                 unit_expr = parse_expr(unit_expr, global_dict=global_dict,
                                        transformations=unit_text_transform)
             except SyntaxError as e:
-                msg = ("Unit expression %s raised an error "
+                msg = ("Unit expression '%s' raised an error "
                        "during parsing:\n%s" % (unit_expr, repr(e)))
                 raise UnitParseError(msg)
         # Simplest case. If user passes a Unit object, just use the expr.
@@ -269,7 +269,7 @@ class Unit(object):
         # Make sure we have an Expr at this point.
         if not isinstance(unit_expr, Expr):
             raise UnitParseError("Unit representation must be a string or "
-                                 "sympy Expr. %s has type %s."
+                                 "sympy Expr. '%s' has type '%s'."
                                  % (unit_expr, type(unit_expr)))
 
         # this is slightly faster if unit_expr is the same object as
@@ -299,7 +299,7 @@ class Unit(object):
                 base_value = float(base_value)
             except ValueError:
                 raise UnitParseError("Could not use base_value as a float. "
-                                     "base_value is '%s' (type %s)."
+                                     "base_value is '%s' (type '%s')."
                                      % (base_value, type(base_value)))
 
             # check that dimensions is valid
diff --git a/unyt/unit_registry.py b/unyt/unit_registry.py
index 55e7d13..783732b 100644
--- a/unyt/unit_registry.py
+++ b/unyt/unit_registry.py
@@ -47,7 +47,12 @@ class UnitRegistry:
             self.lut.update(default_unit_symbol_lut)
 
     def __getitem__(self, key):
-        return self.lut[key]
+        try:
+            ret = self.lut[key]
+        except KeyError:
+            raise SymbolNotFoundError(
+                "The symbol '%s' does not exist in this registry." % key)
+        return ret
 
     def __contains__(self, item):
         if str(item) in self.lut:


Test patch:
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index 6b5b143..ddab37e 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -1920,6 +1920,8 @@ def test_coerce_iterable():
         a + b
     with pytest.raises(IterableUnitCoercionError):
         b + a
+    with pytest.raises(IterableUnitCoercionError):
+        unyt_array(b)
 
 
 def test_bypass_validation():