mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-01 17:03:33 -05:00
examples inspector plugin (#1040)
* replace unnecessary elements from hello_world example and replace with py-tutor tag * add py_tutor plugin * port altair example * add code for more granular tutor mode * add support for including modules source in pytutor * remove js dependencies in hello_world * put antigravity on a diet ;) * use py-tutor on antigravity example * use py-tutor on d3 example * use py-tutor on bokeh example * use py-tutor on bokeh_interactive example * fix issue when module_paths is undefined * remove prism js dependency leftovers * ooops, really remove prism js dependency leftovers * port follium example to pytutor * port pymarkdown and matplotlib example to pytutor * port message_passing and numpy_convas_fractals examples to pytutor * port the panel complex examples to pytutor * port the panel complex examples to pytutor * port last examples to py-tutor * remove prism * remore most debugging logs and replace log with info * add new d3.py file * add comments to connect method * clean pyscript class from logs * revert class pySrc attribute * add check_tutor_generated_code to test code inspector plugin in examples * add doctsting to PyTutor connect * add check for tutor code inspection on all examples * Update pyscriptjs/src/plugins/python/py_tutor.py fix template indentation Co-authored-by: Fábio Rosado <fabioglrosado@gmail.com> * Update examples/todo-pylist.html fix typo (stray = ) Co-authored-by: Fábio Rosado <fabioglrosado@gmail.com> * fix pymarkdown example Co-authored-by: Fabio Pliger <fpliger@anaconda.com> Co-authored-by: Fábio Rosado <fabioglrosado@gmail.com>
This commit is contained in:
215
pyscriptjs/src/plugins/python/py_tutor.py
Normal file
215
pyscriptjs/src/plugins/python/py_tutor.py
Normal file
@@ -0,0 +1,215 @@
|
||||
import html
|
||||
|
||||
from pyscript import Plugin, js
|
||||
|
||||
js.console.warn(
|
||||
"WARNING: This plugin is still in a very experimental phase and will likely change"
|
||||
" and potentially break in the future releases. Use it with caution."
|
||||
)
|
||||
|
||||
plugin = Plugin("PyTutorial")
|
||||
|
||||
# TODO: Part of the CSS is hidden in examples.css ---->> IMPORTANT: move it here!!
|
||||
|
||||
# TODO: Python files running and <py-script src="bla.py"> not in the config are not available...
|
||||
|
||||
# TODO: We can totally implement this in Python
|
||||
PAGE_SCRIPT = """
|
||||
const viewCodeButton = document.getElementById("view-code-button");
|
||||
|
||||
const codeSection = document.getElementById("code-section");
|
||||
const handleClick = () => {
|
||||
if (codeSection.classList.contains("code-section-hidden")) {
|
||||
codeSection.classList.remove("code-section-hidden");
|
||||
codeSection.classList.add("code-section-visible");
|
||||
} else {
|
||||
codeSection.classList.remove("code-section-visible");
|
||||
codeSection.classList.add("code-section-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
viewCodeButton.addEventListener("click", handleClick)
|
||||
viewCodeButton.addEventListener("keydown", (e) => {
|
||||
if (e.key === " " || e.key === "Enter" || e.key === "Spacebar") {
|
||||
handleClick();
|
||||
}
|
||||
})
|
||||
"""
|
||||
|
||||
TEMPLATE_CODE_SECTION = """
|
||||
<div id="view-code-button" role="button" aria-pressed="false" tabindex="0">View Code</div>
|
||||
<div id="code-section" class="code-section-hidden">
|
||||
<p>index.html</p>
|
||||
<pre class="prism-code language-html">
|
||||
<code class="language-html">
|
||||
{source}
|
||||
</code>
|
||||
</pre>
|
||||
|
||||
{modules_section}
|
||||
</div>
|
||||
"""
|
||||
|
||||
TEMPLATE_PY_MODULE_SECTION = """
|
||||
<p>{module_name}</p>
|
||||
<pre class="prism-code language-python">
|
||||
<code class="language-python">
|
||||
{source}
|
||||
</code>
|
||||
</pre>
|
||||
"""
|
||||
|
||||
|
||||
@plugin.register_custom_element("py-tutor")
|
||||
class PyTutor:
|
||||
def __init__(self, element):
|
||||
self.element = element
|
||||
|
||||
def append_script_to_page(self):
|
||||
"""
|
||||
Append the JS script (PAGE_SCRIPT) to the page body in order to attach the
|
||||
click and keydown events to show/hide the source code section on the page.
|
||||
"""
|
||||
el = js.document.createElement("script")
|
||||
el.type = "text/javascript"
|
||||
try:
|
||||
el.appendChild(js.document.createTextNode(PAGE_SCRIPT))
|
||||
except BaseException:
|
||||
el.text = PAGE_SCRIPT
|
||||
|
||||
js.document.body.appendChild(el)
|
||||
|
||||
def add_prism(self):
|
||||
# Add The CSS
|
||||
link = js.document.createElement("link")
|
||||
link.type = "text/css"
|
||||
link.rel = "stylesheet"
|
||||
js.document.head.appendChild(link)
|
||||
link.href = "./assets/prism/prism.css"
|
||||
|
||||
# Add the JS file
|
||||
script = js.document.createElement("script")
|
||||
script.type = "text/javascript"
|
||||
try:
|
||||
script.appendChild(js.document.createTextNode(PAGE_SCRIPT))
|
||||
except BaseException:
|
||||
script.text = PAGE_SCRIPT
|
||||
script.src = "./assets/prism/prism.js"
|
||||
js.document.head.appendChild(script)
|
||||
|
||||
def _create_code_section(self, source, module_paths=None, parent=None):
|
||||
"""
|
||||
Get source and the path to modules to be displayed, create a new `code`
|
||||
`section` where it's contents use TEMPLATE_CODE_SECTION with `source` and
|
||||
`modules_paths` to display the information it needs.
|
||||
|
||||
Args:
|
||||
|
||||
source (str): source within a <py-tutor> tag that needs to be displaed
|
||||
module_paths (list(str)): list of paths to modules that needs to be shown
|
||||
parent(HTMLElement, optional): Element where the code section will be appended
|
||||
to. I None is passed parent == document.body.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
(None)
|
||||
"""
|
||||
if not parent:
|
||||
parent = js.document.body
|
||||
|
||||
js.console.info("Creating code introspection section.")
|
||||
modules_section = self.create_modules_section(module_paths)
|
||||
|
||||
js.console.info("Creating new code section element.")
|
||||
el = js.document.createElement("section")
|
||||
el.classList.add("code")
|
||||
|
||||
el.innerHTML = TEMPLATE_CODE_SECTION.format(
|
||||
source=source, modules_section=modules_section
|
||||
)
|
||||
parent.appendChild(el)
|
||||
|
||||
@classmethod
|
||||
def create_modules_section(cls, module_paths=None):
|
||||
"""Create the HTML content for all modules passed in `module_paths`. More specifically,
|
||||
reads the content of each module and calls PyTytor.create_module_section
|
||||
|
||||
Args:
|
||||
|
||||
module_paths (list(str)): list of paths to modules that needs to be shown
|
||||
|
||||
Returns:
|
||||
(str) HTML code with the content of each module in `module_path`, ready to be
|
||||
attached to the DOM
|
||||
"""
|
||||
js.console.info(f"Module paths to parse: {module_paths}")
|
||||
if not module_paths:
|
||||
return ""
|
||||
|
||||
return "\n\n".join([cls.create_module_section(m) for m in module_paths])
|
||||
|
||||
@staticmethod
|
||||
def create_module_section(module_path):
|
||||
"""Create the HTML content for the module passed as `module_path`.
|
||||
More specifically, reads the content of module and calls PyTytor.create_module_section
|
||||
|
||||
Args:
|
||||
|
||||
module_paths (list(str)): list of paths to modules that needs to be shown
|
||||
|
||||
Returns:
|
||||
(str) HTML code with the content of each module in `module_path`, ready to be
|
||||
attached to the DOM
|
||||
"""
|
||||
js.console.info(f"Creating module section: {module_path}")
|
||||
with open(module_path) as fp:
|
||||
content = fp.read()
|
||||
return TEMPLATE_PY_MODULE_SECTION.format(
|
||||
module_name=module_path, source=content
|
||||
)
|
||||
|
||||
def create_page_code_section(self):
|
||||
"""
|
||||
Create all the code content to be displayed on a page. More specifically:
|
||||
|
||||
* get the HTML code within the <py-tutor> tag
|
||||
* get the source code from all files specified in the py-tytor `modules` attribute
|
||||
* create the HTML to be attached on the page using the content created in
|
||||
the previous 2 items and apply them to TEMPLATE_CODE_SECTION
|
||||
|
||||
Returns:
|
||||
(None)
|
||||
"""
|
||||
# Get the content of all the modules that were passed to be documented
|
||||
module_paths = self.element.getAttribute("modules")
|
||||
if module_paths:
|
||||
js.console.info(f"Module paths detected: {module_paths}")
|
||||
module_paths = str(module_paths).split(";")
|
||||
|
||||
# Get the inner HTML content of the py-tutor tag and document that
|
||||
tutor_tag_innerHTML = html.escape(self.element.innerHTML)
|
||||
|
||||
self._create_code_section(tutor_tag_innerHTML, module_paths)
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Handler meant to be called when the Plugin CE (Custom Element) is attached
|
||||
to the page.
|
||||
|
||||
As so, it's the entry point that coordinates the whole plugin workflow and
|
||||
is responsible for calling the right steps in order:
|
||||
|
||||
* identify what parts of the App (page) that are within the py-tutor tag
|
||||
to be documented as well as any modules specified as attribute
|
||||
* inject the button to show/hide button and related modal
|
||||
* inject the JS code that attaches the click event to the button
|
||||
* build the modal that shows/hides with the correct page/modules code
|
||||
"""
|
||||
# Create the core do show the source code on the page
|
||||
self.create_page_code_section()
|
||||
|
||||
# append the script needed to show source first...
|
||||
self.append_script_to_page()
|
||||
|
||||
# inject the prism JS library dependency
|
||||
self.add_prism()
|
||||
@@ -303,6 +303,72 @@ class PyScriptTest:
|
||||
text = "\n".join(loc.all_inner_texts())
|
||||
raise AssertionError(f"Found {n} alert banners:\n" + text)
|
||||
|
||||
def check_tutor_generated_code(self, modules_to_check=None):
|
||||
"""
|
||||
Ensure that the source code viewer injected by the PyTutor plugin
|
||||
is presend. Raise AssertionError if not found.
|
||||
|
||||
Args:
|
||||
|
||||
modules_to_check(str): iterable with names of the python modules
|
||||
that have been included in the tutor config
|
||||
and needs to be checked (if they are included
|
||||
in the displayed source code)
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Given: a page that has a <py-tutor> tag
|
||||
assert self.page.locator("py-tutor").count()
|
||||
|
||||
# EXPECT that"
|
||||
#
|
||||
# the page has the "view-code-button"
|
||||
view_code_button = self.page.locator("#view-code-button")
|
||||
vcb_count = view_code_button.count()
|
||||
if vcb_count != 1:
|
||||
raise AssertionError(
|
||||
f"Found {vcb_count} code view button. Should have been 1!"
|
||||
)
|
||||
|
||||
# the page has the code-section element
|
||||
code_section = self.page.locator("#code-section")
|
||||
code_section_count = code_section.count()
|
||||
code_msg = (
|
||||
f"One (and only one) code section should exist. Found: {code_section_count}"
|
||||
)
|
||||
assert code_section_count == 1, code_msg
|
||||
|
||||
pyconfig_tag = self.page.locator("py-config")
|
||||
code_section_inner_html = code_section.inner_html()
|
||||
|
||||
# the code_section has the index.html section
|
||||
assert "<p>index.html</p>" in code_section_inner_html
|
||||
|
||||
# the section has the tags highlighting the HTML code
|
||||
assert (
|
||||
'<pre class="prism-code language-html" tabindex="0">'
|
||||
' <code class="language-html">' in code_section_inner_html
|
||||
)
|
||||
|
||||
# if modules were included, these are also presented in the code section
|
||||
if modules_to_check:
|
||||
for module in modules_to_check:
|
||||
assert f"{module}" in code_section_inner_html
|
||||
|
||||
# the section also includes the config
|
||||
assert "<</span>py-config</span>" in code_section_inner_html
|
||||
|
||||
# the contents of the py-config tag are included in the code section
|
||||
assert pyconfig_tag.inner_html() in code_section_inner_html
|
||||
|
||||
# the code section to be invisible by default (by having the hidden class)
|
||||
assert "code-section-hidden" in code_section.get_attribute("class")
|
||||
|
||||
# once the view_code_button is pressed, the code section becomes visible
|
||||
view_code_button.click()
|
||||
assert "code-section-visible" in code_section.get_attribute("class")
|
||||
|
||||
|
||||
# ============== Helpers and utility functions ==============
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class TestExamples(PyScriptTest):
|
||||
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
|
||||
assert re.search(pattern, content)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_simple_clock(self):
|
||||
self.goto("examples/simple_clock.html")
|
||||
@@ -79,6 +80,7 @@ class TestExamples(PyScriptTest):
|
||||
else:
|
||||
assert False, "Espresso time not found :("
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_altair(self):
|
||||
self.goto("examples/altair.html")
|
||||
@@ -97,6 +99,7 @@ class TestExamples(PyScriptTest):
|
||||
assert save_as_png_link.is_visible()
|
||||
assert see_source_link.is_visible()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_antigravity(self):
|
||||
self.goto("examples/antigravity.html")
|
||||
@@ -120,6 +123,7 @@ class TestExamples(PyScriptTest):
|
||||
re.match(ycoord_pattern, char.get_attribute("transform")).group("ycoord")
|
||||
)
|
||||
assert later_y_coord < starting_y_coord
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_bokeh(self):
|
||||
# XXX improve this test
|
||||
@@ -128,6 +132,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "Bokeh Example"
|
||||
wait_for_render(self.page, "*", '<div.*class=\\"bk\\".*>')
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_bokeh_interactive(self):
|
||||
# XXX improve this test
|
||||
@@ -136,6 +141,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "Bokeh Example"
|
||||
wait_for_render(self.page, "*", '<div.*?class=\\"bk\\".*?>')
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
@pytest.mark.skip("flaky, see issue 759")
|
||||
def test_d3(self):
|
||||
@@ -152,6 +158,7 @@ class TestExamples(PyScriptTest):
|
||||
# means that the chart rendered successfully and with the right text
|
||||
assert "🍊21\n🍇13\n🍏8\n🍌5\n🍐3\n🍋2\n🍎1\n🍉1" in pyscript_chart.inner_text()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["d3.py"])
|
||||
|
||||
def test_folium(self):
|
||||
self.goto("examples/folium.html")
|
||||
@@ -175,6 +182,7 @@ class TestExamples(PyScriptTest):
|
||||
assert "−" in zoom_out.inner_text()
|
||||
zoom_out.click()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_markdown_plugin(self):
|
||||
# Given the example page with:
|
||||
@@ -186,6 +194,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "PyMarkdown"
|
||||
# ASSERT markdown is rendered to the corresponding HTML tag
|
||||
wait_for_render(self.page, "*", "<h1>Hello world!</h1>")
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_matplotlib(self):
|
||||
self.goto("examples/matplotlib.html")
|
||||
@@ -213,6 +222,7 @@ class TestExamples(PyScriptTest):
|
||||
deviation = np.mean(np.abs(img_data - ref_data))
|
||||
assert deviation == 0.0
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_numpy_canvas_fractals(self):
|
||||
self.goto("examples/numpy_canvas_fractals.html")
|
||||
@@ -260,6 +270,7 @@ class TestExamples(PyScriptTest):
|
||||
# Confirm that changing the input values, triggered a new computation
|
||||
assert self.console.log.lines[-1] == "Computing Newton set ..."
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel(self):
|
||||
self.goto("examples/panel.html")
|
||||
@@ -278,6 +289,7 @@ class TestExamples(PyScriptTest):
|
||||
# Let's confirm that slider title changed
|
||||
assert slider_title.inner_text() == "Amplitude: 5"
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_deckgl(self):
|
||||
# XXX improve this test
|
||||
@@ -286,6 +298,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "PyScript/Panel DeckGL Demo"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_kmeans(self):
|
||||
# XXX improve this test
|
||||
@@ -294,6 +307,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "Pyscript/Panel KMeans Demo"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_panel_stream(self):
|
||||
# XXX improve this test
|
||||
@@ -302,6 +316,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "PyScript/Panel Streaming Demo"
|
||||
wait_for_render(self.page, "*", "<div.*?class=['\"]bk-root['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_repl(self):
|
||||
self.goto("examples/repl.html")
|
||||
@@ -322,6 +337,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.wait_for_selector("#my-repl-2-2", state="attached")
|
||||
assert self.page.locator("#my-repl-2-2").text_content() == "4"
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_repl2(self):
|
||||
self.goto("examples/repl2.html")
|
||||
@@ -338,6 +354,7 @@ class TestExamples(PyScriptTest):
|
||||
pattern = "\\d+/\\d+/\\d+, \\d+:\\d+:\\d+" # e.g. 08/09/2022 15:57:32
|
||||
assert re.search(pattern, content)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["antigravity.py"])
|
||||
|
||||
def test_todo(self):
|
||||
self.goto("examples/todo.html")
|
||||
@@ -366,6 +383,7 @@ class TestExamples(PyScriptTest):
|
||||
in first_task.inner_html()
|
||||
)
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["./utils.py", "./todo.py"])
|
||||
|
||||
def test_todo_pylist(self):
|
||||
# XXX improve this test
|
||||
@@ -374,6 +392,7 @@ class TestExamples(PyScriptTest):
|
||||
assert self.page.title() == "Todo App"
|
||||
wait_for_render(self.page, "*", "<input.*?id=['\"]new-task-content['\"].*?>")
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code(modules_to_check=["utils.py", "pylist.py"])
|
||||
|
||||
@pytest.mark.xfail(reason="To be moved to collective and updated, see issue #686")
|
||||
def test_toga_freedom(self):
|
||||
@@ -392,6 +411,7 @@ class TestExamples(PyScriptTest):
|
||||
result = self.page.locator("#toga_c_input")
|
||||
assert "40.555" in result.input_value()
|
||||
self.assert_no_banners()
|
||||
self.check_tutor_generated_code()
|
||||
|
||||
def test_webgl_raycaster_index(self):
|
||||
# XXX improve this test
|
||||
|
||||
Reference in New Issue
Block a user