Skip to content

Commit 56bf627

Browse files
authored
refactor: Prepare backlinks support
Issue-153: #153 PR-252: #252
1 parent a0e888c commit 56bf627

File tree

62 files changed

+342
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+342
-178
lines changed

docs/css/mkdocstrings.css

+45
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,48 @@ a.external:hover::after,
2525
a.autorefs-external:hover::after {
2626
background-color: var(--md-accent-fg-color);
2727
}
28+
29+
/* Tree-like output for backlinks. */
30+
.doc-backlink-list {
31+
--tree-clr: var(--md-default-fg-color);
32+
--tree-font-size: 1rem;
33+
--tree-item-height: 1;
34+
--tree-offset: 1rem;
35+
--tree-thickness: 1px;
36+
--tree-style: solid;
37+
display: grid;
38+
list-style: none !important;
39+
}
40+
41+
.doc-backlink-list li > span:first-child {
42+
text-indent: .3rem;
43+
}
44+
.doc-backlink-list li {
45+
padding-inline-start: var(--tree-offset);
46+
border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
47+
position: relative;
48+
margin-left: 0 !important;
49+
50+
&:last-child {
51+
border-color: transparent;
52+
}
53+
&::before{
54+
content: '';
55+
position: absolute;
56+
top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness));
57+
left: calc(var(--tree-thickness) * -1);
58+
width: calc(var(--tree-offset) + var(--tree-thickness) * 2);
59+
height: calc(var(--tree-item-height) * var(--tree-font-size));
60+
border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
61+
border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr);
62+
}
63+
&::after{
64+
content: '';
65+
position: absolute;
66+
border-radius: 50%;
67+
background-color: var(--tree-clr);
68+
top: calc(var(--tree-item-height) / 2 * 1rem);
69+
left: var(--tree-offset) ;
70+
translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1);
71+
}
72+
}

docs/insiders/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## mkdocstrings-python Insiders
44

5+
### 1.10.0 <small>March 10, 2025</small> { id="1.10.0" }
6+
7+
- [Backlinks][backlinks]
8+
59
### 1.9.0 <small>September 03, 2024</small> { id="1.9.0" }
610

711
- [Relative cross-references][relative_crossrefs]

docs/insiders/goals.yml

+3
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,6 @@ goals:
4242
- name: Scoped cross-references
4343
ref: /usage/configuration/docstrings/#scoped_crossrefs
4444
since: 2024/09/03
45+
- name: Backlinks
46+
ref: /usage/configuration/general/#backlinks
47+
since: 2025/03/10

docs/usage/configuration/general.md

+32
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,38 @@ plugins:
6060
////
6161
///
6262
63+
[](){#option-backlinks}
64+
## `backlinks`
65+
66+
[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } &mdash;
67+
[:octicons-tag-24: Insiders 1.10.0](../../insiders/changelog.md#1.10.0)
68+
69+
- **:octicons-package-24: Type <code><autoref identifier="typing.Literal" optional>Literal</autoref>["flat", "tree", False]</code> :material-equal: `False`{ title="default value" }**
70+
71+
The `backlinks` option enables rendering of backlinks within your API documentation.
72+
73+
When an arbitrary section of your documentation links to an API symbol, this link will be collected as a backlink, and rendered below your API symbol. In short, the API symbol will link back to the section that links to it. Such backlinks will help your users navigate the documentation, as they will immediately which functions return a specific symbol, or where a specific symbol is accepted as parameter, etc..
74+
75+
Each backlink is a list of breadcrumbs that represent the navigation, from the root page down to the given section.
76+
77+
The available styles for rendering backlinks are **`flat`** and **`tree`**.
78+
79+
- **`flat`** will render backlinks as a single-layer list. This can lead to repetition of breadcrumbs.
80+
- **`tree`** will combine backlinks into a tree, to remove repetition of breadcrumbs.
81+
82+
WARNING: **Global-only option.** For now, the option only works when set globally in `mkdocs.yml`.
83+
84+
```yaml title="in mkdocs.yml (global configuration)"
85+
plugins:
86+
- mkdocstrings:
87+
handlers:
88+
python:
89+
options:
90+
backlinks: tree
91+
```
92+
93+
<!-- TODO: Add screenshots! -->
94+
6395
[](){#option-extensions}
6496
## `extensions`
6597

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ plugins:
160160
- https://mkdocstrings.github.io/griffe/objects.inv
161161
- https://python-markdown.github.io/objects.inv
162162
options:
163+
backlinks: tree
163164
docstring_options:
164165
ignore_init_summary: true
165166
docstring_section_style: list

src/mkdocstrings_handlers/python/config.py

+8
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,14 @@ class PythonInputOptions:
386386
),
387387
] = "brief"
388388

389+
backlinks: Annotated[
390+
Literal["flat", "tree", False],
391+
Field(
392+
group="general",
393+
description="Whether to render backlinks, and how.",
394+
),
395+
] = False
396+
389397
docstring_options: Annotated[
390398
GoogleStyleOptions | NumpyStyleOptions | SphinxStyleOptions | AutoStyleOptions | None,
391399
Field(

src/mkdocstrings_handlers/python/handler.py

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ def update_env(self, config: Any) -> None: # noqa: ARG002
301301
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
302302
self.env.filters["as_classes_section"] = rendering.do_as_classes_section
303303
self.env.filters["as_modules_section"] = rendering.do_as_modules_section
304+
self.env.filters["backlink_tree"] = rendering.do_backlink_tree
304305
self.env.globals["AutorefsHook"] = rendering.AutorefsHook
305306
self.env.tests["existing_template"] = lambda template_name: template_name in self.env.list_templates()
306307

src/mkdocstrings_handlers/python/rendering.py

+53-5
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import subprocess
99
import sys
1010
import warnings
11+
from collections import defaultdict
1112
from dataclasses import replace
1213
from functools import lru_cache
1314
from pathlib import Path
1415
from re import Match, Pattern
15-
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal
16+
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar
1617

1718
from griffe import (
1819
Alias,
@@ -28,11 +29,11 @@
2829
)
2930
from jinja2 import TemplateNotFound, pass_context, pass_environment
3031
from markupsafe import Markup
31-
from mkdocs_autorefs import AutorefsHookInterface
32+
from mkdocs_autorefs import AutorefsHookInterface, Backlink, BacklinkCrumb
3233
from mkdocstrings import get_logger
3334

3435
if TYPE_CHECKING:
35-
from collections.abc import Iterator, Sequence
36+
from collections.abc import Iterable, Iterator, Sequence
3637

3738
from griffe import Attribute, Class, Function, Module
3839
from jinja2 import Environment, Template
@@ -210,10 +211,15 @@ def do_format_attribute(
210211

211212
signature = str(attribute_path).strip()
212213
if annotations and attribute.annotation:
213-
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
214+
annotation = template.render(
215+
context.parent,
216+
expression=attribute.annotation,
217+
signature=True,
218+
backlink_type="returned-by",
219+
)
214220
signature += f": {annotation}"
215221
if attribute.value:
216-
value = template.render(context.parent, expression=attribute.value, signature=True)
222+
value = template.render(context.parent, expression=attribute.value, signature=True, backlink_type="used-by")
217223
signature += f" = {value}"
218224

219225
signature = do_format_code(signature, line_length)
@@ -725,3 +731,45 @@ def get_context(self) -> AutorefsHookInterface.Context:
725731
filepath=str(filepath),
726732
lineno=lineno,
727733
)
734+
735+
736+
T = TypeVar("T")
737+
Tree = dict[T, "Tree"]
738+
CompactTree = dict[tuple[T, ...], "CompactTree"]
739+
_rtree = lambda: defaultdict(_rtree) # type: ignore[has-type,var-annotated] # noqa: E731
740+
741+
742+
def _tree(data: Iterable[tuple[T, ...]]) -> Tree:
743+
new_tree = _rtree()
744+
for nav in data:
745+
*path, leaf = nav
746+
node = new_tree
747+
for key in path:
748+
node = node[key]
749+
node[leaf] = _rtree()
750+
return new_tree
751+
752+
753+
def _compact_tree(tree: Tree) -> CompactTree:
754+
new_tree = _rtree()
755+
for key, value in tree.items():
756+
child = _compact_tree(value)
757+
if len(child) == 1:
758+
child_key, child_value = next(iter(child.items()))
759+
new_key = (key, *child_key)
760+
new_tree[new_key] = child_value
761+
else:
762+
new_tree[(key,)] = child
763+
return new_tree
764+
765+
766+
def do_backlink_tree(backlinks: list[Backlink]) -> CompactTree[BacklinkCrumb]:
767+
"""Build a tree of backlinks.
768+
769+
Parameters:
770+
backlinks: The list of backlinks.
771+
772+
Returns:
773+
A tree of backlinks.
774+
"""
775+
return _compact_tree(_tree(backlink.crumbs for backlink in backlinks))

src/mkdocstrings_handlers/python/templates/material/_base/attribute.html.jinja

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Context:
113113
{% include "docstring"|get_template with context %}
114114
{% endwith %}
115115
{% endblock docstring %}
116+
117+
{% if config.backlinks %}
118+
<backlinks identifier="{{ html_id }}" handler="python" />
119+
{% endif %}
116120
{% endblock contents %}
117121
</div>
118122

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{#- Template for backlinks.
2+
3+
This template renders backlinks.
4+
5+
Context:
6+
backlinks (Mapping[str, Iterable[str]]): The backlinks to render.
7+
config (dict): The configuration options.
8+
verbose_type (Mapping[str, str]): The verbose backlink types.
9+
default_crumb (BacklinkCrumb): A default, empty crumb.
10+
-#}
11+
12+
{% block logs scoped %}
13+
{#- Logging block.
14+
15+
This block can be used to log debug messages, deprecation messages, warnings, etc.
16+
-#}
17+
{% endblock logs %}

src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja

+9-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ Context:
130130
{% if config.show_bases and class.bases %}
131131
<p class="doc doc-class-bases">
132132
Bases: {% for expression in class.bases -%}
133-
<code>{% include "expression"|get_template with context %}</code>{% if not loop.last %}, {% endif %}
133+
<code>
134+
{%- with backlink_type = "subclassed-by" -%}
135+
{%- include "expression"|get_template with context -%}
136+
{%- endwith -%}
137+
</code>{% if not loop.last %}, {% endif %}
134138
{% endfor -%}
135139
</p>
136140
{% endif %}
@@ -159,6 +163,10 @@ Context:
159163
{% endif %}
160164
{% endblock docstring %}
161165

166+
{% if config.backlinks %}
167+
<backlinks identifier="{{ html_id }}" handler="python" />
168+
{% endif %}
169+
162170
{% block summary scoped %}
163171
{#- Summary block.
164172

src/mkdocstrings_handlers/python/templates/material/_base/docstring/other_parameters.html.jinja

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Context:
3636
<td><code>{{ parameter.name }}</code></td>
3737
<td>
3838
{% if parameter.annotation %}
39-
{% with expression = parameter.annotation %}
39+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
4040
<code>{% include "expression"|get_template with context %}</code>
4141
{% endwith %}
4242
{% endif %}
@@ -60,7 +60,7 @@ Context:
6060
<li class="doc-section-item field-body">
6161
<b><code>{{ parameter.name }}</code></b>
6262
{% if parameter.annotation %}
63-
{% with expression = parameter.annotation %}
63+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
6464
(<code>{% include "expression"|get_template with context %}</code>)
6565
{% endwith %}
6666
{% endif %}
@@ -94,7 +94,7 @@ Context:
9494
{% if parameter.annotation %}
9595
<span class="doc-param-annotation">
9696
<b>{{ lang.t("TYPE:") }}</b>
97-
{% with expression = parameter.annotation %}
97+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
9898
<code>{% include "expression"|get_template with context %}</code>
9999
{% endwith %}
100100
</span>

src/mkdocstrings_handlers/python/templates/material/_base/docstring/parameters.html.jinja

+6-6
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Context:
5151
</td>
5252
<td>
5353
{% if parameter.annotation %}
54-
{% with expression = parameter.annotation %}
54+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
5555
<code>{% include "expression"|get_template with context %}</code>
5656
{% endwith %}
5757
{% endif %}
@@ -63,7 +63,7 @@ Context:
6363
</td>
6464
<td>
6565
{% if parameter.default %}
66-
{% with expression = parameter.default %}
66+
{% with expression = parameter.default, backlink_type = "used-by" %}
6767
<code>{% include "expression"|get_template with context %}</code>
6868
{% endwith %}
6969
{% else %}
@@ -96,10 +96,10 @@ Context:
9696
<b><code>{{ parameter.name }}</code></b>
9797
{% endif %}
9898
{% if parameter.annotation %}
99-
{% with expression = parameter.annotation %}
99+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
100100
(<code>{% include "expression"|get_template with context %}</code>
101101
{%- if parameter.default %}, {{ lang.t("default:") }}
102-
{% with expression = parameter.default %}
102+
{% with expression = parameter.default, backlink_type = "used-by" %}
103103
<code>{% include "expression"|get_template with context %}</code>
104104
{% endwith %}
105105
{% endif %})
@@ -149,15 +149,15 @@ Context:
149149
{% if parameter.annotation %}
150150
<span class="doc-param-annotation">
151151
<b>{{ lang.t("TYPE:") }}</b>
152-
{% with expression = parameter.annotation %}
152+
{% with expression = parameter.annotation, backlink_type = "used-by" %}
153153
<code>{% include "expression"|get_template with context %}</code>
154154
{% endwith %}
155155
</span>
156156
{% endif %}
157157
{% if parameter.default %}
158158
<span class="doc-param-default">
159159
<b>{{ lang.t("DEFAULT:") }}</b>
160-
{% with expression = parameter.default %}
160+
{% with expression = parameter.default, backlink_type = "used-by" %}
161161
<code>{% include "expression"|get_template with context %}</code>
162162
{% endwith %}
163163
</span>

src/mkdocstrings_handlers/python/templates/material/_base/docstring/raises.html.jinja

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Context:
3434
<tr class="doc-section-item">
3535
<td>
3636
{% if raises.annotation %}
37-
{% with expression = raises.annotation %}
37+
{% with expression = raises.annotation, backlink_type = "raised-by" %}
3838
<code>{% include "expression"|get_template with context %}</code>
3939
{% endwith %}
4040
{% endif %}
@@ -57,7 +57,7 @@ Context:
5757
{% for raises in section.value %}
5858
<li class="doc-section-item field-body">
5959
{% if raises.annotation %}
60-
{% with expression = raises.annotation %}
60+
{% with expression = raises.annotation, backlink_type = "raised-by" %}
6161
<code>{% include "expression"|get_template with context %}</code>
6262
{% endwith %}
6363
@@ -84,7 +84,7 @@ Context:
8484
<tr class="doc-section-item">
8585
<td>
8686
<span class="doc-raises-annotation">
87-
{% with expression = raises.annotation %}
87+
{% with expression = raises.annotation, backlink_type = "raised-by" %}
8888
<code>{% include "expression"|get_template with context %}</code>
8989
{% endwith %}
9090
</span>

0 commit comments

Comments
 (0)