Skip to content

Commit 84a3524

Browse files
committed
Fix #81 - Added F.A.Q. & Errors
1 parent 07c15a2 commit 84a3524

File tree

2 files changed

+278
-0
lines changed

2 files changed

+278
-0
lines changed

docs/user-guide/faq.md

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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+
```

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ nav:
8080
- Plugins: user-guide/plugins.md
8181
- Use Offline: user-guide/offline.md
8282
- Example apps: user-guide/examples.md
83+
- F.A.Q.: user-guide/faq.md

0 commit comments

Comments
 (0)