Add REPL plugin hooks; Add output, output-mode, stderr attributes (#1106)

* Add before, after REPL hooks

* Re-introduce 'output-mode' attribute for py-repl

* Add plugin execution tests

* Documentation

* Changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: mariana <marianameireles@protonmail.com>
This commit is contained in:
Jeff Glass
2023-03-22 20:19:22 -05:00
committed by GitHub
parent 51d51409d3
commit ef793aecf3
10 changed files with 514 additions and 91 deletions

View File

@@ -6,8 +6,21 @@
Features
--------
### &lt;py-terminal&gt;
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
### &lt;py-script&gt;
- Restored the `output` attribute of `py-script` tags to route `sys.stdout` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
- Added a `stderr` attribute of `py-script` tags to route `sys.stderr` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
### &lt;py-repl&gt;
- The `output` attribute of `py-repl` tags now specifies the id of the DOM element that `sys.stdout`, `sys.stderr`, and the results of a REPL execution are written to. It no longer affects the location of calls to `display()`
- Added a `stderr` attribute of `py-repl` tags to route `sys.stderr` to a DOM element with the given ID. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
- Resored the `output-mode` attribute of `py-repl` tags. If `output-mode` == 'append', the DOM element where output is printed is _not_ cleared before writing new results.
### Plugins
- Plugins may now implement the `beforePyReplExec()` and `afterPyReplExec()` hooks, which are called immediately before and after code in a `py-repl` tag is executed. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
Bug fixes
---------
@@ -24,12 +37,10 @@ Enhancements
2023.01.1
=========
Features
--------
- Restored the `output` attribute of &lt;py-script&gt; tags to route `sys.stdout` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
- Added a `stderr` attribute of &lt;py-script&gt; tags to route `sys.stderr` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
Bug fixes
---------
@@ -39,6 +50,7 @@ Bug fixes
Enhancements
------------
- When adding a `py-` attribute to an element but didn't added an `id` attribute, PyScript will now generate a random ID for the element instead of throwing an error which caused the splash screen to not shutdown. ([#1122](https://github.com/pyscript/pyscript/pull/1122))
- You can now disable the splashscreen by setting `enabled = false` in your `py-config` under the `[splashscreen]` configuration section. ([#1138](https://github.com/pyscript/pyscript/pull/1138))

View File

@@ -7,24 +7,42 @@ The `<py-repl>` element provides a REPL(Read Eval Print Loop) to evaluate multi-
| attribute | type | default | description |
|-------------------|---------|---------|---------------------------------------|
| **auto-generate** | boolean | | Auto-generates REPL after evaluation |
| **output** | string | | The element to write output into |
| **output-mode** | string | "" | Determines whether the output element is cleared prior to writing output |
| **output** | string | | The id of the element to write `stdout` and `stderr` to |
| **stderr** | string | | The id of the element to write `stderr` to |
### Examples
#### `<py-repl>` element set to auto-generate
### `auto-generate`
If a \<py-repl\> tag has the `auto-generate` attribute, upon execution, another \<pr-repl\> tag will be created and added to the DOM as a sibling of the current tag.
### `output-mode`
By default, the element which displays the output from a REPL is cleared (`innerHTML` set to "") prior to each new execution of the REPL. If `output-mode` == "append", that element is not cleared, and the output is appended instead.
### `output`
The ID of an element in the DOM that `stdout` (e.g. `print()`), `stderr`, and the results of executing the repl are written to. Defaults to an automatically-generated \<div\> as the next sibling of the REPL itself.
### `stderr`
The ID of an element in the DOM that `stderr` will be written to. Defaults to None, though writes to `stderr` will still appear in the location specified by `output`.
## Examples
### `<py-repl>` element set to auto-generate
```html
<py-repl auto-generate="true"> </py-repl>
```
#### `<py-repl>` element with output
### `<py-repl>` element with output
The following will write "Hello! World!" to the div with id `replOutput`.
```html
<div id="replOutput"></div>
<py-repl output="replOutput">
hello = "Hello world!"
print("Hello!")
hello = "World!"
hello
</py-repl>
```
Note that if we `print` any element in the repl, the output will be printed in the [`py-terminal`](../plugins/py-terminal.md) if is enabled.
Note that if we `print` from the REPL (or otherwise write to `sys.stdout`), the output will be printed in the [`py-terminal`](../plugins/py-terminal.md) if is enabled.

View File

@@ -1,10 +1,11 @@
import { InterpreterClient } from '../interpreter_client';
import type { PyScriptApp } from '../main';
import { make_PyRepl } from './pyrepl';
import { make_PyWidget } from './pywidget';
function createCustomElements(interpreter: InterpreterClient) {
function createCustomElements(interpreter: InterpreterClient, app: PyScriptApp) {
const PyWidget = make_PyWidget(interpreter);
const PyRepl = make_PyRepl(interpreter);
const PyRepl = make_PyRepl(interpreter, app);
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyRepl = customElements.define('py-repl', PyRepl);

View File

@@ -7,14 +7,16 @@ import { defaultKeymap } from '@codemirror/commands';
import { oneDarkTheme } from '@codemirror/theme-one-dark';
import { getAttribute, ensureUniqueId, htmlDecode } from '../utils';
import { pyExec, pyDisplay } from '../pyexec';
import { pyExec } from '../pyexec';
import { getLogger } from '../logger';
import { InterpreterClient } from '../interpreter_client';
import type { PyScriptApp } from '../main';
import { Stdio } from '../stdio';
const logger = getLogger('py-repl');
const RUNBUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
export function make_PyRepl(interpreter: InterpreterClient) {
export function make_PyRepl(interpreter: InterpreterClient, app: PyScriptApp) {
/* High level structure of py-repl DOM, and the corresponding JS names.
this <py-repl>
@@ -31,6 +33,8 @@ export function make_PyRepl(interpreter: InterpreterClient) {
shadow: ShadowRoot;
outDiv: HTMLElement;
editor: EditorView;
stdout_manager: Stdio | null;
stderr_manager: Stdio | null;
constructor() {
super();
@@ -152,27 +156,19 @@ export function make_PyRepl(interpreter: InterpreterClient) {
*/
async execute(): Promise<void> {
const pySrc = this.getPySrc();
// determine the output element
const outEl = this.getOutputElement();
if (outEl === undefined) {
// this happens if we specified output="..." but we couldn't
// find the ID. We already displayed an error message inside
// getOutputElement, stop the execution.
return;
}
// clear the old output before executing the new code
outEl.innerHTML = '';
const outEl = this.outDiv;
// execute the python code
app.plugins.beforePyReplExec({ interpreter: interpreter, src: pySrc, outEl: outEl, pyReplTag: this });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const pyResult = (await pyExec(interpreter, pySrc, outEl)).result;
// display the value of the last evaluated expression (REPL-style)
if (pyResult !== undefined) {
pyDisplay(interpreter, pyResult, { target: outEl.id });
}
app.plugins.afterPyReplExec({
interpreter: interpreter,
src: pySrc,
outEl: outEl,
pyReplTag: this,
result: pyResult, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
});
this.autogenerateMaybe();
}
@@ -181,21 +177,6 @@ export function make_PyRepl(interpreter: InterpreterClient) {
return this.editor.state.doc.toString();
}
getOutputElement(): HTMLElement {
const outputID = getAttribute(this, 'output');
if (outputID !== null) {
const el = document.getElementById(outputID);
if (el === null) {
const err = `py-repl ERROR: cannot find the output element #${outputID} in the DOM`;
this.outDiv.innerText = err;
return undefined;
}
return el;
} else {
return this.outDiv;
}
}
// XXX the autogenerate logic is very messy. We should redo it, and it
// should be the default.
autogenerateMaybe(): void {
@@ -206,7 +187,15 @@ export function make_PyRepl(interpreter: InterpreterClient) {
const nextExecId = parseInt(lastExecId) + 1;
const newPyRepl = document.createElement('py-repl');
newPyRepl.setAttribute('root', this.getAttribute('root'));
//Attributes to be copied from old REPL to auto-generated REPL
for (const attribute of ['root', 'output-mode', 'output', 'stderr']) {
const attr = getAttribute(this, attribute);
if (attr) {
newPyRepl.setAttribute(attribute, attr);
}
}
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
if (this.hasAttribute('auto-generate')) {
@@ -214,20 +203,6 @@ export function make_PyRepl(interpreter: InterpreterClient) {
this.removeAttribute('auto-generate');
}
const outputMode = getAttribute(this, 'output-mode');
if (outputMode) {
newPyRepl.setAttribute('output-mode', outputMode);
}
const addReplAttribute = (attribute: string) => {
const attr = getAttribute(this, attribute);
if (attr) {
newPyRepl.setAttribute(attribute, attr);
}
};
addReplAttribute('output');
newPyRepl.setAttribute('exec-id', nextExecId.toString());
if (this.parentElement) {
this.parentElement.appendChild(newPyRepl);

View File

@@ -189,8 +189,9 @@ export class PyScriptApp {
this.logStatus('Initializing web components...');
// lifecycle (8)
createCustomElements(interpreter);
//Takes a runtime and a reference to the PyScriptApp (to access plugins)
createCustomElements(interpreter, this);
await initHandlers(interpreter);
// NOTE: interpreter message is used by integration tests to know that

View File

@@ -55,7 +55,7 @@ export class Plugin {
/** The source of a <py-script>> tag has been fetched, and we're about
* to evaluate that source using the provided interpreter.
*
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
* @param options.src {string} The Python source code to be evaluated
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
*/
@@ -66,7 +66,7 @@ export class Plugin {
/** The Python in a <py-script> has just been evaluated, but control
* has not been ceded back to the JavaScript event loop yet
*
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
* @param options.src {string} The Python source code to be evaluated
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
* @param options.result The returned result of evaluating the Python (if any)
@@ -80,6 +80,36 @@ export class Plugin {
/* empty */
}
/** The source of the <py-repl> tag has been fetched and its output-element determined;
* we're about to evaluate the source using the provided interpreter
*
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
* @param options.src {string} The Python source code to be evaluated
* @param options.outEl The element that the result of the REPL evaluation will be output to.
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
*/
beforePyReplExec(options: { interpreter: InterpreterClient; src: string; outEl: HTMLElement; pyReplTag: any }) {
/* empty */
}
/**
*
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
* @param options.src {string} The Python source code to be evaluated
* @param options.outEl The element that the result of the REPL evaluation will be output to.
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
* @param options.result The result of evaluating the Python (if any)
*/
afterPyReplExec(options: {
interpreter: InterpreterClient;
src: string;
outEl: HTMLElement;
pyReplTag: HTMLElement;
result: any;
}) {
/* empty */
}
/** Startup complete. The interpreter is initialized and ready, user
* scripts have been executed: the main initialization logic ends here and
* the page is ready to accept user interactions.
@@ -158,6 +188,18 @@ export class PluginManager {
for (const p of this._pythonPlugins) p.afterPyScriptExec?.callKwargs(options);
}
beforePyReplExec(options: { interpreter: InterpreterClient; src: string; outEl: HTMLElement; pyReplTag: any }) {
for (const p of this._plugins) p.beforePyReplExec(options);
for (const p of this._pythonPlugins) p.beforePyReplExec?.callKwargs(options);
}
afterPyReplExec(options: { interpreter: InterpreterClient; src: string; outEl; pyReplTag; result }) {
for (const p of this._plugins) p.afterPyReplExec(options);
for (const p of this._pythonPlugins) p.afterPyReplExec?.callKwargs(options);
}
onUserError(error: UserError) {
for (const p of this._plugins) p.onUserError?.(error);

View File

@@ -1,7 +1,10 @@
import { Plugin } from '../plugin';
import { TargetedStdio, StdioMultiplexer } from '../stdio';
import type { InterpreterClient } from '../interpreter_client';
import { createSingularWarning } from '../utils';
import { make_PyScript } from '../components/pyscript';
import { InterpreterClient } from '../interpreter_client';
import { pyDisplay } from '../pyexec';
import { make_PyRepl } from '../components/pyrepl';
type PyScriptTag = InstanceType<ReturnType<typeof make_PyScript>>;
@@ -58,4 +61,71 @@ export class StdioDirector extends Plugin {
options.pyScriptTag.stderr_manager = null;
}
}
beforePyReplExec(options: {
interpreter: InterpreterClient;
src: string;
outEl: HTMLElement;
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
}): void {
//Handle 'output-mode' attribute (removed in PR #881/f9194cc8, restored here)
//If output-mode == 'append', don't clear target tag before writing
if (options.pyReplTag.getAttribute('output-mode') != 'append') {
options.outEl.innerHTML = '';
}
// Handle 'output' attribute; defaults to writing stdout to the existing outEl
// If 'output' attribute is used, the DOM element with the specified ID receives
// -both- sys.stdout and sys.stderr
let output_targeted_io: TargetedStdio;
if (options.pyReplTag.hasAttribute('output')) {
output_targeted_io = new TargetedStdio(options.pyReplTag, 'output', true, true);
} else {
output_targeted_io = new TargetedStdio(options.pyReplTag.outDiv, 'id', true, true);
}
options.pyReplTag.stdout_manager = output_targeted_io;
this._stdioMultiplexer.addListener(output_targeted_io);
//Handle 'stderr' attribute;
if (options.pyReplTag.hasAttribute('stderr')) {
const stderr_targeted_io = new TargetedStdio(options.pyReplTag, 'stderr', false, true);
options.pyReplTag.stderr_manager = stderr_targeted_io;
this._stdioMultiplexer.addListener(stderr_targeted_io);
}
}
afterPyReplExec(options: {
interpreter: InterpreterClient;
src: string;
outEl: HTMLElement;
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
result: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}): void {
// display the value of the last-evaluated expression in the REPL
if (options.result !== undefined) {
const outputId: string | undefined = options.pyReplTag.getAttribute('output');
if (outputId) {
// 'output' attribute also used as location to send
// result of REPL
if (document.getElementById(outputId)) {
pyDisplay(options.interpreter, options.result, { target: outputId });
} else {
//no matching element on page
createSingularWarning(`output = "${outputId}" does not match the id of any element on the page.`);
}
} else {
// 'otuput atribuite not provided
pyDisplay(options.interpreter, options.result, { target: options.outEl.id });
}
}
if (options.pyReplTag.stdout_manager != null) {
this._stdioMultiplexer.removeListener(options.pyReplTag.stdout_manager);
options.pyReplTag.stdout_manager = null;
}
if (options.pyReplTag.stderr_manager != null) {
this._stdioMultiplexer.removeListener(options.pyReplTag.stderr_manager);
options.pyReplTag.stderr_manager = null;
}
}
}

View File

@@ -54,7 +54,7 @@ plugin = TestLogger()
# Source of script that defines a plugin with only beforePyScriptExec and
# afterPyScriptExec methods
EXEC_HOOKS_PLUGIN_CODE = """
PYSCRIPT_HOOKS_PLUGIN_CODE = """
from pyscript import Plugin
from js import console
@@ -75,6 +75,31 @@ class ExecTestLogger(Plugin):
plugin = ExecTestLogger()
"""
# Source of script that defines a plugin with only beforePyScriptExec and
# afterPyScriptExec methods
PYREPL_HOOKS_PLUGIN_CODE = """
from pyscript import Plugin
from js import console
console.warn("This is in pyrepl hooks file")
class PyReplTestLogger(Plugin):
def beforePyReplExec(self, interpreter, outEl, src, pyReplTag):
console.log(f'beforePyReplExec called')
console.log(f'before_src:{src}')
console.log(f'before_id:{pyReplTag.id}')
def afterPyReplExec(self, interpreter, src, outEl, pyReplTag, result):
console.log(f'afterPyReplExec called')
console.log(f'after_src:{src}')
console.log(f'after_id:{pyReplTag.id}')
console.log(f'result:{result}')
plugin = PyReplTestLogger()
"""
# Source of a script that doesn't call define a `plugin` attribute
NO_PLUGIN_CODE = """
from pyscript import Plugin
@@ -195,6 +220,8 @@ class TestPlugin(PyScriptTest):
"beforeLaunch",
"beforePyScriptExec",
"afterPyScriptExec",
"beforePyReplExec",
"afterPyReplExec",
]
# EXPECT it to log the correct logs for the events it intercepts
@@ -211,7 +238,7 @@ class TestPlugin(PyScriptTest):
@prepare_test(
"exec_test_logger",
EXEC_HOOKS_PLUGIN_CODE,
PYSCRIPT_HOOKS_PLUGIN_CODE,
template=HTML_TEMPLATE_NO_TAG + "\n<py-script id='pyid'>x=2; x</py-script>",
)
def test_pyscript_exec_hooks(self):
@@ -231,6 +258,28 @@ class TestPlugin(PyScriptTest):
assert "after_id:pyid" in log_lines
assert "result:2" in log_lines
@prepare_test(
"pyrepl_test_logger",
PYREPL_HOOKS_PLUGIN_CODE,
template=HTML_TEMPLATE_NO_TAG + "\n<py-repl id='pyid'>x=2; x</py-repl>",
)
def test_pyrepl_exec_hooks(self):
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
log_lines: list[str] = self.console.log.lines
assert "beforePyReplExec called" in log_lines
assert "afterPyReplExec called" in log_lines
# These could be made better with a utility function that found log lines
# that match a filter function, or start with something
assert "before_src:x=2; x" in log_lines
assert "before_id:pyid" in log_lines
assert "after_src:x=2; x" in log_lines
assert "after_id:pyid" in log_lines
assert "result:2" in log_lines
@prepare_test("no_plugin", NO_PLUGIN_CODE)
def test_no_plugin_attribute_error(self):
"""

View File

@@ -109,6 +109,27 @@ class TestPyRepl(PyScriptTest):
out_div = py_repl.locator("div.py-repl-output")
assert out_div.all_inner_texts()[0] == "42"
def test_show_last_expression_with_output(self):
"""
Test that we display() the value of the last expression, as you would
expect by a REPL
"""
self.pyscript_run(
"""
<div id="repl-target"></div>
<py-repl output="repl-target">
42
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
out_div = py_repl.locator("div.py-repl-output")
assert out_div.all_inner_texts()[0] == ""
out_div = self.page.locator("#repl-target")
assert out_div.all_inner_texts()[0] == "42"
def test_run_clears_previous_output(self):
"""
Check that we clear the previous output of the cell before executing it
@@ -219,26 +240,6 @@ class TestPyRepl(PyScriptTest):
self.page.keyboard.press("Shift+Enter")
assert out_div.all_inner_texts()[0] == "hello"
def test_output_attribute(self):
self.pyscript_run(
"""
<py-repl output="mydiv">
display('hello world')
</py-repl>
<hr>
<div id="mydiv"></div>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
#
# check that we did NOT write to py-repl-output
out_div = py_repl.locator("div.py-repl-output")
assert out_div.inner_text() == ""
# check that we are using mydiv instead
mydiv = self.page.locator("#mydiv")
assert mydiv.all_inner_texts()[0] == "hello world"
def test_output_attribute_does_not_exist(self):
"""
If we try to use an attribute which doesn't exist, we display an error
@@ -253,11 +254,15 @@ class TestPyRepl(PyScriptTest):
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
#
out_div = py_repl.locator("div.py-repl-output")
msg = "py-repl ERROR: cannot find the output element #I-dont-exist in the DOM"
assert out_div.all_inner_texts()[0] == msg
assert "I will not be executed" not in self.console.log.text
banner = self.page.query_selector_all(".py-warning")
assert len(banner) == 1
banner_content = banner[0].inner_text()
expected = (
'output = "I-dont-exist" does not match the id of any element on the page.'
)
assert banner_content == expected
def test_auto_generate(self):
self.pyscript_run(
@@ -315,3 +320,249 @@ class TestPyRepl(PyScriptTest):
assert self.page.inner_text("#py-internal-1-1-repl-output") == "second children"
assert self.page.inner_text("#py-internal-0-1-repl-output") == "first children"
def test_repl_output_attribute(self):
# Test that output attribute sends stdout to the element
# with the given ID, but not display()
self.pyscript_run(
"""
<div id="repl-target"></div>
<py-repl output="repl-target">
print('print from py-repl')
display('display from py-repl')
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
target = self.page.locator("#repl-target")
assert "print from py-repl" in target.text_content()
out_div = py_repl.locator("div.py-repl-output")
assert out_div.all_inner_texts()[0] == "display from py-repl"
self.assert_no_banners()
def test_repl_output_display_async(self):
# py-repls running async code are not expected to
# send display to element element
self.pyscript_run(
"""
<div id="repl-target"></div>
<py-script>
import asyncio
import js
async def print_it():
await asyncio.sleep(1)
print('print from py-repl')
async def display_it():
display('display from py-repl')
await asyncio.sleep(2)
async def done():
await asyncio.sleep(3)
js.console.log("DONE")
</py-script>
<py-repl output="repl-target">
asyncio.ensure_future(print_it());
asyncio.ensure_future(display_it());
asyncio.ensure_future(done());
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
self.wait_for_console("DONE")
assert self.page.locator("#repl-target").text_content() == ""
self.assert_no_banners()
def test_repl_stdio_dynamic_tags(self):
self.pyscript_run(
"""
<div id="first"></div>
<div id="second"></div>
<py-repl output="first">
import js
print("first.")
# Using string, since no clean way to write to the
# code contents of the CodeMirror in a PyRepl
newTag = '<py-repl id="second-repl" output="second">print("second.")</py-repl>'
js.document.body.innerHTML += newTag
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
assert self.page.locator("#first").text_content() == "first."
second_repl = self.page.locator("py-repl#second-repl")
second_repl.locator("button").click()
assert self.page.locator("#second").text_content() == "second."
def test_repl_output_id_errors(self):
self.pyscript_run(
"""
<py-repl output="not-on-page">
print("bad.")
print("bad.")
</py-repl>
<py-repl output="not-on-page">
print("bad.")
</py-repl>
"""
)
py_repls = self.page.query_selector_all("py-repl")
for repl in py_repls:
repl.query_selector_all("button")[0].click()
banner = self.page.query_selector_all(".py-warning")
assert len(banner) == 1
banner_content = banner[0].inner_text()
expected = (
'output = "not-on-page" does not match the id of any element on the page.'
)
assert banner_content == expected
def test_repl_stderr_id_errors(self):
self.pyscript_run(
"""
<py-repl stderr="not-on-page">
import sys
print("bad.", file=sys.stderr)
print("bad.", file=sys.stderr)
</py-repl>
<py-repl stderr="not-on-page">
print("bad.", file=sys.stderr)
</py-repl>
"""
)
py_repls = self.page.query_selector_all("py-repl")
for repl in py_repls:
repl.query_selector_all("button")[0].click()
banner = self.page.query_selector_all(".py-warning")
assert len(banner) == 1
banner_content = banner[0].inner_text()
expected = (
'stderr = "not-on-page" does not match the id of any element on the page.'
)
assert banner_content == expected
def test_repl_output_stderr(self):
# Test that stderr works, and routes to the same location as stdout
# Also, repls with the stderr attribute route to an additional location
self.pyscript_run(
"""
<div id="stdout-div"></div>
<div id="stderr-div"></div>
<py-repl output="stdout-div" stderr="stderr-div">
import sys
print("one.", file=sys.stderr)
print("two.")
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
assert self.page.locator("#stdout-div").text_content() == "one.two."
assert self.page.locator("#stderr-div").text_content() == "one."
self.assert_no_banners()
def test_repl_output_attribute_change(self):
# If the user changes the 'output' attribute of a <py-repl> tag mid-execution,
# Output should no longer go to the selected div and a warning should appear
self.pyscript_run(
"""
<div id="first"></div>
<div id="second"></div>
<!-- There is no tag with id "third" -->
<py-repl id="repl-tag" output="first">
print("one.")
# Change the 'output' attribute of this tag
import js
this_tag = js.document.getElementById("repl-tag")
this_tag.setAttribute("output", "second")
print("two.")
this_tag.setAttribute("output", "third")
print("three.")
</py-script>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
assert self.page.locator("#first").text_content() == "one."
assert self.page.locator("#second").text_content() == "two."
expected_alert_banner_msg = (
'output = "third" does not match the id of any element on the page.'
)
alert_banner = self.page.locator(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()
def test_repl_output_element_id_change(self):
# If the user changes the ID of the targeted DOM element mid-execution,
# Output should no longer go to the selected element and a warning should appear
self.pyscript_run(
"""
<div id="first"></div>
<div id="second"></div>
<!-- There is no tag with id "third" -->
<py-repl id="pyscript-tag" output="first">
print("one.")
# Change the ID of the targeted DIV to something else
import js
target_tag = js.document.getElementById("first")
# should fail and show banner
target_tag.setAttribute("id", "second")
print("two.")
# But changing both the 'output' attribute and the id of the target
# should work
target_tag.setAttribute("id", "third")
js.document.getElementById("pyscript-tag").setAttribute("output", "third")
print("three.")
</py-repl>
"""
)
py_repl = self.page.locator("py-repl")
py_repl.locator("button").click()
# Note the ID of the div has changed by the time of this assert
assert self.page.locator("#third").text_content() == "one.three."
expected_alert_banner_msg = (
'output = "first" does not match the id of any element on the page.'
)
alert_banner = self.page.locator(".alert-banner")
assert expected_alert_banner_msg in alert_banner.inner_text()

View File

@@ -6,6 +6,10 @@ import pytest
pyscriptjs = Path(__file__).parents[2]
import pytest # noqa
pyscriptjs = Path(__file__).parents[2]
# add pyscript folder to path
python_source = pyscriptjs / "src" / "python"
sys.path.append(str(python_source))