Repository: hgrecco/pint

Issue:
deepcopy(ureg) broken
As of pint 0.9:
```python
import pint
import copy
ureg = pint.UnitRegistry()
copy.deepcopy(ureg)
```
Output:
```
lib/python3.7/copy.py in _deepcopy_dict(x, memo, deepcopy)
    238     memo[id(x)] = y
    239     for key, value in x.items():
--> 240         y[deepcopy(key, memo)] = deepcopy(value, memo)
    241     return y
    242 d[dict] = _deepcopy_dict

lib/python3.7/site-packages/pint/util.py in __hash__(self)
    475 
    476     def __hash__(self):
--> 477         if self.scale != 1.0:
    478             mess = 'Only scale 1.0 ParserHelper instance should be considered hashable'
    479             raise ValueError(mess)

AttributeError: scale
```
```python
>>> c = pint.util.ParserHelper(scale=1)
>>> c2 = copy.deepcopy(c)
>>> c2.scale
AttributeError: scale
```

Discussion:

Patch:
diff --git a/pint/registry.py b/pint/registry.py
index 0129ba5..71e45a8 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -32,6 +32,7 @@
 
 from __future__ import division, unicode_literals, print_function, absolute_import
 
+import copy
 import os
 import re
 import math
@@ -47,7 +48,8 @@ from tokenize import NUMBER, NAME
 
 from . import registry_helpers
 from .context import Context, ContextChain
