mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-20 22:00:19 -04:00
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:
@@ -6,8 +6,21 @@
|
||||
Features
|
||||
--------
|
||||
|
||||
|
||||
### <py-terminal>
|
||||
- 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.
|
||||
|
||||
### <py-script>
|
||||
- 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))
|
||||
|
||||
### <py-repl>
|
||||
- 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 <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))
|
||||
|
||||
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))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user