8000 feat: Parameter headings, more automatic cross-references · hahnbeelee/mkdocstrings-python@0176b83 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0176b83

Browse files
committed
feat: Parameter headings, more automatic cross-references
1 parent b461d14 commit 0176b83

File tree

6 files changed

+173
-54
lines changed

6 files changed

+173
-54
lines changed

src/mkdocstrings_handlers/python/handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class PythonHandler(BaseHandler):
118118
"summary": False,
119119
"show_labels": True,
120120
"unwrap_annotated": False,
121+
"parameter_headings": False,
121122
}
122123
"""Default handler configuration.
123124
@@ -138,6 +139,7 @@ class PythonHandler(BaseHandler):
138139
139140
Attributes: Headings options:
140141
heading_level (int): The initial heading level to use. Default: `2`.
142+
parameter_headings (bool): Whether to render headings for parameters (therefore showing parameters in the ToC). Default: `False`.
141143
show_root_heading (bool): Show the heading of the object at the root of the documentation tree
142144
(i.e. the object referenced by the identifier after `:::`). Default: `False`.
143145
show_root_toc_entry (bool): If the root heading is not shown, at least add a ToC entry for it. Default: `True`.
@@ -426,7 +428,7 @@ def update_env(self, md: Markdown, config: dict) -> None:
426428
self.env.filters["format_signature"] = rendering.do_format_signature
427429
self.env.filters["format_attribute"] = rendering.do_format_attribute
428430
self.env.filters["filter_objects"] = rendering.do_filter_objects
429-
self.env.filters["stash_crossref"] = lambda ref, length: ref
431+
self.env.filters["stash_crossref"] = rendering.do_stash_crossref
430432
self.env.filters["get_template"] = rendering.do_get_template
431433
self.env.filters["as_attributes_section"] = rendering.do_as_attributes_section
432434
self.env.filters["as_functions_section"] = rendering.do_as_functions_section

src/mkdocstrings_handlers/python/rendering.py

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,26 @@ def do_format_code(code: str, line_length: int) -> str:
8383
return formatter(code, line_length)
8484

8585

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

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

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

99+
def __call__(self, crossref: str, *, length: int) -> str:
100+
key = self._gen_stash_key(length)
101+
self.stash[key] = crossref
102+
return key
92103

93-
def _gen_stash_key(stash: dict[str, str], length: int) -> str:
94-
key = _gen_key(length)
95-
while key in stash:
96-
key = _gen_key(length)
97-
return key
98104

99-
100-
def _stash_crossref(stash: dict[str, str], crossref: str, *, length: int) -> str:
101-
key = _gen_stash_key(stash, length)
102-
stash[key] = crossref
103-
return key
105+
do_stash_crossref = _StashCrossRefFilter()
104106

105107

106108
def _format_signature(name: Markup, signature: str, line_length: int) -> str:
@@ -129,7 +131,7 @@ def do_format_signature(
129131
line_length: int,
130132
*,
131133
annotations: bool | None = None,
132-
crossrefs: bool = False,
134+
crossrefs: bool = False, # noqa: ARG001
133135
) -> str:
134136
"""Format a signature using Black.
135137
@@ -147,24 +149,15 @@ def do_format_signature(
147149
env = context.environment
148150
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
149151
template = env.get_template(do_get_template(env, "signature"))
150-
config_annotations = context.parent["config"]["show_signature_annotations"]
151-
old_stash_ref_filter = env.filters["stash_crossref"]
152-
153-
stash: dict[str, str] = {}
154-
if (annotations or config_annotations) and crossrefs:
155-
env.filters["stash_crossref"] = partial(_stash_crossref, stash)
156152

157153
if annotations is None:
158154
new_context = context.parent
159155
else:
160156
new_context = dict(context.parent)
161157
new_context["config"] = dict(new_context["config"])
162158
new_context["config"]["show_signature_annotations"] = annotations
163-
try:
164-
signature = template.render(new_context, function=function, signature=True)
165-
finally:
166-
env.filters["stash_crossref"] = old_stash_ref_filter
167159

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

187-
if stash:
180+
if stash := env.filters["stash_crossref"].stash:
188181
for key, value in stash.items():
189182
signature = re.sub(rf"\b{key}\b", value, signature)
183+
stash.clear()
190184

191185
return signature
192186

@@ -198,7 +192,7 @@ def do_format_attribute(
198192
attribute: Attribute,
199193
line_length: int,
200194
*,
201-
crossrefs: bool = False,
195+
crossrefs: bool = False, # noqa: ARG001
202196
) -> str:
203197
"""Format an attribute using Black.
204198
@@ -216,23 +210,14 @@ def do_format_attribute(
216210
# TODO: Stop using `do_get_template` when `*.html` templates are removed.
217211
template = env.get_template(do_get_template(env, "expression"))
218212
annotations = context.parent["config"]["show_signature_annotations"]
219-
separate_signature = context.parent["config"]["separate_signature"]
220-
old_stash_ref_filter = env.filters["stash_crossref"]
221213

222-
stash: dict[str, str] = {}
223-
if separate_signature and crossrefs:
224-
env.filters["stash_crossref"] = partial(_stash_crossref, stash)
225-
226-
try:
227-
signature = str(attribute_path).strip()
228-
if annotations and attribute.annotation:
229-
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
230-
signature += f": {annotation}"
231-
if attribute.value:
232-
value = template.render(context.parent, expression=attribute.value, signature=True)
233-
signature += f" = {value}"
234-
finally:
235-
env.filters["stash_crossref"] = old_stash_ref_filter
214+
signature = str(attribute_path).strip()
215+
if annotations and attribute.annotation:
216+
annotation = template.render(context.parent, expression=attribute.annotation, signature=True)
217+
signature += f": {annotation}"
218+
if attribute.value:
219+
value = template.render(context.parent, expression=attribute.value, signature=True)
220+
signature += f" = {value}"
236221

237222
signature = do_format_code(signature, line_length)
238223
signature = str(
@@ -244,9 +229,10 @@ def do_format_attribute(
244229
),
245230
)
246231

247-
if stash:
232+
if stash := env.filters["stash_crossref"].stash:
248233
for key, value in stash.items():
249234
signature = re.sub(rf"\b{key}\b", value, signature)
235+
stash.clear()
250236

251237
return signature
252238

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,21 @@ Context:
3434
<tbody>
3535
{% for parameter in section.value %}
3636
<tr class="doc-section-item">
37-
<td><code>{{ parameter.name }}</code></td>
37+
<td>
38+
{% if config.parameter_headings %}
39+
{% filter heading(
40+
heading_level + 1,
41+
role="param",
42+
id=html_id ~ "(" ~ parameter.name ~ ")",
43+
class="doc doc-heading doc-heading-parameter",
44+
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
45+
) %}
46+
<code>{{ parameter.name }}</code>
47+
{% endfilter %}
48+
{% else %}
49+
<code>{{ parameter.name }}</code>
50+
{% endif %}
51+
</td>
3852
<td>
3953
{% if parameter.annotation %}
4054
{% with expression = parameter.annotation %}
@@ -68,7 +82,19 @@ Context:
6882
<ul>
6983
{% for parameter in section.value %}
7084
<li class="doc-section-item field-body">
71-
<b><code>{{ parameter.name }}</code></b>
85+
{% if config.parameter_headings %}
86+
{% filter heading(
87+
heading_level + 1,
88+
role="param",
89+
id=html_id ~ "(" ~ parameter.name ~ ")",
90+
class="doc doc-heading doc-heading-parameter",
91+
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
92+
) %}
93+
<b><code>{{ parameter.name }}</code></b>
94+
{% endfilter %}
95+
{% else %}
96+
<b><code>{{ parameter.name }}</code></b>
97+
{% endif %}
7298
{% if parameter.annotation %}
7399
{% with expression = parameter.annotation %}
74100
(<code>{% include "expression"|get_template with context %}</code>
@@ -100,7 +126,21 @@ Context:
100126
<tbody>
101127
{% for parameter in section.value %}
102128
<tr class="doc-section-item">
103-
<td><code>{{ parameter.name }}</code></td>
129+
<td>
130+
{% if config.parameter_headings %}
131+
{% filter heading(
132+
heading_level + 1,
133+
role="param",
134+
id=html_id ~ "(" ~ parameter.name ~ ")",
135+
class="doc doc-heading doc-heading-parameter",
136+
toc_label=('<code class="doc-symbol doc-symbol-toc doc-symbol-parameter"></code>&nbsp;'|safe if config.show_symbol_type_toc else '') + parameter.name,
137+
) %}
138+
<code>{{ parameter.name }}</code>
139+
{% endfilter %}
140+
{% else %}
141+
<code>{{ parameter.name }}</code>
142+
{% endif %}
143+
</td>
104144
<td class="doc-param-details">
105145
<div class="doc-md-description">
106146
{{ parameter.description|convert_markdown(heading_level, html_id, autoref_hook=autoref_hook) }}

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ which is a tree-like structure representing a Python expression.
3232
{%- set annotation = full -%}
3333
{%- endif -%}
3434
{%- for title, path in annotation|split_path(full) -%}
35-
{%- if not signature or config.signature_crossrefs -%}
36-
{%- filter stash_crossref(length=title|length) -%}
35+
{%- if config.signature_crossrefs -%}
36+
{%- if signature -%}
37+
{%- filter stash_crossref(length=title|length) -%}
38+
<autoref identifier="{{ path }}" optional{% if title != path %} hover{% endif %}>{{ title }}</autoref>
39+
{%- endfilter -%}
40+
{%- else -%}
3741
<autoref identifier="{{ path }}" optional{% if title != path %} hover{% endif %}>{{ title }}</autoref>
38-
{%- endfilter -%}
42+
{%- endif -%}
3943
{%- else -%}
4044
{{ title }}
4145
{%- endif -%}
@@ -44,6 +48,28 @@ which is a tree-like structure representing a Python expression.
4448
{%- endwith -%}
4549
{%- endmacro -%}
4650

51+
{%- macro param_crossref(expression) -%}
52+
{#- Render a cross-reference to a parameter heading.
53+
54+
Parameters:
55+
expression (griffe.expressions.Expr): The expression to render.
56+
57+
Returns:
58+
The autorefs cross-reference, or the parameter name.
59+
-#}
60+
{%- if config.signature_crossrefs -%}
61+
{%- if signature -%}
62+
{%- filter stash_crossref(length=expression.name|length) -%}
63+
<autoref identifier="{{ expression.canonical_path }}" optional hover>{{ expression.name }}</autoref>
64+
{%- endfilter -%}
65+
{%- else -%}
66+
<autoref identifier="{{ expression.canonical_path }}" optional hover>{{ expression.name }}</autoref>
67+
{%- endif -%}
68+
{%- else -%}
69+
{{ expression.name }}
70+
{%- endif -%}
71+
{%- endmacro -%}
72+
4773
{%- macro render(expression, annotations_path) -%}
4874
{#- Render an expression.
4975
@@ -79,6 +105,8 @@ which is a tree-like structure representing a Python expression.
79105
{{ render(element, annotations_path) }}
80106
{%- endfor -%}
81107
{%- endif -%}
108+
{%- elif expression.classname == "ExprKeyword" -%}
109+
{{ param_crossref(expression) }}={{ render(expression.value, annotations_path) }}
82110
{%- else -%}
83111
{%- for element in expression -%}
84112
{{ render(element, annotations_path) }}

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Context:
2525
render_kw_only_separator=True,
2626
annotation="",
2727
equal="=",
28+
default=False,
2829
) -%}
2930

3031
(
@@ -60,17 +61,55 @@ Context:
6061

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

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

71-
{#- Render name, annotation and default. -#}
72-
{% if parameter.kind.value == "variadic positional" %}*{% elif parameter.kind.value == "variadic keyword" %}**{% endif -%}
73-
{{ parameter.name }}{{ ns.annotation }}{{ default }}
74+
{#- Prepare name. -#}
75+
{%- set param_name -%}
76+
{%- if parameter.kind.value == "variadic positional" -%}
77+
*
78+
{%- elif parameter.kind.value == "variadic keyword" -%}
79+
**
80+
{%- endif -%}
81+
{{ parameter.name }}
82+
{%- endset -%}
83+
84+
{#- Render parameter name with optional cross-reference to its heading. -#}
85+
{%- if config.separate_signature and 48DA config.parameter_headings and config.signature_crossrefs -%}
86+
{%- filter stash_crossref(length=param_name|length) -%}
87+
{%- with func_path = function.path -%}
88+
{%- if config.merge_init_into_class and func_path.endswith(".__init__") -%}
89+
{%- set func_path = func_path[:-9] -%}
90+
{%- endif -%}
91+
<autoref identifier="{{ func_path }}({{ param_name }})" optional>{{ param_name }}</autoref>
92+
{%- endwith -%}
93+
{%- endfilter -%}
94+
{%- else -%}
95+
{{ param_name }}
96+
{%- endif -%}
97+
98+
{#- Render parameter annotation. -#}
99+
{{ ns.annotation }}
100+
101+
{#- Render parameter default value. -#}
102+
{%- if ns.default -%}
103+
{{ ns.equal }}
104+
{%- if config.signature_crossrefs and config.separate_signature -%}
105+
{%- with expression = parameter.default -%}
106+
{%- include "expression"|get_template with context -%}
107+
{%- endwith -%}
108+
{%- else -%}
109+
{{ parameter.default }}
110+
{%- endif -%}
111+
{%- endif -%}
112+
74113
{%- if not loop.last %}, {% endif -%}
75114

76115
{%- endif -%}

0 commit comments

Comments
 (0)
0