Skip to content

Commit 78c498c

Browse files
authored
refactor: Return multiple identifiers from fallback method
Since an object can have aliases (different identifiers leading to it), and since users sometimes want to render an object using one of its aliases instead of its canonical identifier, we make sure to register every identifier associated to an object so that autorefs can find it when fixing cross-references. Issue mkdocstrings/autorefs#11: mkdocstrings/autorefs#11 PR #350: #350
1 parent 40c3e5f commit 78c498c

5 files changed

Lines changed: 42 additions & 29 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ build-backend = "pdm.pep517.api"
44

55
[project]
66
name = "mkdocstrings"
7-
version = {use_scm = true}
87
description = "Automatic documentation from sources, for MkDocs."
98
authors = [{name = "Timothée Mazzucotelli", email = "pawamoy@pm.me"}]
109
license = {file = "LICENSE"}
1110
readme = "README.md"
1211
requires-python = ">=3.6.1"
1312
keywords = ["mkdocs", "mkdocs-plugin", "docstrings", "autodoc", "documentation"]
14-
dynamic = ["version", "classifiers"]
13+
dynamic = ["version"]
1514
classifiers = [
1615
"Development Status :: 4 - Beta",
1716
"License :: OSI Approved :: ISC License (ISCL)",
@@ -45,6 +44,7 @@ mkdocstrings = "mkdocstrings.plugin:MkdocstringsPlugin"
4544
[tool.pdm]
4645
package-dir = "src"
4746
includes = ["src/mkdocstrings"]
47+
version = {use_scm = true}
4848

4949
[tool.pdm.dev-dependencies]
5050
duty = ["duty~=0.6"]

src/mkdocstrings/extension.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,25 +117,28 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
117117
heading_level = match["heading"].count("#")
118118
log.debug(f"Matched '::: {identifier}'")
119119

120-
html, handler = self._process_block(identifier, block, heading_level)
120+
html, handler, data = self._process_block(identifier, block, heading_level)
121121
el = Element("div", {"class": "mkdocstrings"})
122122
# The final HTML is inserted as opaque to subsequent processing, and only revealed at the end.
123123
el.text = self.md.htmlStash.store(html)
124124
# So we need to duplicate the headings directly (and delete later), just so 'toc' can pick them up.
125125
headings = handler.renderer.get_headings()
126126
el.extend(headings)
127127

128-
for heading in headings:
129-
page = self._autorefs.current_page
130-
anchor = heading.attrib["id"]
128+
page = self._autorefs.current_page
129+
for anchor in handler.renderer.get_anchors(data):
131130
self._autorefs.register_anchor(page, anchor)
132131

132+
for heading in headings:
133+
anchor = heading.attrib["id"] # noqa: WPS440
134+
self._autorefs.register_anchor(page, anchor) # noqa: WPS441
135+
133136
if "data-role" in heading.attrib:
134137
self._handlers.inventory.register(
135-
name=anchor,
138+
name=anchor, # noqa: WPS441
136139
domain=handler.domain,
137140
role=heading.attrib["data-role"],
138-
uri=f"{page}#{anchor}",
141+
uri=f"{page}#{anchor}", # noqa: WPS441
139142
)
140143

141144
parent.append(el)
@@ -146,7 +149,12 @@ def run(self, parent: Element, blocks: MutableSequence[str]) -> None:
146149
# list for future processing.
147150
blocks.insert(0, the_rest)
148151

149-
def _process_block(self, identifier: str, yaml_block: str, heading_level: int = 0) -> Tuple[str, BaseHandler]:
152+
def _process_block(
153+
self,
154+
identifier: str,
155+
yaml_block: str,
156+
heading_level: int = 0,
157+
) -> Tuple[str, BaseHandler, CollectorItem]:
150158
"""Process an autodoc block.
151159
152160
Arguments:
@@ -159,7 +167,7 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int =
159167
TemplateNotFound: When a template used for rendering could not be found.
160168
161169
Returns:
162-
Rendered HTML and the handler that was used.
170+
Rendered HTML, the handler that was used, and the collected item.
163171
"""
164172
config = yaml.safe_load(yaml_block) or {}
165173
handler_name = self._handlers.get_handler_name(config)
@@ -196,7 +204,7 @@ def _process_block(self, identifier: str, yaml_block: str, heading_level: int =
196204
)
197205
raise
198206

199-
return (rendered, handler)
207+
return rendered, handler, data
200208

201209

202210
def get_item_configs(handler_config: dict, config: dict) -> Tuple[Mapping, Mapping]:

src/mkdocstrings/handlers/base.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,20 @@ def render(self, data: CollectorItem, config: dict) -> str:
125125
The rendered template as HTML.
126126
""" # noqa: DAR202 (excess return section)
127127

128-
def get_anchor(self, data: CollectorItem) -> Optional[str]:
129-
"""Return the canonical identifier (HTML anchor) for a collected item.
130-
131-
This must match what the renderer would've actually rendered,
132-
e.g. if rendering the item contains `<h2 id="foo">...` then the return value should be "foo".
128+
def get_anchors(self, data: CollectorItem) -> Sequence[str]:
129+
"""Return the possible identifiers (HTML anchors) for a collected item.
133130
134131
Arguments:
135132
data: The collected data.
136133
137134
Returns:
138-
The HTML anchor (without '#') as a string, or None if this item doesn't have an anchor.
139-
""" # noqa: DAR202 (excess return section)
135+
The HTML anchors (without '#'), or an empty tuple if this item doesn't have an anchor.
136+
"""
137+
# TODO: remove this at some point
138+
try:
139+
return (self.get_anchor(data),) # type: ignore
140+
except AttributeError:
141+
return ()
140142

141143
def do_convert_markdown(self, text: str, heading_level: int, html_id: str = "") -> Markup:
142144
"""Render Markdown text; for use inside templates.
@@ -329,24 +331,24 @@ def __init__(self, config: dict) -> None:
329331
self._handlers: Dict[str, BaseHandler] = {}
330332
self.inventory: Inventory = Inventory(project=self._config["site_name"])
331333

332-
def get_anchor(self, identifier: str) -> Optional[str]:
334+
def get_anchors(self, identifier: str) -> Sequence[str]:
333335
"""Return the canonical HTML anchor for the identifier, if any of the seen handlers can collect it.
334336
335337
Arguments:
336338
identifier: The identifier (one that [collect][mkdocstrings.handlers.base.BaseCollector.collect] can accept).
337339
338340
Returns:
339-
A string - anchor without '#', or None if there isn't any identifier familiar with it.
341+
A tuple of strings - anchors without '#', or an empty tuple if there isn't any identifier familiar with it.
340342
"""
341343
for handler in self._handlers.values():
342344
fallback_config = getattr(handler.collector, "fallback_config", {})
343345
try:
344-
anchor = handler.renderer.get_anchor(handler.collector.collect(identifier, fallback_config))
346+
anchors = handler.renderer.get_anchors(handler.collector.collect(identifier, fallback_config))
345347
except CollectionError:
346348
continue
347-
if anchor is not None:
348-
return anchor
349-
return None
349+
if anchors:
350+
return anchors
351+
return ()
350352

351353
def get_handler_name(self, config: dict) -> str:
352354
"""Return the handler name defined in an "autodoc" instruction YAML configuration, or the global default handler.

src/mkdocstrings/handlers/python.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import traceback
1111
from collections import ChainMap
1212
from subprocess import PIPE, Popen # noqa: S404 (what other option, more secure that PIPE do we have? sockets?)
13-
from typing import Any, BinaryIO, Callable, Iterator, List, Optional, Tuple
13+
from typing import Any, BinaryIO, Callable, Iterator, List, Optional, Sequence, Tuple
1414

1515
from markdown import Markdown
1616
from markupsafe import Markup
@@ -95,8 +95,11 @@ def render(self, data: CollectorItem, config: dict) -> str: # noqa: D102 (ignor
9595
**{"config": final_config, data["category"]: data, "heading_level": heading_level, "root": True},
9696
)
9797

98-
def get_anchor(self, data: CollectorItem) -> str: # noqa: D102 (ignore missing docstring)
99-
return data.get("path")
98+
def get_anchors(self, data: CollectorItem) -> Sequence[str]: # noqa: D102 (ignore missing docstring)
99+
try:
100+
return (data["path"],)
101+
except KeyError:
102+
return ()
100103

101104
def update_env(self, md: Markdown, config: dict) -> None: # noqa: D102 (ignore missing docstring)
102105
super().update_env(md, config)
@@ -144,7 +147,7 @@ class PythonCollector(BaseCollector):
144147
fallback_config = {"docstring_style": "markdown", "filters": ["!.*"]}
145148
"""The configuration used when falling back to re-collecting an object to get its anchor.
146149
147-
This configuration is used in [`Handlers.get_anchor`][mkdocstrings.handlers.base.Handlers.get_anchor].
150+
This configuration is used in [`Handlers.get_anchors`][mkdocstrings.handlers.base.Handlers.get_anchors].
148151
149152
When trying to fix (optional) cross-references, the autorefs plugin will try to collect
150153
an object with every configured handler until one succeeds. It will then try to get

src/mkdocstrings/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def on_config(self, config: Config, **kwargs) -> Config: # noqa: W0613 (unused
183183
config["plugins"]["autorefs"] = autorefs
184184
log.debug(f"Added a subdued autorefs instance {autorefs!r}")
185185
# Add collector-based fallback in either case.
186-
autorefs.get_fallback_anchor = self.handlers.get_anchor
186+
autorefs.get_fallback_anchor = self.handlers.get_anchors
187187

188188
mkdocstrings_extension = MkdocstringsExtension(extension_config, self.handlers, autorefs)
189189
config["markdown_extensions"].append(mkdocstrings_extension)

0 commit comments

Comments
 (0)