-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Introduce a Plugin system, implement <py-terminal> as a plugin #917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
e55f6d6
rename this file, it's about the Pyodide class not the Runtime class
antocuni b5af865
start to add the necessary machinery to redirect pyodide stdout/stder…
antocuni 051f940
add StdioMultiplexer, which allows to redirect streams to multiple li…
antocuni 4e76376
wooo, introduce the <py-terminal> plugin
antocuni 9226772
use white-on-black for py-terminal colors
antocuni 31ccfc8
make sure that the <py-terminal> is shown even if it's empty
antocuni 08d9c2f
add a passing test
antocuni 940dbe9
use .stdout_writeline() instead of .stdout(), it's clearer
antocuni ad8233c
implement the Stdio interface directly inside PyTerminal
antocuni bec6305
add the 'auto' attribute
antocuni 0c5413c
add support for config.terminal="yes"
antocuni a95ada0
add support for config.terminal
antocuni dde0c89
introduce PluginManager and make PyTerminal a real plugin
antocuni dd294a2
Merge remote-tracking branch 'origin/main' into antocuni/py-terminal
antocuni a80c5f7
fix this test
antocuni 095f156
use a better type
antocuni cb0bac7
Merge remote-tracking branch 'origin/main' into antocuni/py-terminal
antocuni fb0cb61
use UserError
antocuni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { PyScriptApp } from './main'; | ||
import type { AppConfig } from './pyconfig'; | ||
import type { Runtime } from './runtime'; | ||
|
||
export class Plugin { | ||
|
||
/** Validate the configuration of the plugin and handle default values. | ||
* | ||
* Individual plugins are expected to check that the config keys/sections | ||
* which are relevant to them contains valid values, and to raise an error | ||
* if they contains unknown keys. | ||
* | ||
* This is also a good place where set default values for those keys which | ||
* are not specified by the user. | ||
* | ||
* This hook should **NOT** contain expensive operations, else it delays | ||
* the download of the python interpreter which is initiated later. | ||
*/ | ||
configure(config: AppConfig) { | ||
} | ||
|
||
/** The preliminary initialization phase is complete and we are about to | ||
* download and launch the Python interpreter. | ||
* | ||
* We can assume that the page is already shown to the user and that the | ||
* DOM content has been loaded. This is a good place where to add tags to | ||
* the DOM, if needed. | ||
* | ||
* This hook should **NOT** contain expensive operations, else it delays | ||
* the download of the python interpreter which is initiated later. | ||
*/ | ||
beforeLaunch(config: AppConfig) { | ||
} | ||
|
||
/** The Python interpreter has been launched, the virtualenv has been | ||
* installed and we are ready to execute user code. | ||
* | ||
* The <py-script> tags will be executed after this hook. | ||
*/ | ||
afterSetup(runtime: Runtime) { | ||
} | ||
} | ||
|
||
|
||
export class PluginManager { | ||
_plugins: Plugin[]; | ||
|
||
constructor() { | ||
this._plugins = []; | ||
} | ||
|
||
add(p: Plugin) { | ||
this._plugins.push(p); | ||
} | ||
|
||
configure(config: AppConfig) { | ||
for (const p of this._plugins) | ||
p.configure(config); | ||
} | ||
|
||
beforeLaunch(config: AppConfig) { | ||
for (const p of this._plugins) | ||
p.beforeLaunch(config); | ||
} | ||
|
||
afterSetup(runtime: Runtime) { | ||
for (const p of this._plugins) | ||
p.afterSetup(runtime); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import type { PyScriptApp } from '../main'; | ||
import type { AppConfig } from '../pyconfig'; | ||
import { Plugin } from '../plugin'; | ||
EDBE | import { UserError } from "../exceptions" | |
import { getLogger } from '../logger'; | ||
import { type Stdio } from '../stdio'; | ||
|
||
const logger = getLogger('py-terminal'); | ||
|
||
export class PyTerminalPlugin extends Plugin { | ||
app: PyScriptApp; | ||
|
||
constructor(app: PyScriptApp) { | ||
super(); | ||
this.app = app; | ||
} | ||
|
||
configure(config: AppConfig) { | ||
// validate the terminal config and handle default values | ||
const t = config.terminal; | ||
if (t !== undefined && | ||
t !== true && | ||
t !== false && | ||
t !== "auto") { | ||
const got = JSON.stringify(t); | ||
throw new UserError('Invalid value for config.terminal: the only accepted' + | ||
`values are true, false and "auto", got "${got}".`); | ||
} | ||
if (t === undefined) { | ||
config.terminal = "auto"; // default value | ||
} | ||
} | ||
|
||
beforeLaunch(config: AppConfig) { | ||
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to | ||
// the document, unless it's already present. | ||
const t = config.terminal; | ||
if (t === true || t === "auto") { | ||
if (document.querySelector('py-terminal') === null) { | ||
logger.info("No <py-terminal> found, adding one"); | ||
const termElem = document.createElement('py-terminal'); | ||
if (t === "auto") | ||
termElem.setAttribute("auto", ""); | ||
document.body.appendChild(termElem); | ||
} | ||
} | ||
} | ||
|
||
afterSetup() { | ||
// the Python interpreter has been initialized and we are ready to | ||
// execute user code: | ||
// | ||
// 1. define the "py-terminal" custom element | ||
// | ||
// 2. if there is a <py-terminal> tag on the page, it will register | ||
// a Stdio listener just before the user code executes, ensuring | ||
// that we capture all the output | ||
// | ||
// 3. everything which was written to stdout BEFORE this moment will | ||
// NOT be shown on the py-terminal; in particular, pyodide | ||
// startup messages will not be shown (but they will go to the | ||
// console as usual). This is by design, else we would display | ||
// e.g. "Python initialization complete" on every page, which we | ||
// don't want. | ||
// | ||
// 4. (in the future we might want to add an option to start the | ||
// capture earlier, but I don't think it's important now). | ||
const PyTerminal = make_PyTerminal(this.app); | ||
customElements.define('py-terminal', PyTerminal); | ||
} | ||
} | ||
|
||
|
||
function make_PyTerminal(app: PyScriptApp) { | ||
|
||
/** The <py-terminal> custom element, which automatically register a stdio | ||
* listener to capture and display stdout/stderr | ||
*/ | ||
class PyTerminal extends HTMLElement implements Stdio { | ||
outElem: HTMLElement; | ||
autoShowOnNextLine: boolean; | ||
|
||
connectedCallback() { | ||
// should we use a shadowRoot instead? It looks unnecessarily | ||
// complicated to me, but I'm not really sure about the | ||
// implications | ||
this.outElem = document.createElement('pre'); | ||
this.outElem.className = 'py-terminal'; | ||
this.appendChild(this.outElem); | ||
|
||
if (this.isAuto()) { | ||
this.classList.add('py-terminal-hidden'); | ||
this.autoShowOnNextLine = true; | ||
} | ||
else { | ||
this.autoShowOnNextLine = false; | ||
} | ||
|
||
logger.info('Registering stdio listener'); | ||
app.registerStdioListener(this); | ||
} | ||
|
||
isAuto() { | ||
return this.hasAttribute("auto"); | ||
} | ||
|
||
// implementation of the Stdio interface | ||
stdout_writeline(msg: string) { | ||
this.outElem.innerText += msg + "\n"; | ||
if (this.autoShowOnNextLine) { | ||
this.classList.remove('py-terminal-hidden'); | ||
this.autoShowOnNextLine = false; | ||
} | ||
} | ||
|
||
stderr_writeline(msg: string) { | ||
this.stdout_writeline(msg); | ||
} | ||
// end of the Stdio interface | ||
} | ||
|
||
return PyTerminal; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
export interface Stdio { | ||
stdout_writeline: (msg: string) => void; | ||
stderr_writeline: (msg: string) => void; | ||
} | ||
|
||
/** Default implementation of Stdio: stdout and stderr are both sent to the | ||
* console | ||
*/ | ||
export const DEFAULT_STDIO: Stdio = { | ||
stdout_writeline: console.log, | ||
stderr_writeline: console.log | ||
} | ||
|
||
/** Stdio provider which captures and store the messages. | ||
* Useful for tests. | ||
*/ | ||
export class CaptureStdio implements Stdio { | ||
captured_stdout: string; | ||
captured_stderr: string; | ||
|
||
constructor() { | ||
this.reset(); | ||
} | ||
|
||
reset() { | ||
this.captured_stdout = ""; | ||
this.captured_stderr = ""; | ||
} | ||
|
||
stdout_writeline(msg: string) { | ||
this.captured_stdout += msg + "\n"; | ||
} | ||
|
||
stderr_writeline(msg: string) { | ||
this.captured_stderr += msg + "\n"; | ||
} | ||
} | ||
|
||
/** Redirect stdio streams to multiple listeners | ||
*/ | ||
export class StdioMultiplexer implements Stdio { | ||
_listeners: Stdio[]; | ||
|
||
constructor() { | ||
this._listeners = []; | ||
} | ||
|
||
addListener(obj: Stdio) { | ||
this._listeners.push(obj); | ||
} | ||
|
||
stdout_writeline(msg: string) { | ||
for(const obj of this._listeners) | ||
obj.stdout_writeline(msg); | ||
} | ||
|
||
stderr_writeline(msg: string) { | ||
for(const obj of this._listeners) | ||
obj.stderr_writeline(msg); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a nit, but would be nice to have a more discriptive name although the code is small enough to not be too confusing 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm following this rule here. The point is precisely that the code is short, and having a longer name would make it harder to read IMHO.
The reason why I introduce the extra var was to avoid this:
That said, I'm happy to change
t
into a longer name if you think it leads to more readable code: what do you think it should be?I'm serious here: an external reviewer is a better judge w.r.t readability than the original author of the code, so happy to hear your suggestions.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI in that part there is no
t
anymore :)