-from .util import (logger, pi_theorem, solve_dependencies, ParserHelper,
+from .util import (getattr_maybe_raise,
+                   logger, pi_theorem, solve_dependencies, ParserHelper,
                    string_preprocessor, find_connected_nodes,
                    find_shortest_path, UnitsContainer, _is_dim,
                    to_units_container, SourceIterator)
@@ -115,15 +117,7 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
     def __init__(self, filename='', force_ndarray=False, on_redefinition='warn', auto_reduce_dimensions=False):
 
         self._register_parsers()
-
-        from .unit import build_unit_class
-        self.Unit = build_unit_class(self)
-
-        from .quantity import build_quantity_class
-        self.Quantity = build_quantity_class(self, force_ndarray)
-
-        from .measurement import build_measurement_class
-        self.Measurement = build_measurement_class(self, force_ndarray)
+        self._init_dynamic_classes(force_ndarray)
 
         self._filename = filename
 
@@ -168,6 +162,18 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
 
         self._initialized = False
 
+    def _init_dynamic_classes(self, force_ndarray):
+        """Generate subclasses on the fly and attach them to self
+        """
+        from .unit import build_unit_class
+        self.Unit = build_unit_class(self)
+
+        from .quantity import build_quantity_class
+        self.Quantity = build_quantity_class(self, force_ndarray)
+
+        from .measurement import build_measurement_class
+        self.Measurement = build_measurement_class(self, force_ndarray)
+
     def _after_init(self):
         """This should be called after all __init__
         """
@@ -194,9 +200,15 @@ class BaseRegistry(meta.with_metaclass(_Meta)):
             k, v = part.split('=')
             self._defaults[k.strip()] = v.strip()
 
+    def __deepcopy__(self, memo):
+        new = object.__new__(type(self))
+        new.__dict__ = copy.deepcopy(self.__dict__, memo)
+        force_ndarray = self.Quantity.force_ndarray
+        new._init_dynamic_classes(force_ndarray)
+        return new
+
     def __getattr__(self, item):
-        if item[0] == '_':
-            return super(BaseRegistry, self).__getattribute__(item)
+        getattr_maybe_raise(self, item)
         return self.Unit(item)
 
     def __getitem__(self, item):
@@ -1283,12 +1295,14 @@ class SystemRegistry(BaseRegistry):
         #: Map group name to group.
         #: :type: dict[ str | Group]
         self._groups = {}
-        self.Group = systems.build_group_class(self)
         self._groups['root'] = self.Group('root')
-        self.System = systems.build_system_class(self)
-
         self._default_system = system
 
+    def _init_dynamic_classes(self, force_ndarray):
+        super(SystemRegistry, self)._init_dynamic_classes(force_ndarray)
+        self.Group = systems.build_group_class(self)
+        self.System = systems.build_system_class(self)
+
     def _after_init(self):
         """After init function
 
diff --git a/pint/systems.py b/pint/systems.py
index 47b1ed1..e73428e 100644
--- a/pint/systems.py
+++ b/pint/systems.py
@@ -15,7 +15,7 @@ import re
 
 from .definitions import Definition, UnitDefinition
 from .errors import DefinitionSyntaxError, RedefinitionError
-from .util import to_units_container, SharedRegistryObject, SourceIterator, logger
+from .util import to_units_container, getattr_maybe_raise, SharedRegistryObject, SourceIterator, logger
 from .babel_names import _babel_systems
 from pint.compat import Loc
 
@@ -232,6 +232,7 @@ class _Group(SharedRegistryObject):
         return grp
 
     def __getattr__(self, item):
+        getattr_maybe_raise(self, item)
         return self._REGISTRY
 
 
@@ -297,6 +298,7 @@ class _System(SharedRegistryObject):
         return list(self.members)
 
     def __getattr__(self, item):
+        getattr_maybe_raise(self, item)
         u = getattr(self._REGISTRY, self.name + '_' + item, None)
         if u is not None:
             return u
@@ -440,6 +442,7 @@ class Lister(object):
         return list(self.d.keys())
 
     def __getattr__(self, item):
+        getattr_maybe_raise(self, item)
         return self.d[item]
 
 
diff --git a/pint/unit.py b/pint/unit.py
index adb871c..79f3048 100644
--- a/pint/unit.py
+++ b/pint/unit.py
@@ -64,7 +64,7 @@ class _Unit(PrettyIPython, SharedRegistryObject):
         return ret
 
     def __deepcopy__(self, memo):
-      ret = self.__class__(copy.deepcopy(self._units))
+      ret = self.__class__(copy.deepcopy(self._units, memo))
       ret.__used = self.__used
       return ret
 
diff --git a/pint/util.py b/pint/util.py
index 13fc149..587517a 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -243,6 +243,9 @@ class udict(dict):
     def __missing__(self, key):
         return 0.
 
+    def copy(self):
+        return udict(self)
+
 
 class UnitsContainer(Mapping):
     """The UnitsContainer stores the product of units and their respective
@@ -264,7 +267,7 @@ class UnitsContainer(Mapping):
                 raise TypeError('value must be a number, not {}'.format(type(value)))
             if not isinstance(value, float):
                 d[key] = float(value)
-        self._hash = hash(frozenset(self._d.items()))
+        self._hash = None
 
     def copy(self):
         return self.__copy__()
@@ -275,24 +278,28 @@ class UnitsContainer(Mapping):
         if newval:
             new._d[key] = newval
         else:
-            del new._d[key]
-
+            new._d.pop(key, None)
+        new._hash = None
         return new
 
     def remove(self, keys):
         """ Create a new UnitsContainer purged from given keys.
 
         """
-        d = udict(self._d)
-        return UnitsContainer(((key, d[key]) for key in d if key not in keys))
+        new = self.copy()
+        for k in keys:
+            new._d.pop(k, None)
+        new._hash = None
+        return new
 
     def rename(self, oldkey, newkey):
         """ Create a new UnitsContainer in which an entry has been renamed.
 
         """
-        d = udict(self._d)
-        d[newkey] = d.pop(oldkey)
-        return UnitsContainer(d)
+        new = self.copy()
+        new._d[newkey] = new._d.pop(oldkey)
+        new._hash = None
+        return new
 
     def __iter__(self):
         return iter(self._d)
@@ -307,19 +314,22 @@ class UnitsContainer(Mapping):
         return key in self._d
 
     def __hash__(self):
+        if self._hash is None:
+            self._hash = hash(frozenset(self._d.items()))
         return self._hash
 
+    # Only needed by Python 2.7
     def __getstate__(self):
-        return {'_d': self._d, '_hash': self._hash}
+        return self._d, self._hash
 
     def __setstate__(self, state):
-        self._d = state['_d']
-        self._hash = state['_hash']
+        self._d, self._hash = state
 
     def __eq__(self, other):
         if isinstance(other, UnitsContainer):
-            other = other._d
-        elif isinstance(other, string_types):
+            # Not the same as hash(self); see ParserHelper.__hash__ and __eq__
+            return UnitsContainer.__hash__(self) == UnitsContainer.__hash__(other)
+        if isinstance(other, string_types):
             other = ParserHelper.from_string(other)
             other = other._d
 
@@ -340,20 +350,25 @@ class UnitsContainer(Mapping):
         return format_unit(self, spec, **kwspec)
 
     def __copy__(self):
-        return UnitsContainer(self._d)
+        # Skip expensive health checks performed by __init__
+        out = object.__new__(self.__class__)
+        out._d = self._d.copy()
+        out._hash = self._hash
+        return out
 
     def __mul__(self, other):
-        d = udict(self._d)
         if not isinstance(other, self.__class__):
             err = 'Cannot multiply UnitsContainer by {}'
             raise TypeError(err.format(type(other)))
+
+        new = self.copy()
         for key, value in other.items():
-            d[key] += value
-        keys = [key for key, value in d.items() if value == 0]
-        for key in keys:
-            del d[key]
+            new._d[key] += value
+            if new._d[key] == 0:
+                del new._d[key]
 
-        return UnitsContainer(d)
+        new._hash = None
+        return new
 
     __rmul__ = __mul__
 
@@ -361,26 +376,26 @@ class UnitsContainer(Mapping):
         if not isinstance(other, NUMERIC_TYPES):
             err = 'Cannot power UnitsContainer by {}'
             raise TypeError(err.format(type(other)))
-        d = udict(self._d)
-        for key, value in d.items():
-            d[key] *= other
-        return UnitsContainer(d)
+
+        new = self.copy()
+        for key, value in new._d.items():
+            new._d[key] *= other
+        new._hash = None
+        return new
 
     def __truediv__(self, other):
         if not isinstance(other, self.__class__):
             err = 'Cannot divide UnitsContainer by {}'
             raise TypeError(err.format(type(other)))
 
-        d = udict(self._d)
-
+        new = self.copy()
         for key, value in other.items():
-            d[key] -= value
+            new._d[key] -= value
+            if new._d[key] == 0:
+                del new._d[key]
 
-        keys = [key for key, value in d.items() if value == 0]
-        for key in keys:
-            del d[key]
-
-        return UnitsContainer(d)
+        new._hash = None
+        return new
 
     def __rtruediv__(self, other):
         if not isinstance(other, self.__class__) and other != 1:
@@ -468,7 +483,9 @@ class ParserHelper(UnitsContainer):
                                  for key, value in ret.items()))
 
     def __copy__(self):
