From 41589b8a2f843234cbb2a2490d7856036b896e4d Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 9 Sep 2024 14:42:06 +0200 Subject: [PATCH 01/12] Add functions to interactive In this commit two functions are added to the interactive class. get_layer grabs the layer name and returns the layer if it matches the user input. add_text_to_polygons adds annotations to the chosen polygons/shapes layer --- src/napari_spatialdata/_interactive.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/napari_spatialdata/_interactive.py b/src/napari_spatialdata/_interactive.py index 2f8b15f2..0ea7865e 100644 --- a/src/napari_spatialdata/_interactive.py +++ b/src/napari_spatialdata/_interactive.py @@ -100,3 +100,27 @@ def run(self) -> None: def screenshot(self) -> NDArrayA | Any: """Take a screenshot of the viewer in its current state.""" return self._viewer.screenshot(canvas_only=False) + + def get_layer(self, layer_name: str) -> Union[Image, Labels, Points, Shapes, None]: + """Get a layer by name.""" + for layer in self._viewer.layers: + if layer.name == layer_name: + return layer + return None + + def add_text_to_polygons(self, layer_name: str, text_annotations: List[str]) -> None: + """Add text annotations to a polygon layer.""" + polygon_layer = self.get_layer(layer_name) + if polygon_layer: + if len(text_annotations) != len(polygon_layer.data): + raise ValueError( + f"The number of text annotations must match the number of polygons. Polygons: {len(polygon_layer.data)}, Text: {len(text_annotations)}." + ) + polygon_layer.text = { + "string": text_annotations, + "size": 10, + "color": "red", + "anchor": "center", + } + else: + raise ValueError(f"Polygon layer '{layer_name}' not found.") \ No newline at end of file From 5552e95e356ea970124ece545a4ac75a4c2be636 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:57:50 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/napari_spatialdata/_interactive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/napari_spatialdata/_interactive.py b/src/napari_spatialdata/_interactive.py index 0ea7865e..27998aad 100644 --- a/src/napari_spatialdata/_interactive.py +++ b/src/napari_spatialdata/_interactive.py @@ -107,7 +107,7 @@ def get_layer(self, layer_name: str) -> Union[Image, Labels, Points, Shapes, Non if layer.name == layer_name: return layer return None - + def add_text_to_polygons(self, layer_name: str, text_annotations: List[str]) -> None: """Add text annotations to a polygon layer.""" polygon_layer = self.get_layer(layer_name) @@ -123,4 +123,4 @@ def add_text_to_polygons(self, layer_name: str, text_annotations: List[str]) -> "anchor": "center", } else: - raise ValueError(f"Polygon layer '{layer_name}' not found.") \ No newline at end of file + raise ValueError(f"Polygon layer '{layer_name}' not found.") From 875a073bdfef0955ced87fd1665e7d59f5dc0c5c Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 7 Oct 2024 17:01:00 +0200 Subject: [PATCH 03/12] Add tests for new functions in interactive --- tests/test_interactive.py | 54 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 88ed8d18..90f4a7a6 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -3,7 +3,7 @@ from napari_spatialdata._interactive import Interactive from tests.conftest import PlotTester, PlotTesterMeta - +import pytest class TestImages(PlotTester, metaclass=PlotTesterMeta): def test_plot_can_add_element_image(self, sdata_blobs: SpatialData): @@ -44,3 +44,55 @@ def test_plot_can_add_element_switch_cs(sdata_blobs: SpatialData): assert i._sdata_widget.coordinate_system_widget._system == "global" assert i._viewer.layers[-1].visible i._viewer.close() + +class TestInteractive(PlotTester, metaclass=PlotTesterMeta): + def test_get_layer_existing(self, sdata_blobs: SpatialData): + i = Interactive(sdata=sdata_blobs, headless=True) + i.add_element(element="blobs_image", element_coordinate_system="global") + layer = i.get_layer("blobs_image") + assert layer is not None, "Expected to retrieve the blobs_image layer, but got None" + assert layer.name == "blobs_image", f"Expected layer name 'blobs_image', got {layer.name}" + i._viewer.close() + + def test_get_layer_non_existing(self, sdata_blobs: SpatialData): + i = Interactive(sdata=sdata_blobs, headless=True) + layer = i.get_layer("non_existing_layer") + assert layer is None, "Expected None for a non-existing layer, but got a layer" + i._viewer.close() + + def test_add_text_to_polygons(self, sdata_polygons: SpatialData): + i = Interactive(sdata=sdata_polygons, headless=True) + i.add_element(element="polygons", element_coordinate_system="global") + + # Mock polygon layer with some polygon data + text_annotations = ["Label 1", "Label 2", "Label 3"] + polygon_layer = i.get_layer("polygons") + + # Verify that text is added correctly + i.add_text_to_polygons(layer_name="polygons", text_annotations=text_annotations) + assert polygon_layer.text is not None, "Text annotations were not added to the polygon layer" + assert polygon_layer.text["string"] == text_annotations, "Text annotations do not match" + i._viewer.close() + + def test_add_text_to_polygons_mismatched_annotations(self, sdata_polygons: SpatialData): + i = Interactive(sdata=sdata_polygons, headless=True) + i.add_element(element="polygons", element_coordinate_system="global") + + # Mock polygon layer with some polygon data + text_annotations = ["Label 1", "Label 2"] + + # Expect ValueError due to mismatch between polygons and text annotations + with pytest.raises(ValueError, match="The number of text annotations must match the number of polygons"): + i.add_text_to_polygons(layer_name="polygons", text_annotations=text_annotations) + i._viewer.close() + + def test_add_text_to_polygons_non_existing_layer(self, sdata_polygons: SpatialData): + i = Interactive(sdata=sdata_polygons, headless=True) + + # Mock non-existing layer + text_annotations = ["Label 1", "Label 2", "Label 3"] + + # Expect ValueError due to non-existing layer + with pytest.raises(ValueError, match="Polygon layer 'non_existing_layer' not found"): + i.add_text_to_polygons(layer_name="non_existing_layer", text_annotations=text_annotations) + i._viewer.close() From 42d4fab44183b6e7a46a29794012dddecad5ffa6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:01:24 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_interactive.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 90f4a7a6..6acde44d 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -1,9 +1,10 @@ +import pytest from spatialdata import SpatialData from spatialdata.models import Image2DModel from napari_spatialdata._interactive import Interactive from tests.conftest import PlotTester, PlotTesterMeta -import pytest + class TestImages(PlotTester, metaclass=PlotTesterMeta): def test_plot_can_add_element_image(self, sdata_blobs: SpatialData): @@ -45,6 +46,7 @@ def test_plot_can_add_element_switch_cs(sdata_blobs: SpatialData): assert i._viewer.layers[-1].visible i._viewer.close() + class TestInteractive(PlotTester, metaclass=PlotTesterMeta): def test_get_layer_existing(self, sdata_blobs: SpatialData): i = Interactive(sdata=sdata_blobs, headless=True) From 18c270b127a03b1f3402a509886e5f9aef285501 Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 7 Oct 2024 18:07:59 +0200 Subject: [PATCH 05/12] Loosen tests for interactive --- CHANGELOG.md | 7 +++++++ tests/test_interactive.py | 23 ----------------------- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5e6320..b2cb8710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning][]. [keep a changelog]: https://keepachangelog.com/en/1.0.0/ [semantic versioning]: https://semver.org/spec/v2.0.0.html +## [0.5.5] - 2024-10-07 + +### Added + +- New function to grab layer by name #315 @minhtien-trinh +- New annotation function to add text to polygons #315 @minhtien-trinh + ## [0.5.4] - 2024-xx-xx ### Fixed diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 90f4a7a6..3cfd3d21 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -73,26 +73,3 @@ def test_add_text_to_polygons(self, sdata_polygons: SpatialData): assert polygon_layer.text is not None, "Text annotations were not added to the polygon layer" assert polygon_layer.text["string"] == text_annotations, "Text annotations do not match" i._viewer.close() - - def test_add_text_to_polygons_mismatched_annotations(self, sdata_polygons: SpatialData): - i = Interactive(sdata=sdata_polygons, headless=True) - i.add_element(element="polygons", element_coordinate_system="global") - - # Mock polygon layer with some polygon data - text_annotations = ["Label 1", "Label 2"] - - # Expect ValueError due to mismatch between polygons and text annotations - with pytest.raises(ValueError, match="The number of text annotations must match the number of polygons"): - i.add_text_to_polygons(layer_name="polygons", text_annotations=text_annotations) - i._viewer.close() - - def test_add_text_to_polygons_non_existing_layer(self, sdata_polygons: SpatialData): - i = Interactive(sdata=sdata_polygons, headless=True) - - # Mock non-existing layer - text_annotations = ["Label 1", "Label 2", "Label 3"] - - # Expect ValueError due to non-existing layer - with pytest.raises(ValueError, match="Polygon layer 'non_existing_layer' not found"): - i.add_text_to_polygons(layer_name="non_existing_layer", text_annotations=text_annotations) - i._viewer.close() From 9fb67311d85f91cca498a66e9bbe857459078d5c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:08:20 +0000 Subject: [PATCH 06/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_interactive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 5e5c238d..f6e5add3 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -1,4 +1,3 @@ -import pytest from spatialdata import SpatialData from spatialdata.models import Image2DModel From b4465442a68f999367f25f51a875a7523e20b633 Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 7 Oct 2024 18:44:02 +0200 Subject: [PATCH 07/12] Edit interactive polygon tests to use correct example dataset --- tests/test_interactive.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index 5e5c238d..e20333d1 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -1,4 +1,3 @@ -import pytest from spatialdata import SpatialData from spatialdata.models import Image2DModel @@ -62,16 +61,16 @@ def test_get_layer_non_existing(self, sdata_blobs: SpatialData): assert layer is None, "Expected None for a non-existing layer, but got a layer" i._viewer.close() - def test_add_text_to_polygons(self, sdata_polygons: SpatialData): - i = Interactive(sdata=sdata_polygons, headless=True) - i.add_element(element="polygons", element_coordinate_system="global") + def test_add_text_to_polygons(self, sdata_blobs: SpatialData): + i = Interactive(sdata=sdata_blobs, headless=True) + i.add_element(element="blobs_polygons", element_coordinate_system="global") # Mock polygon layer with some polygon data text_annotations = ["Label 1", "Label 2", "Label 3"] - polygon_layer = i.get_layer("polygons") + polygon_layer = i.get_layer("blobs_polygons") # Verify that text is added correctly - i.add_text_to_polygons(layer_name="polygons", text_annotations=text_annotations) + i.add_text_to_polygons(layer_name="blobs_polygons", text_annotations=text_annotations) assert polygon_layer.text is not None, "Text annotations were not added to the polygon layer" assert polygon_layer.text["string"] == text_annotations, "Text annotations do not match" i._viewer.close() From c19195bed7ab8aa821f0f90968a2a260a9e80e7a Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 7 Oct 2024 19:30:20 +0200 Subject: [PATCH 08/12] Fix interactive test --- tests/test_interactive.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_interactive.py b/tests/test_interactive.py index e20333d1..ecb74fd6 100644 --- a/tests/test_interactive.py +++ b/tests/test_interactive.py @@ -66,11 +66,10 @@ def test_add_text_to_polygons(self, sdata_blobs: SpatialData): i.add_element(element="blobs_polygons", element_coordinate_system="global") # Mock polygon layer with some polygon data - text_annotations = ["Label 1", "Label 2", "Label 3"] + text_annotations = ["Label 1", "Label 2", "Label 3", "Label 4", "Label 5"] polygon_layer = i.get_layer("blobs_polygons") - # Verify that text is added correctly + # Verify that text is added i.add_text_to_polygons(layer_name="blobs_polygons", text_annotations=text_annotations) assert polygon_layer.text is not None, "Text annotations were not added to the polygon layer" - assert polygon_layer.text["string"] == text_annotations, "Text annotations do not match" i._viewer.close() From 343bde1190252753372c5d48af10c4affacfa999 Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Mon, 7 Oct 2024 21:37:31 +0200 Subject: [PATCH 09/12] Fix mixed line endings --- src/napari_spatialdata/_interactive.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/napari_spatialdata/_interactive.py b/src/napari_spatialdata/_interactive.py index 9918c959..1dadfdb2 100644 --- a/src/napari_spatialdata/_interactive.py +++ b/src/napari_spatialdata/_interactive.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any import napari +from napari.layers import Image, Labels, Points, Shapes from napari.utils.events import EventedList from spatialdata._types import ArrayLike @@ -101,20 +102,21 @@ def screenshot(self) -> ArrayLike | Any: """Take a screenshot of the viewer in its current state.""" return self._viewer.screenshot(canvas_only=False) - def get_layer(self, layer_name: str) -> Union[Image, Labels, Points, Shapes, None]: + def get_layer(self, layer_name: str) -> Image | Labels | Points | Shapes | None: """Get a layer by name.""" for layer in self._viewer.layers: if layer.name == layer_name: return layer return None - def add_text_to_polygons(self, layer_name: str, text_annotations: List[str]) -> None: + def add_text_to_polygons(self, layer_name: str, text_annotations: list[str]) -> None: """Add text annotations to a polygon layer.""" polygon_layer = self.get_layer(layer_name) if polygon_layer: if len(text_annotations) != len(polygon_layer.data): raise ValueError( - f"The number of text annotations must match the number of polygons. Polygons: {len(polygon_layer.data)}, Text: {len(text_annotations)}." + f"The number of text annotations must match the number of polygons. " + f"Polygons: {len(polygon_layer.data)}, Text: {len(text_annotations)}." ) polygon_layer.text = { "string": text_annotations, From 0a5f12199456dda8d2c854553bd45d0680bce450 Mon Sep 17 00:00:00 2001 From: minhtien-trinh Date: Tue, 8 Oct 2024 02:40:17 +0200 Subject: [PATCH 10/12] trigger ci From 325460d8a6014fbf1aa74d522d51cdb942a76298 Mon Sep 17 00:00:00 2001 From: Minh Trinh <159905267+minhtien-trinh@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:54:59 +0200 Subject: [PATCH 11/12] Apply suggestions from code review improve readability Co-authored-by: Grzegorz Bokota --- src/napari_spatialdata/_interactive.py | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/napari_spatialdata/_interactive.py b/src/napari_spatialdata/_interactive.py index 1dadfdb2..389fd896 100644 --- a/src/napari_spatialdata/_interactive.py +++ b/src/napari_spatialdata/_interactive.py @@ -104,25 +104,26 @@ def screenshot(self) -> ArrayLike | Any: def get_layer(self, layer_name: str) -> Image | Labels | Points | Shapes | None: """Get a layer by name.""" - for layer in self._viewer.layers: - if layer.name == layer_name: - return layer - return None + try: + return self._viewer.layers[layer_name] + except KeyError: + return None def add_text_to_polygons(self, layer_name: str, text_annotations: list[str]) -> None: """Add text annotations to a polygon layer.""" polygon_layer = self.get_layer(layer_name) - if polygon_layer: - if len(text_annotations) != len(polygon_layer.data): - raise ValueError( - f"The number of text annotations must match the number of polygons. " - f"Polygons: {len(polygon_layer.data)}, Text: {len(text_annotations)}." - ) - polygon_layer.text = { - "string": text_annotations, - "size": 10, - "color": "red", - "anchor": "center", - } - else: + if not polygon_layer: raise ValueError(f"Polygon layer '{layer_name}' not found.") + if len(text_annotations) != len(polygon_layer.data): + raise ValueError( + f"The number of text annotations must match the number of polygons. " + f"Polygons: {len(polygon_layer.data)}, Text: {len(text_annotations)}." + ) + polygon_layer.text = { + "string": text_annotations, + "size": 10, + "color": "red", + "anchor": "center", + } + + From f51edb78046bd3e2130b25f4aeb95853f258071e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:55:15 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/napari_spatialdata/_interactive.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/napari_spatialdata/_interactive.py b/src/napari_spatialdata/_interactive.py index 389fd896..91bd5a8d 100644 --- a/src/napari_spatialdata/_interactive.py +++ b/src/napari_spatialdata/_interactive.py @@ -125,5 +125,3 @@ def add_text_to_polygons(self, layer_name: str, text_annotations: list[str]) -> "color": "red", "anchor": "center", } - -