Repository: napari/napari

Issue:
Remove deprecated brush_shape
## 🧰 Task

Following on from #2426 by @ppwadhwa, are we now ready to remove the deprecated brush shape and only allow for the square brush? It will simplify the codebase. @jni if you're good for this to go now then @ppwadhwa you could pick this up when you have time - should be pretty easy once green light from @jni. We'd just be left with the circle brush as the only brush type after this.


Discussion:
Yup I'm good with this! 👍 
@ppwadhwa this could be a good thing to pick up during the "quality sprint" if you like too

Resolving patch:
diff --git a/napari/benchmarks/benchmark_labels_layer.py b/napari/benchmarks/benchmark_labels_layer.py
--- a/napari/benchmarks/benchmark_labels_layer.py
+++ b/napari/benchmarks/benchmark_labels_layer.py
@@ -41,14 +41,8 @@ def time_raw_to_displayed(self, n):
         """Time to convert raw to displayed."""
         self.layer._raw_to_displayed(self.layer._data_raw)
 
-    def time_paint_square(self, n):
-        """Time to paint square."""
-        self.layer.brush_shape = 'square'
-        self.layer.paint((0,) * 2, self.layer.selected_label)
-
     def time_paint_circle(self, n):
         """Time to paint circle."""
-        self.layer.brush_shape = 'circle'
         self.layer.paint((0,) * 2, self.layer.selected_label)
 
     def time_fill(self, n):
@@ -102,14 +96,8 @@ def time_raw_to_displayed(self, n):
         """Time to convert raw to displayed."""
         self.layer._raw_to_displayed(self.layer._data_raw)
 
-    def time_paint_square(self, n):
-        """Time to paint square."""
-        self.layer.brush_shape = 'square'
-        self.layer.paint((0,) * 3, self.layer.selected_label)
-
     def time_paint_circle(self, n):
         """Time to paint circle."""
-        self.layer.brush_shape = 'circle'
         self.layer.paint((0,) * 3, self.layer.selected_label)
 
     def time_fill(self, n):
diff --git a/napari/layers/labels/_labels_constants.py b/napari/layers/labels/_labels_constants.py
--- a/napari/layers/labels/_labels_constants.py
+++ b/napari/layers/labels/_labels_constants.py
@@ -52,26 +52,8 @@ class LabelColorMode(StringEnum):
     DIRECT = auto()
 
 
-class LabelBrushShape(StringEnum):
-    """
-    LabelBrushShape: Labelling brush shape.
-
-    CIRCLE (default) uses circle paintbrush (case insensitive).
-
-    SQUARE uses square paintbrush (case insensitive).
-    """
-
-    CIRCLE = auto()
-    SQUARE = auto()
-
-
 BACKSPACE = 'delete' if sys.platform == 'darwin' else 'backspace'
