Skip to content

Commit 2bf0cc0

Browse files
committed
feat: Support backlinks
Issue-153: #153
1 parent cfa9848 commit 2bf0cc0

27 files changed

+291
-56
lines changed

mkdocs.yml

Lines changed: 1 addition & 0 deletions
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: flat
163164
docstring_options:
164165
ignore_init_summary: true
165166
docstring_section_style: list

src/mkdocstrings_handlers/python/config.py

Lines changed: 8 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

src/mkdocstrings_handlers/python/rendering.py

Lines changed: 62 additions & 4 deletions
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)))

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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 65 additions & 0 deletions
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, last=false) %}
19+
<span class="doc doc-backlink-crumb{{ " last" if last else "" }}">
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, last=loop.last and not child) }}
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, last=loop.last) }}
58+
{% endfor %}
59+
</li>
60+
{% endfor %}
61+
</ul>
62+
{% endfor %}
63+
{% endif %}
64+
</div>
65+
{% endif %}

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

Lines changed: 7 additions & 1 deletion
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

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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 6 additions & 6 deletions
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

Lines changed: 3 additions & 3 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy