Repository navigation
Description
Describe the bug
Hi,
Since Sphinx 4.0 we see "duplicate object description" warnings in Astropy and other related projects that are using our sphinx-automodapi extension. For example : astropy/astropy#11723
It took some time to report the issue because we were not sure where the issue comes from (Sphinx or sphinx-automodapi). But our recent investigations seem to show that this could be an issue in Sphinx (astropy/sphinx-automodapi#130).
#9121 and #9128 added the duplication warning and the :canonical: metadata, which seems to be applied correctly by autodoc, but it seems we still get the warning when there are more than one duplicate.
With this minimal project to reproduce the issue (https://github.com/maxnoe/sphinx-automodapi-duplicated-warning), we have a first duplicate for duplicated.Foo:
.. py:class:: Foo()
:module: duplicated
:canonical: duplicated.foo.foo.Foo
Bases: :py:class:`object`
Awesome Foo class
And another one for duplicated.foo.Foo which is the ones triggering the warning:
.. py:class:: Foo()
:module: duplicated.foo
:canonical: duplicated.foo.foo.Foo
Bases: :py:class:`object`
Awesome Foo class
In
sphinx/sphinx/domains/python.py
Lines 1218 to 1237 in b4276ed
| def note_object(self, name: str, objtype: str, node_id: str, | |
| aliased: bool = False, location: Any = None) -> None: | |
| """Note a python object for cross reference. | |
| .. versionadded:: 2.1 | |
| """ | |
| if name in self.objects: | |
| other = self.objects[name] | |
| if other.aliased and aliased is False: | |
| # The original definition found. Override it! | |
| pass | |
| elif other.aliased is False and aliased: | |
| # The original definition is already registered. | |
| return | |
| else: | |
| # duplicated | |
| logger.warning(__('duplicate object description of %s, ' | |
| 'other instance in %s, use :noindex: for one of them'), | |
| name, other.docname, location=location) | |
| self.objects[name] = ObjectEntry(self.env.docname, node_id, objtype, aliased) |
(Pdb++) pp self.objects
{'duplicated.Foo': ObjectEntry(docname='api/duplicated.Foo', node_id='duplicated.Foo', objtype='class', aliased=False),
'duplicated.foo.Foo': ObjectEntry(docname='api/duplicated.foo.Foo', node_id='duplicated.foo.Foo', objtype='class', aliased=False),
'duplicated.foo.foo.Foo': ObjectEntry(docname='api/duplicated.Foo', node_id='duplicated.Foo', objtype='class', aliased=True)}
So is this an issue in Sphinx, in the treatment of multiple duplicates, or there something else we are missing (and we could fix in sphinx-automodapi) ?
How to Reproduce
$ git clone https://github.com/maxnoe/sphinx-automodapi-duplicated-warning.git
$ cd sphinx-automodapi-duplicated-warning
$ pip install .
$ cd docs
$ make html
Expected behavior
No response
Your project
https://github.com/astropy/astropy
Screenshots
No response
OS
Linux
Python version
3.8
Sphinx version
4.0
Sphinx extensions
Extra tools
No response
Additional context
No response
Activity
cblegare commentedon Apr 15, 2022
Hello @saimn !
I am not a sphinx dev per se, only trying to help.
I ran the example. Here are calls to
sphinx.domains.python.PythonDomain.note_objectfor classes (I skipped modules for brevity).duplicated.Fooduplicated.FooFalseapi/duplicated.Fooduplicated.foo.foo.Foo(canonical name)duplicated.FooTrueapi/duplicated.Fooduplicated.foo.Fooduplicated.foo.FooFalseapi/duplicated.foo.Fooduplicated.foo.foo.Foo(canonical name)duplicated.foo.FooTrueapi/duplicated.foo.FooFrom my understanding, by using explicitely
.. automodapi::twice with two different modules but that generates documentation for the same class, without further configuration autodoc will emit.. py:class::with the:canonical:option set to the actual real location for the class. Both are aliases, so they generate the warning.Your goal is to document one "nice API module having definitions that are actually defined in different deeper modules". Sphinx does this well and I see no bug here.
The example you provided (thank you a lot for this very nice issue by the way), exposes two "nice API module having the same definitions in deeper modules". While I think that this might be a rather exotic use-case, I do not find it wrong in itself.
In case @tk0miya or an other maintainer believe that having multiple "nice API module having the same definitions in deeper modules" is a valid use-case that Sphinx should support, keying the
ObjectEntrybynode_idinstead of bynamein thesphinx.domains.python.PythonDomain.objectsdictionary could be a part of the solution. That would also possibly require to review slightly the xref resolution algorithm.Conclusion, in my opinion Your use-case is quite exotic, but not completely invalid. Sphinx behave as expected (I think) but it looks like it would be doable to adapt the Python domain to also cover your use-case.
Appendix: about the "canonical" feature:
Appendix: content of generated RestructuredText files by
sphinx_automodapiapi/duplicated.Foo.rst:api/duplicated.foo.Foo.rst:saimn commentedon Apr 15, 2022
Hi @cblegare,
Thanks for the helpful answer!
Ok so this explains that. It may not be a widespread usage but I think there are good reasons to do this. Typically we have quite crowded top-level namespaces (
astropy.coordinatesfor example, https://docs.astropy.org/en/latest/coordinates/#reference-api) and we want document a subset of the functions/classes in a specific doc section, e.g. https://docs.astropy.org/en/latest/coordinates/#built-in-frame-classesAnother example is
astropy.units(https://docs.astropy.org/en/latest/units/#module-astropy.units) for which various sub-packages are documented in specific sections. And interestingly for this one, it seems only the sub-sub-packages are raising the duplication warnings (astropy.units.function.logarithmicfor example, https://docs.astropy.org/en/latest/units/#module-astropy.units.function.logarithmic), whereas the sub-packages (astropy.units.function,astropy.units.quantityetc.) are not causing warnings.cblegare commentedon Apr 16, 2022
Well, that is indeed a very rich API!
This is because when you define (again!) you API in intermediate namespaces, you give your objects new names. But their canonical name (that is the real fully qualified importable path where the object is actually defined in the codebase) is the same as the top-level crowded namespace. The canonical names are colliding (more on this in the Remark below).
I see four concerns here
In the list above, I didn't add "enable the user to import a given object from the convenient crowded namespace, the actual deep definition of the object and a few more places in-between". I don't think you really want that. But by the way you are using
sphinx_automodapi, you are indeed exposing your API at multiple levels in addition to the convenient one and the "real" canonical one. This might be induced by the fact that the way you usesphinx_automodapimixes together two side-effects: give an holistic view and expose the API in a convenient but crowded namespace. Sometimes you want to have a more local, or narrow, but still holistic, view of a given region of the API (say... units). That does not mean you want to also expose these APIs in yet another namespace. The screenshot below from you general index shows that LogUnit is advertised as exposed in three (3) different namespaces.While Sphinx supports having both a "real" name (canonical, deep where the definition is in the code) and one alias (usually higher in a crowded namespace), it does not support (yet?) having multiple aliases. While I completely understand your hunger to have a fine control on how to structure documentation with varied levels of granularity, I am not sure that advertising the same API in more than two (2) namespaces (the canonical and the convenient one) is desirable.
Given the example above, I feel you want to (and Sphinx supports to)
astropy.unit.LogUnit)astropy.unit.function.logarithmic.LogUnit)To enable this, I suggest to pick one from three solutions
Change the way you use
sphinx_automodapi. The issue you have is exactly the same that made me stop using apidoc: the need to narrow some API documentation within a consistent context. Nowadays I write a bit more Sphinx directives manually (less automatically), but it pays off in the end.Enable
sphinx_automodapito add the:noindex:option sometimes. This way, you can still use the extension but the newly documented objects are not added to the index, thus not exactly cross-referenceable and would not trigger any warning.Find a way to change the Python domain to make this work and submit a PR But this would be a backward incompatible change, if even possible to implement without breaking badly many documentation out there.
On this matter, I retract my comment above that it seems it would be doable: I now think it barely look feasible with important drawbacks. This would probably involve to find a way to tell Sphinx that a given canonical name associated with multiple names knows about a primary or a default name to pick when resolving xrefs.
saimn commentedon Apr 19, 2022
Thanks @cblegare for the really clear and detailed explanation, now things make much more sense :). We will discuss that with the other Astropy maintainers and see what option we can choose.