@@ -61,10 +61,14 @@ MicroPython.
61
61
62
62
### What's in the files?
63
63
64
+ * ** README.md** - this file, containing project documentation.
64
65
* ** Makefile** - common tasks scripted into convenient targets. See above.
65
66
* ** hello.py** - a simple "hello world" Python script for PyScript to run.
66
67
* ** index.html** - a small web page that uses PyScript.
67
68
* ** 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.
68
72
69
73
To change the configuration of PyScript take a look at the JSON object
70
74
defined in the ` <py-config> ` tag in ` index.html ` . Currently valid runtimes are
@@ -82,131 +86,167 @@ and run the Jasmine based test suite.
82
86
83
87
## How it works
84
88
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
+
85
127
There are copious comments in the ` pyscript.js ` file. My intention is for
86
128
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
88
130
on. To this end, it's laid out in a "literate" manner, so the code "tells the
89
131
story" of this implementation of PyScript by reading it from top to bottom.
90
132
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
100
134
101
- The ` Plugin ` class is based upon Antonio's suggestion
135
+ Plugins are inspired by Antonio's suggestion
102
136
[ found here] ( https://github.com/pyscript/pyscript/issues/763#issuecomment-1245086859 ) ,
103
137
and should be relatively self explanatory.
104
138
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
+ ```
111
221
112
- This probably needs more thought/discussion.
222
+ Then in your HTML file:
113
223
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) .
121
231
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
126
233
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.
128
236
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".
133
243
134
244
The afore mentioned ` MicroPythonRuntime ` , ` CPythonRuntime ` and ` PyodideRuntime `
135
245
all, to a greater or lesser extent, define a uniform shim around their
136
246
respective runtimes. The MicroPython one is most complete, but still needs work
137
247
as I make changes to how MicroPython itself exposes ` stdout ` , ` stderr ` and
138
248
consumes ` stdin ` .
139
249
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
-
210
250
## The future
211
251
212
252
Who knows..? But this is a good scaffold for testing different Python runtimes.
0 commit comments