sphinx-doc / sphinx-doc/sphinx
`:py:deco:` and `:py:const:` roles resolve locally but never through intersphinx
Nobody has claimed this yet.
- 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:876 — find_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:910 — find_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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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