8000 Add REPL plugin hooks; Add `output`, `output-mode`, `stderr` attribut… · SamuelDelmonte/pyscript-1@ef793ae · GitHub
[go: up one dir, main page]

Skip to content

Commit ef793ae

Browse files
JeffersGlasspre-commit-ci[bot]marimeireles
authored
Add REPL plugin hooks; Add output, output-mode, stderr attributes (pyscript#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>
1 parent 51d5140 commit ef793ae

File tree

10 files changed

+514
-91
lines changed

10 files changed

+514
-91
lines changed

docs/changelog.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,21 @@
66
Features
77
--------
88

9+
10+
### &lt;py-terminal&gt;
911
- 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.
1012

13+
### &lt;py-script&gt;
14+
- 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))
15+
- 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))
16+
17+
### &lt;py-repl&gt;
18+
- 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()`
19+
- 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))
20+
- 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.
21+
22+
### Plugins
23+
- 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))
1124

1225
Bug fixes
1326
---------
@@ -24,12 +37,10 @@ Enhancements
2437
2023.01.1
2538
=========
2639

40+
2741
Features
2842
--------
2943

30-
- 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))
31-
- 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))
32-
3344
Bug fixes
3445
---------
3546

@@ -39,6 +50,7 @@ Bug fixes
3950

4051
Enhancements
4152
------------
53+
4254
- 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))
4355
- 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))
4456

docs/reference/elements/py-repl.md

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,42 @@ The `<py-repl>` element provides a REPL(Read Eval Print Loop) to evaluate multi-
77
| attribute | type | default | description |
88
|-------------------|---------|---------|---------------------------------------|
99
| **auto-generate** | boolean | | Auto-generates REPL after evaluation |
10-
| **output** | string | | The element to write output into |
10+
| **output-mode** | string | "" | Determines whether the output element is cleared prior to writing output |
11+
| **output** | string | | The id of the element to write `stdout` and `stderr` to |
12+
| **stderr** | string | | The id of the element to write `stderr` to |
1113

12-
### Examples
1314

14-
#### `<py-repl>` element set to auto-generate
15+
### `auto-generate`
16+
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.
17+
18+
### `output-mode`
19+
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.
20+
21+
### `output`
22+
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.
23+
24+
### `stderr`
25+
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`.
26+
27+
## Examples
28+
29+
### `<py-repl>` element set to auto-generate
1530

1631
```html
1732
<py-repl auto-generate="true"> </py-repl>
1833
```
1934

20-
#### `<py-repl>` element with output
35+
### `<py-repl>` element with output
36+
37+
The following will write "Hello! World!" to the div with id `replOutput`.
2138

2239
```html
2340
<div id="replOutput"></div>
2441
<py-repl output="replOutput">
25-
hello = "Hello world!"
42+
print("Hello!")
43+
hello = "World!"
2644
hello
2745
</py-repl>
2846
```
2947

30-
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.
48+
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.

pyscriptjs/src/components/elements.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { InterpreterClient } from '../interpreter_client';
2+
import type { PyScriptApp } from '../main';
23
import { make_PyRepl } from './pyrepl';
34
import { make_PyWidget } from './pywidget';
45

