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: 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/_func_info.py b/docs/_func_info.py new file mode 100644 index 000000000..87f942d4b --- /dev/null +++ b/docs/_func_info.py @@ -0,0 +1,221 @@ +# import griffe.docstrings.dataclasses as ds +import json +import subprocess +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 # noqa: F401 +from quartodoc.parsers import get_parser_defaults + +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) + + +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 FuncSignature(Renderer): + style = "custom_func_signature" + + # 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]): + 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})" + + @dispatch + def render(self, el: None): + return "None" + + @dispatch + def render_annotation(self, el: str): + return el + + @dispatch + def render_annotation( + self, el: Union[exp.ExprName, exp.ExprSubscript, exp.ExprBinOp] + ): + return el.path + + @dispatch + def render(self, el: ds.DocstringParameter): + # print("\n\n") + # 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: + param = f"{param} = {el.default}" + return param + + @dispatch + def render(self, el: ds.DocstringSectionParameters): + return ", ".join( + [item for item in map(self.render, el.value) if item is not None] + ) + + +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, + "github": 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(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) + +# 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: {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}") + 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 + +# Serializing json +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(FuncSignature().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))