Skip to content

Commit

Permalink
Add REPL plugin hooks; Add output, output-mode, stderr attribut…
Browse files Browse the repository at this point in the history
…es (#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>
  • Loading branch information
3 people committed Mar 23, 2023
1 parent 51d5140 commit ef793ae
Show file tree
Hide file tree
Showing 10 changed files with 514 additions and 91 deletions.
18 changes: 15 additions & 3 deletions docs/changelog.md
Expand Up @@ -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
---------
Expand All @@ -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
---------

Expand All @@ -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))

Expand Down
30 changes: 24 additions & 6 deletions docs/reference/elements/py-repl.md
Expand Up @@ -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.
5 changes: 3 additions & 2 deletions pyscriptjs/src/components/elements.ts
@@ -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);
Expand Down
73 changes: 24 additions & 49 deletions pyscriptjs/src/components/pyrepl.ts
Expand Up @@ -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>
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
Expand All @@ -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 {
Expand All @@ -206,27 +187,21 @@ export function make_PyRepl(interpreter: InterpreterClient) {
const nextExecId = parseInt(lastExecId) + 1;

const newPyRepl = document.createElement('py-repl');
newPyRepl.setAttribute('root', this.getAttribute('root'));
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();

if (this.hasAttribute('auto-generate')) {
newPyRepl.setAttribute('auto-generate', '');
this.removeAttribute('auto-generate');
}

const outputMode = getAttribute(this, 'output-mode');
if (outputMode) {
newPyRepl.setAttribute('output-mode', outputMode);
}

const addReplAttribute = (attribute: string) => {
//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);
}
};
}

addReplAttribute('output');
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();

if (this.hasAttribute('auto-generate')) {
newPyRepl.setAttribute('auto-generate', '');
this.removeAttribute('auto-generate');
}

newPyRepl.setAttribute('exec-id', nextExecId.toString());
if (this.parentElement) {
Expand Down
3 changes: 2 additions & 1 deletion pyscriptjs/src/main.ts
Expand Up @@ -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
Expand Down
46 changes: 44 additions & 2 deletions pyscriptjs/src/plugin.ts
Expand Up @@ -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
*/
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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);

Expand Down
72 changes: 71 additions & 1 deletion pyscriptjs/src/plugins/stdiodirector.ts
@@ -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>>;

Expand Down Expand Up @@ -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;
}
}
}

0 comments on commit ef793ae

Please sign in to comment.