5-
function createCustomElements(interpreter: InterpreterClient) {
6+
function createCustomElements(interpreter: InterpreterClient, app: PyScriptApp) {
67
const PyWidget = make_PyWidget(interpreter);
7-
const PyRepl = make_PyRepl(interpreter);
8+
const PyRepl = make_PyRepl(interpreter, app);
89

910
/* eslint-disable @typescript-eslint/no-unused-vars */
1011
const xPyRepl = customElements.define('py-repl', PyRepl);

pyscriptjs/src/components/pyrepl.ts

Lines changed: 24 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import { defaultKeymap } from '@codemirror/commands';
77
import { oneDarkTheme } from '@codemirror/theme-one-dark';
88

99
import { getAttribute, ensureUniqueId, htmlDecode } from '../utils';
10-
import { pyExec, pyDisplay } from '../pyexec';
10+
import { pyExec } from '../pyexec';
1111
import { getLogger } from '../logger';
1212
import { InterpreterClient } from '../interpreter_client';
13+
import type { PyScriptApp } from '../main';
14+
import { Stdio } from '../stdio';
1315

1416
const logger = getLogger('py-repl');
1517
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>`;
1618

17-
export function make_PyRepl(interpreter: InterpreterClient) {
19+
export function make_PyRepl(interpreter: InterpreterClient, app: PyScriptApp) {
1820
/* High level structure of py-repl DOM, and the corresponding JS names.
1921
2022
this <py-repl>
@@ -31,6 +33,8 @@ export function make_PyRepl(interpreter: InterpreterClient) {
3133
shadow: ShadowRoot;
3234
outDiv: HTMLElement;
3335
editor: EditorView;
36+
stdout_manager: Stdio | null;
37+
stderr_manager: Stdio | null;
3438

3539
constructor() {
3640
super();
@@ -152,27 +156,19 @@ export function make_PyRepl(interpreter: InterpreterClient) {
152156
*/
153157
async execute(): Promise<void> {
154158
const pySrc = this.getPySrc();
155-
156-
// determine the output element
157-
const outEl = this.getOutputElement();
158-
if (outEl === undefined) {
159-
// this happens if we specified output="..." but we couldn't
160-
// find the ID. We already displayed an error message inside
161-
// getOutputElement, stop the execution.
162-
return;
163-
}
164-
165-
// clear the old output before executing the new code
166-
outEl.innerHTML = '';
159+
const outEl = this.outDiv;
167160

168161
// execute the python code
162+
app.plugins.beforePyReplExec({ interpreter: interpreter, src: pySrc, outEl: outEl, pyReplTag: this });
169163
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
170164
const pyResult = (await pyExec(interpreter, pySrc, outEl)).result;
171-
172-
// display the value of the last evaluated expression (REPL-style)
173-
if (pyResult !== undefined) {
174-
pyDisplay(interpreter, pyResult, { target: outEl.id });
175-
}
165+
app.plugins.afterPyReplExec({
166+
interpreter: interpreter,
167+
src: pySrc,
168+
outEl: outEl,
169+
pyReplTag: this,
170+
result: pyResult, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
171+
});
176172

177173
this.autogenerateMaybe();
178174
}
@@ -181,21 +177,6 @@ export function make_PyRepl(interpreter: InterpreterClient) {
181177
return this.editor.state.doc.toString();
182178
}
183179

184-
getOutputElement(): HTMLElement {
185-
const outputID = getAttribute(this, 'output');
186-
if (outputID !== null) {
187-
const el = document.getElementById(outputID);
188-
if (el === null) {
189-
const err = `py-repl ERROR: cannot find the output element #${outputID} in the DOM`;
190-
this.outDiv.innerText = err;
191-
return undefined;
192-
}
193-
return el;
194-
} else {
195-
return this.outDiv;
196-
}
197-
}
198-
199180
// XXX the autogenerate logic is very messy. We should redo it, and it
200181
// should be the default.
201182
autogenerateMaybe(): void {
@@ -206,27 +187,21 @@ export function make_PyRepl(interpreter: InterpreterClient) {
206187
const nextExecId = parseInt(lastExecId) + 1;
207188

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

212-
if (this.hasAttribute('auto-generate')) {
213-
newPyRepl.setAttribute('auto-generate', '');
214-
this.removeAttribute('auto-generate');
215-
}
216-
217-
const outputMode = getAttribute(this, 'output-mode');
218-
if (outputMode) {
219-
newPyRepl.setAttribute('output-mode', outputMode);
220-
}
221-
222-
const addReplAttribute = (attribute: string) => {
191+
//Attributes to be copied from old REPL to auto-generated REPL
192+
for (const attribute of ['root', 'output-mode', 'output', 'stderr']) {
223193
const attr = getAttribute(this, attribute);
224194
if (attr) {
225195
newPyRepl.setAttribute(attribute, attr);
226196
}
227-
};
197+
}
228198

229-
addReplAttribute('output');
199+
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
200+
201+
if (this.hasAttribute('auto-generate')) {
202+
newPyRepl.setAttribute('auto-generate', '');
203+
this.removeAttribute('auto-generate');
204+
}
230205

231206
newPyRepl.setAttribute('exec-id', nextExecId.toString());
232207
if (this.parentElement) {

pyscriptjs/src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ export class PyScriptApp {
189189

190190
this.logStatus('Initializing web components...');
191191
// lifecycle (8)
192-
createCustomElements(interpreter);
193192

193+
//Takes a runtime and a reference to the PyScriptApp (to access plugins)
194+
createCustomElements(interpreter, this);
194195
await initHandlers(interpreter);
195196

196197
// NOTE: interpreter message is used by integration tests to know that

pyscriptjs/src/plugin.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class Plugin {
5555
/** The source of a <py-script>> tag has been fetched, and we're about
5656
* to evaluate that source using the provided interpreter.
5757
*
58-
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
58+
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
5959
* @param options.src {string} The Python source code to be evaluated
6060
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
6161
*/
@@ -66,7 +66,7 @@ export class Plugin {
6666
/** The Python in a <py-script> has just been evaluated, but control
6767
* has not been ceded back to the JavaScript event loop yet
6868
*
69-
* @param options.interpreter The Interpreter object that will be used to evaluated the Python source code
69+
* @param options.interpreter The Interpreter object that will be used to evaluate the Python source code
7070
* @param options.src {string} The Python source code to be evaluated
7171
* @param options.pyScriptTag The <py-script> HTML tag that originated the evaluation
7272
* @param options.result The returned result of evaluating the Python (if any)
@@ -80,6 +80,36 @@ export class Plugin {
8080
/* empty */
8181
}
8282

83+
/** The source of the <py-repl> tag has been fetched and its output-element determined;
84+
* we're about to evaluate the source using the provided interpreter
85+
*
86+
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
87+
* @param options.src {string} The Python source code to be evaluated
88+
* @param options.outEl The element that the result of the REPL evaluation will be output to.
89+
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
90+
*/
91+
beforePyReplExec(options: { interpreter: InterpreterClient; src: string; outEl: HTMLElement; pyReplTag: any }) {
92+
/* empty */
93+
}
94+
95+
/**
96+
*
97+
* @param options.interpreter The interpreter object that will be used to evaluated the Python source code
98+
* @param options.src {string} The Python source code to be evaluated
99+
* @param options.outEl The element that the result of the REPL evaluation will be output to.
100+
* @param options.pyReplTag The <py-repl> HTML tag the originated the evaluation
101+
* @param options.result The result of evaluating the Python (if any)
102+
*/
103+
afterPyReplExec(options: {
104+
interpreter: InterpreterClient;
105+
src: string;
106+
outEl: HTMLElement;
107+
pyReplTag: HTMLElement;
108+
result: any;
109+
}) {
110+
/* empty */
111+
}
112+
83113
/** Startup complete. The interpreter is initialized and ready, user
84114
* scripts have been executed: the main initialization logic ends here and
85115
* the page is ready to accept user interactions.
@@ -158,6 +188,18 @@ export class PluginManager {
158188
for (const p of this._pythonPlugins) p.afterPyScriptExec?.callKwargs(options);
159189
}
160190

191+
beforePyReplExec(options: { interpreter: InterpreterClient; src: string; outEl: HTMLElement; pyReplTag: any }) {
192+
for (const p of this._plugins) p.beforePyReplExec(options);
193+
194+
for (const p of this._pythonPlugins) p.beforePyReplExec?.callKwargs(options);
195+
}
196+
197+
afterPyReplExec(options: { interpreter: InterpreterClient; src: string; outEl; pyReplTag; result }) {
198+
for (const p of this._plugins) p.afterPyReplExec(options);
199+
200+
for (const p of this._pythonPlugins) p.afterPyReplExec?.callKwargs(options);
201+
}
202+
161203
onUserError(error: UserError) {
162204
for (const p of this._plugins) p.onUserError?.(error);
163205

pyscriptjs/src/plugins/stdiodirector.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Plugin } from '../plugin';
22
import { TargetedStdio, StdioMultiplexer } from '../stdio';
3+
import type { InterpreterClient } from '../interpreter_client';
4+
import { createSingularWarning } from '../utils';
35
import { make_PyScript } from '../components/pyscript';
4-
import { InterpreterClient } from '../interpreter_client';
6+
import { pyDisplay } from '../pyexec';
7+
import { make_PyRepl } from '../components/pyrepl';
58

69
type PyScriptTag = InstanceType<ReturnType<typeof make_PyScript>>;
710

@@ -58,4 +61,71 @@ export class StdioDirector extends Plugin {
5861
options.pyScriptTag.stderr_manager = null;
5962
}
6063
}
64+
65+
beforePyReplExec(options: {
66+
interpreter: InterpreterClient;
67+
src: string;
68+
outEl: HTMLElement;
69+
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
70+
}): void {
71+
//Handle 'output-mode' attribute (removed in PR #881/f9194cc8, restored here)
72+
//If output-mode == 'append', don't clear target tag before writing
73+
if (options.pyReplTag.getAttribute('output-mode') != 'append') {
74+
options.outEl.innerHTML = '';
75+
}
76+
77+
// Handle 'output' attribute; defaults to writing stdout to the existing outEl
78+
// If 'output' attribute is used, the DOM element with the specified ID receives
79+
// -both- sys.stdout and sys.stderr
80+
let output_targeted_io: TargetedStdio;
81+
if (options.pyReplTag.hasAttribute('output')) {
82+
output_targeted_io = new TargetedStdio(options.pyReplTag, 'output', true, true);
83+
} else {
84+
output_targeted_io = new TargetedStdio(options.pyReplTag.outDiv, 'id', true, true);
85+
}
86+
options.pyReplTag.stdout_manager = output_targeted_io;
87+
this._stdioMultiplexer.addListener(output_targeted_io);
88+
89+
//Handle 'stderr' attribute;
90+
if (options.pyReplTag.hasAttribute('stderr')) {
91+
const stderr_targeted_io = new TargetedStdio(options.pyReplTag, 'stderr', false, true);
92+
options.pyReplTag.stderr_manager = stderr_targeted_io;
93+
this._stdioMultiplexer.addListener(stderr_targeted_io);
94+
}
95+
}
96+
97+
afterPyReplExec(options: {
98+
interpreter: InterpreterClient;
99+
src: string;
100+
outEl: HTMLElement;
101+
pyReplTag: InstanceType<ReturnType<typeof make_PyRepl>>;
102+
result: any; // eslint-disable-line @typescript-eslint/no-explicit-any
103+
}): void {
104+
// display the value of the last-evaluated expression in the REPL
105+
if (options.result !== undefined) {
106+
const outputId: string | undefined = options.pyReplTag.getAttribute('output');
107+
if (outputId) {
108+
// 'output' attribute also used as location to send
109+
// result of REPL
110+
if (document.getElementById(outputId)) {
111+
pyDisplay(options.interpreter, options.result, { target: outputId });
112+
} else {
113+
//no matching element on page
114+
createSingularWarning(`output = "${outputId}" does not match the id of any element on the page.`);
115+
}
116+
} else {
117+
// 'otuput atribuite not provided
118+
pyDisplay(options.interpreter, options.result, { target: options.outEl.id });
119+
}
120+
}
121+
122+
if (options.pyReplTag.stdout_manager != null) {
123+
this._stdioMultiplexer.removeListener(options.pyReplTag.stdout_manager);
124+
options.pyReplTag.stdout_manager = null;
125+
}
126+
if (options.pyReplTag.stderr_manager != null) {
127+
this._stdioMultiplexer.removeListener(options.pyReplTag.stderr_manager);
128+
options.pyReplTag.stderr_manager = null;
129+
}
130+
}
61131
}

0 commit comments

Comments
 (0)
0