Repository: sphinx-doc/sphinx

Issue:
multiple definitions for std:label:python--m-build--v
### Describe the bug

Similar to #12585, but apparently different, when attempting to build setuptools docs, a warning (treated as error) is emitted:

```
WARNING: inventory <https://build.pypa.io/en/latest> contains multiple definitions for std:label:python--m-build--v
```

Originally reported in https://github.com/pypa/setuptools/issues/4474 and https://github.com/pypa/build/issues/795, I've traced the issue to Sphinx 7.4 (7.4.2, 7.4.4, and 7.4.5). Downgrading to Sphinx 7.3.7 bypasses the issue.

The warning can also be seen by simply downloading the inventory and parsing it with sphinx.

The label of concern exists twice and varies only by case, which is a meaningful distinction in HTML labels. The labels exist because the build project uses sphinx-argparse-cli and exposes command line parameters that vary only by case.

### How to Reproduce

```
 🐚 https build.pypa.io/en/stable/objects.inv > build.inv
 🐚 .tox/docs/bin/python -m sphinx.ext.intersphinx build.inv
WARNING:sphinx.sphinx.util.inventory:inventory <> contains multiple definitions for std:label:python--m-build--v
py:class
    build.ProjectBuilder                                                             : api.html#build.ProjectBuilder
    build._builder.ProjectBuilder                                                    : api.html#build.ProjectBuilder
    build.env.DefaultIsolatedEnv                                                     : api.html#build.env.DefaultIsolatedEnv
    build.env.IsolatedEnv                                                            : api.html#build.env.IsolatedEnv
py:exception
    build.BuildBackendException                                                      : api.html#build.BuildBackendException
    build.BuildException                                                             : api.html#build.BuildException
    build.BuildSystemTableValidationError                                            : api.html#build.BuildSystemTableValidationError
    build.FailedProcessError                                                         : api.html#build.FailedProcessError
    build.TypoWarning                                                                : api.html#build.TypoWarning
    build._exceptions.BuildBackendException                                          : api.html#build.BuildBackendException
    build._exceptions.BuildException                                                 : api.html#build.BuildException
    build._exceptions.BuildSystemTableValidationError                                         : api.html#build.BuildSystemTableValidationError
    build._exceptions.FailedProcessError                                             : api.html#build.FailedProcessError
    build._exceptions.TypoWarning                                                    : api.html#build.TypoWarning
py:function
    build.check_dependency                                                           : api.html#build.check_dependency
    build.util.project_wheel_metadata                                                : api.html#build.util.project_wheel_metadata
py:method
    build.ProjectBuilder.build                                                       : api.html#build.ProjectBuilder.build
    build.ProjectBuilder.check_dependencies                                          : api.html#build.ProjectBuilder.check_dependencies
    build.ProjectBuilder.from_isolated_env                                           : api.html#build.ProjectBuilder.from_isolated_env
    build.ProjectBuilder.get_requires_for_build                                         : api.html#build.ProjectBuilder.get_requires_for_build
    build.ProjectBuilder.metadata_path                                               : api.html#build.ProjectBuilder.metadata_path
    build.ProjectBuilder.prepare                                                     : api.html#build.ProjectBuilder.prepare
    build.env.DefaultIsolatedEnv.install                                             : api.html#build.env.DefaultIsolatedEnv.install
    build.env.DefaultIsolatedEnv.make_extra_environ                                         : api.html#build.env.DefaultIsolatedEnv.make_extra_environ
    build.env.IsolatedEnv.make_extra_environ                                         : api.html#build.env.IsolatedEnv.make_extra_environ
py:module
    build                                                                            : api.html#module-build
    build.env                                                                        : api.html#module-build.env
    build.util                                                                       : api.html#module-build.util
py:property
    build.ProjectBuilder.build_system_requires                                         : api.html#build.ProjectBuilder.build_system_requires
    build.ProjectBuilder.python_executable                                           : api.html#build.ProjectBuilder.python_executable
    build.ProjectBuilder.source_dir                                                  : api.html#build.ProjectBuilder.source_dir
    build.env.DefaultIsolatedEnv.path                                                : api.html#build.env.DefaultIsolatedEnv.path
    build.env.DefaultIsolatedEnv.python_executable                                         : api.html#build.env.DefaultIsolatedEnv.python_executable
    build.env.IsolatedEnv.python_executable                                          : api.html#build.env.IsolatedEnv.python_executable
std:doc
    api                                      API Documentation                       : api.html
    changelog                                Changelog                               : changelog.html
    differences                              Differences from other tools            : differences.html
    index                                    build                                   : index.html
    installation                             Installation                            : installation.html
    mission                                  Mission Statement                       : mission.html
    release                                  Release Process                         : release.html
    test_suite                               Test Suite                              : test_suite.html
std:label
    genindex                                 Index                                   : genindex.html
    modindex                                 Module Index                            : py-modindex.html
    py-modindex                              Python Module Index                     : py-modindex.html
    python--m-build---config-setting         python -m build --config-setting        : index.html#python--m-build---config-setting
    python--m-build---help                   python -m build --help                  : index.html#python--m-build---help
    python--m-build---installer              python -m build --installer             : index.html#python--m-build---installer
    python--m-build---no-isolation           python -m build --no-isolation          : index.html#python--m-build---no-isolation
    python--m-build---outdir                 python -m build --outdir                : index.html#python--m-build---outdir
    python--m-build---sdist                  python -m build --sdist                 : index.html#python--m-build---sdist
    python--m-build---skip-dependency-check  python -m build --skip-dependency-check : index.html#python--m-build---skip-dependency-check
    python--m-build---verbose                python -m build --verbose               : index.html#python--m-build---verbose
    python--m-build---version                python -m build --version               : index.html#python--m-build---version
    python--m-build---wheel                  python -m build --wheel                 : index.html#python--m-build---wheel
    python--m-build--C                       python -m build -C                      : index.html#python--m-build--C
    python--m-build--V                       python -m build -V                      : index.html#python--m-build--V
    python--m-build--h                       python -m build -h                      : index.html#python--m-build--h
    python--m-build--n                       python -m build -n                      : index.html#python--m-build--n
    python--m-build--o                       python -m build -o                      : index.html#python--m-build--o
    python--m-build--s                       python -m build -s                      : index.html#python--m-build--s
    python--m-build--v                       python -m build -v                      : index.html#python--m-build--v
    python--m-build--w                       python -m build -w                      : index.html#python--m-build--w
    python--m-build--x                       python -m build -x                      : index.html#python--m-build--x
    python--m-build-options                  python -m options                       : index.html#python--m-build-options
    python--m-build-positional-arguments     python -m positional arguments          : index.html#python--m-build-positional-arguments
    python--m-build-srcdir                   python -m build srcdir                  : index.html#python--m-build-srcdir
    search                                   Search Page                             : search.html
```

