Skip to content
This repository was archived by the owner on Sep 21, 2023. It is now read-only.

Commit 9cdfbe6

Browse files
committed
Plugin all the logic, refactor, broken tests, updated README.
1 parent 0ba4ca8 commit 9cdfbe6

File tree

4 files changed

+600
-548
lines changed

4 files changed

+600
-548
lines changed

README.md

Lines changed: 144 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ MicroPython.
6161

6262
### What's in the files?
6363

64+
* **README.md** - this file, containing project documentation.
6465
* **Makefile** - common tasks scripted into convenient targets. See above.
6566
* **hello.py** - a simple "hello world" Python script for PyScript to run.
6667
* **index.html** - a small web page that uses PyScript.
6768
* **pyscript.js** - the simple, single file implementation of PyScript.
69+
* **SpecRunner.html** - a web page to run the test specifications with Jasmine.
70+
* **spec** - a directory containing the test specifications for Jasmine.
71+
* **lib** - a directory containing the Jasmine test library.
6872

6973
To change the configuration of PyScript take a look at the JSON object
7074
defined in the `<py-config>` tag in `index.html`. Currently valid runtimes are
@@ -82,131 +86,167 @@ and run the Jasmine based test suite.
8286

8387
## How it works
8488

89+
The PyScript core only loads configuration, starts the Python runtime and
90+
allows the registration of plugins. All other logic, capabilities and features
91+
are contained in the plugins.
92+
93+
Currently, only a single plugin is provided by PyScript: the one that
94+
implements the core `<py-script>` tag.
95+
96+
The story of PyScript's execution is roughly as follows:
97+
98+
1. Configuration is loaded from the `<py-config>` tag. Once complete the
99+
`py-configured` event is dispatched, containing the `config` object based
100+
upon default values overridden by the content of the `<py-config>` tag.
101+
2. When the `py-configured` event is dispatched two things happen:
102+
* The runtime is loaded via injecting a `<script>` tag that references the
103+
runtime's URL. Once loaded the `py-runtime-loaded` event is dispatched.
104+
* Plugins are registered and have their `configure` function called. For each
105+
plugin registered a `py-plugin-registered` event is dispatched, containing
106+
the (potentially changed) `config`, and a reference to the newly registered
107+
plugin.
108+
3. When `py-runtime-loaded` is dispatched the `config` is frozen and two things
109+
happen:
110+
* The runtime is instantiated / started. Once complete the `py-runtime-ready`
111+
event is dispatched.
112+
* All registered plugins have their `start` function called.
113+
4. When the `py-runtime-ready` event is dispatched all plugins have their
114+
`onRuntimeReady` function called with the `config` and `runtime` objects.
115+
5. Any plugins registered after the runtime is ready immediately have their
116+
`configure`, `start` and `onRuntimeReady` functions called, with the
117+
`py-plugin-registered` event being dispatched.
118+
119+
That's it!
120+
121+
When `pyscript.js` is run, it creates a `window.PyScript` object that contains
122+
read-only references to the `config`, registered `plugins`,
123+
`availableRuntimes`, the `runtime` used on the page, an `isRuntimeReady` flag,
124+
a `registerPlugin` function (see below) and a `runPython(code)` function that
125+
takes a string of Python.
126+
85127
There are copious comments in the `pyscript.js` file. My intention is for
86128
simplicity, lack of onerous dependencies (bye-bye `npm`), and
87-
understandability. This code is working if it's easy to understand what's going
129+
understandability. This code is good if it's easy to understand what's going
88130
on. To this end, it's laid out in a "literate" manner, so the code "tells the
89131
story" of this implementation of PyScript by reading it from top to bottom.
90132

91-
In terms of architecture, this version of PyScript aims to provide a very
92-
small core that coordinates features and capabilities provided by plugins. This
93-
coordination is almost always handled by dispatching custom events to the
94-
`document`. Everything is losely coupled, and state is contained within the
95-
function/closure that is the `main` function (called right at the end). The
96-
`main` function returns an object containing methods to interact with
97-
PyScript (useful for testing purposes).
98-
99-
### Base classes and constants
133+
## Plugins
100134

