sphinx-doc / sphinx-doc/sphinx

`:py:deco:` and `:py:const:` roles resolve locally but never through intersphinx

Open
#14,564 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

domains:py extensions:intersphinx type:bug
Dominant language
Python
Stars
8k
Forks
2.6k
PR merge metrics
No merged PRs in 30d

Description

Describe the bug

[!NOTE]
This bug has been reported and investigates by Claude.ai and reviewed by @Paebbels.
Besides a description, it also has a root cause analysis and a tested fix.

PythonDomain registers a deco role, but no entry in PythonDomain.object_types claims it.
The domain builds its role → object-type map exclusively from object_types, so
objtypes_for_role('deco') returns None. Reference resolution then behaves inconsistently
depending on which code path runs.

const has exactly the same defect and is much older.

Prior art (checked existing issues)

Issue State Relevance
#13105 closed Feature request that added :py:deco:, implemented by PR #13292, milestone 8.2.0
#13528 closed deco role didn't support the ~ modifier — an earlier follow-up defect from the same PR
#7243 Background: all py:* roles resolve without checking objtypes in exact-match mode. This is why the bug is invisible in the common case

A search of the tracker for deco intersphinx, py:deco and objtypes_for_role returns nothing covering this.
#13528 is worth citing in the report: it establishes that PR #13292 shipped incomplete once already.

Observed behavior

Reference Result
:deco:m.dec`` — exact name, same project resolves
:deco:.dec`` — refspecific, same project does not resolve
:deco:m.dec`` — via intersphinx does not resolve
:func:m.dec`` — via intersphinx resolves
:const:m.CONST`` — via intersphinx does not resolve
:data:m.CONST`` — via intersphinx resolves

Both failing cases warn only under nitpicky = True. Without it, the reference silently renders
as plain text with no link. That's what makes this expensive in practice: a project that adopts
:deco: for its own decorators looks fine locally, and every downstream project that intersphinxes
into it gets dead text instead of links, with no diagnostic on either side.

Root cause

Three pieces, all in Sphinx 9.1.0.

1. The role is registered; no object type claims it.

sphinx/domains/python/__init__.py:724

object_types = {
    'function': ObjType(_('function'), 'func', 'obj'),          # <- no 'deco'
    'data':     ObjType(_('data'),     'data', 'obj'),          # <- no 'const'
    ...
    'method':   ObjType(_('method'),   'meth', 'obj'),          # <- no 'deco'
    'property': ObjType(_('property'), 'attr', '_prop', 'obj'), # <- '_prop' IS claimed
    ...
}

sphinx/domains/python/__init__.py:759

roles = {
    ...
    'func':  PyXRefRole(fix_parens=True),
    'deco':  _PyDecoXRefRole(),      # added by PR #13292
    ...
    'const': PyXRefRole(),
}

The property entry shows the intended pattern: its private _prop role is listed in the
ObjType. deco and const were never added.

2. The lookup map is built only from object_types.

sphinx/domains/__init__.py:129-134

for name, obj in self.object_types.items():
    for rolename in obj.roles:
        self._role2type.setdefault(rolename, []).append(name)
    self._type2role[name] = obj.roles[0] if obj.roles else ''
self.objtypes_for_role = self._role2type.get

self._role2type.get('deco')None.

3. Two consumers react differently to that None.

sphinx/domains/python/__init__.py:876find_obj, searchmode == 1 (refspecific):

if searchmode == 1:
    if type is None:
        objtypes = list(self.object_types)
    else:
        objtypes = self.objtypes_for_role(type)    # -> None
    if objtypes is not None:                        # -> skipped entirely
        ...

sphinx/domains/python/__init__.py:910find_obj, searchmode == 0:

# NOTE: searching for exact match, object type is not considered

This is why plain local references work — the object type is never consulted. It masks the bug in
the most common case.

sphinx/ext/intersphinx/_resolve.py:241:

objtypes = domain.objtypes_for_role(typ) or ()
if not objtypes:
    return None

No candidate types, so intersphinx gives up before searching the inventory.

Why :func: works and :deco: doesn't

py:decorator does not create a distinct object type. Dumping the inventory of a project that uses
.. py:decorator:: dec shows it registered as a function:

mymod.mydeco  py:function  1  index.html#$  -
mymod.myfunc  py:function  1  index.html#$  -
mymod.MyClass py:class     1  index.html#$  -

So the target is a function, :func: claims function, and :deco: claims nothing.

How to Reproduce

Two minimal projects, the second intersphinxing into the first.

Project A (conf.py: project = "inv_a")

A
=

.. py:module:: m

.. py:decorator:: dec

.. py:function:: fn

.. py:data:: CONST

Project B

# conf.py
project = "inv_b"
extensions = ["sphinx.ext.intersphinx"]
intersphinx_mapping = {"a": ("http://example.invalid/", "../inv_a/_build/objects.inv")}
nitpicky = True
B
=

