Repository: hgrecco/pint

Issue:
iter for single value....
Perhaps related to #55, but I can't see where that was resolved...

I have the input to a function that can either be a single value or an array.  I just want the first value of the array or the value.  Note that this has to work with or without pint.  

If I do 

```python
import pint

ureg = pint.UnitRegistry()
ureg.setup_matplotlib(True)

width = 0.25 * ureg.cm
print(np.iterable(width))
print(width[0])
```

I get:
 
```
True
Traceback (most recent call last):
  File "/Users/jklymak/pint/pint/quantity.py", line 1400, in __getitem__
    value = self._magnitude[key]
TypeError: 'float' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "testBar.py", line 13, in <module>
    print(width[0])
  File "/Users/jklymak/pint/pint/quantity.py", line 1404, in __getitem__
    "supports indexing".format(self._magnitude))
TypeError: Neither Quantity object nor its magnitude (0.25)supports indexing
```

Note that `iter(width)` also works fine...

So maybe I'm just missing something, but how do I check if `width` is a singleton?  



Comments:

Patch:
diff --git a/pint/matplotlib.py b/pint/matplotlib.py
index 0d28d43..5b51cf9 100644
--- a/pint/matplotlib.py
+++ b/pint/matplotlib.py
@@ -13,6 +13,8 @@ from __future__ import absolute_import
 
 import matplotlib.units
 
+from .util import iterable, sized
+
 
 class PintAxisInfo(matplotlib.units.AxisInfo):
     """Support default axis and tick labeling and default limits."""
@@ -31,7 +33,7 @@ class PintConverter(matplotlib.units.ConversionInterface):
 
     def convert(self, value, unit, axis):
         """Convert :`Quantity` instances for matplotlib to use."""
-        if hasattr(value,"__iter__"):
+        if iterable(value):
             return [self._convert_value(v, unit, axis) for v in value]
         else:
             return self._convert_value(value, unit, axis)
@@ -51,7 +53,7 @@ class PintConverter(matplotlib.units.ConversionInterface):
     @staticmethod
     def default_units(x, axis):
         """Get the default unit to use for the given combination of unit and axis."""
-        if hasattr(x,"__iter__") and len(x) > 0:
+        if iterable(x) and sized(x):
             return getattr(x[0], 'units', None)
         return getattr(x, 'units', None)
 
diff --git a/pint/quantity.py b/pint/quantity.py
index 3373552..01d7592 100644
--- a/pint/quantity.py
+++ b/pint/quantity.py
@@ -148,22 +148,24 @@ class _Quantity(PrettyIPython, SharedRegistryObject):
 
         inst.__used = False
         inst.__handling = None
-        # Only instances where the magnitude is iterable should have __iter__()
-        if hasattr(inst._magnitude,"__iter__"):
-            inst.__iter__ = cls._iter
+
         return inst
 
-    def _iter(self):
-        """
-        Will be become __iter__() for instances with iterable magnitudes
-        """
-        # # Allow exception to propagate in case of non-iterable magnitude
-        it_mag = iter(self.magnitude)
-        return iter((self.__class__(mag, self._units) for mag in it_mag))
     @property
     def debug_used(self):
         return self.__used
 
+    def __iter__(self):
+        # Make sure that, if self.magnitude is not iterable, we raise TypeError as soon as one
+        # calls iter(self) without waiting for the first element to be drawn from the iterator
+        it_magnitude = iter(self.magnitude)
+
+        def it_outer():
+            for element in it_magnitude:
+                yield self.__class__(element, self._units)
+
+        return it_outer()
+
     def __copy__(self):
         ret = self.__class__(copy.copy(self._magnitude), self._units)
         ret.__used = self.__used
diff --git a/pint/util.py b/pint/util.py
index d89b957..5480b68 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -810,3 +810,32 @@ class BlockIterator(SourceIterator):
         return lineno, line
 
     next = __next__
+
+
+def iterable(y):
+    """Check whether or not an object can be iterated over.
+
+    Vendored from numpy under the terms of the BSD 3-Clause License. (Copyright
+    (c) 2005-2019, NumPy Developers.)
+
+    :param value: Input object.
+    :param type: object
+    """
+    try:
+        iter(y)
+    except TypeError:
+        return False
+    return True
+
+
+def sized(y):
+    """Check whether or not an object has a defined length.
+
+    :param value: Input object.
+    :param type: object
+    """
+    try:
+        len(y)
+    except TypeError:
+        return False
+    return True


Test patch:
diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py
index 360b29b..200115c 100644
--- a/pint/testsuite/test_numpy.py
+++ b/pint/testsuite/test_numpy.py
@@ -235,6 +235,10 @@ class TestNumpyMethods(QuantityTestCase):
         for q, v in zip(self.q.flatten(), [1, 2, 3, 4]):
             self.assertEqual(q, v * self.ureg.m)
 
+    def test_iterable(self):
+        self.assertTrue(np.iterable(self.q))
+        self.assertFalse(np.iterable(1 * self.ureg.m))
+
     def test_reversible_op(self):
         """
         """
diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py
index 41c6903..e5fc076 100644
--- a/pint/testsuite/test_quantity.py
+++ b/pint/testsuite/test_quantity.py
@@ -392,6 +392,19 @@ class TestQuantity(QuantityTestCase):
         u_array_5 = self.Q_.from_list(u_seq)
         self.assertTrue(all(u_array_5 == u_array_ref))
 
+    @helpers.requires_numpy()
+    def test_iter(self):
+        # Verify that iteration gives element as Quantity with same units
+        x = self.Q_([0, 1, 2, 3], 'm')
+        self.assertQuantityEqual(next(iter(x)), self.Q_(0, 'm'))
+
+    def test_notiter(self):
+        # Verify that iter() crashes immediately, without needing to draw any
+        # element from it, if the magnitude isn't iterable
+        x = self.Q_(1, 'm')
+        with self.assertRaises(TypeError):
+            iter(x)
+
 
 class TestQuantityToCompact(QuantityTestCase):
 
diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py
index 915fc66..da6c02e 100644
--- a/pint/testsuite/test_util.py
+++ b/pint/testsuite/test_util.py
@@ -9,7 +9,7 @@ from decimal import Decimal
 from pint.testsuite import BaseTestCase, QuantityTestCase
 from pint.util import (string_preprocessor, find_shortest_path, matrix_to_string,
                        transpose, tokenizer, find_connected_nodes, ParserHelper,
-                       UnitsContainer, to_units_container)
+                       UnitsContainer, to_units_container, iterable, sized)
 
 
 class TestUnitsContainer(QuantityTestCase):
@@ -333,3 +333,22 @@ class TestMatrix(BaseTestCase):
     def test_transpose(self):
 
         self.assertEqual(transpose([[1, 2], [3, 4]]), [[1, 3], [2, 4]])
+
+
+class TestOtherUtils(BaseTestCase):
+
+    def test_iterable(self):
+
+        # Test with list, string, generator, and scalar
+        self.assertTrue(iterable([0, 1, 2, 3]))
+        self.assertTrue(iterable('test'))
+        self.assertTrue(iterable((i for i in range(5))))
+        self.assertFalse(iterable(0))
+
+    def test_sized(self):
+
+        # Test with list, string, generator, and scalar
+        self.assertTrue(sized([0, 1, 2, 3]))
+        self.assertTrue(sized('test'))
+        self.assertFalse(sized((i for i in range(5))))
+        self.assertFalse(sized(0))