Repository: sphinx-doc/sphinx

Issue:
Warning about duplicate definitions in intersphinx mapping
### Describe the bug

We recently started getting warnings about duplicate definition in intersphinx, from two upstream projects:

- sklearn, see https://github.com/scikit-learn/scikit-learn/issues/29337
- ipywidgets, see https://github.com/jupyter-widgets/ipywidgets/issues/3930

Sklearn maintainers suspect this to be a sphinx bug, so I am opening this issue here.

### How to Reproduce
```
$ pip list | grep  Sphinx
Sphinx                        7.4.2
$ python -m sphinx.ext.intersphinx https://scikit-learn.org/stable/objects.inv | grep "    y "
WARNING:sphinx.sphinx.util.inventory:inventory <> contains multiple definitions for std:term:y
    y        
```

and
```
$ python -m sphinx.ext.intersphinx https://ipywidgets.readthedocs.io/en/stable/objects.inv | grep 'Widget Layout.ipynb#display'
WARNING:sphinx.sphinx.util.inventory:inventory <> contains multiple definitions for std:label:examples/Widget Layout.ipynb#display
    examples/Widget Layout.ipynb#display     display                                 : examples/Widget%20Layout.html#id1
```

### Environment Information

```text
Sphinx 7.4.2
```


### Sphinx extensions

```python
intersphinx
```


### Additional context

_No response_


Discussion:
The warning seems to happen in sphinx 7.4.0 and not 7.3.7, not sure if this is because our (scikit-learn) `objects.inv` has always been problematic somehow and sphinx 7.4.0 only started warning about it ...

In case there is something we can do on the scikit-learn side, let us know ...
Thank you for opening this Max (@maxnoe). This was introduced in 799ae16 (#12329). You can suppress it with ``'intersphinx.external'`` in your ``suppress_warnings``, though the warning is alerting you to that ``intersphinx`` has encountered an ambiguity in resolution.

The issue with *sklearn* is that you have both ``:term:`y` `` and ``:term:`Y` `` defined in the glossary:

https://github.com/scikit-learn/scikit-learn/blob/d79cb58c464f0b54bf0f0286c725d2df837574d0/doc/glossary.rst?plain=1#L1891-L1898

The issue with *ipywidgets* is that the page has two "Display" headings, one capitalised and one uncapitalised (this is mainly an educated guess as it also depends on how the ipynb file is parsed in to Sphinx):

https://github.com/jupyter-widgets/ipywidgets/blob/main/docs/source/examples/Widget%20Layout.ipynb

A
Author of this added warning logic here; apologies for the distraction caused.

I think that the `Y` / `y` case in `scikit` seems like a false-positive, because the duplicate definitions both refer to the same entity -- yes, the resolution is ambiguous, but the resolution result is the same in both cases.

The `ipywidgets` case also seems somewhat noisy to warn about.. however there are genuinely two different hyperlink definitions that can be resolved-to in that case (`examples/Widget%20Layout.html#id1 display` and `examples/Widget%20Layout.html#display Display`).

I'll spend some time to figure out whether we can filter out the first case without affecting the second.
Note: although not (yet?) reported here, I think these cases would also emit warnings during some Intersphinx refeerence resolution code: https://github.com/sphinx-doc/sphinx/blob/c4a7f5bb76bdcf5e5d72654a622c9991d78ed354/sphinx/ext/intersphinx/_resolve.py#L83-L87
[Sphinx 7.4.4](https://pypi.org/project/Sphinx/7.4.4/) has been released with a fix.

A



Patch:
diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py
index 55d7efd8396..9065d31c971 100644
--- a/sphinx/util/inventory.py
+++ b/sphinx/util/inventory.py
@@ -126,7 +126,8 @@ def load_v2(
         invdata: Inventory = {}
         projname = stream.readline().rstrip()[11:]
         version = stream.readline().rstrip()[11:]
-        potential_ambiguities = set()
+        # definition -> priority, location, display name
+        potential_ambiguities: dict[str, tuple[str, str, str]] = {}
         actual_ambiguities = set()
         line = stream.readline()
         if 'zlib' not in line:
@@ -155,10 +156,16 @@ def load_v2(
                 # * 'term': https://github.com/sphinx-doc/sphinx/issues/9291
                 # * 'label': https://github.com/sphinx-doc/sphinx/issues/12008
                 definition = f"{type}:{name}"
-                if definition.lower() in potential_ambiguities:
-                    actual_ambiguities.add(definition)
+                content = prio, location, dispname
+                lowercase_definition = definition.lower()
+                if lowercase_definition in potential_ambiguities:
+                    if potential_ambiguities[lowercase_definition] != content:
+                        actual_ambiguities.add(definition)
+                    else:
+                        logger.debug(__("inventory <%s> contains duplicate definitions of %s"),
+                                     uri, definition, type='intersphinx',  subtype='external')
                 else:
-                    potential_ambiguities.add(definition.lower())
+                    potential_ambiguities[lowercase_definition] = content
             if location.endswith('$'):
                 location = location[:-1] + name
             location = join(uri, location)


Test patch:
diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py
index 889645903dd..95cf80a9b39 100644
--- a/tests/test_util/intersphinx_data.py
+++ b/tests/test_util/intersphinx_data.py
@@ -59,4 +59,6 @@
 ''' + zlib.compress(b'''\
 a term std:term -1 glossary.html#term-a-term -
 A term std:term -1 glossary.html#term-a-term -
+b term std:term -1 document.html#id5 -
+B term std:term -1 document.html#B -
 ''')
diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py
index 0bdef9f67d9..d01785fda24 100644
--- a/tests/test_util/test_util_inventory.py
+++ b/tests/test_util/test_util_inventory.py
@@ -53,7 +53,8 @@ def test_ambiguous_definition_warning(warning):
     f = BytesIO(INVENTORY_V2_AMBIGUOUS_TERMS)
     InventoryFile.load(f, '/util', posixpath.join)
 
-    assert 'contains multiple definitions for std:term:a' in warning.getvalue().lower()
+    assert 'contains multiple definitions for std:term:a' not in warning.getvalue().lower()
+    assert 'contains multiple definitions for std:term:b' in warning.getvalue().lower()
 
 
 def _write_appconfig(dir, language, prefix=None):