Repository: holoviz/holoviews

Issue:
Dimension initializer can't accept a dict because of look-up of preset dimensions
<details>

<summary>
The conda environment's packages are collapsed here.
</summary>

```
# Name                    Version                   Build  Channel
abseil-cpp                20210324.2           hd77b12b_0
argon2-cffi               21.3.0             pyhd3eb1b0_0
argon2-cffi-bindings      21.2.0          py310h2bbff1b_0
arrow-cpp                 7.0.0           py310he67c4b2_4_cpu    conda-forge
asttokens                 2.0.5              pyhd3eb1b0_0
atomicwrites              1.4.0                      py_0
attrs                     21.4.0             pyhd3eb1b0_0
aws-c-cal                 0.5.11               he19cf47_0    conda-forge
aws-c-common              0.6.2                h2bbff1b_0
aws-c-event-stream        0.2.7               h70e1b0c_13    conda-forge
aws-c-io                  0.10.5               h2fe331c_0    conda-forge
aws-checksums             0.1.11               h1e232aa_7    conda-forge
aws-sdk-cpp               1.8.186              hb0612c5_3    conda-forge
backcall                  0.2.0              pyhd3eb1b0_0
beautifulsoup4            4.11.1          py310haa95532_0
blas                      1.0                         mkl
bleach                    4.1.0              pyhd3eb1b0_0
bokeh                     2.4.1           py310haa95532_0
bottleneck                1.3.4           py310h9128911_0
brotli                    1.0.9                ha925a31_2
brotlipy                  0.7.0           py310h2bbff1b_1002
bzip2                     1.0.8                he774522_0
c-ares                    1.18.1               h2bbff1b_0
ca-certificates           2022.4.26            haa95532_0
certifi                   2021.5.30       py310haa95532_0
cffi                      1.15.0          py310h2bbff1b_1
cftime                    1.5.1.1         py310h9128911_0
chardet                   4.0.0           py310haa95532_1003
charset-normalizer        2.0.4              pyhd3eb1b0_0
click                     8.0.4           py310haa95532_0
cloudpickle               2.0.0              pyhd3eb1b0_0
codecov                   2.1.11             pyhd3eb1b0_0
colorama                  0.4.4              pyhd3eb1b0_0
colorcet                  3.0.0                      py_0    pyviz
conda                     4.12.0          py310haa95532_0
conda-build               3.21.8          py310haa95532_2
conda-package-handling    1.8.1           py310h8cc25b3_0
coverage                  6.3.2           py310h2bbff1b_0
cryptography              37.0.1          py310h21b164f_0
curl                      7.82.0               h2bbff1b_0
cycler                    0.11.0             pyhd3eb1b0_0
cytoolz                   0.11.0          py310h2bbff1b_0
dash                      1.19.0             pyhd3eb1b0_0
dash-core-components      1.3.1                      py_0
dash-html-components      1.0.1                      py_0
dash-renderer             1.1.2                      py_0
dash-table                4.4.1              pyhd3eb1b0_0
dask                      2022.2.1           pyhd3eb1b0_0
dask-core                 2022.2.1           pyhd3eb1b0_0
dataclasses               0.8                pyh6d0b6a4_7
datashader                0.14.0                     py_0    pyviz
datashape                 0.5.4           py310haa95532_1
debugpy                   1.5.1           py310hd77b12b_0
decorator                 5.1.1              pyhd3eb1b0_0
defusedxml                0.7.1              pyhd3eb1b0_0
distributed               2022.2.1           pyhd3eb1b0_0
doit                      0.33.1          py310haa95532_0
entrypoints               0.4             py310haa95532_0
executing                 0.8.3              pyhd3eb1b0_0
ffmpeg                    4.2.2                he774522_0
filelock                  3.6.0              pyhd3eb1b0_0
flake8                    4.0.1              pyhd3eb1b0_1
flask                     1.1.2              pyhd3eb1b0_0
flask-compress            1.5.0                      py_0
fonttools                 4.25.0             pyhd3eb1b0_0
freetype                  2.10.4               hd328e21_0
fsspec                    2022.2.0           pyhd3eb1b0_0
future                    0.18.2          py310haa95532_1
geos                      3.8.0                h33f27b4_0
gflags                    2.2.2                ha925a31_0
glob2                     0.7                pyhd3eb1b0_0
glog                      0.6.0                h4797de2_0    conda-forge
grpc-cpp                  1.43.2               he33f7c1_3    conda-forge
hdf4                      4.2.13               h712560f_2
hdf5                      1.10.6               h7ebc959_0
heapdict                  1.0.1              pyhd3eb1b0_0
holoviews                 1.14.4.post130+g221a99471           dev_0    <develop>
icc_rt                    2019.0.0             h0cc432a_1
icu                       58.2                 ha925a31_3
idna                      3.3                pyhd3eb1b0_0
imagecodecs-lite          2019.12.3       py310h2873277_5    conda-forge
imageio                   2.9.0              pyhd3eb1b0_0
importlib-metadata        4.11.3          py310haa95532_0
iniconfig                 1.1.1              pyhd3eb1b0_0
intel-openmp              2021.4.0          haa95532_3556
ipykernel                 6.9.1           py310haa95532_0
ipython                   8.3.0           py310haa95532_0
ipython_genutils          0.2.0              pyhd3eb1b0_1
itsdangerous              2.0.1              pyhd3eb1b0_0
jbig                      2.1               h8d14728_2003    conda-forge
jedi                      0.18.1          py310haa95532_1
jinja2                    2.11.3             pyhd3eb1b0_0
jpeg                      9e                   h2bbff1b_0
jsonschema                4.4.0           py310haa95532_0
jupyter_client            7.2.2           py310haa95532_0
jupyter_core              4.10.0          py310haa95532_0
jupyterlab_pygments       0.1.2                      py_0
kiwisolver                1.3.1           py310hd77b12b_0
lerc                      3.0                  hd77b12b_0
libarchive                3.5.2                hb45042f_1    conda-forge
libbrotlicommon           1.0.9                h8ffe710_7    conda-forge
libbrotlidec              1.0.9                h8ffe710_7    conda-forge
libbrotlienc              1.0.9                h8ffe710_7    conda-forge
libcrc32c                 1.1.2                h0e60522_0    conda-forge
libcurl                   7.82.0               h86230a5_0
libdeflate                1.8                  h2bbff1b_5
libffi                    3.4.2                h604cdb4_1
libgoogle-cloud           1.35.0               h7f3f1e6_2    conda-forge
libiconv                  1.16                 h2bbff1b_2
liblief                   0.11.5               hd77b12b_1
libnetcdf                 4.6.1                hf59b723_4
libpng                    1.6.37               h2a8f88b_0
libprotobuf               3.19.4               h7755175_0    conda-forge
libssh2                   1.10.0               hcd4344a_0
libthrift                 0.16.0               h636ae23_1    conda-forge
libtiff                   4.3.0                hd413186_2    conda-forge
libutf8proc               2.7.0                hcb41399_0    conda-forge
libwebp                   1.2.2                h2bbff1b_0
libxml2                   2.9.12               h0ad7f3c_0
libzlib                   1.2.11            h8ffe710_1014    conda-forge
llvmlite                  0.38.0          py310h23ce68f_0
locket                    0.2.1           py310haa95532_2
lz4-c                     1.9.3                h2bbff1b_1
lzo                       2.10                 he774522_2
markdown                  3.3.4           py310haa95532_0
markupsafe                2.0.1           py310h2bbff1b_0
matplotlib                3.5.1           py310haa95532_1
matplotlib-base           3.5.1           py310hd77b12b_1
matplotlib-inline         0.1.2              pyhd3eb1b0_2
mccabe                    0.7.0              pyhd3eb1b0_0
menuinst                  1.4.17          py310h59b6b97_0
mistune                   0.8.4           py310h2bbff1b_1000
mkl                       2021.4.0           haa95532_640
mkl-service               2.4.0           py310h2bbff1b_0
mkl_fft                   1.3.1           py310ha0764ea_0
mkl_random                1.2.2           py310h4ed8f06_0
mock                      4.0.3              pyhd3eb1b0_0
msgpack-python            1.0.3           py310h59b6b97_0
multipledispatch          0.6.0           py310haa95532_0
munkres                   1.1.4                      py_0
nbclient                  0.5.13          py310haa95532_0
nbconvert                 6.4.4           py310haa95532_0
nbformat                  5.3.0           py310haa95532_0
nbsmoke                   0.6.0                      py_0    pyviz
nest-asyncio              1.5.5           py310haa95532_0
netcdf4                   1.5.7           py310h2805cc6_0
networkx                  2.7.1              pyhd3eb1b0_0
notebook                  6.4.11          py310haa95532_0
numba                     0.55.1          py310h4ed8f06_0
numexpr                   2.8.1           py310hb57aa6b_1
numpy                     1.21.5          py310h6d2d95c_2
numpy-base                1.21.5          py310h206c741_2
openssl                   1.1.1o               h2bbff1b_0
packaging                 21.3               pyhd3eb1b0_0
pandas                    1.4.2           py310hd77b12b_0
pandocfilters             1.5.0              pyhd3eb1b0_0
panel                     0.13.0                     py_0    pyviz
param                     1.12.1                     py_0    pyviz
parquet-cpp               1.5.1                h34088ae_4
parso                     0.8.3              pyhd3eb1b0_0
partd                     1.2.0              pyhd3eb1b0_1
path                      16.2.0             pyhd3eb1b0_0
path.py                   12.5.0               hd3eb1b0_0
pickleshare               0.7.5           pyhd3eb1b0_1003
pillow                    9.0.1           py310hdc2b20a_0
pip                       21.2.4          py310haa95532_0
pkginfo                   1.8.2              pyhd3eb1b0_0
plotly                    5.6.0              pyhd3eb1b0_0
pluggy                    1.0.0           py310haa95532_1
prometheus_client         0.13.1             pyhd3eb1b0_0
prompt-toolkit            3.0.20             pyhd3eb1b0_0
psutil                    5.8.0           py310h2bbff1b_1
pure_eval                 0.2.2              pyhd3eb1b0_0
py                        1.11.0             pyhd3eb1b0_0
py-lief                   0.11.5          py310hd77b12b_1
pyarrow                   7.0.0           py310h4115056_4_cpu    conda-forge
pycodestyle               2.8.0              pyhd3eb1b0_0
pycosat                   0.6.3           py310h2bbff1b_0
pycparser                 2.21               pyhd3eb1b0_0
pyct                      0.4.8                      py_0    pyviz
pyct-core                 0.4.8                      py_0    pyviz
pyctdev                   0.6.6                      py_0    pyviz
pyflakes                  2.4.0              pyhd3eb1b0_0
pygments                  2.11.2             pyhd3eb1b0_0
pyopenssl                 22.0.0             pyhd3eb1b0_0
pyparsing                 3.0.4              pyhd3eb1b0_0
pyqt                      5.9.2           py310hd77b12b_6
pyrsistent                0.18.0          py310h2bbff1b_0
pysocks                   1.7.1           py310haa95532_0
pytest                    7.1.1           py310haa95532_0
pytest-cov                3.0.0              pyhd3eb1b0_0
python                    3.10.4               hbb2ffb3_0
python-dateutil           2.8.2              pyhd3eb1b0_0
python-fastjsonschema     2.15.1             pyhd3eb1b0_0
python-libarchive-c       2.9                pyhd3eb1b0_1
python_abi                3.10                    2_cp310    conda-forge
pytz                      2021.3             pyhd3eb1b0_0
pyviz_comms               2.2.0                      py_0    pyviz
pywavelets                1.3.0           py310h2bbff1b_0
pywin32                   302             py310h2bbff1b_2
pywinpty                  2.0.2           py310h5da7b33_0
pyyaml                    6.0             py310h2bbff1b_0
pyzmq                     22.3.0          py310hd77b12b_2
qt                        5.9.7            vc14h73c81de_0
re2                       2022.02.01           h0e60522_0    conda-forge
requests                  2.27.1             pyhd3eb1b0_0
ruamel_yaml               0.15.100        py310h2bbff1b_0
scikit-image              0.19.2          py310h4ed8f06_0
scipy                     1.7.3           py310h6d2d95c_0
send2trash                1.8.0              pyhd3eb1b0_1
setuptools                61.2.0          py310haa95532_0
shapely                   1.7.0           py310h723df8e_0
sip                       4.19.13         py310hd77b12b_0
six                       1.16.0             pyhd3eb1b0_1
snappy                    1.1.9                h6c2663c_0
sortedcontainers          2.4.0              pyhd3eb1b0_0
soupsieve                 2.3.1              pyhd3eb1b0_0
sqlite                    3.38.3               h2bbff1b_0
stack_data                0.2.0              pyhd3eb1b0_0
streamz                   0.6.3              pyhd3eb1b0_0
tbb                       2021.5.0             h59b6b97_0
tblib                     1.7.0              pyhd3eb1b0_0
tenacity                  8.0.1           py310haa95532_0
terminado                 0.13.1          py310haa95532_0
testpath                  0.5.0              pyhd3eb1b0_0
tifffile                  2020.6.3                   py_0    conda-forge
tk                        8.6.11               h2bbff1b_1
toml                      0.10.2             pyhd3eb1b0_0
tomli                     1.2.2              pyhd3eb1b0_0
toolz                     0.11.2             pyhd3eb1b0_0
tornado                   6.1             py310h2bbff1b_0
tqdm                      4.64.0          py310haa95532_0
traitlets                 5.1.1              pyhd3eb1b0_0
typing-extensions         4.1.1                hd3eb1b0_0
typing_extensions         4.1.1              pyh06a4308_0
tzdata                    2022a                hda174b7_0
urllib3                   1.26.9          py310haa95532_0
vc                        14.2                 h21ff451_1
vs2015_runtime            14.27.29016          h5e58377_2
wcwidth                   0.2.5              pyhd3eb1b0_0
webencodings              0.5.1           py310haa95532_1
werkzeug                  2.0.3              pyhd3eb1b0_0
wheel                     0.37.1             pyhd3eb1b0_0
win_inet_pton             1.1.0           py310haa95532_0
wincertstore              0.2             py310haa95532_2
winpty                    0.4.3                         4
xarray                    0.20.1             pyhd3eb1b0_1
xz                        5.2.5                h8cc25b3_1
yaml                      0.2.5                he774522_0
zict                      2.0.0              pyhd3eb1b0_0
zipp                      3.8.0           py310haa95532_0
zlib                      1.2.11            h8ffe710_1014    conda-forge
zstd                      1.5.2                h6255e5f_0    conda-forge
```

