Repository: napari/napari

Issue:
`TranslationString` somehow preventing layer deepcopy
## 🐛 Bug

It used to be possible to `deepcopy` a Layer object:

```python
from napari.layers import Points
from copy import deepcopy

deepcopy(Points())
```

but if you run that now, you get a super long traceback ending with
```python
~/Dropbox (HMS)/Python/forks/napari/napari/utils/translations.py in __new__(cls, domain, msgctxt, msgid, msgid_plural, n, deferred, **kwargs)
    178     ):
    179         if msgid is None:
--> 180             raise ValueError(
    181                 trans._("Must provide at least a `msgid` parameter!")
    182             )

ValueError: Must provide at least a `msgid` parameter!
```

<details>

```pytb
In [1]: %run t.py
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Dropbox (HMS)/Python/forks/napari/t.py in <module>
      2 from copy import deepcopy
      3
----> 4 deepcopy(Points())

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    170                     y = x
    171                 else:
--> 172                     y = _reconstruct(x, memo, *rv)
    173
    174     # If is its own copy, don't memoize.

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    268     if state is not None:
    269         if deep:
--> 270             state = deepcopy(state, memo)
    271         if hasattr(y, '__setstate__'):
    272             y.__setstate__(state)

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    144     copier = _deepcopy_dispatch.get(cls)
    145     if copier is not None:
--> 146         y = copier(x, memo)
    147     else:
    148         if issubclass(cls, type):

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _deepcopy_dict(x, memo, deepcopy)
    228     memo[id(x)] = y
    229     for key, value in x.items():
--> 230         y[deepcopy(key, memo)] = deepcopy(value, memo)
    231     return y
    232 d[dict] = _deepcopy_dict

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    170                     y = x
    171                 else:
--> 172                     y = _reconstruct(x, memo, *rv)
    173
    174     # If is its own copy, don't memoize.

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    268     if state is not None:
    269         if deep:
--> 270             state = deepcopy(state, memo)
    271         if hasattr(y, '__setstate__'):
    272             y.__setstate__(state)

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    144     copier = _deepcopy_dispatch.get(cls)
    145     if copier is not None:
--> 146         y = copier(x, memo)
    147     else:
    148         if issubclass(cls, type):

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _deepcopy_dict(x, memo, deepcopy)
    228     memo[id(x)] = y
    229     for key, value in x.items():
--> 230         y[deepcopy(key, memo)] = deepcopy(value, memo)
    231     return y
    232 d[dict] = _deepcopy_dict

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    144     copier = _deepcopy_dispatch.get(cls)
    145     if copier is not None:
--> 146         y = copier(x, memo)
    147     else:
    148         if issubclass(cls, type):

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _deepcopy_dict(x, memo, deepcopy)
    228     memo[id(x)] = y
    229     for key, value in x.items():
--> 230         y[deepcopy(key, memo)] = deepcopy(value, memo)
    231     return y
    232 d[dict] = _deepcopy_dict

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    170                     y = x
    171                 else:
--> 172                     y = _reconstruct(x, memo, *rv)
    173
    174     # If is its own copy, don't memoize.

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    268     if state is not None:
    269         if deep:
--> 270             state = deepcopy(state, memo)
    271         if hasattr(y, '__setstate__'):
    272             y.__setstate__(state)

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    144     copier = _deepcopy_dispatch.get(cls)
    145     if copier is not None:
--> 146         y = copier(x, memo)
    147     else:
    148         if issubclass(cls, type):

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _deepcopy_dict(x, memo, deepcopy)
    228     memo[id(x)] = y
    229     for key, value in x.items():
--> 230         y[deepcopy(key, memo)] = deepcopy(value, memo)
    231     return y
    232 d[dict] = _deepcopy_dict

~/miniconda3/envs/napdev/lib/python3.9/copy.py in deepcopy(x, memo, _nil)
    170                     y = x
    171                 else:
--> 172                     y = _reconstruct(x, memo, *rv)
    173
    174     # If is its own copy, don't memoize.

~/miniconda3/envs/napdev/lib/python3.9/copy.py in _reconstruct(x, memo, func, args, state, listiter, dictiter, deepcopy)
    262     if deep and args:
    263         args = (deepcopy(arg, memo) for arg in args)
--> 264     y = func(*args)
    265     if deep:
    266         memo[id(x)] = y

~/miniconda3/envs/napdev/lib/python3.9/copyreg.py in __newobj__(cls, *args)
     93
     94 def __newobj__(cls, *args):
---> 95     return cls.__new__(cls, *args)
     96
     97 def __newobj_ex__(cls, args, kwargs):

~/Dropbox (HMS)/Python/forks/napari/napari/utils/translations.py in __new__(cls, domain, msgctxt, msgid, msgid_plural, n, deferred, **kwargs)
    178     ):
    179         if msgid is None:
--> 180             raise ValueError(
    181                 trans._("Must provide at least a `msgid` parameter!")
    182             )

ValueError: Must provide at least a `msgid` parameter!
```

</details>

git bisect puts it at #2521

CC @goanpeca 

## Expected behavior

i _think_ we should still be able to deepcopy a layer?  (honestly... not entirely sure)



Discussion:
We should indeed

Will take a look :)

Resolving patch:
diff --git a/napari/utils/translations.py b/napari/utils/translations.py
--- a/napari/utils/translations.py
+++ b/napari/utils/translations.py
@@ -166,6 +166,19 @@ class TranslationString(str):
     A class that allows to create a deferred translations.
     """
 
+    def __deepcopy__(self, memo):
+        from copy import deepcopy
+
+        return TranslationString(
+            domain=self._domain,
+            msgctxt=self._msgctxt,
+            msgid=self._msgid,
+            msgid_plural=self._msgid_plural,
+            n=self._n,
+            deferred=self._deferred,
+            kwargs=deepcopy(self._kwargs),
+        )
+
     def __new__(
         cls,
         domain: Optional[str] = None,


Test patch:
diff --git a/napari/utils/_tests/test_translations.py b/napari/utils/_tests/test_translations.py
--- a/napari/utils/_tests/test_translations.py
+++ b/napari/utils/_tests/test_translations.py
@@ -1,6 +1,7 @@
 """Test the translations API."""
 
 import sys
+from copy import deepcopy
 from pathlib import Path
 
 import pytest
@@ -414,3 +415,15 @@ def test_translation_string_exceptions():
 def test_bundle_exceptions(trans):
     with pytest.raises(ValueError):
         trans._dnpgettext()
+
+
+def test_deepcopy():
+    """see https://github.com/napari/napari/issues/2911
+
+    Object containing translation strings can't bee deep-copied.
+    """
+
+    t = TranslationString(msgid='huhu')
+    u = deepcopy(t)
+    assert t is not u
+    assert t == u