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

Handle shadowed plugins #5342

Merged
merged 2 commits into from
Jan 7, 2025
Merged

Handle shadowed plugins #5342

merged 2 commits into from
Jan 7, 2025

Conversation

brimoor
Copy link
Contributor

@brimoor brimoor commented Jan 5, 2025

Change log

  • Updates the plugins API to gracefully ignore plugins that are "shadowed" by other plugins of the same name
  • Ensures that download_plugin(..., overwrite=True) and create_plugin(..., overwrite=True) cannot overwrite builtin plugins

Example usage

PLUGINS_DIR="$(fiftyone config plugins_dir)"

# (if necessary) remove source install of `fiftyone-plugins`
unlink "$PLUGINS_DIR/fiftyone-plugins"

# Install a plugin
fiftyone plugins download \
    https://github.com/voxel51/fiftyone-plugins \
    --plugin-names @voxel51/dashboard

# Duplicate the plugin
ORIG_DIR="$PLUGINS_DIR/@voxel51/dashboard"
DUP_DIR="$PLUGINS_DIR/also_dashboard"
cp -R $ORIG_DIR $DUP_DIR

# See how duplicate plugins are handled
fiftyone plugins list
"""
plugin                            version    enabled    builtin    shadowed    directory
--------------------------------  ---------  ---------  ---------  ----------  ------------------------------------------------------
@voxel51/operators                1.0.0      ✓          ✓                      /Users/Brian/dev/fiftyone/plugins/operators
@voxel51/panels                   1.0.0      ✓          ✓                      /Users/Brian/dev/fiftyone/plugins/panels
@voxel51/dashboard                1.0.1      ✓                                 /Users/Brian/fiftyone/__plugins__/@voxel51/dashboard
@voxel51/dashboard                1.0.1                            ✓           /Users/Brian/fiftyone/__plugins__/also_dashboard
"""

# Verify that the App works
# EG launch the Dashboard panel
fiftyone app launch

# Only affects the "active" plugin
# "Shadowed" plugin remains disabled
fiftyone plugins disable @voxel51/dashboard
fiftyone plugins list

# Only affects the "active" plugin
# "Shadowed" plugin remains disabled
fiftyone plugins enable @voxel51/dashboard
fiftyone plugins list

# Shows the "active" plugin
# The `shadow_paths` row indicates that this plugin is shadowing `also_dashboard`
fiftyone plugins info @voxel51/dashboard

# Show info about a "shadowed" plugin
fiftyone plugins info $DUP_DIR

# Delete a shadowed plugin
rm -rf $DUP_DIR

# The `shadowed` column is only shown if there are actually shadowed plugins
fiftyone plugins list
"""
plugin                            version    enabled    builtin    directory
--------------------------------  ---------  ---------  ---------  ------------------------------------------------------
@voxel51/operators                1.0.0      ✓          ✓          /Users/Brian/dev/fiftyone/plugins/operators
@voxel51/panels                   1.0.0      ✓          ✓          /Users/Brian/dev/fiftyone/plugins/panels
@voxel51/dashboard                1.0.1      ✓                     /Users/Brian/fiftyone/__plugins__/@voxel51/dashboard
"""

# Cleanup
fiftyone plugins delete @voxel51/dashboard

# (if necessary)
cd /path/to/fiftyone-plugins
ln -s "$(pwd)" "$(fiftyone config plugins_dir)/fiftyone-plugins"

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced plugin management with support for shadowed plugins.
    • Expanded fiftyone plugins info command to accept plugin name or directory path.
  • Documentation

    • Updated CLI documentation to clarify plugin information retrieval.
    • Improved command usage examples for plugin-related commands.
  • Improvements

    • Added more comprehensive plugin listing capabilities, including shadowed plugins.
    • Enhanced plugin detection and management logic.
    • Improved error handling and logging for plugin operations.

These updates provide more flexibility and clarity in managing FiftyOne plugins, making it easier to discover and interact with plugin resources.

@brimoor brimoor added the enhancement Code enhancement label Jan 5, 2025
@brimoor brimoor requested a review from ritch January 5, 2025 19:27
Copy link
Contributor

