Repository: napari/napari

Issue:
pre color test fail from numpy 1.21
## 🐛 Bug

Our pre-tests are currently failing - it's a fairly innocuous fail, so no need to rush a release, but we should fix them. The test is passing an intentionally invalid  `color = ('a', 1, 1, 1)` which is now raising a warning that didn't used to happen. I'm not quite sure how we want to fix, I can see many different ways - but given that no-one should really pass a color with a string in it as long as we do something reasonable I don't mind

```python-traceback
  _________________________ test_invalid_colors[color6] __________________________
  
  color = ('a', 1, 1, 1)
  
      @pytest.mark.parametrize("color", invalid_colors)
      def test_invalid_colors(color):
          with pytest.raises((ValueError, AttributeError, KeyError)):
  >           transform_color(color)
  
  napari/utils/colormaps/_tests/test_color_to_array.py:50: 
  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
  napari/utils/colormaps/standardize_color.py:65: in transform_color
      return _color_switch[colortype](colors)
  _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
  
  colors = ('a', 1, 1, 1)
```
...
```python-traceback
  >           color_array = np.atleast_2d(np.asarray(colors))
  E           FutureWarning: Promotion of numbers and bools to strings is deprecated. In the future, code such as `np.concatenate((['string'], [0]))` will raise an error, while `np.asarray(['string', 0])` will return an array with `dtype=object`.  To avoid the warning while retaining a string result use `dtype='U'` (or 'S').  To get an array of Python objects use `dtype=object`. (Warning added in NumPy 1.21)
```


Discussion:
I decided to fix this by explicitly providing dtype if any of the elements are strings, as effectively suggested by the [associated numpy release note](https://numpy.org/devdocs/release/1.21.0-notes.html#promotion-of-strings-with-numbers-and-bools-is-deprecated).

Some other ideas I had with reasons against them.

1. Filter the warning in the test using `pytest.mark.filterwarnings`. The user may still see the FutureWarning, which may distract from the custom warning/error that we want to show.
2. Filter the warning in the implementation using `with warnings.catch_warnings()` and `warning.simplefilter(...)`. Unless we match the message exactly or with a regex, we may also suppress other potentially important FutureWarnings in the future.
3. Raise an exception if the list-like is not homogeneous. This may prevent desired upcasting if mixing ints/floats is allowed. Even if it doesn't we'd need a new translated string for the message, and the existing one for this test case seems fine ("Invalid color found: a at index 0.").

Feel free to comment if you think something else is preferred.

Resolving patch:
diff --git a/napari/utils/colormaps/standardize_color.py b/napari/utils/colormaps/standardize_color.py
--- a/napari/utils/colormaps/standardize_color.py
+++ b/napari/utils/colormaps/standardize_color.py
@@ -115,7 +115,7 @@ def _handle_list_like(colors: Sequence) -> np.ndarray:
     Parameters
     ----------
     colors : Sequence
-        A list-like container of colors. The colors inside should be homogeneuous
+        A list-like container of colors. The colors inside should be homogeneous
         in their representation.
 
     Returns
@@ -123,11 +123,17 @@ def _handle_list_like(colors: Sequence) -> np.ndarray:
     color_array : np.ndarray
         Nx4 numpy array, with N being the length of ``colors``.
     """
+
     try:
         # The following conversion works for most cases, and so it's expected
         # that most valid inputs will pass this .asarray() call
         # with ease. Those who don't are usually too cryptic to decipher.
-        color_array = np.atleast_2d(np.asarray(colors))
+        # If only some of the colors are strings, explicitly provide an object
+        # dtype to avoid the deprecated behavior described in:
+        # https://github.com/napari/napari/issues/2791
+        num_str = len([c for c in colors if isinstance(c, str)])
+        dtype = 'O' if 0 < num_str < len(colors) else None
+        color_array = np.atleast_2d(np.asarray(colors, dtype=dtype))
     except ValueError:
         warnings.warn(
             trans._(


Test patch: