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:
jayaddison: > * Use `set`-comparison of the reference fields.

Perhaps direct tuple-to-tuple comparison is better in this case; I'm checking that currently.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index dc885f9c3..d10b9f81e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,10 @@
+Release 7.4.4 (in development)
+==============================
+
+Bugs fixed
+----------
+
+
 Release 7.4.3 (released Jul 15, 2024)
 =====================================
 
diff --git a/doc/latex.rst b/doc/latex.rst
index 98081765c..821c83297 100644
--- a/doc/latex.rst
+++ b/doc/latex.rst
@@ -1304,14 +1304,24 @@ forget the underscore separating the prefix from the property names.
   | ``<prefix>_TeXcolor``.
     These are colors.
 
-  The shadow color defaults in all cases to ``{rgb}{0,0,0}`` i.e. to black.
+  Since 6.0.0 the border and background colors of :rst:dir:`code-block`,
+  default respectively to ``{RGB}{32,32,32}`` (i.e. ``{HTML}{202020}``), and
+  ``{RGB}{242,242,242}`` (i.e. ``{gray}{0.95}`` or ``{HTML}{F2F2F2}``).
 
-  Since 6.0.0 the border color and background color of :rst:dir:`code-block`,
-  i.e. ``pre`` prefix, default respectively to ``{RGB}{32,32,32}`` and
-  ``{gray}{0.95}``.  They previously defaulted to black, respectively white.
+  At 7.4.0 other directives acquire non-black/white default border and
+  background colors.  Here they are using ``xcolor`` hexadecimal notation
+  (which requires always 6 hexadecimal digits):
 
-  For all other types, the border color defaults to black and the background
-  color to white.
+  - ``{HTML}{F7F7F7}`` serves as background color to all.
+  - ``{HTML}{86989B}`` is border color of light admonitions (inclusive of
+    :rst:dir:`seealso` and :rst:dir:`todo`) as well as of :dudir:`topic` and
+    contents_ directives.
+  - ``{HTML}{940000}`` is border color or :dudir:`warning`-type admonitions,
+    except :dudir:`error` which uses ``{HTML}{B40000}``.
+
+  The only directives displaying a shadow per default are :dudir:`topic` and
+  contents_ (handled identically at LaTeX level) and their shadow color is
+  ``{HTML}{6C6C6C}``.  For all others the default shadow color is black.
 
   The ``<prefix>_TeXcolor`` stands for the CSS property "color", i.e. it
   influences the text color of the contents.  As for the three other options,
@@ -1330,7 +1340,6 @@ forget the underscore separating the prefix from the property names.
   start of the contents; for admonitions, this happens after the heading which
   reproduces the admonition type.
 
-
 The next keys, for admonitions only, were all three added at 7.4.0.  The
 default colors are the ones applying to the current HTML rendering of Sphinx
 own docs at https://www.sphinx-doc.org.
diff --git a/sphinx/__init__.py b/sphinx/__init__.py
index 69fc82803..9a3162951 100644
--- a/sphinx/__init__.py
+++ b/sphinx/__init__.py
@@ -1,6 +1,6 @@
 """The Sphinx documentation toolchain."""
 
-__version__ = '7.4.3'
+__version__ = '7.4.4'
 __display_version__ = __version__  # used for command line version
 
 # Keep this file executable as-is in Python 3!
@@ -27,11 +27,11 @@
 #:
 #: .. versionadded:: 1.2
 #:    Before version 1.2, check the string ``sphinx.__version__``.
-version_info = (7, 4, 3, 'final', 0)
+version_info = (7, 4, 4, 'beta', 0)
 
 package_dir = os.path.abspath(os.path.dirname(__file__))
 
-_in_development = False
+_in_development = True
 if _in_development:
     # Only import subprocess if needed
     import subprocess
diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py
index 55d7efd83..9065d31c9 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 889645903..95cf80a9b 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 0bdef9f67..d01785fda 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):