Opening to keep as a memo for when working on a better solution in Zensical.
Scoped cross-reference resolution
Problem
With scoped_crossrefs=True, mkdocstrings-python currently expands every reference before mkdocs-autorefs resolves it.
For example, consider markdown.something in markdown.core, where markdown is also a local function. The handler changes this identifier to markdown.core.markdown.something.
This change removes the exact identifier before autorefs checks its local anchor maps or external inventories. A documented or imported markdown.something therefore cannot win over the scoped interpretation.
Resolution timing
AutorefsHook.expand_identifier() runs during docstring Markdown conversion. It runs when AutorefsInlineProcessor creates the temporary <autoref> element.
At that time, autorefs does not yet have its complete identifier-to-URL maps:
- MkDocs can render and register targets on later pages.
- Mkdocstrings registers external inventory URLs during
on_env.
- A Griffe object can exist without a rendered heading or registered URL.
Checking autorefs during expand_identifier() would therefore make resolution depend on page order. Checking Griffe would only prove that a Python object exists, not that autorefs can link to it.
Autorefs resolves <autoref> elements during on_env, after MkDocs has rendered all pages and mkdocstrings has registered external inventories. Both candidates must survive until this step.
Applicable reference forms
Relative and scoped cross-references are only manually written. They use an explicit target such as [text][identifier] or a code-style target such as [`identifier`][].
Autorefs treats both forms as exact references and does not create a normal Markdown slug for them. Automatic cross-references are already absolute and do not need scoped resolution.
The required order is therefore:
- For an ordinary manual reference, try the identifier exactly as written.
- If
scoped_crossrefs=True, try the identifier expanded in the current Python scope.
- For an explicit relative reference, use only its context-directed expansion.
When scoped_crossrefs=False, do not emit or try a scoped fallback.
Implementation in mkdocstrings-python
Use the existing identifier and slug attributes of <autoref>. Autorefs already tries identifier first and slug second.
For an ordinary reference, AutorefsHook.expand_identifier() must preserve the exact identifier. When scoped cross-references are enabled, it must also compute and temporarily store the scoped identifier.
AutorefsHook.get_context() must return a context whose as_dict() adds the scoped identifier as slug. Here, slug transports a fallback identifier; it is not a Markdown slug.
For example, the hook must produce the equivalent of:
<autoref identifier="markdown.something" slug="markdown.core.markdown.something">...</autoref>
Autorefs will then try markdown.something before markdown.core.markdown.something. This happens after all local and external URLs are available.
The hook must clear its stored fallback before it processes each reference. It must not emit slug when the scoped identifier equals the exact identifier.
If an enabled relative reference starts with a dot, expand it with the existing relative-reference logic. Return the expanded identifier directly and do not emit a scoped fallback.
This design requires no changes or new capabilities in mkdocs-autorefs. It also requires no coordinated release or new minimum autorefs version.
Tests
Add integration coverage for these cases:
- With
scoped_crossrefs=True, both the exact and scoped targets exist. The exact target wins.
- With
scoped_crossrefs=True, only the scoped target exists. The scoped fallback resolves.
- With
scoped_crossrefs=False, the handler does not emit or try a scoped fallback.
- An exact target from an external inventory wins over a local scoped interpretation.
- Explicit relative cross-references keep their context-directed behavior and do not emit a scoped fallback.
- Automatic cross-references remain absolute and unchanged.
- The hook clears its stored fallback between consecutive references.
Backlink limitation
Autorefs records backlinks against identifier before it tries slug. If the scoped fallback wins, autorefs does not record the backlink against the scoped target.
This behavior already applies to normal slug fallbacks. Link resolution needs no autorefs change, but backlink parity would require separate work in autorefs.
Opening to keep as a memo for when working on a better solution in Zensical.
Scoped cross-reference resolution
Problem
With
scoped_crossrefs=True, mkdocstrings-python currently expands every reference before mkdocs-autorefs resolves it.For example, consider
markdown.somethinginmarkdown.core, wheremarkdownis also a local function. The handler changes this identifier tomarkdown.core.markdown.something.This change removes the exact identifier before autorefs checks its local anchor maps or external inventories. A documented or imported
markdown.somethingtherefore cannot win over the scoped interpretation.Resolution timing
AutorefsHook.expand_identifier()runs during docstring Markdown conversion. It runs whenAutorefsInlineProcessorcreates the temporary<autoref>element.At that time, autorefs does not yet have its complete identifier-to-URL maps:
on_env.Checking autorefs during
expand_identifier()would therefore make resolution depend on page order. Checking Griffe would only prove that a Python object exists, not that autorefs can link to it.Autorefs resolves
<autoref>elements duringon_env, after MkDocs has rendered all pages and mkdocstrings has registered external inventories. Both candidates must survive until this step.Applicable reference forms
Relative and scoped cross-references are only manually written. They use an explicit target such as
[text][identifier]or a code-style target such as[`identifier`][].Autorefs treats both forms as exact references and does not create a normal Markdown slug for them. Automatic cross-references are already absolute and do not need scoped resolution.
The required order is therefore:
scoped_crossrefs=True, try the identifier expanded in the current Python scope.When
scoped_crossrefs=False, do not emit or try a scoped fallback.Implementation in mkdocstrings-python
Use the existing
identifierandslugattributes of<autoref>. Autorefs already triesidentifierfirst andslugsecond.For an ordinary reference,
AutorefsHook.expand_identifier()must preserve the exact identifier. When scoped cross-references are enabled, it must also compute and temporarily store the scoped identifier.AutorefsHook.get_context()must return a context whoseas_dict()adds the scoped identifier asslug. Here,slugtransports a fallback identifier; it is not a Markdown slug.For example, the hook must produce the equivalent of:
Autorefs will then try
markdown.somethingbeforemarkdown.core.markdown.something. This happens after all local and external URLs are available.The hook must clear its stored fallback before it processes each reference. It must not emit
slugwhen the scoped identifier equals the exact identifier.If an enabled relative reference starts with a dot, expand it with the existing relative-reference logic. Return the expanded identifier directly and do not emit a scoped fallback.
This design requires no changes or new capabilities in mkdocs-autorefs. It also requires no coordinated release or new minimum autorefs version.
Tests
Add integration coverage for these cases:
scoped_crossrefs=True, both the exact and scoped targets exist. The exact target wins.scoped_crossrefs=True, only the scoped target exists. The scoped fallback resolves.scoped_crossrefs=False, the handler does not emit or try a scoped fallback.Backlink limitation
Autorefs records backlinks against
identifierbefore it triesslug. If the scoped fallback wins, autorefs does not record the backlink against the scoped target.This behavior already applies to normal slug fallbacks. Link resolution needs no autorefs change, but backlink parity would require separate work in autorefs.