Repository: ahawker/ulid

Issue:
deepcopy doesn't work on ULID object
Running into a cryptic error when trying to deepcopy a ULID object:

```
>>> import ulid
>>> a = ulid.new()
>>> a
<ULID('01EAZF1038723PE2SS9BXRQC80')>
>>> import copy
>>> copy.deepcopy(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 173, in deepcopy
    y = _reconstruct(x, memo, *rv)
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 147, in deepcopy
    y = copier(x, memo)
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 211, in _deepcopy_tuple
    y = [deepcopy(a, memo) for a in x]
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 211, in <listcomp>
    y = [deepcopy(a, memo) for a in x]
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 147, in deepcopy
    y = copier(x, memo)
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
  File "/Users/ashu/.pyenv/versions/3.8.2/lib/python3.8/copy.py", line 162, in deepcopy
    rv = reductor(4)
TypeError: cannot pickle 'memoryview' object
```

Any ideas?

Discussion:

Patch:
diff --git a/ulid/ulid.py b/ulid/ulid.py
index 0a81822..d96adbd 100644
--- a/ulid/ulid.py
+++ b/ulid/ulid.py
@@ -132,6 +132,12 @@ class MemoryView:
     def __str__(self) -> hints.Str:
         return self.str
 
+    def __getstate__(self) -> hints.Str:
+        return self.str
+
+    def __setstate__(self, state: hints.Str) -> None:
+        self.memory = memoryview(base32.decode(state))
+
     @property
     def bytes(self) -> hints.Bytes:
         """


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index c00f3dd..edd1bd6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -40,7 +40,7 @@ def unsupported_type(request):
     return request.param
 
 
-@pytest.fixture('session', params=[bytes, bytearray, memoryview])
+@pytest.fixture(scope='session', params=[bytes, bytearray, memoryview])
 def buffer_type(request):
     """
     Fixture that yields types that support the buffer protocol.
diff --git a/tests/test_bugs.py b/tests/test_bugs.py
index 9cb67f7..a8d7077 100644
--- a/tests/test_bugs.py
+++ b/tests/test_bugs.py
@@ -4,6 +4,8 @@
 
     Tests for validating reported bugs have been fixed.
 """
+import copy
+
 import pytest
 
 from ulid import api
@@ -35,3 +37,13 @@ def test_github_issue_61():
     for s in ('01BX73KC0TNH409RTFD1uXKM00', '01BX73KC0TNH409RTFD1UXKM00'):
         with pytest.raises(ValueError):
             api.from_str(s)
+
+
+def test_github_issue_452():
+    """
+    Assert that :class:`~ulid.ulid.ULID` will not raise during a deepcopy.
+
+    Issue: https://github.com/ahawker/ulid/issues/452
+    """
+    result = copy.deepcopy(api.new())
+    assert result is not None
diff --git a/tests/test_ulid.py b/tests/test_ulid.py
index b487d2e..3c71585 100644
--- a/tests/test_ulid.py
+++ b/tests/test_ulid.py
@@ -4,8 +4,10 @@
 
     Tests for the :mod:`~ulid.ulid` module.
 """
+import copy
 import datetime
 import operator
+import pickle
 import time
 import uuid
 
@@ -259,7 +261,7 @@ def test_memoryview_supports_float(valid_bytes_128):
     assert float(mv) == mv.float
 
 
-def test_memoryview_defines_hash(valid_bytes_128):
+def test_memoryview_supports_hash(valid_bytes_128):
     """
     Assert that the `hash` representation of a :class:`~ulid.ulid.MemoryView` is equal to the
     hash result of the underlying :class:`~memoryview.`
@@ -268,6 +270,49 @@ def test_memoryview_defines_hash(valid_bytes_128):
     assert hash(mv) == hash(mv.memory)
 
 
+def test_memoryview_supports_getstate(valid_bytes_128):
+    """
+    Assert that the `__getstate__` representation of a :class:`~ulid.ulid.MemoryView` is equal to the
+    str result of the underlying :class:`~memoryview.`
+    """
+    mv = ulid.MemoryView(valid_bytes_128)
+    assert mv.__getstate__() == mv.str
+
+
+def test_memoryview_supports_pickle(valid_bytes_128):
+    """
+    Assert that instances of :class:`~ulid.ulid.MemoryView` can be pickled and use the
+    the str result of the underlying :class:`~memoryview.` as the serialized value.
+    """
+    mv = ulid.MemoryView(valid_bytes_128)
+    serialized = pickle.dumps(mv)
+    assert serialized is not None
+    assert isinstance(serialized, bytes)
+    deserialized = pickle.loads(serialized)
+    assert deserialized == mv.str
+
+
+def test_memoryview_supports_copy(valid_bytes_128):
+    """
+    Assert that instances of :class:`~ulid.ulid.MemoryView` can be copied using
+    :func:`~copy.copy`.
+    """
+    mv = ulid.MemoryView(valid_bytes_128)
+    copied = copy.copy(mv)
+    assert copied == mv
+
+
+def test_memoryview_supports_deepcopy(valid_bytes_128):
+    """
+    Assert that instances of :class:`~ulid.ulid.MemoryView` can be copied using
+    :func:`~copy.deepcopy`.
+    """
+    mv = ulid.MemoryView(valid_bytes_128)
+    data = dict(a=dict(b=dict(c=mv)))
+    copied = copy.deepcopy(data)
+    assert copied == data
+
+
 def test_timestamp_coverts_bytes_to_unix_time_seconds():
     """
     Assert that :meth:`~ulid.ulid.Timestamp.timestamp` returns the value in Unix time in seconds from epoch.