Skip to content

Commit

Permalink
Update quartodocs to use a renderer (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
schloerke committed Apr 25, 2023
1 parent 05daee4 commit 724f6eb
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 1,373 deletions.
51 changes: 46 additions & 5 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Python package
on:
workflow_dispatch:
push:
branches: ["main"]
branches: ["main", "quarto**"]
pull_request:
release:
types: [published]
Expand All @@ -26,9 +26,11 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
- name: Install dependencies
run: |
pip install -e ".[dev,test]"
- name: Install
run: |
Expand All @@ -40,21 +42,60 @@ jobs:
run: |
make check
docs:
permissions:
contents: write
name: "Build Docs"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Quarto
uses: quarto-dev/quarto-actions/setup@v2

- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Upgrade pip
run: |
python -m pip install --upgrade pip
- name: Install dependencies
run: |
pip install -e ".[dev,test,docs]"
- name: Install
run: |
make install
- name: Render quarto site
run: |
make docs-ci
- name: Publish to GitHub Pages (on push)
if: github.event_name == 'push'
uses: quarto-dev/quarto-actions/publish@v2
with:
path: docs
render: false
target: gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

deploy:
name: "Deploy to PyPI"
runs-on: ubuntu-latest
if: github.event_name == 'release'
needs: [build]
steps:
- uses: actions/checkout@v3
- name: "Set up Python 3.8"
- name: "Set up Python 3.11"
uses: actions/setup-python@v4
with:
python-version: "3.8"
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,test]"
pip install -e ".[dev,test,docs]"
- name: "Build Package"
run: |
make dist
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,17 @@ coverage: ## check code coverage quickly with the default Python
$(BROWSER) htmlcov/index.html

quarto-shinylive: ## Make sure quarto-shinylive is installed
cd docs && (test -f _extensions/quarto-ext/shinylive/shinylive.lua || quarto install extension quarto-ext/shinylive)
docs: quarto-shinylive ## generate quartodoc HTML documentation, including API docs
cd docs && (test -f _extensions/quarto-ext/shinylive/shinylive.lua || quarto install extension --no-prompt quarto-ext/shinylive)
docs-quartodoc: quarto-shinylive ## Build quartodoc
cd docs && python -m quartodoc build
docs-render: quarto-shinylive
cd docs && quarto render
docs-ci: docs-quartodoc docs-render ## Build quartodoc for CI
docs-open:
$(BROWSER) docs/_site/index.html
docs-watch: quarto-shinylive
cd docs && quarto preview
docs: docs-ci docs-open ## generate quartodoc HTML documentation, including API docs

# # Perform `quarto preview` and `quartodoc build` in parallel
# watchdocs: quarto-shinylive
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ app = App(app_ui, server)
If you want to do development on shinyswatch for Python:

```sh
pip install -e ".[dev,test]"
pip install -e ".[dev,test,docs]"
```

### Examples
Expand Down
4 changes: 4 additions & 0 deletions docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ quartodoc:
# style: single-page
style: pkgdown
sidebar: _sidebar.yml
renderer:
style: _renderer.py
show_signature_annotations: false
display_name: relative
dir: reference
package: shinyswatch
sections:
Expand Down
168 changes: 168 additions & 0 deletions docs/_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# pyright: reportPrivateImportUsage=false
# pyright: reportGeneralTypeIssues=false

from __future__ import annotations

import html
import re
from pathlib import PurePath
from typing import Union

import quartodoc.ast as qast
from griffe import dataclasses as dc
from griffe.docstrings import dataclasses as ds
from plum import dispatch
from quartodoc import MdRenderer
from quartodoc.renderers.base import convert_rst_link_to_md, sanitize

ROOT_PATH = PurePath(__file__).parent.parent
demo_app_path = ROOT_PATH / "examples" / "components" / "app.py"
with open(demo_app_path) as f:
demo_app_code = f.read()

