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 + )