Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
1c664ab
update to 3.13
willingc Jul 14, 2025
a29a814
Fix enum by wrapping functool paritial
willingc Jul 14, 2025
7cd8290
Update black version
willingc Jul 14, 2025
a0ad469
remove commented out code
willingc Jul 14, 2025
26b9853
remove commented out code
willingc Jul 14, 2025
462ad0f
update python version in github action workflow
willingc Jul 14, 2025
608837d
fix black errors
willingc Jul 14, 2025
c6ad36c
set quote for black
willingc Jul 14, 2025
c453af1
fix ruff errors
willingc Jul 14, 2025
20b5b99
update action for imageio
willingc Jul 14, 2025
2d8d75a
Update the new test to mock properly the attribute for 3.10
willingc Jul 14, 2025
6784bec
reenable ffmpeg stuff in CI and update mock
willingc Jul 14, 2025
bec8ad5
Update wrap enum method and test
willingc Jul 14, 2025
445ad3a
skip simulation tests
willingc Jul 15, 2025
2fe8a9f
skip simulation tests correctly this time
willingc Jul 15, 2025
8b0762d
Add A004 to ruff lint ignore since we want math.pow
willingc Jul 15, 2025
b899d2a
Add comment about ruff target version
willingc Jul 15, 2025
d87d4a9
Make qt version more explicit
willingc Jul 15, 2025
55b49d4
make qt in tox more explicit
willingc Jul 15, 2025
c44fae7
Update napari_animation/_enum_compat.py
willingc Jul 21, 2025
0c7deb9
update typing
willingc Jul 21, 2025
f819f70
apply review changes
willingc Jul 21, 2025
f6516ca
capitalization is hard
willingc Jul 21, 2025
4788fe1
troubleshooting
willingc Jul 21, 2025
7b5f065
make test matrix similar to original file
willingc Jul 21, 2025
663e84d
additional tox changes back to initial state
willingc Jul 22, 2025
6ad9f0e
change caps for pyside
willingc Jul 22, 2025
b6c848c
pin pytest-qt to 4.4 for pyside
willingc Jul 22, 2025
90499db
remove mac-13 and macosintel
willingc Jul 22, 2025
43f348b
remove macos python3.10 from PySide2 CI testing
willingc Jul 22, 2025
698a339
drop pyside2 on 3.10 temporarily
willingc Jul 22, 2025
af6b3b7
stop failing fast
willingc Jul 22, 2025
b286862
remove env
willingc Jul 22, 2025
515b86d
Add in peter solution for pyside
willingc Jul 22, 2025
de5ee3d
add qt version to test name
willingc Jul 22, 2025
f95f759
Test removing the nested guard for enum member availability
willingc Jul 22, 2025
b9ae48e
Improve naming of guards for enum member
willingc Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,22 @@ jobs:
- name: Run task
run: tox -e ${{ matrix.task }}
test:
name: ${{ matrix.platform }} py${{ matrix.python-version }}
name: ${{ matrix.platform }} py${{ matrix.python-version }} ${{ matrix.qt_package }}
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: true
fail-fast: false
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ "3.9", "3.10", "3.11", "3.12" ]
python-version: [ "3.10", "3.11", "3.12", "3.13" ]
qt_package: [ pyqt ]
include:
# Add PySide2 only for Python 3.10 on Linux and Windows
- platform: ubuntu-latest
python-version: "3.10"
qt_package: pyside
- platform: windows-latest
python-version: "3.10"
qt_package: pyside

steps:
- uses: actions/checkout@v4
Expand All @@ -65,16 +74,6 @@ jobs:
powershell gl-ci-helpers/appveyor/install_opengl.ps1
if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1}

# Temporary fix for 'pip install imageio-ffmpeg'
# not including the FFMPEG binary on Apple Silicon macs
# This step can be removed when issue is fixed in imageio-ffmpeg
# https://github.com/imageio/imageio-ffmpeg/issues/71
- name: Setup FFmpeg
if: ${{ runner.os == 'macOS' && runner.arch == 'ARM64' }}
run: |
brew update
brew install ffmpeg

- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -87,6 +86,8 @@ jobs:
run: tox
env:
PLATFORM: ${{ matrix.platform }}
TOX_TESTENV_PASSENV: "QT_PACKAGE"
QT_PACKAGE: ${{ matrix.qt_package }}

- name: Coverage
uses: codecov/codecov-action@v4
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.12.1
rev: 25.1.0
hooks:
- id: black
pass_filenames: true
exclude: _vendor|vendored|examples
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.9
rev: v0.12.3
hooks:
- id: ruff
args: [ --fix ]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ If you encounter any problems, please [file an issue](https://github.com/napari/
[@napari]: https://github.com/napari
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin
[file an issue]: https://github.com/sofroniewn/napari-animation/issues
[file an issue]: https://github.com/napari/napari-animation/issues
[napari]: https://github.com/napari/napari
[tox]: https://tox.readthedocs.io/en/latest/
[pip]: https://pypi.org/project/pip/
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ def napari_scraper(block, block_vars, gallery_conf):
for win, img_path in zip(
reversed(napari._qt.qt_main_window._QtMainWindow._instances),
imgpath_iter,
strict=False,
):
img_paths.append(img_path)
win._window.screenshot(img_path, canvas_only=False)
Expand Down
36 changes: 36 additions & 0 deletions napari_animation/_enum_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Compatibility layer for enum behavior across Python versions.

Python 3.13 introduced stricter enum handling requiring `enum.member()` wrapper for callable values. See https://docs.python.org/3/library/enum.html.
This compatibility layer detects Python version and conditionally wraps enum values.
- Python 3.10:
- Python 3.11 added `member`
- Python 3.13+: EnumDict is added, and member is required. See
https://docs.python.org/3/whatsnew/3.13.html#enum

Enum members have a name and a value.
"""

import sys

# Check if member is importable
try:
from enum import member

# member is importable (Python 3.11+)
_ENUM_MEMBER_AVAILABLE = True
except ImportError:
# Python 3.10 has no member support
_ENUM_MEMBER_AVAILABLE = False

# Check if enum.member is required (Python 3.13+)
_ENUM_MEMBER_REQUIRED = sys.version_info >= (3, 13)


def wrap_enum_member(value):
"""Conditionally wrap a value with enum.member if needed."""
# Enum member is required for Python 3.13+, and available for 3.11+
if _ENUM_MEMBER_REQUIRED or _ENUM_MEMBER_AVAILABLE:
return member(value)
# For python 3.10 and older, just return the value directly
if not _ENUM_MEMBER_AVAILABLE:
return value
1 change: 1 addition & 0 deletions napari_animation/_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
discovery at test time.
https://docs.pytest.org/en/stable/fixture.html
"""

import numpy as np
import pytest

Expand Down
109 changes: 109 additions & 0 deletions napari_animation/_tests/test_enum_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Tests for enum compatibility across Python versions."""

import sys
from functools import partial

from napari_animation._enum_compat import (
_ENUM_MEMBER_AVAILABLE,
_ENUM_MEMBER_REQUIRED,
wrap_enum_member,
)
from napari_animation.easing import Easing
from napari_animation.interpolation.interpolation_constants import (
Interpolation,
)


def test_enum_member_detection():
"""Test that enum.member availability is correctly detected."""
if sys.version_info >= (3, 11):
assert _ENUM_MEMBER_AVAILABLE is True
else:
assert _ENUM_MEMBER_AVAILABLE is False

if sys.version_info >= (3, 13):
assert _ENUM_MEMBER_REQUIRED is True
else:
assert _ENUM_MEMBER_REQUIRED is False


def test_wrap_enum_member_basic():
"""Test basic wrap_enum_member functionality."""

def test_func(x):
return x * 2

partial_func = partial(test_func)
wrapped = wrap_enum_member(partial_func)

# The wrapped function should behave the same when called through enum
from enum import Enum

class TestEnum(Enum):
TEST = wrapped

def __call__(self, *args):
return self.value(*args)

result = TestEnum.TEST(5)
assert result == 10


def test_easing_enum_functionality():
"""Test that the Easing enum works correctly with the compatibility fix."""
# Test that all easing functions are callable
for easing in Easing:
result = easing(0.5)
assert isinstance(result, int | float)
assert (
0 <= result <= 1.5
) # Some easing functions can go slightly above 1

# Test specific easing functions
assert Easing.LINEAR(0.5) == 0.5
assert Easing.LINEAR(0.0) == 0.0
assert Easing.LINEAR(1.0) == 1.0


def test_interpolation_enum_functionality():
"""Test that the Interpolation enum works correctly with the compatibility fix."""
# Test DEFAULT interpolation
result = Interpolation.DEFAULT(0.0, 1.0, 0.5)
assert result == 0.5

# Test BOOL interpolation
result = Interpolation.BOOL(False, True, 0.5)
assert result is True

result = Interpolation.BOOL(False, True, 0.0)
assert result is False


def test_enum_member_access():
"""Test that enum members can be accessed properly."""
# Test that we can access the underlying partial function
linear_func = Easing.LINEAR.value
assert callable(linear_func)
assert linear_func(0.5) == 0.5

# Test that enum comparison works
assert Easing.LINEAR == Easing.LINEAR
assert Easing.LINEAR != Easing.QUADRATIC

# Test that enum iteration works
easing_names = [e.name for e in Easing]
assert "LINEAR" in easing_names
assert "QUADRATIC" in easing_names
assert len(easing_names) == 10


def test_enum_in_collection():
"""Test that enums work correctly in collections."""
# Test that we can use enums in lists
easing_list = [Easing.LINEAR, Easing.QUADRATIC]
assert len(easing_list) == 2
assert Easing.LINEAR in easing_list

# Test that we can use enums in dictionaries
easing_dict = {Easing.LINEAR: "linear", Easing.QUADRATIC: "quadratic"}
assert easing_dict[Easing.LINEAR] == "linear"
4 changes: 2 additions & 2 deletions napari_animation/_tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
input_dict = [{"a": 1, "b": {"c": "d"}}]
keys = [["b", "c"]]
expected = ["d"]
test_set = list(zip(input_dict, keys, expected))
test_set = list(zip(input_dict, keys, expected, strict=False))


@pytest.mark.parametrize("input_dict,keys,expected", test_set)
Expand All @@ -16,7 +16,7 @@ def test_nested_get(input_dict, keys, expected):

input_dict = [{"a": 1, "b": {"c": "d"}, "e": {}}]
expected = [[["a"], ["b", "c"], ["e"]]]
test_set = list(zip(input_dict, expected))
test_set = list(zip(input_dict, expected, strict=False))


@pytest.mark.parametrize("input_dict,expected", test_set)
Expand Down
23 changes: 13 additions & 10 deletions napari_animation/easing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
Copyright (c) 2011, Auerhaus Development, LLC
http://sam.zoy.org/wtfpl/COPYING for more details.
"""

from enum import Enum
from functools import partial
from math import cos, pi, pow, sin, sqrt

from ._enum_compat import wrap_enum_member

tau = pi * 2


Expand Down Expand Up @@ -266,16 +269,16 @@ class Easing(Enum):
* bounce: bounce easing in and out.
"""

LINEAR = partial(linear_interpolation)
QUADRATIC = partial(quadratic_ease_in_out)
CUBIC = partial(cubic_ease_in_out)
QUINTIC = partial(quintic_ease_in_out)
SINE = partial(sine_ease_in_out)
CIRCULAR = partial(circular_ease_in_out)
EXPONENTIAL = partial(exponential_ease_in_out)
ELASTIC = partial(elastic_ease_in_out)
BACK = partial(back_ease_in_out)
BOUNCE = partial(bounce_ease_in_out)
LINEAR = wrap_enum_member(partial(linear_interpolation))
QUADRATIC = wrap_enum_member(partial(quadratic_ease_in_out))
CUBIC = wrap_enum_member(partial(cubic_ease_in_out))
QUINTIC = wrap_enum_member(partial(quintic_ease_in_out))
SINE = wrap_enum_member(partial(sine_ease_in_out))
CIRCULAR = wrap_enum_member(partial(circular_ease_in_out))
EXPONENTIAL = wrap_enum_member(partial(exponential_ease_in_out))
ELASTIC = wrap_enum_member(partial(elastic_ease_in_out))
BACK = wrap_enum_member(partial(back_ease_in_out))
BOUNCE = wrap_enum_member(partial(bounce_ease_in_out))

def __call__(self, *args):
return self.value(*args)
7 changes: 5 additions & 2 deletions napari_animation/interpolation/base_interpolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def default_interpolation(a: _T, b: _T, fraction: float) -> _T:
elif isinstance(a, Number) and isinstance(b, Number):
return interpolate_num(a, b, fraction)

elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
elif isinstance(a, list | tuple) and isinstance(b, list | tuple):
return interpolate_sequence(a, b, fraction)

else:
Expand All @@ -59,7 +59,10 @@ def interpolate_sequence(
Interpolated sequence between a and b at fraction.
"""
seq_cls = type(a)
gen = (default_interpolation(v0, v1, fraction) for v0, v1 in zip(a, b))
gen = (
default_interpolation(v0, v1, fraction)
for v0, v1 in zip(a, b, strict=False)
)
try:
seq = seq_cls(gen)
except TypeError:
Expand Down
9 changes: 5 additions & 4 deletions napari_animation/interpolation/interpolation_constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from functools import partial

from .._enum_compat import wrap_enum_member
from .base_interpolation import default_interpolation as _default_interpolation
from .base_interpolation import interpolate_bool as _interpolate_bool
from .base_interpolation import interpolate_log as _interpolate_log
Expand All @@ -18,10 +19,10 @@ class Interpolation(Enum):

"""

DEFAULT = partial(_default_interpolation)
LOG = partial(_interpolate_log)
SLERP = partial(_slerp)
BOOL = partial(_interpolate_bool)
DEFAULT = wrap_enum_member(partial(_default_interpolation))
LOG = wrap_enum_member(partial(_interpolate_log))
SLERP = wrap_enum_member(partial(_slerp))
BOOL = wrap_enum_member(partial(_interpolate_bool))

def __call__(self, *args):
return self.value(*args)
4 changes: 2 additions & 2 deletions napari_animation/interpolation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ def nested_assert_close(a, b):

def nested_seq_assert_close(a, b):
"""Assert close to scalar or potentially nested qequences of numeric types and others."""
if isinstance(a, (list, tuple)) or isinstance(b, (list, tuple)):
for a_v, b_v in zip(a, b):
if isinstance(a, list | tuple) or isinstance(b, list | tuple):
for a_v, b_v in zip(a, b, strict=False):
nested_seq_assert_close(a_v, b_v)
else:
if isinstance(a, Number):
Expand Down
3 changes: 1 addition & 2 deletions napari_animation/interpolation/viewer_state_interpolation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import asdict
from typing import Optional

from ..viewer_state import ViewerState
from .interpolation_constants import Interpolation
Expand All @@ -11,7 +10,7 @@ def interpolate_viewer_state(
initial_state: ViewerState,
final_state: ViewerState,
fraction: float,
interpolation_map: Optional[InterpolationMap] = None,
interpolation_map: InterpolationMap | None = None,
) -> ViewerState:
"""Interpolate a state between two states

Expand Down
2 changes: 1 addition & 1 deletion napari_animation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)
return zip(a, b, strict=False)


def layer_attribute_changed(value, original_value):
Expand Down
Loading
Loading