|
| 1 | +# F.A.Q. |
| 2 | + |
| 3 | +This page contains most common questions and "*gotchas*" asked in [our Discord channel](https://discord.com/channels/972017612454232116/972017612454232119) or within our community. |
| 4 | + |
| 5 | +There are two major areas we'd like to help with, grouped here as [common errors](#common-errors) and as [common hints](#common-hints). |
| 6 | + |
| 7 | +## Common Errors |
| 8 | + |
| 9 | +This area contains most common issues our users might face due technical reasons or some misconception around the topic. |
| 10 | + |
| 11 | +### SharedArrayBuffer |
| 12 | + |
| 13 | +This is not by accident the very first, and most common, error our users might encounter while developing or deploying *PyScript* projects. |
| 14 | + |
| 15 | +!!! failure |
| 16 | + |
| 17 | + Unable to use SharedArrayBuffer due insecure environment. |
| 18 | + Please read requirements in MDN: ... |
| 19 | + |
| 20 | +The error contains [a link to MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements) but it's easy to get lost behind the amount of content provided by this topic. |
| 21 | + |
| 22 | +**When** |
| 23 | + |
| 24 | +This errors happens in one of these combined scenarios: |
| 25 | + |
| 26 | + * the server doesn't provide the correct headers to handle security concerns or there is no *Service Worker* able to override headers, as described [in the worker page](http://127.0.0.1:8000/user-guide/workers/) and ... |
| 27 | + * there is a `worker` attribute in the *py* or *mpy* script element and the [sync_main_only](https://pyscript.github.io/polyscript/#extra-config-features) flag is not present or not `True` |
| 28 | + * there is a `<script type="py-editor">` that uses a *worker* behind the scene |
| 29 | + * there is an explicit *PyWorker* or *MPWorker* bootstrap |
| 30 | + |
| 31 | +The only exception is when the `sync_main_only = True` is part of the config with the following caveats: |
| 32 | + |
| 33 | + * it is not possible to manipulate the DOM or do anything meaningful on the main thread directly because *Atomics* cannot guarantee sync-like locks within *worker* ↔ *main* operations |
| 34 | + * the only desired use case is to expose, from the worker, `pyscript.sync` utilities that will need to be awaited from the *main* once invoked |
| 35 | + * the worker can only *await* main related references, one after the other, so that *DX* is really degraded in case one still needs to interact with main |
| 36 | + |
| 37 | +If your project simply bootstraps on the *main* thread, none of this is relevant because no *worker* would need special features. |
| 38 | + |
| 39 | +**Why** |
| 40 | + |
| 41 | +The only way to make `document.getElementById('some-id').value` work out of a *worker* execution context is to use these two JS primitives: |
| 42 | + |
| 43 | + * **SharedArrayBuffer**, which allows multiple threads to read and / or write into a chunk of memory that is, like the name suggests, shared across threads |
| 44 | + * **Atomics**, which is needed to both `wait(sab, index)` and `notify(sab, index)` to unlock the awaiting thread |
| 45 | + |
| 46 | +While a *worker* is waiting for some operation on main to happen, this is not using the CPU, it just idles until that index of the shared buffer gets notified, effectively never blocking the *main* thread, still pausing its own execution until such buffer is notified for changes. |
| 47 | + |
| 48 | +As overwhelming or complicated as this might sounds, these two fundamental primitives make *main* ↔ *worker* interoperability an absolute wonder in term of *DX* so that we encourage to always prefer *workers* over *main* scripts, specially when it comes to *Pyodide* related projects with its heavier bootstrap or computation abilities, yet still delivering a *main-like* development experience. |
| 49 | + |
| 50 | +Unfortunately, due past security concerns and attacks to shared buffers, each server or page needs to allow extra security to prevent malicious software to also read or write into these buffers but be assured that if you own your code, your project, and you trust the modules or 3rd party code you need and use, **there are no security concerns around this topic within this project**, it's simply an unfortunate "*one rule catch all*" standard any server can either enable or disable as it pleases. |
| 51 | + |
| 52 | +### Borrowed Proxy |
| 53 | + |
| 54 | +This is another classic error that might happen with listeners, timers or any other circumstance where a *Python* callback might be lazily invoked in the *JS* side of affair: |
| 55 | + |
| 56 | +!!! failure |
| 57 | + |
| 58 | + Uncaught Error: This borrowed proxy was automatically destroyed at the end of a function call. Try using create_proxy or create_once_callable. |
| 59 | + For more information about the cause of this error, use `pyodide.setDebug(true)` |
| 60 | + |
| 61 | +**When** |
| 62 | + |
| 63 | +This error usually happens in *Pyodide* only related project, and only if a *Python* callback has been directly passed along as *JS* function parameter: |
| 64 | + |
| 65 | +```python title="An expired borrowed proxy example" |
| 66 | +import js |
| 67 | +# will throw the error in case |
| 68 | +js.setTimeout(lambda msg: print(msg), 1000, "FAIL") |
| 69 | +``` |
| 70 | + |
| 71 | +Please note that this error *does not happen* if the code is executed in a worker and the *JS* reference comes from the *main* thread: |
| 72 | + |
| 73 | +```python title="A worker has no borrowed issue" |
| 74 | +from pyscript import window |
| 75 | +window.setTimeout(lambda x: print(x), 1000, "OK") |
| 76 | +``` |
| 77 | + |
| 78 | +In this case, because proxies cannot survive a *worker* ↔ *main* communication, the *Python* reference gets inevitably translated into a *JS* function and its unique *id* propagated and awaited to be released with a returning value, so that technically that lambda can be freed without causing any issue. |
| 79 | + |
| 80 | +We provided an experimental way to always act in a similar way on both main and workers through the `experimental_create_proxy = "auto"` *config* flag. |
| 81 | + |
| 82 | +This flag tries to intercept all *Python* proxies passed to a *JS* callback and it orchestrates an automatic memory free operation through the *JS* garbage collector. |
| 83 | + |
| 84 | +!!! Note |
| 85 | + |
| 86 | + The [FinalizationRegistry](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry) is the primitive used to do so. It is not observable and nobody can predict when it will run to free, hence destroy, retained *Python* proxies. This means that *RAM* consumption might be slightly higher, but it's the *JS* engine responsibility to guarantee that when such *RAM* consumption is too high, that finalization registry would call and free all retained proxies, leaving room for more *RAM*. |
| 87 | + |
| 88 | +**Why** |
| 89 | + |
| 90 | +Most *WASM* based runtimes have their own garbage collector or memory management but when their references are passed along another programming language they cannot guarantee these references will ever be freed, or better, they lose control over that memory allocation because they cannot know when such allocation won't be needed anymore. |
| 91 | + |
| 92 | +The theoretical solution to this is to allow users to explicitly create proxies of these references and then still explicitly invoke `proxy.destroy()` to communicate to the original *PL* that such reference and allocated can be freed, if not used internally for other reasons, effectively invalidating that *JS* reference, resulting into a "*dead reference*" that its garbage collector might get rid of when it's convenient. |
| 93 | + |
| 94 | +At the practical level though, there are dozen use cases where users just don't, or can't, disambiguate the need for such proxy creation or easily forget to call `destroy()` at the right time. |
| 95 | + |
| 96 | +To help most common use cases, *Pyodide* provide various [ffi.wrappers](https://pyodide.org/en/stable/usage/api/python-api/ffi.html#module-pyodide.ffi.wrappers) but in theory none of these is strictly needed if we orchestrate a *FinalizationRegistry* to automatically `destroy` those proxies when not needed anymore, moving the responsibility from the user to the running *JS* engine, which is why we have provided the `experimental_create_proxy = "auto"` *config* flag. |
| 97 | + |
| 98 | +### Python Modules |
| 99 | + |
| 100 | +There is a huge difference in what can be imported in *pyodide* VS what can be imported in *micropython* and the reason is: |
| 101 | + |
| 102 | + * *pyodide* can import modules at runtime as long as these [have been ported](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) to it |
| 103 | + * *micropython* can only import at runtime *Python* only modules, or better, modules that use the same syntax and primitives allowed in *micropython* itself |
| 104 | + |
| 105 | +Behind the scene *pyodide* uses [micropip](https://github.com/pyodide/micropip) while *micropython* uses [mip](https://docs.micropython.org/en/latest/reference/packages.html#installing-packages-with-mip). |
| 106 | + |
| 107 | +Due different architecture though, *micropython* cannot expose modules that require native compilation / translation for the resulting *WASM* artifact, but we're working on a "*super charged*" version of *micropython* that would bring at least the most common requested modules too (i.e. *numpy*). |
| 108 | + |
| 109 | +!!! warning |
| 110 | + |
| 111 | + Accordingly to the current state, it could be hard to seamlessly port 1:1 a Pyodide project to MicroPython: expect possible issues while trying but please consider the mentioned caveats around or be sure the issue is something we could actually fix on our side or file an upstream issue otherwise, thank you! |
| 112 | + |
| 113 | +### JS Modules |
| 114 | + |
| 115 | +In more than one occasion, since we introduced the `pyscript.js_modules` feature, our users have encountered errors like: |
| 116 | + |
| 117 | +!!! failure |
| 118 | + |
| 119 | + Uncaught SyntaxError: The requested module './library.js' does not provide an export named 'default' |
| 120 | + |
| 121 | +!!! failure |
| 122 | + |
| 123 | + Uncaught SyntaxError: The requested module './library.js' does not provide an export named 'util' |
| 124 | + |
| 125 | +**When** |
| 126 | + |
| 127 | +99% of the time the issue with JS modules is that what we are importing are not, effectively, JS modules. |
| 128 | + |
| 129 | +When the file uses `module.exports` or `globalThis.util` or anything that is not standard *ECMAScript* syntax for modules, such as `export default value` or `export const util = {}`, we cannot retrieve that file content in a standard way. |
| 130 | + |
| 131 | +To **solve** this issue various *CDNs* provide a way to automatically deliver *ESM* (aka: *ECMAScript Modules*) out of the box and one of the most reliable and famous one is [esm.run](https://esm.run/). |
| 132 | + |
| 133 | +```html title="An example of esm.run" |
| 134 | +<mpy-config> |
| 135 | +[js_modules.main] |
| 136 | +"https://esm.run/d3" = "d3" |
| 137 | +</mpy-config> |
| 138 | +<script type="mpy"> |
| 139 | + from pyscript.js_modules import d3 |
| 140 | +</script> |
| 141 | +``` |
| 142 | + |
| 143 | +Alternatively, please be sure any `.js` file you are importing as module actually uses `export ...` within its content and, if that's not the case, ask for an `.mjs` counter-equivalent of that library or framework or trust `esm.run` produced artifacts. |
| 144 | + |
| 145 | +**Why** |
| 146 | + |
| 147 | +Even if standard *JS* modules have been around since 2015, a lot of old to even new libraries still produce files that are incompatible with modern *JS* expectations. |
| 148 | + |
| 149 | +There is no logical or simple explanation to this situation for modules that target browsers, but there are various server side related projects that still rely on the legacy, *NodeJS* only, module system (aka: *CommonJS*). |
| 150 | + |
| 151 | +Until that legacy module system exists, be aware some module might require special care. |
| 152 | + |
| 153 | +### Reading Errors |
| 154 | + |
| 155 | +Each interpreter might provide different error messages but the easy way to find what's going on is, most of the time, described in the last line with both *Pyodide* and *MicroPython*: |
| 156 | + |
| 157 | +```text title="A Pyodide Error" |
| 158 | +Traceback (most recent call last): |
| 159 | + File "/lib/python311.zip/_pyodide/_base.py", line 501, in eval_code |
| 160 | + .run(globals, locals) |
| 161 | + ^^^^^^^^^^^^^^^^^^^^ |
| 162 | + File "/lib/python311.zip/_pyodide/_base.py", line 339, in run |
| 163 | + coroutine = eval(self.code, globals, locals) |
| 164 | + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 165 | + File "<exec>", line 1, in <module> |
| 166 | +NameError: name 'failure' is not defined |
| 167 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 168 | +``` |
| 169 | + |
| 170 | +```text title="A MicroPython Error" |
| 171 | +Traceback (most recent call last): |
| 172 | + File "<stdin>", line 1, in <module> |
| 173 | +NameError: name 'failure' isn't defined |
| 174 | +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| 175 | +``` |
| 176 | + |
| 177 | +The same applies when the error is shown in devtools/console where unfortunately the stack right after the error message might be a bit distracting but it's still well separated from the error message itself. |
| 178 | + |
| 179 | +## Common Hints |
| 180 | + |
| 181 | +This area contains most common questions, hacks, or hints we provide to the community. |
| 182 | + |
| 183 | +### PyScript latest |
| 184 | + |
| 185 | +For various reasons previously discussed at length, we decided to remove our `latest` channel from our own CDN. |
| 186 | + |
| 187 | +We were not super proud of users trusting that channel coming back with suddenly broken projects so we now [release only official versions](https://github.com/pyscript/pyscript/releases) everyone can pin-point in time. |
| 188 | + |
| 189 | +We are also developing behind the scene through *npm* to be able to test in the wild breaking changes and what's not and it's no secret that *CDNs* could also deliver our "*canary*" or "*development*" channel so that we're better off telling you exactly which links one should use to have the latest, whenever latest lands on the *CDN* which is usually within 24 hours from the last *npm* version change. |
| 190 | + |
| 191 | +We still **do not guarantee any stability** around this channel so be aware this is never a good idea to use in production, documentation might lack behind landed changes, APIs might break or change too, and so on. |
| 192 | + |
| 193 | +If you are still reading though, this is the template to have *latest*: |
| 194 | + |
| 195 | +```html title="PyScript latest" |
| 196 | +<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css"> |
| 197 | +<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.js"></script> |
| 198 | +``` |
| 199 | + |
| 200 | +!!! warning |
| 201 | + |
| 202 | + Do not use shorter urls or other CDNs because the project needs both the correct headers to eventually run in *workers* and it needs to find its own assets at runtime so that other CDN links might result into a **broken experience** out of the box. |
| 203 | + |
| 204 | +### Worker Bootstrap |
| 205 | + |
| 206 | +In some occasion users asked to bootstrap a *pyodide* or *micropython* worker directly via *JS*. |
| 207 | + |
| 208 | +```html title="PyScript Worker in JS" |
| 209 | +<script type="module"> |
| 210 | + // use sourceMap for @pyscript/core or change to a CDN |
| 211 | + import { |
| 212 | + PyWorker, // Pyodide Worker |
| 213 | + MPWorker // MicroPython Worker |
| 214 | + } from '@pyscript/core'; |
| 215 | +
|
| 216 | + const worker = await MPWorker( |
| 217 | + // Python code to execute |
| 218 | + './micro.py', |
| 219 | + // optional details or config with flags |
| 220 | + { config: { sync_main_only: true } } |
| 221 | + // ^ just as example ^ |
| 222 | + ); |
| 223 | +
|
| 224 | + // "heavy computation" |
| 225 | + await worker.sync.doStuff(); |
| 226 | +
|
| 227 | + // kill the worker when/if needed |
| 228 | + worker.terminate(); |
| 229 | +</script> |
| 230 | +``` |
| 231 | + |
| 232 | +```python title="micro.py" |
| 233 | +from pyscript import sync |
| 234 | + |
| 235 | +def do_stuff(): |
| 236 | + print("heavy computation") |
| 237 | + |
| 238 | +sync.doStuff = do_stuff |
| 239 | +``` |
| 240 | + |
| 241 | +### About Class.new |
| 242 | + |
| 243 | +In more than one occasion users asked why there's the need to write `Class.new()`, when the class comes from the *JS* world, as opposite of just typing `Class()` like it is for *Python*. |
| 244 | + |
| 245 | +The reason is very technical and related to *JS* history and poor introspection ability in this regard: |
| 246 | + |
| 247 | + * `typeof function () {}` and `typeof class {}` produce the same outcome: **function**, making it very hard to disambiguate the intention as both are valid and, strawberry on top, legacy *JS* used `function` to create instances, not `class`, so that legacy code might still use that good'ol convention |
| 248 | + * the *JS* proxy has `apply` and `construct` traps but because of the previous point, it's not possible to be sure that `apply` meant to `construct` the instance |
| 249 | + * diffrently from *Python*, just invoking `Class()` throws an error in *JS* if that is actually defined as `class {}` so that `new` is mandatory in the that case |
| 250 | + * the `new Class()` is invalid syntax in *Python*, there is a need to disambiguate the intent |
| 251 | + * the capitalized naming convention is lost once the code gets minified on the *JS* side, hence it's unreliable as convention |
| 252 | + * the `Class.new()` explicitly describe the intent ... it's true that it's ugly, when mixed up with *Python* classes too, but at least it clearly indicates that such `Class` is a *JS* one, not a *Python* thing |
| 253 | + |
| 254 | +As summary, we all agree that in an ideal world just having `Class()` all over would be cool, but unless the *JS* code has been created via quite outdated artifacts we need to use the `.new()` convention which is adopted by both *pyodide* and *micropython*. |
| 255 | + |
| 256 | +To close this paragraph, here an example of how it was possible before to avoid `new` *VS* now: |
| 257 | + |
| 258 | +```js title="Legacy VS Modern JS" |
| 259 | +// legacy pseudo pattern: it allows just Legacy() |
| 260 | +// with or without `new Legacy` requirement |
| 261 | +function Legacy(name) { |
| 262 | + if (!(this instanceof Legacy)) |
| 263 | + return new Legacy(name); |
| 264 | + // bootstrap the instance |
| 265 | + this.name = name; |
| 266 | +} |
| 267 | + |
| 268 | +// legacy way to define classes |
| 269 | +Legacy.prototype = {...}; |
| 270 | + |
| 271 | +// modern JS classes: private fields, own fields, |
| 272 | +// super and other goodness available to devs |
| 273 | +class Modern {} |
| 274 | + |
| 275 | +// throws error: it requires `new Modern` |
| 276 | +Modern() |
| 277 | +``` |
0 commit comments