Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Goto instance change key binding #2148

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from

Conversation

emdavis02
Copy link
Contributor

@emdavis02 emdavis02 commented Mar 26, 2025

Description

Adding a keybinding to jump to the next (or previous) frame where the number of displayed instances changes from the current frame

Types of changes

  • Bugfix
  • New feature
  • Refactor / Code style update (no logical changes)
  • Build / CI changes
  • Documentation Update
  • Other (explain)

Does this address any currently open issues?

[list open issues here]

Outside contributors checklist

  • Review the guidelines for contributing to this repository
  • Read and sign the CLA and add yourself to the authors list
  • Make sure you are making a pull request against the develop branch (not main). Also you should start your branch off develop
  • Add tests that prove your fix is effective or that your feature works
  • Add necessary documentation (if appropriate)

Thank you for contributing to SLEAP!

❤️

Summary by CodeRabbit

  • New Features
    • Introduced new keyboard shortcuts (Ctrl+Shift+Right and Ctrl+Shift+Left) for quick navigation to the next and previous instance changes.
    • Added corresponding menu items under the "Go" menu for both next and previous instance changes, enhancing navigation within the application.
    • Expanded the list of available shortcuts with the addition of "goto next instance change" and "goto prev instance change".
  • Tests
    • Added a new test to validate the functionality of navigating to the next instance change in labeled frames.
    • Added a new test to validate the functionality of navigating to the previous instance change in labeled frames.

Copy link

coderabbitai bot commented Mar 26, 2025

Walkthrough

The changes introduce two new keyboard shortcuts in the configuration file sleap/config/shortcuts.yaml for navigating instance changes within the application. The shortcuts, goto next instance change (Ctrl+Shift+Right) and goto prev instance change (Ctrl+Shift+Left), are complemented by new menu items in the MainWindow class of sleap/gui/app.py. The command logic for these actions is implemented in sleap/gui/commands.py, allowing users to navigate to frames with differing instance counts. Additionally, test cases for these functionalities are added in tests/gui/test_commands.py.

Changes

Files Change Summary
sleap/config/shortcuts.yaml, sleap/gui/shortcuts.py Added new keyboard shortcuts: "goto next instance change" (Ctrl+Shift+Right) and "goto prev instance change" (Ctrl+Shift+Left)
sleap/gui/app.py Introduced new menu items for "goto next instance change" and "goto prev instance change" in the MainWindow class
sleap/gui/commands.py Added methods and classes for navigating to the next and previous instance changes (nextInstanceChange, GoNextInstanceChange, GoPrevInstanceChange)
tests/gui/test_commands.py Added test functions test_go_next_instance_change and test_go_prev_instance_change to validate the new navigation commands

Sequence Diagram(s)

Loading
sequenceDiagram
    participant U as User
    participant UI as Interface (Menu/Shortcut)
    participant MW as MainWindow
    participant CC as CommandContext
    participant GN as GoNextInstanceChange
    participant GP as GoPrevInstanceChange
    participant V as Video/Data

    U->>UI: Triggers "goto next instance change"
    UI->>MW: Activates menu/shortcut command
    MW->>CC: Calls nextInstanceChange()
    CC->>GN: Executes GoNextInstanceChange.do_action(context, params)
    GN->>V: Retrieves current frame and instance count
    V-->>GN: Returns current data
    GN->>V: Searches for next frame with a different instance count
    V-->>GN: Returns target frame index
    GN->>CC: Issues navigation instruction
    CC-->>MW: Updates UI to target frame
    MW-->>U: Displays the new instance change

    U->>UI: Triggers "goto prev instance change"
    UI->>MW: Activates menu/shortcut command
    MW->>CC: Calls prevInstanceChange()
    CC->>GP: Executes GoPrevInstanceChange.do_action(context, params)
    GP->>V: Retrieves current frame and instance count
    V-->>GP: Returns current data
    GP->>V: Searches for previous frame with a different instance count
    V-->>GP: Returns target frame index
    GP->>CC: Issues navigation instruction
    CC-->>MW: Updates UI to target frame
    MW-->>U: Displays the new instance change

Suggested labels

2024-hackathon

Suggested reviewers

  • roomrys

Poem

I’m a rabbit in the codefield, so spry and keen,
Hopping through shortcuts like a digital dream,
With a tap and a skip, I jump to the next change,
Instance hops in the code, daring and strange!
Celebrate each key press, let the changes gleam –
A bunny’s joy in every clean code stream! 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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

🧹 Nitpick comments (1)
sleap/gui/commands.py (1)

1608-1629: Consider handling edge cases and optimizing performance

The implementation works well for the main use case, but could benefit from a few improvements:

  1. Converting the iterator to a list with list(later_frames_list) is unnecessary and could be memory-intensive for large datasets. Consider iterating directly through later_frames.

  2. There's no feedback when no frames with different instance counts are found. Consider adding a status message in this case.

  3. For better error handling, check if current_frame exists before accessing its instances.

class GoNextInstanceChange(NavCommand):
    @classmethod
    def do_action(cls, context: CommandContext, params: dict):
        video = context.state["video"]
        cur_idx = context.state["frame_idx"]

        # Get current number of instances
        current_frames = context.labels.find(video, cur_idx, return_new=True)
        if not current_frames:
+           context.app.updateStatusMessage("No current frame found.")
+           return
        current_frame = current_frames[0]
        current_instance_count = len(current_frame.instances)

        # Get all labeled frames after current frame
        later_frames = context.labels.frames(video, from_frame_idx=cur_idx + 1)
-       later_frames_list = list(later_frames)

        # Find first frame where instance count changes
        found_change = False
-       for frame in later_frames_list:
+       for frame in later_frames:
            instance_count = len(frame.instances)
            if instance_count != current_instance_count:
                cls.go_to(context, frame.frame_idx)
+               found_change = True
                break
                
+       if not found_change:
+           context.app.updateStatusMessage("No frames with different instance counts found.")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6531447 and 0ef5b6e.

