@@ -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
6973To change the configuration of PyScript take a look at the JSON object
7074defined 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+
85127There are copious comments in the ` pyscript.js ` file. My intention is for
86128simplicity, 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
88130on. To this end, it's laid out in a "literate" manner, so the code "tells the
89131story" 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 ) ,
103137and 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
134244The afore mentioned ` MicroPythonRuntime ` , ` CPythonRuntime ` and ` PyodideRuntime `
135245all, to a greater or lesser extent, define a uniform shim around their
136246respective runtimes. The MicroPython one is most complete, but still needs work
137247as I make changes to how MicroPython itself exposes ` stdout ` , ` stderr ` and
138248consumes ` 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
212252Who knows..? But this is a good scaffold for testing different Python runtimes.
0 commit comments