### Environment Information

```text
Platform:              darwin; (macOS-14.5-arm64-arm-64bit)
Python version:        3.12.4 (main, Jun  6 2024, 18:26:44) [Clang 15.0.0 (clang-1500.3.9.4)])
Python implementation: CPython
Sphinx version:        7.4.4
Docutils version:      0.21.2
Jinja2 version:        3.1.4
Pygments version:      2.18.0
```


### Sphinx extensions

```python
n/a
```


### Additional context

_No response_


Discussion:
Note - it would have been nice if `sphinx.ext.intersphinx` would accept `-` as a parameter (indicating use stdin for input) as that would have allowed the repro to fit on one line and avoid writing state to disk.
> Note - it would have been nice if `sphinx.ext.intersphinx` would accept `-` as a parameter (indicating use stdin for input) as that would have allowed the repro to fit on one line and avoid writing state to disk.

@jaraco it accepts URLs as an argument, not just files on disk. So you can still have a one-liner:
```
$ python -m sphinx.ext.intersphinx https://build.pypa.io/en/stable/objects.inv
```
Thanks @jaraco - a quick acknowledgement here that I've begun reading this bugreport and am investigating.
I think that we are going to have to reduce the severity level of this warning when loading `objects.inv` files.  There are popular projects that contain ambiguity in those inventory files, and because we shouldn't break existing external references that point to them, they are probably going to need to remain in place for a while.
@jayaddison you could consider making it an INFO logging message for now. We should, though, look at adding some logic in ``note_term`` or whatever that emits a ``RemovedInSphinxXXWarning`` and avoids duplicates -- extension developers should take mitigating action before it affects users (e.g. the issue here is with sphinx-argparse-cli, which generates duplicate labels). 

A
Thanks @AA-Turner - agreed.  Let's reduce the log severity for now so that this does not fail strict project builds.

In terms of resolving the cause of the ambiguous entries: unless you or @jaraco already plan to file an upstream bugreport with `sphinx-argparse-cli` yourself, I'll do that soon and may investigate a fix while doing so.

I'm not too familiar with `note_term` and how things work on the Sphinx side, so I'd appreciate any help there, but will also get around to that after the `sphinx-argparse-cli` bugreport if it's not picked up by someone else by then.
One subtlety I anticipate about the `sphinx-argparse-cli` bugreport is that I don't think it would be sensible to entirely omit the existing short-name argument entries that it produces.  For many utilities I expect there'll be no ambiguity, and arguments from the resulting documentation could be referenced (unambiguously) from elsewhere -- it wouldn't be great to break those links (perhaps that fits into the deprecation process somehow).
I'd suggest that they preference generating a long-option target where possible, and if not then fall back to the short-option name. E.g. ``-V`` would link to ``#<prefix>--version``. But this is getting off topic...

A
After reading the `sphinx-argparse-cli` code, and perhaps catching up with previous investigation: I think I'm mistaken about the existence of a bug there.  It expand each argparse action into _all_ configured option names -- and so there _are_ both `v` and `verbose`, and also `V` and `version` entries in `build::objects.inv` (as is in fact visible in the output kindly included in the bugreport).

