diff --git a/app/packages/plugins/src/index.ts b/app/packages/plugins/src/index.ts index afb9171f51..649c32d397 100644 --- a/app/packages/plugins/src/index.ts +++ b/app/packages/plugins/src/index.ts @@ -84,6 +84,7 @@ class PluginDefinition { serverPath: string; hasPy: boolean; hasJS: boolean; + builtin: boolean; constructor(json: any) { const serverPathPrefix = fou.getFetchPathPrefix(); @@ -102,6 +103,7 @@ class PluginDefinition { this.hasPy = json.has_py; this.hasJS = json.has_js; this.serverPath = `${serverPathPrefix}${json.server_path}`; + this.builtin = json.builtin; } } diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst index fe0ffb5576..2a6dcdad0a 100644 --- a/docs/source/plugins/developing_plugins.rst +++ b/docs/source/plugins/developing_plugins.rst @@ -86,7 +86,7 @@ configurable backend. Operators can even be composed of other operators or be used to add functionality to custom panels. FiftyOne comes with a number of builtin -:mod:`Python ` and +:mod:`Python ` and `JavaScript `_ operators for common tasks that are intended for either user-facing or internal plugin use. diff --git a/fiftyone/operators/registry.py b/fiftyone/operators/registry.py index 58e08a7a3c..1919f777e0 100644 --- a/fiftyone/operators/registry.py +++ b/fiftyone/operators/registry.py @@ -9,8 +9,6 @@ from fiftyone.operators.panel import Panel import fiftyone.plugins.context as fopc -from .builtin import BUILTIN_OPERATORS, BUILTIN_PANELS - def get_operator(operator_uri, enabled=True): """Gets the operator with the given URI. @@ -34,7 +32,7 @@ def get_operator(operator_uri, enabled=True): return operator -def list_operators(enabled=True, type=None): +def list_operators(enabled=True, type=None, builtins_only=False): """Returns all available operators. Args: @@ -42,12 +40,17 @@ def list_operators(enabled=True, type=None): only disabled operators (False) or all operators ("all") type (None): whether to include only ``"panel"`` or ``"operator"`` type operators + builtins_only (False): whether to include only builtin operators Returns: a list of :class:`fiftyone.operators.Operator` instances """ registry = OperatorRegistry(enabled=enabled) - return registry.list_operators(include_builtin=enabled != False, type=type) + return registry.list_operators( + include_builtin=enabled != False, + type=type, + builtins_only=builtins_only, + ) def operator_exists(operator_uri, enabled=True): @@ -65,6 +68,9 @@ def operator_exists(operator_uri, enabled=True): return registry.operator_exists(operator_uri) +_EXTRA_OPERATORS = [] + + class OperatorRegistry(object): """Operator registry. @@ -75,24 +81,30 @@ class OperatorRegistry(object): def __init__(self, enabled=True): self.plugin_contexts = fopc.build_plugin_contexts(enabled=enabled) - def list_operators(self, include_builtin=True, type=None): + def list_operators( + self, include_builtin=True, type=None, builtins_only=False + ): """Lists the available FiftyOne operators. Args: include_builtin (True): whether to include builtin operators type (None): whether to include only ``"panel"`` or ``"operator"`` type operators + builtins_only (False): whether to include only builtin operators Returns: a list of :class:`fiftyone.operators.Operator` instances """ operators = [] + operators.extend(_EXTRA_OPERATORS) for pctx in self.plugin_contexts: operators.extend(pctx.instances) - if include_builtin: - operators.extend(BUILTIN_OPERATORS) - operators.extend(BUILTIN_PANELS) + if not include_builtin: + operators = [op for op in operators if op._builtin is False] + + if builtins_only: + operators = [op for op in operators if op._builtin is True] if type == "panel": operators = [op for op in operators if isinstance(op, Panel)] @@ -126,7 +138,7 @@ def operator_exists(self, operator_uri): Returns: True/False """ - for operator in self.list_operators(): + for operator in self.list_operators(include_builtin=True): if operator_uri == operator.uri: return True diff --git a/fiftyone/operators/server.py b/fiftyone/operators/server.py index f16a0b6a64..7a0f7bd9ed 100644 --- a/fiftyone/operators/server.py +++ b/fiftyone/operators/server.py @@ -27,8 +27,8 @@ def get_operators(registry: PermissionedOperatorRegistry): - operators = registry.list_operators(True, "operator") - panels = registry.list_operators(True, "panel") + operators = registry.list_operators(include_builtin=True, type="operator") + panels = registry.list_operators(include_builtin=True, type="panel") return operators + panels diff --git a/fiftyone/plugins/constants.py b/fiftyone/plugins/constants.py new file mode 100644 index 0000000000..4712e722e5 --- /dev/null +++ b/fiftyone/plugins/constants.py @@ -0,0 +1,12 @@ +""" +FiftyOne plugins constants. + +| Copyright 2017-2024, Voxel51, Inc. +| `voxel51.com `_ +| +""" +import os + +BUILTIN_PLUGINS_DIR = os.path.join( + os.path.dirname(__file__), "..", "..", "plugins" +) diff --git a/fiftyone/plugins/context.py b/fiftyone/plugins/context.py index ff8d71be95..c36f0b854d 100644 --- a/fiftyone/plugins/context.py +++ b/fiftyone/plugins/context.py @@ -35,7 +35,7 @@ def build_plugin_contexts(enabled=True): a list of :class:`PluginContext` instances """ plugin_contexts = [] - for pd in fop.list_plugins(enabled=enabled): + for pd in fop.list_plugins(enabled=enabled, include_builtin=True): pctx = PluginContext(pd) pctx.register_all() plugin_contexts.append(pctx) @@ -96,10 +96,10 @@ def register(self, cls): Any errors are logged rather than being raised. Args: - cls: an :class:`fiftyone.operators.operator.Operator` class + cls: an :class:`fiftyone.operators.operator.Operator` or :class:`fiftyone.operators.panel.Panel` class """ try: - instance = cls() + instance = cls(_builtin=self.plugin_definition.builtin) if self.can_register(instance): instance.plugin_name = self.name if self.secrets: diff --git a/fiftyone/plugins/core.py b/fiftyone/plugins/core.py index 6e56d4d3ea..7e1c883019 100644 --- a/fiftyone/plugins/core.py +++ b/fiftyone/plugins/core.py @@ -25,7 +25,7 @@ import fiftyone.core.utils as fou from fiftyone.plugins.definitions import PluginDefinition from fiftyone.utils.github import GitHubRepository - +import fiftyone.plugins.constants as constants PLUGIN_METADATA_FILENAMES = ("fiftyone.yml", "fiftyone.yaml") @@ -48,12 +48,13 @@ def __repr__(self): return f"Plugin(name={self.name}, path={self.path})" -def list_plugins(enabled=True): - """Returns the definitions of downloaded plugins. +def list_plugins(enabled=True, include_builtin=False): + """Returns the definitions of available plugins. Args: enabled (True): whether to include only enabled plugins (True) or only disabled plugins (False) or all plugins ("all") + include_builtin (False): whether to include built-in plugins Returns: a list of :class:`PluginDefinition` instances @@ -62,7 +63,7 @@ def list_plugins(enabled=True): enabled = None plugins = [] - for p in _list_plugins(enabled=enabled): + for p in _list_plugins(enabled=enabled, include_builtin=include_builtin): try: plugins.append(_load_plugin_definition(p)) except: @@ -488,9 +489,18 @@ def _parse_plugin_metadata(metadata_path): return PluginPackage(plugin_name, plugin_path) -def _list_plugins(enabled=None): +def _list_plugins(enabled=None, include_builtin=False): plugins = [] - for metadata_path in _iter_plugin_metadata_files(): + external_plugin_paths = list(_iter_plugin_metadata_files()) + builtin_plugin_paths = list( + _iter_plugin_metadata_files(constants.BUILTIN_PLUGINS_DIR) + ) + all_plugin_paths = ( + (external_plugin_paths + builtin_plugin_paths) + if include_builtin + else external_plugin_paths + ) + for metadata_path in all_plugin_paths: try: plugin = _parse_plugin_metadata(metadata_path) plugins.append(plugin) diff --git a/fiftyone/plugins/definitions.py b/fiftyone/plugins/definitions.py index a07e0f6387..62ffcc546e 100644 --- a/fiftyone/plugins/definitions.py +++ b/fiftyone/plugins/definitions.py @@ -13,6 +13,7 @@ import eta.core.serial as etas import fiftyone as fo +import fiftyone.plugins.constants as constants class PluginDefinition(object): @@ -40,6 +41,11 @@ def directory(self): """The directory containing the plugin.""" return self._directory + @property + def builtin(self): + """Whether the plugin is a builtin plugin.""" + self.directory.startswith(constants.BUILTIN_PLUGINS_DIR) + @property def author(self): """The author of the plugin.""" @@ -205,6 +211,7 @@ def to_dict(self): "has_js": self.has_js, "server_path": self.server_path, "secrets": self.secrets, + "builtin": self.builtin, } @classmethod diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000000..5159a21299 --- /dev/null +++ b/plugins/__init__.py @@ -0,0 +1,7 @@ +""" +FiftyOne builtin plugins. + +| Copyright 2017-2024, Voxel51, Inc. +| `voxel51.com `_ +| +""" diff --git a/fiftyone/operators/builtin.py b/plugins/operators/__init__.py similarity index 98% rename from fiftyone/operators/builtin.py rename to plugins/operators/__init__.py index e0ae15b914..6df710702f 100644 --- a/fiftyone/operators/builtin.py +++ b/plugins/operators/__init__.py @@ -16,9 +16,6 @@ import fiftyone.operators.types as types from fiftyone.core.odm.workspace import default_workspace_factory -# pylint: disable=no-name-in-module -from fiftyone.operators.builtins.panels.model_evaluation import EvaluationPanel - class EditFieldInfo(foo.Operator): @property @@ -2373,39 +2370,36 @@ def _parse_spaces(ctx, spaces): return fo.Space.from_dict(spaces) -BUILTIN_OPERATORS = [ - EditFieldInfo(_builtin=True), - CloneSelectedSamples(_builtin=True), - CloneSampleField(_builtin=True), - CloneFrameField(_builtin=True), - RenameSampleField(_builtin=True), - RenameFrameField(_builtin=True), - ClearSampleField(_builtin=True), - ClearFrameField(_builtin=True), - DeleteSelectedSamples(_builtin=True), - DeleteSelectedLabels(_builtin=True), - DeleteSampleField(_builtin=True), - DeleteFrameField(_builtin=True), - CreateIndex(_builtin=True), - DropIndex(_builtin=True), - CreateSummaryField(_builtin=True), - UpdateSummaryField(_builtin=True), - DeleteSummaryField(_builtin=True), - AddGroupSlice(_builtin=True), - RenameGroupSlice(_builtin=True), - DeleteGroupSlice(_builtin=True), - ListSavedViews(_builtin=True), - LoadSavedView(_builtin=True), - SaveView(_builtin=True), - EditSavedViewInfo(_builtin=True), - DeleteSavedView(_builtin=True), - ListWorkspaces(_builtin=True), - LoadWorkspace(_builtin=True), - SaveWorkspace(_builtin=True), - EditWorkspaceInfo(_builtin=True), - DeleteWorkspace(_builtin=True), - SyncLastModifiedAt(_builtin=True), - ListFiles(_builtin=True), -] - -BUILTIN_PANELS = [EvaluationPanel(_builtin=True)] +def register(p): + p.register(EditFieldInfo) + p.register(CloneSelectedSamples) + p.register(CloneSampleField) + p.register(CloneFrameField) + p.register(RenameSampleField) + p.register(RenameFrameField) + p.register(ClearSampleField) + p.register(ClearFrameField) + p.register(DeleteSelectedSamples) + p.register(DeleteSelectedLabels) + p.register(DeleteSampleField) + p.register(DeleteFrameField) + p.register(CreateIndex) + p.register(DropIndex) + p.register(CreateSummaryField) + p.register(UpdateSummaryField) + p.register(DeleteSummaryField) + p.register(AddGroupSlice) + p.register(RenameGroupSlice) + p.register(DeleteGroupSlice) + p.register(ListSavedViews) + p.register(LoadSavedView) + p.register(SaveView) + p.register(EditSavedViewInfo) + p.register(DeleteSavedView) + p.register(ListWorkspaces) + p.register(LoadWorkspace) + p.register(SaveWorkspace) + p.register(EditWorkspaceInfo) + p.register(DeleteWorkspace) + p.register(SyncLastModifiedAt) + p.register(ListFiles) diff --git a/plugins/operators/fiftyone.yml b/plugins/operators/fiftyone.yml new file mode 100644 index 0000000000..a37fcf5b0b --- /dev/null +++ b/plugins/operators/fiftyone.yml @@ -0,0 +1,39 @@ +name: "@voxel51/operators" +description: Core FiftyOne operators +version: 1.0.0 +fiftyone: + version: "*" +url: https://github.com/voxel51/fiftyone/fiftyone/builtins/operators +operators: + - edit_field_info + - clone_selected_samples + - clone_sample_field + - clone_frame_field + - rename_sample_field + - rename_frame_field + - clear_sample_field + - clear_frame_field + - delete_selected_samples + - delete_selected_labels + - delete_sample_field + - delete_frame_field + - create_index + - drop_index + - create_summary_field + - update_summary_field + - delete_summary_field + - add_group_slice + - rename_group_slice + - delete_group_slice + - list_saved_views + - load_saved_view + - save_view + - edit_saved_view_info + - delete_saved_view + - list_workspaces + - load_workspace + - save_workspace + - edit_workspace_info + - delete_workspace + - sync_last_modified_at + - list_files diff --git a/plugins/panels/__init__.py b/plugins/panels/__init__.py new file mode 100644 index 0000000000..9e8fd7f8b3 --- /dev/null +++ b/plugins/panels/__init__.py @@ -0,0 +1,13 @@ +""" +Builtin panels. + +| Copyright 2017-2024, Voxel51, Inc. +| `voxel51.com `_ +| +""" + +from .model_evaluation import EvaluationPanel + + +def register(p): + p.register(EvaluationPanel) diff --git a/plugins/panels/fiftyone.yml b/plugins/panels/fiftyone.yml new file mode 100644 index 0000000000..0003293aa4 --- /dev/null +++ b/plugins/panels/fiftyone.yml @@ -0,0 +1,8 @@ +name: "@voxel51/panel" +description: Core FiftyOne panels +version: 1.0.0 +fiftyone: + version: "*" +url: https://github.com/voxel51/fiftyone/plugins/panels +panels: + - model_evaluation_panel_builtin diff --git a/fiftyone/operators/builtins/panels/model_evaluation/__init__.py b/plugins/panels/model_evaluation.py similarity index 100% rename from fiftyone/operators/builtins/panels/model_evaluation/__init__.py rename to plugins/panels/model_evaluation.py diff --git a/tests/unittests/operators/executor_tests.py b/tests/unittests/operators/executor_tests.py index ef2321a8dc..f418a994d2 100644 --- a/tests/unittests/operators/executor_tests.py +++ b/tests/unittests/operators/executor_tests.py @@ -10,7 +10,7 @@ ExecutionContext, ) from fiftyone.operators import OperatorConfig -import fiftyone.operators.builtin as builtin +import fiftyone.operators.registry as registry ECHO_URI = "@voxel51/operators/echo" @@ -31,7 +31,7 @@ def execute(self, ctx): # Force registration of the operator for testing -builtin.BUILTIN_OPERATORS.append(EchoOperator(_builtin=True)) +registry._EXTRA_OPERATORS.append(EchoOperator(_builtin=True)) @pytest.mark.asyncio