Skip to content

refactor: effects to frame-generator pattern, add new effects, rename examples#107

Merged
Djelibeybi merged 6 commits intomainfrom
refactor/effects
Feb 7, 2026
Merged

refactor: effects to frame-generator pattern, add new effects, rename examples#107
Djelibeybi merged 6 commits intomainfrom
refactor/effects

Conversation

@Djelibeybi
Copy link
Owner

@Djelibeybi Djelibeybi commented Feb 7, 2026

Summary

  • Migrated effects to frame-generator pattern built on the animation module for per-pixel control on multizone and matrix devices
  • Added 5 new effects: flame, aurora, progress bar, sunrise, and sunset with full test coverage
  • Added effect registry for dynamic effect lookup by name
  • Renamed all 21 example scripts from numbered prefixes (01_, 02_, ...) to functional group prefixes (discovery_, control_, effects_, matrix_, animation_) for better discoverability
  • Added examples/README.md documenting every script with descriptions and parameter tables
  • Updated all cross-references in docstrings and docs to match new filenames

Test plan

  • Run uv run pytest to verify all existing and new tests pass
  • Run uv run pyright to verify type checking passes
  • Verify example scripts run correctly against real devices
  • Verify docs build cleanly with uv run mkdocs build

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added frame-based effects (Rainbow, Flame, Aurora, Progress, Sunrise, Sunset), a FrameEffect base and centralized Effect Registry; Conductor can add/remove lights at runtime and query the last emitted frame; ColorLoop now uses a synchronized option.
  • Documentation

    • Major docs expanded (getting-started, custom effects, animation, troubleshooting), many new/renamed examples and an examples README with usage guidance.
  • Tests

    • Comprehensive new and updated test suites covering frame effects, animators, packets, registry and conductor behaviours.

Djelibeybi and others added 2 commits February 7, 2026 13:53
Introduce FrameEffect base class and refactor EffectColorloop to use
the animation module for direct UDP frame delivery instead of imperative
set_color() calls. Add EffectRainbow for per-pixel rainbow effects on
multizone strips and matrix lights.

Key changes:
- Add LightPacketGenerator for single-light animation support
- Add FrameBuffer.for_light() and Animator.for_light() factories
- Add duration_ms parameter to all packet generators and animator factories
- New FrameEffect base class with generate_frame() pattern and FrameContext
- Refactor EffectColorloop from LIFXEffect to FrameEffect
- New EffectRainbow effect spreading 360-degree rainbow across pixels
- Conductor creates/manages Animators for FrameEffect lifecycle
- Fix participants timing bug in Conductor.start()
- Use 1.5x frame interval for duration_ms to prevent flicker
- Split example into separate colorloop and rainbow scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
Add flame, aurora, progress bar, and sunrise/sunset effects with full
test coverage. Add an effect registry for dynamic effect lookup. Rename
all example scripts from numbered prefixes to functional group prefixes
(discovery_, control_, effects_, matrix_, animation_) and add an
examples/README.md documenting every script and its parameters. Update
all cross-references in docstrings and docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
@github-actions github-actions bot added documentation Improvements or additions to documentation tests examples size/xl labels Feb 7, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Introduces a frame-based effects architecture: FrameEffect/FrameContext base, multiple new frame effects, EffectRegistry and state utilities, Conductor methods for dynamic light management and per-device last-frame access, single-light animator/packet support, docs/examples updates and extensive tests.

Changes

