Repository: h5io/h5io

Issue:
Should it be possible to store a class?
I'm starting to feel like I might have misunderstood and be reaching a bit here; is it reasonable to use `h5io` for storing a class? E.g.

```python
class MyClass:
    @classmethod
    def foo(cls):
        return 42

import h5io

h5io.write_hdf5(
    fname="stuff.h5",
    data=MyClass,
    overwrite=True
)
loaded = h5io.read_hdf5(fname="stuff.h5")

assert loaded.foo() == MyClass.foo()
```

My underlying motivation the last couple days has been to be able to `h5io.write/read_hdf5` to all the objects I could otherwise `pickle.dumps/loads` -- is this even a reasonable direction to be heading?

I'll open a PR, but where my other PRs feel like completing existing functionality, I'm open to the possibility that what I'm doing here is just not in the spirit of things.

Discussion:
larsoner: My initial reaction was to expect this to be complicated. But it looks like it's not too bad.

Would it be worth adding a few other classes, like maybe a couple builtins like `datetime.datetime` or similar? It would show us that it really should generalize to other stuff which is nice.

Patch:
diff --git a/h5io/_h5io.py b/h5io/_h5io.py
index b389a5c..3baed51 100644
--- a/h5io/_h5io.py
+++ b/h5io/_h5io.py
@@ -8,6 +8,7 @@ import importlib
 import json
 import sys
 import tempfile
+from inspect import isclass
 from io import UnsupportedOperation
 from os import path as op
 from pathlib import PurePath
@@ -230,6 +231,11 @@ def _triage_write(
             )
     elif isinstance(value, type(None)):
         _create_titled_dataset(root, key, "None", [False])
+    elif isclass(value):
+        class_str = value.__module__ + "." + value.__name__
+        _create_titled_dataset(
+            root, key, "class", np.frombuffer(class_str.encode("utf-8"), np.uint8)
+        )
     elif isinstance(value, (int, float)):
         if isinstance(value, int):
             title = "int"
@@ -584,6 +590,9 @@ def _triage_read(node, slash="ignore"):
         data = json.loads(node_unicode)
     elif type_str == "None":
         data = None
+    elif type_str == "class":
+        class_str = str(np.array(node).tobytes().decode("utf-8"))
+        data = _import_class(class_str)
     else:
         raise TypeError("Unknown node type: {0}".format(type_str))
     return data


Test patch:
diff --git a/h5io/tests/test_io.py b/h5io/tests/test_io.py
index fe218f6..29f8485 100644
--- a/h5io/tests/test_io.py
+++ b/h5io/tests/test_io.py
@@ -62,7 +62,6 @@ def test_hdf5(tmp_path):
     pytest.raises(ValueError, read_hdf5, test_file, title=1)
     # unsupported objects
     pytest.raises(TypeError, write_hdf5, test_file, {1: "foo"}, overwrite=True)
-    pytest.raises(TypeError, write_hdf5, test_file, object, overwrite=True)
     # special_chars
     spec_dict = {"first/second": "third"}
     pytest.raises(ValueError, write_hdf5, test_file, spec_dict, overwrite=True)
@@ -296,6 +295,58 @@ def test_timezone(name, tmp_path):
         assert y.tzname(None) == name
 
 
+class SaveableClass:
+    """A toy class to test saving and loading classes."""
+
+    @classmethod
+    def foo(cls):
+        """Just a thing on our class to easily see we're getting what we ask for."""
+        return 42
+
+
+def test_class_storage(tmp_path):
+    """Test storing a class."""
+    test_file = tmp_path / "test.hdf5"
+
+    # Built-in
+    write_hdf5(
+        fname=test_file,
+        data=int,
+        title="myclass",
+        overwrite="update",
+        use_json=False,
+        use_state=False,
+    )
+    loaded_class = read_hdf5(fname=test_file, title="myclass")
+    assert loaded_class is int
+
+    # Std. Lib / importable class
+    write_hdf5(
+        fname=test_file,
+        data=datetime.datetime,
+        title="myclass",
+        overwrite="update",
+        use_json=False,
+        use_state=False,
+    )
+    loaded_class = read_hdf5(fname=test_file, title="myclass")
+    assert loaded_class is datetime.datetime
+
+    # Local custom class
+    write_hdf5(
+        fname=test_file,
+        data=SaveableClass,
+        title="myclass",
+        overwrite="update",
+        use_json=False,
+        use_state=False,
+    )
+    loaded_class = read_hdf5(fname=test_file, title="myclass")
+
+    assert loaded_class is SaveableClass
+    assert loaded_class.foo() == SaveableClass.foo()
+
+
 @pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher")
 def test_state_with_numpy_poly1d(tmp_path):
     """The np.poly1d() object has an instance __dict__."""