</details>

I expected the `Dimension` class and the `process_dimensions` function to accept a `dict` to specify the dimension.  For example, one encounters an exception message to that effect when initializing a `MultiDimensionalMapping` (such as its subclasses `NdLayout` and `NdOverlay`) with an incorrect type in the `kdims` argument, as in the following example with an intentionally wrong type to produce the message:

``` python
In [1]: import holoviews

In [2]: holoviews.core.ndmapping.MultiDimensionalMapping([], kdims=[b"bad type"])
Traceback (most recent call last):
  Input In [2] in <cell line: 1>
    holoviews.core.ndmapping.MultiDimensionalMapping([], kdims=[b"bad type"])
  File ~\Documents\Repositories\holoviews\holoviews\core\ndmapping.py:104 in __init__
    super().__init__(OrderedDict(), **dict(params))
  File ~\Documents\Repositories\holoviews\holoviews\core\dimension.py:840 in __init__
    params.update(process_dimensions(kdims, vdims))
  File ~\Documents\Repositories\holoviews\holoviews\core\dimension.py:123 in process_dimensions
    raise ValueError('Dimensions must be defined as a tuple, '
ValueError: Dimensions must be defined as a tuple, string, dictionary or Dimension instance, found a bytes type.
```

Also, *Holoviews* development includes the closed issue #2315 and PR #2332, which intended to implement specifying a `Dimension` with a `dict`.

