Skip to content
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

refactor: Add backlinks metadata to autorefs elements #252

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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: flat
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
14 changes: 14 additions & 0 deletions src/mkdocstrings_handlers/python/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import posixpath
import sys
from collections.abc import Iterable
from contextlib import suppress
from dataclasses import asdict
from pathlib import Path
Expand All @@ -25,6 +26,7 @@
from mkdocstrings.handlers.base import BaseHandler, CollectionError, CollectorItem, HandlerOptions
from mkdocstrings.inventory import Inventory
from mkdocstrings.loggers import get_logger
from mkdocs_autorefs.plugin import BacklinkCrumb

from mkdocstrings_handlers.python import rendering
from mkdocstrings_handlers.python.config import PythonConfig, PythonOptions
Expand All @@ -33,6 +35,7 @@
from collections.abc import Iterator, Mapping, MutableMapping, Sequence

from mkdocs.config.defaults import MkDocsConfig
from mkdocs_autorefs.plugin import Backlink


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

def render_backlinks(self, backlinks: Mapping[str, Iterable[Backlink]]) -> str: # noqa: D102 (ignore missing docstring)
template = self.env.get_template("backlinks.html.jinja")
verbose_type = {key: key.capitalize().replace("-by", " by") for key in backlinks.keys()}
return template.render(
backlinks=backlinks,
config=self.get_options({}),
verbose_type=verbose_type,
default_crumb=BacklinkCrumb(title="", url=""),
)

def update_env(self, config: Any) -> None: # noqa: ARG002
"""Update the Jinja environment with custom filters and tests.

Expand All @@ -303,6 +316,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
66 changes: 62 additions & 4 deletions src/mkdocstrings_handlers/python/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections.abc import Iterable
import random
import re
import string
Expand All @@ -12,7 +13,7 @@
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 @@ -26,9 +27,12 @@
DocstringSectionModules,
Object,
)
from collections import defaultdict
from typing import Optional, Sequence, Union

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.loggers import get_logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -210,10 +214,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 +734,52 @@ 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)


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 print_tree(tree: Tree, level: int = 0) -> None:
for key, value in tree.items():
print(" " * level + str(key))
if value:
print_tree(value, level + 1)


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,8 @@ Context:
{% include "docstring"|get_template with context %}
{% endwith %}
{% endblock docstring %}

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

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

This template renders backlinks.

Context:
backlinks (Mapping[str, Iterable[str]]): The backlinks to render.
config (dict): The configuration options.
-#}

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

This block can be used to log debug messages, deprecation messages, warnings, etc.
-#}
{{ log.debug("Rendering backlinks") }}
{% endblock logs %}

{% macro render_crumb(crumb, last=false) %}
<span class="doc doc-backlink-crumb{{ " last" if last else "" }}">
{% if crumb.url and crumb.title %}
<a href="{{ crumb.url }}">{{ crumb.title | safe }}</a>
{% elif crumb.title %}
<span>{{ crumb.title | safe }}</span>
{% endif %}
</span>
{% endmacro %}

{% macro render_tree(tree) %}
<ul class="doc doc-backlink-list">
{% for node, child in tree | dictsort %}
<li class="doc doc-backlink">
{% for crumb in node %}
{{ render_crumb(crumb, last=loop.last and not child) }}
{% endfor %}
{% if child %}
{{ render_tree(child) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}

{% if config.backlinks %}
<div class="doc doc-backlinks">
{% if config.backlinks == "tree" %}
{% for backlink_type, backlink_list in backlinks | dictsort %}
<b class="doc doc-backlink-type">{{ verbose_type[backlink_type] }}:</b>
{{ render_tree(backlink_list|backlink_tree) }}
{% endfor %}
{% elif config.backlinks == "flat" %}
{% for backlink_type, backlink_list in backlinks | dictsort %}
<b class="doc doc-backlink-type">{{ verbose_type[backlink_type] }}:</b>
<ul class="doc doc-backlink-list">
{% for backlink in backlink_list | sort(attribute="crumbs") %}
<li class="doc doc-backlink">
{% for crumb in backlink.crumbs %}
{{ render_crumb(crumb, last=loop.last) }}
{% endfor %}
</li>
{% endfor %}
</ul>
{% endfor %}
{% endif %}
</div>
{% endif %}
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,8 @@ Context:
{% endif %}
{% endblock docstring %}

<backlinks identifier="{{ html_id }}" handler="python" />

{% 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