Repository: OpenFreeEnergy/gufe (language: python)

Issue:
UUID JSON codec?
We often find ourselves trying to attach UUIDs to GUFE objects. These can't be serialised as a class but can easily be represented as a string. Would you be open to adding a UUID codec? It would looks something like: 

```python
PATH_CODEC = JSONCodec(
    cls=uuid.UUID,
    to_dict=lambda u: {"uuid": str(u)},
    from_dict=lambda dct: UUID(dct["uuid"]),
)
```

See https://github.com/choderalab/asapdiscovery/issues/886 for occurrence in the wild.  Happy to contribute the change myself if good to go ahead. Tagging @jthorton here also. 


Discussion / hints:
Yeah adding a codec is a good idea if you can?

On Tue, 27 Feb 2024 at 23:36, Hugo MacDermott-Opeskin <
***@***.***> wrote:

> We often find ourselves trying to attach UUIDs to GUFE objects. These
> can't be serialised as a class but can easily be represented as a string.
> Would you be open to adding a UUID codec? It would looks something like:
>
> PATH_CODEC = JSONCodec(
>     cls=uuid.UUID,
>     to_dict=lambda u: {"uuid": str(u)},
>     from_dict=lambda dct: pathlib.PosixPath(dct["uuid"]),
> )
>
> See choderalab/asapdiscovery#886
> <https://github.com/choderalab/asapdiscovery/issues/886> for occurrence
> in the wild. Happy to contribute the change myself if good to go ahead.
> Tagging @jthorton <https://github.com/jthorton> here also.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/OpenFreeEnergy/gufe/issues/293>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACGSGB6XP4XLVOCHMJYYAE3YVZUW7AVCNFSM6AAAAABD5ALBWWVHI2DSMVQWIX3LMV43ASLTON2WKOZSGE2TONZZHA4TENA>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>


Fix patch:
diff --git a/gufe/custom_codecs.py b/gufe/custom_codecs.py
index d28e1882..a2ed55ea 100644
--- a/gufe/custom_codecs.py
+++ b/gufe/custom_codecs.py
@@ -8,6 +8,7 @@
 
 import numpy as np
 from openff.units import DEFAULT_UNIT_REGISTRY
+from uuid import UUID
 
 import gufe
 from gufe.custom_json import JSONCodec
@@ -72,7 +73,6 @@ def is_openff_quantity_dict(dct):
     from_dict=lambda dct: datetime.datetime.fromisoformat(dct['isotime']),
 )
 
-
 # Note that this has inconsistent behaviour for some generic types
 # which end up being handled by the default JSON encoder/decoder.
 # The main example of this is np.float64 which will be turned into
@@ -137,3 +137,10 @@ def is_openff_quantity_dict(dct):
     is_my_obj=lambda obj: isinstance(obj, DEFAULT_UNIT_REGISTRY.Unit),
     is_my_dict=is_openff_unit_dict,
 )
+
+
+UUID_CODEC = JSONCodec(
+    cls=UUID,
+    to_dict=lambda p: {"uuid": str(p)},
+    from_dict=lambda dct: UUID(dct["uuid"]),
+)
diff --git a/gufe/tokenization.py b/gufe/tokenization.py
index 1214a819..a4118ac3 100644
--- a/gufe/tokenization.py
+++ b/gufe/tokenization.py
@@ -23,6 +23,7 @@
     OPENFF_UNIT_CODEC,
     PATH_CODEC,
     SETTINGS_CODEC,
+    UUID_CODEC,
 )
 from gufe.custom_json import JSONSerializerDeserializer
 
@@ -35,6 +36,7 @@
     SETTINGS_CODEC,
     OPENFF_UNIT_CODEC,
     OPENFF_QUANTITY_CODEC,
+    UUID_CODEC,
 ]
 JSON_HANDLER = JSONSerializerDeserializer(_default_json_codecs)
 


Test patch:
diff --git a/gufe/tests/test_custom_json.py b/gufe/tests/test_custom_json.py
index 2be5e438..093f2ed4 100644
--- a/gufe/tests/test_custom_json.py
+++ b/gufe/tests/test_custom_json.py
@@ -11,7 +11,7 @@
 from openff.units import unit
 import pytest
 from numpy import testing as npt
-
+from uuid import uuid4
 from gufe.custom_codecs import (
     BYTES_CODEC,
     NUMPY_CODEC,
@@ -20,6 +20,7 @@
     OPENFF_UNIT_CODEC,
     PATH_CODEC,
     SETTINGS_CODEC,
+    UUID_CODEC,
 )
 from gufe.custom_json import JSONSerializerDeserializer, custom_json_factory
 from gufe import tokenization
@@ -299,3 +300,19 @@ def setup_method(self):
                 "unit_name": "unified_atomic_mass_unit",
             }
         ]
+
+
+class TestUUIDCodec(CustomJSONCodingTest):
+    def setup_method(self):
+        self.codec = UUID_CODEC
+        self.objs = [
+            uuid4()
+        ]
+        self.dcts = [
+            {
+                ":is_custom:": True,
+                "__class__": "UUID",
+                "__module__": "uuid",
+                "uuid": f"{str(self.objs[0])}",
+            }
+        ]