However, trying to create a `Dimension` from a `dict` causes a `TypeError`, although it occurs not because the code checks the type directly but rather because it attempts a membership test with a mutable object:

``` python
In [3]: holoviews.Dimension(dict(name="thing"))
Traceback (most recent call last):
  Input In [3] in <cell line: 1>
    holoviews.Dimension(dict(name="thing"))
  File ~\Documents\Repositories\holoviews\holoviews\core\dimension.py:246 in __init__
    elif (spec, params.get('unit', None)) in self.presets.keys():
TypeError: unhashable type: 'dict'
```

That membership test to look up the spec in the preset dimensions prevents a `dict` spec from making it to the clause shown below that PR #2332 introduced to check whether `spec` is a `dict`.

``` diff
         elif (spec, params.get('unit', None)) in self.presets.keys():
             preset = self.presets[(str(spec), str(params['unit']))]
             existing_params = dict(preset.get_param_values())
+        elif isinstance(spec, dict):
+            existing_params = spec
         elif spec in self.presets:
             existing_params = dict(self.presets[spec].get_param_values())
```


Discussion:

Resolving patch:
diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py
--- a/holoviews/core/dimension.py
+++ b/holoviews/core/dimension.py
@@ -56,14 +56,8 @@ def asdim(dimension):
         A Dimension object constructed from the dimension spec. No
         copy is performed if the input is already a Dimension.
     """
-    if isinstance(dimension, Dimension):
-        return dimension
-    elif isinstance(dimension, (tuple, dict, str)):
-        return Dimension(dimension)
-    else:
-        raise ValueError('%s type could not be interpreted as Dimension. '
-                         'Dimensions must be declared as a string, tuple, '
-                         'dictionary or Dimension type.')
+    return dimension if isinstance(dimension, Dimension) else Dimension(dimension)
+
 
 def dimension_name(dimension):
     """Return the Dimension.name for a dimension-like object.