Cohort / File(s) Summary
Core Frame API & Effects
src/lifx/effects/frame_effect.py, src/lifx/effects/rainbow.py, src/lifx/effects/flame.py, src/lifx/effects/aurora.py, src/lifx/effects/progress.py, src/lifx/effects/sunrise.py, src/lifx/effects/colorloop.py
Add FrameContext and FrameEffect base class; implement FrameEffect subclasses (Rainbow, Flame, Aurora, Progress, Sunrise/Sunset); refactor EffectColorloop to frame-based lifecycle (async_setup/generate_frame) and remove prior loop/stop logic.
Conductor & Effect Base
src/lifx/effects/conductor.py, src/lifx/effects/base.py
Add Conductor.add_lights, Conductor.remove_lights, Conductor.get_last_frame; integrate per-device animator lifecycle and last-frame caching; add restore_on_complete property to LIFXEffect.
Animation / Packet / Framebuffer
src/lifx/animation/animator.py, src/lifx/animation/packets.py, src/lifx/animation/framebuffer.py, src/lifx/animation/__init__.py
Introduce LightPacketGenerator and Animator.for_light; add FrameBuffer.for_light; propagate duration_ms into packet templates; make single-light SetColor packet generator and wire exports.
Registry & State Utilities
src/lifx/effects/registry.py, src/lifx/effects/state_manager.py, src/lifx/effects/const.py
Add EffectRegistry (DeviceType, DeviceSupport, EffectInfo, default registry builder), DeviceStateManager and effect constants; provide device-aware effect lookup and default registrations.
Public Exports
src/lifx/__init__.py, src/lifx/effects/__init__.py
Expand top-level and effects package all to export FrameEffect, FrameContext, new effect classes, registry types and helpers, and related symbols.
Docs: API / Guides / Architecture
docs/api/effects.md, docs/architecture/effects-architecture.md, docs/getting-started/effects.md, docs/user-guide/effects-custom.md, docs/user-guide/effects-troubleshooting.md, docs/user-guide/ceiling-lights.md, docs/user-guide/animation.md
Add FrameEffect and new effect docs; update ColorLoop semantics (transition -> synchronized), describe per-device frame delivery and animators, add registry and dynamic light management docs, examples and notes; one duplicated ceiling-lights block present.
Examples: Added / Removed / Updated
examples/*
Remove legacy 06_pulse_effect.py and 07_colorloop_effect.py; add many new examples for rainbow, flame, aurora, progress, sunrise/sunset, refactor pulse examples; update CLI script names and add examples/README.md.
Tests: Effects, Animator & Packets
tests/test_effects/*, tests/test_animation/*, tests/test_animation/test_packets.py
Large new and updated test suites for FrameEffect base, each new effect, registry, conductor add/remove lifecycles, animator/packet duration handling, FrameBuffer/Animator factories and LightPacketGenerator tests.
Minor / Misc
examples/animation_basic.py, examples/animation_numpy.py, examples/* discovery/control
Docstring/usage updates to script names and serial formats across multiple example scripts.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Conductor as Conductor
    participant FrameEffect as FrameEffect
    participant Animator as Animator
    participant UDP as UDP Network

    User->>Conductor: start(effect, lights)
    activate Conductor
    Conductor->>FrameEffect: async_setup(lights)
    FrameEffect-->>Conductor: setup complete
    Conductor->>Conductor: create_animators(effect, lights)
    Conductor->>Animator: for_light/for_multizone/for_matrix
    Animator-->>Conductor: Animator instance(s)
    Conductor->>FrameEffect: async_play()
    activate FrameEffect
    loop Frame loop
        FrameEffect->>FrameEffect: build FrameContext
        FrameEffect->>FrameEffect: generate_frame(ctx)
        FrameEffect->>Animator: send_frame(protocol_frame)
        Animator->>UDP: UDP packet
    end
    FrameEffect-->>Conductor: finished/stop
    Conductor->>Animator: close()
    deactivate Conductor
Loading
sequenceDiagram
    participant User as User
    participant Conductor as Conductor
    participant StateMgr as StateMgr
    participant Animator as Animator

    User->>Conductor: add_lights(effect, new_lights)
    activate Conductor
    Conductor->>StateMgr: capture_prestate(new_lights)
    StateMgr-->>Conductor: PreState
    Conductor->>Animator: for_light/for_multizone/for_matrix
    Animator-->>Conductor: Animator instance(s)
    Conductor->>Conductor: append participants / update animators
    Conductor-->>User: return
    deactivate Conductor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

devices

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: refactoring effects to a frame-generator pattern, adding new effects, and renaming examples.
Docstring Coverage ✅ Passed Docstring coverage is 95.52% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/effects

Comment @coderabbitai help to get the list of available commands and usage tips.

@Djelibeybi Djelibeybi changed the title Refactor effects to frame-generator pattern, add new effects, rename examples refactor: effects to frame-generator pattern, add new effects, rename examples Feb 7, 2026
@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

❌ Patch coverage is 98.63574% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.00%. Comparing base (472608a) to head (be6f678).
⚠️ Report is 7 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/lifx/effects/conductor.py 92.37% 0 Missing and 9 partials ⚠️
src/lifx/effects/frame_effect.py 98.57% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #107      +/-   ##
==========================================
+ Coverage   92.26%   93.00%   +0.73%     
==========================================
  Files          50       57       +7     
  Lines        5692     6344     +652     
  Branches      702      799      +97     
==========================================
+ Hits         5252     5900     +648     
+ Misses        281      280       -1     
- Partials      159      164       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/lifx/animation/animator.py (1)

135-291: ⚠️ Potential issue | 🟡 Minor

Add explicit validation for duration_ms to raise a clear ValueError instead of a low-level struct error.

Negative or invalid duration_ms values currently pass through to struct.pack_into() during Animator initialization (when create_templates() is called), raising an unhelpful struct.error: argument out of range. Add validation in the factory methods or packet generator constructors so invalid values are rejected early with a semantic error message.

All three factory methods—for_matrix(), for_multizone(), and for_light()—need this check.

docs/api/effects.md (1)

576-702: ⚠️ Potential issue | 🟡 Minor

FPS formula doesn’t match the implementation.
Line 588 and Line 700 say max(1.0, …), but the code clamps to a minimum of 20.0. This can mislead users about network load and smoothness.

Proposed doc correction
- The FPS is automatically calculated from `period` and `change`: `fps = max(1.0, (360 / change) / period)`
+ The FPS is automatically calculated from `period` and `change`: `fps = max(20.0, (360 / change) / period)`
@@
-- Frame interval: `1 / fps` where `fps = max(1.0, (360 / change) / period)`
+- Frame interval: `1 / fps` where `fps = max(20.0, (360 / change) / period)`
src/lifx/effects/colorloop.py (1)

76-91: ⚠️ Potential issue | 🟡 Minor

Transition docstring is now misleading.
The parameter is deprecated/ignored in the frame‑based flow, but the docstring still describes runtime behaviour. Suggest aligning it with the public docs.

Docstring tweak
-            transition: Color transition time in seconds, or None for
-                        random per device (default None). When synchronized=True
-                        and transition=None, uses iteration_period as transition.
+            transition: Deprecated. Accepted for backward compatibility but ignored.
🤖 Fix all issues with AI agents
In `@docs/api/effects.md`:
- Around line 1181-1184: The docs currently state the Conductor passes
duration_ms = 1000 / fps to the Animator but the implementation/tests use a 1.5×
interval; update the docs so the fps description and the Conductor->Animator
contract reference duration_ms = 1500 / fps (or "1.5× frame interval") instead
of 1000 / fps; ensure the symbols mentioned are fps, duration_ms, Conductor, and
Animator so readers can reconcile the doc with the code/tests.

In `@docs/user-guide/effects-troubleshooting.md`:
- Around line 336-344: The two adjacent bullets repeat “zone colors/colours”;
update the list to remove the duplicate phrasing by rewording the bullets
mentioning FrameEffect and zone restoration—e.g., keep the point that
FrameEffect subclasses can generate per-zone output (reference: FrameEffect,
generate_frame, pixel_count) and combine or rephrase the restoration point to
something like “zones are restored after effects” (reference: zone restoration
behavior) so you no longer repeat “zone colours”; also ensure EffectPulse and
EffectColorloop lines (reference: EffectPulse, EffectColorloop,
MultiZonePacketGenerator) remain unchanged in meaning.

In `@examples/effects_progress.py`:
- Around line 29-57: The resolve_devices function currently logs and skips
unresolved targets; change it to raise LifxDeviceNotFoundError instead so
callers get immediate failure: in resolve_devices, after calling find_by_ip or
find_by_serial, if device is None raise LifxDeviceNotFoundError(target) (include
a helpful message or the target value) and likewise if the resolved object is
not an instance of Light raise LifxDeviceNotFoundError(target, "resolved object
is not a Light"); ensure you import LifxDeviceNotFoundError and update any
callers/tests that expect exceptions instead of silent skips.

In `@examples/effects_pulse.py`:
- Around line 23-57: The resolve_devices function currently logs and skips when
find_by_ip/find_by_serial returns None; change that behavior to raise
LifxDeviceNotFoundError with a clear message including the target (e.g.,
f"Device not found: {target}") instead of printing and continuing. Update the
code path in resolve_devices to raise LifxDeviceNotFoundError when device is
None, and ensure LifxDeviceNotFoundError is imported or referenced (and keep the
existing isinstance(device, Light) check for successful resolution).

In `@examples/effects_sunrise_sunset.py`:
- Around line 38-65: The resolve_devices function currently logs and skips
missing or invalid targets; change this to raise the project-specific
LifxDeviceNotFoundError so callers can fail fast. Import
LifxDeviceNotFoundError, and in resolve_devices replace the branches that
print-and-skip (when device is None or when device is not an instance of Light)
with raising LifxDeviceNotFoundError containing the target identifier and a
short context message (e.g., "device not found" or "unexpected device type") so
callers receive a clear exception instead of silent skipping.

In `@src/lifx/effects/registry.py`:
- Around line 52-70: The _classify_device function currently only checks
MatrixLight and MultiZoneLight and falls back to DeviceType.LIGHT; add lazy
imports for InfraredLight and HevLight and insert isinstance checks in the
priority order Matrix → Multizone → Infrared → HEV → Light so InfraredLight maps
to DeviceType.INFRARED and HevLight maps to DeviceType.HEV; update the imports
inside _classify_device and add the two conditional checks (isinstance(device,
InfraredLight) and isinstance(device, HevLight)) before returning
DeviceType.LIGHT.

In `@tests/test_effects/test_aurora.py`:
- Around line 14-74: Several top-level test functions in this file (e.g.,
test_aurora_default_parameters, test_aurora_custom_parameters,
test_aurora_invalid_speed, test_aurora_invalid_brightness,
test_aurora_invalid_palette_too_short, test_aurora_invalid_palette_hue,
test_aurora_invalid_spread and other untyped tests referenced around lines
270-323) are missing explicit return type annotations; add "-> None" to each of
these test function signatures so they conform to strict Pyright typing. Locate
each top-level test def (functions named test_*) and update the signature to
include the return type annotation (def test_...(...) -> None:) consistently
across the file.

In `@tests/test_effects/test_integration.py`:
- Around line 638-669: Add precise type hints to the new test helper classes:
import FrameContext from lifx.effects.frame_effect and Any from typing, then
annotate both classes' __init__ signatures as __init__(self, **kwargs: Any) ->
None and annotate generate_frame as generate_frame(self, ctx: FrameContext) ->
list[HSBK]. Update both ConcreteFrameEffectForIntegration and FailingFrameEffect
accordingly so generate_frame returns list[HSBK] and __init__ matches the
recommended typing.

In `@tests/test_effects/test_progress.py`:
- Around line 14-107: Add explicit return type annotations "-> None" to all
module-level test functions that currently lack them; update functions shown in
this diff—test_progress_default_parameters, test_progress_custom_parameters,
test_progress_invalid_start_end, test_progress_invalid_position,
test_progress_gradient_foreground, test_progress_gradient_too_few_stops,
test_progress_invalid_spot_brightness, test_progress_invalid_spot_width, and
test_progress_invalid_spot_speed—to have signatures like "def <name>() ->
None:", and likewise add "-> None" to the other module-level test functions
later in the file referenced in the review (the ones around the later sections)
so the file is Pyright strict-compliant.
🧹 Nitpick comments (5)
docs/user-guide/effects-custom.md (1)

161-171: Minor documentation inconsistency in async_setup() example.

The async_setup() method receives participants as a parameter but the example iterates over self.participants. While this may work if self.participants is set before async_setup() is called, it could confuse readers. Consider using the parameter directly for clarity:

     async def async_setup(self, participants: list[Light]) -> None:
         """Fetch initial colors from all devices."""
-        for light in self.participants:
+        for light in participants:
             color = await self.fetch_light_color(light)
             self._base_hues.append(color.hue)
src/lifx/effects/aurora.py (1)

192-205: Consider documenting the use of private method _ensure_capabilities.

The compatibility check accesses light._ensure_capabilities() which is a private method. While this is acceptable within the library, it would be helpful to add a brief comment noting this is internal API usage, especially since this pattern appears in documentation examples.

tests/test_animation/test_framebuffer.py (1)

383-412: Consider adding copy assertion for consistency.

The existing test_apply_passthrough test (line 48) verifies that apply() returns a copy with assert result is not data. For consistency, consider adding the same assertion to test_for_light_apply_passthrough:

Suggested addition
     def test_for_light_apply_passthrough(self) -> None:
         """Test for_light apply is passthrough for single pixel."""
         device = MagicMock()
         fb = FrameBuffer.for_light(device)
         data: list[tuple[int, int, int, int]] = [(65535, 32768, 16384, 3500)]
         result = fb.apply(data)
         assert result == data
+        assert result is not data  # Should be a copy
src/lifx/effects/frame_effect.py (1)

164-185: Consider guarding against wrong frame lengths.
A mis‑sized frame will currently flow into send_frame() without a clear error. A fast explicit check would make debugging easier.

Suggested defensive check
                 # Generate user-friendly HSBK frame
                 hsbk_frame = self.generate_frame(ctx)
+                if len(hsbk_frame) != ctx.pixel_count:
+                    raise ValueError(
+                        f"generate_frame returned {len(hsbk_frame)} pixels, "
+                        f"expected {ctx.pixel_count}"
+                    )
src/lifx/effects/sunrise.py (1)

375-380: Move asyncio import to module level.

The asyncio import inside async_play() should be moved to the top of the file with other imports. Importing inside a method is less efficient and unconventional.

♻️ Proposed fix

Add at the top of the file with other imports:

import asyncio
import math
from typing import TYPE_CHECKING, Literal

Then remove line 376:

     async def async_play(self) -> None:
         """Run the sunset frame loop, then optionally power off."""
         await super().async_play()

         if self.power_off and self.participants:
-            import asyncio
-
             await asyncio.gather(
                 *(light.set_power(False) for light in self.participants)
             )

Djelibeybi and others added 2 commits February 7, 2026 21:50
Cover all uncovered patch lines across four effect files:
- aurora: hue wrapping branches in _interpolate_hue
- progress: gradient hue wrapping, from_poweroff_hsbk
- sunrise: is_light_compatible with None capabilities (both sunrise and sunset)
- conductor: get_last_frame for non-FrameEffect, add_lights with no task, remove_lights for non-running light

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
- docs: fix duration_ms formula (1000 -> 1500/fps) in effects API docs
- docs: deduplicate "zone colors" phrasing in troubleshooting
- sunrise: move asyncio import to module level
- tests: add -> None return type annotations to all module-level test functions
- tests: add type hints to FrameEffect integration test helpers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/test_effects/test_aurora.py`:
- Around line 346-349: The nested async function ensure_caps is missing an
explicit return type annotation; update its signature to include "-> None"
(e.g., async def ensure_caps() -> None:) so it satisfies strict Pyright type
checks while keeping its body that creates a MagicMock and assigns it to
light.capabilities unchanged.

In `@tests/test_effects/test_conductor.py`:
- Around line 64-85: The fixtures conductor, light1, light2, and light3 lack
explicit return type annotations required by strict Pyright; update each fixture
signature to include the correct return type (e.g., Conductor for conductor and
whatever type _make_color_light returns for light1/2/3) so Pyright can validate
them—locate the fixture functions named conductor, light1, light2, light3 and
add explicit return type hints referencing Conductor and the color light type
returned by _make_color_light.
🧹 Nitpick comments (1)
tests/test_effects/test_progress.py (1)

519-524: Replace fixed sleep with polling to reduce flaky async tests.
A fixed 0.1s sleep can be brittle on slower CI; polling for send_frame is more reliable.

♻️ Suggested update
-        await asyncio.sleep(0.1)
+        for _ in range(20):
+            if animator.send_frame.call_count:
+                break
+            await asyncio.sleep(0.01)
+        else:
+            pytest.fail("send_frame was not called")

Add `-> None` return type annotations to module-level test functions,
fixtures, and nested helper functions across all effect test files to
satisfy strict Pyright validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_effects/test_integration.py (1)

91-108: ⚠️ Potential issue | 🟡 Minor

Add a return type for mock_white_light.

Line 91 defines a fixture without a return type, which conflicts with strict Pyright expectations.

Proposed update
 `@pytest.fixture`
-def mock_white_light():
+def mock_white_light() -> MagicMock:
     """Create a mock white light (no color support)."""

As per coding guidelines: "Use type hints with strict Pyright validation across all Python code".

🧹 Nitpick comments (3)
tests/test_effects/test_state_manager.py (1)

20-24: LGTM! Return type annotation added to fixture.

The -> MagicMock return type satisfies strict Pyright validation. However, for consistency, the state_manager fixture (line 14) and mock_multizone_light fixture (line 28) should also receive return type annotations.

♻️ Suggested additions for consistency
 `@pytest.fixture`
-def state_manager():
+def state_manager() -> DeviceStateManager:
     """Create a DeviceStateManager instance."""
     return DeviceStateManager()
 `@pytest.fixture`
-def mock_multizone_light():
+def mock_multizone_light() -> MagicMock:
     """Create a mock multizone light device."""
     light = MagicMock(spec=MultiZoneLight)
tests/test_effects/test_capability_filtering.py (1)

13-39: Consider adding return type annotations to fixtures.

The color_light and white_light fixtures are missing return type annotations. For full Pyright strict compliance, consider adding -> MagicMock to these fixtures, consistent with the pattern seen in other test files (e.g., tests/test_effects/test_state_manager.py).

Suggested fix
 `@pytest.fixture`
-def color_light():
+def color_light() -> MagicMock:
     """Create a mock color light."""
     ...

 `@pytest.fixture`
-def white_light():
+def white_light() -> MagicMock:
     """Create a mock white-only light (no color capability)."""

Also applies to: 42-68

tests/test_effects/test_base.py (1)

26-29: Consider adding return type annotation to effect fixture.

For consistency with the mock_light fixture, consider adding a return type annotation to the effect fixture.

Suggested fix
 `@pytest.fixture`
-def effect():
+def effect() -> ConcreteEffect:
     """Create a concrete effect instance for testing."""
     return ConcreteEffect(power_on=True)

Add return type annotations to remaining fixtures in test_integration,
test_state_manager, test_capability_filtering, and test_base.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Avi Miller <me@dje.li>
@Djelibeybi Djelibeybi merged commit 4c9bf57 into main Feb 7, 2026
49 of 50 checks passed
@Djelibeybi Djelibeybi deleted the refactor/effects branch February 7, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples size/xl tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant