Skip to content

Commit

Permalink
Restore and improve @add_example() docstring-generating decorator (#…
Browse files Browse the repository at this point in the history
…1029)

Co-authored-by: Barret Schloerke <[email protected]>
  • Loading branch information
gadenbuie and schloerke authored Jan 19, 2024
1 parent 96a6279 commit 3691b35
Show file tree
Hide file tree
Showing 38 changed files with 413 additions and 164 deletions.
2 changes: 2 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ deps: $(PYBIN) ## Install build dependencies
$(PYBIN)/pip install -e ..[doc]

quartodoc: $(PYBIN) ## Build qmd files for API docs
$(eval export SHINY_ADD_EXAMPLES=true)
$(eval export IN_QUARTODOC=true)
. $(PYBIN)/activate \
&& quartodoc interlinks \
&& quartodoc build --config _quartodoc.yml --verbose
Expand Down
48 changes: 31 additions & 17 deletions docs/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ quartodoc:
package: shiny
rewrite_all_pages: false
sidebar: api/_sidebar.yml
dynamic: true
renderer:
style: _renderer.py
show_signature_annotations: false
Expand Down Expand Up @@ -63,8 +64,6 @@ quartodoc:
- ui.showcase_bottom
- ui.showcase_left_center
- ui.showcase_top_right
- ui.ValueBoxTheme
- ui.ShowcaseLayout
- title: Navigation (tab) panels
desc: Create segments of UI content.
contents:
Expand Down Expand Up @@ -177,10 +176,14 @@ quartodoc:
desc: ""
contents:
- render.renderer.Renderer
- render.renderer.Jsonifiable
- render.renderer.ValueFn
- render.renderer.AsyncValueFn
- render.renderer.RendererT
- name: render.renderer.Jsonifiable
dynamic: false
- name: render.renderer.ValueFn
dynamic: false
- name: render.renderer.AsyncValueFn
dynamic: false
- name: render.renderer.RendererT
dynamic: false
- title: Reactive programming
desc: ""
contents:
Expand Down Expand Up @@ -251,7 +254,8 @@ quartodoc:
- ui.Sidebar
- ui.CardItem
- ui.AccordionPanel
- ui.css.CssUnit
- name: ui.css.CssUnit
dynamic: false
- ui._input_slider.SliderValueArg
- ui._input_slider.SliderStepArg
- kind: page
Expand All @@ -262,11 +266,16 @@ quartodoc:
flatten: true
package: null
contents:
- htmltools.Tag
- htmltools.TagAttrs
- htmltools.TagAttrValue
- htmltools.TagChild
- htmltools.TagList
- name: htmltools.Tag
dynamic: false
- name: htmltools.TagAttrs
dynamic: false
- name: htmltools.TagAttrValue
dynamic: false
- name: htmltools.TagChild
dynamic: false
- name: htmltools.TagList
dynamic: false
- kind: page
path: ExceptionTypes
summary:
Expand Down Expand Up @@ -336,8 +345,13 @@ quartodoc:
desc: "Cards are a common organizing unit for modern user interfaces (UI). At their core, they're just rectangular containers with borders and padding. However, when utilized properly to group related information, they help users better digest, engage, and navigate through content."
flatten: true
contents:
- experimental.ui.card_body
- experimental.ui.card_title
- experimental.ui.card_image
- experimental.ui.ImgContainer
- experimental.ui.WrapperCallable
- name: experimental.ui.card_body
dynamic: false
- name: experimental.ui.card_title
dynamic: false
- name: experimental.ui.card_image
dynamic: false
- name: experimental.ui.ImgContainer
dynamic: false
- name: experimental.ui.WrapperCallable
dynamic: false
105 changes: 58 additions & 47 deletions docs/_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,6 @@

SHINY_PATH = Path(files("shiny").joinpath())

SHINYLIVE_CODE_TEMPLATE = """
```{{shinylive-python}}
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 400{0}
```
"""

DOCSTRING_TEMPLATE = """\
{rendered}
{header} Examples
{examples}
"""