* r-deco:  :deco:`m.dec`
* r-func:  :func:`m.dec`
* r-const: :const:`m.CONST`
* r-data:  :data:`m.CONST`

Build A, then B:

WARNING: py:deco  reference target not found: m.dec    [ref.deco]
WARNING: py:const reference target not found: m.CONST  [ref.const]

  r-deco   NOT LINKED
  r-func   LINKED
  r-const  NOT LINKED
  r-data   LINKED

The refspecific variant needs only one project:

.. py:module:: m
.. py:decorator:: dec
.. py:data:: CONST

* :deco:`.dec`      -> NOT LINKED, WARNING py:deco reference target not found: dec
* :func:`.dec`      -> LINKED
* :const:`.CONST`   -> NOT LINKED, WARNING py:const reference target not found: CONST
* :data:`.CONST`    -> LINKED
* :deco:`m.dec`     -> LINKED   (exact match, objtype ignored)
Environment Information
Sphinx:   9.1.0
Python:   3.14
OS:       Debian Trixie


Affected since **8.2.0** for `deco` (when PR #13292 landed). `const` predates that — it has been in
`roles` without a matching `ObjType` for far longer, and is worth mentioning in the same report so
both get fixed together.
Sphinx extensions
intersphinx
Additional context

Suggested fix

Claim the roles from the object types that actually back them, mirroring property/_prop:

 object_types = {
-    'function':     ObjType(_('function'), 'func', 'obj'),
-    'data':         ObjType(_('data'), 'data', 'obj'),
+    'function':     ObjType(_('function'), 'func', 'deco', 'obj'),
+    'data':         ObjType(_('data'), 'data', 'const', 'obj'),
     'class':        ObjType(_('class'), 'class', 'exc', 'obj'),
     'exception':    ObjType(_('exception'), 'exc', 'class', 'obj'),
-    'method':       ObjType(_('method'), 'meth', 'obj'),
+    'method':       ObjType(_('method'), 'meth', 'deco', 'obj'),
     'classmethod':  ObjType(_('class method'), 'meth', 'obj'),
     ...
 }

method gets deco for py:decoratormethod, which registers as a method the same way
py:decorator registers as a function.

ObjType.roles[0] stays unchanged for every entry, so role_for_objtype — used when generating
references — is unaffected: a function still reverse-maps to func, not to deco.

Fix verified

Applied at runtime in a test conf.py (site-packages is read-only in my container), then rebuilt both reproductions:

from sphinx.domains import ObjType
from sphinx.domains.python import PythonDomain
from sphinx.locale import _
PythonDomain.object_types["function"] = ObjType(_("function"), "func", "deco", "obj")
PythonDomain.object_types["method"]   = ObjType(_("method"),   "meth", "deco", "obj")
PythonDomain.object_types["data"]     = ObjType(_("data"),     "data", "const", "obj")

Intersphinx case — no warnings, all four resolve, @ prefix preserved:

  r-deco   LINKED      @m.dec
  r-func   LINKED      m.dec()
  r-const  LINKED      m.CONST
  r-data   LINKED      m.CONST

refspecific case — no warnings:

  refspecific-deco   LINKED
  refspecific-const  LINKED
  exact-deco         LINKED
A caveat worth stating in the issue

Adding deco to function means :deco: will also resolve to a plain py:function, and :const: to any py:data. That's already true today for local exact-match references (searchmode 0 ignores the object type), so the change makes intersphinx consistent with local behaviour rather than introducing new looseness. #7243 tracks the broader "roles should check objtypes" question; this report shouldn't try to settle it.

An alternative — giving decorators their own decorator object type — would be a larger change and would alter objects.inv contents, breaking :func: references to decorators in every existing project. Not worth it; the one-line fix matches how property already handles _prop.

How this surfaced

Not hypothetical. In pyTooling PR #263 we swept 28 decorator cross-references from :func: to :deco:. All 28 are internal and resolve correctly. One reference — :class:property`` — points at the CPython docs through intersphinx, and converting it would have silently dropped the link. doc/conf.py doesn't set `nitpicky`, so nothing would have warned; it was caught only by diffing the rendered HTML for `` anchors.

The practical guidance we settled on, which is what any project adopting :deco: needs today:

Use :deco: only for decorators documented in your own project. For decorators from an
intersphinx'd project — property, staticmethod, functools.wraps, dataclasses.dataclass
keep :class: or :func:, or the link disappears without warning.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sphinx/domains/python/init.py, especially PythonDomain.object_types and the find_obj paths, then inspect sphinx/ext/intersphinx/_resolve.py. Reproduce the two minimal projects described in the issue and verify that :deco: and :const: resolve locally and through intersphinx without nitpicky warnings, while existing :func: and :data: behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.