Repository: hgrecco/pint

Issue:
unit redefinition in a context breaks transform in another context
```python
ureg = pint.UnitRegistry()
for i in range(2):
    print(f"run {i}")
    ctx = pint.Context.from_lines(["@context _", "cal = 4 J"])
    with ureg.context("sp", ctx):
        u = ureg.Unit("nm")
        q = ureg.Quantity(1, u)
        q.to('J')
```
Output:
```
run 0
run 1
KeyError: 'nanometer'
```
The first run never fails.
The failure on the second run only happens if
1. ctx is recreated from lines between runs, and
2. the Unit instance is created while the contexts are enabled, and
3. the target unit is one used as a source in a redefinition in ctx (``q.to('ft')`` has no problem)

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index d3e751f..d2fddea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ dist/
 MANIFEST
 *pytest_cache*
 .eggs
+.mypy_cache
 
 # WebDAV file system cache files
 .DAV/
diff --git a/CHANGES b/CHANGES
index 39f8248..b21be59 100644
--- a/CHANGES
+++ b/CHANGES
@@ -20,6 +20,8 @@ Pint Changelog
   disequality (Issue #1051, Thanks Guido Imperiale)
 - Fixed issue where quantities with a very large magnitude would throw an IndexError 
   when using to_compact()
+- Fixed crash when a Unit with prefix is declared for the first time while a Context
+  containing unit redefinitions is active (Issue #1062, Thanks Guido Imperiale)
 
 
 0.11 (2020-02-19)
diff --git a/pint/quantity.py b/pint/quantity.py
index 3c5bb86..454c462 100644
--- a/pint/quantity.py
+++ b/pint/quantity.py
@@ -17,6 +17,7 @@ import numbers
 import operator
 import re
 import warnings
+from typing import List
 
 from pkg_resources.extern.packaging import version
 
@@ -1795,38 +1796,46 @@ class Quantity(PrettyIPython, SharedRegistryObject):
 
         return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units)
 
+    def _get_unit_definition(self, unit: str) -> UnitDefinition:
+        try:
+            return self._REGISTRY._units[unit]
+        except KeyError:
+            # pint#1062: The __init__ method of this object added the unit to
+            # UnitRegistry._units (e.g. units with prefix are added on the fly the
+            # first time they're used) but the key was later removed, e.g. because
+            # a Context with unit redefinitions was deactivated.
+            self._REGISTRY.parse_units(unit)
+            return self._REGISTRY._units[unit]
+
     # methods/properties that help for math operations with offset units
     @property
-    def _is_multiplicative(self):
+    def _is_multiplicative(self) -> bool:
         """Check if the Quantity object has only multiplicative units."""
         return not self._get_non_multiplicative_units()
 
-    def _get_non_multiplicative_units(self):
+    def _get_non_multiplicative_units(self) -> List[str]:
         """Return a list of the of non-multiplicative units of the Quantity object."""
-        offset_units = [
+        return [
             unit
-            for unit in self._units.keys()
-            if not self._REGISTRY._units[unit].is_multiplicative
+            for unit in self._units
+            if not self._get_unit_definition(unit).is_multiplicative
         ]
-        return offset_units
 
-    def _get_delta_units(self):
+    def _get_delta_units(self) -> List[str]:
         """Return list of delta units ot the Quantity object."""
-        delta_units = [u for u in self._units.keys() if u.startswith("delta_")]
-        return delta_units
+        return [u for u in self._units if u.startswith("delta_")]
 
-    def _has_compatible_delta(self, unit):
+    def _has_compatible_delta(self, unit: str) -> bool:
         """"Check if Quantity object has a delta_unit that is compatible with unit
         """
         deltas = self._get_delta_units()
         if "delta_" + unit in deltas:
             return True
-        else:  # Look for delta units with same dimension as the offset unit
-            offset_unit_dim = self._REGISTRY._units[unit].reference
-            for d in deltas:
-                if self._REGISTRY._units[d].reference == offset_unit_dim:
-                    return True
-        return False
+        # Look for delta units with same dimension as the offset unit
+        offset_unit_dim = self._get_unit_definition(unit).reference
+        return any(
+            self._get_unit_definition(d).reference == offset_unit_dim for d in deltas
+        )
 
     def _ok_for_muldiv(self, no_offset_units=None):
         """Checks if Quantity object can be multiplied or divided


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 230b797..a3be41f 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -459,6 +459,19 @@ class TestIssues(QuantityTestCase):
         p = (q ** q).m
         np.testing.assert_array_equal(p, a ** a)
 
+    def test_issue507(self):
+        # leading underscore in unit works with numbers
+        ureg.define("_100km = 100 * kilometer")
+        battery_ec = 16 * ureg.kWh / ureg._100km  # noqa: F841
+        # ... but not with text
+        ureg.define("_home = 4700 * kWh / year")
+        with self.assertRaises(AttributeError):
+            home_elec_power = 1 * ureg._home  # noqa: F841
+        # ... or with *only* underscores
+        ureg.define("_ = 45 * km")
+        with self.assertRaises(AttributeError):
+            one_blank = 1 * ureg._  # noqa: F841
+
     def test_issue523(self):
         src, dst = UnitsContainer({"meter": 1}), UnitsContainer({"degF": 1})
         value = 10.0
@@ -653,28 +666,32 @@ class TestIssues(QuantityTestCase):
         with self.assertRaises(DimensionalityError):
             q.to("joule")
 
-    def test_issue507(self):
-        # leading underscore in unit works with numbers
-        ureg.define("_100km = 100 * kilometer")
-        battery_ec = 16 * ureg.kWh / ureg._100km  # noqa: F841
-        # ... but not with text
-        ureg.define("_home = 4700 * kWh / year")
-        with self.assertRaises(AttributeError):
-            home_elec_power = 1 * ureg._home  # noqa: F841
-        # ... or with *only* underscores
-        ureg.define("_ = 45 * km")
-        with self.assertRaises(AttributeError):
-            one_blank = 1 * ureg._  # noqa: F841
-
     def test_issue960(self):
         q = (1 * ureg.nanometer).to_compact("micrometer")
         assert q.units == ureg.nanometer
         assert q.magnitude == 1
 
+    @helpers.requires_numpy()
+    def test_issue973(self):
+        """Verify that an empty array Quantity can be created through multiplication."""
+        q0 = np.array([]) * ureg.m  # by Unit
+        q1 = np.array([]) * ureg("m")  # by Quantity
+        assert isinstance(q0, ureg.Quantity)
+        assert isinstance(q1, ureg.Quantity)
+        assert len(q0) == len(q1) == 0
+
+    def test_issue1062(self):
+        # Must not be used by any other tests
+        assert "nanometer" not in ureg._units
+        for i in range(2):
+            ctx = Context.from_lines(["@context _", "cal = 4 J"])
+            with ureg.context("sp", ctx):
+                q = ureg.Quantity(1, "nm")
+                q.to("J")
+
 
-try:
+if np is not None:
 
-    @pytest.mark.skipif(np is None, reason="NumPy is not available")
     @pytest.mark.parametrize(
         "callable",
         [
@@ -706,17 +723,3 @@ try:
         type_before = type(q._magnitude)
         callable(q)
         assert isinstance(q._magnitude, type_before)
-
-    @pytest.mark.skipif(np is None, reason="NumPy is not available")
-    def test_issue973():
-        """Verify that an empty array Quantity can be created through multiplication."""
-        q0 = np.array([]) * ureg.m  # by Unit
-        q1 = np.array([]) * ureg("m")  # by Quantity
-        assert isinstance(q0, ureg.Quantity)
-        assert isinstance(q1, ureg.Quantity)
-        assert len(q0) == len(q1) == 0
-
-
-except AttributeError:
-    # Calling attributes on np will fail if NumPy is not available
-    pass