101-
The `Plugin` class is based upon Antonio's suggestion
135+
Plugins are inspired by Antonio's suggestion
102136
[found here](https://github.com/pyscript/pyscript/issues/763#issuecomment-1245086859),
103137
and should be relatively self explanatory.
104138

105-
The only difference between this implementation and Antonio's is that his
106-
version has `before_evalute` and `after_evaluate` methods. These are redundant
107-
in this version of PyScript since it's not known ahead of time when either the
108-
scripts nor runtime will be ready for evaluation. As far as I can tell, I think
109-
these functions are supposed to be run either before or after ALL the scripts
110-
are evaluated (at once) rather than before or after each individual script.
139+
Since simplicity is the focus, plugins are simply JavaScript objects.
140+
141+
Such objects are expected they have a `name` attribute referencing a string
142+
naming the plugin (useful for logging purposes). Plugins should also provide
143+
one or more of the following functions attached to them, called in the
144+
following order (as the lifecycle of the plugin):
145+
146+
* `configure(config)` - Gives the plugin early access to the `config` object.
147+
Potentially, the plugin can modify it, and modifications will be visible to
148+
later steps and other plugins. Plugins must only modify the config at this
149+
point in their life-cycle. Examples of things which plugins might want to do
150+
at this point:
151+
- Early sanity check about their own options.
152+
- Rename/remap some options.
153+
- Add new packages to install.
154+
- Modify options for other plugins (e.g. a debugger plugin might set the
155+
option `show_terminal`).
156+
* `start(config)` - The main entry point for plugins. At this point, config
157+
should not be modified by the plugin. Example use cases:
158+
- Define custom HTML elements.
159+
- Start fetching external resources.
160+
* `onRuntimeReady(config, runtime)` - Called once the runtime is ready to
161+
evaluate Python code. Example use cases:
162+
- `pip install` packages.
163+
- Import/initialize Python plugins.
164+
165+
The following events, dispatched by PyScript itself, are related to plugins:
166+
167+
* `py-plugin-registered` - Dispatched when a plugin is registered (and the
168+
event contains a reference to the newly registered plugin). This happens
169+
immediately after the plugin's `configure` function is called.
170+
* `py-plugin-started` - Dispatched immediately after a plugin's `start`
171+
function is called. The event contains a reference to the started plugin.
172+
* `py-runtime-ready` - causes each plugin's `onRuntimeReady` function to be
173+
called.
174+
175+
If a plugin is registered *after* the runtime is ready, all three functions are
176+
immediately called in the expected sequence, one after the other.
177+
178+
The recommended way to create and register plugins is:
179+
180+
```JavaScript
181+
const myPlugin = function(e) {
182+
/*
183+
Private and internal logic, event handlers and event dispatch can happen
184+
within the closure defined by this function.
185+
186+
e.g.
187+
188+
const FOO = "bar";
189+
190+
function foo() {
191+
const myEvent = new CustomEvent("my-event", {detail: {"foo": FOO}});
192+
document.dispatchEvent(myEvent);
193+
}
194+
195+
function onFoo(e) {
196+
console.log(e.detail);
197+
}
198+
199+
document.addEventListener("my-event", onFoo);
200+
201+
...
202+
*/
203+
204+
const plugin = {
205+
configure: function(config) {
206+
// ...
207+
},
208+
start: function(config) {
209+
// ...
210+
foo();
211+
},
212+
onRuntimeReady: function(config, runtime) {
213+
// ...
214+
}
215+
};
216+
window.pyScript.registerPlugin(plugin);
217+
}();
218+
219+
document.addEventListener("py-configured", myPlugin);
220+
```
111221

112-
This probably needs more thought/discussion.
222+
Then in your HTML file:
113223

114-
The `Runtime` class abstracts away all the implementation details of the
115-
various Python runtimes we might use. To see a complete implementation see the
116-
`MicroPythonRuntime` class that inherits from `Runtime`. There is also an
117-
incomplete `PyodideRuntime` class so I was able to compare and contrast the
118-
differences between implementations and arrive at a general abstraction (still
119-
very much a work in progress). Again, the comments in the code should explain
120-
what's going on in terms of the life-cycle and capabilities of a "runtime".
224+
```html
225+
<script src="myplugin.js"></script>
226+
<script src="pyscript.js" type="module"></script>
227+
```
228+
229+
A good example of a plugin is the built-in plugin for the `<py-script>` tag
230+
found in `pyscript.js` (search for pyScriptTag).
121231

122-
Finally, the `defaultSplash` is the `innerHtml` added to / removed from the DOM
123-
to indicate PyScript is starting up. It currently overlays an opaque DIV with
124-
the centred words "Loading PyScript...". This can be overridden by adding a
125-
`splash` entry to the JSON configuration in the `<py-config>` tag.
232+
## Runtimes
126233

127-
### Built-in plugins and runtimes
234+
The `Runtime` class abstracts away all the implementation details of the
235+
various Python runtimes we might use.
128236

129-
Currently, only one plugin is currently defined to handle the `<py-script>`
130-
tag: `PyScriptTag`. This is a rather simple plugin which ultimately dispatches
131-
the `py-script-registered` custom event (containing relevant metadata) to
132-
indicate Python source code has been found in the page (more on this later).
237+
To see a complete implementation see the `MicroPythonRuntime` class that
238+
inherits from `Runtime`. There is also an incomplete `PyodideRuntime` class so
239+
I was able to compare and contrast the differences between implementations and
240+
arrive at a general abstraction (still very much a work in progress). Comments
241+
in the code should explain what's going on in terms of the life-cycle and
242+
capabilities of a "runtime".
133243

134244
The afore mentioned `MicroPythonRuntime`, `CPythonRuntime` and `PyodideRuntime`
135245
all, to a greater or lesser extent, define a uniform shim around their
136246
respective runtimes. The MicroPython one is most complete, but still needs work
137247
as I make changes to how MicroPython itself exposes `stdout`, `stderr` and
138248
consumes `stdin`.
139249

140-
### The core PyScript app definition
141-
142-
This is simply a `main` function / closure, in which is stored lots of private
143-
state and definitions that we don't want bleeding out into the
144-
external-to-PyScript context.
145-
146-
The function starts with a definition of a very simple logger that pre-pends
147-
"🐍" to all PyScript related `console.log` messages, for ease of reading.
148-
149-
Next comes some declarations and initial states for various objects used to
150-
store state and coordinate the activity of PyScript. Because they only exist
151-
within the context of the closure, they're effectively private to the outside
152-
world. The comments and their names should indicate their function and how they
153-
relate to each other.
154-
155-
Next comes the definition of an `app` object. This is what is eventually
156-
returned by the `main` function (for testing purposes). The object contains
157-
various functions that manage the state and coordinate the activity of
158-
PyScript. Again, the function names and their associated commentary should
159-
describe what the intention is for each one. To be honest, they're all really
160-
very serious, with the most complicated being due to conditional paths
161-
depending on the state of the runtime.
162-
163-
As each function finishes its task, if required, it signals a change in state
164-
through dispatching custom events via the `document` object.
165-
166-
Underneath the `app` object are defined some event handler functions that
167-
"plumb together" the various capabilities defined in the `app`'s functions. How
168-
these relate to each other is described below.
169-
170-
Finally, depending on a `window.pyscriptTest` flag (set to `true` in a testing
171-
context), the event handlers are registered against the relevant events and
172-
the `loadConfig` function is called to boot up the whole thing.
173-
174-
The story of the PyScript app, roughly unfolds like this:
175-
176-
1. The `main` function is called, with the resulting `app` object bound to
177-
`window.pyscriptApp`.
178-
2. Calling `main` also causes PyScript to load any user configuration
179-
(currently, for simplicity's sake, expressed as JSON). When this is finished
180-
the `py-configured` event is fired.
181-
3. Next, built-in plugins are registered, after which the (internal) `config`
182-
object is frozen (i.e. can't change).
183-
4. The default `<py-script>` tag dispatches a `py-script-registered` event
184-
when a Python script is found.
185-
5. If the Python script's code is inline (i.e. a part of the document already)
186-
then a `py-script-loaded` event is dispatched for that script. However, if
187-
the Python script is referenced via a `src` URL, then PyScript fetches the
188-
remote asset and only dispatches `py-script-loaded` for the script when the
189-
code is retrieved.
190-
6. If the runtime is ready, each newly registered script is immediately
191-
evaluated by dispatching the `py-eval-script`. Otherwise, the scripts are
192-
added to a `pendingScripts` array for later processing when the runtime has
193-
finally started.
194-
7. In the meantime, the runtime specified in the `config` is loaded into the
195-
browser by injecting a `script` tag into the `head` of the document. When
196-
this script has finished loading a `py-runtime-loaded` event is dispatched.
197-
8. The `Runtime` subclass instance, representing the loaded runtime, then has
198-
its `start` method called. Upon completion of starting up the runtime, the
199-
`py-runtime-ready` event is dispatched and the `runtimeReady` flag is set
200-
to true.
201-
9. At this point, any scripts in the `pendingScripts` array are evaluated in
202-
order, after which the array is cleared and discarded.
203-
204-
That's it. You can see this unfolding in the image below, taken from the
205-
console logs for the example `hello.py` based application found in
206-
`index.html`.
207-
208-
![PyScript logs](https://raw.githubusercontent.com/ntoll/micropyscript/main/pyscript-log.png "PyScript logs")
209-
210250
## The future
211251

212252
Who knows..? But this is a good scaffold for testing different Python runtimes.

customtags.js

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ See the License for the specific language governing permissions and
2020
limitations under the License.
2121
******************************************************************************/
2222

23-
class PyREPLTag extends Plugin {
23+
const pyReplTag = function(e) {
2424
/*
2525
Adds a REPL to the DOM. The REPL session is only initialised when the
2626
runtime is ready. The content of the REPL is inserted in the following
@@ -33,22 +33,35 @@ class PyREPLTag extends Plugin {
3333
arrangement of tags will make it look like a TTY session). Bespoke CSS
3434
should use the pyscriptREPL class to attach styling.
3535
*/
36-
start(config) {
37-
// Define the py-repl element.
38-
class PyREPL extends HTMLElement {
39-
connectedCallback() {
40-
/*
41-
Create a shadow DOM with the expected child elements and event
42-
handlers defined in it.
43-
*/
44-
const shadow = this.attachShadow({ mode: "open" });
45-
const pre = document.createElement("pre");
46-
pre.setAttribute("class", "pyscriptREPL");
47-
const code = document.createElement("code");
48-
pre.appendChild(code);
49-
shadow.appendChild(pre);
36+
37+
const plugin = {
38+
configure: function(config) {
39+
// Just set a flag to indicate that a REPL is active.
40+
config.repl = true
41+
},
42+
start: function(config) {
43+
// Define the py-repl element.
44+
class PyREPL extends HTMLElement {
45+
connectedCallback() {
46+
/*
47+
Create a shadow DOM with the expected child elements and
48+
event handlers defined in it.
49+
*/
50+
const shadow = this.attachShadow({ mode: "open" });
51+
const pre = document.createElement("pre");
52+
pre.setAttribute("class", "pyscriptREPL");
53+
const code = document.createElement("code");
54+
pre.appendChild(code);
55+
shadow.appendChild(pre);
56+
}
5057
}
58+
customElements.define("py-repl", PyREPL);
59+
},
60+
onRuntimeReady: function(config, runtime) {
61+
//
5162
}
52-
customElements.define("py-repl", PyREPL);
53-
}
54-
}
63+
};
64+
65+
window.pyScript.registerPlugin(plugin);
66+
};
67+
document.addEventListener("py-configured", pyReplTag);

pyscript-log.png

-254 KB
Binary file not shown.

0 commit comments

Comments
 (0)