Skip to content

Commit

Permalink
feat: Parameter headings, more automatic cross-references
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Oct 12, 2024
1 parent b461d14 commit 0176b83
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 54 deletions.
4 changes: 3 additions & 1 deletion src/mkdocstrings_handlers/python/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class PythonHandler(BaseHandler):
"summary": False,
"show_labels": True,
"unwrap_annotated": False,
"parameter_headings": False,
}
"""Default handler configuration.
Expand All @@ -138,6 +139,7 @@ class PythonHandler(BaseHandler):
Attributes: Headings options:
heading_level (int): The initial heading level to use. Default: `2`.
parameter_headings (bool): Whether to render headings for parameters (therefore showing parameters in the ToC). Default: `False`.
show_root_heading (bool): Show the heading of the object at the root of the documentation tree
(i.e. the object referenced by the identifier after `:::`). Default: `False`.
show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`.
Expand Down Expand Up @@ -426,7 +428,7 @@ def update_env(self, md: Markdown, config: dict) -> None:
self.env.filters["format_signature"] = rendering.do_format_signature
self.env.filters["format_attribute"] = rendering.do_format_attribute
self.env.filters["filter_objects"] = rendering.do_filter_objects
self.env.filters["stash_crossref"] = lambda ref, length: ref
self.env.filters["stash_crossref"] = rendering.do_stash_crossref
self.env.filters["get_template"] = rendering.do_get_template
self.env.filters["as_attributes_section"] = rendering.do_as_attributes_section
self.env.filters["as_functions_section"] = rendering.do_as_functions_section
Expand Down
72 changes: 29 additions & 43 deletions src/mkdocstrings_handlers/python/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,26 @@ def do_format_code(code: str, line_length: int) -> str:
return formatter(code, line_length)


_stash_key_alphabet = string.ascii_letters + string.digits
class _StashCrossRefFilter:
stash: ClassVar[dict[str, str]] = {}

@staticmethod
def _gen_key(length: int) -> str:
return "_" + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(max(1, length - 1))) # noqa: S311

def _gen_key(length: int) -> str:
return "_" + "".join(random.choice(_stash_key_alphabet) for _ in range(max(1, length - 1))) # noqa: S311
def _gen_stash_key(self, length: int) -> str:
key = self._gen_key(length)
while key in self.stash:
key = self._gen_key(length)
return key

def __call__(self, crossref: str, *, length: int) -> str:
key = self._gen_stash_key(length)
self.stash[key] = crossref
return key

def _gen_stash_key(stash: dict[str, str], length: int) -> str:
key = _gen_key(length)
while key in stash:
key = _gen_key(length)
return key


def _stash_crossref(stash: dict[str, str], crossref: str, *, length: int) -> str:
key = _gen_stash_key(stash, length)
stash[key] = crossref
return key
do_stash_crossref = _StashCrossRefFilter()


def _format_signature(name: Markup, signature: str, line_length: int) -> str:
Expand Down Expand Up @@ -129,7 +131,7 @@ def do_format_signature(
line_length: int,
*,
annotations: bool | None = None,
crossrefs: bool = False,
crossrefs: bool = False, # noqa: ARG001
) -> str:
"""Format a signature using Black.
Expand All @@ -147,24 +149,15 @@ def do_format_signature(
env = context.environment
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
template = env.get_template(do_get_template(env, "signature"))
config_annotations = context.parent["config"]["show_signature_annotations"]
old_stash_ref_filter = env.filters["stash_crossref"]

stash: dict[str, str] = {}
if (annotations or config_annotations) and crossrefs:
env.filters["stash_crossref"] = partial(_stash_crossref, stash)

if annotations is None:
new_context = context.parent
else:
new_context = dict(context.parent)
new_context["config"] = dict(new_context["config"])
new_context["config"]["show_signature_annotations"] = annotations
try:
signature = template.render(new_context, function=function, signature=True)
finally:
env.filters["stash_crossref"] = old_stash_ref_filter

signature = template.render(new_context, function=function, signature=True)
signature = _format_signature(callable_path, signature, line_length)
signature = str(
env.filters["highlight"](
Expand All @@ -184,9 +177,10 @@ def do_format_signature(
if signature.find('class="nf"') == -1:
signature = signature.replace('class="n"', 'class="nf"', 1)

if stash:
if stash := env.filters["stash_crossref"].stash:
for key, value in stash.items():
signature = re.sub(rf"\b{key}\b", value, signature)
stash.clear()

return signature

Expand All @@ -198,7 +192,7 @@ def do_format_attribute(
attribute: Attribute,
line_length: int,
*,
crossrefs: bool = False,
crossrefs: bool = False, # noqa: ARG001
) -> str:
"""Format an attribute using Black.
Expand All @@ -216,23 +210,14 @@ def do_format_attribute(
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
template = env.get_template(do_get_template(env, "expression"))
annotations = context.parent["config"]["show_signature_annotations"]
separate_signature = context.parent["config"]["separate_signature"]
old_stash_ref_filter = env.filters["stash_crossref"]

stash: dict[str, str] = {}
if separate_signature and crossrefs:
env.filters["stash_crossref"] = partial(_stash_crossref, stash)

try:
signature = str(attribute_path).strip()
if annotations and attribute.annotation:
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
signature += f": {annotation}"
if attribute.value:
value = template.render(context.parent, expression=attribute.value, signature=True)
signature += f" = {value}"
finally:
env.filters["stash_crossref"] = old_stash_ref_filter
signature = str(attribute_path).strip()
if annotations and attribute.annotation:
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
signature += f": {annotation}"
if attribute.value:
value = template.render(context.parent, expression=attribute.value, signature=True)
signature += f" = {value}"

signature = do_format_code(signature, line_length)
signature = str(
Expand All @@ -244,9 +229,10 @@ def do_format_attribute(
),
)

if stash:
if stash := env.filters["stash_crossref"].stash:
for key, value in stash.items():
signature = re.sub(rf"\b{key}\b", value, signature)
stash.clear()

return signature

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,21 @@ Context:
<tbody>
{% for parameter in section.value %}
<tr class="doc-section-item">
<td><code>{{ parameter.name }}</code></td>
<td>
{% if config.parameter_headings %}
{% filter heading(
heading_level + 1,
role="param",
id=html_id ~ "(" ~ parameter.name ~ ")",
class="doc doc-heading doc-heading-parameter",
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
) %}
<code>{{ parameter.name }}</code>
{% endfilter %}
{% else %}
<code>{{ parameter.name }}</code>
{% endif %}
</td>
<td>
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
Expand Down Expand Up @@ -68,7 +82,19 @@ Context:
<ul>
{% for parameter in section.value %}
<li class="doc-section-item field-body">
<b><code>{{ parameter.name }}</code></b>
{% if config.parameter_headings %}
{% filter heading(
heading_level + 1,
role="param",
id=html_id ~ "(" ~ parameter.name ~ ")",
class="doc doc-heading doc-heading-parameter",
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
) %}
<b><code>{{ parameter.name }}</code></b>
{% endfilter %}
{% else %}
<b><code>{{ parameter.name }}</code></b>
{% endif %}
{% if parameter.annotation %}
{% with expression = parameter.annotation %}
(<code>{% include "expression"|get_template with context %}</code>
Expand Down Expand Up @@ -100,7 +126,21 @@ Context:
<tbody>
{% for parameter in section.value %}
<tr class="doc-section-item">
<td><code>{{ parameter.name }}</code></td>
<td>
{% if config.parameter_headings %}
{% filter heading(
heading_level + 1,
role="param",
id=html_id ~ "(" ~ parameter.name ~ ")",
class="doc doc-heading doc-heading-parameter",
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
) %}
<code>{{ parameter.name }}</code>
{% endfilter %}
{% else %}
<code>{{ parameter.name }}</code>
{% endif %}
</td>
<td class="doc-param-details">
<div class="doc-md-description">
{{ parameter.description|convert_markdown(heading_level, html_id, autoref_hook=autoref_hook) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ which is a tree-like structure representing a Python expression.
{%- set annotation = full -%}
{%- endif -%}
{%- for title, path in annotation|split_path(full) -%}
{%- if not signature or config.signature_crossrefs -%}
{%- filter stash_crossref(length=title|length) -%}
{%- if config.signature_crossrefs -%}
{%- if signature -%}
{%- filter stash_crossref(length=title|length) -%}
<autoref identifier="{{ path }}" optional{% if title != path %} hover{% endif %}>{{ title }}</autoref>
{%- endfilter -%}
{%- else -%}
<autoref identifier="{{ path }}" optional{% if title != path %} hover{% endif %}>{{ title }}</autoref>
{%- endfilter -%}
{%- endif -%}
{%- else -%}
{{ title }}
{%- endif -%}
Expand All @@ -44,6 +48,28 @@ which is a tree-like structure representing a Python expression.
{%- endwith -%}
{%- endmacro -%}

{%- macro param_crossref(expression) -%}
{#- Render a cross-reference to a parameter heading.
Parameters:
expression (griffe.expressions.Expr): The expression to render.
Returns:
The autorefs cross-reference, or the parameter name.
-#}
{%- if config.signature_crossrefs -%}
{%- if signature -%}
{%- filter stash_crossref(length=expression.name|length) -%}
<autoref identifier="{{ expression.canonical_path }}" optional hover>{{ expression.name }}</autoref>
{%- endfilter -%}
{%- else -%}
<autoref identifier="{{ expression.canonical_path }}" optional hover>{{ expression.name }}</autoref>
{%- endif -%}
{%- else -%}
{{ expression.name }}
{%- endif -%}
{%- endmacro -%}

{%- macro render(expression, annotations_path) -%}
{#- Render an expression.
Expand Down Expand Up @@ -79,6 +105,8 @@ which is a tree-like structure representing a Python expression.
{{ render(element, annotations_path) }}
{%- endfor -%}
{%- endif -%}
{%- elif expression.classname == "ExprKeyword" -%}
{{ param_crossref(expression) }}={{ render(expression.value, annotations_path) }}
{%- else -%}
{%- for element in expression -%}
{{ render(element, annotations_path) }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Context:
render_kw_only_separator=True,
annotation="",
equal="=",
default=False,
) -%}

(
Expand Down Expand Up @@ -60,17 +61,55 @@ Context:

{#- Prepare default value. -#}
{%- if parameter.default is not none and parameter.kind.value != "variadic positional" and parameter.kind.value != "variadic keyword" -%}
{%- set default = ns.equal + parameter.default|safe -%}
{%- set ns.default = True -%}
{%- else -%}
{%- set ns.default = False -%}
{%- endif -%}

{#- TODO: Move inside kind handling above? -#}
{%- if parameter.kind.value == "variadic positional" -%}
{%- set ns.render_kw_only_separator = False -%}
{%- endif -%}

{#- Render name, annotation and default. -#}
{% if parameter.kind.value == "variadic positional" %}*{% elif parameter.kind.value == "variadic keyword" %}**{% endif -%}
{{ parameter.name }}{{ ns.annotation }}{{ default }}
{#- Prepare name. -#}
{%- set param_name -%}
{%- if parameter.kind.value == "variadic positional" -%}
*
{%- elif parameter.kind.value == "variadic keyword" -%}
**
{%- endif -%}
{{ parameter.name }}
{%- endset -%}

{#- Render parameter name with optional cross-reference to its heading. -#}
{%- if config.separate_signature and config.parameter_headings and config.signature_crossrefs -%}
{%- filter stash_crossref(length=param_name|length) -%}
{%- with func_path = function.path -%}
{%- if config.merge_init_into_class and func_path.endswith(".__init__") -%}
{%- set func_path = func_path[:-9] -%}
{%- endif -%}
<autoref identifier="{{ func_path }}({{ param_name }})" optional>{{ param_name }}</autoref>
{%- endwith -%}
{%- endfilter -%}
{%- else -%}
{{ param_name }}
{%- endif -%}

{#- Render parameter annotation. -#}
{{ ns.annotation }}

{#- Render parameter default value. -#}
{%- if ns.default -%}
{{ ns.equal }}
{%- if config.signature_crossrefs and config.separate_signature -%}
{%- with expression = parameter.default -%}
{%- include "expression"|get_template with context -%}
{%- endwith -%}
{%- else -%}
{{ parameter.default }}
{%- endif -%}
{%- endif -%}

{%- if not loop.last %}, {% endif -%}

{%- endif -%}
Expand Down
Loading

0 comments on commit 0176b83

Please sign in to comment.