Repository: hgrecco/pint

Issue:
`infer_base_unit` and `to_compact` break with `non_int_type=Decimal` and Decimal Quantity, dependent on prior actions.
When using `non_int_type=Decimal`, some unit conversions fail, some don't fail, and some unit conversions that fail initially start working after certain others are made.

Whether conversions fail appears to depend on how the quantities are instantiated.  Using string parsing with the UnitRegistry, eg, `ureg("1000.0 nm")` will often work.  Using a direct instantiation, eg, `ureg.Quantity(Decimal("1000.0")`, will not work, even though the two are equal.  However,  a conversion done on the first will cause future conversions of the second to work.  As a minimal example:

```python
import pint
from decimal import Decimal
ureg = pint.UnitRegistry(non_int_type=Decimal)
print(ureg.Quantity(Decimal("1000.0"), "1.0 nm").to_compact()) # fails with TypeError: unsupported operand type(s) for *: 'float' and 'Decimal'
print(ureg("1000.0 nm").to_compact()) # Doesn't fail
print(ureg.Quantity(Decimal("1000.0"), "1.0 nm").to_compact()) # Now this doesn't fail
```

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 2de0a4e..ba0397c 100644
--- a/CHANGES
+++ b/CHANGES
@@ -16,6 +16,7 @@ Pint Changelog
 - Use default numpy `np.printoptions` available since numpy 1.15.
 - Implement `numpy.nanprod` (Issue #1369)
 - Fix default_format ignored for measurement (Issue #1456)
+- Fix `to_compact` and `infer_base_unit` for non-float non_int_type.
 
 ### Breaking Changes
 
diff --git a/pint/quantity.py b/pint/quantity.py
index 1305a56..03b9e84 100644
--- a/pint/quantity.py
+++ b/pint/quantity.py
@@ -851,9 +851,9 @@ class Quantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]):
         SI_bases = [item[1] for item in SI_prefixes_list]
 
         if unit is None:
-            unit = infer_base_unit(self)
+            unit = infer_base_unit(self, registry=self._REGISTRY)
         else:
-            unit = infer_base_unit(self.__class__(1, unit))
+            unit = infer_base_unit(self.__class__(1, unit), registry=self._REGISTRY)
 
         q_base = self.to(unit)
 
@@ -868,9 +868,9 @@ class Quantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]):
             unit_str, unit_power = units[0]
 
         if unit_power > 0:
-            power = math.floor(math.log10(abs(magnitude)) / unit_power / 3) * 3
+            power = math.floor(math.log10(abs(magnitude)) / float(unit_power) / 3) * 3
         else:
-            power = math.ceil(math.log10(abs(magnitude)) / unit_power / 3) * 3
+            power = math.ceil(math.log10(abs(magnitude)) / float(unit_power) / 3) * 3
 
         index = bisect.bisect_left(SI_powers, power)
 
diff --git a/pint/util.py b/pint/util.py
index 6a8c2f9..d72f15f 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -906,29 +906,50 @@ def to_units_container(
             return UnitsContainer(unit_like)
 
 
-def infer_base_unit(q):
+def infer_base_unit(
+    unit_like: Union[UnitLike, Quantity], registry: Optional[BaseRegistry] = None
+) -> UnitsContainer:
     """
+    Given a Quantity or UnitLike, give the UnitsContainer for it's base units.
 
     Parameters
     ----------
-    q :
+    unit_like : Union[UnitLike, Quantity]
+        Quantity or Unit to infer the base units from.
 
+    registry: Optional[BaseRegistry]
+        If provided, uses the registry's UnitsContainer and parse_unit_name.  If None,
+        uses the registry attached to unit_like.
 
     Returns
     -------
-    type
+    UnitsContainer
 
+    Raises
+    ------
+    ValueError
+        The unit_like did not reference a registry, and no registry was provided.
 
     """
     d = udict()
-    for unit_name, power in q._units.items():
-        candidates = q._REGISTRY.parse_unit_name(unit_name)
+
+    original_units = to_units_container(unit_like, registry)
+
+    if registry is None and hasattr(unit_like, "_REGISTRY"):
+        registry = unit_like._REGISTRY
+    if registry is None:
+        raise ValueError("No registry provided.")
+
+    for unit_name, power in original_units.items():
+        candidates = registry.parse_unit_name(unit_name)
         assert len(candidates) == 1
         _, base_unit, _ = candidates[0]
         d[base_unit] += power
 
     # remove values that resulted in a power of 0
-    return UnitsContainer({k: v for k, v in d.items() if v != 0})
+    nonzero_dict = {k: v for k, v in d.items() if v != 0}
+
+    return registry.UnitsContainer(nonzero_dict)
 
 
 def getattr_maybe_raise(self, item):


Test patch:
diff --git a/pint/testsuite/test_infer_base_unit.py b/pint/testsuite/test_infer_base_unit.py
index 54a4720..f2605c6 100644
--- a/pint/testsuite/test_infer_base_unit.py
+++ b/pint/testsuite/test_infer_base_unit.py
@@ -1,4 +1,10 @@
+from decimal import Decimal
+from fractions import Fraction
+
+import pytest
+
 from pint import Quantity as Q
+from pint import UnitRegistry
 from pint.testsuite import helpers
 from pint.util import infer_base_unit
 
@@ -7,7 +13,44 @@ class TestInferBaseUnit:
     def test_infer_base_unit(self):
         from pint.util import infer_base_unit
 
-        assert infer_base_unit(Q(1, "millimeter * nanometer")) == Q(1, "meter**2").units
+        test_units = Q(1, "meter**2").units
+        registry = Q(1, "meter**2")._REGISTRY
+
+        assert infer_base_unit(Q(1, "millimeter * nanometer")) == test_units
+
+        assert infer_base_unit("millimeter * nanometer", registry) == test_units
+
+        assert (
+            infer_base_unit(Q(1, "millimeter * nanometer").units, registry)
+            == test_units
+        )
+
+        with pytest.raises(ValueError, match=r"No registry provided."):
+            infer_base_unit("millimeter")
+
+    def test_infer_base_unit_decimal(self):
+        from pint.util import infer_base_unit
+
+        ureg = UnitRegistry(non_int_type=Decimal)
+        QD = ureg.Quantity
+
+        ibu_d = infer_base_unit(QD(Decimal("1"), "millimeter * nanometer"))
+
+        assert ibu_d == QD(Decimal("1"), "meter**2").units
+
+        assert all(isinstance(v, Decimal) for v in ibu_d.values())
+
+    def test_infer_base_unit_fraction(self):
+        from pint.util import infer_base_unit
+
+        ureg = UnitRegistry(non_int_type=Fraction)
+        QD = ureg.Quantity
+
+        ibu_d = infer_base_unit(QD(Fraction("1"), "millimeter * nanometer"))
+
+        assert ibu_d == QD(Fraction("1"), "meter**2").units
+
+        assert all(isinstance(v, Fraction) for v in ibu_d.values())
 
     def test_units_adding_to_zero(self):
         assert infer_base_unit(Q(1, "m * mm / m / um * s")) == Q(1, "s").units
@@ -21,6 +64,42 @@ class TestInferBaseUnit:
         r = (Q(1, "m") * Q(1, "mm") / Q(1, "m") / Q(2, "um") * Q(2, "s")).to_compact()
         helpers.assert_quantity_almost_equal(r, Q(1000, "s"))
 
+    def test_to_compact_decimal(self):
+        ureg = UnitRegistry(non_int_type=Decimal)
+        Q = ureg.Quantity
+        r = (
+            Q(Decimal("1000000000.0"), "m")
+            * Q(Decimal("1"), "mm")
+            / Q(Decimal("1"), "s")
+            / Q(Decimal("1"), "ms")
+        )
+        compact_r = r.to_compact()
+        expected = Q(Decimal("1000.0"), "kilometer**2 / second**2")
+        assert compact_r == expected
+
+        r = (
+            Q(Decimal(1), "m") * Q(1, "mm") / Q(1, "m**2") / Q(2, "um") * Q(2, "s")
+        ).to_compact()
+        assert r == Q(1000, "s/m")
+
+    def test_to_compact_fraction(self):
+        ureg = UnitRegistry(non_int_type=Fraction)
+        Q = ureg.Quantity
+        r = (
+            Q(Fraction("10000000000/10"), "m")
+            * Q(Fraction("1"), "mm")
+            / Q(Fraction("1"), "s")
+            / Q(Fraction("1"), "ms")
+        )
+        compact_r = r.to_compact()
+        expected = Q(Fraction("1000.0"), "kilometer**2 / second**2")
+        assert compact_r == expected
+
+        r = (
+            Q(Fraction(1), "m") * Q(1, "mm") / Q(1, "m**2") / Q(2, "um") * Q(2, "s")
+        ).to_compact()
+        assert r == Q(1000, "s/m")
+
     def test_volts(self):
         from pint.util import infer_base_unit