📒 Files selected for processing (4)
  • sleap/config/shortcuts.yaml (1 hunks)
  • sleap/gui/app.py (1 hunks)
  • sleap/gui/commands.py (2 hunks)
  • sleap/gui/shortcuts.py (1 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
sleap/gui/commands.py (3)
sleap/io/dataset.py (5)
  • video (569-579)
  • labels (553-555)
  • find (933-961)
  • instances (1156-1172)
  • frames (963-979)
sleap/instance.py (5)
  • video (908-913)
  • find (1502-1521)
  • instances (1524-1526)
  • instances (1529-1550)
  • frame_idx (916-921)
sleap/gui/app.py (2)
  • labels (293-294)
  • labels (297-298)
sleap/gui/app.py (1)
sleap/gui/commands.py (1)
  • nextInstanceChange (635-637)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Tests (macos-14)
  • GitHub Check: Tests (windows-2022)
  • GitHub Check: Tests (ubuntu-22.04)
🔇 Additional comments (4)
sleap/config/shortcuts.yaml (1)

21-21: LGTM: New shortcut for navigating to the next instance change.

The shortcut Ctrl+Shift+Right follows the existing pattern in the file - similar to other navigation shortcuts like "goto next labeled" and appropriately uses the directional key (Right) to indicate forward movement.

sleap/gui/shortcuts.py (1)

44-44: LGTM: Added the shortcut name to the supported shortcuts list.

The new "goto next instance change" entry is correctly added to the _names tuple in the Shortcuts class, allowing it to be recognized and managed by the application's shortcut system.

sleap/gui/app.py (1)

578-583: LGTM: Added menu item for navigating to the next instance change.

The new menu item is correctly integrated into the "Go" menu alongside other navigation-related options. The implementation follows the established pattern for menu items, using the add_menu_item helper function with appropriate parameters.

sleap/gui/commands.py (1)

635-638: LGTM: Well-documented command method

The new method is clear and follows the established pattern for command methods in this class.

Copy link

codecov bot commented Mar 26, 2025

Codecov Report

Attention: Patch coverage is 92.85714% with 2 lines in your changes missing coverage. Please review.

Project coverage is 76.17%. Comparing base (7991f14) to head (4e9e869).
Report is 98 commits behind head on develop.

Files with missing lines Patch % Lines
sleap/gui/commands.py 92.30% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2148      +/-   ##
===========================================
+ Coverage    75.43%   76.17%   +0.73%     
===========================================
  Files          134      134              
  Lines        24749    24837      +88     
===========================================
+ Hits         18670    18920     +250     
+ Misses        6079     5917     -162     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

🧹 Nitpick comments (1)
tests/gui/test_commands.py (1)

1052-1079: Well-implemented test for the new navigation feature.

The test thoroughly verifies the functionality of GoNextInstanceChange by:

  1. Setting up frames with different instance counts
  2. Executing the command
  3. Verifying it correctly navigates to the frame with a different instance count

Consider adding additional tests for edge cases:

  • What happens when there are no frames with different instance counts?
  • Behavior when there are multiple frames with different counts
def test_go_next_instance_change(centered_pair_predictions: Labels):
    """Test that goto next instance change command works correctly."""
    # Set up test data
    labels = centered_pair_predictions
    video = labels.videos[0]
    
    # Create frames with different numbers of instances
    lf1 = labels[0]  # First frame (already exists)
    initial_count = len(lf1.instances)
    
    # Create a new frame with different number of instances
    lf2 = LabeledFrame(frame_idx=lf1.frame_idx + 10, video=video)
    new_instance = Instance(labels.skeleton)  # Create one new instance
    lf2.add_instance(new_instance)
    labels.add_frame(lf2)
    
    # Set up command context
    context = CommandContext.from_labels(labels)
    context.state["video"] = video
    context.state["frame_idx"] = lf1.frame_idx
    
    # Execute the command
    context.execute(GoNextInstanceChange)
    
    # Verify that we moved to the frame with different instance count
    assert context.state["frame_idx"] == lf2.frame_idx
    assert len(labels.find(video, lf2.frame_idx)[0].instances) != initial_count
+
+    # Additional test: Navigate back to original frame
+    context.state["frame_idx"] = lf2.frame_idx
+    # Create a third frame with same instance count as the first
+    lf3 = LabeledFrame(frame_idx=lf2.frame_idx + 10, video=video)
+    for _ in range(initial_count):
+        lf3.add_instance(Instance(labels.skeleton))
+    labels.add_frame(lf3)
+    
+    # Execute the command again
+    context.execute(GoNextInstanceChange)
+    
+    # Verify that we moved to the frame with different instance count
+    assert context.state["frame_idx"] == lf3.frame_idx
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ef5b6e and fb6cc65.

📒 Files selected for processing (1)
  • tests/gui/test_commands.py (2 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
tests/gui/test_commands.py (5)
sleap/gui/commands.py (3)
  • GoNextInstanceChange (1608-1627)
  • labels (208-210)
  • CommandContext (177-637)
tests/fixtures/datasets.py (1)
  • centered_pair_predictions (40-41)
sleap/io/dataset.py (5)
  • Labels (389-2710)
  • labels (553-555)
  • video (569-579)
  • instances (1156-1172)
  • find (933-961)
sleap/instance.py (7)
  • video (908-913)
  • instances (1524-1526)
  • instances (1529-1550)
  • LabeledFrame (1429-1941)
  • frame_idx (916-921)
  • Instance (344-991)
  • find (1502-1521)
sleap/io/videowriter.py (3)
  • add_frame (26-27)
  • add_frame (67-70)
  • add_frame (121-124)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Tests (macos-14)
  • GitHub Check: Tests (windows-2022)
  • GitHub Check: Tests (ubuntu-22.04)
🔇 Additional comments (1)
tests/gui/test_commands.py (1)

25-25: Good job adding the required import for the test.

The new import of GoNextInstanceChange is correctly added to the import list from sleap.gui.commands.

Copy link
Collaborator

@roomrys roomrys left a comment

Choose a reason for hiding this comment

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

Current functionality to jump works great. Some modifications requested. Please also add a short PR description.


# Get current number of instances
current_frame = context.labels.find(video, cur_idx, return_new=True)[0]
current_instance_count = len(current_frame.instances)
Copy link
Collaborator

Choose a reason for hiding this comment

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

LabeledFrame.instances returns both the user instances and underlying predictions (i.e. when a user double clicks a prediction to correct it, the prediction will still be saved in the instances list, but the user only sees instance_to_show:

sleap/sleap/instance.py

Lines 1677 to 1698 in 6531447

@property
def instances_to_show(self) -> List[Instance]:
"""Return a list of instances to show in GUI for this frame.
This list will not include any predicted instances for which
there's a corresponding regular instance.
Returns:
List of instances to show in GUI.
"""
unused_predictions = self.unused_predictions
inst_to_show = [
inst
for inst in self._instances
if type(inst) == Instance or inst in unused_predictions
]
inst_to_show.sort(
key=lambda inst: inst.track.spawned_on
if inst.track is not None
else math.inf
)
return inst_to_show


# Find first frame where instance count changes
for frame in later_frames_list:
instance_count = len(frame.instances)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here: instances are not necessarily the Instances the user is seeing in the GUI.

Comment on lines 1618 to 1619
# Get all labeled frames after current frame
later_frames = context.labels.frames(video, from_frame_idx=cur_idx + 1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

If I am at a frame with n>0 instances, I would expect it to jump to a frame where there are 0 instances as well since that is a number of instances change. However, Labels.labeled_frames only stores labeled frames with instances>0.

@@ -18,6 +18,7 @@ goto next user: Ctrl+U
goto next labeled: Alt+Right
goto prev suggestion: Shift+Space
goto prev labeled: Alt+Left
goto next instance change: Ctrl+Shift+Right
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would also be nice to go the other direction please and thank you!

emdavis02 and others added 4 commits March 28, 2025 14:29
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

🧹 Nitpick comments (4)
sleap/gui/commands.py (2)

1608-1631: Refactor repeated calls for clarity and efficiency.

Within the loop, the code repeatedly calls:

context.labels.find(video, frame, return_new=True)[0].instances_to_show

Storing the labeled frame in a variable will improve readability and avoid recalculating the same object repeatedly:

 for frame in range(cur_idx + 1, video.frames):
-    if len(context.labels.find(video, frame, return_new=True)[0].instances_to_show)
-       != current_instance_count:
+    next_frame = context.labels.find(video, frame, return_new=True)[0]
+    if len(next_frame.instances_to_show) != current_instance_count:
         cls.go_to(context, frame)
         break

1632-1654: Update the comment to reflect reverse iteration.

The comment says “Get all labeled frames after current frame,” but this code block is iterating backward. Consider adjusting or removing the comment for accuracy.

tests/gui/test_commands.py (2)

1057-1070: Add error handling for no instance count changes

The test assumes there will always be a frame with a different instance count, but doesn't handle the case where none is found.

Consider adding a fallback or assertion if no frame with a different instance count is found:

    # Create frames with different numbers of instances
    lf1 = context.labels.find(video, 0, return_new=True)[
        0
    ]  # First frame (already exists)
    initial_count = len(lf1.instances_to_show)

+   skipped_to_frame = None
    for frame_idx in range(1, video.num_frames):
        lf = context.labels.find(video, frame_idx, return_new=True)[0]
        if len(lf.instances_to_show) != initial_count:
            skipped_to_frame = lf.frame_idx
            break
+   
+   # Skip test if no frame with different instance count is found
+   if skipped_to_frame is None:
+       pytest.skip("No frame with different instance count found")
🧰 Tools
🪛 Ruff (0.8.2)

1060-1060: Undefined name context

(F821)


1066-1066: Undefined name context

(F821)


1084-1085: Fix docstring in test_go_prev_instance_change

The docstring incorrectly states "goto next instance change" instead of "goto prev instance change".

def test_go_prev_instance_change(centered_pair_predictions: Labels):
-   """Test that goto next instance change command works correctly."""
+   """Test that goto previous instance change command works correctly."""
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2815e and ddb009a.

📒 Files selected for processing (5)
  • sleap/config/shortcuts.yaml (1 hunks)
  • sleap/gui/app.py (1 hunks)
  • sleap/gui/commands.py (2 hunks)
  • sleap/gui/shortcuts.py (1 hunks)
  • tests/gui/test_commands.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • sleap/gui/shortcuts.py
  • sleap/config/shortcuts.yaml
🧰 Additional context used
🧬 Code Definitions (3)
sleap/gui/app.py (1)
sleap/gui/commands.py (1)
  • nextInstanceChange (635-637)
tests/gui/test_commands.py (2)
sleap/gui/commands.py (4)
  • GoNextInstanceChange (1608-1629)
  • GoPrevInstanceChange (1632-1653)
  • labels (208-210)
  • CommandContext (177-637)
tests/fixtures/datasets.py (1)
  • centered_pair_predictions (40-41)
sleap/gui/commands.py (2)
sleap/io/dataset.py (3)
  • video (569-579)
  • find (933-961)
  • frames (963-979)
sleap/instance.py (3)
  • video (908-913)
  • find (1502-1521)
  • instances_to_show (1678-1698)
🪛 Ruff (0.8.2)
tests/gui/test_commands.py

1060-1060: Undefined name context

(F821)


1066-1066: Undefined name context

(F821)


1091-1091: Undefined name context

(F821)


1097-1097: Undefined name context

(F821)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Tests (macos-14)
  • GitHub Check: Tests (windows-2022)
  • GitHub Check: Tests (ubuntu-22.04)
🔇 Additional comments (3)
sleap/gui/app.py (1)

578-590: Well-integrated navigation feature for instance count changes.

The new menu items seamlessly integrate with the existing navigation options in the "Go" menu. The addition enhances the user experience by providing a way to quickly navigate between frames where the instance count changes.

The implementation follows the established pattern of using add_menu_item with appropriate command references, and the menu item labels are clear and descriptive.

sleap/gui/commands.py (1)

635-638: Looks clean and straightforward.

This method neatly delegates the logic to the GoNextInstanceChange command.

tests/gui/test_commands.py (1)

25-26: LGTM: Command imports added properly

The imports for the new command classes are correctly added to support the new tests.

Comment on lines 1053 to 1082
def test_go_next_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]

# Create frames with different numbers of instances
lf1 = context.labels.find(video, 0, return_new=True)[
0
] # First frame (already exists)
initial_count = len(lf1.instances_to_show)

for frame_idx in range(1, video.num_frames):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break

# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = lf1.frame_idx

# Execute the command
context.execute(GoNextInstanceChange)

# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix undefined variable error

This test function references context before it's defined, which will cause a NameError when the test runs.

def test_go_next_instance_change(centered_pair_predictions: Labels):
    """Test that goto next instance change command works correctly."""
    # Set up test data
    labels = centered_pair_predictions
    video = labels.videos[0]

+   # Set up command context
+   context = CommandContext.from_labels(labels)
+   context.state["video"] = video
+   context.state["frame_idx"] = 0

    # Create frames with different numbers of instances
    lf1 = context.labels.find(video, 0, return_new=True)[
        0
    ]  # First frame (already exists)
    initial_count = len(lf1.instances_to_show)

    for frame_idx in range(1, video.num_frames):
        lf = context.labels.find(video, frame_idx, return_new=True)[0]
        if len(lf.instances_to_show) != initial_count:
            skipped_to_frame = lf.frame_idx
            break

-   # Set up command context
-   context = CommandContext.from_labels(labels)
-   context.state["video"] = video
-   context.state["frame_idx"] = lf1.frame_idx
+   # Update frame index to start position
+   context.state["frame_idx"] = lf1.frame_idx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_go_next_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]
# Create frames with different numbers of instances
lf1 = context.labels.find(video, 0, return_new=True)[
0
] # First frame (already exists)
initial_count = len(lf1.instances_to_show)
for frame_idx in range(1, video.num_frames):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break
# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = lf1.frame_idx
# Execute the command
context.execute(GoNextInstanceChange)
# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count
def test_go_next_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]
# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = 0
# Create frames with different numbers of instances
lf1 = context.labels.find(video, 0, return_new=True)[0] # First frame (already exists)
initial_count = len(lf1.instances_to_show)
for frame_idx in range(1, video.num_frames):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break
# Update frame index to start position
context.state["frame_idx"] = lf1.frame_idx
# Execute the command
context.execute(GoNextInstanceChange)
# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count
🧰 Tools
🪛 Ruff (0.8.2)

1060-1060: Undefined name context

(F821)


1066-1066: Undefined name context

(F821)

Comment on lines 1084 to 1112
def test_go_prev_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]

