From f816c64e4ad492c94a8c14eb5c99adef86c524b5 Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Wed, 30 Aug 2023 14:59:40 -0400 Subject: [PATCH] feat: show_source config option --- docs/_quarto.yml | 3 ++- quartodoc/builder/blueprint.py | 7 ++++- quartodoc/layout.py | 6 +++++ quartodoc/renderers/base.py | 13 +++++++++ quartodoc/renderers/md_renderer.py | 27 +++++++++++++++---- .../tests/__snapshots__/test_renderers.ambr | 24 +++++++++++++++++ quartodoc/tests/example_docstring_styles.py | 9 +++++++ quartodoc/tests/test_renderers.py | 22 +++++++++++++++ 8 files changed, 104 insertions(+), 7 deletions(-) diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 80b1d0d7..ec41e4e9 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -87,7 +87,8 @@ quartodoc: These functions fetch and analyze python objects, including parsing docstrings. They prepare a basic representation of your doc site that can be rendered and built. contents: - - Auto + - name: Auto + show_source: true - blueprint - collect - get_object diff --git a/quartodoc/builder/blueprint.py b/quartodoc/builder/blueprint.py index 695da8ab..de5cd489 100644 --- a/quartodoc/builder/blueprint.py +++ b/quartodoc/builder/blueprint.py @@ -327,7 +327,12 @@ def enter(self, el: Auto): is_flat = el.children == ChoicesChildren.flat return Doc.from_griffe( - el.name, obj, children, flat=is_flat, signature_path=el.signature_path + el.name, + obj, + children, + flat=is_flat, + signature_path=el.signature_path, + show_source=el.show_source, ) @staticmethod diff --git a/quartodoc/layout.py b/quartodoc/layout.py index d10f4555..77970eb3 100644 --- a/quartodoc/layout.py +++ b/quartodoc/layout.py @@ -224,6 +224,7 @@ class AutoOptions(_Base): exclude: Optional[str] = None dynamic: Union[None, bool, str] = None children: ChoicesChildren = ChoicesChildren.embedded + show_source: bool = False package: Union[str, None, MISSING] = MISSING() member_options: Optional["AutoOptions"] = None @@ -270,6 +271,8 @@ class Auto(AutoOptions): to return an alias for that object. children: Style for presenting members. Either separate, embedded, or flat. + show_source: + Whether to show the source code for the object. package: If specified, object lookup will be relative to this path. member_options: @@ -336,6 +339,7 @@ class Doc(_Docable): obj: Union[dc.Object, dc.Alias] anchor: str signature_path: SignatureOptions = "relative" + show_source: bool = False class Config: arbitrary_types_allowed = True @@ -350,6 +354,7 @@ def from_griffe( anchor: str = None, flat: bool = False, signature_path: str = "relative", + show_source: bool = False, ): if members is None: members = [] @@ -362,6 +367,7 @@ def from_griffe( "obj": obj, "anchor": anchor, "signature_path": signature_path, + "show_source": show_source, } if kind == "function": diff --git a/quartodoc/renderers/base.py b/quartodoc/renderers/base.py index 8802b299..c1467282 100644 --- a/quartodoc/renderers/base.py +++ b/quartodoc/renderers/base.py @@ -1,4 +1,5 @@ import re +import html from plum import dispatch @@ -10,6 +11,18 @@ def escape(val: str): return f"`{val}`" +def escape_source(source: str): + """Escape python source code for adding to a qmd file. + + Since quarto looks for ```{python} blocks, even in indented markdown, + we need to be careful to ensure it doesn't attempt to execute parts of + python source code (since the output will look wrong and weird). + """ + + # escape html characters, and replace ` with its html encoding + return html.escape(source, quote=False).replace("`", "`") + + def sanitize(val: str, allow_markdown=False): # sanitize common tokens that break tables res = val.replace("\n", " ").replace("|", "\\|") diff --git a/quartodoc/renderers/md_renderer.py b/quartodoc/renderers/md_renderer.py index 582396ef..b8d61e4c 100644 --- a/quartodoc/renderers/md_renderer.py +++ b/quartodoc/renderers/md_renderer.py @@ -11,7 +11,7 @@ from typing import Tuple, Union, Optional from quartodoc import layout -from .base import Renderer, escape, sanitize, convert_rst_link_to_md +from .base import Renderer, escape, escape_source, sanitize, convert_rst_link_to_md try: @@ -176,6 +176,22 @@ def render_header(self, el: layout.Doc): # e.g. get_object, rather than quartodoc.get_object _anchor = f"{{ #{el.obj.path} }}" return f"{'#' * self.crnt_header_level} {_str_dispname} {_anchor}" + + # render source ----------------------------------------------------------- + + @dispatch + def render_source(self, el: Union[dc.Object, dc.Alias]): + """Render the source code for an object.""" + + # TODO: what happens if no source + code = escape_source(el.source) + + code_block = f"
{code}
" + + # TODO: could use el.relative_filepath to show file path, but + # this does it relative to the current working directory, so if + # your cwd is a docs folder, surprising things can happen. + return f"
\nShow source\n{code_block}\n
" # render method ----------------------------------------------------------- @@ -330,20 +346,21 @@ def render(self, el: Union[layout.DocClass, layout.DocModule]): str_sig = self.signature(el) sig_part = [str_sig] if self.show_signature else [] + source = [self.render_source(el.obj)] if el.show_source else [] body = self.render(el.obj) - return "\n\n".join([title, *sig_part, body, *attr_docs, *class_docs, *meth_docs]) + return "\n\n".join([title, *sig_part, body, *source, *attr_docs, *class_docs, *meth_docs]) @dispatch def render(self, el: Union[layout.DocFunction, layout.DocAttribute]): title = self.render_header(el) - str_sig = self.signature(el) - sig_part = [str_sig] if self.show_signature else [] + sig_part = [self.signature(el)] if self.show_signature else [] + source = [self.render_source(el.obj)] if el.show_source else [] - return "\n\n".join([title, *sig_part, self.render(el.obj)]) + return "\n\n".join([title, *sig_part, self.render(el.obj), *source]) # render griffe objects =================================================== diff --git a/quartodoc/tests/__snapshots__/test_renderers.ambr b/quartodoc/tests/__snapshots__/test_renderers.ambr index 4e4026a8..fb9f334f 100644 --- a/quartodoc/tests/__snapshots__/test_renderers.ambr +++ b/quartodoc/tests/__snapshots__/test_renderers.ambr @@ -232,6 +232,30 @@ A function ''' # --- +# name: test_render_doc_show_source + ''' + # f_quarto_block_in_docstring { #quartodoc.tests.example_docstring_styles.f_quarto_block_in_docstring } + + `tests.example_docstring_styles.f_quarto_block_in_docstring(a, b)` + + A quarto style docstring. + + ```{python} + 1 < 2 + ``` + +
+ Show source +
def f_quarto_block_in_docstring(a, b: str):
+      """A quarto style docstring.
+  
+      ```{python}
+      1 < 2
+      ```
+      """
+
+ ''' +# --- # name: test_render_doc_signature_path ''' # example.a_func { #quartodoc.tests.example.a_func } diff --git a/quartodoc/tests/example_docstring_styles.py b/quartodoc/tests/example_docstring_styles.py index c6389492..27129fc6 100644 --- a/quartodoc/tests/example_docstring_styles.py +++ b/quartodoc/tests/example_docstring_styles.py @@ -43,3 +43,12 @@ def f_numpy_with_linebreaks(a, b: str): The b parameter. """ + + +def f_quarto_block_in_docstring(a, b: str): + """A quarto style docstring. + + ```{python} + 1 < 2 + ``` + """ diff --git a/quartodoc/tests/test_renderers.py b/quartodoc/tests/test_renderers.py index 60609b1c..786b1f2e 100644 --- a/quartodoc/tests/test_renderers.py +++ b/quartodoc/tests/test_renderers.py @@ -155,3 +155,25 @@ def test_render_doc_signature_path(snapshot, renderer): res = renderer.render(bp) assert res == snapshot + + +def test_render_source(renderer): + obj = get_object("quartodoc.tests.example.a_func") + res = renderer.render_source(obj) + + code = '''\ +def a_func(): + """A function"""\ +''' + + res == f"
{code}
" + + +def test_render_doc_show_source(renderer, snapshot): + package = "quartodoc.tests.example_docstring_styles" + auto = Auto(name="f_quarto_block_in_docstring", package=package, show_source=True) + bp = blueprint(auto) + + res = renderer.render(bp) + + assert res == snapshot