Repository: hgrecco/pint

Issue:
Bug in compound unit dimensionality/unit reduction after caching?
I came across a bizarre bug that popped up on the master branch recently:

```python
import pint
ureg = pint.UnitRegistry()
print(ureg('joule').to_base_units())
print(ureg('joule * second ** 2 / kilogram / meter').to_base_units())
```

gives the following traceback

```
DimensionalityError                       Traceback (most recent call last)
<ipython-input-18-62589ad7e050> in <module>
----> 1 print((1 * ureg.joule * ureg.second ** 2 / ureg.kilogram / ureg.meter).to_base_units())

~/dev/pint/pint/quantity.py in to_base_units(self)
    478         _, other = self._REGISTRY._get_base_units(self._units)
    479 
--> 480         magnitude = self._convert_magnitude_not_inplace(other)
    481 
    482         return self.__class__(magnitude, other)

~/dev/pint/pint/quantity.py in _convert_magnitude_not_inplace(self, other, *contexts, **ctx_kwargs)
    406                 return self._REGISTRY.convert(self._magnitude, self._units, other)
    407 
--> 408         return self._REGISTRY.convert(self._magnitude, self._units, other)
    409 
    410     def _convert_magnitude(self, other, *contexts, **ctx_kwargs):

~/dev/pint/pint/registry.py in convert(self, value, src, dst, inplace)
    707             return value
    708 
--> 709         return self._convert(value, src, dst, inplace)
    710 
    711     def _convert(self, value, src, dst, inplace=False, check_dimensionality=True):

~/dev/pint/pint/registry.py in _convert(self, value, src, dst, inplace)
   1237                 value, src = src._magnitude, src._units
   1238 
-> 1239         return super(ContextRegistry, self)._convert(value, src, dst, inplace)
   1240 
   1241     def _get_compatible_units(self, input_units, group_or_system):

~/dev/pint/pint/registry.py in _convert(self, value, src, dst, inplace)
    990 
    991         if not (src_offset_unit or dst_offset_unit):
--> 992             return super(NonMultiplicativeRegistry, self)._convert(value, src, dst, inplace)
    993 
    994         src_dim = self._get_dimensionality(src)

~/dev/pint/pint/registry.py in _convert(self, value, src, dst, inplace, check_dimensionality)
    729             # then the conversion cannot be performed.
    730             if src_dim != dst_dim:
--> 731                 raise DimensionalityError(src, dst, src_dim, dst_dim)
    732 
    733         # Here src and dst have only multiplicative units left. Thus we can

DimensionalityError: Cannot convert from 'joule * second ** 2 / kilogram / meter' ([length]) to 'dimensionless' (dimensionless)
```

but it works fine after removing the `joule` line.

After digging through the commit history, it seems to lead back to https://github.com/hgrecco/pint/commit/a9a97ba98167a6a20df874a14343d303a3cd2163, since any point in the history I checked including that commit fails with this error, and any without it returns the expected result of `1.0 meter` for the second line. Glancing through the changes made there, it makes sense that it is tied to this seemingly cache-related issue. That being said, I have no clue what is particularly going wrong here or what exactly in https://github.com/hgrecco/pint/commit/a9a97ba98167a6a20df874a14343d303a3cd2163 could have caused this.

In case it helps with troubleshooting...

...the error no longer arises when any of the compound pieces of the last unit are removed.

...it still arises with the following:

```python
import pint
ureg = pint.UnitRegistry()
print((1 * ureg.joule).to_base_units())
print((1 * ureg.joule * ureg.second ** 2 / ureg.kilogram / ureg.meter).to_base_units())
```

...but does not arise with the following:

```python
import pint
ureg = pint.UnitRegistry()
print((1 * ureg.joule).to_base_units())
print((1 * ureg.joule * ureg.second ** 2 / ureg.kilogram / ureg.meter).to('m'))
```

Another oddity is that, despite what the last non-failing example may suggest, this first arose in MetPy through a function that does not use `.to_base_units`, but rather just `.to`. However, after spending a decent amount of time on it, I can't seem to come up with a short example that replicates the failure with `.to`.

ping @crusaderky, in case you may have any insight here with the seemingly problematic commit from https://github.com/hgrecco/pint/pull/864.

Discussion:

Patch:
diff --git a/pint/util.py b/pint/util.py
index 587517a..d89b957 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -327,9 +327,16 @@ class UnitsContainer(Mapping):
 
     def __eq__(self, other):
         if isinstance(other, UnitsContainer):
-            # Not the same as hash(self); see ParserHelper.__hash__ and __eq__
-            return UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other)
-        if isinstance(other, string_types):
+            # UnitsContainer.__hash__(self) is not the same as hash(self); see
+            # ParserHelper.__hash__ and __eq__.
+            # Different hashes guarantee that the actual contents are different, but
+            # identical hashes give no guarantee of equality.
+            # e.g. in CPython, hash(-1) == hash(-2)
+            if UnitsContainer.__hash__(self) != UnitsContainer.__hash__(other):
+                return False
+            other = other._d
+
+        elif isinstance(other, string_types):
             other = ParserHelper.from_string(other)
             other = other._d
 
@@ -504,9 +511,11 @@ class ParserHelper(UnitsContainer):
         self._d, self._hash, self.scale = state
 
     def __eq__(self, other):
-        if isinstance(other, self.__class__):
-            return self.scale == other.scale and\
+        if isinstance(other, ParserHelper):
+            return (
+                self.scale == other.scale and
                 super(ParserHelper, self).__eq__(other)
+            )
         elif isinstance(other, string_types):
             return self == ParserHelper.from_string(other)
         elif isinstance(other, Number):


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 6ca7035..e031fe5 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -11,7 +11,7 @@ from pint import UnitRegistry
 from pint.unit import UnitsContainer
 from pint.util import ParserHelper
 
-from pint.compat import np, long_type
+from pint.compat import np
 from pint.errors import UndefinedUnitError, DimensionalityError
 from pint.testsuite import QuantityTestCase, helpers
 
@@ -699,3 +699,21 @@ class TestIssues(QuantityTestCase):
         ureg2.define('test123 = 456 kg')
         assert ureg1('1 test123').to('kg').magnitude == 123
         assert ureg2('1 test123').to('kg').magnitude == 456
+
+    def test_issue876(self):
+        # Same hash must not imply equality.
+
+        # As an implementation detail of CPython, hash(-1) == hash(-2).
+        # This test is useless in potential alternative Python implementations where
+        # hash(-1) != hash(-2); one would need to find hash collisions specific for each
+        # implementation
+
+        a = UnitsContainer({"[mass]": -1})
+        b = UnitsContainer({"[mass]": -2})
+        c = UnitsContainer({"[mass]": -3})
+
+        # Guarantee working on alternative Python implementations
+        assert (hash(-1) == hash(-2)) == (hash(a) == hash(b))
+        assert (hash(-1) == hash(-3)) == (hash(a) == hash(c))
+        assert a != b
+        assert a != c