# Create frames with different numbers of instances
lf1 = context.labels.find(video, video.num_frames - 1, return_new=True)[
0
] # First frame (already exists)
initial_count = len(lf1.instances_to_show)

for frame_idx in range(video.num_frames - 1, 0, -1):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break

# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = lf1.frame_idx

# Execute the command
context.execute(GoPrevInstanceChange)

# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix undefined variable error

Similar to the previous test, this function references context before it's defined, which will cause a NameError when the test runs.

def test_go_prev_instance_change(centered_pair_predictions: Labels):
    """Test that goto next instance change command works correctly."""
    # Set up test data
    labels = centered_pair_predictions
    video = labels.videos[0]

+   # Set up command context
+   context = CommandContext.from_labels(labels)
+   context.state["video"] = video
+   context.state["frame_idx"] = video.num_frames - 1

    # Create frames with different numbers of instances
    lf1 = context.labels.find(video, video.num_frames - 1, return_new=True)[
        0
    ]  # First frame (already exists)
    initial_count = len(lf1.instances_to_show)

    for frame_idx in range(video.num_frames - 1, 0, -1):
        lf = context.labels.find(video, frame_idx, return_new=True)[0]
        if len(lf.instances_to_show) != initial_count:
            skipped_to_frame = lf.frame_idx
            break

-   # Set up command context
-   context = CommandContext.from_labels(labels)
-   context.state["video"] = video
-   context.state["frame_idx"] = lf1.frame_idx
+   # Update frame index to start position
+   context.state["frame_idx"] = lf1.frame_idx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_go_prev_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]
# Create frames with different numbers of instances
lf1 = context.labels.find(video, video.num_frames - 1, return_new=True)[
0
] # First frame (already exists)
initial_count = len(lf1.instances_to_show)
for frame_idx in range(video.num_frames - 1, 0, -1):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break
# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = lf1.frame_idx
# Execute the command
context.execute(GoPrevInstanceChange)
# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count
def test_go_prev_instance_change(centered_pair_predictions: Labels):
"""Test that goto next instance change command works correctly."""
# Set up test data
labels = centered_pair_predictions
video = labels.videos[0]
# Set up command context
context = CommandContext.from_labels(labels)
context.state["video"] = video
context.state["frame_idx"] = video.num_frames - 1
# Create frames with different numbers of instances
lf1 = context.labels.find(video, video.num_frames - 1, return_new=True)[0] # First frame (already exists)
initial_count = len(lf1.instances_to_show)
for frame_idx in range(video.num_frames - 1, 0, -1):
lf = context.labels.find(video, frame_idx, return_new=True)[0]
if len(lf.instances_to_show) != initial_count:
skipped_to_frame = lf.frame_idx
break
# Update frame index to start position
context.state["frame_idx"] = lf1.frame_idx
# Execute the command
context.execute(GoPrevInstanceChange)
# Verify that we moved to the frame with different instance count
assert context.state["frame_idx"] == skipped_to_frame
assert len(labels.find(video, skipped_to_frame)[0].instances) != initial_count
🧰 Tools
🪛 Ruff (0.8.2)

