From 8b6250df6bf372e2033b16e2dc16a6a602ac9ff1 Mon Sep 17 00:00:00 2001 From: "Erik M. Bray" Date: Fri, 15 May 2015 16:58:06 -0400 Subject: [PATCH 1/5] Possible partial solution to the issue raised in #3777--it doesn't create a new class but it does at least make arrays of Quantity objects printable without breaking or changing too much else. It adds a new UnitConversionError specifically for errors related to that, and is also a ValueError. --- astropy/nddata/compat.py | 8 ++++---- astropy/time/core.py | 3 +++ astropy/units/core.py | 17 ++++++++++++----- astropy/units/quantity_helper.py | 4 ++-- docs/units/conversion.rst | 2 +- docs/units/equivalencies.rst | 6 +++--- docs/units/index.rst | 2 +- 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/astropy/nddata/compat.py b/astropy/nddata/compat.py index a8d15a1b4fe..f34dc6ad3c9 100644 --- a/astropy/nddata/compat.py +++ b/astropy/nddata/compat.py @@ -6,7 +6,7 @@ import numpy as np -from ..units import UnitsError, Unit +from ..units import UnitsError, UnitConversionError, Unit from .. import log from .nddata import NDData @@ -115,9 +115,9 @@ def uncertainty(self, value): try: scaling = (1 * value._unit).to(self.unit) except UnitsError: - raise UnitsError('Cannot convert unit of uncertainty ' - 'to unit of ' - '{0} object.'.format(class_name)) + raise UnitConversionError( + 'Cannot convert unit of uncertainty to unit of ' + '{0} object.'.format(class_name)) value.array *= scaling elif not self.unit and value._unit: # Raise an error if uncertainty has unit and data does not diff --git a/astropy/time/core.py b/astropy/time/core.py index b8e6b4827a5..fa8bdd01c91 100644 --- a/astropy/time/core.py +++ b/astropy/time/core.py @@ -20,6 +20,7 @@ from .. import units as u from .. import _erfa as erfa +from ..units import UnitConversionError from ..utils.compat.odict import OrderedDict from ..utils.compat.misc import override__dir__ from ..extern import six @@ -282,6 +283,8 @@ def _get_time_fmt(self, val, val2, format, scale): try: return FormatClass(val, val2, scale, self.precision, self.in_subfmt, self.out_subfmt) + except UnitConversionError: + raise except (ValueError, TypeError): pass else: diff --git a/astropy/units/core.py b/astropy/units/core.py index 7d245f26d16..b1a38d71e01 100644 --- a/astropy/units/core.py +++ b/astropy/units/core.py @@ -27,8 +27,8 @@ # TODO: Support functional units, e.g. log(x), ln(x) __all__ = [ - 'UnitsError', 'UnitsWarning', 'UnitBase', 'NamedUnit', - 'IrreducibleUnit', 'Unit', 'def_unit', 'CompositeUnit', + 'UnitsError', 'UnitsWarning', 'UnitConversionError', 'UnitBase', + 'NamedUnit', 'IrreducibleUnit', 'Unit', 'def_unit', 'CompositeUnit', 'PrefixUnit', 'UnrecognizedUnit', 'get_current_unit_registry', 'set_enabled_units', 'add_enabled_units', 'set_enabled_equivalencies', 'add_enabled_equivalencies', @@ -451,6 +451,13 @@ class UnitScaleError(UnitsError, ValueError): pass +class UnitConversionError(UnitsError, ValueError): + """ + Used specifically for errors related to converting between units or + interpreting units in terms of other units. + """ + + # Maintain error in old location for backward compatibility from .format import fits as _fits _fits.UnitScaleError = UnitScaleError @@ -837,7 +844,7 @@ def get_err_str(unit): unit_str = get_err_str(orig_unit) other_str = get_err_str(orig_other) - raise UnitsError( + raise UnitConversionError( "{0} and {1} are not convertible".format( unit_str, other_str)) @@ -910,7 +917,7 @@ def _to(self, other): in zip(self_decomposed.bases, other_decomposed.bases))): return self_decomposed.scale / other_decomposed.scale - raise UnitsError( + raise UnitConversionError( "'{0!r}' is not a scaled version of '{1!r}'".format(self, other)) def to(self, other, value=1.0, equivalencies=[]): @@ -1636,7 +1643,7 @@ def decompose(self, bases=set()): return CompositeUnit(scale, [base], [1], _error_check=False) - raise UnitsError( + raise UnitConversionError( "Unit {0} can not be decomposed into the requested " "bases".format(self)) diff --git a/astropy/units/quantity_helper.py b/astropy/units/quantity_helper.py index 04e959a4909..c1a1db0a014 100644 --- a/astropy/units/quantity_helper.py +++ b/astropy/units/quantity_helper.py @@ -2,7 +2,7 @@ # quantities (http://pythonhosted.org/quantities/) package. import numpy as np -from .core import (UnitsError, dimensionless_unscaled, +from .core import (UnitsError, UnitConversionError, dimensionless_unscaled, get_current_unit_registry) from ..utils.compat.fractions import Fraction @@ -279,7 +279,7 @@ def get_converters_and_unit(f, *units): converters[changeable] = get_converter(units[changeable], units[fixed]) except UnitsError: - raise UnitsError( + raise UnitConversionError( "Can only apply '{0}' function to quantities " "with compatible dimensions" .format(f.__name__)) diff --git a/docs/units/conversion.rst b/docs/units/conversion.rst index f005a22ef63..a40af1846af 100644 --- a/docs/units/conversion.rst +++ b/docs/units/conversion.rst @@ -34,7 +34,7 @@ If you attempt to convert to a incompatible unit, an exception will result: >>> cms.to(u.km) Traceback (most recent call last): ... - UnitsError: 'cm / s' (speed) and 'km' (length) are not convertible + UnitConversionError: 'cm / s' (speed) and 'km' (length) are not convertible You can check whether a particular conversion is possible using the `~astropy.units.core.UnitBase.is_equivalent` method:: diff --git a/docs/units/equivalencies.rst b/docs/units/equivalencies.rst index 590ad410944..944147c7529 100644 --- a/docs/units/equivalencies.rst +++ b/docs/units/equivalencies.rst @@ -38,7 +38,7 @@ Length and angles are not normally convertible, so >>> (8.0 * u.arcsec).to(u.parsec) Traceback (most recent call last): ... - UnitsError: 'arcsec' (angle) and 'pc' (length) are not convertible + UnitConversionError: 'arcsec' (angle) and 'pc' (length) are not convertible However, when passing the result of :func:`~astropy.units.equivalencies.parallax` as the third argument to the @@ -68,11 +68,11 @@ dimensionless). For instance, normally the following raise exceptions:: >>> u.degree.to('') Traceback (most recent call last): ... - UnitsError: 'deg' (angle) and '' (dimensionless) are not convertible + UnitConversionError: 'deg' (angle) and '' (dimensionless) are not convertible >>> (u.kg * u.m**2 * (u.cycle / u.s)**2).to(u.J) Traceback (most recent call last): ... - UnitsError: 'cycle2 kg m2 / s2' and 'J' (energy) are not convertible + UnitConversionError: 'cycle2 kg m2 / s2' and 'J' (energy) are not convertible But when passing we pass the proper conversion function, :func:`~astropy.units.equivalencies.dimensionless_angles`, it works. diff --git a/docs/units/index.rst b/docs/units/index.rst index 0798ee5f14c..7ed9a0108f4 100644 --- a/docs/units/index.rst +++ b/docs/units/index.rst @@ -104,7 +104,7 @@ conversion from wavelength to frequency doesn't normally work: >>> (1000 * u.nm).to(u.Hz) Traceback (most recent call last): ... - UnitsError: 'nm' (length) and 'Hz' (frequency) are not convertible + UnitConversionError: 'nm' (length) and 'Hz' (frequency) are not convertible but by passing an equivalency list, in this case ``spectral()``, it does: From 69966af08cd1ea9b282c3dca1626a613c6bc9704 Mon Sep 17 00:00:00 2001 From: "Erik M. Bray" Date: Fri, 15 May 2015 17:08:44 -0400 Subject: [PATCH 2/5] Add a simple test for this --- astropy/units/tests/test_quantity.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/astropy/units/tests/test_quantity.py b/astropy/units/tests/test_quantity.py index 0bdbf3a04d4..3319254d3b2 100644 --- a/astropy/units/tests/test_quantity.py +++ b/astropy/units/tests/test_quantity.py @@ -1155,3 +1155,17 @@ def test_insert(): q2 = q.insert(1, 10 * u.m, axis=1) assert np.all(q2.value == [[ 1, 10, 2], [ 3, 10, 4]]) + + +def test_repr_array_of_quantity(): + """ + Test print/repr of object arrays of Quantity objects with different + units. + + Regression test for the issue first reported in + https://github.com/astropy/astropy/issues/3777 + """ + + a = np.array([1 * u.m, 2 * u.s], dtype=object) + assert repr(a) == 'array([, ], dtype=object)' + assert str(a) == '[ ]' From 71ce94b8739af6ab21e41476ab83f61b68a38b24 Mon Sep 17 00:00:00 2001 From: "Erik M. Bray" Date: Fri, 15 May 2015 17:17:01 -0400 Subject: [PATCH 3/5] Added changelog entry for #3778 [skip ci] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6c78e4e5349..720bd270d9c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -254,6 +254,9 @@ New Features - ``astropy.units`` + - Fixed printing of object ndarrays containing multiple Quantity + objects with differing / incompatible units. [#3778] + - ``astropy.utils`` - ``astropy.vo`` From 3dbc520e64f96f130c1b7530c9b05d7ace42043a Mon Sep 17 00:00:00 2001 From: "Erik M. Bray" Date: Mon, 18 May 2015 10:36:23 -0400 Subject: [PATCH 4/5] Added some clarification about exception changes. [skip ci] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 720bd270d9c..54da3dca909 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -255,7 +255,10 @@ New Features - ``astropy.units`` - Fixed printing of object ndarrays containing multiple Quantity - objects with differing / incompatible units. [#3778] + objects with differing / incompatible units. Note: Unit conversion errors + now cause a ``UnitConversionError`` exception to be raised. However, this + is a subclass of the ``UnitsError`` exception used previously, so existing + code that catches ``UnitsError`` should still work. [#3778] - ``astropy.utils`` From 584a304597c317fb563a30542f3f165d9fb4e059 Mon Sep 17 00:00:00 2001 From: "Erik M. Bray" Date: Mon, 18 May 2015 10:52:51 -0400 Subject: [PATCH 5/5] Fix the test to work on Numpy 1.6.x which has a different output format for object arrays. --- astropy/units/tests/test_quantity.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/astropy/units/tests/test_quantity.py b/astropy/units/tests/test_quantity.py index 3319254d3b2..ba31395f23f 100644 --- a/astropy/units/tests/test_quantity.py +++ b/astropy/units/tests/test_quantity.py @@ -1167,5 +1167,11 @@ def test_repr_array_of_quantity(): """ a = np.array([1 * u.m, 2 * u.s], dtype=object) - assert repr(a) == 'array([, ], dtype=object)' - assert str(a) == '[ ]' + if NUMPY_LT_1_7: + # Numpy 1.6.x has some different defaults for how to display object + # arrays (it uses the str() of the objects instead of the repr() + assert repr(a) == 'array([1.0 m, 2.0 s], dtype=object)' + assert str(a) == '[1.0 m 2.0 s]' + else: + assert repr(a) == 'array([, ], dtype=object)' + assert str(a) == '[ ]'