Repository: scalableminds/webknossos-libs

Issue:
WKDataset#get_or_add_layer not idempotent
I would expect that I can call the method `WKDataset#get_or_add_layer` with the same arguments as often as I want and it should either always succeed or always fail. That doesn't seem to be the case.

```python
from wkcuber.api.Dataset import WKDataset
from pathlib import Path
import numpy as np

ds = WKDataset.create(Path("test"), scale=(139,139,139))
m = ds.get_or_add_layer("color", "color", np.uint8).get_or_add_mag("1") # works
m = ds.get_or_add_layer("color", "color", np.uint8).get_or_add_mag("1") # fails with error
```

Error message
```
Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/wk/lib/python3.8/site-packages/wkcuber/api/Dataset.py", line 59, in dtype_per_layer_to_dtype_per_channel
    return np.dtype(
TypeError: data type "<class 'numpy.uint8'>" not understood

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/anaconda3/envs/wk/lib/python3.8/site-packages/wkcuber/api/Dataset.py", line 240, in get_or_add_layer
    or dtype_per_layer_to_dtype_per_channel(
  File "/usr/local/anaconda3/envs/wk/lib/python3.8/site-packages/wkcuber/api/Dataset.py", line 65, in dtype_per_layer_to_dtype_per_channel
    raise TypeError(
TypeError: Converting dtype_per_layer to dtype_per_channel failed. Double check if the dtype_per_layer value is correct. data type "<class 'numpy.uint8'>" not understood
```


Discussion:

Resolving patch:
diff --git a/wkcuber/api/Dataset.py b/wkcuber/api/Dataset.py
--- a/wkcuber/api/Dataset.py
+++ b/wkcuber/api/Dataset.py
@@ -52,6 +52,28 @@ def convert_dtypes(
     return "".join(converted_dtype_parts)
 
 
+def normalize_dtype_per_channel(
+    dtype_per_channel: Union[str, np.dtype, type]
+) -> np.dtype:
+    try:
+        return np.dtype(dtype_per_channel)
+    except TypeError as e:
+        raise TypeError(
+            "Cannot add layer. The specified 'dtype_per_channel' must be a valid dtype. "
+            + str(e)
+        )
+
+
+def normalize_dtype_per_layer(
+    dtype_per_layer: Union[str, np.dtype, type]
+) -> Union[str, np.dtype]:
+    try:
+        dtype_per_layer = str(np.dtype(dtype_per_layer))
+    except Exception:
+        pass  # casting to np.dtype fails if the user specifies a special dtype like "uint24"
+    return dtype_per_layer
+
+
 def dtype_per_layer_to_dtype_per_channel(
     dtype_per_layer: Union[str, np.dtype], num_channels: int
 ) -> np.dtype:
@@ -147,8 +169,8 @@ def add_layer(
         self,
         layer_name: str,
         category: str,
-        dtype_per_layer: Union[str, np.dtype] = None,
-        dtype_per_channel: Union[str, np.dtype] = None,
+        dtype_per_layer: Union[str, np.dtype, type] = None,
+        dtype_per_channel: Union[str, np.dtype, type] = None,
         num_channels: int = None,
         **kwargs: Any,
     ) -> LayerT:
@@ -164,21 +186,12 @@ def add_layer(
                 "Cannot add layer. Specifying both 'dtype_per_layer' and 'dtype_per_channel' is not allowed"
             )
         elif dtype_per_channel is not None:
-            try:
-                dtype_per_channel = np.dtype(dtype_per_channel)
-            except TypeError as e:
-                raise TypeError(
-                    "Cannot add layer. The specified 'dtype_per_channel' must be a valid dtype. "
-                    + str(e)
-                )
+            dtype_per_channel = normalize_dtype_per_channel(dtype_per_channel)
             dtype_per_layer = dtype_per_channel_to_dtype_per_layer(
                 dtype_per_channel, num_channels
             )
         elif dtype_per_layer is not None:
-            try:
-                dtype_per_layer = str(np.dtype(dtype_per_layer))
-            except Exception:
-                pass  # casting to np.dtype fails if the user specifies a special dtype like "uint24"
+            dtype_per_layer = normalize_dtype_per_layer(dtype_per_layer)
             dtype_per_channel = dtype_per_layer_to_dtype_per_channel(
                 dtype_per_layer, num_channels
             )
@@ -210,8 +223,8 @@ def get_or_add_layer(
         self,
         layer_name: str,
         category: str,
-        dtype_per_layer: Union[str, np.dtype] = None,
-        dtype_per_channel: Union[str, np.dtype] = None,
+        dtype_per_layer: Union[str, np.dtype, type] = None,
+        dtype_per_channel: Union[str, np.dtype, type] = None,
         num_channels: int = None,
         **kwargs: Any,
     ) -> LayerT:
@@ -234,6 +247,12 @@ def get_or_add_layer(
                 + f"and the passed parameter is '{category}'."
             )
 
+            if dtype_per_channel is not None:
+                dtype_per_channel = normalize_dtype_per_channel(dtype_per_channel)
+
+            if dtype_per_layer is not None:
+                dtype_per_layer = normalize_dtype_per_layer(dtype_per_layer)
+
             if dtype_per_channel is not None or dtype_per_layer is not None:
                 dtype_per_channel = (
                     dtype_per_channel


Test patch:
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -681,6 +681,13 @@ def test_get_or_add_layer() -> None:
         pass
 
 
+def test_get_or_add_layer_idempotence() -> None:
+    delete_dir(TESTOUTPUT_DIR / "wk_dataset")
+    ds = WKDataset.create(TESTOUTPUT_DIR / "wk_dataset", scale=(1, 1, 1))
+    ds.get_or_add_layer("color2", "color", np.uint8).get_or_add_mag("1")
+    ds.get_or_add_layer("color2", "color", np.uint8).get_or_add_mag("1")
+
+
 def test_get_or_add_mag_for_wk() -> None:
     delete_dir(TESTOUTPUT_DIR / "wk_dataset")