-LABEL_BRUSH_SHAPE_TRANSLATIONS = OrderedDict(
-    [
-        (LabelBrushShape.CIRCLE, trans._("circle")),
-        (LabelBrushShape.SQUARE, trans._("square")),
-    ]
-)
+
 LABEL_COLOR_MODE_TRANSLATIONS = OrderedDict(
     [
         (LabelColorMode.AUTO, trans._("auto")),
diff --git a/napari/layers/labels/labels.py b/napari/layers/labels/labels.py
--- a/napari/layers/labels/labels.py
+++ b/napari/layers/labels/labels.py
@@ -20,7 +20,7 @@
 from ..image.image import _ImageBase
 from ..utils.color_transformations import transform_color
 from ..utils.layer_utils import validate_properties
-from ._labels_constants import LabelBrushShape, LabelColorMode, Mode
+from ._labels_constants import LabelColorMode, Mode
 from ._labels_mouse_bindings import draw, pick
 from ._labels_utils import indices_in_shape, sphere_indices
 
@@ -199,7 +199,6 @@ def __init__(
         )
         self._all_vals[0] = 0
         self._color_mode = LabelColorMode.AUTO
-        self._brush_shape = LabelBrushShape.CIRCLE
         self._show_selected_label = False
         self._contour = 0
 
@@ -554,41 +553,6 @@ def show_selected_label(self, filter):
         self._show_selected_label = filter
         self.refresh()
 
-    @property
-    def brush_shape(self):
-        """str: Paintbrush shape"""
-
-        warnings.warn(
-            (
-                trans._(
-                    "The square brush shape is deprecated and will be removed in version 0.4.9. Afterward, only the circle brush shape will be available, and the layer.brush_shape attribute will be removed.",
-                    deferred=True,
-                )
-            ),
-            category=FutureWarning,
-            stacklevel=2,
-        )
-
-        return str(self._brush_shape)
-
-    @brush_shape.setter
-    def brush_shape(self, brush_shape):
-        """Set current brush shape."""
-
-        warnings.warn(
-            (
-                trans._(
-                    "The square brush shape is deprecated and will be removed in version 0.4.9. Afterward, only the circle brush shape will be available, and the layer.brush_shape attribute will be removed.",
-                    deferred=True,
-                )
-            ),
-            category=FutureWarning,
-            stacklevel=2,
-        )
-
-        self._brush_shape = LabelBrushShape(brush_shape)
-        self.cursor = self.brush_shape
-
     @property
     def mode(self):
         """MODE: Interactive mode. The normal, default mode is PAN_ZOOM, which
@@ -643,7 +607,7 @@ def mode(self, mode: Union[str, Mode]):
             )
             self.mouse_drag_callbacks.append(pick)
         elif mode == Mode.PAINT:
-            self.cursor = str(self._brush_shape)
+            self.cursor = 'circle'
             self.cursor_size = self._calculate_cursor_size()
             self.interactive = False
             self.help = trans._(
@@ -658,7 +622,7 @@ def mode(self, mode: Union[str, Mode]):
             )
             self.mouse_drag_callbacks.append(draw)
         elif mode == Mode.ERASE:
-            self.cursor = str(self._brush_shape)
+            self.cursor = 'circle'
             self.cursor_size = self._calculate_cursor_size()
             self.interactive = False
             self.help = trans._(
@@ -1074,63 +1038,40 @@ def paint(self, coord, new_label, refresh=True):
         paint_scale = np.array(
             [self.scale[i] for i in dims_to_paint], dtype=float
         )
-        if str(self._brush_shape) == "square":
-            brush_size_dims = [self.brush_size] * self.ndim
-            if self.n_edit_dimensions < self.ndim:
-                for i in range(self.ndim):
-                    if i not in dims_to_paint:
-                        brush_size_dims[i] = 1
-
-            slice_coord = tuple(
-                slice(
-                    np.round(np.clip(c - brush_size / 2 + 0.5, 0, s)).astype(
-                        int
-                    ),
-                    np.round(np.clip(c + brush_size / 2 + 0.5, 0, s)).astype(
-                        int
-                    ),
-                    1,
-                )
-                for c, s, brush_size in zip(
-                    coord, self.data.shape, brush_size_dims
+
+        slice_coord = [int(np.round(c)) for c in coord]
+        if self.n_edit_dimensions < self.ndim:
+            coord_paint = [coord[i] for i in dims_to_paint]
+            shape = [shape[i] for i in dims_to_paint]
+        else:
+            coord_paint = coord
+
+        # Ensure circle doesn't have spurious point
+        # on edge by keeping radius as ##.5
+        radius = np.floor(self.brush_size / 2) + 0.5
+        mask_indices = sphere_indices(radius, tuple(paint_scale))
+
+        mask_indices = mask_indices + np.round(np.array(coord_paint)).astype(
+            int
+        )
+
+        # discard candidate coordinates that are out of bounds
+        mask_indices = indices_in_shape(mask_indices, shape)
+
+        # Transfer valid coordinates to slice_coord,
+        # or expand coordinate if 3rd dim in 2D image
+        slice_coord_temp = [m for m in mask_indices.T]
+        if self.n_edit_dimensions < self.ndim:
+            for j, i in enumerate(dims_to_paint):
+                slice_coord[i] = slice_coord_temp[j]
+            for i in dims_not_painted:
+                slice_coord[i] = slice_coord[i] * np.ones(
+                    mask_indices.shape[0], dtype=int
                 )
-            )
-            slice_coord = tuple(map(np.ravel, np.mgrid[slice_coord]))
-            slice_coord = indices_in_shape(slice_coord, shape)
-        elif str(self._brush_shape) == "circle":
-            slice_coord = [int(np.round(c)) for c in coord]
-            if self.n_edit_dimensions < self.ndim:
-                coord_paint = [coord[i] for i in dims_to_paint]
-                shape = [shape[i] for i in dims_to_paint]
-            else:
-                coord_paint = coord
-
-            # Ensure circle doesn't have spurious point
-            # on edge by keeping radius as ##.5
-            radius = np.floor(self.brush_size / 2) + 0.5
-            mask_indices = sphere_indices(radius, tuple(paint_scale))
-
-            mask_indices = mask_indices + np.round(
-                np.array(coord_paint)
-            ).astype(int)
-
-            # discard candidate coordinates that are out of bounds
-            mask_indices = indices_in_shape(mask_indices, shape)
-
-            # Transfer valid coordinates to slice_coord,
-            # or expand coordinate if 3rd dim in 2D image
-            slice_coord_temp = [m for m in mask_indices.T]
-            if self.n_edit_dimensions < self.ndim:
-                for j, i in enumerate(dims_to_paint):
-                    slice_coord[i] = slice_coord_temp[j]
-                for i in dims_not_painted:
-                    slice_coord[i] = slice_coord[i] * np.ones(
-                        mask_indices.shape[0], dtype=int
-                    )
-            else:
-                slice_coord = slice_coord_temp
+        else:
+            slice_coord = slice_coord_temp
 
-            slice_coord = tuple(slice_coord)
+        slice_coord = tuple(slice_coord)
 
         # Fix indexing for xarray if necessary
         # See http://xarray.pydata.org/en/stable/indexing.html#vectorized-indexing


Test patch:
diff --git a/napari/layers/labels/_tests/test_labels.py b/napari/layers/labels/_tests/test_labels.py
--- a/napari/layers/labels/_tests/test_labels.py
+++ b/napari/layers/labels/_tests/test_labels.py
@@ -619,31 +619,32 @@ def test_show_selected_label():
 
 
 def test_paint():
-    """Test painting labels with different square brush sizes."""
+    """Test painting labels with different circle brush sizes."""
     np.random.seed(0)
     data = np.random.randint(20, size=(10, 15))
     data[:10, :10] = 1
     layer = Labels(data)
 
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = 'square'
     assert np.unique(layer.data[:5, :5]) == 1
     assert np.unique(layer.data[5:10, 5:10]) == 1
 
     layer.brush_size = 9
     layer.paint([0, 0], 2)
-    assert np.unique(layer.data[:5, :5]) == 2
+    assert np.unique(layer.data[:4, :4]) == 2
     assert np.unique(layer.data[5:10, 5:10]) == 1
 
     layer.brush_size = 10
     layer.paint([0, 0], 2)
-    assert np.unique(layer.data[:6, :6]) == 2
+    assert np.unique(layer.data[0:6, 0:3]) == 2
+    assert np.unique(layer.data[0:3, 0:6]) == 2
     assert np.unique(layer.data[6:10, 6:10]) == 1
 
     layer.brush_size = 19
     layer.paint([0, 0], 2)
-    assert np.unique(layer.data[:5, :5]) == 2
-    assert np.unique(layer.data[5:10, 5:10]) == 2
+    assert np.unique(layer.data[0:4, 0:10]) == 2
+    assert np.unique(layer.data[0:10, 0:4]) == 2
+    assert np.unique(layer.data[3:7, 3:7]) == 2
+    assert np.unique(layer.data[7:10, 7:10]) == 1
 
 
 def test_paint_with_preserve_labels():
@@ -651,30 +652,23 @@ def test_paint_with_preserve_labels():
     data = np.zeros((15, 10), dtype=np.uint32)
     data[:3, :3] = 1
     layer = Labels(data)
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = 'square'
+
     layer.preserve_labels = True
     assert np.unique(layer.data[:3, :3]) == 1
 
     layer.brush_size = 9
     layer.paint([0, 0], 2)
 
-    assert np.unique(layer.data[3:5, 0:5]) == 2
-    assert np.unique(layer.data[0:5, 3:5]) == 2
+    assert np.unique(layer.data[3:5, 0:3]) == 2
+    assert np.unique(layer.data[0:3, 3:5]) == 2
     assert np.unique(layer.data[:3, :3]) == 1
 
 
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum",
-    [("circle", [41, 137, 137, 41, 349]), ("square", [36, 144, 169, 36, 400])],
-)
-def test_paint_2d(brush_shape, expected_sum):
-    """Test painting labels with circle/square brush."""
+def test_paint_2d():
+    """Test painting labels with circle brush."""
     data = np.zeros((40, 40), dtype=np.uint32)
     layer = Labels(data)
     layer.brush_size = 12
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
     layer.mode = 'paint'
     layer.paint((0, 0), 3)
 
@@ -690,43 +684,31 @@ def test_paint_2d(brush_shape, expected_sum):
     layer.brush_size = 20
     layer.paint((15, 27), 7)
 
-    assert np.sum(layer.data[:8, :8] == 3) == expected_sum[0]
-    assert np.sum(layer.data[9:22, 2:15] == 4) == expected_sum[1]
-    assert np.sum(layer.data[24:37, 2:15] == 5) == expected_sum[2]
-    assert np.sum(layer.data[33:, 33:] == 6) == expected_sum[3]
-    assert np.sum(layer.data[5:26, 17:38] == 7) == expected_sum[4]
+    assert np.sum(layer.data[:8, :8] == 3) == 41
+    assert np.sum(layer.data[9:22, 2:15] == 4) == 137
+    assert np.sum(layer.data[24:37, 2:15] == 5) == 137
+    assert np.sum(layer.data[33:, 33:] == 6) == 41
+    assert np.sum(layer.data[5:26, 17:38] == 7) == 349
 
 
 @pytest.mark.timeout(1)
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum",
-    [("circle", 411), ("square", 432)],
-)
-def test_paint_2d_xarray(brush_shape, expected_sum):
+def test_paint_2d_xarray():
     """Test the memory usage of painting an xarray indirectly via timeout."""
     data = xr.DataArray(np.zeros((3, 3, 1024, 1024), dtype=np.uint32))
 
     layer = Labels(data)
     layer.brush_size = 12
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
     layer.mode = 'paint'
     layer.paint((1, 1, 512, 512), 3)
     assert isinstance(layer.data, xr.DataArray)
-    assert layer.data.sum() == expected_sum
+    assert layer.data.sum() == 411
 
 
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum",
-    [("circle", [137, 1189, 1103]), ("square", [144, 1728, 1548])],
-)
-def test_paint_3d(brush_shape, expected_sum):
-    """Test painting labels with circle/square brush on 3D image."""
+def test_paint_3d():
+    """Test painting labels with circle brush on 3D image."""
     data = np.zeros((30, 40, 40), dtype=np.uint32)
     layer = Labels(data)
     layer.brush_size = 12
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
     layer.mode = 'paint'
 
     # Paint in 2D
@@ -741,9 +723,9 @@ def test_paint_3d(brush_shape, expected_sum):
     layer.preserve_labels = True
     layer.paint((10, 15, 15), 5)
 
-    assert np.sum(layer.data[4:17, 4:17, 4:17] == 3) == expected_sum[0]
-    assert np.sum(layer.data[4:17, 19:32, 4:17] == 4) == expected_sum[1]
-    assert np.sum(layer.data[4:17, 9:32, 9:32] == 5) == expected_sum[2]
+    assert np.sum(layer.data[4:17, 4:17, 4:17] == 3) == 137
+    assert np.sum(layer.data[4:17, 19:32, 4:17] == 4) == 1189
+    assert np.sum(layer.data[4:17, 9:32, 9:32] == 5) == 1103
 
 
 def test_fill():
@@ -799,10 +781,9 @@ def test_world_data_extent():
 
 
 @pytest.mark.parametrize(
-    'brush_shape, brush_size, mode, selected_label, preserve_labels, n_dimensional',
+    'brush_size, mode, selected_label, preserve_labels, n_dimensional',
     list(
         itertools.product(
-            ['square', 'circle'],
             list(range(1, 22, 5)),
             ['fill', 'erase', 'paint'],
             [1, 20, 100],
@@ -812,7 +793,6 @@ def test_world_data_extent():
     ),
 )
 def test_undo_redo(
-    brush_shape,
     brush_size,
     mode,
     selected_label,
@@ -822,8 +802,6 @@ def test_undo_redo(
     blobs = data.binary_blobs(length=64, volume_fraction=0.3, n_dim=3)
     layer = Labels(blobs)
     data_history = [blobs.copy()]
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
     layer.brush_size = brush_size
     layer.mode = mode
     layer.selected_label = selected_label
@@ -870,8 +848,6 @@ def test_ndim_paint():
     test_array = np.zeros((5, 6, 7, 8), dtype=int)
     layer = Labels(test_array)
     layer.n_edit_dimensions = 3
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = 'circle'
     layer.brush_size = 2  # equivalent to 18-connected 3D neighborhood
     layer.paint((1, 1, 1, 1), 1)
 
diff --git a/napari/layers/labels/_tests/test_labels_mouse_bindings.py b/napari/layers/labels/_tests/test_labels_mouse_bindings.py
--- a/napari/layers/labels/_tests/test_labels_mouse_bindings.py
+++ b/napari/layers/labels/_tests/test_labels_mouse_bindings.py
@@ -27,17 +27,12 @@ def Event():
     )
 
 
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum", [("circle", 244), ("square", 274)]
-)
-def test_paint(Event, brush_shape, expected_sum):
-    """Test painting labels with circle/square brush."""
+def test_paint(Event):
+    """Test painting labels with circle brush."""
     data = np.ones((20, 20), dtype=np.int32)
     layer = Labels(data)
     layer.brush_size = 10
     assert layer.cursor_size == 10
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
 
     layer.mode = 'paint'
     layer.selected_label = 3
@@ -66,19 +61,14 @@ def test_paint(Event, brush_shape, expected_sum):
     assert np.unique(layer.data[-8:, -8:]) == 3
     assert np.unique(layer.data[:5, -5:]) == 1
     assert np.unique(layer.data[-5:, :5]) == 1
-    assert np.sum(layer.data == 3) == expected_sum
+    assert np.sum(layer.data == 3) == 244
 
 
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum", [("circle", 244), ("square", 274)]
-)
-def test_paint_scale(Event, brush_shape, expected_sum):
-    """Test painting labels with circle/square brush when scaled."""
+def test_paint_scale(Event):
+    """Test painting labels with circle brush when scaled."""
     data = np.ones((20, 20), dtype=np.int32)
     layer = Labels(data, scale=(2, 2))
     layer.brush_size = 10
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
 
     layer.mode = 'paint'
     layer.selected_label = 3
@@ -107,25 +97,16 @@ def test_paint_scale(Event, brush_shape, expected_sum):
     assert np.unique(layer.data[-8:, -8:]) == 3
     assert np.unique(layer.data[:5, -5:]) == 1
     assert np.unique(layer.data[-5:, :5]) == 1
-    assert np.sum(layer.data == 3) == expected_sum
+    assert np.sum(layer.data == 3) == 244
 
 
-@pytest.mark.parametrize(
-    "brush_shape, expected_sum", [("circle", 156), ("square", 126)]
-)
-def test_erase(Event, brush_shape, expected_sum):
+def test_erase(Event):
     """Test erasing labels with different brush shapes."""
     data = np.ones((20, 20), dtype=np.int32)
     layer = Labels(data)
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
-
     layer.brush_size = 10
     layer.mode = 'erase'
 
-    with pytest.warns(FutureWarning):
-        layer.brush_shape = brush_shape
-
     layer.selected_label = 3
 
     # Simulate click
@@ -152,7 +133,7 @@ def test_erase(Event, brush_shape, expected_sum):
     assert np.unique(layer.data[-8:, -8:]) == 0
     assert np.unique(layer.data[:5, -5:]) == 1
     assert np.unique(layer.data[-5:, :5]) == 1
-    assert np.sum(layer.data == 1) == expected_sum
+    assert np.sum(layer.data == 1) == 156
 
 
 def test_pick(Event):