In other words: it does also produce unambiguous option names, and those could and should be preferred instead for use as reference targets.
@jayaddison @AA-Turner 

If we are to emit a warning at all, would it not be better for the warning to be emitted at  "source", i.e. during the creation of the `objects.inv`, rather than when reading it, e.g. here: https://github.com/sphinx-doc/sphinx/blob/174348677617c31008cbd504c608cb2c5fd66f36/sphinx/util/inventory.py#L180

(or something domain specific)

Obviously the problem with warning when reading the `objects.inv` is that there is not much you can do about it, because usually you are not the one who created it.


I think that makes sense @chrisjsewell, yep.  I think I've slightly overloaded myself at the moment, but I should be able to get around to that within the next week or so.

For completeness I'll mention that we do have a second warning location currently: when a reference target to an external project resolves ambiguously.  I hope that's useful too, because informing people about creation of potentially-mislocated hyperlinks seems important to me.
> Thanks @AA-Turner - agreed. Let's reduce the log severity for now so that this does not fail strict project builds.
> 
> In terms of resolving the cause of the ambiguous entries: unless you or @jaraco already plan to file an upstream bugreport with `sphinx-argparse-cli` yourself, I'll do that soon and may investigate a fix while doing so.
> 
> I'm not too familiar with `note_term` and how things work on the Sphinx side, so I'd appreciate any help there, but will also get around to that after the `sphinx-argparse-cli` bugreport if it's not picked up by someone else by then.

Please feel free to report the issue to `sphinx-argparse-cli`, as you'll be better positioned to explain _why_ the behavior needs to change. I haven't read enough on the recent changes to understand why Sphinx is complaining about this ambiguity at all. My understanding is that HTML considers these labels case-sensitive, so there is no ambiguity. Is sphinx warning about this condition because ambiguity arises when outputting to other formats?

> In other words: it does also produce unambiguous option names, and those could and should be preferred instead for use as reference targets.

I'm not sure this condition is true in general. Yes, it's true for `build` because they provide long-form options for each possibly ambiguous argument, but there are other CLI interfaces that only expose short-form arguments, where the ambiguity could not be resolved by simply relying on the long form. If the labels need to resolve unambiguously and case-insensitively, `sphinx-argparse-cli` may need to devise another encoding for generating these labels.



Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index a77bb761d79..02cdd53ad31 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -7,6 +7,8 @@ Bugs fixed
 * #12859, #9743, #12609: autosummary: Do not add the package prefix when
   generating autosummary directives for modules within a package.
   Patch by Adam Turner.
+* #12613: Reduce log severity for ambiguity detection during inventory loading.
+  Patch by James Addison.
 
 Release 7.4.5 (released Jul 16, 2024)
 =====================================
diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py
index 9065d31c971..bb0eca6a5cb 100644
--- a/sphinx/util/inventory.py
+++ b/sphinx/util/inventory.py
@@ -172,8 +172,8 @@ def load_v2(
             inv_item: InventoryItem = projname, version, location, dispname
             invdata.setdefault(type, {})[name] = inv_item
         for ambiguity in actual_ambiguities:
-            logger.warning(__("inventory <%s> contains multiple definitions for %s"),
-                           uri, ambiguity, type='intersphinx',  subtype='external')
+            logger.info(__("inventory <%s> contains multiple definitions for %s"),
+                        uri, ambiguity, type='intersphinx',  subtype='external')
         return invdata
 
     @classmethod


Test patch:
diff --git a/tests/test_util/test_util_inventory.py b/tests/test_util/test_util_inventory.py
index d01785fda24..211dc17e7a6 100644
--- a/tests/test_util/test_util_inventory.py
+++ b/tests/test_util/test_util_inventory.py
@@ -49,12 +49,22 @@ def test_read_inventory_v2_not_having_version():
         ('foo', '', '/util/foo.html#module-module1', 'Long Module desc')
 
 
-def test_ambiguous_definition_warning(warning):
+def test_ambiguous_definition_warning(warning, status):
     f = BytesIO(INVENTORY_V2_AMBIGUOUS_TERMS)
     InventoryFile.load(f, '/util', posixpath.join)
 
-    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 _multiple_defs_notice_for(entity: str) -> str:
+        return f'contains multiple definitions for {entity}'
+
+    # was warning-level; reduced to info-level - see https://github.com/sphinx-doc/sphinx/issues/12613
+    mult_defs_a, mult_defs_b = (
+        _multiple_defs_notice_for('std:term:a'),
+        _multiple_defs_notice_for('std:term:b'),
+    )
+    assert mult_defs_a not in warning.getvalue().lower()
+    assert mult_defs_a not in status.getvalue().lower()
+    assert mult_defs_b not in warning.getvalue().lower()
+    assert mult_defs_b in status.getvalue().lower()
 
 
 def _write_appconfig(dir, language, prefix=None):