1091-1091: Undefined name context

(F821)


1097-1097: Undefined name context

(F821)

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

🧹 Nitpick comments (1)
sleap/gui/commands.py (1)

1636-1658: Consider refactoring to eliminate code duplication

The implementation for GoPrevInstanceChange is nearly identical to GoNextInstanceChange except for the frame iteration direction. Consider refactoring to reduce duplication by creating a common helper method with a direction parameter.

-class GoNextInstanceChange(NavCommand):
+class InstanceChangeNavigator:
+    @staticmethod
+    def find_instance_change(context: CommandContext, direction: int = 1):
+        video = context.state["video"]
+        cur_idx = context.state["frame_idx"]
+
+        # Get current number of instances
+        current_frame = context.labels.find(video, cur_idx, return_new=True)[0]
+        current_instance_count = len(current_frame.instances_to_show)
+
+        # Determine frame range and iteration
+        start = cur_idx + direction
+        end = video.frames if direction > 0 else -1
+        step = direction
+
+        # Find frame with different instance count
+        for frame in range(start, end, step):
+            if (
+                len(
+                    context.labels.find(video, frame, return_new=True)[
+                        0
+                    ].instances_to_show
+                )
+                != current_instance_count
+            ):
+                return frame
+        return None
+
+class GoNextInstanceChange(NavCommand):
     @classmethod
     def do_action(cls, context: CommandContext, params: dict):
