Skip to content

Commit 5ab3405

Browse files
committed
feat: Support backlinks
Issue-153: #153
1 parent cfa9848 commit 5ab3405

25 files changed

+234
-56
lines changed

Diff for: src/mkdocstrings_handlers/python/handler.py

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import posixpath
88
import sys
9+
from collections.abc import Iterable
910
from contextlib import suppress
1011
from dataclasses import asdict
1112
from pathlib import Path
@@ -25,6 +26,7 @@
2526
from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, HandlerOptions
2627
from mkdocstrings.inventory import Inventory
2728
from mkdocstrings.loggers import get_logger
29+
from mkdocs_autorefs.plugin import BacklinkCrumb
2830

2931
from mkdocstrings_handlers.python import rendering
3032
from mkdocstrings_handlers.python.config import PythonConfig, PythonOptions
@@ -33,6 +35,7 @@
3335
from collections.abc import Iterator, Mapping, MutableMapping, Sequence
3436

3537
from mkdocs.config.defaults import MkDocsConfig
38+
from mkdocs_autorefs.plugin import Backlink
3639

3740

3841
if sys.version_info >= (3, 11):
@@ -280,6 +283,16 @@ def render(self, data: CollectorItem, options: PythonOptions) -> str: # noqa: D
280283
},
281284
)
282285

286+
def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: D102 (ignore missing docstring)
287+
template = self.env.get_template("backlinks.html.jinja")
288+
verbose_type = {key: key.capitalize().replace("-by", " by") for key in backlinks.keys()}
289+
return template.render(
290+
backlinks=backlinks,
291+
config=self.get_options({}),
292+
verbose_type=verbose_type,
293+
default_crumb=BacklinkCrumb(title="", url=""),
294+
)
295+
283296
def update_env(self, config: Any) -> None: # noqa: ARG002
284297
"""Update the Jinja environment with custom filters and tests.
285298
@@ -303,6 +316,7 @@ def update_env(self, config: Any) -> None: # noqa: ARG002
303316
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
304317
self.env.filters["as_classes_section"] = rendering.do_as_classes_section
305318
self.env.filters["as_modules_section"] = rendering.do_as_modules_section
319+
self.env.filters["backlink_tree"] = rendering.do_backlink_tree
306320
self.env.globals["AutorefsHook"] = rendering.AutorefsHook
307321
self.env.tests["existing_template"] = lambda template_name: template_name in self.env.list_templates()
308322

Diff for: src/mkdocstrings_handlers/python/rendering.py

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

33
from __future__ import annotations
44

5+
from collections.abc import Iterable
56
import random
67
import re
78
import string
@@ -12,7 +13,7 @@
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,
@@ -26,9 +27,12 @@
2627
DocstringSectionModules,
2728
Object,
2829
)
30+
from collections import defaultdict
31+
from typing import Optional, Sequence, Union
32+
2933
from jinja2 import TemplateNotFound, pass_context, pass_environment
3034
from markupsafe import Markup
31-
from mkdocs_autorefs import AutorefsHookInterface
35+
from mkdocs_autorefs import AutorefsHookInterface, Backlink, BacklinkCrumb
3236
from mkdocstrings.loggers import get_logger
3337

3438
if TYPE_CHECKING:
@@ -210,10 +214,15 @@ def do_format_attribute(
210214

211215
signature = str(attribute_path).strip()
212216
if annotations and attribute.annotation:
213-
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
217+
annotation = template.render(
218+
context.parent,
219+
expression=attribute.annotation,
220+
signature=True,
221+
backlink_type="returned-by",
222+
)
214223
signature += f": {annotation}"
215224
if attribute.value:
216-
value = template.render(context.parent, expression=attribute.value, signature=True)
225+
value = template.render(context.parent, expression=attribute.value, signature=True, backlink_type="used-by")
217226
signature += f" = {value}"
218227

219228
signature = do_format_code(signature, line_length)
@@ -725,3 +734,52 @@ def get_context(self) -> AutorefsHookInterface.Context:
725734
filepath=str(filepath),
726735
lineno=lineno,
727736
)
737+
738+
739+
T = TypeVar("T")
740+
Tree = dict[T, "Tree"]
741+
CompactTree = dict[tuple[T, ...], "CompactTree"]
742+
_rtree = lambda: defaultdict(_rtree)
743+
744+
745+
def _tree(data: Iterable[tuple[T, ...]]) -> Tree:
746+
new_tree = _rtree()
747+
for nav in data:
748+
*path, leaf = nav
749+
node = new_tree
750+
for key in path:
751+
node = node[key]
752+
node[leaf] = _rtree()
753+
return new_tree
754+
755+
756+
def print_tree(tree: Tree, level: int = 0) -> None:
757+
for key, value in tree.items():
758+
print(" " * level + str(key))
759+
if value:
760+
print_tree(value, level + 1)
761+
762+
763+
def _compact_tree(tree: Tree) -> CompactTree:
764+
new_tree = _rtree()
765+
for key, value in tree.items():
766+
child = _compact_tree(value)
767+
if len(child) == 1:
768+
child_key, child_value = next(iter(child.items()))
769+
new_key = (key, *child_key)
770+
new_tree[new_key] = child_value
771+
else:
772+
new_tree[(key,)] = child
773+
return new_tree
774+
775+
776+
def do_backlink_tree(backlinks: list[Backlink]) -> CompactTree[BacklinkCrumb]:
777+
"""Build a tree of backlinks.
778+
779+
Parameters:
780+
backlinks: The list of backlinks.
781+
782+
Returns:
783+
A tree of backlinks.
784+
"""
785+
return _compact_tree(_tree((backlink.crumbs for backlink in backlinks)))

Diff for: src/mkdocstrings_handlers/python/templates/material/_base/attribute.html.jinja

+2
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ Context:
113113
{% include "docstring"|get_template with context %}
114114
{% endwith %}
115115
{% endblock docstring %}
116+
117+
<backlinks identifier="{{ html_id }}" handler="python" />
116118
{% endblock contents %}
117119
</div>
118120

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
-#}
9+
10+
{% block logs scoped %}
11+
{#- Logging block.
12+
13+
This block can be used to log debug messages, deprecation messages, warnings, etc.
14+
-#}
15+
{{ log.debug("Rendering backlinks") }}
16+
{% endblock logs %}
17+
18+
{% macro render_crumb(crumb) %}
19+
<span class="doc doc-backlink-crumb">
20+
{% if crumb.url and crumb.title %}
21+
<a href="{{ crumb.url }}">{{ crumb.title | safe }}</a>
22+
{% elif crumb.title %}
23+
<span>{{ crumb.title | safe }}</span>
24+
{% endif %}
25+
</span>
26+
{% endmacro %}
27+
28+
{% macro render_tree(tree) %}
29+
<ul class="doc doc-backlink-list">
30+
{% for node, child in tree | dictsort %}
31+
<li class="doc doc-backlink">
32+
{% for crumb in node %}
33+
{{ render_crumb(crumb) }}
34+
{% endfor %}
35+
{% if child %}
36+
{{ render_tree(child) }}
37+
{% endif %}
38+
</li>
39+
{% endfor %}
40+
</ul>
41+
{% endmacro %}
42+
43+
{% if config.backlinks %}
44+
<div class="doc doc-backlinks">
45+
{% if config.backlinks == "tree" %}
46+
{% for backlink_type, backlink_list in backlinks | dictsort %}
47+
<b class="doc doc-backlink-type">{{ verbose_type[backlink_type] }}:</b>
48+
{{ render_tree(backlink_list|backlink_tree) }}
49+
{% endfor %}
50+
{% elif config.backlinks == "flat" %}
51+
{% for backlink_type, backlink_list in backlinks | dictsort %}
52+
<b class="doc doc-backlink-type">{{ verbose_type[backlink_type] }}:</b>
53+
<ul class="doc doc-backlink-list">
54+
{% for backlink in backlink_list | sort(attribute="crumbs") %}
55+
<li class="doc doc-backlink">
56+
{% for crumb in backlink.crumbs %}
57+
{{ render_crumb(crumb) }}
58+
{% endfor %}
59+
</li>
60+
{% endfor %}
61+
</ul>
62+
{% endfor %}
63+
{% endif %}
64+
</div>
65+
{% endif %}

Diff for: src/mkdocstrings_handlers/python/templates/material/_base/class.html.jinja

+7-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,8 @@ Context:
159163
{% endif %}
160164
{% endblock docstring %}
161165

166+
<backlinks identifier="{{ html_id }}" handler="python" />
167+
162168
{% block summary scoped %}
163169
{#- Summary block.
164170

Diff for: 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>

Diff for: 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>

Diff for: 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>

Diff for: src/mkdocstrings_handlers/python/templates/material/_base/docstring/receives.html.jinja

+4-4
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Context:
3737
{% if name_column %}<td>{% if receives.name %}<code>{{ receives.name }}</code>{% endif %}</td>{% endif %}
3838
<td>
3939
{% if receives.annotation %}
40-
{% with expression = receives.annotation %}
40+
{% with expression = receives.annotation, backlink_type = "received-by" %}
4141
<code>{% include "expression"|get_template with context %}</code>
4242
{% endwith %}
4343
{% endif %}
@@ -61,7 +61,7 @@ Context:
6161
<li class="doc-section-item field-body">
6262
{% if receives.name %}<b><code>{{ receives.name }}</code></b>{% endif %}
6363
{% if receives.annotation %}
64-
{% with expression = receives.annotation %}
64+
{% with expression = receives.annotation, backlink_type = "received-by" %}
6565
{% if receives.name %} ({% endif %}
6666
<code>{% include "expression"|get_template with context %}</code>
6767
{% if receives.name %}){% endif %}
@@ -93,7 +93,7 @@ Context:
9393
<code>{{ receives.name }}</code>
9494
{% elif receives.annotation %}
9595
<span class="doc-receives-annotation">
96-
{% with expression = receives.annotation %}
96+
{% with expression = receives.annotation, backlink_type = "received-by" %}
9797
<code>{% include "expression"|get_template with context %}</code>
9898
{% endwith %}
9999
</span>
@@ -107,7 +107,7 @@ Context:
107107
<p>
108108
<span class="doc-receives-annotation">
109109
<b>{{ lang.t("TYPE:") }}</b>
110-
{% with expression = receives.annotation %}
110+
{% with expression = receives.annotation, backlink_type = "received-by" %}
111111
<code>{% include "expression"|get_template with context %}</code>
112112
{% endwith %}
113113
</span>

0 commit comments

Comments
 (0)