coderabbitai bot commented Jan 5, 2025

Walkthrough

This pull request introduces enhancements to the FiftyOne plugin management system, focusing on improving the CLI interface and plugin handling. The changes primarily involve updating the fiftyone plugins info command to accept both plugin names and directory paths, adding support for tracking shadowed plugins, and modifying various plugin-related functions to provide more comprehensive plugin information and management capabilities.

Changes

File Change Summary
docs/source/cli/index.rst Updated CLI documentation for fiftyone plugins info command, changing argument from NAME to NAME_OR_DIR and adding usage example.
fiftyone/core/cli.py Modified PluginsListCommand and PluginsInfoCommand to support shadowed plugins and flexible plugin information retrieval by changing argument from name to name_or_dir.
fiftyone/plugins/core.py Added shadow_paths attribute to PluginPackage, updated plugin listing and retrieval functions to handle shadowed plugins, and improved error handling.
fiftyone/plugins/definitions.py Introduced shadow_paths parameter to PluginDefinition class and related methods, updating metadata handling.
tests/unittests/plugins/core_tests.py Updated test case to reflect new behavior for handling duplicate plugin names, allowing the test to pass without raising errors.

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • ritch
  • imanjra

Poem

🐰 Plugins dance, shadows play light,
CLI commands now shine so bright!
Names and paths, a flexible friend,
FiftyOne's magic knows no end!
Hop along the plugin trail, hooray! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @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.

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.

@brimoor brimoor force-pushed the handle-shadowed-plugins branch from d697f93 to 4a75ccf Compare January 5, 2025 19:28
Copy link
Contributor

@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: 1

🧹 Nitpick comments (1)
fiftyone/plugins/core.py (1)

73-75: **Simplify repeated 'all' → None pattern **

The shadowed == "all" check effectively aligns with patterns used for enabled and builtin. For clarity, consider refactoring repetitive checks into a small helper function if it appears in multiple areas.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 199872a and 4a75ccf.