-        video = context.state["video"]
-        cur_idx = context.state["frame_idx"]
-
-        # Get current number of instances
-        current_frame = context.labels.find(video, cur_idx, return_new=True)[0]
-        current_instance_count = len(current_frame.instances_to_show)
-
-        # Get all labeled frames after current frame
-        for frame in range(cur_idx + 1, video.frames):
-            if (
-                len(
-                    context.labels.find(video, frame, return_new=True)[
-                        0
-                    ].instances_to_show
-                )
-                != current_instance_count
-            ):
-                cls.go_to(context, frame)
-                break
+        frame = InstanceChangeNavigator.find_instance_change(context, direction=1)
+        if frame is not None:
+            cls.go_to(context, frame)


class GoPrevInstanceChange(NavCommand):
     @classmethod
     def do_action(cls, context: CommandContext, params: dict):
-        video = context.state["video"]
-        cur_idx = context.state["frame_idx"]
-
-        # Get current number of instances
-        current_frame = context.labels.find(video, cur_idx, return_new=True)[0]
-        current_instance_count = len(current_frame.instances_to_show)
-
-        # Get all labeled frames after current frame
-        for frame in range(cur_idx - 1, -1, -1):
-            if (
-                len(
-                    context.labels.find(video, frame, return_new=True)[
-                        0
-                    ].instances_to_show
-                )
-                != current_instance_count
-            ):
-                cls.go_to(context, frame)
-                break
+        frame = InstanceChangeNavigator.find_instance_change(context, direction=-1)
+        if frame is not None:
+            cls.go_to(context, frame)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ddb009a and 4e9e869.

📒 Files selected for processing (2)
  • sleap/gui/commands.py (2 hunks)
  • tests/gui/test_commands.py (2 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
tests/gui/test_commands.py (3)
sleap/gui/commands.py (4)
  • GoNextInstanceChange (1612-1633)
  • GoPrevInstanceChange (1636-1657)
  • labels (208-210)
  • CommandContext (177-641)
tests/fixtures/datasets.py (1)
  • centered_pair_predictions (40-41)
sleap/instance.py (6)
  • video (908-913)
  • find (1502-1521)
  • instances_to_show (1678-1698)
  • frame_idx (916-921)
  • instances (1524-1526)
  • instances (1529-1550)
sleap/gui/commands.py (2)
sleap/io/dataset.py (3)
  • video (569-579)
  • find (933-961)
  • frames (963-979)
sleap/instance.py (3)
  • video (908-913)
  • find (1502-1521)
  • instances_to_show (1678-1698)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Tests (macos-14)
  • GitHub Check: Tests (windows-2022)
  • GitHub Check: Tests (ubuntu-22.04)
🔇 Additional comments (5)
sleap/gui/commands.py (2)

635-641: Good implementation of instance change navigation methods

These methods provide a clean interface for navigating to frames where the number of instances changes, following the established pattern for navigation methods in the CommandContext class.


1612-1634: Well-structured implementation for navigating to next instance change

The implementation correctly:

  • Uses instances_to_show to match what's visible in the GUI
  • Properly finds frames where instance count changes
  • Handles edge cases by using return_new=True when finding labeled frames

This addresses the previous reviewer's concerns about ensuring frames with zero instances are included in the navigation.

tests/gui/test_commands.py (3)

25-26: Imports added correctly for new functionality.

The necessary imports for GoNextInstanceChange and GoPrevInstanceChange are properly added to support the new test functions.


1053-1080: Test implementation for next instance change navigation looks good.

This test function correctly verifies the behavior of the GoNextInstanceChange command by:

  1. Finding a frame with a different instance count
  2. Executing the command
  3. Verifying that the frame index changes to the expected frame

The test function has fixed the previous issue where context was referenced before being defined.


1082-1110: Test implementation for previous instance change navigation looks good.

This test function properly verifies the behavior of the GoPrevInstanceChange command, starting from the last frame and moving backward to find a frame with a different instance count.

Like the previous test, this also fixes the issue where context was referenced before being defined.

@emdavis02 emdavis02 requested a review from roomrys April 1, 2025 21:07
@roomrys roomrys removed their request for review April 7, 2025 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants