Skip to content

refactor: Prepare backlinks support #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/css/mkdocstrings.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,48 @@ a.external:hover::after,
a.autorefs-external:hover::after {
background-color: var(--md-accent-fg-color);
}

/* Tree-like output for backlinks. */
.doc-backlink-list {
--tree-clr: var(--md-default-fg-color);
--tree-font-size: 1rem;
--tree-item-height: 1;
--tree-offset: 1rem;
--tree-thickness: 1px;
--tree-style: solid;
display: grid;
list-style: none !important;
}

.doc-backlink-list li > span:first-child {
text-indent: .3rem;
}
.doc-backlink-list li {
padding-inline-start: var(--tree-offset);
border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
position: relative;
margin-left: 0 !important;

&:last-child {
border-color: transparent;
}
&::before{
content: '';
position: absolute;
top: calc(var(--tree-item-height) / 2 * -1 * var(--tree-font-size) + var(--tree-thickness));
left: calc(var(--tree-thickness) * -1);
width: calc(var(--tree-offset) + var(--tree-thickness) * 2);
height: calc(var(--tree-item-height) * var(--tree-font-size));
border-left: var(--tree-thickness) var(--tree-style) var(--tree-clr);
border-bottom: var(--tree-thickness) var(--tree-style) var(--tree-clr);
}
&::after{
content: '';
position: absolute;
border-radius: 50%;
background-color: var(--tree-clr);
top: calc(var(--tree-item-height) / 2 * 1rem);
left: var(--tree-offset) ;
translate: calc(var(--tree-thickness) * -1) calc(var(--tree-thickness) * -1);
}
}
4 changes: 4 additions & 0 deletions docs/insiders/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## mkdocstrings-python Insiders

### 1.10.0 <small>March 10, 2025</small> { id="1.10.0" }

- [Backlinks][backlinks]

### 1.9.0 <small>September 03, 2024</small> { id="1.9.0" }

- [Relative cross-references][relative_crossrefs]
Expand Down
3 changes: 3 additions & 0 deletions docs/insiders/goals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ goals:
- name: Scoped cross-references
ref: /usage/configuration/docstrings/#scoped_crossrefs
since: 2024/09/03
- name: Backlinks
ref: /usage/configuration/general/#backlinks
since: 2025/03/10
32 changes: 32 additions & 0 deletions docs/usage/configuration/general.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,38 @@ plugins:
////
///

[](){#option-backlinks}
## `backlinks`

[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders/index.md){ .insiders } &mdash;
[:octicons-tag-24: Insiders 1.10.0](../../insiders/changelog.md#1.10.0)

- **:octicons-package-24: Type <code><autoref identifier="typing.Literal" optional>Literal</autoref>["flat", "tree", False]</code> :material-equal: `False`{ title="default value" }**

The `backlinks` option enables rendering of backlinks within your API documentation.

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..

Each backlink is a list of breadcrumbs that represent the navigation, from the root page down to the given section.

The available styles for rendering backlinks are **`flat`** and **`tree`**.

- **`flat`** will render backlinks as a single-layer list. This can lead to repetition of breadcrumbs.
- **`tree`** will combine backlinks into a tree, to remove repetition of breadcrumbs.

WARNING: **Global-only option.** For now, the option only works when set globally in `mkdocs.yml`.

```yaml title="in mkdocs.yml (global configuration)"
plugins:
- mkdocstrings:
handlers:
python:
options:
backlinks: tree
```

<!-- TODO: Add screenshots! -->

[](){#option-extensions}
## `extensions`

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ plugins:
- https://mkdocstrings.github.io/griffe/objects.inv
- https://python-markdown.github.io/objects.inv
options:
backlinks: tree
docstring_options:
ignore_init_summary: true
docstring_section_style: list
Expand Down
8 changes: 8 additions & 0 deletions src/mkdocstrings_handlers/python/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ class PythonInputOptions:
),
] = "brief"

backlinks: Annotated[
Literal["flat", "tree", False],
Field(
group="general",
description="Whether to render backlinks, and how.",
),
] = False

docstring_options: Annotated[
GoogleStyleOptions | NumpyStyleOptions | SphinxStyleOptions | AutoStyleOptions | None,
Field(
Expand Down
1 change: 1 addition & 0 deletions src/mkdocstrings_handlers/python/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ def update_env(self, config: Any) -> None: # noqa: ARG002
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
self.env.filters["as_classes_section"] = rendering.do_as_classes_section
self.env.filters["as_modules_section"] = rendering.do_as_modules_section
self.env.filters["backlink_tree"] = rendering.do_backlink_tree
self.env.globals["AutorefsHook"] = rendering.AutorefsHook
self.env.tests["existing_template"] = lambda template_name: template_name in self.env.list_templates()

Expand Down
58 changes: 53 additions & 5 deletions src/mkdocstrings_handlers/python/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
import subprocess
import sys
import warnings
from collections import defaultdict
from dataclasses import replace
from functools import lru_cache
from pathlib import Path
from re import Match, Pattern
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Literal, TypeVar

from griffe import (
Alias,
Expand All @@ -28,11 +29,11 @@
)
from jinja2 import TemplateNotFound, pass_context, pass_environment
from markupsafe import Markup
from mkdocs_autorefs import AutorefsHookInterface
from mkdocs_autorefs import AutorefsHookInterface, Backlink, BacklinkCrumb
from mkdocstrings import get_logger

if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from collections.abc import Iterable, Iterator, Sequence

from griffe import Attribute, Class, Function, Module
from jinja2 import Environment, Template
Expand Down Expand Up @@ -210,10 +211,15 @@ def do_format_attribute(

signature = str(attribute_path).strip()
if annotations and attribute.annotation:
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
annotation = template.render(
context.parent,
expression=attribute.annotation,
signature=True,
backlink_type="returned-by",
)
signature += f": {annotation}"
if attribute.value:
value = template.render(context.parent, expression=attribute.value, signature=True)
value = template.render(context.parent, expression=attribute.value, signature=True, backlink_type="used-by")
signature += f" = {value}"

signature = do_format_code(signature, line_length)
Expand Down Expand Up @@ -725,3 +731,45 @@ def get_context(self) -> AutorefsHookInterface.Context:
filepath=str(filepath),
lineno=lineno,
)


T = TypeVar("T")
Tree = dict[T, "Tree"]
CompactTree = dict[tuple[T, ...], "CompactTree"]
_rtree = lambda: defaultdict(_rtree) # type: ignore[has-type,var-annotated] # noqa: E731


def _tree(data: Iterable[tuple[T, ...]]) -> Tree:
new_tree = _rtree()
for nav in data:
*path, leaf = nav
node = new_tree
for key in path:
node = node[key]
node[leaf] = _rtree()
return new_tree


def _compact_tree(tree: Tree) -> CompactTree:
new_tree = _rtree()
for key, value in tree.items():
child = _compact_tree(value)
if len(child) == 1:
child_key, child_value = next(iter(child.items()))
new_key = (key, *child_key)
new_tree[new_key] = child_value
else:
new_tree[(key,)] = child
return new_tree


def do_backlink_tree(backlinks: list[Backlink]) -> CompactTree[BacklinkCrumb]:
"""Build a tree of backlinks.

Parameters:
backlinks: The list of backlinks.

Returns:
A tree of backlinks.
"""
return _compact_tree(_tree(backlink.crumbs for backlink in backlinks))
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ Context:
{% include "docstring"|get_template with context %}
{% endwith %}
{% endblock docstring %}

{% if config.backlinks %}
<backlinks identifier="{{ html_id }}" handler="python" />
{% endif %}
{% endblock contents %}
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{#- Template for backlinks.

This template renders backlinks.

Context:
backlinks (Mapping[str, Iterable[str]]): The backlinks to render.
config (dict): The configuration options.
verbose_type (Mapping[str, str]): The verbose backlink types.
default_crumb (BacklinkCrumb): A default, empty crumb.
-#}

{% block logs scoped %}
{#- Logging block.

This block can be used to log debug messages, deprecation messages, warnings, etc.
-#}
{% endblock logs %}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ Context:
{% if config.show_bases and class.bases %}
<p class="doc doc-class-bases">
Bases: {% for expression in class.bases -%}
<code>{% include "expression"|get_template with context %}</code>{% if not loop.last %}, {% endif %}
<code>
{%- with backlink_type = "subclassed-by" -%}
{%- include "expression"|get_template with context -%}
{%- endwith -%}
</code>{% if not loop.last %}, {% endif %}
{% endfor -%}
</p>
{% endif %}
Expand Down Expand Up @@ -159,6 +163,10 @@ Context:
{% endif %}
{% endblock docstring %}

{% if config.backlinks %}
<backlinks identifier="{{ html_id }}" handler="python" />
{% endif %}

{% block summary scoped %}
{#- Summary block.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Context:
<td><code>{{ parameter.name }}</code></td>
<td>
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
{% endif %}
Expand All @@ -60,7 +60,7 @@ Context:
<li class="doc-section-item field-body">
<b><code>{{ parameter.name }}</code></b>
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
(<code>{% include "expression"|get_template with context %}</code>)
{% endwith %}
{% endif %}
Expand Down Expand Up @@ -94,7 +94,7 @@ Context:
{% if parameter.annotation %}
<span class="doc-param-annotation">
<b>{{ lang.t("TYPE:") }}</b>
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Context:
</td>
<td>
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
{% endif %}
Expand All @@ -63,7 +63,7 @@ Context:
</td>
<td>
{% if parameter.default %}
{% with expression = parameter.default %}
{% with expression = parameter.default, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
{% else %}
Expand Down Expand Up @@ -96,10 +96,10 @@ Context:
<b><code>{{ parameter.name }}</code></b>
{% endif %}
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
(<code>{% include "expression"|get_template with context %}</code>
{%- if parameter.default %}, {{ lang.t("default:") }}
{% with expression = parameter.default %}
{% with expression = parameter.default, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
{% endif %})
Expand Down Expand Up @@ -149,15 +149,15 @@ Context:
{% if parameter.annotation %}
<span class="doc-param-annotation">
<b>{{ lang.t("TYPE:") }}</b>
{% with expression = parameter.annotation %}
{% with expression = parameter.annotation, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
</span>
{% endif %}
{% if parameter.default %}
<span class="doc-param-default">
<b>{{ lang.t("DEFAULT:") }}</b>
{% with expression = parameter.default %}
{% with expression = parameter.default, backlink_type = "used-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Context:
<tr class="doc-section-item">
<td>
{% if raises.annotation %}
{% with expression = raises.annotation %}
{% with expression = raises.annotation, backlink_type = "raised-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
{% endif %}
Expand All @@ -57,7 +57,7 @@ Context:
{% for raises in section.value %}
<li class="doc-section-item field-body">
{% if raises.annotation %}
{% with expression = raises.annotation %}
{% with expression = raises.annotation, backlink_type = "raised-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
Expand All @@ -84,7 +84,7 @@ Context:
<tr class="doc-section-item">
<td>
<span class="doc-raises-annotation">
{% with expression = raises.annotation %}
{% with expression = raises.annotation, backlink_type = "raised-by" %}
<code>{% include "expression"|get_template with context %}</code>
{% endwith %}
</span>
Expand Down
Loading
Loading