Skip to content

Commit 5e4ebda

Browse files
authored
Merge pull request #340 from rstudio/add-example-files
2 parents d5a108a + ab2be6d commit 5e4ebda

File tree

7 files changed

+89
-5
lines changed

7 files changed

+89
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Bug fixes
1313

14+
* Closed #240, #330: Fixed live examples with additional files. (#340)
15+
1416
### Other changes
1517

1618

docs/source/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ UI (`output_*()`) and server (``render``)ing functions for generating content se
205205
render.plot
206206
ui.output_image
207207
render.image
208+
ui.output_table
209+
render.table
208210
ui.output_text
209211
ui.output_text_verbatim
210212
render.text

docs/sphinxext/pyshinyapp.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
:height: 500px
1818
"""
1919

20+
import base64
21+
import json
2022
import os
2123
import shutil
24+
import sys
2225
from os.path import abspath, dirname, join
2326

2427
from docutils.nodes import Element, SkipNode
@@ -27,6 +30,18 @@
2730
from sphinx.application import Sphinx
2831
from sphinx.util.docutils import SphinxDirective
2932

33+
if sys.version_info >= (3, 8):
34+
from typing import Literal, TypedDict
35+
else:
36+
from typing_extensions import Literal, TypedDict
37+
38+
# This is the same as the FileContentJson type in TypeScript.
39+
class FileContentJson(TypedDict):
40+
name: str
41+
content: str
42+
type: Literal["text", "binary"]
43+
44+
3045
# Local path to the shinylive/ directory (currently provided by rstudio/shinylive)
3146
# This is only needed for the "self-contained" version of the API reference.
3247
# (in other words, you can set this to "" if you already have the shinylive/ directory
@@ -46,10 +61,19 @@ def html(self):
4661
# TODO: allow the layout to be specified (right now I don't think we need
4762
# horizontal layout, but maybe someday we will)
4863
code = (
49-
"#| standalone: true\n#| components: [editor, viewer]\n#| layout: vertical\n#| viewerHeight: 400\n"
64+
"#| standalone: true\n#| components: [editor, viewer]\n#| layout: vertical\n#| viewerHeight: 400\n## file: app.py\n"
5065
+ code
5166
)
5267

68+
for f in self["files"]:
69+
file_info = _read_file(f)
70+
if file_info["type"] == "text":
71+
code += (
72+
f"\n\n## file: {os.path.basename(f)}\n{file_info['content']}"
73+
)
74+
else:
75+
code += f"\n\n## file: {os.path.basename(f)}\n## type: binary\n{file_info['content']}"
76+
5377
return (
5478
f'<pre class="shinylive-python" style="{style}"><code>{code}</code></pre>'
5579
)
@@ -59,8 +83,9 @@ def _run(self: SphinxDirective, type: str):
5983
code = "\n".join(self.content)
6084
width = self.options.pop("width", "100%")
6185
height = self.options.pop("height", None)
86+
files = json.loads(self.options.pop("files", "[]"))
6287

63-
return [ShinyElement(type=type, code=code, height=height, width=width)]
88+
return [ShinyElement(type=type, code=code, height=height, width=width, files=files)]
6489

6590

6691
class BaseDirective(SphinxDirective):
@@ -69,6 +94,7 @@ class BaseDirective(SphinxDirective):
6994
option_spec = {
7095
"width": directives.unchanged,
7196
"height": directives.unchanged,
97+
"files": directives.unchanged,
7298
}
7399

74100

@@ -134,3 +160,23 @@ def skip(self: Sphinx, node: Element):
134160
app.add_directive("terminal", TerminalDirective)
135161

136162
return {"version": "0.1"}
163+
164+
165+
def _read_file(filename: str) -> FileContentJson:
166+
type: Literal["text", "binary"] = "text"
167+
try:
168+
with open(filename, "r") as f:
169+
file_content = f.read()
170+
type = "text"
171+
except UnicodeDecodeError:
172+
# If text failed, try binary.
173+
with open(filename, "rb") as f:
174+
file_content_bin = f.read()
175+
file_content = base64.b64encode(file_content_bin).decode("utf-8")
176+
type = "binary"
177+
178+
return {
179+
"name": filename,
180+
"content": file_content,
181+
"type": type,
182+
}

shiny/_docstring.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import sys
34
from typing import Any, Callable, List, TypeVar
@@ -29,7 +30,7 @@ def add_example(
2930
"cell::",
3031
"terminal::",
3132
] = "shinylive-editor::",
32-
**options: str,
33+
**options: object,
3334
) -> Callable[[F], F]:
3435
"""
3536
Add an example to the docstring of a function, method, or class.
@@ -61,10 +62,20 @@ def _(func: F) -> F:
6162
return func
6263

6364
fn_name = func.__name__
64-
example_file = os.path.join(ex_dir, fn_name, "app.py")
65+
example_dir = os.path.join(ex_dir, fn_name)
66+
example_file = os.path.join(example_dir, "app.py")
6567
if not os.path.exists(example_file):
6668
raise ValueError(f"No example for {fn_name}")
6769

70+
other_files: List[str] = []
71+
for f in os.listdir(example_dir):
72+
abs_f = os.path.join(example_dir, f)
73+
if os.path.isfile(abs_f) and f != "app.py":
74+
other_files.append(abs_f)
75+
76+
if "files" not in options:
77+
options["files"] = json.dumps(other_files)
78+
6879
if func.__doc__ is None:
6980
func.__doc__ = ""
7081

@@ -97,7 +108,7 @@ def _(func: F) -> F:
97108
"",
98109
*example_prefix,
99110
f".. {directive}",
100-
*[f"{indent}:{k}: {v}" for k, v in options.items()],
111+
*[f" :{k}: {v}" for k, v in options.items()],
101112
"",
102113
example,
103114
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Pandas needs Jinja2 for table styling, but it doesn't (yet) load automatically
2+
# in Pyodide, so we need to explicitly list it here.
3+
Jinja2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Pandas needs Jinja2 for table styling, but it doesn't (yet) load automatically
2+
# in Pyodide, so we need to explicitly list it here.
3+
Jinja2

shiny/ui/_output.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ def output_text_verbatim(id: str, placeholder: bool = False) -> Tag:
153153

154154
@add_example()
155155
def output_table(id: str, **kwargs: TagAttrArg) -> Tag:
156+
"""
157+
Create a output container for a table.
158+
159+
Parameters
160+
----------
161+
id
162+
An input id.
163+
**kwargs
164+
Additional attributes to add to the container.
165+
166+
Returns
167+
-------
168+
169+
See Also
170+
-------
171+
~shiny.render.table
172+
"""
156173
return tags.div({"class": "shiny-html-output"}, id=resolve_id(id), **kwargs)
157174

158175

0 commit comments

Comments
 (0)