-        return ParserHelper(scale=self.scale, **self)
+        new = super(ParserHelper, self).__copy__()
+        new.scale = self.scale
+        return new
 
     def copy(self):
         return self.__copy__()
@@ -477,7 +494,14 @@ class ParserHelper(UnitsContainer):
         if self.scale != 1.0:
             mess = 'Only scale 1.0 ParserHelper instance should be considered hashable'
             raise ValueError(mess)
-        return self._hash
+        return super(ParserHelper, self).__hash__()
+
+    # Only needed by Python 2.7
+    def __getstate__(self):
+        return self._d, self._hash, self.scale
+
+    def __setstate__(self, state):
+        self._d, self._hash, self.scale = state
 
     def __eq__(self, other):
         if isinstance(other, self.__class__):
@@ -696,6 +720,16 @@ def fix_str_conversions(cls):
     return cls
 
 
+def getattr_maybe_raise(self, item):
+    """Helper function to invoke at the beginning of all overridden ``__getattr__``
+    methods. Raise AttributeError if the user tries to ask for a _ or __ attribute.
+    """
+    # Double-underscore attributes are tricky to detect because they are
+    # automatically prefixed with the class name - which may be a subclass of self
+    if item.startswith('_') or item.endswith('__'):
+        raise AttributeError("%r object has no attribute %r" % (self, item))
+
+
 class SourceIterator(object):
     """Iterator to facilitate reading the definition files.
 


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 6e79601..6ca7035 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -679,3 +679,23 @@ class TestIssues(QuantityTestCase):
     def test_issue783(self):
         ureg = UnitRegistry()
         assert not ureg('g') == []
+
+    def test_issue856(self):
+        ph1 = ParserHelper(scale=123)
+        ph2 = copy.deepcopy(ph1)
+        assert ph2.scale == ph1.scale
+
+        ureg1 = UnitRegistry()
+        ureg2 = copy.deepcopy(ureg1)
+        # Very basic functionality test
+        assert ureg2('1 t').to('kg').magnitude == 1000
+
+    def test_issue856b(self):
+        # Test that, after a deepcopy(), the two UnitRegistries are
+        # independent from each other
+        ureg1 = UnitRegistry()
+        ureg2 = copy.deepcopy(ureg1)
+        ureg1.define('test123 = 123 kg')
+        ureg2.define('test123 = 456 kg')
+        assert ureg1('1 test123').to('kg').magnitude == 123
+        assert ureg2('1 test123').to('kg').magnitude == 456