Repository: napari/napari

Issue:
Contrast limits ValueError
## 🐛 Bug

When I adjust the contrast limits on an image layer via the sliders, I receive a `ValueError`

```python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/opt/miniconda3/envs/napari-test/lib/python3.8/site-packages/napari/utils/events/event_utils.py in _cb(*value=((0.006301976274698973, 0.1730983406305313),))
     44 
     45     def _cb(*value):
---> 46         setattr(ref(), attr, value)
        global setattr = undefined
        global ref = undefined
        global attr = undefined
        value = ((0.006301976274698973, 0.1730983406305313),)
     47 
     48     emitter.connect(_cb)

~/opt/miniconda3/envs/napari-test/lib/python3.8/site-packages/napari/layers/intensity_mixin.py in contrast_limits(self=, contrast_limits=((0.006301976274698973, 0.1730983406305313),))
     74     @contrast_limits.setter
     75     def contrast_limits(self, contrast_limits):
---> 76         validate_2_tuple(contrast_limits)
        global validate_2_tuple = .func at 0x7fe1a54b58b0>
        contrast_limits = ((0.006301976274698973, 0.1730983406305313),)
     77         self._contrast_limits_msg = (
     78             format_float(contrast_limits[0])

~/opt/miniconda3/envs/napari-test/lib/python3.8/site-packages/napari/utils/validators.py in func(obj=((0.006301976274698973, 0.1730983406305313),))
     61             )
     62         if not len(obj) == n:
---> 63             raise ValueError(
        global ValueError = undefined
        global trans._ = >
        global deferred = undefined
        global number = undefined
        global n = undefined
        global obj_len = undefined
        global len = undefined
        obj = ((0.006301976274698973, 0.1730983406305313),)
     64                 trans._(
     65                     "object must have length {number}, got {obj_len}",

ValueError: object must have length 2, got 1
```

## To Reproduce

1. Open napari (0.4.11.rc3)
2. load grayscale image
3. adjust contrast limits from the GUI

## Expected behavior

The clims should be adjusted

## Environment

napari: 0.4.11rc3
Platform: macOS-10.15.7-x86_64-i386-64bit
System: MacOS 10.15.7
Python: 3.8.11 (default, Aug 6 2021, 08:56:27) [Clang 10.0.0 ]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.2
SciPy: 1.7.1
Dask: 2021.08.1
VisPy: 0.8.1

OpenGL:
- GL version: 2.1 INTEL-14.7.17
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1440x900, scale 2.0

Plugins:
- console: 0.0.4
- scikit-image: 0.4.11rc3
- svg: 0.1.5

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:

https://user-images.githubusercontent.com/1120672/131849800-f7bd8868-9fd4-4cd3-a3e7-b3634bc61e38.mov



Resolving patch:
diff --git a/napari/utils/events/event_utils.py b/napari/utils/events/event_utils.py
--- a/napari/utils/events/event_utils.py
+++ b/napari/utils/events/event_utils.py
@@ -43,7 +43,7 @@ def connect_setattr(emitter: Emitter, obj, attr: str):
     ref = weakref.ref(obj)
 
     def _cb(*value):
-        setattr(ref(), attr, value)
+        setattr(ref(), attr, value[0] if len(value) == 1 else value)
 
     emitter.connect(_cb)
     # There are scenarios where emmiter is deleted before obj.


Test patch:
diff --git a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
--- a/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
+++ b/napari/_qt/layer_controls/_tests/test_qt_image_base_layer_.py
@@ -110,3 +110,11 @@ def test_clim_slider_step_size_and_precision(qtbot, mag):
     # the slider step size should also be inversely proportional to the data
     # range, with 1000 steps across the data range
     assert popup.slider.singleStep() == 10 ** -decimals
+
+
+def test_qt_image_controls_change_contrast(qtbot):
+    layer = Image(np.random.rand(8, 8))
+    qtctrl = QtBaseImageControls(layer)
+    qtbot.addWidget(qtctrl)
+    qtctrl.contrastLimitsSlider.setValue((0.1, 0.8))
+    assert tuple(layer.contrast_limits) == (0.1, 0.8)