📒 Files selected for processing (4)
  • docs/source/cli/index.rst (2 hunks)
  • fiftyone/core/cli.py (3 hunks)
  • fiftyone/plugins/core.py (15 hunks)
  • fiftyone/plugins/definitions.py (5 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
fiftyone/core/cli.py

3676-3676: Do not use bare except

(E722)

fiftyone/plugins/core.py

82-82: Do not use bare except

(E722)


495-495: Use existing_plugins.get(plugin_name) instead of existing_plugins.get(plugin_name, None)

Replace existing_plugins.get(plugin_name, None) with existing_plugins.get(plugin_name)

(SIM910)

🔇 Additional comments (24)
fiftyone/plugins/core.py (12)

Line range hint 15-47: Add shadow_paths attribute to PluginPackage [Approved]

The addition of shadow_paths: Optional[list[str]] is well-structured. Using Optional and a default of None aligns with Python best practices for optional fields.


53-62: Enhance list_plugins to handle shadowed parameter [Approved]

Introducing the new shadowed parameter broadens the functionality to include or exclude shadowed plugins. The docstring updates at lines 61-62 also provide clear guidance on how to use this parameter.


77-83: **Exception handling on plugin parsing **

A bare except block is used at line 82 for _load_plugin_definition. Although the log message at line 83 clarifies the failure, consider catching more specific exceptions (e.g. Exception or a narrower subset) to avoid masking unrelated errors.

🧰 Tools
🪛 Ruff (0.8.2)

82-82: Do not use bare except

(E722)


Line range hint 152-173: Filtering plugins when listing downloaded, enabled, and disabled [Approved]

The calls to _list_plugins(...) with explicit parameters for builtin and shadowed are consistent with the new logic. Returning only the plugin names is a straightforward design.


176-190: New get_plugin signature [Approved]

Allowing a plugin directory or a plugin name is a valuable extension. The docstring at lines 180-181 is concise, accurately describing the new usage.


251-256: Support skipping existing plugins [Approved]

The introduction of skipped_plugins cleanly handles duplicates that do not need to be overwritten. This approach helps keep the user informed of what was skipped.


492-505: Creation logic for new plugins [Approved]

This block correctly prevents overwriting built-in plugins at lines 500-503. The approach to clearing out existing directories, then populating with new contents, is clear and consistent.

🧰 Tools
🪛 Ruff (0.8.2)

495-495: Use existing_plugins.get(plugin_name) instead of existing_plugins.get(plugin_name, None)

Replace existing_plugins.get(plugin_name, None) with existing_plugins.get(plugin_name)

(SIM910)


Line range hint 579-602: Refactored _list_plugins to handle shadowed parameter [Approved]

The introduction of _handle_shadowed before filtering by enablement is a neat approach to ensure that built-in overrides take the correct precedence. The usage of a single pass to gather and then handle shadowed items is efficient.

🧰 Tools
🪛 Ruff (0.8.2)

596-596: Do not use bare except

(E722)


614-633: _handle_shadowed logic [Approved]

This function suitably updates the existing_plugin.shadow_paths field. The return set elegantly accommodates hidden/shadowed plugins.


671-678: _get_plugin shadowed=False parameter [Approved]

Ensuring that shadowed plugins are excluded by default is consistent with the typical usage scenario. The explicit raise ValueError on line 678 is correct for missing plugins.


681-689: Enhance _load_plugin_definition to capture shadow_paths [Approved]

Loading the plugin definition with shadow_paths ensures shadowed plugin directories are tracked consistently across the codebase.


Line range hint 299-326: **Safe overwrite logic for download_plugin **

The logic around lines 306-310 correctly prevents overwriting built-in plugins. On line 299-300, usage of existing_plugins.get(plugin.name, None) can be shortened to existing_plugins.get(plugin.name) to address the static analysis hint (SIM910). Please confirm that making this refactor would not cause side effects.

-            existing_plugin = existing_plugins.get(plugin.name, None)
+            existing_plugin = existing_plugins.get(plugin.name)
✅ Verification successful

Let me verify the safety of this refactoring by checking the usage of existing_plugins.get() across the codebase.


Let me check how the None value is used in the conditional logic.


The suggested refactoring to remove explicit None is safe to apply

The code consistently checks for the existence of plugins using if existing_plugin is not None after calling existing_plugins.get(). Since dict.get() returns None by default when the key is not found, explicitly passing None as the default value is redundant. The suggested refactoring won't change the behavior of the code.

  • fiftyone/plugins/core.py:299: Can safely change to existing_plugins.get(plugin.name)
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Search for similar usage patterns of existing_plugins.get()
rg "existing_plugins\.get\(" -A 2 -B 2

# Search for the definition and usage of existing_plugins
ast-grep --pattern 'existing_plugins = $_'

Length of output: 1507


Script:

#!/bin/bash
# Get more context around the usage of existing_plugin
rg "existing_plugin = existing_plugins\.get\(" -A 10 -B 2

Length of output: 2557

fiftyone/plugins/definitions.py (4)

25-26: Clarify docstring for shadow_paths [Approved]

These lines clearly describe that shadow_paths is a list of plugin directories that the plugin shadows. The docstring provides valuable context for consumers of this class.


31-34: Initialize shadow_paths in constructor [Approved]

Introducing shadow_paths in the __init__ method is straightforward and maintains backward compatibility by defaulting to None.


52-56: New shadow_paths property [Approved]

The property approach elegantly exposes the _shadow_paths list. Simple and effective.


Line range hint 227-242: from_disk method supports shadow_paths [Approved]

Accepting shadow_paths=None in from_disk ensures consistent usage with the constructor. The dictionary merge and final return cls(...) at line 242 is cleanly implemented.

fiftyone/core/cli.py (7)

3599-3601: Request all plugins, including shadowed [Approved]

Using shadowed="all" at lines 3599-3601 ensures the CLI can provide a complete view of plugins, matching the new core logic.


3611-3615: Accumulating shadow_paths for CLI listing [Approved]

Collecting shadowed_paths in a set is straightforward. Good approach to handle multiple shadowed sources.


3616-3623: Enrichment of output headers for plugin listing [Approved]

Including shadowed in the table columns at lines 3617-3621 is a clear, user-friendly addition.


3626-3634: Plugin enabling logic respects shadowed state [Approved]

The line 3627 check (pd.builtin or pd.name in enabled_plugins) and not shadowed properly excludes shadowed plugins from being flagged as enabled.


3639-3641: Handle empty shadowed_paths [Approved]

Removing the shadowed column from the CLI output if no shadowed paths exist prevents empty columns in the table. Nicely done.


3655-3658: Clarification of usage examples [Approved]

These lines help users see that fiftyone plugins info <dir> is now supported. Great addition to the usage example.


3662-3666: New argument name_or_dir [Approved]

Updating the argument from name to name_or_dir is a functional improvement. The docstring at lines 3664-3665 is clear.

docs/source/cli/index.rst (1)

Line range hint 1242-1263: Document name_or_dir usage for plugins info [Approved]

Renaming the parameter to NAME_OR_DIR and adding an example with <dir> clarifies how to query plugins by directory path. This reflects the updated CLI functionality accurately.

fiftyone/core/cli.py Show resolved Hide resolved
@brimoor brimoor force-pushed the handle-shadowed-plugins branch from 4a75ccf to 8482229 Compare January 5, 2025 21:14
Copy link
Contributor

@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: 1

🧹 Nitpick comments (1)
fiftyone/plugins/core.py (1)

495-495: Minor simplification: remove redundant default in .get()
Using existing_plugins.get(plugin_name) is simpler than including , None.

Apply the following small diff:

-        existing_plugin = existing_plugins.get(plugin_name, None)
+        existing_plugin = existing_plugins.get(plugin_name)
🧰 Tools
🪛 Ruff (0.8.2)

495-495: Use existing_plugins.get(plugin_name) instead of existing_plugins.get(plugin_name, None)

Replace existing_plugins.get(plugin_name, None) with existing_plugins.get(plugin_name)

(SIM910)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a75ccf and 8482229.

📒 Files selected for processing (5)
  • docs/source/cli/index.rst (2 hunks)
  • fiftyone/core/cli.py (3 hunks)
  • fiftyone/plugins/core.py (15 hunks)
  • fiftyone/plugins/definitions.py (5 hunks)
  • tests/unittests/plugins/core_tests.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/source/cli/index.rst
  • fiftyone/plugins/definitions.py
🧰 Additional context used
🪛 Ruff (0.8.2)
fiftyone/plugins/core.py

82-82: Do not use bare except

(E722)


495-495: Use existing_plugins.get(plugin_name) instead of existing_plugins.get(plugin_name, None)

Replace existing_plugins.get(plugin_name, None) with existing_plugins.get(plugin_name)

(SIM910)

fiftyone/core/cli.py

3676-3676: Do not use bare except

(E722)

⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: test / test-python (ubuntu-latest-m, 3.10)
  • GitHub Check: test / test-app
  • GitHub Check: e2e / test-e2e
  • GitHub Check: build / build
  • GitHub Check: build
🔇 Additional comments (36)
fiftyone/core/cli.py (9)

3599-3601: Passing 'shadowed="all"' is well-considered
Including all plugins, whether shadowed or not, ensures a comprehensive listing.


3611-3615: Accumulating shadowed plugin paths
Collecting all shadow_paths into a set avoids duplication and helps efficiently track shadowed plugins.


3616-3623: Added 'shadowed' column for clarity
Displaying the shadowed status in the table headers meaningfully improves plugin visualization.


3626-3627: Logic for disabling shadowed plugins
Preventing a plugin from being enabled if it’s shadowed is a solid approach to avoid conflicts.


3632-3634: Updating 'enabled' and 'shadowed' fields
Storing and displaying enabled and shadowed states ensures transparency about a plugin’s usability.


3639-3641: Dynamically remove 'shadowed' column when unused
Removing the column if there are no shadowed plugins keeps the table output clean.


3655-3657: Documentation now mentions plugins by directory
It’s helpful to clarify users can specify a directory for plugin info, aligning with the new plugin resolution logic.


3662-3666: Extended CLI argument accepts name or directory
Enabling NAME_OR_DIR usage provides flexible plugin lookups.


3670-3683: Refactor bare except to be more specific
Line 3676 uses a bare except, which can mask unexpected errors.

Apply the following diff for clarity:

-    except:
+    except ValueError:

Or specify other relevant exception types that accurately reflect any legitimate failures.

🧰 Tools
🪛 Ruff (0.8.2)

3676-3676: Do not use bare except

(E722)

tests/unittests/plugins/core_tests.py (1)

153-155: Meaningful no-error check for shadowed plugins
Verifying that duplicate-named plugins do not raise an error is a good test of your new shadowing logic.

fiftyone/plugins/core.py (26)

15-15: Import for typing hints
The addition of from typing import Optional is appropriate for the newly introduced optional fields.


47-47: Introducing shadow_paths
Adding an optional shadow_paths list in PluginPackage enables robust tracking of duplicate or shadowed plugins.


53-62: Expanded plugin listing with shadowed parameter
The new shadowed parameter in list_plugins accommodates specialized queries for overshadowed plugins.


73-75: Graceful handling of 'all' for shadowed
Setting shadowed = None when the user supplies "all" is consistent with the existing logic for other parameters.


77-79: Passing shadowed to _list_plugins
All filter criteria (enabled, builtin, shadowed) are now delegated properly to _list_plugins.


152-153: Excluding shadowed plugins from downloaded list
Retrieving only non-shadowed, non-builtin plugins ensures this list is strictly user-downloadable plugins.


162-163: Listing enabled plugins
Filtering by enabled=True and excluding shadowed or builtins provides a neat set of active plugins.


172-173: Listing disabled plugins
Similarly ensuring shadowed or builtin plugins aren’t mixed simplifies plugin state visibility.


176-190: Expanded logic in get_plugin
Allowing a plugin to be fetched by name or directory fosters flexibility in various usage scenarios.


251-253: Gathering existing plugins for duplication checks
Capturing all plugins (enabled="all", builtin="all") is correct for identifying potential conflicts.


255-256: Initialized skipped_plugins
Using a set maintains quick membership checks for any plugin intentionally skipped during download.


299-300: Safe retrieval of existing plugins
Using existing_plugins.get(plugin.name, None) is functional here, though see the subsequent note for a minor improvement.


306-310: Prevent overwriting of builtin plugins
Raising an error ensures system-level plugins remain untouched, preserving core stability.


311-313: Selective overwrite of existing user plugin
Clearing out the old plugin directory before copying in the new code is prudent to avoid stale files.


322-326: Inform if requested plugins aren’t found
Gracefully warns about missing plugins for better user feedback.


492-496: Consolidated plugin fetch for creation
Consistently enumerates "all" plugins to detect name collisions, aligning with the approach used elsewhere.

🧰 Tools
🪛 Ruff (0.8.2)

495-495: Use existing_plugins.get(plugin_name) instead of existing_plugins.get(plugin_name, None)

Replace existing_plugins.get(plugin_name, None) with existing_plugins.get(plugin_name)

(SIM910)


500-505: Alerting users when builtin plugins are locked
Mirroring the approach in download_plugin keeps the plugin system robust.


514-514: Overwriting existing plugin directory
Displays a clear log message before resetting directories, keeping maintainers informed.


Line range hint 579-602: Refined _list_plugins handles builtins, user plugins, and shadowed
Merging them seamlessly, then filtering on shadowed status before applying enablement info, ensures consistent plugin finalization.

🧰 Tools
🪛 Ruff (0.8.2)

596-596: Do not use bare except

(E722)


614-623: Core logic for tracking shadowed plugins
This block neatly appends newly discovered duplicates to shadow_paths, designating them as overshadowed.


625-627: Conditional acceptance of strictly shadowed plugins
When shadowed is True or None, these duplicates appear in the returned list; otherwise, they remain hidden.


628-630: Adding non-shadowed to plugin list
Includes the primary plugin in the final set if shadowed in (False, None).


631-633: Caching plugin references
Storing references in existing_plugins ensures duplicates can be properly tracked as future shadow_paths.


671-676: Known plugin check
Returns the first matching plugin by name, ignoring overshadowed duplicates due to shadowed=False.


678-678: Raising error for unknown plugin
Clear error message if no plugin matches, preventing silent failures.


681-689: Support for shadowed paths in _load_plugin_definition
Retaining shadow_paths ensures detailed introspection of overshadowed plugin definitions.

fiftyone/plugins/core.py Show resolved Hide resolved
Copy link
Contributor

@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: 1

🧹 Nitpick comments (2)
tests/unittests/plugins/core_tests.py (2)

Line range hint 143-152: Remove unused mock object and improve test setup clarity.

The mock object m is created but never used in the test. Additionally, the test setup could be more explicit about the expected plugin paths and configurations.

Consider this improvement:

 def test_duplicate_plugins(mocker, fiftyone_plugins_dir):
     mocker.patch("fiftyone.config.plugins_dir", fiftyone_plugins_dir)
 
     plugin_name = "test-plugin1"
     dup_plugin_dir = fiftyone_plugins_dir / "test-plugin2"
-    m = mock.Mock(spec=fop.core.PluginPackage(plugin_name, "path/to/plugin"))
+    # Create two plugins with the same name but different paths
+    original_plugin_dir = fiftyone_plugins_dir / "test-plugin1"
     with open(os.path.join(dup_plugin_dir, "fiftyone.yml"), "w") as f:
         pd = {k: plugin_name + "-" + k for k in _REQUIRED_YML_KEYS}
         f.write(yaml.dump(pd))

153-158: Enhance test coverage for shadowed plugins.

While the test verifies the count of plugins, it could be more comprehensive by checking:

  1. Which plugin is considered active vs shadowed
  2. The paths of both plugins
  3. Other relevant plugin attributes

Consider adding these verifications:

     plugin_names = [p.name for p in fop.list_plugins()]
     assert plugin_names.count("test-plugin1-name") == 1
+    # Verify the active plugin is the one we expect
+    active_plugin = next(p for p in fop.list_plugins() if p.name == "test-plugin1-name")
+    assert active_plugin.path == str(fiftyone_plugins_dir / "test-plugin1")
 
     plugin_names = [p.name for p in fop.list_plugins(shadowed="all")]
     assert plugin_names.count("test-plugin1-name") == 2
+    # Verify both plugins are found with correct paths
+    plugins = [p for p in fop.list_plugins(shadowed="all") if p.name == "test-plugin1-name"]
+    paths = {str(fiftyone_plugins_dir / "test-plugin1"), str(fiftyone_plugins_dir / "test-plugin2")}
+    assert {p.path for p in plugins} == paths
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8482229 and 3cb63f1.

📒 Files selected for processing (1)
  • tests/unittests/plugins/core_tests.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: build / build
  • GitHub Check: e2e / test-e2e
  • GitHub Check: test / test-app
  • GitHub Check: build

tests/unittests/plugins/core_tests.py Show resolved Hide resolved
@ritch
Copy link
Contributor

ritch commented Jan 6, 2025

(fiftyone-env2) ➜  plugins git:(prompt-on-cancel) ✗ fiftyone plugins disable @voxel51/panels
Traceback (most recent call last):
  File "/Users/ritchie/.pyenv/versions/fiftyone-env2/bin/fiftyone", line 8, in <module>
    sys.exit(main())
  File "/Users/ritchie/Projects/voxel51/fiftyone/fiftyone/core/cli.py", line 4715, in main
    args.execute(args)
  File "/Users/ritchie/Projects/voxel51/fiftyone/fiftyone/core/cli.py", line 4698, in <lambda>
    parser.set_defaults(execute=lambda args: command.execute(parser, args))
  File "/Users/ritchie/Projects/voxel51/fiftyone/fiftyone/core/cli.py", line 3986, in execute
    fop.disable_plugin(name)
  File "/Users/ritchie/Projects/voxel51/fiftyone/fiftyone/plugins/core.py", line 125, in disable_plugin
    raise ValueError(
ValueError: Cannot change enablement of builtin plugin '@voxel51/panels'

Lots of unrelated output, would have expected just the error message instead of the trace.

@ritch
Copy link
Contributor

ritch commented Jan 6, 2025

Manually tested duplicate plugins, duplicate builtins, disabling builtins (possibly unrelated to this PR!) ✅

@ritch
Copy link
Contributor

ritch commented Jan 6, 2025

Also tested duplicate builtins in the app ✅

@@ -44,19 +44,22 @@ class PluginPackage:

name: str
path: str
shadow_paths: Optional[list[str]] = None
Copy link
Contributor

@ritch ritch Jan 6, 2025

Choose a reason for hiding this comment

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

I never thought I would see the day 😀. Doesn't this break your mantra: don't introduce types partially?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My official mantra is follow the existing convention in the module you're contributing to, so I had to do it 😄

And... also had to do it because it's required when defining data classes 😉


Args:
enabled (True): whether to include only enabled plugins (True) or only
disabled plugins (False) or all plugins ("all")
builtin (False): whether to include only builtin plugins (True) or only
non-builtin plugins (False) or all plugins ("all")
shadowed (False): whether to include only "shadowed" duplicate plugins
(True) or only usable plugins (False) or all plugins ("all")
Copy link
Contributor

@ritch ritch Jan 6, 2025

Choose a reason for hiding this comment

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

Can builtin and shadowed be used together? The wording here "all plugins" is confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep all of these can be specified independently or together. They're composable filters


logger.info(f"Overwriting existing plugin '{plugin.name}'")
plugin_dir = existing_plugin.directory
etau.delete_dir(plugin_dir)
Copy link
Contributor

Choose a reason for hiding this comment

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

When does this happen? I would hope that something like fiftyone plugins download XXX --force would be required to avoid accidentally deleting something. Possibly exists, just commenting here to make sure we double check this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct, user must specify download_plugin(..., overwrite=True) or fiftyone plugins download ... --overwrite in order to overwrite an existing plugin.

They'll get an error without forcing the overwrite.

for plugin in _list_plugins(
enabled=enabled, builtin=builtin, shadowed=False
):
if plugin.name == name:
Copy link
Contributor

Choose a reason for hiding this comment

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

We use this style all over and its not that efficient. Instead of loading everything and then searching for a name we should load only until we get the named plugin. Mostly concerned about unnecessary fs calls.

Maybe a _find_plugin(name=, plugin_dir=) would be useful for this.

Can handle such improvements elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes refactoring _list_plugins() to be a generator would 2x average performance here.

Importantly, however, I believe all of the App's plugin usage goes through build_plugin_contexts(), which is cached via @plugins_cache.

@@ -194,6 +202,7 @@ def to_dict(self):
"""
return {
"name": self.name,
"builtin": self.builtin,
Copy link
Contributor

Choose a reason for hiding this comment

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

?

Copy link
Contributor

@ritch ritch Jan 6, 2025

Choose a reason for hiding this comment

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

I find that moving things like this adds to the OSS => teams conflicts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved it so that builtin appears immediately below name when printing plugin info:

$ fiftyone plugins info @voxel51/operators
key                     value
----------------------  -----------------------------------------------------
name                    @voxel51/operators
builtin                 True
...

That's a bit hacky of course as it relies on dicts having a stable iteration order, which isn't necessarily guaranteed in general.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

+1 for not causing diffs that have no functional purpose though

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah actually this makes sense, just didn't realize.

Copy link
Contributor

@ritch ritch left a comment

Choose a reason for hiding this comment

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

LGTM

@brimoor brimoor merged commit f19e6e6 into develop Jan 7, 2025
14 checks passed
@brimoor brimoor deleted the handle-shadowed-plugins branch January 7, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Code enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants