From 9acaa659a1b594a965f762add8aa468096c47795 Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Tue, 23 May 2023 13:39:31 -0700 Subject: [PATCH 01/13] add help to entrypoints --- quartodoc/__main__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/quartodoc/__main__.py b/quartodoc/__main__.py index 54c8a49f..4d321e85 100644 --- a/quartodoc/__main__.py +++ b/quartodoc/__main__.py @@ -34,13 +34,13 @@ def chdir(new_dir): finally: os.chdir(prev) - @click.group() def cli(): pass -@click.command() +@click.command(help='Build the reference api docs automatically according ' + 'to the configuration in _quarto.yml.') @click.argument("config", default="_quarto.yml") @click.option("--filter", nargs=1, default="*") @click.option("--dry-run", is_flag=True, default=False) @@ -59,10 +59,19 @@ def build(config, filter, dry_run, verbose): builder.build(filter=filter) -@click.command() +@click.command(short_help='Generate inventory files that the Quarto ' + '`interlink` extension can use to auto-link to other docs.') @click.argument("config", default="_quarto.yml") @click.option("--dry-run", is_flag=True, default=False) def interlinks(config, dry_run): + """ + Generate inventory files that the Quarto `interlink` extension can use to + auto-link to other docs. + + The files are stored in a cache directory, which defaults to _inv. + The Quarto extension `interlinks` will look for these files in the cache + and add links to your docs accordingly. + """ cfg = yaml.safe_load(open(config)) interlinks = cfg.get("interlinks", None) @@ -85,7 +94,6 @@ def interlinks(config, dry_run): p_dst = p_root / cache / f"{k}_objects.json" p_dst.parent.mkdir(exist_ok=True, parents=True) - convert_inventory(inv, p_dst) From 5c338aa239e9ca7e0aee9ed00f9a5a16e2cbc6cb Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Tue, 23 May 2023 13:40:07 -0700 Subject: [PATCH 02/13] add type hint --- quartodoc/renderers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartodoc/renderers/base.py b/quartodoc/renderers/base.py index 0bea302d..df64ddf2 100644 --- a/quartodoc/renderers/base.py +++ b/quartodoc/renderers/base.py @@ -10,7 +10,7 @@ def escape(val: str): return f"`{val}`" -def sanitize(val: str): +def sanitize(val: str) -> str: return ( val.replace("\n", " ") .replace("|", "\\|") From 6dfb4b6268455bee216ec4791a9e45d8d05a393f Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Tue, 23 May 2023 13:40:33 -0700 Subject: [PATCH 03/13] enable interlinks --- quartodoc/renderers/md_renderer.py | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index 601495d7..f892cca7 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -95,32 +95,37 @@ def _fetch_object_dispname(self, el: "dc.Alias | dc.Object"): raise ValueError(f"Unsupported display_name: `{self.display_name}`") - def render_annotation(self, el: "str | dc.Name | dc.Expression | None"): - """Special hook for rendering a type annotation. + # render_annotation method -------------------------------------------------------- + @dispatch + def render_annotation(self, el: str) -> str: + """Special hook for rendering a type annotation. Parameters ---------- el: An object representing a type annotation. - """ - - if isinstance(el, (type(None), str)): - return el - + return el + + @dispatch + def render_annotation(self, el: None) -> str: + return "" + + @dispatch + def render_annotation(self, el: dc.Name) -> str: # TODO: maybe there is a way to get tabulate to handle this? # unescaped pipes screw up table formatting - if isinstance(el, dc.Name): - return sanitize(el.source) + return f"[{sanitize(el.source)}](`{el.full}`)" - return sanitize(el.full) + @dispatch + def render_annotation(self, el: dc.Expression) -> str: + return "".join(map(self.render_annotation, el)) # signature method -------------------------------------------------------- @dispatch def signature(self, el: dc.Alias, source: Optional[dc.Alias] = None): """Return a string representation of an object's signature.""" - return self.signature(el.target, el) @dispatch @@ -373,9 +378,9 @@ def render(self, el: dc.Parameter): annotation = self.render_annotation(el.annotation) if self.show_signature_annotations: if annotation and has_default: - res = f"{glob}{el.name}: {el.annotation} = {el.default}" + res = f"{glob}{el.name}: {annotation} = {el.default}" elif annotation: - res = f"{glob}{el.name}: {el.annotation}" + res = f"{glob}{el.name}: {annotation}" elif has_default: res = f"{glob}{el.name}={el.default}" else: @@ -403,7 +408,8 @@ def render(self, el: ds.DocstringSectionText): def render(self, el: ds.DocstringSectionParameters): rows = list(map(self.render, el.value)) header = ["Name", "Type", "Description", "Default"] - + # if rows[1][2].startswith("(Deprecated). A function name"): + # import ipdb; ipdb.set_trace() return tabulate(rows, header, tablefmt="github") @dispatch From b27c87f34acead2f3c2b203401d60ab7725603bc Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Tue, 23 May 2023 14:02:31 -0700 Subject: [PATCH 04/13] fix test to reflect interlinks --- quartodoc/tests/test_renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index af54f584..080d58a5 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -26,7 +26,7 @@ def test_render_param_kwargs_annotated(): assert ( res - == "a: int, b: int = 1, *args: list\\[str\\], c: int, d: int, **kwargs: dict\\[str, str\\]" + == "a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" ) From baac169bc364692d4a50aa380b29e79ef889b8f8 Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Fri, 18 Aug 2023 09:31:16 -0700 Subject: [PATCH 05/13] fix tests --- .flake8 | 2 + quartodoc/renderers/md_renderer.py | 89 +++++++++++-------- .../tests/__snapshots__/test_renderers.ambr | 26 +++--- quartodoc/tests/test_renderers.py | 9 +- 4 files changed, 70 insertions(+), 56 deletions(-) diff --git a/.flake8 b/.flake8 index 83e8e340..5faa395c 100644 --- a/.flake8 +++ b/.flake8 @@ -8,3 +8,5 @@ ignore = W503, # redefinition of unused function name F811 +exclude = + test_* \ No newline at end of file diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index ae9cf4fd..0b617fe9 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -27,7 +27,6 @@ def _has_attr_section(el: dc.Docstring | None): return any([isinstance(x, ds.DocstringSectionAttributes) for x in el.parsed]) - class MdRenderer(Renderer): """Render docstrings to markdown. @@ -66,8 +65,7 @@ def __init__( show_signature_annotations: bool = False, display_name: str = "relative", hook_pre=None, - use_interlinks = False, - + use_interlinks=False, ): self.header_level = header_level self.show_signature = show_signature @@ -79,14 +77,13 @@ def __init__( self.crnt_header_level = self.header_level @contextmanager - def _increment_header(self, n = 1): + def _increment_header(self, n=1): self.crnt_header_level += n try: yield - finally: + finally: self.crnt_header_level -= n - def _fetch_object_dispname(self, el: "dc.Alias | dc.Object"): # TODO: copied from Builder, should move into util function if self.display_name == "name": @@ -101,13 +98,12 @@ def _fetch_object_dispname(self, el: "dc.Alias | dc.Object"): return el.canonical_path raise ValueError(f"Unsupported display_name: `{self.display_name}`") - + def _render_table(self, rows, headers): table = tabulate(rows, headers=headers, tablefmt="github") return table - # render_annotation method -------------------------------------------------------- @dispatch @@ -119,11 +115,11 @@ def render_annotation(self, el: str) -> str: An object representing a type annotation. """ return el - + @dispatch def render_annotation(self, el: None) -> str: return "" - + @dispatch def render_annotation(self, el: expr.Name) -> str: # TODO: maybe there is a way to get tabulate to handle this? @@ -142,18 +138,21 @@ def signature(self, el: dc.Alias, source: Optional[dc.Alias] = None): return self.signature(el.target, el) @dispatch - def signature(self, el: Union[dc.Class, dc.Function], source: Optional[dc.Alias] = None): + def signature( + self, el: Union[dc.Class, dc.Function], source: Optional[dc.Alias] = None + ): name = self._fetch_object_dispname(source or el) pars = self.render(el.parameters) return f"`{name}({pars})`" @dispatch - def signature(self, el: Union[dc.Module, dc.Attribute], source: Optional[dc.Alias] = None): + def signature( + self, el: Union[dc.Module, dc.Attribute], source: Optional[dc.Alias] = None + ): name = self._fetch_object_dispname(source or el) return f"`{name}`" - @dispatch def render_header(self, el: layout.Doc): """Render the header of a docstring, including any anchors.""" @@ -164,7 +163,6 @@ def render_header(self, el: layout.Doc): _anchor = f"{{ #{el.obj.path} }}" return f"{'#' * self.crnt_header_level} {_str_dispname} {_anchor}" - # render method ----------------------------------------------------------- @dispatch @@ -199,7 +197,7 @@ def render(self, el: layout.Section): body = list(map(self.render, el.contents)) return "\n\n".join([section_top, *body]) - + @dispatch def render(self, el: layout.Interlaced): # render a sequence of objects with like-sections together. @@ -221,7 +219,6 @@ def render(self, el: layout.Interlaced): if first_doc.obj.docstring is None: raise ValueError("The first element of Interlaced must have a docstring.") - str_title = self.render_header(first_doc) str_sig = "\n\n".join(map(self.signature, objs)) str_body = [] @@ -265,7 +262,6 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]): raw_meths = [x for x in el.members if x.obj.is_function] raw_classes = [x for x in el.members if x.obj.is_class] - header = "| Name | Description |\n| --- | --- |" # attribute summary table ---- @@ -274,16 +270,16 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]): # TODO: for now, we skip making an attribute table on classes, unless # they contain an attributes section in the docstring if ( - raw_attrs - and not _has_attr_section(el.obj.docstring) - # TODO: what should backwards compat be? - # and not isinstance(el, layout.DocClass) - ): + raw_attrs + and not _has_attr_section(el.obj.docstring) + # TODO: what should backwards compat be? + # and not isinstance(el, layout.DocClass) + ): _attrs_table = "\n".join(map(self.summarize, raw_attrs)) attrs = f"{sub_header} Attributes\n\n{header}\n{_attrs_table}" attr_docs.append(attrs) - + # classes summary table ---- if raw_classes: _summary_table = "\n".join(map(self.summarize, raw_classes)) @@ -293,14 +289,19 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]): n_incr = 1 if el.flat else 2 with self._increment_header(n_incr): - class_docs.extend([self.render(x) for x in raw_classes if isinstance(x, layout.Doc)]) + class_docs.extend( + [ + self.render(x) + for x in raw_classes + if isinstance(x, layout.Doc) + ] + ) # method summary table ---- if raw_meths: _summary_table = "\n".join(map(self.summarize, raw_meths)) section_name = ( - "Methods" if isinstance(el, layout.DocClass) - else "Functions" + "Methods" if isinstance(el, layout.DocClass) else "Functions" ) objs = f"{sub_header} {section_name}\n\n{header}\n{_summary_table}" meth_docs.append(objs) @@ -308,7 +309,9 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]): # TODO use context manager, or context variable? n_incr = 1 if el.flat else 2 with self._increment_header(n_incr): - meth_docs.extend([self.render(x) for x in raw_meths if isinstance(x, layout.Doc)]) + meth_docs.extend( + [self.render(x) for x in raw_meths if isinstance(x, layout.Doc)] + ) body = self.render(el.obj) return "\n\n".join([title, body, *attr_docs, *class_docs, *meth_docs]) @@ -366,11 +369,16 @@ def render(self, el: dc.Parameters): # index for final positionly only args (via /) try: - pos_only = max([ii for ii, el in enumerate(el) if el.kind == dc.ParameterKind.positional_only]) + pos_only = max( + [ + ii + for ii, el in enumerate(el) + if el.kind == dc.ParameterKind.positional_only + ] + ) except ValueError: pos_only = None - pars = list(map(self.render, el)) # insert a single `*,` argument to represent the shift to kw only arguments, @@ -378,7 +386,7 @@ def render(self, el: dc.Parameters): if ( kw_only is not None and kw_only > 0 - and el[kw_only-1].kind != dc.ParameterKind.var_positional + and el[kw_only - 1].kind != dc.ParameterKind.var_positional ): pars.insert(kw_only, sanitize("*")) @@ -461,7 +469,7 @@ def render(self, el: ds.DocstringAttribute): row = [ sanitize(el.name), self.render_annotation(el.annotation), - sanitize(el.description or "", allow_markdown=True) + sanitize(el.description or "", allow_markdown=True), ] return row @@ -530,7 +538,6 @@ def render(self, el: Union[ds.DocstringReturn, ds.DocstringRaise]): def render(self, el): raise NotImplementedError(f"{type(el)}") - # Summarize =============================================================== # this method returns a summary description, such as a table summarizing a # layout.Section, or a row in the table for layout.Page or layout.DocFunction. @@ -569,14 +576,16 @@ def summarize(self, el: layout.Section): str_func_table = "\n".join([thead, *rendered]) return f"{header}\n\n{str_func_table}" - + return header @dispatch def summarize(self, el: layout.Page): if el.summary is not None: # TODO: assumes that files end with .qmd - return self._summary_row(f"[{el.summary.name}]({el.path}.qmd)", el.summary.desc) + return self._summary_row( + f"[{el.summary.name}]({el.path}.qmd)", el.summary.desc + ) if len(el.contents) > 1 and not el.flatten: raise ValueError( @@ -591,8 +600,8 @@ def summarize(self, el: layout.Page): @dispatch def summarize(self, el: layout.MemberPage): # TODO: model should validate these only have a single entry - return self.summarize(el.contents[0], el.path, shorten = True) - + return self.summarize(el.contents[0], el.path, shorten=True) + @dispatch def summarize(self, el: layout.Interlaced, *args, **kwargs): rows = [self.summarize(doc, *args, **kwargs) for doc in el.contents] @@ -600,7 +609,9 @@ def summarize(self, el: layout.Interlaced, *args, **kwargs): return "\n".join(rows) @dispatch - def summarize(self, el: layout.Doc, path: Optional[str] = None, shorten: bool = False): + def summarize( + self, el: layout.Doc, path: Optional[str] = None, shorten: bool = False + ): if path is None: link = f"[{el.name}](#{el.anchor})" else: @@ -612,8 +623,8 @@ def summarize(self, el: layout.Doc, path: Optional[str] = None, shorten: bool = @dispatch def summarize(self, el: layout.Link): - description = self.summarize(el.obj) - return self._summary_row(f"[](`{el.name}`)", description) + description = self.summarize(el.obj) + return self._summary_row(f"[](`{el.name}`)", description) @dispatch def summarize(self, obj: Union[dc.Object, dc.Alias]) -> str: diff --git a/quartodoc/tests/__snapshots__/test_renderers.ambr b/quartodoc/tests/__snapshots__/test_renderers.ambr index 6b653358..551d392c 100644 --- a/quartodoc/tests/__snapshots__/test_renderers.ambr +++ b/quartodoc/tests/__snapshots__/test_renderers.ambr @@ -12,10 +12,10 @@ ## Parameters - | Name | Type | Description | Default | - |--------|--------|----------------------|------------| - | `x` | str | Uses signature type. | _required_ | - | `y` | int | Uses manual type. | _required_ | + | Name | Type | Description | Default | + |--------|--------------|----------------------|------------| + | `x` | [str](`str`) | Uses signature type. | _required_ | + | `y` | [int](`int`) | Uses manual type. | _required_ | ## Attributes @@ -51,10 +51,10 @@ ## Parameters - | Name | Type | Description | Default | - |--------|--------|----------------------|------------| - | `x` | str | Uses signature type. | _required_ | - | `y` | int | Uses manual type. | _required_ | + | Name | Type | Description | Default | + |--------|--------------|----------------------|------------| + | `x` | [str](`str`) | Uses signature type. | _required_ | + | `y` | [int](`int`) | Uses manual type. | _required_ | ## Attributes @@ -87,11 +87,11 @@ ## Attributes - | Name | Type | Description | - |--------|--------|---------------------| - | x | str | Uses signature type | - | y | int | Uses manual type | - | z | float | Defined in init | + | Name | Type | Description | + |--------|------------------|---------------------| + | x | [str](`str`) | Uses signature type | + | y | [int](`int`) | Uses manual type | + | z | [float](`float`) | Defined in init | ''' # --- # name: test_render_doc_module[embedded] diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index a1d9eb11..33e8fb2a 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -25,8 +25,8 @@ def test_render_param_kwargs_annotated(): res = renderer.render(f.parameters) assert ( - res - == "a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" + res # noqa: W605 + == "a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" # noqa: W605 ) @@ -94,13 +94,14 @@ def test_render_doc_attribute(renderer): attr = ds.DocstringAttribute( name="abc", description="xyz", - annotation=exp.Expression(exp.Name("Optional", full="Optional"), "[", "]"), + annotation=exp.Expression(exp.Name("Optional[]", full="Optional")), value=1, ) res = renderer.render(attr) + print(res) - assert res == ["abc", r"Optional\[\]", "xyz"] + assert res == ["abc", "[Optional\\[\\]](`Optional`)", "xyz"] # noqa @pytest.mark.parametrize("children", ["embedded", "flat"]) From aa1ce3b23aa4a8da8cc5f03a169597dfa9896dfa Mon Sep 17 00:00:00 2001 From: Hamel Husain Date: Fri, 18 Aug 2023 09:31:40 -0700 Subject: [PATCH 06/13] pre-commit --- .flake8 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.flake8 b/.flake8 index 5faa395c..0439fa0b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -exclude = docs +exclude = docs, test_*, .flake8 max-line-length = 90 ignore = # line too long @@ -8,5 +8,3 @@ ignore = W503, # redefinition of unused function name F811 -exclude = - test_* \ No newline at end of file From 672e55589f9ca1d8ef7f610e0389eae07e9cc955 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 15:02:25 -0400 Subject: [PATCH 07/13] fix: use raw string in test --- quartodoc/tests/test_renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index 33e8fb2a..ad4e8a53 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -26,7 +26,7 @@ def test_render_param_kwargs_annotated(): assert ( res # noqa: W605 - == "a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" # noqa: W605 + == r"a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" # noqa: W605 ) @@ -101,7 +101,7 @@ def test_render_doc_attribute(renderer): res = renderer.render(attr) print(res) - assert res == ["abc", "[Optional\\[\\]](`Optional`)", "xyz"] # noqa + assert res == ["abc", r"[Optional\[\]](`Optional`)", "xyz"] # noqa @pytest.mark.parametrize("children", ["embedded", "flat"]) From b3bf95bb45e44cd87fb14d52760b1cbe8ef695a5 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 15:22:31 -0400 Subject: [PATCH 08/13] tests: restore modified test --- quartodoc/tests/test_renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index ad4e8a53..a0176b9c 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -19,14 +19,14 @@ def test_render_param_kwargs(renderer): def test_render_param_kwargs_annotated(): - renderer = MdRenderer(show_signature_annotations=True) + renderer = MdRenderer() f = get_object("quartodoc.tests.example_signature.yes_annotations") res = renderer.render(f.parameters) assert ( - res # noqa: W605 - == r"a: \[int\](`int`), b: \[int\](`int`) = 1, *args: \[list\](`list`)\[\[str\](`str`)\], c: \[int\](`int`), d: \[int\](`int`), **kwargs: \[dict\](`dict`)\[\[str\](`str`), \[str\](`str`)\]" # noqa: W605 + res + == "a: int, b: int = 1, *args: list\\[str\\], c: int, d: int, **kwargs: dict\\[str, str\\]" ) From 7ff00166b2235d58db81bacbbe804188839addd1 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 15:47:03 -0400 Subject: [PATCH 09/13] fix: add snapshot, ensure original render tests pass again --- quartodoc/renderers/md_renderer.py | 11 ++++--- .../tests/__snapshots__/test_renderers.ambr | 33 +++++++++++-------- quartodoc/tests/example_signature.py | 8 +++++ quartodoc/tests/test_renderers.py | 16 ++++++--- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index 0b617fe9..2c927b18 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -65,14 +65,14 @@ def __init__( show_signature_annotations: bool = False, display_name: str = "relative", hook_pre=None, - use_interlinks=False, + render_interlinks=False, ): self.header_level = header_level self.show_signature = show_signature self.show_signature_annotations = show_signature_annotations self.display_name = display_name self.hook_pre = hook_pre - self.use_interlinks = use_interlinks + self.render_interlinks = render_interlinks self.crnt_header_level = self.header_level @@ -124,7 +124,10 @@ def render_annotation(self, el: None) -> str: def render_annotation(self, el: expr.Name) -> str: # TODO: maybe there is a way to get tabulate to handle this? # unescaped pipes screw up table formatting - return f"[{sanitize(el.source)}](`{el.full}`)" + if self.render_interlinks: + return f"[{el.source}](`{el.full}`)" + + return el.source @dispatch def render_annotation(self, el: expr.Expression) -> str: @@ -468,7 +471,7 @@ def render(self, el: ds.DocstringSectionAttributes): def render(self, el: ds.DocstringAttribute): row = [ sanitize(el.name), - self.render_annotation(el.annotation), + sanitize(self.render_annotation(el.annotation)), sanitize(el.description or "", allow_markdown=True), ] return row diff --git a/quartodoc/tests/__snapshots__/test_renderers.ambr b/quartodoc/tests/__snapshots__/test_renderers.ambr index 551d392c..dd852962 100644 --- a/quartodoc/tests/__snapshots__/test_renderers.ambr +++ b/quartodoc/tests/__snapshots__/test_renderers.ambr @@ -1,4 +1,11 @@ # serializer version: 1 +# name: test_render_annotations_complex + ''' + # quartodoc.tests.example_signature.a_complex_signature { #quartodoc.tests.example_signature.a_complex_signature } + + `tests.example_signature.a_complex_signature(x)` + ''' +# --- # name: test_render_doc_class[embedded] ''' # quartodoc.tests.example_class.C { #quartodoc.tests.example_class.C } @@ -12,10 +19,10 @@ ## Parameters - | Name | Type | Description | Default | - |--------|--------------|----------------------|------------| - | `x` | [str](`str`) | Uses signature type. | _required_ | - | `y` | [int](`int`) | Uses manual type. | _required_ | + | Name | Type | Description | Default | + |--------|--------|----------------------|------------| + | `x` | str | Uses signature type. | _required_ | + | `y` | int | Uses manual type. | _required_ | ## Attributes @@ -51,10 +58,10 @@ ## Parameters - | Name | Type | Description | Default | - |--------|--------------|----------------------|------------| - | `x` | [str](`str`) | Uses signature type. | _required_ | - | `y` | [int](`int`) | Uses manual type. | _required_ | + | Name | Type | Description | Default | + |--------|--------|----------------------|------------| + | `x` | str | Uses signature type. | _required_ | + | `y` | int | Uses manual type. | _required_ | ## Attributes @@ -87,11 +94,11 @@ ## Attributes - | Name | Type | Description | - |--------|------------------|---------------------| - | x | [str](`str`) | Uses signature type | - | y | [int](`int`) | Uses manual type | - | z | [float](`float`) | Defined in init | + | Name | Type | Description | + |--------|--------|---------------------| + | x | str | Uses signature type | + | y | int | Uses manual type | + | z | float | Defined in init | ''' # --- # name: test_render_doc_module[embedded] diff --git a/quartodoc/tests/example_signature.py b/quartodoc/tests/example_signature.py index f916070d..36c1f581 100644 --- a/quartodoc/tests/example_signature.py +++ b/quartodoc/tests/example_signature.py @@ -22,3 +22,11 @@ def early_args(x, *args, a, b=2, **kwargs): def late_args(x, a, b=2, *args, **kwargs): ... + + +class C: + ... + + +def a_complex_signature(x: "list[C | int | None]"): + ... diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index a0176b9c..4b792644 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -19,14 +19,14 @@ def test_render_param_kwargs(renderer): def test_render_param_kwargs_annotated(): - renderer = MdRenderer() + renderer = MdRenderer(show_signature_annotations=True) f = get_object("quartodoc.tests.example_signature.yes_annotations") res = renderer.render(f.parameters) assert ( res - == "a: int, b: int = 1, *args: list\\[str\\], c: int, d: int, **kwargs: dict\\[str, str\\]" + == "a: int, b: int = 1, *args: list\[str\], c: int, d: int, **kwargs: dict\[str, str\]" ) @@ -94,14 +94,14 @@ def test_render_doc_attribute(renderer): attr = ds.DocstringAttribute( name="abc", description="xyz", - annotation=exp.Expression(exp.Name("Optional[]", full="Optional")), + annotation=exp.Expression(exp.Name("Optional", full="Optional"), "[", "]"), value=1, ) res = renderer.render(attr) print(res) - assert res == ["abc", r"[Optional\[\]](`Optional`)", "xyz"] # noqa + assert res == ["abc", r"Optional\[\]", "xyz"] @pytest.mark.parametrize("children", ["embedded", "flat"]) @@ -112,6 +112,14 @@ def test_render_doc_module(snapshot, renderer, children): assert res == snapshot +def test_render_annotations_complex(snapshot): + renderer = MdRenderer(render_interlinks=True) + bp = blueprint(Auto(name="quartodoc.tests.example_signature.a_complex_signature")) + res = renderer.render(bp) + + assert res == snapshot + + @pytest.mark.parametrize("children", ["embedded", "flat"]) def test_render_doc_class(snapshot, renderer, children): bp = blueprint(Auto(name="quartodoc.tests.example_class.C", children=children)) From fc6f1821cc58eac60325b90c180a821ab60b7237 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 16:03:04 -0400 Subject: [PATCH 10/13] fix: move sanitization back inside render_annotations --- quartodoc/renderers/md_renderer.py | 20 ++++++++++--------- .../tests/__snapshots__/test_renderers.ambr | 8 +++++++- quartodoc/tests/example_signature.py | 7 ++++++- quartodoc/tests/test_renderers.py | 2 +- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index 2c927b18..10bee418 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -114,7 +114,7 @@ def render_annotation(self, el: str) -> str: el: An object representing a type annotation. """ - return el + return sanitize(el) @dispatch def render_annotation(self, el: None) -> str: @@ -125,9 +125,9 @@ def render_annotation(self, el: expr.Name) -> str: # TODO: maybe there is a way to get tabulate to handle this? # unescaped pipes screw up table formatting if self.render_interlinks: - return f"[{el.source}](`{el.full}`)" + return f"[{sanitize(el.source)}](`{el.full}`)" - return el.source + return sanitize(el.source) @dispatch def render_annotation(self, el: expr.Expression) -> str: @@ -415,17 +415,19 @@ def render(self, el: dc.Parameter): glob = "" annotation = self.render_annotation(el.annotation) + name = sanitize(el.name) + if self.show_signature_annotations: if annotation and has_default: - res = f"{glob}{el.name}: {annotation} = {el.default}" + res = f"{glob}{name}: {annotation} = {el.default}" elif annotation: - res = f"{glob}{el.name}: {annotation}" + res = f"{glob}{name}: {annotation}" elif has_default: - res = f"{glob}{el.name}={el.default}" + res = f"{glob}{name}={el.default}" else: - res = f"{glob}{el.name}" + res = f"{glob}{name}" - return sanitize(res) + return res # docstring parts ------------------------------------------------------------- @@ -471,7 +473,7 @@ def render(self, el: ds.DocstringSectionAttributes): def render(self, el: ds.DocstringAttribute): row = [ sanitize(el.name), - sanitize(self.render_annotation(el.annotation)), + self.render_annotation(el.annotation), sanitize(el.description or "", allow_markdown=True), ] return row diff --git a/quartodoc/tests/__snapshots__/test_renderers.ambr b/quartodoc/tests/__snapshots__/test_renderers.ambr index dd852962..9b190ec0 100644 --- a/quartodoc/tests/__snapshots__/test_renderers.ambr +++ b/quartodoc/tests/__snapshots__/test_renderers.ambr @@ -3,7 +3,13 @@ ''' # quartodoc.tests.example_signature.a_complex_signature { #quartodoc.tests.example_signature.a_complex_signature } - `tests.example_signature.a_complex_signature(x)` + `tests.example_signature.a_complex_signature(x: [list](`list`)\[[C](`quartodoc.tests.example_signature.C`) \| [int](`int`) \| None\])` + + ## Parameters + + | Name | Type | Description | Default | + |--------|--------------------------------------------------------------------------------------|-----------------|------------| + | `x` | [list](`list`)\[[C](`quartodoc.tests.example_signature.C`) \| [int](`int`) \| None\] | The x parameter | _required_ | ''' # --- # name: test_render_doc_class[embedded] diff --git a/quartodoc/tests/example_signature.py b/quartodoc/tests/example_signature.py index 36c1f581..f14185ea 100644 --- a/quartodoc/tests/example_signature.py +++ b/quartodoc/tests/example_signature.py @@ -29,4 +29,9 @@ class C: def a_complex_signature(x: "list[C | int | None]"): - ... + """ + Parameters + ---------- + x: + The x parameter + """ diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index 4b792644..12289100 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -113,7 +113,7 @@ def test_render_doc_module(snapshot, renderer, children): def test_render_annotations_complex(snapshot): - renderer = MdRenderer(render_interlinks=True) + renderer = MdRenderer(render_interlinks=True, show_signature_annotations=True) bp = blueprint(Auto(name="quartodoc.tests.example_signature.a_complex_signature")) res = renderer.render(bp) From 351b7e60cc4062c3a1da97214300ac0449c83f60 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 16:04:48 -0400 Subject: [PATCH 11/13] chore: remove unneeded flake8 header --- .flake8 | 1 - 1 file changed, 1 deletion(-) diff --git a/.flake8 b/.flake8 index 0439fa0b..c4cb973e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,3 @@ -[flake8] exclude = docs, test_*, .flake8 max-line-length = 90 ignore = From f68fa4a0bcfc2194021289b4534be7138a335c15 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 16:26:04 -0400 Subject: [PATCH 12/13] feat: add render_interlinks option to Builder --- .flake8 | 1 + docs/get-started/interlinks.qmd | 14 ++++++++++++++ quartodoc/autosummary.py | 8 ++++++++ 3 files changed, 23 insertions(+) diff --git a/.flake8 b/.flake8 index c4cb973e..0439fa0b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ +[flake8] exclude = docs, test_*, .flake8 max-line-length = 90 ignore = diff --git a/docs/get-started/interlinks.qmd b/docs/get-started/interlinks.qmd index bb3f117d..48801302 100644 --- a/docs/get-started/interlinks.qmd +++ b/docs/get-started/interlinks.qmd @@ -55,6 +55,20 @@ By default, downloaded inventory files will be saved in the `_inv` folder of you documentation directory. +### Rendering interlinks in API docs + +quartodoc can convert type annotations in function signatures to interlinks. + +In order to enable this behavior, set `render_interlinks: true` in the quartodoc config. + + +```yaml +quartodoc: + render_interlinks: true +``` + + + ## Running the interlinks filter First, build the reference for your own site, which includes an objects.json inventory: diff --git a/quartodoc/autosummary.py b/quartodoc/autosummary.py index b89a5cd8..84a7c8da 100644 --- a/quartodoc/autosummary.py +++ b/quartodoc/autosummary.py @@ -391,6 +391,9 @@ class Builder: dynamic: Whether to dynamically load all python objects. By default, objects are loaded using static analysis. + render_interlinks: + Whether to render interlinks syntax inside documented objects. Note that the + interlinks filter is required to generate the links in quarto. """ @@ -436,6 +439,7 @@ def __init__( source_dir: "str | None" = None, dynamic: bool | None = None, parser="numpy", + render_interlinks: bool = False, ): self.layout = self.load_layout(sections=sections, package=package) @@ -447,6 +451,10 @@ def __init__( self.parser = parser self.renderer = Renderer.from_config(renderer) + if render_interlinks: + # this is a top-level option, but lives on the renderer + # so we just manually set it there for now. + self.renderer.render_interlinks = render_interlinks if out_index is not None: self.out_index = out_index From 184eb5006af53a45f4e86b0479799df2b0f8ff2b Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Thu, 31 Aug 2023 17:11:54 -0400 Subject: [PATCH 13/13] docs: render interlinks in our own docs --- docs/_quarto.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 80b1d0d7..6c7a4bad 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -80,6 +80,7 @@ quartodoc: style: pkgdown dir: api package: quartodoc + render_interlinks: true sidebar: "api/_sidebar.yml" sections: - title: Preperation Functions