From 2f12e1bb9004cc58d7d2bc2c820a5849a24405d7 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:56:11 -0500 Subject: [PATCH 01/10] switch to ruff, drop conda cruft from tox.ini --- .pre-commit-config.yaml | 25 ++++++------------------- README.md | 7 +++---- pyproject.toml | 40 ++++++++++++++++++++++++++++++++++++++++ setup.cfg | 22 +--------------------- tox.ini | 17 ++--------------- 5 files changed, 52 insertions(+), 59 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2efce83..5033761 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,12 @@ repos: -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - exclude: _vendor|vendored|examples -- repo: https://github.com/python/black - rev: 22.3.0 +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.12.1 hooks: - id: black pass_filenames: true exclude: _vendor|vendored|examples -- repo: https://github.com/PyCQA/flake8 - rev: 3.8.4 - hooks: - - id: flake8 - pass_filenames: true - # this seems to need to be here in addition to setup.cfg - exclude: _vendor|vendored|__init__.py|examples -- repo: https://github.com/asottile/pyupgrade - rev: v2.7.4 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.9 hooks: - - id: pyupgrade - args: ["--py37-plus"] - exclude: _vendor|vendored|examples + - id: ruff + exclude: _vendor|vendored \ No newline at end of file diff --git a/README.md b/README.md index 5edf801..4858b82 100644 --- a/README.md +++ b/README.md @@ -106,10 +106,9 @@ You can make sure your `[dev]` installation is working properly by running `pytest .` from within the repository. > [!NOTE] -> We use [`pre-commit`](https://pre-commit.com) to sort imports with -> [`isort`](https://github.com/timothycrosley/isort), format code with -> [`black`](https://github.com/psf/black), and lint with -> [`flake8`](https://github.com/PyCQA/flake8) automatically prior to each commit. +> We use [`pre-commit`](https://pre-commit.com) to sort imports and lint with +> [`ruff`](https://github.com/astral-sh/ruff) and format code with +> [`black`](https://github.com/psf/black) automatically prior to each commit. > To minmize test errors when submitting pull requests, please install `pre-commit` > in your environment as follows: > ```sh diff --git a/pyproject.toml b/pyproject.toml index b9e32bb..c0074a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,43 @@ exclude = ''' | ) ''' + +# same as napari/napari +[tool.ruff] +line-length = 79 +select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "YTT", #flake8-2020 + "TCH", # flake8-type-checing + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "COM", # flake8-commas + "SIM", # flake8-simplify + "INP", # flake8-no-pep420 + "PYI", # flake8-pyi + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "TID", # flake8-tidy-imports # replace absolutify import + "TRY", # tryceratops + "ICN", # flake8-import-conventions + "RUF", # ruff specyfic rules +] +ignore = [ + "E501", "UP006", "UP007", "TCH001", "TCH002", "TCH003", + "A003", # flake8-builtins - we have class attributes violating these rule + "COM812", # flake8-commas - we don't like adding comma on single line of arguments + "SIM117", # flake8-simplify - we some of merged with statements are not looking great with black, reanble after drop python 3.9 + "Q000", + "RET504", # not fixed yet https://github.com/charliermarsh/ruff/issues/2950 + "TRY003", # require implement multiple exception class + "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 + "B028", # need to be fixed + "PYI015", # it produces bad looking files (@jni opinion) diff --git a/setup.cfg b/setup.cfg index c5afde7..96f8b41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,8 +47,7 @@ testing = dev = pre-commit black - flake8 - isort + ruff check-manifest %(testing)s @@ -60,25 +59,6 @@ napari.manifest = [options.package_data] napari-animation = napari.yaml -[isort] -profile = black -line_length = 79 -skip_glob = examples/ -multi_line_output=3 - - -[flake8] -# Ignores - https://lintlyci.github.io/Flake8Rules -# E203 Whitespace before ':' (sometimes conflicts with black) -# E501 line too long (84 > 79 characters) (sometimes too annoying) -# W503 Line break occurred before a binary operator -# C901 McCabe complexity test. Would be nice to re-enable, but takes work -ignore = E203,W503,E501,C901 -max-line-length = 79 -max-complexity = 18 -exclude = __init__.py,examples - - [coverage:report] exclude_lines = pragma: no cover diff --git a/tox.ini b/tox.ini index 5af5a54..3311010 100644 --- a/tox.ini +++ b/tox.ini @@ -26,11 +26,6 @@ passenv = XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN -conda_deps = - # use conda to install numcodecs on mac py3.9 - py39-macos: numcodecs -conda_channels = - conda-forge deps = pytest # https://docs.pytest.org/en/latest/contents.html pytest-cov # https://pytest-cov.readthedocs.io/en/latest/ @@ -40,16 +35,8 @@ deps = pyside: napari[pyside2,testing] commands = pytest -v --color=yes --cov=napari_animation --cov-report=xml -[testenv:isort] -skip_install = True -deps = pre-commit -commands = pre-commit run isort --all-files - - -[testenv:flake8] -skip_install = True -deps = pre-commit -commands = pre-commit run flake8 --all-files +[testenv:ruff] +commands = pre-commit run ruff --all-files [testenv:black] From b243d339b04479757742322d33b6e768b3d0af5d Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:06:20 -0500 Subject: [PATCH 02/10] fix workflow --- .github/workflows/test_and_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index fd00677..baee21c 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - task: [flake8, black, isort] + task: [black, ruff] steps: - uses: actions/checkout@v4 - name: Set up Python 3.8 From 374304d0655b856d6775435210a1a20dcd949b4a Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:07:50 -0500 Subject: [PATCH 03/10] fix pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c0074a0..97b4f89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,4 +69,4 @@ ignore = [ "TRY003", # require implement multiple exception class "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 "B028", # need to be fixed - "PYI015", # it produces bad looking files (@jni opinion) + "PYI015", # it produces bad looking files (@jni opinion) \ No newline at end of file From 758da18f234dafbc9f95111ba26784661a82057b Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:11:45 -0500 Subject: [PATCH 04/10] really fix pyproject.toml --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 97b4f89..3aa4456 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,4 +69,5 @@ ignore = [ "TRY003", # require implement multiple exception class "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 "B028", # need to be fixed - "PYI015", # it produces bad looking files (@jni opinion) \ No newline at end of file + "PYI015", # it produces bad looking files (@jni opinion) +] From 25533f56502c7150e45e526ffddf48316bf455f7 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:23:17 -0500 Subject: [PATCH 05/10] black and ruff on animation_widget --- .pre-commit-config.yaml | 1 + napari_animation/_qt/animation_widget.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5033761..34f7bbf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,4 +9,5 @@ repos: rev: v0.1.9 hooks: - id: ruff + args: [ --fix ] exclude: _vendor|vendored \ No newline at end of file diff --git a/napari_animation/_qt/animation_widget.py b/napari_animation/_qt/animation_widget.py index 0c64299..a5ecc86 100644 --- a/napari_animation/_qt/animation_widget.py +++ b/napari_animation/_qt/animation_widget.py @@ -10,7 +10,8 @@ QWidget, ) -from ..animation import Animation +from napari_animation.animation import Animation + from .frame_widget import FrameWidget from .keyframelistcontrol_widget import KeyFrameListControlWidget from .keyframeslist_widget import KeyFramesListWidget @@ -152,7 +153,6 @@ def _on_slider_moved(self, event=None): self.animation.set_movie_frame_index(frame_index) def _save_callback(self, event=None): - saveDialogWidget = SaveDialogWidget(self) animation_kwargs = saveDialogWidget.getAnimationParameters( self, "Save animation", str(Path.home()) From a0c53acc5466a5aa5ab4764193edf69f1d27ddd4 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:45:57 -0500 Subject: [PATCH 06/10] skip examples in pre-comm ruff --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 34f7bbf..4ede3f8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,4 +10,4 @@ repos: hooks: - id: ruff args: [ --fix ] - exclude: _vendor|vendored \ No newline at end of file + exclude: _vendor|vendored|examples \ No newline at end of file From 109fca5622ffc8c30e3fd80ab29c7d3dddfad7bf Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:47:52 -0500 Subject: [PATCH 07/10] use napari-cookiecutter ruff config --- pyproject.toml | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3aa4456..dd39c52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,15 +31,13 @@ exclude = ''' ) ''' -# same as napari/napari +# same as napari-cookiecutter [tool.ruff] line-length = 79 select = [ "E", "F", "W", #flake8 "UP", # pyupgrade "I", # isort - "YTT", #flake8-2020 - "TCH", # flake8-type-checing "BLE", # flake8-blind-exception "B", # flake8-bugbear "A", # flake8-builtins @@ -47,27 +45,10 @@ select = [ "ISC", # flake8-implicit-str-concat "G", # flake8-logging-format "PIE", # flake8-pie - "COM", # flake8-commas "SIM", # flake8-simplify - "INP", # flake8-no-pep420 - "PYI", # flake8-pyi - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "TID", # flake8-tidy-imports # replace absolutify import - "TRY", # tryceratops - "ICN", # flake8-import-conventions - "RUF", # ruff specyfic rules ] ignore = [ - "E501", "UP006", "UP007", "TCH001", "TCH002", "TCH003", - "A003", # flake8-builtins - we have class attributes violating these rule - "COM812", # flake8-commas - we don't like adding comma on single line of arguments - "SIM117", # flake8-simplify - we some of merged with statements are not looking great with black, reanble after drop python 3.9 - "Q000", - "RET504", # not fixed yet https://github.com/charliermarsh/ruff/issues/2950 - "TRY003", # require implement multiple exception class - "RUF005", # problem with numpy compatybility, see https://github.com/charliermarsh/ruff/issues/2142#issuecomment-1451038741 - "B028", # need to be fixed - "PYI015", # it produces bad looking files (@jni opinion) + "E501", # line too long. let black handle this + "UP006", "UP007", # type annotation. As using magicgui require runtime type annotation then we disable this. + "SIM117", # flake8-simplify - some of merged with statements are not looking great with black, reanble after drop python 3.9 ] From a9d806c1b5459cf274311deb1e134f6552b3e260 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:52:21 -0500 Subject: [PATCH 08/10] add target version and fix true --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dd39c52..30237bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,3 +52,6 @@ ignore = [ "UP006", "UP007", # type annotation. As using magicgui require runtime type annotation then we disable this. "SIM117", # flake8-simplify - some of merged with statements are not looking great with black, reanble after drop python 3.9 ] + +target-version = "py38" +fix = true From 22e397eef18179e0a371ce8f2b1dd287df7a77c9 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:56:27 -0500 Subject: [PATCH 09/10] fix tox.ini --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 3311010..78b16cb 100644 --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,8 @@ deps = commands = pytest -v --color=yes --cov=napari_animation --cov-report=xml [testenv:ruff] +skip_install = True +deps = pre-commit commands = pre-commit run ruff --all-files From bee395dad9c05119e5f5088a2c7e080f8477bc40 Mon Sep 17 00:00:00 2001 From: Peter Sobolewski <76622105+psobolewskiPhD@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:17:00 -0500 Subject: [PATCH 10/10] ruff fixes --- napari_animation/__init__.py | 2 ++ napari_animation/_qt/__init__.py | 2 ++ napari_animation/_qt/savedialog_widget.py | 4 ++-- napari_animation/_tests/conftest.py | 2 +- napari_animation/_tests/test_animation.py | 2 +- napari_animation/_tests/test_utils.py | 6 +++--- napari_animation/animation.py | 8 ++++---- napari_animation/frame_sequence.py | 6 +++--- napari_animation/interpolation/__init__.py | 2 ++ napari_animation/interpolation/utils.py | 4 ++-- .../interpolation/viewer_state_interpolation.py | 2 +- napari_animation/utils.py | 2 +- napari_animation/viewer_state.py | 5 ++--- 13 files changed, 26 insertions(+), 21 deletions(-) diff --git a/napari_animation/__init__.py b/napari_animation/__init__.py index 9dfef51..0162c84 100644 --- a/napari_animation/__init__.py +++ b/napari_animation/__init__.py @@ -2,3 +2,5 @@ from .animation import Animation from .key_frame import KeyFrame from .viewer_state import ViewerState + +__all__ = ["AnimationWidget", "Animation", "KeyFrame", "ViewerState"] diff --git a/napari_animation/_qt/__init__.py b/napari_animation/_qt/__init__.py index 3096911..5f55813 100644 --- a/napari_animation/_qt/__init__.py +++ b/napari_animation/_qt/__init__.py @@ -1 +1,3 @@ from .animation_widget import AnimationWidget + +__all__ = ["AnimationWidget"] diff --git a/napari_animation/_qt/savedialog_widget.py b/napari_animation/_qt/savedialog_widget.py index a540195..0189cf0 100644 --- a/napari_animation/_qt/savedialog_widget.py +++ b/napari_animation/_qt/savedialog_widget.py @@ -35,12 +35,12 @@ def getAnimationParameters( self, parent=None, caption="Select a file :", - dir=".", + directory=".", options=None, ): # Set dialog parameters self.setWindowTitle(caption) - self.setDirectory(dir) + self.setDirectory(directory) self.setNameFilter(self._qt_file_name_filters) self.setFileMode(QFileDialog.AnyFile) self.setAcceptMode(QFileDialog.AcceptSave) diff --git a/napari_animation/_tests/conftest.py b/napari_animation/_tests/conftest.py index 068485b..116dc81 100644 --- a/napari_animation/_tests/conftest.py +++ b/napari_animation/_tests/conftest.py @@ -18,7 +18,7 @@ def empty_animation(make_napari_viewer): @pytest.fixture def animation_with_key_frames(empty_animation): - for i in range(2): + for _i in range(2): empty_animation.capture_keyframe() empty_animation.viewer.camera.zoom *= 2 return empty_animation diff --git a/napari_animation/_tests/test_animation.py b/napari_animation/_tests/test_animation.py index 7843a1d..fe273d1 100644 --- a/napari_animation/_tests/test_animation.py +++ b/napari_animation/_tests/test_animation.py @@ -139,7 +139,7 @@ def test_animation_file_metadata(animation_with_key_frames, tmp_path, ext): def test_layer_attribute_capture(layer_state, attribute): """Test that 'attribute' is captured in the layer state dictionary""" for layer_state_dict in layer_state.values(): - assert attribute in layer_state_dict.keys() + assert attribute in layer_state_dict def test_end_state_reached(image_animation): diff --git a/napari_animation/_tests/test_utils.py b/napari_animation/_tests/test_utils.py index 5f8bf0f..7d580e5 100644 --- a/napari_animation/_tests/test_utils.py +++ b/napari_animation/_tests/test_utils.py @@ -5,7 +5,7 @@ input_dict = [{"a": 1, "b": {"c": "d"}}] keys = [["b", "c"]] expected = ["d"] -test_set = [param for param in zip(input_dict, keys, expected)] +test_set = list(zip(input_dict, keys, expected)) @pytest.mark.parametrize("input_dict,keys,expected", test_set) @@ -16,12 +16,12 @@ def test_nested_get(input_dict, keys, expected): input_dict = [{"a": 1, "b": {"c": "d"}, "e": {}}] expected = [[["a"], ["b", "c"], ["e"]]] -test_set = [param for param in zip(input_dict, expected)] +test_set = list(zip(input_dict, expected)) @pytest.mark.parametrize("input_dict,expected", test_set) def test_keys_to_list(input_dict, expected): - result = [keys for keys in keys_to_list(input_dict)] + result = list(keys_to_list(input_dict)) for keys in result: assert isinstance(keys, list) assert result == expected diff --git a/napari_animation/animation.py b/napari_animation/animation.py index faf49c6..4179e76 100644 --- a/napari_animation/animation.py +++ b/napari_animation/animation.py @@ -142,7 +142,7 @@ def animate( filename, fps=20, quality=5, - format=None, + file_format=None, canvas_only=True, scale_factor=None, ): @@ -158,7 +158,7 @@ def animate( quality: float number from 1 (lowest quality) to 9 only applies to non-gif extensions - format: str + file_format: str The format to use to write the file. By default imageio selects the appropriate for you based on the filename. canvas_only : bool @@ -202,14 +202,14 @@ def animate( filename, fps=fps, quality=quality, - format=format, + format=file_format, output_params=output_params, ) else: writer = imageio.get_writer( filename, duration=duration, - format=format, + format=file_format, ) except ValueError as err: print(err) diff --git a/napari_animation/frame_sequence.py b/napari_animation/frame_sequence.py index 7f74bdf..ba8f5fa 100644 --- a/napari_animation/frame_sequence.py +++ b/napari_animation/frame_sequence.py @@ -125,10 +125,10 @@ def __getitem__(self, key: int) -> ViewerState: if key not in self._cache: try: kf0, kf1, frac = self._keyframe_index[key] - except KeyError: + except KeyError as err: raise IndexError( f"Frame index ({key}) out of range ({len(self)} frames)" - ) + ) from err if frac == 0: self._cache[key] = kf0.viewer_state else: @@ -148,7 +148,7 @@ def iter_frames( scale_factor: float = None, ) -> Iterator[np.ndarray]: """Iterate over interpolated viewer states, and yield rendered frames.""" - for i, state in enumerate(self): + for _i, state in enumerate(self): frame = state.render(viewer, canvas_only=canvas_only) if scale_factor not in (None, 1): from scipy import ndimage as ndi diff --git a/napari_animation/interpolation/__init__.py b/napari_animation/interpolation/__init__.py index ad0f261..5dc9163 100644 --- a/napari_animation/interpolation/__init__.py +++ b/napari_animation/interpolation/__init__.py @@ -1,3 +1,5 @@ from .interpolation_constants import Interpolation from .typing import InterpolationMap from .viewer_state_interpolation import interpolate_viewer_state + +__all__ = ["Interpolation", "InterpolationMap", "interpolate_viewer_state"] diff --git a/napari_animation/interpolation/utils.py b/napari_animation/interpolation/utils.py index a8c1d48..5f1a27f 100644 --- a/napari_animation/interpolation/utils.py +++ b/napari_animation/interpolation/utils.py @@ -67,8 +67,8 @@ def keys_to_list(input_dict): def nested_assert_close(a, b): """Assert close on nested dicts.""" - a_keys = [key for key in keys_to_list(a)] - b_keys = [key for key in keys_to_list(b)] + a_keys = list(keys_to_list(a)) + b_keys = list(keys_to_list(b)) assert a_keys == b_keys diff --git a/napari_animation/interpolation/viewer_state_interpolation.py b/napari_animation/interpolation/viewer_state_interpolation.py index c8c34dd..40827ab 100644 --- a/napari_animation/interpolation/viewer_state_interpolation.py +++ b/napari_animation/interpolation/viewer_state_interpolation.py @@ -43,7 +43,7 @@ def interpolate_viewer_state( v0 = nested_get(initial_state, keys) v1 = nested_get(final_state, keys) - all_keys_are_strings = all([isinstance(key, str) for key in keys]) + all_keys_are_strings = all(isinstance(key, str) for key in keys) if interpolation_map is not None and all_keys_are_strings: attribute_name = ".".join(keys) interpolation_function = interpolation_map.get( diff --git a/napari_animation/utils.py b/napari_animation/utils.py index 7c2a38c..ccb38f2 100644 --- a/napari_animation/utils.py +++ b/napari_animation/utils.py @@ -43,6 +43,6 @@ def layer_attribute_changed(value, original_value): return True return any( layer_attribute_changed(value[key], original_value[key]) - for key in value.keys() + for key in value ) return not np.array_equal(value, original_value) diff --git a/napari_animation/viewer_state.py b/napari_animation/viewer_state.py index f3deb4c..24b6a84 100644 --- a/napari_animation/viewer_state.py +++ b/napari_animation/viewer_state.py @@ -1,3 +1,4 @@ +import contextlib from dataclasses import dataclass import napari @@ -59,10 +60,8 @@ def apply(self, viewer: napari.viewer.Viewer): # Only setattr if value has changed to avoid expensive redraws # dicts can hold arrays, e.g. `color`, requiring comparisons of key/value pairs if layer_attribute_changed(value, original_value): - try: + with contextlib.suppress(AttributeError): setattr(layer, attribute_name, value) - except AttributeError: - pass def render( self, viewer: napari.viewer.Viewer, canvas_only: bool = True