diff --git a/docs/_quarto.yml b/docs/_quarto.yml
index 0ec437c..88cb53b 100644
--- a/docs/_quarto.yml
+++ b/docs/_quarto.yml
@@ -79,7 +79,9 @@ website:
format:
html:
theme: cosmo
- css: styles.css
+ css:
+ - api/_styles-quartodoc.css
+ - styles.css
toc: true
@@ -88,7 +90,11 @@ quartodoc:
dir: api
package: quartodoc
render_interlinks: true
+ renderer:
+ style: markdown
+ table_style: description-list
sidebar: "api/_sidebar.yml"
+ css: "api/_styles-quartodoc.css"
sections:
- title: Preparation Functions
desc: |
diff --git a/docs/get-started/overview.qmd b/docs/get-started/overview.qmd
index cf67318..4265480 100644
--- a/docs/get-started/overview.qmd
+++ b/docs/get-started/overview.qmd
@@ -77,15 +77,20 @@ project:
# tell quarto to read the generated sidebar
metadata-files:
- - _sidebar.yml
+ - api/_sidebar.yml
+# tell quarto to read the generated styles
+format:
+ css:
+ - api/_styles-quartodoc.css
quartodoc:
# the name used to import the package you want to create reference docs for
package: quartodoc
- # write sidebar data to this file
- sidebar: _sidebar.yml
+ # write sidebar and style data
+ sidebar: api/_sidebar.yml
+ css: api/_styles-quartodoc.css
sections:
- title: Some functions
diff --git a/pyproject.toml b/pyproject.toml
index 02ebe57..95c2f2e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ classifiers = [
dynamic = ["version"]
requires-python = ">=3.9"
dependencies = [
+ "black",
"click",
"griffe >= 0.33",
"sphobjinv >= 2.3.1",
diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py
index a2242b7..c6bea4e 100644
--- a/quartodoc/autosummary.py
+++ b/quartodoc/autosummary.py
@@ -427,6 +427,8 @@ class Builder:
The output path of the index file, used to list all API functions.
sidebar:
The output path for a sidebar yaml config (by default no config generated).
+ css:
+ The output path for the default css styles.
rewrite_all_pages:
Whether to rewrite all rendered doc pages, or only those with changes.
source_dir:
@@ -486,6 +488,7 @@ def __init__(
renderer: "dict | Renderer | str" = "markdown",
out_index: str = None,
sidebar: "str | None" = None,
+ css: "str | None" = None,
rewrite_all_pages=False,
source_dir: "str | None" = None,
dynamic: bool | None = None,
@@ -502,6 +505,7 @@ def __init__(
self.dir = dir
self.title = title
self.sidebar = sidebar
+ self.css = css
self.parser = parser
self.renderer = Renderer.from_config(renderer)
@@ -587,6 +591,12 @@ def build(self, filter: str = "*"):
_log.info(f"Writing sidebar yaml to {self.sidebar}")
self.write_sidebar(blueprint)
+ # css ----
+
+ if self.css:
+ _log.info(f"Writing css styles to {self.css}")
+ self.write_css()
+
def write_index(self, blueprint: layout.Layout):
"""Write API index page."""
@@ -685,6 +695,22 @@ def write_sidebar(self, blueprint: layout.Layout):
d_sidebar = self._generate_sidebar(blueprint)
yaml.dump(d_sidebar, open(self.sidebar, "w"))
+ def write_css(self):
+ """Write default css styles to a file."""
+ from importlib_resources import files
+ from importlib_metadata import version
+
+ v = version("quartodoc")
+
+ note = (
+ f"/*\nThis file generated automatically by quartodoc version {v}.\n"
+ "Modifications may be overwritten by quartodoc build. If you want to\n"
+ "customize styles, create a new .css file to avoid losing changes.\n"
+ "*/\n\n\n"
+ )
+ with open(files("quartodoc.static") / "styles.css") as f:
+ Path(self.css).write_text(note + f.read())
+
def _page_to_links(self, el: layout.Page) -> list[str]:
# if el.flatten:
# links = []
diff --git a/quartodoc/renderers/base.py b/quartodoc/renderers/base.py
index b9f5141..d8a8968 100644
--- a/quartodoc/renderers/base.py
+++ b/quartodoc/renderers/base.py
@@ -17,6 +17,9 @@ def sanitize(val: str, allow_markdown=False):
# sanitize common tokens that break tables
res = val.replace("\n", " ").replace("|", "\\|")
+ # sanitize elements that get turned into smart quotes
+ res = res.replace("'", r"\'").replace('"', r"\"")
+
# sanitize elements that can get interpreted as markdown links
# or citations
if not allow_markdown:
diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py
index bb4194f..583071e 100644
--- a/quartodoc/renderers/md_renderer.py
+++ b/quartodoc/renderers/md_renderer.py
@@ -1,16 +1,20 @@
from __future__ import annotations
+import black
import quartodoc.ast as qast
from contextlib import contextmanager
+from dataclasses import dataclass
from functools import wraps
from .._griffe_compat import docstrings as ds
from .._griffe_compat import dataclasses as dc
from .._griffe_compat import expressions as expr
from tabulate import tabulate
from plum import dispatch
-from typing import Tuple, Union, Optional
+from typing import Any, Literal, Tuple, Union, Optional
from quartodoc import layout
+from quartodoc.pandoc.blocks import DefinitionList
+from quartodoc.pandoc.inlines import Span, Strong, Attr, Code, Inlines
from .base import Renderer, escape, sanitize, convert_rst_link_to_md
@@ -22,6 +26,68 @@ def _has_attr_section(el: dc.Docstring | None):
return any([isinstance(x, ds.DocstringSectionAttributes) for x in el.parsed])
+@dataclass
+class ParamRow:
+ name: str | None
+ description: str
+ annotation: str | None = None
+ default: str | None = None
+
+ def to_definition_list(self):
+ name = self.name
+ anno = self.annotation
+ desc = self.description
+ default = sanitize(str(self.default))
+
+ part_name = (
+ Span(Strong(name), Attr(classes=["parameter-name"]))
+ if name is not None
+ else ""
+ )
+ part_anno = (
+ Span(anno, Attr(classes=["parameter-annotation"]))
+ if anno is not None
+ else ""
+ )
+
+ # TODO: _required_ is set when parsing parameters, but only used
+ # in the table display format, not description lists....
+ # by this stage _required_ is basically a special token to indicate
+ # a required argument.
+ if default is not None:
+ part_default_sep = Span(" = ", Attr(classes=["parameter-default-sep"]))
+ part_default = Span(default, Attr(classes=["parameter-default"]))
+ else:
+ part_default_sep = ""
+ part_default = ""
+
+ part_desc = desc if desc is not None else ""
+
+ anno_sep = Span(":", Attr(classes=["parameter-annotation-sep"]))
+
+ # TODO: should code wrap the whole thing like this?
+ param = Code(
+ str(
+ Inlines(
+ [part_name, anno_sep, part_anno, part_default_sep, part_default]
+ )
+ )
+ ).html
+ return (param, part_desc)
+
+ def to_tuple(self, style: Literal["parameters", "attributes", "returns"]):
+ name = self.name
+ if style == "parameters":
+ default = "_required_" if self.default is None else escape(self.default)
+ return (name, self.annotation, self.description, default)
+ elif style == "attributes":
+ return (name, self.annotation, self.description)
+ elif style == "returns":
+ return (name, self.annotation, self.description)
+
+ raise NotImplementedError(f"Unsupported table style: {style}")
+
+
class MdRenderer(Renderer):
"""Render docstrings to markdown.
@@ -61,6 +127,8 @@ def __init__(
display_name: str = "relative",
hook_pre=None,
render_interlinks=False,
+ # table_style="description-list",
+ table_style="table",
):
self.header_level = header_level
self.show_signature = show_signature
@@ -68,6 +136,7 @@ def __init__(
self.display_name = display_name
self.hook_pre = hook_pre
self.render_interlinks = render_interlinks
+ self.table_style = table_style
self.crnt_header_level = self.header_level
@@ -102,10 +171,18 @@ def _fetch_method_parameters(self, el: dc.Function):
return el.parameters
- def _render_table(self, rows, headers):
- table = tabulate(rows, headers=headers, tablefmt="github")
-
- return table
+ def _render_table(
+ self,
+ rows,
+ headers,
+ style: Literal["parameters", "attributes", "returns"],
+ ):
+ if self.table_style == "description-list":
+ return str(DefinitionList([row.to_definition_list() for row in rows]))
+ else:
+ row_tuples = [row.to_tuple(style) for row in rows]
+ table = tabulate(row_tuples, headers=headers, tablefmt="github")
+ return table
# render_annotation method --------------------------------------------------------
@@ -164,7 +241,14 @@ def signature(
name = self._fetch_object_dispname(source or el)
pars = self.render(self._fetch_method_parameters(el))
- return f"`{name}({pars})`"
+ flat_sig = f"{name}({', '.join(pars)})"
+ if len(flat_sig) > 80:
+ indented = [" " * 4 + par for par in pars]
+ sig = "\n".join([f"{name}(", *indented, ")"])
+ else:
+ sig = flat_sig
+
+ return f"```python\n{sig}\n```"
@dispatch
def signature(
@@ -174,7 +258,7 @@ def signature(
return f"`{name}`"
@dispatch
- def render_header(self, el: layout.Doc):
+ def render_header(self, el: layout.Doc) -> str:
"""Render the header of a docstring, including any anchors."""
_str_dispname = el.name
@@ -183,6 +267,13 @@ def render_header(self, el: layout.Doc):
_anchor = f"{{ #{el.obj.path} }}"
return f"{'#' * self.crnt_header_level} {_str_dispname} {_anchor}"
+ @dispatch
+ def render_header(self, el: ds.DocstringSection) -> str:
+ title = el.title or el.kind.value
+ _classes = [".doc-section", ".doc-section-" + title.replace(" ", "-")]
+ _str_classes = " ".join(_classes)
+ return f"{'#' * self.crnt_header_level} {title.title()} {{{_str_classes}}}"
+
# render method -----------------------------------------------------------
@dispatch
@@ -336,7 +427,8 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]):
str_sig = self.signature(el)
sig_part = [str_sig] if self.show_signature else []
- body = self.render(el.obj)
+ with self._increment_header():
+ body = self.render(el.obj)
return "\n\n".join(
[title, *sig_part, body, *attr_docs, *class_docs, *meth_docs]
@@ -349,7 +441,10 @@ def render(self, el: Union[layout.DocFunction, layout.DocAttribute]):
str_sig = self.signature(el)
sig_part = [str_sig] if self.show_signature else []
- return "\n\n".join([title, *sig_part, self.render(el.obj)])
+ with self._increment_header():
+ body = self.render(el.obj)
+
+ return "\n\n".join([title, *sig_part, body])
# render griffe objects ===================================================
@@ -357,20 +452,26 @@ def render(self, el: Union[layout.DocFunction, layout.DocAttribute]):
def render(self, el: Union[dc.Object, dc.Alias]):
"""Render high level objects representing functions, classes, etc.."""
- str_body = []
if el.docstring is None:
- pass
+ return ""
else:
- patched_sections = qast.transform(el.docstring.parsed)
- for section in patched_sections:
- title = section.title or section.kind.value
- body = self.render(section)
+ return self.render(el.docstring)
+
+ @dispatch
+ def render(self, el: dc.Docstring):
+ str_body = []
+ patched_sections = qast.transform(el.parsed)
- if title != "text":
- header = f"{'#' * (self.crnt_header_level + 1)} {title.title()}"
- str_body.append("\n\n".join([header, body]))
- else:
- str_body.append(body)
+ for section in patched_sections:
+ title = section.title or section.kind.value
+ body: str = self.render(section)
+
+ if title != "text":
+ header = self.render_header(section)
+ # header = f"{'#' * (self.crnt_header_level + 1)} {title.title()}"
+ str_body.append("\n\n".join([header, body]))
+ else:
+ str_body.append(body)
parts = [*str_body]
@@ -379,7 +480,7 @@ def render(self, el: Union[dc.Object, dc.Alias]):
# signature parts -------------------------------------------------------------
@dispatch
- def render(self, el: dc.Parameters):
+ def render(self, el: dc.Parameters) -> "list":
# index for switch from positional to kw args (via an unnamed *)
try:
kw_only = [par.kind for par in el].index(dc.ParameterKind.keyword_only)
@@ -407,15 +508,15 @@ def render(self, el: dc.Parameters):
and kw_only > 0
and el[kw_only - 1].kind != dc.ParameterKind.var_positional
):
- pars.insert(kw_only, sanitize("*"))
+ pars.insert(kw_only, "*")
# insert a single `/, ` argument to represent shift from positional only arguments
# note that this must come before a single *, so it's okay that both this
# and block above insert into pars
if pos_only is not None:
- pars.insert(pos_only + 1, sanitize("/"))
+ pars.insert(pos_only + 1, "/")
- return ", ".join(pars)
+ return pars
@dispatch
def render(self, el: dc.Parameter):
@@ -430,8 +531,8 @@ def render(self, el: dc.Parameter):
else:
glob = ""
- annotation = self.render_annotation(el.annotation)
- name = sanitize(el.name)
+ annotation = el.annotation # self.render_annotation(el.annotation)
+ name = el.name
if self.show_signature_annotations:
if annotation and has_default:
@@ -463,18 +564,15 @@ def render(self, el: ds.DocstringSectionText):
@dispatch
def render(self, el: ds.DocstringSectionParameters):
- rows = list(map(self.render, el.value))
+ rows: "list[ParamRow]" = list(map(self.render, el.value))
header = ["Name", "Type", "Description", "Default"]
- return self._render_table(rows, header)
+ return self._render_table(rows, header, "parameters")
@dispatch
- def render(self, el: ds.DocstringParameter) -> Tuple[str]:
- # TODO: if default is not, should return the word "required" (unescaped)
- default = "_required_" if el.default is None else escape(el.default)
-
+ def render(self, el: ds.DocstringParameter) -> ParamRow:
annotation = self.render_annotation(el.annotation)
clean_desc = sanitize(el.description, allow_markdown=True)
- return (escape(el.name), annotation, clean_desc, default)
+ return ParamRow(el.name, clean_desc, annotation=annotation, default=el.default)
# attributes ----
@@ -483,16 +581,15 @@ def render(self, el: ds.DocstringSectionAttributes):
header = ["Name", "Type", "Description"]
rows = list(map(self.render, el.value))
- return self._render_table(rows, header)
+ return self._render_table(rows, header, "attributes")
@dispatch
- def render(self, el: ds.DocstringAttribute):
- row = [
- sanitize(el.name),
- self.render_annotation(el.annotation),
+ def render(self, el: ds.DocstringAttribute) -> ParamRow:
+ return ParamRow(
+ el.name,
sanitize(el.description or "", allow_markdown=True),
- ]
- return row
+ annotation=self.render_annotation(el.annotation),
+ )
# admonition ----
# note this can be a see-also, warnings, or notes section
@@ -550,15 +647,27 @@ def render(self, el: qast.ExampleText):
@dispatch
def render(self, el: Union[ds.DocstringSectionReturns, ds.DocstringSectionRaises]):
rows = list(map(self.render, el.value))
- header = ["Type", "Description"]
+ header = ["Name", "Type", "Description"]
- return self._render_table(rows, header)
+ return self._render_table(rows, header, "returns")
@dispatch
- def render(self, el: Union[ds.DocstringReturn, ds.DocstringRaise]):
+ def render(self, el: ds.DocstringReturn):
# similar to DocstringParameter, but no name or default
- annotation = self.render_annotation(el.annotation)
- return (annotation, sanitize(el.description, allow_markdown=True))
+ return ParamRow(
+ el.name,
+ sanitize(el.description, allow_markdown=True),
+ annotation=self.render_annotation(el.annotation),
+ )
+
+ @dispatch
+ def render(self, el: ds.DocstringRaise) -> ParamRow:
+ # similar to DocstringParameter, but no name or default
+ return ParamRow(
+ None,
+ sanitize(el.description, allow_markdown=True),
+ annotation=self.render_annotation(el.annotation),
+ )
# unsupported parts ----
diff --git a/quartodoc/static/styles.css b/quartodoc/static/styles.css
new file mode 100644
index 0000000..a029aba
--- /dev/null
+++ b/quartodoc/static/styles.css
@@ -0,0 +1,15 @@
+/* styles for parameter tables, etc.. ----
+*/
+
+.doc-section dt code {
+ background: none;
+}
+
+.doc-section dt {
+ /* background-color: lightyellow; */
+ display: block;
+}
+
+.doc-section dl dd {
+ margin-left: 3rem;
+}
diff --git a/quartodoc/tests/__snapshots__/test_renderers.ambr b/quartodoc/tests/__snapshots__/test_renderers.ambr
index 719abe0..a46143e 100644
--- a/quartodoc/tests/__snapshots__/test_renderers.ambr
+++ b/quartodoc/tests/__snapshots__/test_renderers.ambr
@@ -3,47 +3,59 @@
'''
# quartodoc.tests.example_signature.a_complex_signature { #quartodoc.tests.example_signature.a_complex_signature }
- `tests.example_signature.a_complex_signature(x: [list](`list`)\[[C](`quartodoc.tests.example_signature.C`) \| [int](`int`) \| None\], y: [pathlib](`pathlib`).[Pathlib](`pathlib.Pathlib`))`
+ ```python
+ tests.example_signature.a_complex_signature(
+ x: list[C | int | None]
+ y: pathlib.Pathlib
+ )
+ ```
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------------------------------------------------------------------------------------|-----------------|------------|
- | `x` | [list](`list`)\[[C](`quartodoc.tests.example_signature.C`) \| [int](`int`) \| None\] | The x parameter | _required_ |
- | `y` | [pathlib](`pathlib`).[Pathlib](`pathlib.Pathlib`) | The y parameter | _required_ |
+ | x | [list](`list`)\[[C](`quartodoc.tests.example_signature.C`) \| [int](`int`) \| None\] | The x parameter | _required_ |
+ | y | [pathlib](`pathlib`).[Pathlib](`pathlib.Pathlib`) | The y parameter | _required_ |
'''
# ---
# name: test_render_annotations_complex_no_interlinks
'''
# quartodoc.tests.example_signature.a_complex_signature { #quartodoc.tests.example_signature.a_complex_signature }
- `tests.example_signature.a_complex_signature(x: list\[C \| int \| None\], y: pathlib.Pathlib)`
+ ```python
+ tests.example_signature.a_complex_signature(
+ x: list[C | int | None]
+ y: pathlib.Pathlib
+ )
+ ```
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------------------------|-----------------|------------|
- | `x` | list\[C \| int \| None\] | The x parameter | _required_ |
- | `y` | pathlib.Pathlib | The y parameter | _required_ |
+ | x | list\[C \| int \| None\] | The x parameter | _required_ |
+ | y | pathlib.Pathlib | The y parameter | _required_ |
'''
# ---
# name: test_render_doc_class[embedded]
'''
# quartodoc.tests.example_class.C { #quartodoc.tests.example_class.C }
- `tests.example_class.C(self, x, y)`
+ ```python
+ tests.example_class.C(self, x, y)
+ ```
The short summary.
The extended summary,
which may be multiple lines.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|----------------------|------------|
- | `x` | str | Uses signature type. | _required_ |
- | `y` | int | Uses manual type. | _required_ |
+ | x | str | Uses signature type. | _required_ |
+ | y | int | Uses manual type. | _required_ |
## Attributes
@@ -61,7 +73,9 @@
### D { #quartodoc.tests.example_class.C.D }
- `tests.example_class.C.D()`
+ ```python
+ tests.example_class.C.D()
+ ```
A nested class
@@ -74,13 +88,17 @@
### some_class_method { #quartodoc.tests.example_class.C.some_class_method }
- `tests.example_class.C.some_class_method()`
+ ```python
+ tests.example_class.C.some_class_method()
+ ```
A class method
### some_method { #quartodoc.tests.example_class.C.some_method }
- `tests.example_class.C.some_method()`
+ ```python
+ tests.example_class.C.some_method()
+ ```
A method
'''
@@ -89,19 +107,21 @@
'''
# quartodoc.tests.example_class.C { #quartodoc.tests.example_class.C }
- `tests.example_class.C(self, x, y)`
+ ```python
+ tests.example_class.C(self, x, y)
+ ```
The short summary.
The extended summary,
which may be multiple lines.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|----------------------|------------|
- | `x` | str | Uses signature type. | _required_ |
- | `y` | int | Uses manual type. | _required_ |
+ | x | str | Uses signature type. | _required_ |
+ | y | int | Uses manual type. | _required_ |
## Attributes
@@ -119,7 +139,9 @@
## D { #quartodoc.tests.example_class.C.D }
- `tests.example_class.C.D()`
+ ```python
+ tests.example_class.C.D()
+ ```
A nested class
@@ -132,13 +154,17 @@
## some_class_method { #quartodoc.tests.example_class.C.some_class_method }
- `tests.example_class.C.some_class_method()`
+ ```python
+ tests.example_class.C.some_class_method()
+ ```
A class method
## some_method { #quartodoc.tests.example_class.C.some_method }
- `tests.example_class.C.some_method()`
+ ```python
+ tests.example_class.C.some_method()
+ ```
A method
'''
@@ -147,11 +173,13 @@
'''
# quartodoc.tests.example_class.AttributesTable { #quartodoc.tests.example_class.AttributesTable }
- `tests.example_class.AttributesTable(self)`
+ ```python
+ tests.example_class.AttributesTable(self)
+ ```
The short summary.
- ## Attributes
+ ## Attributes {.doc-section .doc-section-attributes}
| Name | Type | Description |
|--------|--------|---------------------|
@@ -182,7 +210,9 @@
### AClass { #quartodoc.tests.example.AClass }
- `tests.example.AClass()`
+ ```python
+ tests.example.AClass()
+ ```
A class
@@ -200,7 +230,9 @@
##### a_method { #quartodoc.tests.example.AClass.a_method }
- `tests.example.AClass.a_method()`
+ ```python
+ tests.example.AClass.a_method()
+ ```
A method
@@ -212,7 +244,9 @@
### a_func { #quartodoc.tests.example.a_func }
- `tests.example.a_func()`
+ ```python
+ tests.example.a_func()
+ ```
A function
'''
@@ -239,7 +273,9 @@
## AClass { #quartodoc.tests.example.AClass }
- `tests.example.AClass()`
+ ```python
+ tests.example.AClass()
+ ```
A class
@@ -257,7 +293,9 @@
#### a_method { #quartodoc.tests.example.AClass.a_method }
- `tests.example.AClass.a_method()`
+ ```python
+ tests.example.AClass.a_method()
+ ```
A method
@@ -269,7 +307,9 @@
## a_func { #quartodoc.tests.example.a_func }
- `tests.example.a_func()`
+ ```python
+ tests.example.a_func()
+ ```
A function
'''
@@ -278,7 +318,9 @@
'''
# example.a_func { #quartodoc.tests.example.a_func }
- `a_func()`
+ ```python
+ a_func()
+ ```
A function
'''
@@ -287,7 +329,9 @@
'''
# example.a_nested_alias { #quartodoc.tests.example.a_nested_alias }
- `tests.example.a_nested_alias()`
+ ```python
+ tests.example.a_nested_alias()
+ ```
A nested alias target
'''
@@ -296,34 +340,38 @@
'''
# f_numpy_with_linebreaks { #quartodoc.tests.example_docstring_styles.f_numpy_with_linebreaks }
- `tests.example_docstring_styles.f_numpy_with_linebreaks(a, b)`
+ ```python
+ tests.example_docstring_styles.f_numpy_with_linebreaks(a, b)
+ ```
A numpy style docstring.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|------------------|------------|
- | `a` | | The a parameter. | _required_ |
- | `b` | str | The b parameter. | _required_ |
+ | a | | The a parameter. | _required_ |
+ | b | str | The b parameter. | _required_ |
'''
# ---
# name: test_render_docstring_styles[google]
'''
# f_google { #quartodoc.tests.example_docstring_styles.f_google }
- `tests.example_docstring_styles.f_google(a, b)`
+ ```python
+ tests.example_docstring_styles.f_google(a, b)
+ ```
A google style docstring.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|------------------|------------|
- | `a` | int | The a parameter. | _required_ |
- | `b` | str | The b parameter. | _required_ |
+ | a | int | The a parameter. | _required_ |
+ | b | str | The b parameter. | _required_ |
- ## Custom Admonition
+ ## Custom Admonition {.doc-section .doc-section-Custom-Admonition}
Some text.
'''
@@ -332,18 +380,20 @@
'''
# f_numpy { #quartodoc.tests.example_docstring_styles.f_numpy }
- `tests.example_docstring_styles.f_numpy(a, b)`
+ ```python
+ tests.example_docstring_styles.f_numpy(a, b)
+ ```
A numpy style docstring.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|------------------|------------|
- | `a` | | The a parameter. | _required_ |
- | `b` | str | The b parameter. | _required_ |
+ | a | | The a parameter. | _required_ |
+ | b | str | The b parameter. | _required_ |
- ## Custom Admonition
+ ## Custom Admonition {.doc-section .doc-section-Custom-Admonition}
Some text.
'''
@@ -352,15 +402,131 @@
'''
# f_sphinx { #quartodoc.tests.example_docstring_styles.f_sphinx }
- `tests.example_docstring_styles.f_sphinx(a, b)`
+ ```python
+ tests.example_docstring_styles.f_sphinx(a, b)
+ ```
A sphinx style docstring.
- ## Parameters
+ ## Parameters {.doc-section .doc-section-parameters}
| Name | Type | Description | Default |
|--------|--------|------------------|------------|
- | `a` | int | The a parameter. | _required_ |
- | `b` | str | The b parameter. | _required_ |
+ | a | int | The a parameter. | _required_ |
+ | b | str | The b parameter. | _required_ |
+ '''
+# ---
+# name: test_render_numpydoc_section_return[int\n A description.]
+ '''
+ Code
+ Parameters
+ ---
+ int
+ A description.
+
+ Returns
+ ---
+ int
+ A description.
+
+ Attributes
+ ---
+ int
+ A description.
+
+ Default
+ # Parameters {.doc-section .doc-section-parameters}
+
+ | Name | Type | Description | Default |
+ |--------|--------|----------------|------------|
+ | int | | A description. | _required_ |
+
+ # Returns {.doc-section .doc-section-returns}
+
+ | Name | Type | Description |
+ |--------|--------|----------------|
+ | | int | A description. |
+
+ # Attributes {.doc-section .doc-section-attributes}
+
+ | Name | Type | Description |
+ |--------|--------|----------------|
+ | int | | A description. |
+
+ List
+ # Parameters {.doc-section .doc-section-parameters}
+
+ [**int**]{.parameter-name} [:]{.parameter-annotation-sep} []{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
+
+ # Returns {.doc-section .doc-section-returns}
+
+ []{.parameter-name} [:]{.parameter-annotation-sep} [int]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
+
+ # Attributes {.doc-section .doc-section-attributes}
+
+ [**int**]{.parameter-name} [:]{.parameter-annotation-sep} []{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
+ '''
+# ---
+# name: test_render_numpydoc_section_return[name: int\n A description.]
+ '''
+ Code
+ Parameters
+ ---
+ name: int
+ A description.
+
+ Returns
+ ---
+ name: int
+ A description.
+
+ Attributes
+ ---
+ name: int
+ A description.
+
+ Default
+ # Parameters {.doc-section .doc-section-parameters}
+
+ | Name | Type | Description | Default |
+ |--------|--------|----------------|------------|
+ | name | | A description. | _required_ |
+
+ # Returns {.doc-section .doc-section-returns}
+
+ | Name | Type | Description |
+ |--------|--------|----------------|
+ | name | int | A description. |
+
+ # Attributes {.doc-section .doc-section-attributes}
+
+ | Name | Type | Description |
+ |--------|--------|----------------|
+ | name | int | A description. |
+
+ List
+ # Parameters {.doc-section .doc-section-parameters}
+
+ [**name**]{.parameter-name} [:]{.parameter-annotation-sep} []{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
+
+ # Returns {.doc-section .doc-section-returns}
+
+ [**name**]{.parameter-name} [:]{.parameter-annotation-sep} [int]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
+
+ # Attributes {.doc-section .doc-section-attributes}
+
+ [**name**]{.parameter-name} [:]{.parameter-annotation-sep} [int]{.parameter-annotation} [ = ]{.parameter-default-sep} [None]{.parameter-default}
+
+ : A description.
'''
# ---
diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py
index c789e46..d02b0e2 100644
--- a/quartodoc/tests/test_renderers.py
+++ b/quartodoc/tests/test_renderers.py
@@ -1,10 +1,17 @@
import pytest
+from quartodoc._griffe_compat import dataclasses as dc
from quartodoc._griffe_compat import docstrings as ds
from quartodoc._griffe_compat import expressions as exp
from quartodoc.renderers import MdRenderer
from quartodoc import layout, get_object, blueprint, Auto
+from textwrap import indent
+
+
+def indented_sections(**kwargs: str):
+ return "\n\n".join([f"{k}\n" + indent(v, " " * 4) for k, v in kwargs.items()])
+
@pytest.fixture
def renderer():
@@ -15,7 +22,7 @@ def test_render_param_kwargs(renderer):
f = get_object("quartodoc.tests.example_signature.no_annotations")
res = renderer.render(f.parameters)
- assert res == "a, b=1, *args, c, d=2, **kwargs"
+ assert ", ".join(res) == "a, b=1, *args, c, d=2, **kwargs"
def test_render_param_kwargs_annotated():
@@ -25,8 +32,8 @@ def test_render_param_kwargs_annotated():
res = renderer.render(f.parameters)
assert (
- res
- == "a: int, b: int = 1, *args: list\[str\], c: int, d: int, **kwargs: dict\[str, str\]"
+ ", ".join(res)
+ == "a: int, b: int = 1, *args: list[str], c: int, d: int, **kwargs: dict[str, str]"
)
@@ -43,7 +50,7 @@ def test_render_param_kwonly(src, dst, renderer):
f = get_object("quartodoc.tests", src)
res = renderer.render(f.parameters)
- assert res == dst
+ assert ", ".join(res) == dst
@pytest.mark.parametrize(
@@ -101,7 +108,9 @@ def test_render_doc_attribute(renderer):
res = renderer.render(attr)
print(res)
- assert res == ["abc", r"Optional\[\]", "xyz"]
+ assert res.name == "abc"
+ assert res.annotation == "Optional\[\]"
+ assert res.description == "xyz"
def test_render_doc_section_admonition(renderer):
@@ -194,3 +203,32 @@ def test_render_doc_signature_name_alias_of_alias(snapshot, renderer):
res = renderer.render(bp)
assert res == snapshot
+
+
+@pytest.mark.parametrize(
+ "doc",
+ [
+ """name: int\n A description.""",
+ """int\n A description.""",
+ ],
+)
+def test_render_numpydoc_section_return(snapshot, doc):
+ from quartodoc.parsers import get_parser_defaults
+ from griffe import Parser
+
+ full_doc = (
+ f"""Parameters\n---\n{doc}\n\nReturns\n---\n{doc}\n\nAttributes\n---\n{doc}"""
+ )
+
+ el = dc.Docstring(
+ value=full_doc, parser=Parser.numpy, parser_options=get_parser_defaults("numpy")
+ )
+
+ assert el.parsed is not None and len(el.parsed) == 3
+
+ res_default = MdRenderer().render(el)
+ res_list = MdRenderer(table_style="description-list").render(el)
+
+ assert snapshot == indented_sections(
+ Code=full_doc, Default=res_default, List=res_list
+ )