Repository: matplotlib/matplotlib

Issue:
[Bug]: Backend entry points can be erroneously duplicated
### Summary

Under certain circumstances outside of our control, an `entry_points`-registering backend such as `matplotlib-inline` can appear as two entry points so that we raise a `RuntimeError(f"Entry point name '{name}' duplicated")`. Reported on [Matplotlib Discourse](https://discourse.matplotlib.org/t/latest-versions-via-pip-jupyterlab-import-of-matplotlib-broken/24477/6).

### Proposed fix

The `BackendRegistry` purposefully does not allow multiple entry points with the same name as it has no way to know which one to prefer. This has now caused a problem in the linked discussion when using system python 3.9 and a virtual environment on Rocky 9.4 Linux. Although there is only a single `entry_points.txt` file for e.g. `matplotlib-inline` the system python `importlib.metadata.entry_points` erroneously returns two identical items. I think this is because the virtualenv has both a `lib` directory and a `lib64` symlink to the `lib` directory, and this particular combination of Linux distribution, python version and use of `venv` does not check that they both refer to the same file. Regardless of where the problem really lies here, Matplotlib needs to be able to deal with it.

I'll submit a PR later in the week. I think the simplest change is to allow (i.e. discard) entry points that have the same `group` (e.g. `matplotlib.backend`), `name` (e.g. `inline`) and `value` (e.g. `matplotlib_inline.backend_inline`), and then raise if there are multiple entry points of the same `group` and `name` but different `value`.


Discussion:

Patch:
diff --git a/lib/matplotlib/backends/registry.py b/lib/matplotlib/backends/registry.py
index 19b4cba254ab..47d5f65e350e 100644
--- a/lib/matplotlib/backends/registry.py
+++ b/lib/matplotlib/backends/registry.py
@@ -168,8 +168,11 @@ def backward_compatible_entry_points(
     def _validate_and_store_entry_points(self, entries):
         # Validate and store entry points so that they can be used via matplotlib.use()
         # in the normal manner. Entry point names cannot be of module:// format, cannot
-        # shadow a built-in backend name, and cannot be duplicated.
-        for name, module in entries:
+        # shadow a built-in backend name, and there cannot be multiple entry points
+        # with the same name but different modules. Multiple entry points with the same
+        # name and value are permitted (it can sometimes happen outside of our control,
+        # see https://github.com/matplotlib/matplotlib/issues/28367).
+        for name, module in set(entries):
             name = name.lower()
             if name.startswith("module://"):
                 raise RuntimeError(


Test patch:
diff --git a/lib/matplotlib/tests/test_backend_registry.py b/lib/matplotlib/tests/test_backend_registry.py
index eaf8417e7a5f..141ffd69c266 100644
--- a/lib/matplotlib/tests/test_backend_registry.py
+++ b/lib/matplotlib/tests/test_backend_registry.py
@@ -121,6 +121,17 @@ def test_entry_point_name_duplicate(clear_backend_registry):
             [('some_name', 'module1'), ('some_name', 'module2')])
 
 
+def test_entry_point_identical(clear_backend_registry):
+    # Issue https://github.com/matplotlib/matplotlib/issues/28367
+    # Multiple entry points with the same name and value (value is the module)
+    # are acceptable.
+    n = len(backend_registry._name_to_module)
+    backend_registry._validate_and_store_entry_points(
+        [('some_name', 'some.module'), ('some_name', 'some.module')])
+    assert len(backend_registry._name_to_module) == n+1
+    assert backend_registry._name_to_module['some_name'] == 'module://some.module'
+
+
 def test_entry_point_name_is_module(clear_backend_registry):
     with pytest.raises(RuntimeError):
         backend_registry._validate_and_store_entry_points(