Repository: napari/napari

Issue:
Unexpectedly delayed error when viewing points layer with invalid edge_width
## 🐛 Bug

You can add coordinates to the Points layer with negative `edge_width` values. An error is raised if you try to view it, rather than when the property is set.

*This bug was found using [Hypothesis](https://hypothesis.readthedocs.io/).

## To Reproduce

This will create an empty Points layer with a default `edge_width` of `-1`. 

```Python
data = np.array([])
edge_width = -1

viewer = napari.view_points(data, edge_width=edge_width) # Should error here
viewer.run() # Errors here
```

The following error is produced

```Python
Traceback (most recent call last):
  File "test.py", line 20, in <module>
    viewer = napari.view_points(data, edge_width=edge_width)
  File "venv/lib/python3.8/site-packages/napari/view_layers.py", line 142, in view_points
    return _make_viewer_then('add_points', args, kwargs)
  File "venv/lib/python3.8/site-packages/napari/view_layers.py", line 117, in _make_viewer_then
    method(*args, **kwargs)
  File "<string>", line 5, in add_points
  File "/usr/lib/python3.8/_collections_abc.py", line 962, in append
    self.insert(len(self), value)
  File "venv/lib/python3.8/site-packages/napari/components/layerlist.py", line 82, in insert
    super().insert(index, new_layer)
  File "venv/lib/python3.8/site-packages/napari/utils/events/containers/_selectable_list.py", line 63, in insert
    super().insert(index, value)
  File "venv/lib/python3.8/site-packages/napari/utils/events/containers/_evented_list.py", line 179, in insert
    self.events.inserted(index=index, value=value)
  File "venv/lib/python3.8/site-packages/napari/utils/events/event.py", line 599, in __call__
    self._invoke_callback(cb, event)
  File "venv/lib/python3.8/site-packages/napari/utils/events/event.py", line 631, in _invoke_callback
    _handle_exception(
  File "venv/lib/python3.8/site-packages/napari/utils/events/event.py", line 620, in _invoke_callback
    cb(event)
  File "venv/lib/python3.8/site-packages/napari/_qt/qt_viewer.py", line 403, in _on_add_layer_change
    self._add_layer(layer)
  File "venv/lib/python3.8/site-packages/napari/_qt/qt_viewer.py", line 413, in _add_layer
    vispy_layer = create_vispy_visual(layer)
  File "venv/lib/python3.8/site-packages/napari/_vispy/utils/visual.py", line 62, in create_vispy_visual
    return visual_class(layer)
  File "venv/lib/python3.8/site-packages/napari/_vispy/layers/points.py", line 36, in __init__
    self._on_data_change()
  File "venv/lib/python3.8/site-packages/napari/_vispy/layers/points.py", line 58, in _on_data_change
    set_data(
  File "venv/lib/python3.8/site-packages/vispy/visuals/markers.py", line 678, in set_data
    raise ValueError('edge_width cannot be negative')
ValueError: edge_width cannot be negative
```

## Expected behavior

An error should be returned if the user tries to set the `edge_width` property of the Points layer to a negative value. 

## Environment

```
napari: 0.4.12
Platform: Linux-5.11.0-41-generic-x86_64-with-glibc2.29
System: Ubuntu 20.04.3 LTS
Python: 3.8.10 (default, Sep 28 2021, 16:10:42)  [GCC 9.3.0]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.4
SciPy: 1.7.3
Dask: 2021.11.2
VisPy: 0.9.4

OpenGL:
  - GL version:  4.6.0 NVIDIA 450.156.00
  - MAX_TEXTURE_SIZE: 32768

Screens:
  - screen 1: resolution 2560x1440, scale 1.0
  - screen 2: resolution 2560x1440, scale 1.0
  - screen 3: resolution 2560x1440, scale 1.0

Plugins:
  - console: 0.0.4
  - svg: 0.1.5
```


Discussion:

Resolving patch:
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -831,16 +831,26 @@ def edge_width(self) -> np.ndarray:
     def edge_width(
         self, edge_width: Union[int, float, np.ndarray, list]
     ) -> None:
+        # broadcast to np.array
         edge_width = np.broadcast_to(edge_width, self.data.shape[0]).copy()
-        if self.edge_width_is_relative and np.any(
-            (edge_width > 1) | (edge_width < 0)
-        ):
+
+        # edge width cannot be negative
+        if np.any(edge_width < 0):
+            raise ValueError(
+                trans._(
+                    'All edge_width must be > 0',
+                    deferred=True,
+                )
+            )
+        # if relative edge width is enabled, edge_width must be between 0 and 1
+        if self.edge_width_is_relative and np.any(edge_width > 1):
             raise ValueError(
                 trans._(
-                    'edge_width must be between 0 and 1 if edge_width_is_relative is enabled',
+                    'All edge_width must be between 0 and 1 if edge_width_is_relative is enabled',
                     deferred=True,
                 )
             )
+
         self._edge_width = edge_width
         self.refresh()
 


Test patch:
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -906,6 +906,34 @@ def test_edge_width():
     layer = Points(data, edge_width=3, edge_width_is_relative=False)
     np.testing.assert_array_equal(layer.edge_width, 3)
     assert layer.edge_width_is_relative is False
+    with pytest.raises(ValueError):
+        layer.edge_width = -2
+
+
+@pytest.mark.parametrize(
+    "edge_width",
+    [int(1), float(1), np.array([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]],
+)
+def test_edge_width_types(edge_width):
+    """Test edge_width dtypes with valid values"""
+    shape = (5, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    layer = Points(data, edge_width=edge_width, edge_width_is_relative=False)
+    np.testing.assert_array_equal(layer.edge_width, edge_width)
+
+
+@pytest.mark.parametrize(
+    "edge_width",
+    [int(-1), float(-1), np.array([-1, 2, 3, 4, 5]), [-1, 2, 3, 4, 5]],
+)
+def test_edge_width_types_negative(edge_width):
+    """Test negative values in all edge_width dtypes"""
+    shape = (5, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    with pytest.raises(ValueError):
+        Points(data, edge_width=edge_width, edge_width_is_relative=False)
 
 
 def test_out_of_slice_display():