From 59cee20235fb888c398561a9688d6ad4eb4d38cc Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 6 Dec 2023 10:55:04 -0500 Subject: [PATCH 1/6] Update quartodoc and griffe --- docs/_renderer.py | 6 +++--- setup.cfg | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/_renderer.py b/docs/_renderer.py index 7020ec69b..ee72ca8af 100644 --- a/docs/_renderer.py +++ b/docs/_renderer.py @@ -128,7 +128,7 @@ def render_annotation(self, el: None): return "" @dispatch - def render_annotation(self, el: exp.Expression): + def render_annotation(self, el: exp.Expr): # an expression is essentially a list[exp.Name | str] # e.g. Optional[TagList] # -> [Name(source="Optional", ...), "[", Name(...), "]"] @@ -136,9 +136,9 @@ def render_annotation(self, el: exp.Expression): return "".join(map(self.render_annotation, el)) @dispatch - def render_annotation(self, el: exp.Name): + def render_annotation(self, el: exp.ExprName): # e.g. Name(source="Optional", full="typing.Optional") - return f"[{el.source}](`{el.full}`)" + return f"[{el.path}](`{el.canonical_path}`)" @dispatch def summarize(self, el: dc.Object | dc.Alias): diff --git a/setup.cfg b/setup.cfg index 57bdecd67..44b7b81e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,8 +103,8 @@ doc = tabulate shinylive==0.1.1 pydantic==1.10 - quartodoc==0.4.1 - griffe==0.32.3 + quartodoc==0.7.1 + griffe [options.packages.find] include = shiny, shiny.* From 35f7e1bf4abb357b1ff9263f8d102d426998f578 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 6 Dec 2023 10:55:21 -0500 Subject: [PATCH 2/6] first pass at creating signatures for every function --- docs/barret.py | 146 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/barret.py diff --git a/docs/barret.py b/docs/barret.py new file mode 100644 index 000000000..a9dcf94ec --- /dev/null +++ b/docs/barret.py @@ -0,0 +1,146 @@ +# import griffe.docstrings.dataclasses as ds +from typing import Union + +import griffe.dataclasses as dc +import griffe.docstrings.dataclasses as ds +import griffe.expressions as exp +from _renderer import Renderer +from plum import dispatch +from quartodoc import MdRenderer, get_object, preview +from quartodoc.renderers.base import convert_rst_link_to_md, sanitize + +from shiny import reactive, render, ui + +# from quartodoc import Auto, blueprint, get_object, layout +# from quartodoc.renderers import MdRenderer + +# package = "quartodoc.tests.example_docstring_styles" +# auto = Auto(name=f"f_{parser}", package=package) +# bp = blueprint(auto, parser=parser) +# res = renderer.render(bp) + + +# renderer = MdRenderer() + +# obj = get_object("shiny.ui.input_action_button") +# # preview(obj) +# preview(obj.parameters) + + +class BarretFnSig(Renderer): + style = "custom_greg_styles" + + # def __init__(self, header_level: int = 1): + # self.header_level = header_level + + # @dispatch + # def render(self, el): + # raise NotImplementedError(f"Unsupported type: {type(el)}") + + @dispatch + def render(self, el: Union[dc.Alias, dc.Object]): + # preview(el) + param_str = "" + if hasattr(el, "docstring") and hasattr(el.docstring, "parsed"): + for docstring_val in el.docstring.parsed: + if isinstance(docstring_val, ds.DocstringSectionParameters): + param_str = self.render(docstring_val) + elif hasattr(el, "parameters"): + for param in el.parameters: + param_str += self.render(param) + return f"{el.name}({param_str})" + # return f"{el.name}({self.render(el.parameters)})" + # return self.render_annotation(el.annotation) + # # header = "#" * self.header_level + # # str_header = f"{header} {el.name}" + # str_header = f"## {el.name}" + # str_params = f"N PARAMETERS: {len(el.parameters)}" + # str_sections = "SECTIONS: " + self.render(el.docstring) + + # # return something pretty + # return "\n".join([str_header, str_params, str_sections]) + + # Consolidate the parameter type info into a single column + @dispatch + def render(self, el: None): + return "None" + + @dispatch + def render_annotation(self, el: exp.ExprName): + # print("exp.ExprName") + # preview(el) + return el.path + return f"[{el.path}](`{el.canonical_path}`)" + + @dispatch + def render_annotation(self, el: exp.ExprSubscript): + # print("exp.ExprSubscript") + # preview(el) + return el.path + return f"[{el.path}](`{el.canonical_path}`)" + + @dispatch + def render(self, el: ds.DocstringParameter): + # print("ds.DocstringParameter") + # preview(el) + + param = self.render(el.name) + # annotation = self.render_annotation(el.annotation) + # if annotation: + # param = f"{param}: {annotation}" + if el.default: + return None + # if el.default: + # param = f"{param} = {el.default}" + return param + + @dispatch + def render(self, el: ds.DocstringSectionParameters): + # print("ds.DocstringSectionParameters") + # preview(el) + return ", ".join( + [item for item in map(self.render, el.value) if item is not None] + ) + # rows = list(map(self.render, el.value)) + # header = ["Parameter", "Description"] + + # return self._render_table(rows, header) + + # @dispatch + # def render(self, el: dc.Docstring): + # return f"A docstring with {len(el.parsed)} pieces" + + +# TODO-barret; Add sentance in template to describe what the Relevant Function section is: "To learn more about details about the functions covered here, visit the reference links below." +# print(BarretFnSig().render(get_object("shiny:ui.input_action_button"))) +# print(BarretFnSig().render(get_object("shiny:ui.input_action_button"))) +# preview(get_object("shiny:ui")) +# print("") + + +ret = {} +for mod_name, mod in [ + ("ui", ui), + ("render", render), + ("reactive", reactive), +]: + ret[mod_name] = {} + for key, f_obj in mod.__dict__.items(): + if key.startswith("_") or key in ("AnimationOptions",): + continue + if not callable(f_obj): + continue + print(f"## {mod_name}.{key}") + signature = BarretFnSig().render(get_object(f"shiny:{mod_name}.{key}")) + ret[mod_name][key] = signature +preview(ret) + +# TODO-barret; Include link to GitHub source +# print(BarretFnSig().render(f_obj.annotation)) +# print(preview(f_obj)) + +# # get annotation of first parameter +# obj.parameters[0].annotation + +# render annotation +# print(renderer.render_annotation(obj.parameters[0].annotation)) From 596062a1150bfbc8ffd823805e98230d82c0305b Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 6 Dec 2023 10:55:37 -0500 Subject: [PATCH 3/6] Add TODO --- .github/workflows/build-docs.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yaml b/.github/workflows/build-docs.yaml index fe9055eda..bdcd6bc89 100644 --- a/.github/workflows/build-docs.yaml +++ b/.github/workflows/build-docs.yaml @@ -41,6 +41,7 @@ jobs: run: | cd docs make quartodoc + # TODO-barret add make step to update signatures? - name: Build site run: | @@ -53,15 +54,14 @@ jobs: with: path: "docs/_site" - deploy: if: github.ref == 'refs/heads/main' needs: build # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source # Deploy to the github-pages environment environment: From 850fdf25470513c50fc0618f2fb6b29f41308a9a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Fri, 15 Dec 2023 16:37:42 -0500 Subject: [PATCH 4/6] Update _signatures.py --- docs/{barret.py => _signatures.py} | 42 +++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) rename docs/{barret.py => _signatures.py} (78%) diff --git a/docs/barret.py b/docs/_signatures.py similarity index 78% rename from docs/barret.py rename to docs/_signatures.py index a9dcf94ec..936b21605 100644 --- a/docs/barret.py +++ b/docs/_signatures.py @@ -1,12 +1,17 @@ # import griffe.docstrings.dataclasses as ds +import json from typing import Union import griffe.dataclasses as dc import griffe.docstrings.dataclasses as ds import griffe.expressions as exp from _renderer import Renderer +from griffe.collections import LinesCollection, ModulesCollection +from griffe.docstrings import Parser +from griffe.loader import GriffeLoader from plum import dispatch from quartodoc import MdRenderer, get_object, preview +from quartodoc.parsers import get_parser_defaults from quartodoc.renderers.base import convert_rst_link_to_md, sanitize from shiny import reactive, render, ui @@ -27,6 +32,18 @@ # preview(obj.parameters) +loader = GriffeLoader( + docstring_parser=Parser("numpy"), + docstring_options=get_parser_defaults("numpy"), + modules_collection=ModulesCollection(), + lines_collection=LinesCollection(), +) + + +def fast_get_object(path: str): + return get_object(path, loader=loader) + + class BarretFnSig(Renderer): style = "custom_greg_styles" @@ -112,9 +129,9 @@ def render(self, el: ds.DocstringSectionParameters): # TODO-barret; Add sentance in template to describe what the Relevant Function section is: "To learn more about details about the functions covered here, visit the reference links below." -# print(BarretFnSig().render(get_object("shiny:ui.input_action_button"))) -# print(BarretFnSig().render(get_object("shiny:ui.input_action_button"))) -# preview(get_object("shiny:ui")) +# print(BarretFnSig().render(fast_get_object("shiny:ui.input_action_button"))) +# print(BarretFnSig().render(fast_get_object("shiny:ui.input_action_button"))) +# preview(fast_get_object("shiny:ui")) # print("") @@ -124,17 +141,30 @@ def render(self, el: ds.DocstringSectionParameters): ("render", render), ("reactive", reactive), ]: + print(f"## Collecting signatures: {mod_name}") ret[mod_name] = {} for key, f_obj in mod.__dict__.items(): if key.startswith("_") or key in ("AnimationOptions",): continue if not callable(f_obj): continue - print(f"## {mod_name}.{key}") - signature = BarretFnSig().render(get_object(f"shiny:{mod_name}.{key}")) + # print(f"## {mod_name}.{key}") + signature = BarretFnSig().render(fast_get_object(f"shiny:{mod_name}.{key}")) ret[mod_name][key] = signature -preview(ret) +# preview(ret) + +print("## Saving signatures") + + +with open("objects.json") as infile: + objects_content = json.load(infile) +objects_content["signatures"] = ret +# Serializing json +json_object = json.dumps(objects_content) +# Writing to sample.json +with open("objects.json", "w") as outfile: + outfile.write(json_object) # TODO-barret; Include link to GitHub source # print(BarretFnSig().render(f_obj.annotation)) # print(preview(f_obj)) From 4861979c793cbcf68c6ac61030208432436ede5c Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 19 Dec 2023 12:48:00 -0500 Subject: [PATCH 5/6] Store function information into `objects.json` to be retrieved in documentation --- docs/Makefile | 7 +- docs/{_signatures.py => _func_info.py} | 127 +++++++++++++++++++------ 2 files changed, 104 insertions(+), 30 deletions(-) rename docs/{_signatures.py => _func_info.py} (62%) diff --git a/docs/Makefile b/docs/Makefile index 91fabe65c..26a061e06 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,10 +46,15 @@ deps: $(PYBIN) ## Install build dependencies $(PYBIN)/pip install pip --upgrade $(PYBIN)/pip install -e ..[doc] -quartodoc: $(PYBIN) ## Build qmd files for API docs +quartodoc_impl: $(PYBIN) ## Build qmd files for API docs . $(PYBIN)/activate \ && quartodoc interlinks \ && quartodoc build --config _quartodoc.yml --verbose +quartodoc_func_info: $(PYBIN) ## Attaches func information to objects.json + . $(PYBIN)/activate \ + && python _func_info.py + +quartodoc: quartodoc_impl quartodoc_func_info ## Build qmd files for API docs site: ## Build website . $(PYBIN)/activate \ diff --git a/docs/_signatures.py b/docs/_func_info.py similarity index 62% rename from docs/_signatures.py rename to docs/_func_info.py index 936b21605..4f9e86554 100644 --- a/docs/_signatures.py +++ b/docs/_func_info.py @@ -1,5 +1,6 @@ # import griffe.docstrings.dataclasses as ds import json +import subprocess from typing import Union import griffe.dataclasses as dc @@ -10,9 +11,8 @@ from griffe.docstrings import Parser from griffe.loader import GriffeLoader from plum import dispatch -from quartodoc import MdRenderer, get_object, preview +from quartodoc import MdRenderer, get_object, preview # noqa: F401 from quartodoc.parsers import get_parser_defaults -from quartodoc.renderers.base import convert_rst_link_to_md, sanitize from shiny import reactive, render, ui @@ -44,8 +44,8 @@ def fast_get_object(path: str): return get_object(path, loader=loader) -class BarretFnSig(Renderer): - style = "custom_greg_styles" +class FuncSignature(Renderer): + style = "custom_func_signature" # def __init__(self, header_level: int = 1): # self.header_level = header_level @@ -57,6 +57,7 @@ class BarretFnSig(Renderer): @dispatch def render(self, el: Union[dc.Alias, dc.Object]): # preview(el) + param_str = "" if hasattr(el, "docstring") and hasattr(el.docstring, "parsed"): for docstring_val in el.docstring.parsed: @@ -102,13 +103,13 @@ def render(self, el: ds.DocstringParameter): # preview(el) param = self.render(el.name) - # annotation = self.render_annotation(el.annotation) - # if annotation: - # param = f"{param}: {annotation}" - if el.default: - return None + annotation = self.render_annotation(el.annotation) + if annotation: + param = f"{param}: {annotation}" # if el.default: - # param = f"{param} = {el.default}" + # return None + if el.default: + param = f"{param} = {el.default}" return param @dispatch @@ -123,50 +124,118 @@ def render(self, el: ds.DocstringSectionParameters): # return self._render_table(rows, header) - # @dispatch - # def render(self, el: dc.Docstring): - # return f"A docstring with {len(el.parsed)} pieces" + +def get_git_revision_short_hash() -> str: + return ( + subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]) + .decode("ascii") + .strip() + ) + + +def get_git_current_tag() -> str: + return ( + subprocess.check_output(["git", "tag", "--points-at", "HEAD"]) + .decode("ascii") + .strip() + ) + + +class FuncFileLocation(Renderer): + style = "custom_func_location" + sha: str + + def __init__(self): + sha = get_git_current_tag() + if not sha: + sha = get_git_revision_short_hash() + self.sha = sha + + @dispatch + def render(self, el): + raise NotImplementedError(f"Unsupported type: {type(el)}") + + @dispatch + def render(self, el: Union[dc.Alias, dc.Object]): + # preview(el) + # import ipdb + + # ipdb.set_trace() + + rel_path = str(el.filepath).split("/shiny/")[-1] + + return { + # "name": el.name, + # "path": el.path, + "gitpath": f"https://github.com/posit-dev/py-shiny/blob/{self.sha}/shiny/{rel_path}#L{el.lineno}-L{el.endlineno}", + } # TODO-barret; Add sentance in template to describe what the Relevant Function section is: "To learn more about details about the functions covered here, visit the reference links below." -# print(BarretFnSig().render(fast_get_object("shiny:ui.input_action_button"))) -# print(BarretFnSig().render(fast_get_object("shiny:ui.input_action_button"))) +# print(FuncSignature().render(fast_get_object("shiny:ui.input_action_button"))) +# print(FuncSignature().render(fast_get_object("shiny:ui.input_action_button"))) # preview(fast_get_object("shiny:ui")) # print("") +with open("objects.json") as infile: + objects_content = json.load(infile) -ret = {} +# Collect rel links to functions +links = {} +for item in objects_content["items"]: + if not item["name"].startswith("shiny."): + continue + name = item["name"].replace("shiny.", "") + links[name] = item["uri"] +# preview(links) + +fn_sig = FuncSignature() +file_locs = FuncFileLocation() +fn_info = {} for mod_name, mod in [ ("ui", ui), ("render", render), ("reactive", reactive), ]: - print(f"## Collecting signatures: {mod_name}") - ret[mod_name] = {} + print(f"## Collecting: {mod_name}") for key, f_obj in mod.__dict__.items(): if key.startswith("_") or key in ("AnimationOptions",): continue if not callable(f_obj): continue # print(f"## {mod_name}.{key}") - signature = BarretFnSig().render(fast_get_object(f"shiny:{mod_name}.{key}")) - ret[mod_name][key] = signature -# preview(ret) - -print("## Saving signatures") - + fn_obj = fast_get_object(f"shiny:{mod_name}.{key}") + signature = f"{mod_name}.{fn_sig.render(fn_obj)}" + name = f"{mod_name}.{key}" + uri = None + if name in links: + uri = links[name] + else: + print(f"#### WARNING: No quartodoc entry/link found for {name}") + fn_info[name] = { + # "name": name, + "uri": uri, + "signature": signature, + **file_locs.render(fn_obj), + } +# preview(fn_info) + +print("## Saving function information to objects.json") + +objects_content["func_info"] = fn_info -with open("objects.json") as infile: - objects_content = json.load(infile) -objects_content["signatures"] = ret # Serializing json -json_object = json.dumps(objects_content) +json_object = json.dumps( + objects_content, + # TODO-barret; remove + indent=2, +) # Writing to sample.json with open("objects.json", "w") as outfile: outfile.write(json_object) # TODO-barret; Include link to GitHub source -# print(BarretFnSig().render(f_obj.annotation)) +# print(FuncSignature().render(f_obj.annotation)) # print(preview(f_obj)) # # get annotation of first parameter From 2ed4c2ca279464684f557e17b3b33f65b1ba7a8a Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 20 Dec 2023 10:37:48 -0500 Subject: [PATCH 6/6] `gitpath` -> `github`; Do not sanitize `|` character --- docs/_func_info.py | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/docs/_func_info.py b/docs/_func_info.py index 4f9e86554..87f942d4b 100644 --- a/docs/_func_info.py +++ b/docs/_func_info.py @@ -56,8 +56,6 @@ class FuncSignature(Renderer): @dispatch def render(self, el: Union[dc.Alias, dc.Object]): - # preview(el) - param_str = "" if hasattr(el, "docstring") and hasattr(el.docstring, "parsed"): for docstring_val in el.docstring.parsed: @@ -67,38 +65,24 @@ def render(self, el: Union[dc.Alias, dc.Object]): for param in el.parameters: param_str += self.render(param) return f"{el.name}({param_str})" - # return f"{el.name}({self.render(el.parameters)})" - # return self.render_annotation(el.annotation) - # # header = "#" * self.header_level - # # str_header = f"{header} {el.name}" - # str_header = f"## {el.name}" - # str_params = f"N PARAMETERS: {len(el.parameters)}" - # str_sections = "SECTIONS: " + self.render(el.docstring) - - # # return something pretty - # return "\n".join([str_header, str_params, str_sections]) - - # Consolidate the parameter type info into a single column + @dispatch def render(self, el: None): return "None" @dispatch - def render_annotation(self, el: exp.ExprName): - # print("exp.ExprName") - # preview(el) - return el.path - return f"[{el.path}](`{el.canonical_path}`)" + def render_annotation(self, el: str): + return el @dispatch - def render_annotation(self, el: exp.ExprSubscript): - # print("exp.ExprSubscript") - # preview(el) + def render_annotation( + self, el: Union[exp.ExprName, exp.ExprSubscript, exp.ExprBinOp] + ): return el.path - return f"[{el.path}](`{el.canonical_path}`)" @dispatch def render(self, el: ds.DocstringParameter): + # print("\n\n") # print("ds.DocstringParameter") # preview(el) @@ -106,23 +90,15 @@ def render(self, el: ds.DocstringParameter): annotation = self.render_annotation(el.annotation) if annotation: param = f"{param}: {annotation}" - # if el.default: - # return None if el.default: param = f"{param} = {el.default}" return param @dispatch def render(self, el: ds.DocstringSectionParameters): - # print("ds.DocstringSectionParameters") - # preview(el) return ", ".join( [item for item in map(self.render, el.value) if item is not None] ) - # rows = list(map(self.render, el.value)) - # header = ["Parameter", "Description"] - - # return self._render_table(rows, header) def get_git_revision_short_hash() -> str: @@ -167,7 +143,7 @@ def render(self, el: Union[dc.Alias, dc.Object]): return { # "name": el.name, # "path": el.path, - "gitpath": f"https://github.com/posit-dev/py-shiny/blob/{self.sha}/shiny/{rel_path}#L{el.lineno}-L{el.endlineno}", + "github": f"https://github.com/posit-dev/py-shiny/blob/{self.sha}/shiny/{rel_path}#L{el.lineno}-L{el.endlineno}", }