APP_TMPL = (
demo_app_code.replace("{", "{{")
.replace("}", "}}")
.replace("shinyswatch.theme.superhero()", "{code}")
)
THEME_TMPL = APP_TMPL.replace("{code}", "shinyswatch.theme.{theme_name}()")
GET_THEME_TMPL = APP_TMPL.replace("{code}", 'shinyswatch.get_theme("superhero")')

SHINYLIVE_TMPL = """```{{shinylive-python}}
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 800
## file: app.py
{app_code}
## file: requirements.txt
Jinja2
pandas
shiny
shinyswatch
```
"""


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


class Renderer(MdRenderer):
style = "shiny"

@dispatch
def render(self, el: qast.DocstringSectionSeeAlso):
# The See Also section in the Shiny docs has bare function references, ones that
# lack a leading :func: and backticks. This function fixes them. In the future,
# we can fix the docstrings in Shiny, once we decide on a standard. Then we can
# remove this function.
return prefix_bare_functions_with_func(el.value)

@dispatch
def render(self, el: Union[dc.Object, dc.Alias]):
rendered = super().render(el)

converted = convert_rst_link_to_md(rendered)

if not (el.module.name == "theme" or el.name == "get_theme"):
return converted

app_code = ""
if el.name == "get_theme":
app_code = GET_THEME_TMPL
else:
app_code = THEME_TMPL.format(theme_name=el.name)
example = SHINYLIVE_TMPL.format(app_code=app_code)
converted_with_examples = DOCSTRING_TMPL.format(
rendered=converted,
header="#" * (self.crnt_header_level + 1),
examples=example,
)
return converted_with_examples

@dispatch
def render(self, el: ds.DocstringSectionText):
# functions like shiny.ui.tags.b have html in their docstrings, so
# we escape them. Note that we are only escaping text sections, but
# since these cover the top text of the docstring, it should solve
# the immediate problem.
rendered = super().render(el)
return html_escape_except_backticks(rendered)

@dispatch
def render_annotation(self, el: str):
return sanitize(el)

@dispatch
def render_annotation(self, el: None):
return ""

@dispatch
def render_annotation(self, el: dc.Expression):
# an expression is essentially a list[dc.Name | str]
# e.g. Optional[TagList]
# -> [Name(source="Optional", ...), "[", Name(...), "]"]

return "".join(map(self.render_annotation, el))

@dispatch
def render_annotation(self, el: dc.Name):
# e.g. Name(source="Optional", full="typing.Optional")
return f"[{el.source}](`{el.full}`)"

@dispatch
def summarize(self, el: dc.Object | dc.Alias):
result = super().summarize(el)
return html.escape(result)


def html_escape_except_backticks(s: str) -> str:
"""
HTML-escape a string, except for content inside of backticks.
Examples
--------
s = "This is a <b>test</b> string with `backticks <i>unescaped</i>`."
print(html_escape_except_backticks(s))
#> This is a &lt;b&gt;test&lt;/b&gt; string with `backticks <i>unescaped</i>`.
"""
# Split the string using backticks as delimiters
parts = re.split(r"(`[^`]*`)", s)

# Iterate over the parts, escaping the non-backtick parts, and preserving backticks in the backtick parts
escaped_parts = [
html.escape(part) if i % 2 == 0 else part for i, part in enumerate(parts)
]

# Join the escaped parts back together
escaped_string = "".join(escaped_parts)
return escaped_string


def prefix_bare_functions_with_func(s: str) -> str:
"""
The See Also section in the Shiny docs has bare function references, ones that lack
a leading :func: and backticks. This function fixes them.
If there are bare function references, like "~shiny.ui.panel_sidebar", this will
prepend with :func: and wrap in backticks.
For example, if the input is this:
"~shiny.ui.panel_sidebar :func:`~shiny.ui.panel_sidebar`"
This function will return:
":func:`~shiny.ui.panel_sidebar` :func:`~shiny.ui.panel_sidebar`"
"""

def replacement(match: re.Match[str]) -> str:
return f":func:`{match.group(0)}`"

pattern = r"(?<!:func:`)~\w+(\.\w+)*"
return re.sub(pattern, replacement, s)
Loading

0 comments on commit 724f6eb

Please sign in to comment.