# This is the same as the FileContentJson type in TypeScript.
class FileContentJson(TypedDict):
Expand Down Expand Up @@ -68,37 +51,9 @@ def render(self, el: Union[dc.Object, dc.Alias]):

converted = convert_rst_link_to_md(rendered)

if isinstance(el, dc.Alias) and "experimental" in el.target_path:
p_example_dir = SHINY_PATH / "experimental" / "api-examples" / el.name
else:
p_example_dir = SHINY_PATH / "api-examples" / el.name

if (p_example_dir / "app.py").exists():
example = ""
check_if_missing_expected_example(el, converted)

files = list(p_example_dir.glob("**/*"))

# Sort, and then move app.py to first position.
files.sort()
app_py_idx = files.index(p_example_dir / "app.py")
files = [files[app_py_idx]] + files[:app_py_idx] + files[app_py_idx + 1 :]

for f in files:
if f.is_dir():
continue
file_info = read_file(f, p_example_dir)
if file_info["type"] == "text":
example += f"\n## file: {file_info['name']}\n{file_info['content']}"
else:
example += f"\n## file: {file_info['name']}\n## type: binary\n{file_info['content']}"

example = SHINYLIVE_CODE_TEMPLATE.format(example)

return DOCSTRING_TEMPLATE.format(
rendered=converted,
examples=example,
header="#" * (self.crnt_header_level + 1),
)
assert_no_sphinx_comments(el, converted)

return converted

Expand Down Expand Up @@ -282,3 +237,59 @@ def read_file(file: str | Path, root_dir: str | Path | None = None) -> FileConte
"content": file_content,
"type": type,
}


def check_if_missing_expected_example(el, converted):
if re.search(r"(^|\n)#{2,6} Examples\n", converted):
# Manually added examples are fine
return

if not el.canonical_path.startswith("shiny"):
# Only check Shiny objects for examples
return

if hasattr(el, "decorators") and "no_example" in [
d.value.canonical_name for d in el.decorators
]:
# When an example is intentionally omitted, we mark the fn with `@no_example`
return

if not el.is_function:
# Don't throw for things that can't be decorated
return

if not el.is_explicitely_exported:
# Don't require examples on "implicitly exported" functions
# In practice, this covers methods of exported classes (class still needs ex)
return

# TODO: Remove shiny.express from no_req_examples when we have examples ready
no_req_examples = ["shiny.express", "shiny.experimental"]
if any([el.target_path.startswith(mod) for mod in no_req_examples]):
return

raise RuntimeError(
f"{el.name} needs an example, use `@add_example()` or manually add `Examples` section:\n"
+ (f"> file : {el.filepath}\n" if hasattr(el, "filepath") else "")
+ (f"> target : {el.target_path}\n" if hasattr(el, "target_path") else "")
+ (f"> canonical: {el.canonical_path}" if hasattr(el, "canonical_path") else "")
)


def assert_no_sphinx_comments(el, converted: str) -> None:
"""
Sphinx allows `..`-prefixed comments in docstrings, which are not valid markdown.
We don't allow Sphinx comments or directives, sorry!
"""
pattern = r"\n[.]{2} .+(\n|$)"
if re.search(pattern, converted):
raise RuntimeError(
f"{el.name} includes Sphinx-styled comments or directives, please remove.\n"
+ (f"> file : {el.filepath}\n" if hasattr(el, "filepath") else "")
+ (f"> target : {el.target_path}\n" if hasattr(el, "target_path") else "")
+ (
f"> canonical: {el.canonical_path}"
if hasattr(el, "canonical_path")
else ""
)
)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ doc =
jupyter
jupyter_client < 8.0.0
tabulate
shinylive==0.1.1
shinylive @ git+https://github.com/posit-dev/py-shinylive.git@main
pydantic==1.10
quartodoc==0.7.2
griffe==0.33.0
Expand Down
4 changes: 2 additions & 2 deletions shiny/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ class App:
debug
Whether to enable debug mode.
Example
-------
Examples
--------
```{python}
#| eval: false
Expand Down
Loading

0 comments on commit 3691b35

Please sign in to comment.