Repository: equinor/webviz-config

Issue:
Raise a warning if multiple plugins exist with same name
Since we are a plugin framework, there might be plugin projects creating plugins with the same name.

Raise a warning during runtime to give attention to name conflict. This could be done in
https://github.com/equinor/webviz-config/blob/623ab6fbabf85dd658be1d668d9f6e216939fdd2/webviz_config/plugins/__init__.py#L37-L39
by simply checking if a plugin with name is already in `__all__`. 

The warning should probably include package name of both plugins in order to make it easy for the user to understand where they come from (one way of getting package name from the plugin reference is to use `inspect` from std.lib).

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0faa365..7b43905 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [#337](https://github.com/equinor/webviz-config/pull/337) - New generic plugin to
 generate a [Pivot table](https://en.wikipedia.org/wiki/Pivot_table) based on
 [Dash Pivottable](https://github.com/plotly/dash-pivottable).
+- [#333](https://github.com/equinor/webviz-config/pull/333) - Warning is now raised
+if more than one plugin project provides a plugin with the same name.
 
 ## [0.2.2] - 2020-11-16
 
diff --git a/webviz_config/plugins/__init__.py b/webviz_config/plugins/__init__.py
index fd3a4af..b22bb5d 100644
--- a/webviz_config/plugins/__init__.py
+++ b/webviz_config/plugins/__init__.py
@@ -2,45 +2,16 @@
 the utility itself.
 """
 
-import inspect
 from typing import Optional
 
 try:
     # Python 3.8+
-    # pylint: disable=ungrouped-imports
     from importlib.metadata import distributions  # type: ignore
-    from typing import TypedDict  # type: ignore
 except ModuleNotFoundError:
     # Python < 3.8
     from importlib_metadata import distributions  # type: ignore
-    from typing_extensions import TypedDict  # type: ignore
 
+from ._utils import load_webviz_plugins_with_metadata, PluginDistInfo
 
-class PluginDistInfo(TypedDict):
-    dist_name: str
-    dist_version: str
-    documentation_url: Optional[str]
-    download_url: Optional[str]
-    issue_url: Optional[str]
 
-
-metadata = {}
-
-for dist in distributions():
-    for entry_point in dist.entry_points:
-        if entry_point.group == "webviz_config_plugins":
-            project_urls = {
-                value.split(",")[0]: value.split(",")[1].strip()
-                for (key, value) in dist.metadata.items()
-                if key == "Project-URL"
-            }
-
-            metadata[entry_point.name] = {
-                "dist_name": dist.metadata["name"],
-                "dist_version": dist.version,
-                "documentation_url": project_urls.get("Documentation"),
-                "download_url": project_urls.get("Download"),
-                "tracker_url": project_urls.get("Tracker"),
-            }
-
-            globals()[entry_point.name] = entry_point.load()
+metadata = load_webviz_plugins_with_metadata(distributions(), globals())
diff --git a/webviz_config/plugins/_utils.py b/webviz_config/plugins/_utils.py
new file mode 100644
index 0000000..fbb19bf
--- /dev/null
+++ b/webviz_config/plugins/_utils.py
@@ -0,0 +1,59 @@
+import warnings
+from typing import Any, Dict, Iterable, Optional
+
+try:
+    # Python 3.8+
+    # pylint: disable=ungrouped-imports
+    from typing import TypedDict  # type: ignore
+except ImportError:
+    # Python < 3.8
+    from typing_extensions import TypedDict  # type: ignore
+
+
+class PluginDistInfo(TypedDict):
+    dist_name: str
+    dist_version: str
+    documentation_url: Optional[str]
+    download_url: Optional[str]
+    tracker_url: Optional[str]
+
+
+def load_webviz_plugins_with_metadata(
+    distributions: Iterable, loaded_plugins: Dict[str, Any]
+) -> Dict[str, PluginDistInfo]:
+    """Loads the given distributions, finds entry points corresponding to webviz-config
+    plugins, and put them into the mutable input dictionary loaded_plugins
+    (key is plugin name string, value is reference to plugin class).
+    Also returns a dictionary of plugin metadata.
+    """
+
+    metadata: Dict[str, PluginDistInfo] = {}
+
+    for dist in distributions:
+        for entry_point in dist.entry_points:
+            if entry_point.group == "webviz_config_plugins":
+                project_urls = {
+                    value.split(",")[0]: value.split(",")[1].strip()
+                    for (key, value) in dist.metadata.items()
+                    if key == "Project-URL"
+                }
+
+                if entry_point.name in metadata:
+                    warnings.warn(
+                        f"Multiple versions of plugin with name {entry_point.name}. "
+                        f"Already loaded from project {metadata[entry_point.name]['dist_name']}. "
+                        f"Overwriting using plugin with from project {dist.metadata['name']}",
+                        RuntimeWarning,
+                    )
+
+                metadata[entry_point.name] = {
+                    "dist_name": dist.metadata["name"],
+                    "dist_version": dist.version,
+                    "documentation_url": project_urls.get("Documentation"),
+                    "download_url": project_urls.get("Download"),
+                    "tracker_url": project_urls.get("Tracker"),
+                }
+
+                loaded_plugins[entry_point.name] = entry_point.load()
+
+    return metadata


Test patch:
diff --git a/tests/test_plugin_init.py b/tests/test_plugin_init.py
new file mode 100644
index 0000000..011df9a
--- /dev/null
+++ b/tests/test_plugin_init.py
@@ -0,0 +1,60 @@
+import warnings
+
+import mock
+
+from webviz_config.plugins._utils import load_webviz_plugins_with_metadata
+
+
+class DistMock:
+    # pylint: disable=too-few-public-methods
+    def __init__(self, entry_points, name):
+        self.metadata = {"name": name}
+
+        self.entry_points = entry_points
+        self.version = "123"
+
+
+plugin_entrypoint_mock1 = mock.Mock()
+plugin_entrypoint_mock1.group = "webviz_config_plugins"
+plugin_entrypoint_mock1.name = "SomePlugin1"
+
+plugin_entrypoint_mock2 = mock.Mock()
+plugin_entrypoint_mock2.group = "webviz_config_plugins"
+plugin_entrypoint_mock2.name = "SomePlugin2"
+
+dist_mock1 = DistMock([plugin_entrypoint_mock1], "dist_mock1")
+dist_mock2 = DistMock([plugin_entrypoint_mock1], "dist_mock2")
+dist_mock3 = DistMock([plugin_entrypoint_mock2], "dist_mock3")
+
+
+def test_no_warning():
+    globals_mock = {}
+    with warnings.catch_warnings(record=True) as warn:
+        metadata = load_webviz_plugins_with_metadata(
+            [dist_mock1, dist_mock3], globals_mock
+        )
+        assert len(warn) == 0, "Too many warnings"
+
+    assert len(metadata) == 2, "Wrong number of items in metadata"
+    assert "SomePlugin1" in globals_mock
+    assert "SomePlugin2" in globals_mock
+
+
+def test_warning_multiple():
+    globals_mock = {}
+    with warnings.catch_warnings(record=True) as warn:
+        metadata = load_webviz_plugins_with_metadata(
+            [dist_mock1, dist_mock2], globals_mock
+        )
+
+        assert len(warn) == 1
+        assert issubclass(warn[-1].category, RuntimeWarning)
+        assert str(warn[-1].message) == (
+            "Multiple versions of plugin with name SomePlugin1. "
+            "Already loaded from project dist_mock1. "
+            "Overwriting using plugin with from project dist_mock2"
+        )
+
+    assert len(metadata) == 1, "Wrong number of items in metadata"
+    assert metadata["SomePlugin1"]["dist_name"] == "dist_mock2", "Wrong dist name"
+    assert "SomePlugin1" in globals_mock