@@ -118,11 +112,6 @@ def process_dimensions(kdims, vdims):
                              "specified as tuples, strings, dictionaries or Dimension "
                              "instances, not a %s type. Ensure you passed the data as the "
                              "first argument." % (group, type(dims).__name__))
-        for dim in dims:
-            if not isinstance(dim, (tuple, str, Dimension, dict)):
-                raise ValueError('Dimensions must be defined as a tuple, '
-                                 'string, dictionary or Dimension instance, '
-                                 'found a %s type.' % type(dim).__name__)
         dimensions[group] = [asdim(d) for d in dims]
     return dimensions
 
@@ -241,42 +230,52 @@ def __init__(self, spec, **params):
         if 'name' in params:
             raise KeyError('Dimension name must only be passed as the positional argument')
 
+        all_params = {}
         if isinstance(spec, Dimension):
-            existing_params = dict(spec.param.get_param_values())
-        elif (spec, params.get('unit', None)) in self.presets.keys():
-            preset = self.presets[(str(spec), str(params['unit']))]
-            existing_params = dict(preset.param.get_param_values())
-        elif isinstance(spec, dict):
-            existing_params = spec
-        elif spec in self.presets:
-            existing_params = dict(self.presets[spec].param.get_param_values())
-        elif (spec,) in self.presets:
-            existing_params = dict(self.presets[(spec,)].param.get_param_values())
-        else:
-            existing_params = {}
-
-        all_params = dict(existing_params, **params)
-        if isinstance(spec, tuple):
-            if not all(isinstance(s, str) for s in spec) or len(spec) != 2:
-                raise ValueError("Dimensions specified as a tuple must be a tuple "
-                                 "consisting of the name and label not: %s" % str(spec))
-            name, label = spec
-            all_params['name'] = name
-            all_params['label'] = label
-            if 'label' in params and (label != params['label']):
-                if params['label'] != label:
-                    self.param.warning(
-                        'Using label as supplied by keyword ({!r}), ignoring '
-                        'tuple value {!r}'.format(params['label'], label))
-                all_params['label'] = params['label']
+            all_params.update(spec.param.get_param_values())
         elif isinstance(spec, str):
+            if (spec, params.get('unit', None)) in self.presets.keys():
+                preset = self.presets[(str(spec), str(params['unit']))]
+                all_params.update(preset.param.get_param_values())
+            elif spec in self.presets:
+                all_params.update(self.presets[spec].param.get_param_values())
+            elif (spec,) in self.presets:
+                all_params.update(self.presets[(spec,)].param.get_param_values())
             all_params['name'] = spec
-            all_params['label'] = params.get('label', spec)
-
-        if all_params['name'] == '':
-            raise ValueError('Dimension name cannot be the empty string')
-        if all_params['label'] in ['', None]:
-            raise ValueError('Dimension label cannot be None or the empty string')
+            all_params['label'] = spec
+        elif isinstance(spec, tuple):
+            try:
+                all_params['name'], all_params['label'] = spec
+            except ValueError as exc:
+                raise ValueError(
+                    "Dimensions specified as a tuple must be a tuple "
+                    "consisting of the name and label not: %s" % str(spec)
+                ) from exc
+            if 'label' in params and params['label'] != all_params['label']:
+                self.param.warning(
+                    'Using label as supplied by keyword ({!r}), ignoring '
+                    'tuple value {!r}'.format(params['label'], all_params['label'])
+                )
+        elif isinstance(spec, dict):
+            all_params.update(spec)
+            try:
+                all_params.setdefault('label', spec['name'])
+            except KeyError as exc:
+                raise ValueError(
+                    'Dimension specified as a dict must contain a "name" key'
+                ) from exc
+        else:
+            raise ValueError(
+                '%s type could not be interpreted as Dimension.  Dimensions must be '
+                'declared as a string, tuple, dictionary or Dimension type.'
+                % type(spec).__name__
+            )
+        all_params.update(params)
+
+        if not all_params['name']:
+            raise ValueError('Dimension name cannot be empty')
+        if not all_params['label']:
+            raise ValueError('Dimension label cannot be empty')
 
         values = params.get('values', [])
         if isinstance(values, str) and values == 'initial':


Test patch:
diff --git a/holoviews/tests/core/test_dimensions.py b/holoviews/tests/core/test_dimensions.py
--- a/holoviews/tests/core/test_dimensions.py
+++ b/holoviews/tests/core/test_dimensions.py
@@ -34,6 +34,24 @@ def test_dimension_label_kwarg(self):
         dim = Dimension('test', label='A test')
         self.assertEqual(dim.label, 'A test')
 
+    def test_dimension_dict_empty(self):
+        with self.assertRaisesRegex(ValueError, 'must contain a "name" key'):
+            Dimension(dict())
+
+    def test_dimension_dict_label(self):
+        with self.assertRaisesRegex(ValueError, 'must contain a "name" key'):
+            Dimension(dict(label='A test'))
+
+    def test_dimension_dict_name(self):
+        dim = Dimension(dict(name='test'))
+        self.assertEqual(dim.name, 'test')
+        self.assertEqual(dim.label, 'test')
+
+    def test_dimension_dict_name_and_label(self):
+        dim = Dimension(dict(name='test', label='A test'))
+        self.assertEqual(dim.name, 'test')
+        self.assertEqual(dim.label, 'A test')
+
     def test_dimension_label_kwarg_and_tuple(self):
         dim = Dimension(('test', 'A test'), label='Another test')
         substr = "Using label as supplied by keyword ('Another test'), ignoring tuple value 'A test'"
diff --git a/holoviews/tests/core/test_ndmapping.py b/holoviews/tests/core/test_ndmapping.py
--- a/holoviews/tests/core/test_ndmapping.py
+++ b/holoviews/tests/core/test_ndmapping.py
@@ -56,6 +56,12 @@ def test_idxmapping_init_item_list(self):
     def test_idxmapping_init_dimstr(self):
         MultiDimensionalMapping(self.init_item_odict, kdims=self.dimension_labels)
 
+    def test_idxmapping_init_dimdict(self):
+        MultiDimensionalMapping(
+            self.init_item_odict,
+            kdims=[dict(name=label) for label in self.dimension_labels],
+        )
+
     def test_idxmapping_init_dimensions(self):
         MultiDimensionalMapping(self.init_item_odict, kdims=[self.dim1, self.dim2])