mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 02:37:41 -05:00
Compare commits
727 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b41cfb7b60 | ||
|
|
1c675307e1 | ||
|
|
ac56f82c6d | ||
|
|
2ac5ca79d7 | ||
|
|
cb9ee6f7e2 | ||
|
|
9abaef33bd | ||
|
|
320a537db2 | ||
|
|
9b775ce015 | ||
|
|
66f72eda1e | ||
|
|
39ca29749c | ||
|
|
85da548447 | ||
|
|
9985787e4b | ||
|
|
18ec6ce775 | ||
|
|
ed6d0136b8 | ||
|
|
e7216d26e7 | ||
|
|
d1a0d8ea98 | ||
|
|
04222b0d03 | ||
|
|
8ec3381789 | ||
|
|
9bd4737708 | ||
|
|
c49cb9231b | ||
|
|
d1d1c5740f | ||
|
|
1a05ea5fd2 | ||
|
|
5b4e8527da | ||
|
|
83c2afeaf1 | ||
|
|
643b76479f | ||
|
|
cf92996071 | ||
|
|
c653296821 | ||
|
|
44cd6273ba | ||
|
|
d7d2dfb383 | ||
|
|
2d5cf096e0 | ||
|
|
6ee8217593 | ||
|
|
6d45728787 | ||
|
|
65954a627e | ||
|
|
2f1b764251 | ||
|
|
1fb6cddd70 | ||
|
|
239add4e20 | ||
|
|
4e4ac56729 | ||
|
|
1447cb3094 | ||
|
|
2f3659b676 | ||
|
|
910c666319 | ||
|
|
eee2f64c1d | ||
|
|
d080246a0f | ||
|
|
98c0f5e50d | ||
|
|
a1268f1aa2 | ||
|
|
69b8884045 | ||
|
|
df1d699fe6 | ||
|
|
84f197b657 | ||
|
|
5bed5ede52 | ||
|
|
f6d5cf06c8 | ||
|
|
30c6c830ae | ||
|
|
d7084f7f55 | ||
|
|
a87d2b3fea | ||
|
|
81a26363a3 | ||
|
|
53e945201d | ||
|
|
181d276c8b | ||
|
|
bcaab0eb93 | ||
|
|
3ff0f84391 | ||
|
|
2b411fc635 | ||
|
|
2128572ce5 | ||
|
|
63f2453091 | ||
|
|
f6470dcad5 | ||
|
|
a9717afeb7 | ||
|
|
cea52b4334 | ||
|
|
7ad7f0abfb | ||
|
|
1efd73af8f | ||
|
|
1e7fb9af44 | ||
|
|
154e00d320 | ||
|
|
0f788fa284 | ||
|
|
355866a1f1 | ||
|
|
6eca06ac0b | ||
|
|
a4aef0b530 | ||
|
|
136e95498f | ||
|
|
6a3e2834b6 | ||
|
|
c0d45d368b | ||
|
|
f18ec3d20a | ||
|
|
b0377cc7ab | ||
|
|
96e671b55f | ||
|
|
40e99abbdf | ||
|
|
8b6b055681 | ||
|
|
8e5605fa42 | ||
|
|
06e1fdecc2 | ||
|
|
a82e8334d6 | ||
|
|
539bc2ae0e | ||
|
|
0711acd30e | ||
|
|
1476131ab4 | ||
|
|
89902a440c | ||
|
|
156c23d550 | ||
|
|
30396ba79a | ||
|
|
a4343c62ca | ||
|
|
4b89c84692 | ||
|
|
df68449b82 | ||
|
|
48e3383f66 | ||
|
|
e750fa7393 | ||
|
|
5a15199a3a | ||
|
|
1801472fc4 | ||
|
|
ab15ac37ff | ||
|
|
0955a6be49 | ||
|
|
d58237ea15 | ||
|
|
2d50ca86a6 | ||
|
|
f1a46be738 | ||
|
|
3e2a67d434 | ||
|
|
aef028be6e | ||
|
|
c8ec29a3d8 | ||
|
|
e81830a2ea | ||
|
|
54df7171a2 | ||
|
|
b31af823d1 | ||
|
|
72f266532b | ||
|
|
d9bf5cae12 | ||
|
|
cd95a42e5e | ||
|
|
e67eb06d8b | ||
|
|
28d37cdead | ||
|
|
13604e0a47 | ||
|
|
aeb6f1a755 | ||
|
|
92e6f711b7 | ||
|
|
a24113f42b | ||
|
|
7a6f8ab3ad | ||
|
|
6dd242f3ce | ||
|
|
88fa82c61a | ||
|
|
2299ba5f61 | ||
|
|
117df6ca38 | ||
|
|
4256a81653 | ||
|
|
d5b6935c0b | ||
|
|
b4503ef729 | ||
|
|
a00a6750b4 | ||
|
|
a08f891b20 | ||
|
|
bc1cac9c41 | ||
|
|
50f7ab0f34 | ||
|
|
fdc35ce3ed | ||
|
|
5c4e400d32 | ||
|
|
7a23e355b9 | ||
|
|
dffac642a1 | ||
|
|
97699eaded | ||
|
|
c6aaacdbf1 | ||
|
|
abfc68765f | ||
|
|
3ac2ac0982 | ||
|
|
b9a1227e47 | ||
|
|
801c63947a | ||
|
|
ffee4add4a | ||
|
|
f0be7ef418 | ||
|
|
e4eedd80bc | ||
|
|
c9e7fe16e4 | ||
|
|
5079dd19cb | ||
|
|
b4c686f411 | ||
|
|
287d0fa1af | ||
|
|
b78455c4c1 | ||
|
|
312b6b0706 | ||
|
|
924e530096 | ||
|
|
ef8918f3a7 | ||
|
|
91ae242e49 | ||
|
|
fd307e52ae | ||
|
|
a68967c773 | ||
|
|
52da45bb9c | ||
|
|
ad0dde3f17 | ||
|
|
8f3c36deea | ||
|
|
23e1ab81b3 | ||
|
|
77b40aa348 | ||
|
|
f6decfd93d | ||
|
|
e8d5138cfa | ||
|
|
a088fbd6fb | ||
|
|
19214901f9 | ||
|
|
c330a623b2 | ||
|
|
f77241e977 | ||
|
|
ed6de66c08 | ||
|
|
5191c45113 | ||
|
|
840bc803b7 | ||
|
|
00fdc73015 | ||
|
|
9660976d1d | ||
|
|
7f666dc6a0 | ||
|
|
e2a2292a6f | ||
|
|
4d89cbde01 | ||
|
|
d8e1cb8b0f | ||
|
|
3aef5a99dc | ||
|
|
7994207c78 | ||
|
|
f376097a15 | ||
|
|
2a2ff4066d | ||
|
|
32c3fb72cc | ||
|
|
e44e18114d | ||
|
|
7d2df4895e | ||
|
|
59db56feec | ||
|
|
fd60b4789a | ||
|
|
0696e4682d | ||
|
|
d56eeb59ed | ||
|
|
1d015c7534 | ||
|
|
264675d0c3 | ||
|
|
37d4cb7c48 | ||
|
|
cabb1c72b6 | ||
|
|
489a2bb20e | ||
|
|
d5f42e57ce | ||
|
|
94b0bf4131 | ||
|
|
12428c0617 | ||
|
|
ef44df5dda | ||
|
|
da3b43abdd | ||
|
|
4cc9647dc6 | ||
|
|
74cd7c840a | ||
|
|
0f2deeb71a | ||
|
|
93539c9b5a | ||
|
|
e48e6276e1 | ||
|
|
75a57a49f5 | ||
|
|
8a1db288fc | ||
|
|
84dcde188b | ||
|
|
27c91e9703 | ||
|
|
b5a0cd4057 | ||
|
|
77d8fe3562 | ||
|
|
a484aff457 | ||
|
|
c96f5912df | ||
|
|
8a01a56e51 | ||
|
|
2774e49ab9 | ||
|
|
26e7a54f1f | ||
|
|
f0e69cbc36 | ||
|
|
413428f535 | ||
|
|
0c54036466 | ||
|
|
2555833831 | ||
|
|
7e0aceced1 | ||
|
|
77234f6df3 | ||
|
|
45af96aad4 | ||
|
|
184d29055e | ||
|
|
9e73181816 | ||
|
|
0b0e03456c | ||
|
|
c6b5ce7f55 | ||
|
|
a14e701be4 | ||
|
|
7813c3f03f | ||
|
|
3a3cb7b11d | ||
|
|
d7b0731385 | ||
|
|
df8973736f | ||
|
|
9121071ba3 | ||
|
|
bf6470c046 | ||
|
|
3b7099cd3d | ||
|
|
f6dfc5361e | ||
|
|
0a7e1ce0d7 | ||
|
|
d6b1c393f6 | ||
|
|
bccd5e3750 | ||
|
|
6df5905b2b | ||
|
|
6284c02032 | ||
|
|
db27d52352 | ||
|
|
8ba28989fb | ||
|
|
da544929ac | ||
|
|
bb364b0524 | ||
|
|
818614b798 | ||
|
|
50b1a1d7c5 | ||
|
|
7d3b792a79 | ||
|
|
af72e232c3 | ||
|
|
0cdbfbeb30 | ||
|
|
339e40063a | ||
|
|
4467898473 | ||
|
|
17d16b987f | ||
|
|
8e86daac71 | ||
|
|
856720da49 | ||
|
|
8f2c150d1e | ||
|
|
7d8b4c980a | ||
|
|
932756c7a0 | ||
|
|
538aac9a28 | ||
|
|
856bf8f5fb | ||
|
|
e1758ae2e2 | ||
|
|
61b3154461 | ||
|
|
fb9b30d144 | ||
|
|
b0df96b13f | ||
|
|
a469062a32 | ||
|
|
89d5d5c7db | ||
|
|
b8c2d6b05d | ||
|
|
b247864414 | ||
|
|
d3bcd87cfa | ||
|
|
82e5b64bad | ||
|
|
73e0271c23 | ||
|
|
a2dabee0e9 | ||
|
|
6a27c6d9f2 | ||
|
|
213ced0c7f | ||
|
|
5086c23d47 | ||
|
|
ee345a5206 | ||
|
|
f74cddc3b1 | ||
|
|
5b986b8b26 | ||
|
|
14887b9814 | ||
|
|
ecc40315b3 | ||
|
|
e7aed7fcf0 | ||
|
|
cd1aa948f9 | ||
|
|
82613d016a | ||
|
|
3a66be585f | ||
|
|
0a4e36ae09 | ||
|
|
92643539cf | ||
|
|
a1281d1331 | ||
|
|
074ca0ef8f | ||
|
|
464a9633dc | ||
|
|
fc2d91c5bb | ||
|
|
d68169bffb | ||
|
|
7efdb04e1e | ||
|
|
0155e122fd | ||
|
|
eb03f16a77 | ||
|
|
5ac39641ab | ||
|
|
8d1e48e400 | ||
|
|
0021ccb49f | ||
|
|
8590c7e5b8 | ||
|
|
8c5475f78f | ||
|
|
dfa116eb70 | ||
|
|
3a9fd3c074 | ||
|
|
5a92ef3c11 | ||
|
|
d3902f5c93 | ||
|
|
c886f887ae | ||
|
|
fc5089ac59 | ||
|
|
e3602f464b | ||
|
|
f3db6a339c | ||
|
|
c05195c045 | ||
|
|
af981fc719 | ||
|
|
088a264910 | ||
|
|
d7e80ad51b | ||
|
|
b53ddd401f | ||
|
|
e9122bca9d | ||
|
|
b61e8435d1 | ||
|
|
146afb6532 | ||
|
|
854e9d1378 | ||
|
|
689878ce32 | ||
|
|
d7ab177cc5 | ||
|
|
f4c6093c47 | ||
|
|
9fedfe3699 | ||
|
|
26f07246e1 | ||
|
|
3ae4b3c4de | ||
|
|
c8f9f16791 | ||
|
|
88f0738500 | ||
|
|
03c79d5f2f | ||
|
|
e7c3b7bcfe | ||
|
|
c8becca044 | ||
|
|
543a27271f | ||
|
|
a62aba83a0 | ||
|
|
53c6cf5f45 | ||
|
|
89842e20da | ||
|
|
ef793aecf3 | ||
|
|
51d51409d3 | ||
|
|
371b5eac45 | ||
|
|
5319bd13d5 | ||
|
|
e10d055453 | ||
|
|
716254e655 | ||
|
|
4c00b1683f | ||
|
|
37c9db09c6 | ||
|
|
653e2c9be4 | ||
|
|
a2a9613da1 | ||
|
|
e8d92d0d34 | ||
|
|
755b98a8c0 | ||
|
|
13e9252260 | ||
|
|
6a9c27325a | ||
|
|
a1cb78eb85 | ||
|
|
716b57ebd3 | ||
|
|
8e231313b8 | ||
|
|
84e4e361c5 | ||
|
|
41a8d804e3 | ||
|
|
03e798a079 | ||
|
|
34a0205757 | ||
|
|
ba145f04ea | ||
|
|
22fd023635 | ||
|
|
08f34f748b | ||
|
|
7ffe6a598e | ||
|
|
71d24a445e | ||
|
|
6bcbbfb085 | ||
|
|
04fe1348d8 | ||
|
|
3033c779b0 | ||
|
|
4483f0db0f | ||
|
|
727267ae22 | ||
|
|
b5d15c2f7e | ||
|
|
589c614e57 | ||
|
|
4588e90226 | ||
|
|
8665a14dec | ||
|
|
43d598d951 | ||
|
|
68018cf078 | ||
|
|
ef4ab0d7a8 | ||
|
|
e66a2702df | ||
|
|
c57d4a7054 | ||
|
|
a36f08f0f1 | ||
|
|
760a8c75a5 | ||
|
|
740fd921e1 | ||
|
|
065c697070 | ||
|
|
e2c2459290 | ||
|
|
11c79a5344 | ||
|
|
429fe4c356 | ||
|
|
a18b4edfc0 | ||
|
|
b14a2bba5f | ||
|
|
1f825edc28 | ||
|
|
6ed834807a | ||
|
|
9a908e5fd0 | ||
|
|
4c30359b71 | ||
|
|
34dfe2d80b | ||
|
|
25bcff10b7 | ||
|
|
81268d0545 | ||
|
|
8f0a7706d7 | ||
|
|
46150f9b80 | ||
|
|
247745b7e7 | ||
|
|
94cc09b610 | ||
|
|
a210b2d5f5 | ||
|
|
12bf6db331 | ||
|
|
697ac9de9a | ||
|
|
4124bb5edc | ||
|
|
d55340a817 | ||
|
|
0de8cd9ab7 | ||
|
|
4e8281c749 | ||
|
|
357fbc644d | ||
|
|
7947a8a2dc | ||
|
|
35de3aa154 | ||
|
|
1ea687beb8 | ||
|
|
bb5c59307a | ||
|
|
5a3c414c8f | ||
|
|
cc4b460183 | ||
|
|
470c3489dd | ||
|
|
e1b4415193 | ||
|
|
77d98a565e | ||
|
|
412da2de08 | ||
|
|
dbdcd0b3d0 | ||
|
|
5c67384fbf | ||
|
|
35b0f9d377 | ||
|
|
95783bc284 | ||
|
|
4b840f7cbd | ||
|
|
f73d6cd9f2 | ||
|
|
15bb8f03ea | ||
|
|
059dbc88c9 | ||
|
|
c0f36aa047 | ||
|
|
d4120d2af3 | ||
|
|
dd1c008447 | ||
|
|
3721d2cd72 | ||
|
|
e0dda0e547 | ||
|
|
3c7568c72c | ||
|
|
6be1758548 | ||
|
|
25809660ef | ||
|
|
6b9eff45bb | ||
|
|
08e83feaf5 | ||
|
|
4f05b5afc6 | ||
|
|
9a5bf9918e | ||
|
|
1c7cf0ba7d | ||
|
|
ce2d1a4513 | ||
|
|
7d50d7eea0 | ||
|
|
e9411dc796 | ||
|
|
5cf2de16d1 | ||
|
|
e53bcf15a9 | ||
|
|
bec70b60b8 | ||
|
|
8b7fb89c68 | ||
|
|
b2bbdda73d | ||
|
|
ee2f46cfb9 | ||
|
|
4337e6833a | ||
|
|
a73c73b814 | ||
|
|
e8318a98f0 | ||
|
|
94f2ac6204 | ||
|
|
cc6cb4ded0 | ||
|
|
c61d4191c3 | ||
|
|
e284da7c09 | ||
|
|
9992096654 | ||
|
|
c696d92f40 | ||
|
|
af60299324 | ||
|
|
5aa9135a34 | ||
|
|
72e23ac86f | ||
|
|
33d49ad87d | ||
|
|
aa2335ca2e | ||
|
|
b31428006c | ||
|
|
dc1d583791 | ||
|
|
4299a74e40 | ||
|
|
3e408b7baa | ||
|
|
446c131ccb | ||
|
|
b062efcf17 | ||
|
|
30e31a86ef | ||
|
|
182272e8c7 | ||
|
|
06df21e8e3 | ||
|
|
cafebd68f2 | ||
|
|
061d4b3f72 | ||
|
|
6586e79d5e | ||
|
|
cda6c6bc7e | ||
|
|
a628026838 | ||
|
|
6700856b9f | ||
|
|
411aa0bbed | ||
|
|
0d79d31b96 | ||
|
|
cb05a9b067 | ||
|
|
536f359fb9 | ||
|
|
56e888ed33 | ||
|
|
687b93d148 | ||
|
|
0e1c396d7c | ||
|
|
7e24289703 | ||
|
|
0b23310a06 | ||
|
|
41ebaaf366 | ||
|
|
b79ceea7a8 | ||
|
|
b990bcb67a | ||
|
|
3f0f2d9910 | ||
|
|
4333f5f979 | ||
|
|
07e75293b8 | ||
|
|
a9ca7106cb | ||
|
|
da2728e6df | ||
|
|
adfa9a9b05 | ||
|
|
be9b9f66d3 | ||
|
|
9521bc7175 | ||
|
|
b445f8a834 | ||
|
|
3c3dffd5ed | ||
|
|
4c8443fd00 | ||
|
|
06a5a54103 | ||
|
|
0d3c3eef4e | ||
|
|
f0a6fb913f | ||
|
|
5f0c508fed | ||
|
|
16d9657982 | ||
|
|
40d098310e | ||
|
|
1345449d57 | ||
|
|
515858f313 | ||
|
|
2f452e9dc7 | ||
|
|
66119157a7 | ||
|
|
5b671dd1d0 | ||
|
|
1017362eec | ||
|
|
f67b8e0285 | ||
|
|
4c635fe84c | ||
|
|
68e463493e | ||
|
|
f1979d60b7 | ||
|
|
9150ebafec | ||
|
|
9543019336 | ||
|
|
1c53d91c6b | ||
|
|
4850f39b5a | ||
|
|
ab085c2d92 | ||
|
|
bf4d835948 | ||
|
|
214e39537b | ||
|
|
2d33afc195 | ||
|
|
87ea24ebd4 | ||
|
|
5380f8b9b3 | ||
|
|
00121ff8ba | ||
|
|
80e5d20e37 | ||
|
|
58f7c2137d | ||
|
|
f9194cc833 | ||
|
|
d9b8b48972 | ||
|
|
aa85f5f596 | ||
|
|
5341a0be4a | ||
|
|
0cfe20ca65 | ||
|
|
c352b502c4 | ||
|
|
58b4df6b3d | ||
|
|
29ba9436c8 | ||
|
|
2a044e88ad | ||
|
|
63092f9d72 | ||
|
|
0209324d57 | ||
|
|
1587273868 | ||
|
|
beb3aa1574 | ||
|
|
fe708c9fb4 | ||
|
|
e45d8bf973 | ||
|
|
b184c92f01 | ||
|
|
6c8afb05a7 | ||
|
|
d3dd4573cf | ||
|
|
d5cf68391a | ||
|
|
4e54e93450 | ||
|
|
e4f6387f18 | ||
|
|
54cb35b60a | ||
|
|
18ede2b729 | ||
|
|
f138b5a4f4 | ||
|
|
11a517bba4 | ||
|
|
66b57bf812 | ||
|
|
a9357bd97e | ||
|
|
d7c6d42c3d | ||
|
|
4dd1dc28b1 | ||
|
|
1e05ff7c95 | ||
|
|
e8e2e65584 | ||
|
|
3727e60152 | ||
|
|
0254012db6 | ||
|
|
8b97e4757f | ||
|
|
c70e121078 | ||
|
|
a4f97e6e46 | ||
|
|
800145a83c | ||
|
|
c75f885cb4 | ||
|
|
4011a51013 | ||
|
|
f4165dabcf | ||
|
|
de6c26eb05 | ||
|
|
5f319452d5 | ||
|
|
60d505d2d1 | ||
|
|
f64cc4dcae | ||
|
|
60e6f4293a | ||
|
|
00ab9a8d02 | ||
|
|
7d5f6c9ead | ||
|
|
a295edf19d | ||
|
|
b674515d06 | ||
|
|
d033ab04da | ||
|
|
304d76d088 | ||
|
|
978afdad97 | ||
|
|
d4e41e679d | ||
|
|
146264ff12 | ||
|
|
dcb107ae65 | ||
|
|
c236269d13 | ||
|
|
8f658e6d85 | ||
|
|
d203b60f44 | ||
|
|
a1a16aba74 | ||
|
|
6ded003447 | ||
|
|
4841e29fc6 | ||
|
|
0b014eea56 | ||
|
|
1c0be16f30 | ||
|
|
27ba8bea2f | ||
|
|
c566977749 | ||
|
|
5c1b785b4b | ||
|
|
8657dfb5da | ||
|
|
dfa837754e | ||
|
|
0a7df78770 | ||
|
|
066ecbe022 | ||
|
|
6c80db810f | ||
|
|
6023c413ab | ||
|
|
7910d040b6 | ||
|
|
5bd99f5224 | ||
|
|
f3157b377f | ||
|
|
e31e03afde | ||
|
|
eddde7c94c | ||
|
|
7be72ee4c1 | ||
|
|
6731467514 | ||
|
|
8dd699d235 | ||
|
|
6cb81b5c3d | ||
|
|
17187ba3ec | ||
|
|
531ee928b0 | ||
|
|
db806a5df9 | ||
|
|
9de154595a | ||
|
|
9e4cb79679 | ||
|
|
b7834073b8 | ||
|
|
b0e56577b5 | ||
|
|
ccb0e6b269 | ||
|
|
edfd4baa1f | ||
|
|
0f50f4a9fd | ||
|
|
47494e62a7 | ||
|
|
7aa25712d9 | ||
|
|
1db155570d | ||
|
|
1054e8e644 | ||
|
|
aa429f34d8 | ||
|
|
e351889811 | ||
|
|
24a70a8273 | ||
|
|
3f26657116 | ||
|
|
d41669af8b | ||
|
|
56466c2a00 | ||
|
|
fa7a97ca30 | ||
|
|
8aba271a42 | ||
|
|
8275aa2810 | ||
|
|
410ddf314c | ||
|
|
fa217bee20 | ||
|
|
817d0edc69 | ||
|
|
513dfe0b42 | ||
|
|
bd7a20309b | ||
|
|
a726be3c7c | ||
|
|
10f2054e9a | ||
|
|
2fa47f310d | ||
|
|
5b927a70c2 | ||
|
|
2a59ff8e68 | ||
|
|
e4d1befcdb | ||
|
|
844e04ff96 | ||
|
|
cc05a98b0e | ||
|
|
006d161a32 | ||
|
|
a4839db79a | ||
|
|
87e3b5b1dc | ||
|
|
faa900d502 | ||
|
|
a5275db3ec | ||
|
|
77e017a574 | ||
|
|
8ed8ddbf76 | ||
|
|
eb31978488 | ||
|
|
677d708588 | ||
|
|
ade0dca8f9 | ||
|
|
8e1cd0b268 | ||
|
|
9102768366 | ||
|
|
0c722b9164 | ||
|
|
b6f514451a | ||
|
|
c49fdfc56c | ||
|
|
676e04b28e | ||
|
|
6aa864a351 | ||
|
|
72acb4826c | ||
|
|
734be5f355 | ||
|
|
cede06ae19 | ||
|
|
19491d8010 | ||
|
|
c580aac991 | ||
|
|
032d1aaad7 | ||
|
|
afa216dc5e | ||
|
|
69339fe3de | ||
|
|
571bb2b294 | ||
|
|
91a09a09f7 | ||
|
|
9b3433f6ae | ||
|
|
ee9b0960f7 | ||
|
|
506ac2574f | ||
|
|
dc84d7c1b5 | ||
|
|
fcaa57307f | ||
|
|
d25e754beb | ||
|
|
7f6f411ea8 | ||
|
|
96a73e31f3 | ||
|
|
c7942d7d8f | ||
|
|
479348eec9 | ||
|
|
ebfed27630 | ||
|
|
8923485169 | ||
|
|
d62de26683 | ||
|
|
1dd9c5b009 | ||
|
|
01b64e18ab | ||
|
|
fa61339a49 | ||
|
|
deb2eee3ad | ||
|
|
829cc9f6f9 | ||
|
|
1c7ef6622b | ||
|
|
f54ae52b2a | ||
|
|
171ae3cabe | ||
|
|
f3888df88c | ||
|
|
9274f9ec08 | ||
|
|
6822105c70 | ||
|
|
d5d855e4c3 | ||
|
|
1e09ac4fd3 | ||
|
|
bc513400c5 | ||
|
|
715ceb9ed4 | ||
|
|
0115b152a8 | ||
|
|
e1622a56fa | ||
|
|
03fee56a54 | ||
|
|
d00171a629 | ||
|
|
fa28b218ec | ||
|
|
eb882170d5 | ||
|
|
48a565a51d | ||
|
|
f60dd6a788 | ||
|
|
d6a88d4c9e | ||
|
|
4b4ff08131 | ||
|
|
698478ef95 | ||
|
|
4ab9ab51f6 | ||
|
|
bf3995d45c | ||
|
|
4fe603bf96 | ||
|
|
0639827d00 | ||
|
|
eaacd45672 | ||
|
|
5d733ab915 | ||
|
|
1bf6cc0e72 | ||
|
|
a9470ed9c1 | ||
|
|
2c5ef95027 | ||
|
|
f712b1369a | ||
|
|
2f03b18619 | ||
|
|
8cd5ba6361 | ||
|
|
c951961a92 | ||
|
|
3b68e1b27d | ||
|
|
48e6b1a84e | ||
|
|
ee46b46234 | ||
|
|
db49fd498a | ||
|
|
e95b90363e | ||
|
|
82c0d741e4 | ||
|
|
87b150d539 | ||
|
|
508a7d8605 | ||
|
|
9119939c3d | ||
|
|
59fc667651 | ||
|
|
0d946f853f | ||
|
|
b767a78b05 | ||
|
|
39774a83c5 | ||
|
|
c04015899b | ||
|
|
6898daf0ce | ||
|
|
eb3a31a698 | ||
|
|
0476627f34 | ||
|
|
eba42ad9b4 | ||
|
|
ca909b4f6b |
65
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
65
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
labels: ["type: bug", "needs-triage"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for helping PyScript! 🐍
|
||||||
|
|
||||||
|
Going through bugs and issues takes up a lot of time, so please be so kind and take a few minutes to fill out all the areas to the best of your ability.
|
||||||
|
|
||||||
|
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
|
||||||
|
|
||||||
|
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
|
||||||
|
|
||||||
|
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
|
||||||
|
|
||||||
|
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
|
||||||
|
- type: checkboxes
|
||||||
|
id: checks
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
description: Please confirm and check all the following options.
|
||||||
|
options:
|
||||||
|
- label: I added a descriptive title
|
||||||
|
required: true
|
||||||
|
- label: I searched for other issues and couldn't find a solution or duplication
|
||||||
|
required: true
|
||||||
|
- label: I already searched in Google and didn't find any good information or help
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: And what should have happened instead? This really helps everyone review quicker and greatly increases the chance that someone can get around to solve your issue
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: What browsers are you seeing the problem on? (if applicable)
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Firefox
|
||||||
|
- Chrome
|
||||||
|
- Safari
|
||||||
|
- Microsoft Edge
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: list
|
||||||
|
attributes:
|
||||||
|
label: Console info
|
||||||
|
description: |
|
||||||
|
If there are errors in your browser console then its helpful to be able to troubleshoot.
|
||||||
|
- Chrome , Firefox, and Edge: Right-click on the page and select *Inspect*. Alternatively you can press F12 on your keyboard.
|
||||||
|
- Safari: Find instructions [here](https://support.apple.com/guide/safari/use-the-developer-tools-in-the-develop-menu-sfri20948/mac).
|
||||||
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any additional context information or screenshots you think are useful.
|
||||||
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: needs-triage
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior: either a code snippet or a link to an HTML page which shows the bug.
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Feature Proposals
|
||||||
|
url: https://github.com/pyscript/pyscript/discussions/new?category=proposals
|
||||||
|
about: Create a feature request to make PyScript even better
|
||||||
|
- name: Questions
|
||||||
|
url: https://github.com/pyscript/pyscript/discussions/new?category=q-a
|
||||||
|
about: For questions or discussions about pyscript
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: "[FEATURE]"
|
|
||||||
labels: needs-triage
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. As a user, I'd like to [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you expect to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!--Please describe the changes in your pull request in few words here. -->
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
<!-- List the changes done to fix a bug or introduce a new feature.Please note both user-facing changes and changes to internal API's here -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
|
||||||
|
|
||||||
|
- [ ] All tests pass locally
|
||||||
|
- [ ] I have updated `CHANGELOG.md`
|
||||||
|
- [ ] I have created documentation for this(if applicable)
|
||||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||||
|
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
groups:
|
||||||
|
github-actions:
|
||||||
|
patterns:
|
||||||
|
- "*" # Group all Actions updates into a single larger pull request
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
5
.github/release.yml
vendored
Normal file
5
.github/release.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
changelog:
|
||||||
|
categories:
|
||||||
|
- title: New Features
|
||||||
|
- title: Breaking Changes
|
||||||
|
- title: Known Issues
|
||||||
56
.github/workflows/build-alpha.yml
vendored
56
.github/workflows/build-alpha.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: '[CI] Build Alpha'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
|
||||||
- '**' # Currently any tag, need to slim down
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/**
|
|
||||||
- .github/workflows/build-alpha.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: pyscriptjs
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules
|
|
||||||
with:
|
|
||||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
- name: Build pyscript
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
- name: Sync to S3
|
|
||||||
run: aws s3 sync --quiet ./examples/build/ s3://pyscript.net/alpha/
|
|
||||||
63
.github/workflows/build-latest.yml
vendored
63
.github/workflows/build-latest.yml
vendored
@@ -1,63 +0,0 @@
|
|||||||
name: '[CI] Build Latest'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: # Only run on merges into main that modify files under pyscriptjs/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/**
|
|
||||||
- .github/workflows/build-latest.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
pull_request: # Run on any PR that modifies files in pyscriptjs/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/**
|
|
||||||
- .github/workflows/build-latest.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: pyscriptjs
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install node
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
- name: Cache node modules
|
|
||||||
uses: actions/cache@v3
|
|
||||||
env:
|
|
||||||
cache-name: cache-node-modules
|
|
||||||
with:
|
|
||||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
|
||||||
${{ runner.os }}-build-
|
|
||||||
${{ runner.os }}-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
npm install
|
|
||||||
- name: Build pyscript
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
- name: Sync to S3
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
run: aws s3 sync --quiet ./examples/build/ s3://pyscript.net/unstable
|
|
||||||
56
.github/workflows/prepare-release.yml
vendored
Normal file
56
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: "Prepare Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9][0-9][0-9][0-9].[0-9][0-9].[0-9]+" # YYYY.MM.MICRO
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./pyscript.core
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: NPM Install
|
||||||
|
run: npm install && npx playwright install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.html
|
||||||
|
working-directory: .
|
||||||
|
run: sed 's#_PATH_#./#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||||
|
|
||||||
|
- name: Zip dist folder
|
||||||
|
run: zip -r -q ./build.zip ./dist
|
||||||
|
|
||||||
|
- name: Prepare Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
generate_release_notes: true
|
||||||
|
files: ./build.zip
|
||||||
63
.github/workflows/publish-release.yml
vendored
Normal file
63
.github/workflows/publish-release.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
name: "Publish Release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./pyscript.core
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: npm install
|
||||||
|
run: npm install && npx playwright install
|
||||||
|
|
||||||
|
- name: build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.html in snapshot
|
||||||
|
working-directory: .
|
||||||
|
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||||
|
|
||||||
|
- name: Generate release.tar from snapshot and put it in dist/
|
||||||
|
working-directory: .
|
||||||
|
run: tar -cvf ../release.tar * && mv ../release.tar .
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
|
- name: Sync to S3
|
||||||
|
run:
|
||||||
|
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
|
||||||
|
aws s3 sync --quiet ./dist/ s3://pyscript.net/latest/
|
||||||
|
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/
|
||||||
61
.github/workflows/publish-snapshot.yml
vendored
Normal file
61
.github/workflows/publish-snapshot.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: "Publish Snapshot"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
snapshot_version:
|
||||||
|
description: "The calver version of this snapshot: 2022.09.1 or 2022.09.1.RC1"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./pyscript.core
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-snapshot:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm install && npx playwright install
|
||||||
|
|
||||||
|
- name: Build Pyscript.core
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
|
- name: Generate index.html in snapshot
|
||||||
|
working-directory: .
|
||||||
|
run: sed 's#_PATH_#https://pyscript.net/snapshots/${{ inputs.snapshot_version }}/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||||
|
|
||||||
|
- name: Copy to Snapshot
|
||||||
|
run: >
|
||||||
|
aws s3 sync ./dist/ s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/
|
||||||
61
.github/workflows/publish-unstable.yml
vendored
Normal file
61
.github/workflows/publish-unstable.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
name: "Publish Unstable"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: # Only run on merges into main that modify files under pyscript.core/ and examples/
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- pyscript.core/**
|
||||||
|
- examples/**
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-unstable:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./pyscript.core
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: NPM Install
|
||||||
|
run: npm install && npx playwright install
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Generate index.html in snapshot
|
||||||
|
working-directory: .
|
||||||
|
run: sed 's#_PATH_#https://pyscript.net/unstable/#' ./public/index.html > ./pyscript.core/dist/index.html
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
||||||
|
|
||||||
|
- name: Sync to S3
|
||||||
|
run: aws s3 sync --quiet ./dist/ s3://pyscript.net/unstable/
|
||||||
33
.github/workflows/sync-examples.yml
vendored
33
.github/workflows/sync-examples.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: '[CI] Sync Examples'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push: # Only run on merges into main that modify files under pyscriptjs/examples/
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- pyscriptjs/examples/**
|
|
||||||
- .github/workflows/sync-examples.yml # Test that workflow works when changed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: pyscriptjs/examples
|
|
||||||
|
|
||||||
steps:
|
|
||||||
|
|
||||||
# Deploy to S3
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1.6.1
|
|
||||||
with:
|
|
||||||
aws-region: ${{secrets.AWS_REGION}}
|
|
||||||
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
|
|
||||||
- name: Sync to S3
|
|
||||||
# Sync outdated or new files, delete ones no longer in source
|
|
||||||
run: aws s3 sync --quiet --delete . s3://pyscript.net/examples/ # Sync directory, delete what is not in source
|
|
||||||
92
.github/workflows/test.yml
vendored
Normal file
92
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
name: "[CI] Test"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push: # Only run on merges into main that modify certain files
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- pyscript.core/**
|
||||||
|
- .github/workflows/test.yml
|
||||||
|
|
||||||
|
pull_request: # Only run on merges into main that modify certain files
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- pyscript.core/**
|
||||||
|
- .github/workflows/test.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
BuildAndTest:
|
||||||
|
runs-on: ubuntu-latest-8core
|
||||||
|
env:
|
||||||
|
MINICONDA_PYTHON_VERSION: py38
|
||||||
|
MINICONDA_VERSION: 4.11.0
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 3
|
||||||
|
|
||||||
|
# display a git log: when you run CI on PRs, github automatically
|
||||||
|
# merges the PR into main and run the CI on that commit. The idea
|
||||||
|
# here is to show enough of git log to understand what is the
|
||||||
|
# actual commit (in the PR) that we are using. See also
|
||||||
|
# 'fetch-depth: 3' above.
|
||||||
|
- name: git log
|
||||||
|
run: git log --graph -3
|
||||||
|
|
||||||
|
- name: Install node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: setup Miniconda
|
||||||
|
uses: conda-incubator/setup-miniconda@v3
|
||||||
|
|
||||||
|
- name: Create and activate virtual environment
|
||||||
|
run: |
|
||||||
|
python3 -m venv test_venv
|
||||||
|
source test_venv/bin/activate
|
||||||
|
echo PATH=$PATH >> $GITHUB_ENV
|
||||||
|
echo VIRTUAL_ENV=$VIRTUAL_ENV >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Setup dependencies in virtual environment
|
||||||
|
run: |
|
||||||
|
make setup
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: Integration Tests
|
||||||
|
#run: make test-integration-parallel
|
||||||
|
run: |
|
||||||
|
make test-integration
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pyscript
|
||||||
|
path: |
|
||||||
|
pyscript.core/dist/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: success() || failure()
|
||||||
|
with:
|
||||||
|
name: test_results
|
||||||
|
path: test_results/
|
||||||
|
if-no-files-found: error
|
||||||
16
.github/workflows/test_report.yml
vendored
Normal file
16
.github/workflows/test_report.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Test Report
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ['\[CI\] Test']
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
jobs:
|
||||||
|
report:
|
||||||
|
runs-on: ubuntu-latest-8core
|
||||||
|
steps:
|
||||||
|
- uses: dorny/test-reporter@v1.9.0
|
||||||
|
with:
|
||||||
|
artifact: test_results
|
||||||
|
name: Test reports
|
||||||
|
path: "*.xml"
|
||||||
|
reporter: java-junit
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -71,6 +71,7 @@ instance/
|
|||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
docs/_env/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
@@ -134,3 +135,18 @@ dmypy.json
|
|||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# junit xml for test results
|
||||||
|
test_results
|
||||||
|
|
||||||
|
# @pyscript/core npm artifacts
|
||||||
|
pyscript.core/test-results/*
|
||||||
|
pyscript.core/core.*
|
||||||
|
pyscript.core/dist
|
||||||
|
pyscript.core/dist.zip
|
||||||
|
pyscript.core/src/plugins.js
|
||||||
|
pyscript.core/src/stdlib/pyscript.js
|
||||||
|
pyscript.core/src/3rd-party/*
|
||||||
|
!pyscript.core/src/3rd-party/READMEmd
|
||||||
|
|||||||
@@ -1,77 +1,53 @@
|
|||||||
# This is the configuration for pre-commit, a local framework for managing pre-commit hooks
|
# This is the configuration for pre-commit, a local framework for managing pre-commit hooks
|
||||||
# Check out the docs at: https://pre-commit.com/
|
# Check out the docs at: https://pre-commit.com/
|
||||||
|
ci:
|
||||||
|
#skip: [eslint]
|
||||||
|
autoupdate_schedule: monthly
|
||||||
|
|
||||||
default_stages: [commit]
|
default_stages: [commit]
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.2.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
- id: check-case-conflict
|
- id: check-case-conflict
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
- id: check-json
|
- id: check-json
|
||||||
exclude: tsconfig.json
|
exclude: tsconfig\.json
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
|
exclude: bad\.toml
|
||||||
- id: check-xml
|
- id: check-xml
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
exclude: \.min\.js$
|
exclude: pyscript\.core/dist|\.min\.js$
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/bandit
|
- repo: https://github.com/psf/black
|
||||||
rev: 1.7.4
|
rev: 24.4.2
|
||||||
hooks:
|
|
||||||
- id: bandit
|
|
||||||
args:
|
|
||||||
- --skip=B201
|
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
|
||||||
rev: 22.3.0
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
|
||||||
|
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.1.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell # See 'setup.cfg' for args
|
- id: codespell # See 'pyproject.toml' for args
|
||||||
|
exclude: \.js\.map$
|
||||||
|
additional_dependencies:
|
||||||
|
- tomli
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||||
rev: 4.0.1
|
rev: "v3.0.0-alpha.6"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8 # See 'setup.cfg' for args
|
- id: prettier
|
||||||
additional_dependencies: [flake8-bugbear, flake8-comprehensions]
|
exclude: pyscript\.core/test|pyscript\.core/dist|pyscript\.core/types|pyscript.core/src/stdlib/pyscript.js|pyscript\.sw/|pyscript.core/src/3rd-party
|
||||||
|
args: [--tab-width, "4"]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: 5.10.1
|
rev: 5.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
args: [--profile, black]
|
args: [--profile, black]
|
||||||
|
|
||||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
|
||||||
rev: v2.3.0
|
|
||||||
hooks:
|
|
||||||
- id: pretty-format-yaml
|
|
||||||
args: [--autofix, --indent, '4']
|
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v2.32.1
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args:
|
|
||||||
- --py310-plus
|
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
|
||||||
rev: v8.15.0
|
|
||||||
hooks:
|
|
||||||
- id: eslint
|
|
||||||
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx
|
|
||||||
types: [file]
|
|
||||||
additional_dependencies:
|
|
||||||
- eslint
|
|
||||||
- eslint-plugin-svelte3
|
|
||||||
- typescript
|
|
||||||
- '@typescript-eslint/eslint-plugin'
|
|
||||||
- '@typescript-eslint/parser'
|
|
||||||
|
|||||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ISSUE_TEMPLATE
|
||||||
|
*.min.*
|
||||||
|
package-lock.json
|
||||||
|
docs
|
||||||
|
examples/panel.html
|
||||||
28
.readthedocs.yml
Normal file
28
.readthedocs.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-20.04
|
||||||
|
tools:
|
||||||
|
python: miniconda3-4.7
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
conda:
|
||||||
|
environment: docs/environment.yml
|
||||||
|
|
||||||
|
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||||
|
# formats:
|
||||||
|
# - pdf
|
||||||
|
|
||||||
|
# Optionally declare the Python requirements required to build your docs
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: docs/requirements.txt
|
||||||
97
CHANGELOG.md
Normal file
97
CHANGELOG.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Release Notes
|
||||||
|
|
||||||
|
## 2024.05.21
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- `py-editor` run buttons now display a spinner when disabled, which occurs when the editor is running code.
|
||||||
|
|
||||||
|
## 2023.05.01
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Added the `xterm` attribute to `py-config`. When set to `True` or `xterm`, an (output-only) [xterm.js](http://xtermjs.org/) terminal will be used in place of the default py-terminal.
|
||||||
|
- The default version of Pyodide is now `0.23.2`. See the [Pyodide Changelog](https://pyodide.org/en/stable/project/changelog.html#version-0-23-2) for a detailed list of changes.
|
||||||
|
- Added the `@when` decorator for attaching Python functions as event handlers
|
||||||
|
- The `py-mount` attribute on HTML elements has been deprecated, and will be removed in a future release.
|
||||||
|
|
||||||
|
#### Runtime py- attributes
|
||||||
|
|
||||||
|
- Added logic to react to `py-*` attributes changes, removal, `py-*` attributes added to already live nodes but also `py-*` attributes added or defined via injected nodes (either appended or via `innerHTML` operations). ([#1435](https://github.com/pyscript/pyscript/pull/1435))
|
||||||
|
|
||||||
|
#### <script type="py">
|
||||||
|
|
||||||
|
- Added the ability to optionally use `<script type="py">`, `<script type="pyscript">` or `<script type="py-script">` instead of a `<py-script>` custom element, in order to tackle cases where the content of the `<py-script>` tag, inevitably parsed by browsers, could accidentally contain _HTML_ able to break the surrounding page layout. ([#1396](https://github.com/pyscript/pyscript/pull/1396))
|
||||||
|
|
||||||
|
#### <py-terminal>
|
||||||
|
|
||||||
|
- Added a `docked` field and attribute for the `<py-terminal>` custom element, enabled by default when the terminal is in `auto` mode, and able to dock the terminal at the bottom of the page with auto scroll on new code execution.
|
||||||
|
|
||||||
|
#### <py-script>
|
||||||
|
|
||||||
|
- Restored the `output` attribute of `py-script` tags to route `sys.stdout` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
|
||||||
|
- Added a `stderr` attribute of `py-script` tags to route `sys.stderr` to a DOM element with the given ID. ([#1063](https://github.com/pyscript/pyscript/pull/1063))
|
||||||
|
|
||||||
|
#### <py-repl>
|
||||||
|
|
||||||
|
- The `output` attribute of `py-repl` tags now specifies the id of the DOM element that `sys.stdout`, `sys.stderr`, and the results of a REPL execution are written to. It no longer affects the location of calls to `display()`
|
||||||
|
- Added a `stderr` attribute of `py-repl` tags to route `sys.stderr` to a DOM element with the given ID. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
|
||||||
|
- Resored the `output-mode` attribute of `py-repl` tags. If `output-mode` == 'append', the DOM element where output is printed is _not_ cleared before writing new results.
|
||||||
|
- Load code from the attribute src of py-repl and preload it into the corresponding py-repl tag by use the attribute `str` in your `py-repl` tag([#1292](https://github.com/pyscript/pyscript/pull/1292))
|
||||||
|
- <py-repl> elements now have a `getPySrc()` method, which returns the code inside the REPL as a string.([#1516](https://github.com/pyscript/pyscript/pull/1292))
|
||||||
|
|
||||||
|
#### Plugins
|
||||||
|
|
||||||
|
- Plugins may now implement the `beforePyReplExec()` and `afterPyReplExec()` hooks, which are called immediately before and after code in a `py-repl` tag is executed. ([#1106](https://github.com/pyscript/pyscript/pull/1106))
|
||||||
|
|
||||||
|
#### Web worker support
|
||||||
|
|
||||||
|
- introduced the new experimental `execution_thread` config option: if you set `execution_thread = "worker"`, the python interpreter runs inside a web worker
|
||||||
|
- worker support is still **very** experimental: not everything works, use it at your own risk
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Fixes [#1280](https://github.com/pyscript/pyscript/issues/1280), which describes the errors on the PyRepl tests related to having auto-gen tags that shouldn't be there.
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- Py-REPL tests now run on both osx and non osx OSs
|
||||||
|
- migrated from _rollup_ to _esbuild_ to create artifacts
|
||||||
|
- updated `@codemirror` dependency to its latest
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- Add docs for event handlers
|
||||||
|
|
||||||
|
## 2023.03.1
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- Fixed an issue where `pyscript` would not be available when using the minified version of PyScript. ([#1054](https://github.com/pyscript/pyscript/pull/1054))
|
||||||
|
- Fixed missing closing tag when rendering an image with `display`. ([#1058](https://github.com/pyscript/pyscript/pull/1058))
|
||||||
|
- Fixed a bug where Python plugins methods were being executed twice. ([#1064](https://github.com/pyscript/pyscript/pull/1064))
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
- When adding a `py-` attribute to an element but didn't added an `id` attribute, PyScript will now generate a random ID for the element instead of throwing an error which caused the splash screen to not shutdown. ([#1122](https://github.com/pyscript/pyscript/pull/1122))
|
||||||
|
- You can now disable the splashscreen by setting `enabled = false` in your `py-config` under the `[splashscreen]` configuration section. ([#1138](https://github.com/pyscript/pyscript/pull/1138))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Fixed 'Direct usage of document is deprecated' warning in the getting started guide. ([#1052](https://github.com/pyscript/pyscript/pull/1052))
|
||||||
|
- Added reference documentation for the `py-splashscreen` plugin ([#1138](https://github.com/pyscript/pyscript/pull/1138))
|
||||||
|
- Adds doc for installing tests ([#1156](https://github.com/pyscript/pyscript/pull/1156))
|
||||||
|
- Adds docs for custom Pyscript attributes (`py-*`) that allow you to add event listeners to an element ([#1125](https://github.com/pyscript/pyscript/pull/1125))
|
||||||
|
|
||||||
|
### Deprecations and Removals
|
||||||
|
|
||||||
|
- The py-config `runtimes` to specify an interpreter has been deprecated. The `interpreters` config should be used instead. ([#1082](https://github.com/pyscript/pyscript/pull/1082))
|
||||||
|
- The attributes `pys-onClick` and `pys-onKeyDown` have been deprecated, but the warning was only shown in the console. An alert banner will now be shown on the page if the attributes are used. They will be removed in the next release. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
|
||||||
|
- The pyscript elements `py-button`, `py-inputbox`, `py-box` and `py-title` have now completed their deprecation cycle and have been removed. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
|
||||||
|
- The attributes `pys-onClick` and `pys-onKeyDown` have been removed. Use `py-click` and `py-keydown` instead ([#1361](https://github.com/pyscript/pyscript/pull/1361))
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
The Code of Conduct is available in the pyscript Governance repo.
|
The Code of Conduct is available in the PyScript Governance repo.
|
||||||
See https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md
|
See https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md
|
||||||
|
|||||||
194
CONTRIBUTING.md
194
CONTRIBUTING.md
@@ -4,81 +4,64 @@ Thank you for wanting to contribute to the PyScript project!
|
|||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
* [Code of Conduct](#code-of-conduct)
|
- [Contributing to PyScript](#contributing-to-pyscript)
|
||||||
* [Contributing](#contributing)
|
- [Table of contents](#table-of-contents)
|
||||||
* [Reporting bugs](#reporting-bugs)
|
- [Code of Conduct](#code-of-conduct)
|
||||||
* [Reporting security issues](#reporting-security-issues)
|
- [Contributing](#contributing)
|
||||||
* [Asking questions](#asking-questions)
|
- [Reporting bugs](#reporting-bugs)
|
||||||
* [Setting up your environment](#setting-up-your-environment)
|
- [Creating useful issues](#creating-useful-issues)
|
||||||
* [Places to start](#places-to-start)
|
- [Reporting security issues](#reporting-security-issues)
|
||||||
* [Submitting a change](#submitting-a-change)
|
- [Asking questions](#asking-questions)
|
||||||
* [License terms for contributions](#license-terms-for-contributions)
|
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||||
* [Becoming a maintainer](#becoming-a-maintainer)
|
- [Developing](#developing)
|
||||||
* [Trademarks](#trademarks)
|
- [Rebasing changes](#rebasing-changes)
|
||||||
|
- [Building the docs](#building-the-docs)
|
||||||
|
- [Places to start](#places-to-start)
|
||||||
|
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||||
|
- [Submitting a change](#submitting-a-change)
|
||||||
|
- [License terms for contributions](#license-terms-for-contributions)
|
||||||
|
- [Becoming a maintainer](#becoming-a-maintainer)
|
||||||
|
- [Trademarks](#trademarks)
|
||||||
|
|
||||||
## Code of Conduct
|
# Code of Conduct
|
||||||
|
|
||||||
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
||||||
|
|
||||||
## Contributing
|
# Contributing
|
||||||
|
|
||||||
### Reporting bugs
|
## Reporting bugs
|
||||||
|
|
||||||
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
||||||
|
|
||||||
#### Creating useful issues
|
## Creating useful issues
|
||||||
|
|
||||||
* Use a clear and descriptive title.
|
- Use a clear and descriptive title.
|
||||||
* Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
- Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
||||||
* Describe the behavior you observed, and the behavior you had expected.
|
- Describe the behavior you observed, and the behavior you had expected.
|
||||||
* Include screenshots if they help make the issue clear.
|
- Include screenshots if they help make the issue clear.
|
||||||
|
|
||||||
### Reporting security issues
|
## Reporting security issues
|
||||||
|
|
||||||
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
||||||
|
|
||||||
### Asking questions
|
## Asking questions
|
||||||
|
|
||||||
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
||||||
|
|
||||||
### Setting up your environment
|
## Places to start
|
||||||
|
|
||||||
* clone the repo
|
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions:
|
||||||
```
|
|
||||||
git clone https://github.com/pyscript/pyscript
|
|
||||||
```
|
|
||||||
* cd into the main project folder
|
|
||||||
```
|
|
||||||
cd pyscript/pyscriptjs
|
|
||||||
```
|
|
||||||
* install the dependencies with npm install - make sure to use nodejs version >= 16
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
* run npm run dev to build and run the dev server. This will also watch for changes and rebuild when a file is saved.
|
|
||||||
```
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Places to start
|
- **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
||||||
|
- **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
||||||
|
- **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
||||||
|
- **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
||||||
|
|
||||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions.
|
## Setting up your local environment and developing
|
||||||
|
|
||||||
* **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/contributing/#set-up-your-development-environment) will help you get started.
|
||||||
* **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
|
||||||
* **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
|
||||||
* **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
|
||||||
|
|
||||||
### Submitting a change
|
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||||
|
|
||||||
All contributions must be licensed Apache 2.0, and all files must have a copy of the boilerplate license comment (can be copied from an existing file).
|
|
||||||
|
|
||||||
To create a change for PyScript, you can follow the process described [here](https://docs.github.com/en/get-started/quickstart/contributing-to-projects).
|
|
||||||
|
|
||||||
* Fork a personal copy of the PyScript project.
|
|
||||||
* Make the changes you would like (don't forget to test them!)
|
|
||||||
* Please squash all commits for a change into a single commit (this can be done using "git rebase -i"). Do your best to have a well-formed commit message for the change.
|
|
||||||
* Open a pull request back to the PyScript project and address any comments/questions from the maintainers and other contributors.
|
|
||||||
|
|
||||||
## License terms for contributions
|
## License terms for contributions
|
||||||
|
|
||||||
@@ -86,12 +69,113 @@ This Project welcomes contributions, suggestions, and feedback. All contribution
|
|||||||
|
|
||||||
## Becoming a maintainer
|
## Becoming a maintainer
|
||||||
|
|
||||||
Contributors are invited to be maintainers to the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
|
Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
|
||||||
|
|
||||||
## Trademarks
|
## Trademarks
|
||||||
|
|
||||||
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
|
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|
||||||
|
# Quick guide to pytest
|
||||||
|
|
||||||
|
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
|
||||||
|
|
||||||
|
- To run all tests in the current directory and subdirectories: pytest
|
||||||
|
|
||||||
|
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
|
||||||
|
|
||||||
|
- -s: disables output capturing
|
||||||
|
|
||||||
|
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
|
||||||
|
|
||||||
|
- -v: verbose mode
|
||||||
|
|
||||||
|
- -x: stop the execution as soon as one test fails
|
||||||
|
|
||||||
|
- -k foo: run only the tests whose full name contains foo
|
||||||
|
|
||||||
|
- -k 'foo and bar'
|
||||||
|
|
||||||
|
- -k 'foo and not bar'
|
||||||
|
|
||||||
|
## Running integration tests under pytest
|
||||||
|
|
||||||
|
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
|
||||||
|
|
||||||
|
### To run all the integration tests, single or multi core
|
||||||
|
|
||||||
|
$ pytest -xv
|
||||||
|
...
|
||||||
|
|
||||||
|
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
|
||||||
|
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
|
||||||
|
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
|
||||||
|
...
|
||||||
|
|
||||||
|
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
|
||||||
|
|
||||||
|
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
|
||||||
|
|
||||||
|
$ pytest -n 4
|
||||||
|
|
||||||
|
### To run a single test, headless
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s
|
||||||
|
...
|
||||||
|
[ 0.00 page.goto ] pyscript_hello.html
|
||||||
|
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
||||||
|
...
|
||||||
|
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
||||||
|
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
||||||
|
...
|
||||||
|
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
||||||
|
[ 3.60 console.log ] hello pyscript
|
||||||
|
|
||||||
|
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
|
||||||
|
|
||||||
|
### To run a single test, headed
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
|
||||||
|
...
|
||||||
|
|
||||||
|
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
|
||||||
|
|
||||||
|
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
|
||||||
|
...
|
||||||
|
|
||||||
|
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
|
||||||
|
|
||||||
|
Finally:
|
||||||
|
|
||||||
|
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
||||||
|
...
|
||||||
|
|
||||||
|
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
|
||||||
|
|
||||||
|
### To run only main thread or worker tests
|
||||||
|
|
||||||
|
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
|
||||||
|
|
||||||
|
$ pytest -m main # run only the tests in the main thread
|
||||||
|
$ pytest -m worker # ron only the tests in the web worker
|
||||||
|
|
||||||
|
## Fake server, HTTP cache
|
||||||
|
|
||||||
|
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
|
||||||
|
|
||||||
|
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
|
||||||
|
|
||||||
|
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
|
||||||
|
|
||||||
|
If you want to clear the cache, you can use the special option --clear-http-cache:
|
||||||
|
|
||||||
|
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
# Getting started with PyScript
|
|
||||||
|
|
||||||
This page will guide you through getting started with PyScript.
|
|
||||||
|
|
||||||
## Development setup
|
|
||||||
|
|
||||||
PyScript does not require any development environment other
|
|
||||||
than a web browser. We recommend using [Chrome](https://www.google.com/chrome/).
|
|
||||||
|
|
||||||
If you're using [VSCode](https://code.visualstudio.com/), the
|
|
||||||
[Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
|
|
||||||
can be used to reload the page as you edit the HTML file.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
There is no installation required. In this document we'll use
|
|
||||||
the PyScript assets served on https://pyscript.net.
|
|
||||||
|
|
||||||
If you want to download the source and build it yourself, follow
|
|
||||||
the instructions in the README.md file.
|
|
||||||
|
|
||||||
## Your first PyScript HTML file
|
|
||||||
|
|
||||||
Here's a "Hello, world!" example using PyScript.
|
|
||||||
|
|
||||||
Using your favorite editor create a new file called `hello.html` in
|
|
||||||
the same directory as your PyScript, JavaScript, and CSS files with the
|
|
||||||
following content, and open the file in your web browser. You can typically
|
|
||||||
open an HTML by double clicking it in your file explorer.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
|
||||||
</head>
|
|
||||||
<body> <py-script> print('Hello, World!') </py-script> </body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
Notice the use of the `<py-script>` tag in the HTML body. This
|
|
||||||
is where you'll write your Python code. In the following sections we'll
|
|
||||||
introduce the 8 tags provided by PyScript.
|
|
||||||
|
|
||||||
## The py-script tag
|
|
||||||
|
|
||||||
The `<py-script>` tag lets you execute multi-line Python scripts and
|
|
||||||
print back onto the page. For
|
|
||||||
example, we can compute π.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<py-script>
|
|
||||||
print("Let's compute π:")
|
|
||||||
def compute_pi(n):
|
|
||||||
pi = 2
|
|
||||||
for i in range(1,n):
|
|
||||||
pi *= 4 * i ** 2 / (4 * i ** 2 - 1)
|
|
||||||
return pi
|
|
||||||
|
|
||||||
pi = compute_pi(100000)
|
|
||||||
s = f"π is approximately {pi:.3f}"
|
|
||||||
print(s)
|
|
||||||
</py-script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Writing into labeled elements
|
|
||||||
|
|
||||||
In the example above we had a single `<py-script>` tag and it printed
|
|
||||||
one or more lines onto the page in order. Within the `<py-script>` you
|
|
||||||
have access to the `pyscript` module, which provides a `.write()` method
|
|
||||||
to send strings into labeled elements on the page.
|
|
||||||
|
|
||||||
For example we'll add some style elements and provide place holders for
|
|
||||||
the `<py-script>` tag write to.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<b><p>Today is <u><label id='today'></label></u></p></b>
|
|
||||||
<br>
|
|
||||||
<div id="pi" class="alert alert-primary"></div>
|
|
||||||
<py-script>
|
|
||||||
import datetime as dt
|
|
||||||
pyscript.write('today', dt.date.today().strftime('%A %B %d, %Y'))
|
|
||||||
|
|
||||||
def compute_pi(n):
|
|
||||||
pi = 2
|
|
||||||
for i in range(1,n):
|
|
||||||
pi *= 4 * i ** 2 / (4 * i ** 2 - 1)
|
|
||||||
return pi
|
|
||||||
|
|
||||||
pi = compute_pi(100000)
|
|
||||||
pyscript.write('pi', f'π is approximately {pi:.3f}')
|
|
||||||
</py-script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Packages and modules
|
|
||||||
|
|
||||||
In addition to the [Python Standard Library](https://docs.python.org/3/library/) and
|
|
||||||
the `pyscript` module, many 3rd-party OSS packages will work out-of-the-box with PyScript.
|
|
||||||
|
|
||||||
In order to use them you will need to declare the dependencies using the `<py-env>` in the
|
|
||||||
HTML head. You can also link to `.whl` files directly on disk like in our [toga example](https://github.com/pyscript/pyscript/blob/main/pyscriptjs/examples/toga/freedom.html)
|
|
||||||
|
|
||||||
```
|
|
||||||
<py-env>
|
|
||||||
- './static/wheels/travertino-0.1.3-py3-none-any.whl'
|
|
||||||
</py-env>
|
|
||||||
```
|
|
||||||
|
|
||||||
If your `.whl` is not a pure Python wheel, then open a PR or issue with [pyodide](https://github.com/pyodide/pyodide) to get it added [here](https://github.com/pyodide/pyodide/tree/main/packages).
|
|
||||||
If there's enough popular demand the pyodide team will likely work on supporting your package, regardless things will likely move faster if you make the PR and consult with the team to get unblocked.
|
|
||||||
|
|
||||||
For example, NumPy and Matplotlib are available. Notice here we're using `<py-script output="plot">`
|
|
||||||
as a shortcut, which takes the expression on the last line of the script and runs `pyscript.write('plot', fig)`.
|
|
||||||
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
|
||||||
<py-env>
|
|
||||||
- numpy
|
|
||||||
- matplotlib
|
|
||||||
</py-env>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Let's plot random numbers</h1>
|
|
||||||
<div id="plot"></div>
|
|
||||||
<py-script output="plot">
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
x = np.random.randn(1000)
|
|
||||||
y = np.random.randn(1000)
|
|
||||||
|
|
||||||
fig, ax = plt.subplots()
|
|
||||||
ax.scatter(x, y)
|
|
||||||
fig
|
|
||||||
</py-script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local modules
|
|
||||||
|
|
||||||
In addition to packages you can declare local Python modules that will
|
|
||||||
be imported in the `<py-script>` tag. For example, we can place the random
|
|
||||||
number generation steps in a function in the file `data.py`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
# data.py
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def make_x_and_y(n):
|
|
||||||
x = np.random.randn(n)
|
|
||||||
y = np.random.randn(n)
|
|
||||||
return x, y
|
|
||||||
```
|
|
||||||
|
|
||||||
In the HTML tag `<py-env>` paths to local modules are provided in the
|
|
||||||
`paths:` key.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
|
||||||
<py-env>
|
|
||||||
- numpy
|
|
||||||
- matplotlib
|
|
||||||
- paths:
|
|
||||||
- /data.py
|
|
||||||
</py-env>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<h1>Let's plot random numbers</h1>
|
|
||||||
<div id="plot"></div>
|
|
||||||
<py-script output="plot">
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from data import make_x_and_y
|
|
||||||
|
|
||||||
x, y = make_x_and_y(n=1000)
|
|
||||||
|
|
||||||
fig, ax = plt.subplots()
|
|
||||||
ax.scatter(x, y)
|
|
||||||
fig
|
|
||||||
</py-script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Governance Policy
|
# Governance Policy
|
||||||
|
|
||||||
This document provides the governance policy for the Project. Maintainers agree to this policy and to abide by all Project polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/ANTITRUST.md) by adding their name to the [maintainers.md file](./MAINTAINERS.md).
|
This document provides the governance policy for the Project. Maintainers agree to this policy and to abide by all Project policies, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/ANTITRUST.md) by adding their name to the [maintainers.md file](https://github.com/pyscript/pyscript/blob/main/MAINTAINERS.md).
|
||||||
|
|
||||||
## 1. Roles.
|
## 1. Roles.
|
||||||
|
|
||||||
@@ -41,5 +41,6 @@ Any names, trademarks, logos, or goodwill developed by and associated with the P
|
|||||||
Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee.
|
Amendments to this governance policy may be made by affirmative vote of 2/3 of all Maintainers, with approval by the Organization's Steering Committee.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|||||||
6
LICENSE
6
LICENSE
@@ -186,7 +186,11 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
Copyright (c) 2022-present, PyScript Development Team
|
||||||
|
|
||||||
|
Originated at Anaconda, Inc. in 2022
|
||||||
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
# Maintainers
|
# Maintainers
|
||||||
|
|
||||||
This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](./GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
|
This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](https://github.com/pyscript/pyscript/blob/main/GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
|
||||||
|
|
||||||
| **NAME** | **Organization** |
|
| **NAME** | **Organization** |
|
||||||
| --- | --- |
|
| -------------------- | ---------------- |
|
||||||
| Fabio Pliger | Anaconda, Inc |
|
| Fabio Pliger | Anaconda, Inc |
|
||||||
| Antonio Cuni | Anaconda, Inc |
|
| Antonio Cuni | Anaconda, Inc |
|
||||||
| Philipp Rudiger | Anaconda, Inc |
|
| Philipp Rudiger | Anaconda, Inc |
|
||||||
| Peter Wang | Anaconda, Inc |
|
| Peter Wang | Anaconda, Inc |
|
||||||
| Kevin Goldsmith | Anaconda, Inc |
|
| Kevin Goldsmith | Anaconda, Inc |
|
||||||
| Mariana Meireles | Anaconda, Inc |
|
| Mariana Meireles | |
|
||||||
| --- | --- |
|
| Nicholas H.Tollervey | Anaconda, Inc |
|
||||||
|
| Madhur Tandon | Anaconda, Inc |
|
||||||
|
| Ted Patrick | Anaconda, Inc |
|
||||||
|
| Jeff Glass | |
|
||||||
|
| Paul Everitt | |
|
||||||
|
| Fabio Rosado | Anaconda, Inc |
|
||||||
|
| Andrea Giammarchi | Anaconda, Inc |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Part of MVG-0.1-beta.
|
Part of MVG-0.1-beta.
|
||||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||||
|
|||||||
92
Makefile
Normal file
92
Makefile
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
MIN_NODE_VER := 20
|
||||||
|
MIN_NPM_VER := 6
|
||||||
|
MIN_PY3_VER := 8
|
||||||
|
NODE_VER := $(shell node -v | cut -d. -f1 | sed 's/^v\(.*\)/\1/')
|
||||||
|
NPM_VER := $(shell npm -v | cut -d. -f1)
|
||||||
|
PY3_VER := $(shell python3 -c "import sys;t='{v[1]}'.format(v=list(sys.version_info[:2]));print(t)")
|
||||||
|
PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
||||||
|
|
||||||
|
all:
|
||||||
|
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||||
|
@echo "make setup - check your environment and install the dependencies."
|
||||||
|
@echo "make clean - clean up auto-generated assets."
|
||||||
|
@echo "make build - build PyScript."
|
||||||
|
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||||
|
@echo "make test-integration - run all integration tests sequentially."
|
||||||
|
@echo "make fmt - format the code."
|
||||||
|
@echo "make fmt-check - check the code formatting.\n"
|
||||||
|
|
||||||
|
.PHONY: check-node
|
||||||
|
check-node:
|
||||||
|
@if [ $(NODE_VER) -lt $(MIN_NODE_VER) ]; then \
|
||||||
|
echo "\033[0;31mBuild requires Node $(MIN_NODE_VER).x or higher: $(NODE_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: check-npm
|
||||||
|
check-npm:
|
||||||
|
@if [ $(NPM_VER) -lt $(MIN_NPM_VER) ]; then \
|
||||||
|
echo "\033[0;31mBuild requires Node $(MIN_NPM_VER).x or higher: $(NPM_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: check-python
|
||||||
|
check-python:
|
||||||
|
@if [ $(PY_OK) -eq 0 ]; then \
|
||||||
|
echo "\033[0;31mRequires Python 3.$(MIN_PY3_VER).x or higher: 3.$(PY3_VER) detected.\033[0m"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check the environment, install the dependencies.
|
||||||
|
setup: check-node check-npm check-python
|
||||||
|
cd pyscript.core && npm install && cd ..
|
||||||
|
ifeq ($(VIRTUAL_ENV),)
|
||||||
|
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
||||||
|
false
|
||||||
|
else
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
playwright install
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Clean up generated assets.
|
||||||
|
clean:
|
||||||
|
find . -name \*.py[cod] -delete
|
||||||
|
rm -rf $(env) *.egg-info
|
||||||
|
rm -rf .pytest_cache .coverage coverage.xml
|
||||||
|
|
||||||
|
# Build PyScript.
|
||||||
|
build:
|
||||||
|
cd pyscript.core && npx playwright install && npm run build
|
||||||
|
|
||||||
|
# Run the precommit checks (run eslint).
|
||||||
|
precommit-check:
|
||||||
|
pre-commit run --all-files
|
||||||
|
|
||||||
|
# Run all integration tests sequentially.
|
||||||
|
test-integration:
|
||||||
|
mkdir -p test_results
|
||||||
|
pytest -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||||
|
|
||||||
|
# Run all integration tests in parallel.
|
||||||
|
test-integration-parallel:
|
||||||
|
mkdir -p test_results
|
||||||
|
pytest --numprocesses auto -vv $(ARGS) pyscript.core/tests/integration/ --log-cli-level=warning --junitxml=test_results/integration.xml
|
||||||
|
|
||||||
|
# Format the code.
|
||||||
|
fmt: fmt-py
|
||||||
|
@echo "Format completed"
|
||||||
|
|
||||||
|
# Check the code formatting.
|
||||||
|
fmt-check: fmt-py-check
|
||||||
|
@echo "Format check completed"
|
||||||
|
|
||||||
|
# Format Python code.
|
||||||
|
fmt-py:
|
||||||
|
black -l 88 --skip-string-normalization .
|
||||||
|
isort --profile black .
|
||||||
|
|
||||||
|
# Check the format of Python code.
|
||||||
|
fmt-py-check:
|
||||||
|
black -l 88 --check .
|
||||||
|
|
||||||
|
.PHONY: $(MAKECMDGOALS)
|
||||||
91
README.md
91
README.md
@@ -3,44 +3,93 @@
|
|||||||
## What is PyScript
|
## What is PyScript
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
PyScript is a Pythonic alternative to Scratch, JSFiddle, and other "easy to use" programming frameworks, with the goal of making the web a friendly, hackable place where anyone can author interesting and interactive applications.
|
|
||||||
|
|
||||||
To get started see [GETTING-STARTED](GETTING-STARTED.md).
|
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [MicroPython](https://micropython.org/) and [WASM](https://webassembly.org/), and modern web technologies.
|
||||||
|
|
||||||
For examples see [the pyscript folder](pyscriptjs).
|
To get started see the [Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||||
|
|
||||||
|
For examples see [here](https://pyscript.com/@examples).
|
||||||
|
|
||||||
|
Other useful resources:
|
||||||
|
|
||||||
|
- The [official technical docs](https://docs.pyscript.net/).
|
||||||
|
- Our current [Home Page](https://pyscript.net/) on the web.
|
||||||
|
- A free-to-use [online editor](https://pyscript.com/) for trying PyScript.
|
||||||
|
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in touch .
|
||||||
|
|
||||||
|
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom, where we can talk about PyScript development in the open. Most of the maintainers regularly participate in the call, and everybody is welcome to join.
|
||||||
|
|
||||||
|
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: this is a call in which everybody is encouraged to show what they did with PyScript.
|
||||||
|
|
||||||
|
For more details on how to join the calls and up to date schedule, consult the official calendar:
|
||||||
|
|
||||||
|
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
|
||||||
|
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
|
||||||
|
|
||||||
### Longer Version
|
### Longer Version
|
||||||
|
|
||||||
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
||||||
|
|
||||||
## Try PyScript
|
## Try PyScript
|
||||||
|
|
||||||
To try PyScript, import the appropriate pyscript files to your html page with:
|
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
|
<head>
|
||||||
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://pyscript.net/releases/2024.5.2/core.css"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="https://pyscript.net/releases/2024.5.2/core.js"
|
||||||
|
></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py" terminal>
|
||||||
|
from pyscript import display
|
||||||
|
display("Hello World!") # this goes to the DOM
|
||||||
|
print("Hello terminal") # this goes to the terminal
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
```
|
```
|
||||||
You can then use PyScript components in your html page. PyScript currently implements the following elements:
|
|
||||||
|
|
||||||
* `<py-script>`: can be used to define python code that is executable within the web page. The element itself is not rendered to the page and is only used to add logic
|
You can then use PyScript components in your html page. PyScript currently offers various ways of running Python code:
|
||||||
* `<py-repl>`: creates a REPL component that is rendered to the page as a code editor and allows users to write executable code
|
|
||||||
|
|
||||||
Check out the [pyscriptjs/examples](pyscriptjs/examples) folder for more examples on how to use it, all you need to do is open them in Chrome.
|
- `<script type="py">`: can be used to define python code that is executable within the web page.
|
||||||
|
- `<script type="py" src="hello.py">`: same as above, but the python source is fetched from the given URL.
|
||||||
|
- `<script type="py" terminal>`: same as above, but also creates a terminal where to display stdout and stderr (e.g., the output of `print()`); `input()` does not work.
|
||||||
|
- `<script type="py" terminal worker>`: run Python inside a web worker: the terminal is fully functional and `input()` works.
|
||||||
|
- `<py-script>`: same as `<script type="py">`, but it is not recommended because if the code contains HTML tags, they could be parsed wrongly.
|
||||||
|
- `<script type="mpy">`: same as above but use MicroPython instead of Python.
|
||||||
|
|
||||||
|
Check out the [official docs](https://docs.pyscript.net/) for more detailed documentation.
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
|
||||||
To contribute see the [CONTRIBUTING](CONTRIBUTING.md) document.
|
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||||
|
|
||||||
## Resources
|
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
||||||
|
|
||||||
* [Discussion board](https://community.anaconda.cloud/c/tech-topics/pyscript)
|
|
||||||
* [Home Page](https://pyscript.net/)
|
|
||||||
* [Blog Post](https://engineering.anaconda.com/2022/04/welcome-pyscript.html)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
* This is an extremely experimental project, so expect things to break!
|
|
||||||
* PyScript has been only tested on Chrome at the moment.
|
|
||||||
|
|
||||||
## Governance
|
## Governance
|
||||||
|
|
||||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
||||||
|
|
||||||
|
## Release
|
||||||
|
|
||||||
|
To cut a new release of PyScript simply
|
||||||
|
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||||
|
remembering to write a comprehensive changelog. A [GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||||
|
will kick in and ensure the release is described and deployed to a URL with the
|
||||||
|
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||||
|
our [CalVer](https://calver.org/) versioning scheme).
|
||||||
|
|
||||||
|
Then, the following three separate repositories need updating:
|
||||||
|
|
||||||
|
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||||
|
file in the root of the directory and then `node version-update.js`.
|
||||||
|
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||||
|
referenced in `index.html` is the latest version.
|
||||||
|
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||||
|
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||||
|
their project templates.
|
||||||
|
|||||||
19
TROUBLESHOOTING.md
Normal file
19
TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
This page is meant for troubleshooting common problems with PyScript.
|
||||||
|
|
||||||
|
## Table of contents:
|
||||||
|
|
||||||
|
- [Make Setup](#make-setup)
|
||||||
|
|
||||||
|
## Make setup
|
||||||
|
|
||||||
|
A lot of problems related to `make setup` are related to node and npm being outdated. Once npm and node are updated, `make setup` should work. You can follow the steps on the [npm documentation](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) to update npm (the update command for Linux should work for Mac as well). Once npm has been updated you can continue to the instructions to update node below.
|
||||||
|
|
||||||
|
To update Node run the following commands in order (Most likely you'll be prompted for your user password, this is normal):
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo npm cache clean -f
|
||||||
|
sudo npm install -g n
|
||||||
|
sudo n stable
|
||||||
|
```
|
||||||
54
public/index.html
Normal file
54
public/index.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/mvp.css@1.12/mvp.css" />
|
||||||
|
<link rel="stylesheet" href="_PATH_core.css" />
|
||||||
|
<script type="module" src="_PATH_core.js"></script>
|
||||||
|
<title>PyScript</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1><py-script></h1>
|
||||||
|
<ul>
|
||||||
|
<li><a href="core.js">core.js</a></li>
|
||||||
|
<li><a href="core.js.map">core.js.map</a></li>
|
||||||
|
<li><a href="core.css">core.css</a></li>
|
||||||
|
</ul>
|
||||||
|
<div id="out"></div>
|
||||||
|
<py-script>
|
||||||
|
from pyscript import display
|
||||||
|
import sys
|
||||||
|
display(sys.version)
|
||||||
|
</py-script>
|
||||||
|
|
||||||
|
<h2>Example</h2>
|
||||||
|
<pre style="padding: 1em; border: 1px solid #000000">
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>PyScript Hello World</title>
|
||||||
|
<link rel="stylesheet" href="_PATH_core.css" />
|
||||||
|
<script type="module" src="_PATH_core.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
Hello world! <br>
|
||||||
|
This is the current date and time, as computed by Python:
|
||||||
|
<py-script>
|
||||||
|
from pyscript import display
|
||||||
|
from datetime import datetime
|
||||||
|
now = datetime.now()
|
||||||
|
display(now.strftime("%m/%d/%Y, %H:%M:%S"))
|
||||||
|
</py-script>
|
||||||
|
</body>
|
||||||
|
</html></pre
|
||||||
|
>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.2"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[tool.codespell]
|
||||||
|
ignore-words-list = "afterall"
|
||||||
|
skip = "*.js,*.json"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = false
|
||||||
12
pyscript.core/.npmignore
Normal file
12
pyscript.core/.npmignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.eslintrc.cjs
|
||||||
|
eslint.config.mjs
|
||||||
|
.pytest_cache/
|
||||||
|
node_modules/
|
||||||
|
rollup/
|
||||||
|
test/
|
||||||
|
tests/
|
||||||
|
test-results/
|
||||||
|
src/stdlib/_pyscript
|
||||||
|
src/stdlib/pyscript.py
|
||||||
|
package-lock.json
|
||||||
|
tsconfig.json
|
||||||
203
pyscript.core/LICENSE
Normal file
203
pyscript.core/LICENSE
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright (c) 2022-present, PyScript Development Team
|
||||||
|
|
||||||
|
Originated at Anaconda, Inc. in 2022
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
61
pyscript.core/README.md
Normal file
61
pyscript.core/README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# @pyscript/core
|
||||||
|
|
||||||
|
We have moved and renamed previous _core_ module as [polyscript](https://github.com/pyscript/polyscript/#readme), which is the base module used in here to build up _PyScript Next_, now hosted in this folder.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Please read [core documentation](./docs/README.md) to know more about this project.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Clone this repository then run `npm install` within its folder.
|
||||||
|
|
||||||
|
Use `npm run build` to create all artifacts and _dist_ files.
|
||||||
|
|
||||||
|
Use `npm run server` to test locally, via the `http://localhost:8080/test/` url, smoke tests or to test manually anything you'd like to check.
|
||||||
|
|
||||||
|
### Artifacts
|
||||||
|
|
||||||
|
There are two main artifacts in this project:
|
||||||
|
|
||||||
|
- **stdlib** and its content, where `src/stdlib/pyscript.js` exposes as object literal all the _Python_ content within the folder (recursively)
|
||||||
|
- **plugins** and its content, where `src/plugins.js` exposes all available _dynamic imports_, able to instrument the bundler to create files a part within the _dist/_ folder, so that by default _core_ remains as small as possible
|
||||||
|
|
||||||
|
Accordingly, whenever a file contains this warning at its first line, please do not change such file directly before submitting a merge request, as that file will be overwritten at the next `npm run build` command, either here or in _CI_:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
Before running the tests, we need to create a tests environment first. To do so run the following command from the root folder of the project:
|
||||||
|
|
||||||
|
```
|
||||||
|
make setup
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a tests environment [in the root of the project, named `./env`]and install all the dependencies needed to run the tests.
|
||||||
|
|
||||||
|
After the command has completed and the tests environment has been created, you can run the **integration tests** with
|
||||||
|
the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
make test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
## `pyscript` python package
|
||||||
|
|
||||||
|
The `pyscript` package available in _Python_ lives in the folder `src/stdlib/pyscript/`.
|
||||||
|
|
||||||
|
All _Python_ files will be embedded automatically whenever `npm run build` happens and reflected into the `src/stdlib/pyscript.js` file.
|
||||||
|
|
||||||
|
It is _core_ responsibility to ensure those files will be available through the Filesystem in either the _main_ thread, or any _worker_.
|
||||||
|
|
||||||
|
## JS plugins
|
||||||
|
|
||||||
|
While community or third party plugins don't need to be part of this repository and can be added just importing `@pyscript/core` as module, there are a few plugins that we would like to make available by default and these are considered _core plugins_.
|
||||||
|
|
||||||
|
To add a _core plugin_ to this project you can define your plugin entry-point and name in the `src/plugins` folder (see the `error.js` example) and create, if necessary, a folder with the same name where extra files or dependencies can be added.
|
||||||
|
|
||||||
|
The _build_ command will bring plugins by name as artifact so that the bundler can create ad-hoc files within the `dist/` folder.
|
||||||
31
pyscript.core/dev.cjs
Normal file
31
pyscript.core/dev.cjs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
const { exec } = require("node:child_process");
|
||||||
|
|
||||||
|
const build = (fileName) => {
|
||||||
|
if (fileName) console.log(fileName, "changed");
|
||||||
|
else console.log("building without optimizations");
|
||||||
|
queue = queue.then(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
exec(
|
||||||
|
"npm run build:stdlib && npm run build:plugins && npm run build:core",
|
||||||
|
{ cwd: __dirname, env: { ...process.env, NO_MIN: true } },
|
||||||
|
(error) => {
|
||||||
|
if (error) console.error(error);
|
||||||
|
else console.log(fileName || "", "build completed");
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
ignored: /\/(?:toml|plugins|pyscript)\.[mc]?js$/,
|
||||||
|
persistent: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
require("chokidar").watch("./src", options).on("change", build);
|
||||||
|
|
||||||
|
build();
|
||||||
271
pyscript.core/docs/README.md
Normal file
271
pyscript.core/docs/README.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# PyScript Next
|
||||||
|
|
||||||
|
<sup>A summary of <code>@pyscript/core</code> features</sup>
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
Differently from [pyscript classic](https://github.com/pyscript/pyscript), where "*classic*" is the disambiguation name we use to describe the two versions of the project, `@pyscript/core` is an *ECMAScript Module* with the follow benefits:
|
||||||
|
|
||||||
|
* it doesn't block the page like a regular script, without a `deferred` attribute, would
|
||||||
|
* it allows modularity in the future
|
||||||
|
* it bootstraps itself once but it allows exports via the module
|
||||||
|
|
||||||
|
Accordingly, this is the bare minimum required output to bootstrap *PyScript Next* in your page via a CDN:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Option 1: based on esm.sh which in turns is jsdlvr -->
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core"></script>
|
||||||
|
|
||||||
|
<!-- Option 2: based on unpkg.com -->
|
||||||
|
<script type="module" src="https://unpkg.com/@pyscript/core"></script>
|
||||||
|
|
||||||
|
<!-- Option X: any CDN that uses npmjs registry should work -->
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the module is loaded, any `<script type="py"></script>` on the page, or any `<py-script>` tag, would automatically run its own code or the file defined as `src` attribute, after bootstrapping the *pyodide* interpreter.
|
||||||
|
|
||||||
|
If no `<script type="py">` or `<py-script>` tag is present, it is still possible to use the module to bootstrap via JS a *Worker*, bypassing the need to bootstrap *pyodide* on the main thread, hence without ever blocking the page.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import { PyWorker } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
|
||||||
|
|
||||||
|
const worker = PyWorker("./code.py", { config: "./config.toml" /* optional */ });
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, it is possible to specify a `worker` attribute to either run embedded code or the provided `src` file.
|
||||||
|
|
||||||
|
#### CSS
|
||||||
|
|
||||||
|
If you are planning to use either `<py-config>` or `<py-script>` tags on the page, where latter case is usually better off with `<script type="py">` instead, you can also use CDNs to land our custom CSS:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Option 1: based on esm.sh which in turns is jsdlvr -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css">
|
||||||
|
|
||||||
|
<!-- Option 2: based on unpkg.com -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@pyscript/core/dist/core.css">
|
||||||
|
|
||||||
|
<!-- Option X: any CDN that uses npmjs registry should work -->
|
||||||
|
```
|
||||||
|
|
||||||
|
The CSS is needed to avoid seeing content on the page before *PyScript* gets a chance to initialize itself. This means both `py-config` and `py-script` tags will have a `display:none` property which is overwritten by *PyScript* once it initialize each `py-script` custom element.
|
||||||
|
|
||||||
|
Once again, if you use `<script type="py">` instead, you won't need CSS unless you also have a `py-config` on the page, instead of using an external `config` file, defined via the `config` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="py" config="./config.toml">
|
||||||
|
from pyscript import display
|
||||||
|
|
||||||
|
display("Hello PyScript Next")
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTML Example
|
||||||
|
|
||||||
|
This is a complete reference to bootstrap *PyScript* in a HTML document.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pyscript/core/dist/core.css">
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@pyscript/core"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py">
|
||||||
|
from pyscript import document
|
||||||
|
|
||||||
|
document.body.textContent = "PyScript Next"
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Tag attributes API
|
||||||
|
|
||||||
|
Either `<script type="py">` or `<py-script>` can have zero, one or more attributes:
|
||||||
|
|
||||||
|
* **src** if defined, the content of the tag is ignored and the *Python* code in the file will be evaluated instead.
|
||||||
|
* **config** if defined, the code will be evaluated after the configuration has been parsed but this can also be directly *JSON* so that both `config='{"packages":["numpy"]}'` and `config="./config.json"`, or `config="./config.toml"`, would be valid options.
|
||||||
|
* **async** if present, it will run the *Python* code asynchronously.
|
||||||
|
* **worker** if present, it will not bootstrap *pyodide* on the main page, only on the worker file it points at, as in `<script type="py" worker="./worker.py"></script>`. Both `async` and `config` attributes are also available and used to bootstrap the worker as desired.
|
||||||
|
|
||||||
|
Please note that other [polyscript's attributes](https://pyscript.github.io/polyscript/#script-attributes) are available too but their usage is more advanced.
|
||||||
|
|
||||||
|
|
||||||
|
## JS Module API
|
||||||
|
|
||||||
|
The module itself is currently exporting the following utilities:
|
||||||
|
|
||||||
|
* **PyWorker**, which allows to bootstrap a *worker* with *pyodide* and the *pyscript* module available within the code. This callback accepts a file as argument, and an additional, and optional, `options` object literal, able to understand a `config`, which could also be directly a *JS* object literal instead of a JSON string or a file to point at, and `async` which if `true` will run the worker code with top level await enabled. Please note that the returned reference is exactly the same as [the polyscript's XWorker](https://pyscript.github.io/polyscript/#the-xworker-reference), exposing exact same utilities but granting on bootstrap all hooks are in place and the type is always *pyodide*.
|
||||||
|
* **hooks**, which allows plugins to define *ASAP* callbacks or strings that should be executed either in the main thread or the worker before, or after, the code has been executed.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { hooks } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
|
||||||
|
|
||||||
|
// example
|
||||||
|
hooks.onInterpreterReady.add((utils, element) => {
|
||||||
|
console.log(element, 'found', 'pyscript is ready');
|
||||||
|
});
|
||||||
|
|
||||||
|
// the hooks namespace
|
||||||
|
({
|
||||||
|
// each function is invoked before or after python gets executed
|
||||||
|
// via: callback(pyScriptUtils, currentElement)
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRun: new Set(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRunAync: new Set(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRun: new Set(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRunAsync: new Set(),
|
||||||
|
|
||||||
|
// each function is invoked once when PyScript is ready
|
||||||
|
// and for each element via: callback(pyScriptUtils, currentElement)
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onInterpreterReady: new Set(),
|
||||||
|
|
||||||
|
// each string is prepended or appended to the worker code
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunWorker: new Set(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunWorkerAsync: new Set(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunWorker: new Set(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunWorkerAsync: new Set(),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that a *worker* is a completely different environment and it's not possible, by specifications, to pass a callback to it, which is why worker sets are strings and not functions.
|
||||||
|
|
||||||
|
However, each worker string can use `from pyscript import x, y, z` as that will be available out of the box.
|
||||||
|
|
||||||
|
## PyScript Python API
|
||||||
|
|
||||||
|
The `pyscript` python package offers various utilities in either the main thread or the worker.
|
||||||
|
|
||||||
|
The commonly shared utilities are:
|
||||||
|
|
||||||
|
* **window** in both main and worker, refers to the actual main thread global window context. In classic PyScript that'd be the equivalent of `import js` in the main, which is still available in *PyScript Next*. However, to make code easily portable between main and workers, we decided to offer this named export but please note that in workers, this is still the *main* window, not the worker global context, which would be reachable instead still via `import js`.
|
||||||
|
* **document** in both main and worker, refers to the actual main page `document`. In classic PyScript, this is the equivalent of `from js import document` on the main thread, but this won't ever work in a worker because there is no `document` in there. Fear not though, *PyScript Next* `document` will instead work out of the box, still accessing the main document behind the scene, so that `from pyscript import document` is granted to work in both main and workers seamlessly.
|
||||||
|
* **display** in both main and worker, refers to the good old `display` utility except:
|
||||||
|
* in the *main* it automatically uses the current script `target` to display content
|
||||||
|
* in the *worker* it still needs to know *where* to display content using the `target="dom-id"` named argument, as workers don't get a default target attached
|
||||||
|
* in both main and worker, the `append=True` is the *default* behavior, which is inherited from the classic PyScript.
|
||||||
|
|
||||||
|
#### Extra main-only features
|
||||||
|
|
||||||
|
* **PyWorker** which allows Python code to create a PyScript worker with the *pyscript* module pre-bundled. Please note that running PyScript on the main requires *pyodide* bootstrap, but also every worker requires *pyodide* bootstrap a part, as each worker is an environment / sandbox a part. This means that using *PyWorker* in the main will take, even if the main interpreter is already up and running, a bit of time to bootstrap the worker, also accordingly to the config files or packages in it.
|
||||||
|
|
||||||
|
|
||||||
|
#### Extra worker-only features
|
||||||
|
|
||||||
|
* **sync** which allows both main and the worker to seamlessly pass any serializable data around, without the need to convert Python dictionaries to JS object literals, as that's done automatically.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import { PyWorker } from "https://cdn.jsdelivr.net/npm/@pyscript/core";
|
||||||
|
|
||||||
|
const worker = PyWorker("./worker.py");
|
||||||
|
|
||||||
|
worker.sync.alert_message = message => {
|
||||||
|
alert(message);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyscript import sync
|
||||||
|
|
||||||
|
sync.alert_message("Hello Main!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Worker requirements
|
||||||
|
|
||||||
|
To make it possible to use what looks like *synchronous* DOM APIs, or any other API available via the `window` within a *worker*, we are using latest Web features such as [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics).
|
||||||
|
|
||||||
|
Without going into too many details, this means that the *SharedArrayBuffer* primitive must be available, and to do so, the server should enable the following headers:
|
||||||
|
|
||||||
|
```
|
||||||
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
|
Cross-Origin-Embedder-Policy: require-corp
|
||||||
|
Cross-Origin-Resource-Policy: cross-origin
|
||||||
|
```
|
||||||
|
|
||||||
|
These headers allow local files to be secured and yet able to load resources from the Web (i.e. pyodide library or its packages).
|
||||||
|
|
||||||
|
> ℹ️ **Careful**: we are using and testing these headers on both Desktop and Mobile to be sure all major browsers work as expected (Safari, Firefox, Chromium based browsers). If you change the value of these headers please be sure you test your target devices and browsers properly.
|
||||||
|
|
||||||
|
Please note that if you don't have control over your server's headers, it is possible to simply put [mini-coi](https://github.com/WebReflection/mini-coi#readme) script at the root of your *PyScript with Workers* enabled folder (site root, or any subfolder).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd project-folder
|
||||||
|
|
||||||
|
# grab mini-coi content and save it locally as mini-coi.js
|
||||||
|
curl -Ls https://unpkg.com/mini-coi -o mini-coi.js
|
||||||
|
```
|
||||||
|
|
||||||
|
With either these two solutions, it should be now possible to bootstrap a *PyScript Worker* without any issue.
|
||||||
|
|
||||||
|
#### mini-coi example
|
||||||
|
```html
|
||||||
|
<!doctype html>
|
||||||
|
<script src="/mini-coi.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import { PyWorker } from "https://unpkg.com/@pyscript/core";
|
||||||
|
PyWorker("./test.py");
|
||||||
|
</script>
|
||||||
|
<!-- ./test.py -->
|
||||||
|
<!--
|
||||||
|
from pyscript import document
|
||||||
|
document.body.textContent = "Hello PyScript Worker"
|
||||||
|
-->
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that a local or remote web server is still needed to allow the Service Worker and `python -m http.server` would do locally, *except* we need to reach `http://localhost:8000/`, not `http://0.0.0.0:8000/`, because the browser does not consider safe non localhost sites when the insecure `http://` protocol, instead of `https://`, is reached.
|
||||||
|
|
||||||
|
|
||||||
|
#### local server example
|
||||||
|
If you'd like to test locally these headers, without needing the *mini-coi* Service Worker, you can use various projects or, if you have *NodeJS* available, simply run the following command in the folder containing the site/project:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# bootstrap a local server with all headers needed
|
||||||
|
npx static-handler --cors --coep --coop --corp .
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### F.A.Q.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>why config attribute can also contain JSON but not TOML?</strong></summary>
|
||||||
|
<div markdown=1>
|
||||||
|
|
||||||
|
The *JSON* standard doesn't require new lines or indentation so it felt quick and desired to allow inline JSON as attribute content.
|
||||||
|
|
||||||
|
It's true that HTML attributes can be multi-line too, if properly embedded, but that looked too awkward and definitively harder to explain to me.
|
||||||
|
|
||||||
|
We might decide to allow TOML too in the future, but the direct config as attribute, instead of a proper file, or the usage of `<py-config>`, is meant for quick and simple packages or files dependencies and not much else.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>what are the worker's caveats?</strong></summary>
|
||||||
|
<div markdown=1>
|
||||||
|
|
||||||
|
When interacting with `window` or `document` it's important to understand that these use, behind the scene, an orchestrated [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) dance.
|
||||||
|
|
||||||
|
This means that some kind of data that cannot be passed around, specially not compatible with the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).
|
||||||
|
|
||||||
|
In short, please try to stick with *JS* references when passing along, or dealing with, *DOM* or other *APIs*.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
22
pyscript.core/eslint.config.mjs
Normal file
22
pyscript.core/eslint.config.mjs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import js from "@eslint/js";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
ignores: ["**/3rd-party/"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.es2021,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"no-implicit-globals": ["error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
1
pyscript.core/index.js
Normal file
1
pyscript.core/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./dist/core.js";
|
||||||
2
pyscript.core/jsdelivr.js
Normal file
2
pyscript.core/jsdelivr.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// @see https://github.com/jsdelivr/jsdelivr/issues/18528
|
||||||
|
export * from "./core/dist/core.js";
|
||||||
4152
pyscript.core/package-lock.json
generated
Normal file
4152
pyscript.core/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
84
pyscript.core/package.json
Normal file
84
pyscript.core/package.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"name": "@pyscript/core",
|
||||||
|
"version": "0.4.42",
|
||||||
|
"type": "module",
|
||||||
|
"description": "PyScript",
|
||||||
|
"module": "./index.js",
|
||||||
|
"unpkg": "./index.js",
|
||||||
|
"jsdelivr": "./jsdelivr.js",
|
||||||
|
"browser": "./index.js",
|
||||||
|
"main": "./index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./types/core.d.ts",
|
||||||
|
"import": "./src/core.js"
|
||||||
|
},
|
||||||
|
"./css": {
|
||||||
|
"import": "./dist/core.css"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"server": "npx static-handler --coi .",
|
||||||
|
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
|
||||||
|
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
|
||||||
|
"build:plugins": "node rollup/plugins.cjs",
|
||||||
|
"build:stdlib": "node rollup/stdlib.cjs",
|
||||||
|
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||||
|
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||||
|
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
|
||||||
|
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
|
||||||
|
"dev": "node dev.cjs",
|
||||||
|
"release": "npm run build && npm run zip",
|
||||||
|
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||||
|
"ts": "rm -rf types && tsc -p .",
|
||||||
|
"zip": "zip -r dist.zip ./dist"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"pyscript",
|
||||||
|
"core"
|
||||||
|
],
|
||||||
|
"author": "Anaconda Inc.",
|
||||||
|
"license": "APACHE-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ungap/with-resolvers": "^0.1.0",
|
||||||
|
"basic-devtools": "^0.1.6",
|
||||||
|
"polyscript": "^0.12.14",
|
||||||
|
"sticky-module": "^0.1.1",
|
||||||
|
"to-json-callback": "^0.1.1",
|
||||||
|
"type-checked-collections": "^0.1.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/commands": "^6.6.0",
|
||||||
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
|
"@codemirror/language": "^6.10.2",
|
||||||
|
"@codemirror/state": "^6.4.1",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@playwright/test": "^1.44.1",
|
||||||
|
"@rollup/plugin-commonjs": "^25.0.8",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
|
"@webreflection/toml-j0.4": "^1.1.3",
|
||||||
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
|
"bun": "^1.1.12",
|
||||||
|
"chokidar": "^3.6.0",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
|
"eslint": "^9.4.0",
|
||||||
|
"rollup": "^4.18.0",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
|
"static-handler": "^0.4.3",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-readline": "^1.1.1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/pyscript/pyscript.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/pyscript/pyscript/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/pyscript/pyscript#readme"
|
||||||
|
}
|
||||||
77
pyscript.core/rollup/3rd-party.cjs
Normal file
77
pyscript.core/rollup/3rd-party.cjs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const { copyFileSync, writeFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const CDN = "https://cdn.jsdelivr.net/npm";
|
||||||
|
|
||||||
|
const targets = join(__dirname, "..", "src", "3rd-party");
|
||||||
|
const node_modules = join(__dirname, "..", "node_modules");
|
||||||
|
|
||||||
|
const { devDependencies } = require(join(__dirname, "..", "package.json"));
|
||||||
|
|
||||||
|
const v = (name) => devDependencies[name].replace(/[^\d.]/g, "");
|
||||||
|
|
||||||
|
const dropSourceMap = (str) =>
|
||||||
|
str.replace(/^\/.+? sourceMappingURL=\/.+$/m, "");
|
||||||
|
|
||||||
|
// Fetch a module via jsdelivr CDN `/+esm` orchestration
|
||||||
|
// then sanitize the resulting outcome to avoid importing
|
||||||
|
// anything via `/npm/...` through Rollup
|
||||||
|
const resolve = (name) => {
|
||||||
|
const cdn = `${CDN}/${name}@${v(name)}/+esm`;
|
||||||
|
console.debug("fetching", cdn);
|
||||||
|
return fetch(cdn)
|
||||||
|
.then((b) => b.text())
|
||||||
|
.then((text) =>
|
||||||
|
text.replace(
|
||||||
|
/("|')\/npm\/(.+)?\+esm\1/g,
|
||||||
|
// normalize `/npm/module@version/+esm` as
|
||||||
|
// just `module` so that rollup can do the rest
|
||||||
|
(_, quote, module) => {
|
||||||
|
const i = module.lastIndexOf("@");
|
||||||
|
return `${quote}${module.slice(0, i)}${quote}`;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a file rollup can then process and understand
|
||||||
|
const reBundle = (name) => Promise.resolve(`export * from "${name}";\n`);
|
||||||
|
|
||||||
|
// key/value pairs as:
|
||||||
|
// "3rd-party/file-name.js"
|
||||||
|
// string as content or
|
||||||
|
// Promise<string> as resolved content
|
||||||
|
const modules = {
|
||||||
|
// toml
|
||||||
|
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||||
|
|
||||||
|
// xterm
|
||||||
|
"xterm.js": resolve("xterm"),
|
||||||
|
"xterm-readline.js": resolve("xterm-readline"),
|
||||||
|
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||||
|
b.text(),
|
||||||
|
),
|
||||||
|
"xterm_addon-web-links.js": fetch(
|
||||||
|
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||||
|
).then((b) => b.text()),
|
||||||
|
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||||
|
(b) => b.text(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// codemirror
|
||||||
|
"codemirror.js": reBundle("codemirror"),
|
||||||
|
"codemirror_state.js": reBundle("@codemirror/state"),
|
||||||
|
"codemirror_lang-python.js": reBundle("@codemirror/lang-python"),
|
||||||
|
"codemirror_language.js": reBundle("@codemirror/language"),
|
||||||
|
"codemirror_view.js": reBundle("@codemirror/view"),
|
||||||
|
"codemirror_commands.js": reBundle("@codemirror/commands"),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [target, source] of Object.entries(modules)) {
|
||||||
|
if (typeof source === "string") copyFileSync(source, join(targets, target));
|
||||||
|
else {
|
||||||
|
source.then((text) =>
|
||||||
|
writeFileSync(join(targets, target), dropSourceMap(text)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
pyscript.core/rollup/core.config.js
Normal file
43
pyscript.core/rollup/core.config.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// This file generates /core.js minified version of the module, which is
|
||||||
|
// the default exported as npm entry.
|
||||||
|
|
||||||
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
||||||
|
import commonjs from "@rollup/plugin-commonjs";
|
||||||
|
import terser from "@rollup/plugin-terser";
|
||||||
|
import postcss from "rollup-plugin-postcss";
|
||||||
|
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
input: "./src/core.js",
|
||||||
|
plugins: plugins.concat(
|
||||||
|
process.env.NO_MIN
|
||||||
|
? [nodeResolve(), commonjs()]
|
||||||
|
: [nodeResolve(), commonjs(), terser()],
|
||||||
|
),
|
||||||
|
output: {
|
||||||
|
esModule: true,
|
||||||
|
dir: "./dist",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "./src/core.css",
|
||||||
|
plugins: [
|
||||||
|
postcss({
|
||||||
|
extract: true,
|
||||||
|
sourceMap: false,
|
||||||
|
minimize: !process.env.NO_MIN,
|
||||||
|
plugins: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
file: "./dist/core.css",
|
||||||
|
},
|
||||||
|
onwarn(warning, warn) {
|
||||||
|
if (warning.code === "FILE_NAME_CONFLICT") return;
|
||||||
|
warn(warning);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
28
pyscript.core/rollup/plugins.cjs
Normal file
28
pyscript.core/rollup/plugins.cjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { readdirSync, writeFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const plugins = [""];
|
||||||
|
|
||||||
|
for (const file of readdirSync(join(__dirname, "..", "src", "plugins"))) {
|
||||||
|
if (/\.js$/.test(file)) {
|
||||||
|
const name = file.slice(0, -3);
|
||||||
|
const key = /^[a-zA-Z0-9$_]+$/.test(name)
|
||||||
|
? name
|
||||||
|
: `[${JSON.stringify(name)}]`;
|
||||||
|
const value = JSON.stringify(`./plugins/${file}`);
|
||||||
|
plugins.push(
|
||||||
|
// this comment is needed to avoid bundlers eagerly embedding lazy
|
||||||
|
// dependencies, causing all sort of issues once in production
|
||||||
|
` ${key}: () => import(/* webpackIgnore: true */ ${value}),`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.push("");
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(__dirname, "..", "src", "plugins.js"),
|
||||||
|
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default {${plugins.join(
|
||||||
|
"\n",
|
||||||
|
)}};\n`,
|
||||||
|
);
|
||||||
29
pyscript.core/rollup/stdlib.cjs
Normal file
29
pyscript.core/rollup/stdlib.cjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
const {
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const crawl = (path, json) => {
|
||||||
|
for (const file of readdirSync(path)) {
|
||||||
|
const full = join(path, file);
|
||||||
|
if (/\.py$/.test(file)) json[file] = readFileSync(full).toString();
|
||||||
|
else if (statSync(full).isDirectory() && !file.endsWith("_"))
|
||||||
|
crawl(full, (json[file] = {}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const json = {};
|
||||||
|
|
||||||
|
crawl(join(__dirname, "..", "src", "stdlib"), json);
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(__dirname, "..", "src", "stdlib", "pyscript.js"),
|
||||||
|
`// ⚠️ This file is an artifact: DO NOT MODIFY\nexport default ${JSON.stringify(
|
||||||
|
json,
|
||||||
|
null,
|
||||||
|
" ",
|
||||||
|
)};\n`,
|
||||||
|
);
|
||||||
7
pyscript.core/src/3rd-party/README.md
vendored
Normal file
7
pyscript.core/src/3rd-party/README.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# PyScript 3rd Party
|
||||||
|
|
||||||
|
This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-party.cjs).
|
||||||
|
|
||||||
|
As we would like to offer a way to run PyScript offline, and we already offer a `dist` folder with all the necessary scripts, we have created a foreign dependencies resolver that allow to lazy-load CDN dependencies out of the box.
|
||||||
|
|
||||||
|
Please **note** these dependencies are **not interpreters**, because interpreters have their own mechanism, folders structure, WASM files, and whatnot, to work locally, but at least XTerm or the TOML parser, among other lazy dependencies, should be available within the dist folder.
|
||||||
17
pyscript.core/src/all-done.js
Normal file
17
pyscript.core/src/all-done.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import TYPES from "./types.js";
|
||||||
|
|
||||||
|
const waitForIt = [];
|
||||||
|
|
||||||
|
for (const [TYPE] of TYPES) {
|
||||||
|
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
|
||||||
|
for (const element of document.querySelectorAll(selectors.join(","))) {
|
||||||
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
|
waitForIt.push(promise);
|
||||||
|
element.addEventListener(`${TYPE}:done`, resolve, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all the things then cleanup
|
||||||
|
Promise.all(waitForIt).then(() => {
|
||||||
|
dispatchEvent(new Event("py:all-done"));
|
||||||
|
});
|
||||||
156
pyscript.core/src/config.js
Normal file
156
pyscript.core/src/config.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* This file parses a generic <py-config> or config attribute
|
||||||
|
* to use as base config for all py-script elements, importing
|
||||||
|
* also a queue of plugins *before* the interpreter (if any) resolves.
|
||||||
|
*/
|
||||||
|
import { $$ } from "basic-devtools";
|
||||||
|
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
import allPlugins from "./plugins.js";
|
||||||
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
|
import { ErrorCode } from "./exceptions.js";
|
||||||
|
|
||||||
|
const { BAD_CONFIG, CONFLICTING_CODE } = ErrorCode;
|
||||||
|
|
||||||
|
const badURL = (url, expected = "") => {
|
||||||
|
let message = `(${BAD_CONFIG}): Invalid URL: ${url}`;
|
||||||
|
if (expected) message += `\nexpected ${expected} content`;
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string, returns its trimmed content as text,
|
||||||
|
* fetching it from a file if the content is a URL.
|
||||||
|
* @param {string} config either JSON, TOML, or a file to fetch
|
||||||
|
* @param {string?} type the optional type to enforce
|
||||||
|
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||||
|
*/
|
||||||
|
const configDetails = async (config, type) => {
|
||||||
|
let text = config?.trim();
|
||||||
|
// we only support an object as root config
|
||||||
|
let url = "",
|
||||||
|
toml = false,
|
||||||
|
json = /^{/.test(text) && /}$/.test(text);
|
||||||
|
// handle files by extension (relaxing urls parts after)
|
||||||
|
if (!json && /\.(\w+)(?:\?\S*)?$/.test(text)) {
|
||||||
|
const ext = RegExp.$1;
|
||||||
|
if (ext === "json" && type !== "toml") json = true;
|
||||||
|
else if (ext === "toml" && type !== "json") toml = true;
|
||||||
|
else badURL(text, type);
|
||||||
|
url = text;
|
||||||
|
text = (await fetch(url).then(getText)).trim();
|
||||||
|
}
|
||||||
|
return { json, toml: toml || (!json && !!text), text, url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
|
||||||
|
|
||||||
|
const syntaxError = (type, url, { message }) => {
|
||||||
|
let str = `(${BAD_CONFIG}): Invalid ${type}`;
|
||||||
|
if (url) str += ` @ ${url}`;
|
||||||
|
return new SyntaxError(`${str}\n${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
|
for (const [TYPE] of TYPES) {
|
||||||
|
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
|
||||||
|
let plugins;
|
||||||
|
|
||||||
|
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||||
|
let parsed;
|
||||||
|
|
||||||
|
/** @type {Error | undefined} The error thrown when parsing the PyScript config, if any.*/
|
||||||
|
let error;
|
||||||
|
|
||||||
|
/** @type {string | undefined} The `configURL` field to normalize all config operations as opposite of guessing it once resolved */
|
||||||
|
let configURL;
|
||||||
|
|
||||||
|
let config,
|
||||||
|
type,
|
||||||
|
pyElement,
|
||||||
|
pyConfigs = $$(`${TYPE}-config`),
|
||||||
|
attrConfigs = $$(
|
||||||
|
[
|
||||||
|
`script[type="${TYPE}"][config]:not([worker])`,
|
||||||
|
`${TYPE}-script[config]:not([worker])`,
|
||||||
|
].join(","),
|
||||||
|
);
|
||||||
|
|
||||||
|
// throw an error if there are multiple <py-config> or <mpy-config>
|
||||||
|
if (pyConfigs.length > 1) {
|
||||||
|
error = conflictError(`Too many ${TYPE}-config`);
|
||||||
|
} else {
|
||||||
|
// throw an error if there are <x-config> and config="x" attributes
|
||||||
|
if (pyConfigs.length && attrConfigs.length) {
|
||||||
|
error = conflictError(
|
||||||
|
`Ambiguous ${TYPE}-config VS config attribute`,
|
||||||
|
);
|
||||||
|
} else if (pyConfigs.length) {
|
||||||
|
[pyElement] = pyConfigs;
|
||||||
|
config = pyElement.getAttribute("src") || pyElement.textContent;
|
||||||
|
type = pyElement.getAttribute("type");
|
||||||
|
} else if (attrConfigs.length) {
|
||||||
|
[pyElement, ...attrConfigs] = attrConfigs;
|
||||||
|
config = pyElement.getAttribute("config");
|
||||||
|
// throw an error if dirrent scripts use different configs
|
||||||
|
if (
|
||||||
|
attrConfigs.some((el) => el.getAttribute("config") !== config)
|
||||||
|
) {
|
||||||
|
error = conflictError(
|
||||||
|
"Unable to use different configs on main",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catch possible fetch errors
|
||||||
|
if (!error && config) {
|
||||||
|
try {
|
||||||
|
const { json, toml, text, url } = await configDetails(config, type);
|
||||||
|
if (url) configURL = new URL(url, location.href).href;
|
||||||
|
config = text;
|
||||||
|
if (json || type === "json") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("JSON", url, e);
|
||||||
|
}
|
||||||
|
} else if (toml || type === "toml") {
|
||||||
|
try {
|
||||||
|
const { parse } = await import(
|
||||||
|
/* webpackIgnore: true */ "./3rd-party/toml.js"
|
||||||
|
);
|
||||||
|
parsed = parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
error = syntaxError("TOML", url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse all plugins and optionally ignore only
|
||||||
|
// those flagged as "undesired" via `!` prefix
|
||||||
|
const toBeAwaited = [];
|
||||||
|
for (const [key, value] of Object.entries(allPlugins)) {
|
||||||
|
if (error) {
|
||||||
|
if (key === "error") {
|
||||||
|
// show on page the config is broken, meaning that
|
||||||
|
// it was not possible to disable error plugin neither
|
||||||
|
// as that part wasn't correctly parsed anyway
|
||||||
|
value().then(({ notify }) => notify(error.message));
|
||||||
|
}
|
||||||
|
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||||
|
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign plugins as Promise.all only if needed
|
||||||
|
plugins = Promise.all(toBeAwaited);
|
||||||
|
|
||||||
|
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default configs;
|
||||||
75
pyscript.core/src/core.css
Normal file
75
pyscript.core/src/core.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
py-script,
|
||||||
|
py-config,
|
||||||
|
mpy-script,
|
||||||
|
mpy-config {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PyEditor */
|
||||||
|
.py-editor-box,
|
||||||
|
.mpy-editor-box {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.py-editor-input,
|
||||||
|
.mpy-editor-input {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.py-editor-box::before,
|
||||||
|
.mpy-editor-box::before {
|
||||||
|
content: attr(data-env);
|
||||||
|
display: block;
|
||||||
|
font-size: x-small;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
.py-editor-output,
|
||||||
|
.mpy-editor-output {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.py-editor-run-button,
|
||||||
|
.mpy-editor-run-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.py-editor-box:hover .py-editor-run-button,
|
||||||
|
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||||
|
.py-editor-run-button:focus,
|
||||||
|
.py-editor-run-button:disabled,
|
||||||
|
.mpy-editor-run-button:focus,
|
||||||
|
.mpy-editor-run-button:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled > *,
|
||||||
|
.mpy-editor-run-button:disabled > * {
|
||||||
|
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled,
|
||||||
|
.mpy-editor-run-button:disabled {
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
.py-editor-run-button:disabled::before,
|
||||||
|
.mpy-editor-run-button:disabled::before {
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 100%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
|
||||||
|
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #aaa;
|
||||||
|
border-top-color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
animation: spinner 0.6s linear infinite;
|
||||||
|
}
|
||||||
358
pyscript.core/src/core.js
Normal file
358
pyscript.core/src/core.js
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
/*! (c) PyScript Development Team */
|
||||||
|
|
||||||
|
import stickyModule from "sticky-module";
|
||||||
|
import "@ungap/with-resolvers";
|
||||||
|
|
||||||
|
import {
|
||||||
|
INVALID_CONTENT,
|
||||||
|
Hook,
|
||||||
|
XWorker,
|
||||||
|
assign,
|
||||||
|
dedent,
|
||||||
|
define,
|
||||||
|
defineProperty,
|
||||||
|
dispatch,
|
||||||
|
queryTarget,
|
||||||
|
unescape,
|
||||||
|
whenDefined,
|
||||||
|
} from "polyscript/exports";
|
||||||
|
|
||||||
|
import "./all-done.js";
|
||||||
|
import TYPES from "./types.js";
|
||||||
|
import configs from "./config.js";
|
||||||
|
import sync from "./sync.js";
|
||||||
|
import bootstrapNodeAndPlugins from "./plugins-helper.js";
|
||||||
|
import { ErrorCode } from "./exceptions.js";
|
||||||
|
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||||
|
import {
|
||||||
|
hooks,
|
||||||
|
main,
|
||||||
|
worker,
|
||||||
|
codeFor,
|
||||||
|
createFunction,
|
||||||
|
inputFailure,
|
||||||
|
} from "./hooks.js";
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
export { stdlib, optional, inputFailure };
|
||||||
|
|
||||||
|
// generic helper to disambiguate between custom element and script
|
||||||
|
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||||
|
|
||||||
|
// Used to create either Pyodide or MicroPython workers
|
||||||
|
// with the PyScript module available within the code
|
||||||
|
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
|
||||||
|
([TYPE, interpreter]) =>
|
||||||
|
/**
|
||||||
|
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
|
||||||
|
* @param {string} file the python file to run ina worker.
|
||||||
|
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
|
||||||
|
* @returns {Promise<Worker & {sync: object}>}
|
||||||
|
*/
|
||||||
|
async function PyScriptWorker(file, options) {
|
||||||
|
await configs.get(TYPE).plugins;
|
||||||
|
const xworker = XWorker.call(
|
||||||
|
new Hook(null, hooked.get(TYPE)),
|
||||||
|
file,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
type: interpreter,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assign(xworker.sync, sync);
|
||||||
|
return xworker.ready;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// avoid multiple initialization of the same library
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
PyWorker: exportedPyWorker,
|
||||||
|
MPWorker: exportedMPWorker,
|
||||||
|
hooks: exportedHooks,
|
||||||
|
config: exportedConfig,
|
||||||
|
whenDefined: exportedWhenDefined,
|
||||||
|
},
|
||||||
|
alreadyLive,
|
||||||
|
] = stickyModule("@pyscript/core", {
|
||||||
|
PyWorker,
|
||||||
|
MPWorker,
|
||||||
|
hooks,
|
||||||
|
config: {},
|
||||||
|
whenDefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
TYPES,
|
||||||
|
exportedPyWorker as PyWorker,
|
||||||
|
exportedMPWorker as MPWorker,
|
||||||
|
exportedHooks as hooks,
|
||||||
|
exportedConfig as config,
|
||||||
|
exportedWhenDefined as whenDefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const offline_interpreter = (config) =>
|
||||||
|
config?.interpreter && new URL(config.interpreter, location.href).href;
|
||||||
|
|
||||||
|
const hooked = new Map();
|
||||||
|
|
||||||
|
for (const [TYPE, interpreter] of TYPES) {
|
||||||
|
// avoid any dance if the module already landed
|
||||||
|
if (alreadyLive) break;
|
||||||
|
|
||||||
|
const dispatchDone = (element, isAsync, result) => {
|
||||||
|
if (isAsync) result.then(() => dispatch(element, TYPE, "done"));
|
||||||
|
else dispatch(element, TYPE, "done");
|
||||||
|
};
|
||||||
|
|
||||||
|
const { config, configURL, plugins, error } = configs.get(TYPE);
|
||||||
|
|
||||||
|
// create a unique identifier when/if needed
|
||||||
|
let id = 0;
|
||||||
|
const getID = (prefix = TYPE) => `${prefix}-${id++}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
|
||||||
|
* It either throws an error if the 'src' can't be fetched or it returns a fallback
|
||||||
|
* content as source.
|
||||||
|
*/
|
||||||
|
const fetchSource = async (tag, io, asText) => {
|
||||||
|
if (tag.hasAttribute("src")) {
|
||||||
|
try {
|
||||||
|
return await fetch(tag.getAttribute("src")).then(getText);
|
||||||
|
} catch (error) {
|
||||||
|
io.stderr(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asText) return dedent(tag.textContent);
|
||||||
|
|
||||||
|
const code = dedent(unescape(tag.innerHTML));
|
||||||
|
console.warn(
|
||||||
|
`Deprecated: use <script type="${TYPE}"> for an always safe content parsing:\n`,
|
||||||
|
code,
|
||||||
|
);
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
|
||||||
|
// register once any interpreter
|
||||||
|
let alreadyRegistered = false;
|
||||||
|
|
||||||
|
// allows lazy element features on code evaluation
|
||||||
|
let currentElement;
|
||||||
|
|
||||||
|
const registerModule = ({ XWorker, interpreter, io }) => {
|
||||||
|
// avoid multiple registration of the same interpreter
|
||||||
|
if (alreadyRegistered) return;
|
||||||
|
alreadyRegistered = true;
|
||||||
|
|
||||||
|
// automatically use the pyscript stderr (when/if defined)
|
||||||
|
// this defaults to console.error
|
||||||
|
function PyWorker(...args) {
|
||||||
|
const worker = XWorker(...args);
|
||||||
|
worker.onerror = ({ error }) => io.stderr(error);
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich the Python env with some JS utility for main
|
||||||
|
interpreter.registerJsModule("_pyscript", {
|
||||||
|
PyWorker,
|
||||||
|
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||||
|
get target() {
|
||||||
|
return isScript(currentElement)
|
||||||
|
? currentElement.target.id
|
||||||
|
: currentElement.id;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// define the module as both `<script type="py">` and `<py-script>`
|
||||||
|
// but only if the config didn't throw an error
|
||||||
|
if (!error) {
|
||||||
|
// ensure plugins are bootstrapped already before custom type definition
|
||||||
|
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||||
|
// from core.js itself so that custom definition should not be blocking.
|
||||||
|
plugins.then(() => {
|
||||||
|
// possible early errors sent by polyscript
|
||||||
|
const errors = new Map();
|
||||||
|
|
||||||
|
// specific main and worker hooks
|
||||||
|
const hooks = {
|
||||||
|
main: {
|
||||||
|
...codeFor(main, TYPE),
|
||||||
|
async onReady(wrap, element) {
|
||||||
|
registerModule(wrap);
|
||||||
|
|
||||||
|
// allows plugins to do whatever they want with the element
|
||||||
|
// before regular stuff happens in here
|
||||||
|
for (const callback of main("onReady"))
|
||||||
|
await callback(wrap, element);
|
||||||
|
|
||||||
|
// now that all possible plugins are configured,
|
||||||
|
// bail out if polyscript encountered an error
|
||||||
|
if (errors.has(element)) {
|
||||||
|
let { message } = errors.get(element);
|
||||||
|
errors.delete(element);
|
||||||
|
const clone = message === INVALID_CONTENT;
|
||||||
|
message = `(${ErrorCode.CONFLICTING_CODE}) ${message} for `;
|
||||||
|
message += element.cloneNode(clone).outerHTML;
|
||||||
|
wrap.io.stderr(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScript(element)) {
|
||||||
|
const {
|
||||||
|
attributes: { async: isAsync, target },
|
||||||
|
} = element;
|
||||||
|
const hasTarget = !!target?.value;
|
||||||
|
const show = hasTarget
|
||||||
|
? queryTarget(element, target.value)
|
||||||
|
: document.createElement("script-py");
|
||||||
|
|
||||||
|
if (!hasTarget) {
|
||||||
|
const { head, body } = document;
|
||||||
|
if (head.contains(element)) body.append(show);
|
||||||
|
else element.after(show);
|
||||||
|
}
|
||||||
|
if (!show.id) show.id = getID();
|
||||||
|
|
||||||
|
// allows the code to retrieve the target element via
|
||||||
|
// document.currentScript.target if needed
|
||||||
|
defineProperty(element, "target", { value: show });
|
||||||
|
|
||||||
|
// notify before the code runs
|
||||||
|
dispatch(element, TYPE, "ready");
|
||||||
|
dispatchDone(
|
||||||
|
element,
|
||||||
|
isAsync,
|
||||||
|
wrap[`run${isAsync ? "Async" : ""}`](
|
||||||
|
await fetchSource(element, wrap.io, true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// resolve PyScriptElement to allow connectedCallback
|
||||||
|
element._wrap.resolve(wrap);
|
||||||
|
}
|
||||||
|
console.debug("[pyscript/main] PyScript Ready");
|
||||||
|
},
|
||||||
|
onWorker(_, xworker) {
|
||||||
|
assign(xworker.sync, sync);
|
||||||
|
for (const callback of main("onWorker"))
|
||||||
|
callback(_, xworker);
|
||||||
|
},
|
||||||
|
onBeforeRun(wrap, element) {
|
||||||
|
currentElement = element;
|
||||||
|
bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onBeforeRun",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBeforeRunAsync(wrap, element) {
|
||||||
|
currentElement = element;
|
||||||
|
return bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onBeforeRunAsync",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAfterRun(wrap, element) {
|
||||||
|
bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onAfterRun",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAfterRunAsync(wrap, element) {
|
||||||
|
return bootstrapNodeAndPlugins(
|
||||||
|
main,
|
||||||
|
wrap,
|
||||||
|
element,
|
||||||
|
"onAfterRunAsync",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
...codeFor(worker, TYPE),
|
||||||
|
// these are lazy getters that returns a composition
|
||||||
|
// of the current hooks or undefined, if no hook is present
|
||||||
|
get onReady() {
|
||||||
|
return createFunction(this, "onReady", true);
|
||||||
|
},
|
||||||
|
get onBeforeRun() {
|
||||||
|
return createFunction(this, "onBeforeRun", false);
|
||||||
|
},
|
||||||
|
get onBeforeRunAsync() {
|
||||||
|
return createFunction(this, "onBeforeRunAsync", true);
|
||||||
|
},
|
||||||
|
get onAfterRun() {
|
||||||
|
return createFunction(this, "onAfterRun", false);
|
||||||
|
},
|
||||||
|
get onAfterRunAsync() {
|
||||||
|
return createFunction(this, "onAfterRunAsync", true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
hooked.set(TYPE, hooks);
|
||||||
|
|
||||||
|
define(TYPE, {
|
||||||
|
config,
|
||||||
|
configURL,
|
||||||
|
interpreter,
|
||||||
|
hooks,
|
||||||
|
env: `${TYPE}-script`,
|
||||||
|
version: offline_interpreter(config),
|
||||||
|
onerror(error, element) {
|
||||||
|
errors.set(element, error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
`${TYPE}-script`,
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
assign(super(), {
|
||||||
|
_wrap: Promise.withResolvers(),
|
||||||
|
srcCode: "",
|
||||||
|
executed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get id() {
|
||||||
|
return super.id || (super.id = getID());
|
||||||
|
}
|
||||||
|
set id(value) {
|
||||||
|
super.id = value;
|
||||||
|
}
|
||||||
|
async connectedCallback() {
|
||||||
|
if (!this.executed) {
|
||||||
|
this.executed = true;
|
||||||
|
const isAsync = this.hasAttribute("async");
|
||||||
|
const { io, run, runAsync } = await this._wrap
|
||||||
|
.promise;
|
||||||
|
this.srcCode = await fetchSource(
|
||||||
|
this,
|
||||||
|
io,
|
||||||
|
!this.childElementCount,
|
||||||
|
);
|
||||||
|
this.replaceChildren();
|
||||||
|
this.style.display = "block";
|
||||||
|
dispatch(this, TYPE, "ready");
|
||||||
|
dispatchDone(
|
||||||
|
this,
|
||||||
|
isAsync,
|
||||||
|
(isAsync ? runAsync : run)(this.srcCode),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export the used config without allowing leaks through it
|
||||||
|
exportedConfig[TYPE] = structuredClone(config);
|
||||||
|
}
|
||||||
109
pyscript.core/src/exceptions.js
Normal file
109
pyscript.core/src/exceptions.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { assign } from "polyscript/exports";
|
||||||
|
|
||||||
|
const CLOSEBUTTON =
|
||||||
|
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='currentColor' width='12px'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These error codes are used to identify the type of error that occurred.
|
||||||
|
* @see https://pyscript.github.io/docs/latest/reference/exceptions.html?highlight=errors
|
||||||
|
*/
|
||||||
|
export const ErrorCode = {
|
||||||
|
GENERIC: "PY0000", // Use this only for development then change to a more specific error code
|
||||||
|
CONFLICTING_CODE: "PY0409",
|
||||||
|
BAD_CONFIG: "PY1000",
|
||||||
|
MICROPIP_INSTALL_ERROR: "PY1001",
|
||||||
|
BAD_PLUGIN_FILE_EXTENSION: "PY2000",
|
||||||
|
NO_DEFAULT_EXPORT: "PY2001",
|
||||||
|
TOP_LEVEL_AWAIT: "PY9000",
|
||||||
|
// Currently these are created depending on error code received from fetching
|
||||||
|
FETCH_ERROR: "PY0001",
|
||||||
|
FETCH_NAME_ERROR: "PY0002",
|
||||||
|
FETCH_UNAUTHORIZED_ERROR: "PY0401",
|
||||||
|
FETCH_FORBIDDEN_ERROR: "PY0403",
|
||||||
|
FETCH_NOT_FOUND_ERROR: "PY0404",
|
||||||
|
FETCH_SERVER_ERROR: "PY0500",
|
||||||
|
FETCH_UNAVAILABLE_ERROR: "PY0503",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys of the ErrorCode object
|
||||||
|
* @typedef {keyof ErrorCode} ErrorCodes
|
||||||
|
* */
|
||||||
|
|
||||||
|
export class UserError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* @param {string} messageType
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message = "", messageType = "text") {
|
||||||
|
super(`(${errorCode}): ${message}`);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.messageType = messageType;
|
||||||
|
this.name = "UserError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FetchError extends UserError {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message) {
|
||||||
|
super(errorCode, message);
|
||||||
|
this.name = "FetchError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstallError extends UserError {
|
||||||
|
/**
|
||||||
|
* @param {ErrorCodes} errorCode
|
||||||
|
* @param {string} message
|
||||||
|
* */
|
||||||
|
constructor(errorCode, message) {
|
||||||
|
super(errorCode, message);
|
||||||
|
this.name = "InstallError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal function for creating alert banners on the page
|
||||||
|
* @param {string} message The message to be displayed to the user
|
||||||
|
* @param {string} level The alert level of the message. Can be any string; 'error' or 'warning' cause matching messages to be emitted to the console
|
||||||
|
* @param {string} [messageType="text"] If set to "html", the message content will be assigned to the banner's innerHTML directly, instead of its textContent
|
||||||
|
* @param {any} [logMessage=true] An additional flag for whether the message should be sent to the console log.
|
||||||
|
*/
|
||||||
|
export function _createAlertBanner(
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
messageType = "text",
|
||||||
|
logMessage = true,
|
||||||
|
) {
|
||||||
|
switch (`log-${level}-${logMessage}`) {
|
||||||
|
case "log-error-true":
|
||||||
|
console.error(message);
|
||||||
|
break;
|
||||||
|
case "log-warning-true":
|
||||||
|
console.warn(message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = messageType === "html" ? "innerHTML" : "textContent";
|
||||||
|
const banner = assign(document.createElement("div"), {
|
||||||
|
className: `alert-banner py-${level}`,
|
||||||
|
[content]: message,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (level === "warning") {
|
||||||
|
const closeButton = assign(document.createElement("button"), {
|
||||||
|
id: "alert-close-button",
|
||||||
|
innerHTML: CLOSEBUTTON,
|
||||||
|
});
|
||||||
|
|
||||||
|
banner.appendChild(closeButton).addEventListener("click", () => {
|
||||||
|
banner.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.prepend(banner);
|
||||||
|
}
|
||||||
69
pyscript.core/src/fetch.js
Normal file
69
pyscript.core/src/fetch.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { FetchError, ErrorCode } from "./exceptions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Response} response
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getText = (response) => response.text();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a fetch wrapper that handles any non 200 responses and throws a
|
||||||
|
* FetchError with the right ErrorCode. This is useful because our FetchError
|
||||||
|
* will automatically create an alert banner.
|
||||||
|
*
|
||||||
|
* @param {string} url - URL to fetch
|
||||||
|
* @param {Request} [options] - options to pass to fetch
|
||||||
|
* @returns {Promise<Response>}
|
||||||
|
*/
|
||||||
|
export async function robustFetch(url, options) {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
// Note: We need to wrap fetch into a try/catch block because fetch
|
||||||
|
// throws a TypeError if the URL is invalid such as http://blah.blah
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
const error = err;
|
||||||
|
let errMsg;
|
||||||
|
if (url.startsWith("http")) {
|
||||||
|
errMsg =
|
||||||
|
`Fetching from URL ${url} failed with error ` +
|
||||||
|
`'${error.message}'. Are your filename and path correct?`;
|
||||||
|
} else {
|
||||||
|
errMsg = `Polyscript: Access to local files
|
||||||
|
(using [[fetch]] configurations in <py-config>)
|
||||||
|
is not available when directly opening a HTML file;
|
||||||
|
you must use a webserver to serve the additional files.
|
||||||
|
See <a style="text-decoration: underline;" href="https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062">this reference</a>
|
||||||
|
on starting a simple webserver with Python.
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
throw new FetchError(ErrorCode.FETCH_ERROR, errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that response.ok is true for 200-299 responses
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorMsg = `Fetching from URL ${url} failed with error ${response.status} (${response.statusText}). Are your filename and path correct?`;
|
||||||
|
switch (response.status) {
|
||||||
|
case 404:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_NOT_FOUND_ERROR, errorMsg);
|
||||||
|
case 401:
|
||||||
|
throw new FetchError(
|
||||||
|
ErrorCode.FETCH_UNAUTHORIZED_ERROR,
|
||||||
|
errorMsg,
|
||||||
|
);
|
||||||
|
case 403:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_FORBIDDEN_ERROR, errorMsg);
|
||||||
|
case 500:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_SERVER_ERROR, errorMsg);
|
||||||
|
case 503:
|
||||||
|
throw new FetchError(
|
||||||
|
ErrorCode.FETCH_UNAVAILABLE_ERROR,
|
||||||
|
errorMsg,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new FetchError(ErrorCode.FETCH_ERROR, errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
105
pyscript.core/src/hooks.js
Normal file
105
pyscript.core/src/hooks.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { typedSet } from "type-checked-collections";
|
||||||
|
import { dedent } from "polyscript/exports";
|
||||||
|
import toJSONCallback from "to-json-callback";
|
||||||
|
|
||||||
|
import { stdlib, optional } from "./stdlib.js";
|
||||||
|
|
||||||
|
export const main = (name) => hooks.main[name];
|
||||||
|
export const worker = (name) => hooks.worker[name];
|
||||||
|
|
||||||
|
const code = (hooks, branch, key, lib) => {
|
||||||
|
hooks[key] = () => {
|
||||||
|
const arr = lib ? [lib] : [];
|
||||||
|
arr.push(...branch(key));
|
||||||
|
return arr.map(dedent).join("\n");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const codeFor = (branch, type) => {
|
||||||
|
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
|
||||||
|
const hooks = {};
|
||||||
|
code(hooks, branch, `codeBeforeRun`, pylib);
|
||||||
|
code(hooks, branch, `codeBeforeRunAsync`, pylib);
|
||||||
|
code(hooks, branch, `codeAfterRun`);
|
||||||
|
code(hooks, branch, `codeAfterRunAsync`);
|
||||||
|
return hooks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFunction = (self, name) => {
|
||||||
|
const cbs = [...worker(name)];
|
||||||
|
if (cbs.length) {
|
||||||
|
const cb = toJSONCallback(
|
||||||
|
self[`_${name}`] ||
|
||||||
|
(name.endsWith("Async")
|
||||||
|
? async (wrap, xworker, ...cbs) => {
|
||||||
|
for (const cb of cbs) await cb(wrap, xworker);
|
||||||
|
}
|
||||||
|
: (wrap, xworker, ...cbs) => {
|
||||||
|
for (const cb of cbs) cb(wrap, xworker);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const a = cbs.map(toJSONCallback).join(", ");
|
||||||
|
return Function(`return(w,x)=>(${cb})(w,x,...[${a}])`)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const SetFunction = typedSet({ typeof: "function" });
|
||||||
|
const SetString = typedSet({ typeof: "string" });
|
||||||
|
|
||||||
|
export const inputFailure = `
|
||||||
|
import builtins
|
||||||
|
def input(prompt=""):
|
||||||
|
raise Exception("\\n ".join([
|
||||||
|
"input() doesn't work when PyScript runs in the main thread.",
|
||||||
|
"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/"
|
||||||
|
]))
|
||||||
|
|
||||||
|
builtins.input = input
|
||||||
|
del builtins
|
||||||
|
del input
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const hooks = {
|
||||||
|
main: {
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onWorker: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onReady: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRun: new SetString([inputFailure]),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunAsync: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunAsync: new SetString(),
|
||||||
|
},
|
||||||
|
worker: {
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onReady: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onBeforeRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRun: new SetFunction(),
|
||||||
|
/** @type {Set<function>} */
|
||||||
|
onAfterRunAsync: new SetFunction(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeBeforeRunAsync: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRun: new SetString(),
|
||||||
|
/** @type {Set<string>} */
|
||||||
|
codeAfterRunAsync: new SetString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
26
pyscript.core/src/plugins-helper.js
Normal file
26
pyscript.core/src/plugins-helper.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineProperty } from "polyscript/exports";
|
||||||
|
|
||||||
|
// helper for all script[type="py"] out there
|
||||||
|
const before = (script) => {
|
||||||
|
defineProperty(document, "currentScript", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => script,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const after = () => {
|
||||||
|
delete document.currentScript;
|
||||||
|
};
|
||||||
|
|
||||||
|
// common life-cycle handlers for any node
|
||||||
|
export default async (main, wrap, element, hook) => {
|
||||||
|
const isAsync = hook.endsWith("Async");
|
||||||
|
const isBefore = hook.startsWith("onBefore");
|
||||||
|
// make it possible to reach the current target node via Python
|
||||||
|
// or clean up for other scripts executing around this one
|
||||||
|
(isBefore ? before : after)(element);
|
||||||
|
for (const fn of main(hook)) {
|
||||||
|
if (isAsync) await fn(wrap, element);
|
||||||
|
else fn(wrap, element);
|
||||||
|
}
|
||||||
|
};
|
||||||
27
pyscript.core/src/plugins/deprecations-manager.js
Normal file
27
pyscript.core/src/plugins/deprecations-manager.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// PyScript Derepcations Plugin
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
|
||||||
|
// react lazily on PyScript bootstrap
|
||||||
|
hooks.main.onReady.add(checkDeprecations);
|
||||||
|
hooks.main.onWorker.add(checkDeprecations);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that there are no scripts loading from pyscript.net/latest
|
||||||
|
*/
|
||||||
|
function checkDeprecations() {
|
||||||
|
const scripts = document.querySelectorAll("script");
|
||||||
|
for (const script of scripts) checkLoadingScriptsFromLatest(script.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if src being loaded from pyscript.net/latest and display a notification if true
|
||||||
|
* * @param {string} src
|
||||||
|
*/
|
||||||
|
function checkLoadingScriptsFromLatest(src) {
|
||||||
|
if (/\/pyscript\.net\/latest/.test(src)) {
|
||||||
|
notify(
|
||||||
|
"Loading scripts from latest is deprecated and will be removed soon. Please use a specific version instead.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
pyscript.core/src/plugins/error.js
Normal file
47
pyscript.core/src/plugins/error.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// PyScript Error Plugin
|
||||||
|
import { hooks } from "../core.js";
|
||||||
|
|
||||||
|
hooks.main.onReady.add(function override(pyScript) {
|
||||||
|
// be sure this override happens only once
|
||||||
|
hooks.main.onReady.delete(override);
|
||||||
|
|
||||||
|
// trap generic `stderr` to propagate to it regardless
|
||||||
|
const { stderr } = pyScript.io;
|
||||||
|
|
||||||
|
// override it with our own logic
|
||||||
|
pyScript.io.stderr = (error, ...rest) => {
|
||||||
|
notify(error.message || error);
|
||||||
|
// let other plugins or stderr hook, if any, do the rest
|
||||||
|
return stderr(error, ...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
// be sure uncaught Python errors are also visible
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error hook utilities
|
||||||
|
|
||||||
|
// Custom function to show notifications
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a banner to the top of the page, notifying the user of an error
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
export function notify(message) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "py-error";
|
||||||
|
div.textContent = message;
|
||||||
|
div.style.cssText = `
|
||||||
|
border: 1px solid red;
|
||||||
|
background: #ffdddd;
|
||||||
|
color: black;
|
||||||
|
font-family: courier, monospace;
|
||||||
|
white-space: pre;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
`;
|
||||||
|
document.body.append(div);
|
||||||
|
}
|
||||||
324
pyscript.core/src/plugins/py-editor.js
Normal file
324
pyscript.core/src/plugins/py-editor.js
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
// PyScript py-editor plugin
|
||||||
|
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||||
|
import { TYPES, offline_interpreter, stdlib } from "../core.js";
|
||||||
|
|
||||||
|
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||||
|
|
||||||
|
let id = 0;
|
||||||
|
const getID = (type) => `${type}-editor-${id++}`;
|
||||||
|
|
||||||
|
const envs = new Map();
|
||||||
|
const configs = new Map();
|
||||||
|
|
||||||
|
const hooks = {
|
||||||
|
worker: {
|
||||||
|
codeBeforeRun: () => stdlib,
|
||||||
|
// works on both Pyodide and MicroPython
|
||||||
|
onReady: ({ runAsync, io }, { sync }) => {
|
||||||
|
io.stdout = io.buffered(sync.write);
|
||||||
|
io.stderr = io.buffered(sync.writeErr);
|
||||||
|
sync.revoke();
|
||||||
|
sync.runAsync = runAsync;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function execute({ currentTarget }) {
|
||||||
|
const { env, pySrc, outDiv } = this;
|
||||||
|
const hasRunButton = !!currentTarget;
|
||||||
|
|
||||||
|
if (hasRunButton) {
|
||||||
|
currentTarget.disabled = true;
|
||||||
|
outDiv.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envs.has(env)) {
|
||||||
|
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||||
|
const details = { type: this.interpreter };
|
||||||
|
const { config } = this;
|
||||||
|
if (config) {
|
||||||
|
details.configURL = config;
|
||||||
|
const { parse } = config.endsWith(".toml")
|
||||||
|
? await import(/* webpackIgnore: true */ "../3rd-party/toml.js")
|
||||||
|
: JSON;
|
||||||
|
details.config = parse(await fetch(config).then((r) => r.text()));
|
||||||
|
details.version = offline_interpreter(details.config);
|
||||||
|
} else {
|
||||||
|
details.config = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
const { promise, resolve } = Promise.withResolvers();
|
||||||
|
envs.set(env, promise);
|
||||||
|
sync.revoke = () => {
|
||||||
|
URL.revokeObjectURL(srcLink);
|
||||||
|
resolve(xworker);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the env then set the target div
|
||||||
|
// before executing the current code
|
||||||
|
return envs.get(env).then((xworker) => {
|
||||||
|
xworker.onerror = ({ error }) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.innerHTML += `<span style='color:red'>${
|
||||||
|
error.message || error
|
||||||
|
}</span>\n`;
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const enable = () => {
|
||||||
|
if (hasRunButton) currentTarget.disabled = false;
|
||||||
|
};
|
||||||
|
const { sync } = xworker;
|
||||||
|
sync.write = (str) => {
|
||||||
|
if (hasRunButton) outDiv.innerText += `${str}\n`;
|
||||||
|
};
|
||||||
|
sync.writeErr = (str) => {
|
||||||
|
if (hasRunButton) {
|
||||||
|
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sync.runAsync(pySrc).then(enable, enable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeRunButton = (listener, type) => {
|
||||||
|
const runButton = document.createElement("button");
|
||||||
|
runButton.className = `absolute ${type}-editor-run-button`;
|
||||||
|
runButton.innerHTML = RUN_BUTTON;
|
||||||
|
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||||
|
runButton.addEventListener("click", listener);
|
||||||
|
return runButton;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEditorDiv = (listener, type) => {
|
||||||
|
const editorDiv = document.createElement("div");
|
||||||
|
editorDiv.className = `${type}-editor-input`;
|
||||||
|
editorDiv.setAttribute("aria-label", "Python Script Area");
|
||||||
|
|
||||||
|
const runButton = makeRunButton(listener, type);
|
||||||
|
const editorShadowContainer = document.createElement("div");
|
||||||
|
|
||||||
|
// avoid outer elements intercepting key events (reveal as example)
|
||||||
|
editorShadowContainer.addEventListener("keydown", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
editorDiv.append(runButton, editorShadowContainer);
|
||||||
|
|
||||||
|
return editorDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeOutDiv = (type) => {
|
||||||
|
const outDiv = document.createElement("div");
|
||||||
|
outDiv.className = `${type}-editor-output`;
|
||||||
|
outDiv.id = `${getID(type)}-output`;
|
||||||
|
return outDiv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeBoxDiv = (listener, type) => {
|
||||||
|
const boxDiv = document.createElement("div");
|
||||||
|
boxDiv.className = `${type}-editor-box`;
|
||||||
|
|
||||||
|
const editorDiv = makeEditorDiv(listener, type);
|
||||||
|
const outDiv = makeOutDiv(type);
|
||||||
|
boxDiv.append(editorDiv, outDiv);
|
||||||
|
|
||||||
|
return [boxDiv, outDiv];
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = async (script, type, interpreter) => {
|
||||||
|
const [
|
||||||
|
{ basicSetup, EditorView },
|
||||||
|
{ Compartment },
|
||||||
|
{ python },
|
||||||
|
{ indentUnit },
|
||||||
|
{ keymap },
|
||||||
|
{ defaultKeymap },
|
||||||
|
] = await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||||
|
),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let isSetup = script.hasAttribute("setup");
|
||||||
|
const hasConfig = script.hasAttribute("config");
|
||||||
|
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||||
|
|
||||||
|
if (hasConfig && configs.has(env)) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
configs.get(env)
|
||||||
|
? `duplicated config for env: ${env}`
|
||||||
|
: `unable to add a config to the env: ${env}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
configs.set(env, hasConfig);
|
||||||
|
|
||||||
|
let source = script.src
|
||||||
|
? await fetch(script.src).then((b) => b.text())
|
||||||
|
: script.textContent;
|
||||||
|
const context = {
|
||||||
|
interpreter,
|
||||||
|
env,
|
||||||
|
config:
|
||||||
|
hasConfig &&
|
||||||
|
new URL(script.getAttribute("config"), location.href).href,
|
||||||
|
get pySrc() {
|
||||||
|
return isSetup ? source : editor.state.doc.toString();
|
||||||
|
},
|
||||||
|
get outDiv() {
|
||||||
|
return isSetup ? null : outDiv;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let target;
|
||||||
|
defineProperties(script, {
|
||||||
|
target: { get: () => target },
|
||||||
|
code: {
|
||||||
|
get: () => context.pySrc,
|
||||||
|
set: (insert) => {
|
||||||
|
if (isSetup) return;
|
||||||
|
editor.update([
|
||||||
|
editor.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
/**
|
||||||
|
* Simulate a setup node overriding the source to evaluate.
|
||||||
|
* @param {string} code the Python code to evaluate.
|
||||||
|
* @returns {Promise<...>} fulfill once code has been evaluated.
|
||||||
|
*/
|
||||||
|
value(code) {
|
||||||
|
const wasSetup = isSetup;
|
||||||
|
const wasSource = source;
|
||||||
|
isSetup = true;
|
||||||
|
source = code;
|
||||||
|
const restore = () => {
|
||||||
|
isSetup = wasSetup;
|
||||||
|
source = wasSource;
|
||||||
|
};
|
||||||
|
return execute
|
||||||
|
.call(context, { currentTarget: null })
|
||||||
|
.then(restore, restore);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const notify = () => {
|
||||||
|
const event = new Event(`${type}-editor`, { bubbles: true });
|
||||||
|
script.dispatchEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
await execute.call(context, { currentTarget: null });
|
||||||
|
notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selector = script.getAttribute("target");
|
||||||
|
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement(`${type}-editor`);
|
||||||
|
target.style.display = "block";
|
||||||
|
script.after(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target.id) target.id = getID(type);
|
||||||
|
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
|
||||||
|
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
|
||||||
|
|
||||||
|
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
|
||||||
|
const listener = execute.bind(context);
|
||||||
|
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
|
||||||
|
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
|
||||||
|
|
||||||
|
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
|
||||||
|
const parent = inputChild.attachShadow({ mode: "open" });
|
||||||
|
// avoid inheriting styles from the outer component
|
||||||
|
parent.innerHTML = `<style> :host { all: initial; }</style>`;
|
||||||
|
|
||||||
|
target.appendChild(boxDiv);
|
||||||
|
|
||||||
|
const doc = dedent(script.textContent).trim();
|
||||||
|
|
||||||
|
// preserve user indentation, if any
|
||||||
|
const indentation = /^(\s+)/m.test(doc) ? RegExp.$1 : " ";
|
||||||
|
|
||||||
|
const editor = new EditorView({
|
||||||
|
extensions: [
|
||||||
|
indentUnit.of(indentation),
|
||||||
|
new Compartment().of(python()),
|
||||||
|
keymap.of([
|
||||||
|
...defaultKeymap,
|
||||||
|
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Cmd-Enter", run: listener, preventDefault: true },
|
||||||
|
{ key: "Shift-Enter", run: listener, preventDefault: true },
|
||||||
|
]),
|
||||||
|
basicSetup,
|
||||||
|
],
|
||||||
|
parent,
|
||||||
|
doc,
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.focus();
|
||||||
|
notify();
|
||||||
|
};
|
||||||
|
|
||||||
|
// avoid too greedy MutationObserver operations at distance
|
||||||
|
let timeout = 0;
|
||||||
|
|
||||||
|
// avoid delayed initialization
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
|
||||||
|
// reset interval value then check for new scripts
|
||||||
|
const resetTimeout = () => {
|
||||||
|
timeout = 0;
|
||||||
|
pyEditor();
|
||||||
|
};
|
||||||
|
|
||||||
|
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||||
|
const pyEditor = () => {
|
||||||
|
if (timeout) return;
|
||||||
|
timeout = setTimeout(resetTimeout, 250);
|
||||||
|
for (const [type, interpreter] of TYPES) {
|
||||||
|
const selector = `script[type="${type}-editor"]`;
|
||||||
|
for (const script of document.querySelectorAll(selector)) {
|
||||||
|
// avoid any further bootstrap by changing the type as active
|
||||||
|
script.type += "-active";
|
||||||
|
// don't await in here or multiple calls might happen
|
||||||
|
// while the first script is being initialized
|
||||||
|
queue = queue.then(() => init(script, type, interpreter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
};
|
||||||
|
|
||||||
|
new MutationObserver(pyEditor).observe(document, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// try to check the current document ASAP
|
||||||
|
export default pyEditor();
|
||||||
60
pyscript.core/src/plugins/py-terminal.js
Normal file
60
pyscript.core/src/plugins/py-terminal.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { TYPES } from "../core.js";
|
||||||
|
import { notify } from "./error.js";
|
||||||
|
import { customObserver } from "polyscript/exports";
|
||||||
|
|
||||||
|
// will contain all valid selectors
|
||||||
|
const SELECTORS = [];
|
||||||
|
|
||||||
|
// avoid processing same elements twice
|
||||||
|
const processed = new WeakSet();
|
||||||
|
|
||||||
|
// show the error on main and
|
||||||
|
// stops the module from keep executing
|
||||||
|
const notifyAndThrow = (message) => {
|
||||||
|
notify(message);
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||||
|
|
||||||
|
let addStyle = true;
|
||||||
|
|
||||||
|
for (const type of TYPES.keys()) {
|
||||||
|
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
|
||||||
|
SELECTORS.push(selector);
|
||||||
|
customObserver.set(selector, async (element) => {
|
||||||
|
// we currently support only one terminal on main as in "classic"
|
||||||
|
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||||
|
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||||
|
notifyAndThrow("You can use at most 1 main terminal");
|
||||||
|
|
||||||
|
// import styles lazily
|
||||||
|
if (addStyle) {
|
||||||
|
addStyle = false;
|
||||||
|
document.head.append(
|
||||||
|
Object.assign(document.createElement("link"), {
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: new URL("./xterm.css", import.meta.url),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processed.has(element)) return;
|
||||||
|
processed.add(element);
|
||||||
|
|
||||||
|
const bootstrap = (module) => module.default(element);
|
||||||
|
|
||||||
|
// we can't be smart with template literals for the dynamic import
|
||||||
|
// or bundlers are incapable of producing multiple files around
|
||||||
|
if (type === "mpy") {
|
||||||
|
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
|
||||||
|
bootstrap,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
|
||||||
|
bootstrap,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
239
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
239
pyscript.core/src/plugins/py-terminal/mpy.js
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// PyScript pyodide terminal plugin
|
||||||
|
import { hooks, inputFailure } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "mpy" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
|
||||||
|
|
||||||
|
interpreter.registerJsModule("_pyscript_input", {
|
||||||
|
input: pyterminal_read,
|
||||||
|
});
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from _pyscript_input import input",
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) pyterminal_write(missingReturn);
|
||||||
|
pyterminal_write(buffer);
|
||||||
|
};
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
// tiny shim of the code module with only interact
|
||||||
|
// to bootstrap a REPL like environment
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer) interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const writer = encoder.writable.getWriter();
|
||||||
|
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
|
||||||
|
pyterminal_ready();
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const terminalOptions = {
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = () => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...terminalOptions,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
await stream.write(`${line}\r`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const { sync } = xworker;
|
||||||
|
|
||||||
|
// handle the read mode on input
|
||||||
|
let promisedChunks = null;
|
||||||
|
let readChunks = "";
|
||||||
|
|
||||||
|
sync.is_pyterminal = () => true;
|
||||||
|
|
||||||
|
// put the terminal in a read-only state
|
||||||
|
// frees the worker on \r
|
||||||
|
sync.pyterminal_read = (buffer) => {
|
||||||
|
terminal.write(buffer);
|
||||||
|
promisedChunks = Promise.withResolvers();
|
||||||
|
return promisedChunks.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// write if not reading input
|
||||||
|
sync.pyterminal_write = (buffer) => {
|
||||||
|
if (!promisedChunks) terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// add the onData terminal listener which forwards to the worker
|
||||||
|
// everything typed in a queued char-by-char way
|
||||||
|
sync.pyterminal_ready = () => {
|
||||||
|
let queue = Promise.resolve();
|
||||||
|
stream = {
|
||||||
|
write: (buffer) =>
|
||||||
|
(queue = queue.then(() =>
|
||||||
|
sync.pyterminal_stream_write(buffer),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
terminal.onData((buffer) => {
|
||||||
|
if (promisedChunks) {
|
||||||
|
readChunks += buffer;
|
||||||
|
terminal.write(buffer);
|
||||||
|
if (readChunks.endsWith("\r")) {
|
||||||
|
terminal.write("\n");
|
||||||
|
promisedChunks.resolve(readChunks.slice(0, -1));
|
||||||
|
promisedChunks = null;
|
||||||
|
readChunks = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream.write(buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// ⚠️ In an ideal world the inputFailure should never be used on main.
|
||||||
|
// However, Pyodide still can't compete with MicroPython REPL mode
|
||||||
|
// so while it's OK to keep that entry on main as default, we need
|
||||||
|
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
|
||||||
|
// also throw whenever an `input(...)` is required / digited.
|
||||||
|
hooks.main.codeBeforeRun.delete(inputFailure);
|
||||||
|
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "mpy") return;
|
||||||
|
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
const terminal = init();
|
||||||
|
|
||||||
|
const missingReturn = new Uint8Array([13]);
|
||||||
|
io.stdout = (buffer) => {
|
||||||
|
if (buffer[0] === 10) terminal.write(missingReturn);
|
||||||
|
terminal.write(buffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// expose the __terminal__ one-off reference
|
||||||
|
globalThis.__py_terminal__ = terminal;
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from js import prompt as input",
|
||||||
|
"from js import __py_terminal__ as __terminal__",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
// NOTE: this is NOT the same as the one within
|
||||||
|
// the onWorkerReady callback!
|
||||||
|
interpreter.registerJsModule("code", {
|
||||||
|
interact() {
|
||||||
|
const encoder = new TextEncoderStream();
|
||||||
|
encoder.readable.pipeTo(
|
||||||
|
new WritableStream({
|
||||||
|
write(buffer) {
|
||||||
|
for (const c of buffer)
|
||||||
|
interpreter.replProcessChar(c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
stream = encoder.writable.getWriter();
|
||||||
|
terminal.onData((buffer) => stream.write(buffer));
|
||||||
|
|
||||||
|
interpreter.replInit();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
179
pyscript.core/src/plugins/py-terminal/py.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
// PyScript py-terminal plugin
|
||||||
|
import { hooks } from "../../core.js";
|
||||||
|
import { defineProperties } from "polyscript/exports";
|
||||||
|
|
||||||
|
const bootstrapped = new WeakSet();
|
||||||
|
|
||||||
|
// this callback will be serialized as string and it never needs
|
||||||
|
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||||
|
// only once thanks to the `sync.is_pyterminal()` check.
|
||||||
|
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||||
|
if (type !== "py" || !sync.is_pyterminal()) return;
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
"from polyscript import currentScript as _",
|
||||||
|
"__terminal__ = _.terminal",
|
||||||
|
"del _",
|
||||||
|
].join(";"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const { pyterminal_read, pyterminal_write } = sync;
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
pyterminal_write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
pyterminal_write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => pyterminal_read(data),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async (element) => {
|
||||||
|
// lazy load these only when a valid terminal is found
|
||||||
|
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
|
||||||
|
await Promise.all([
|
||||||
|
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
|
||||||
|
),
|
||||||
|
import(
|
||||||
|
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const readline = new Readline();
|
||||||
|
|
||||||
|
// common main thread initialization for both worker
|
||||||
|
// or main case, bootstrapping the terminal on its target
|
||||||
|
const init = (options) => {
|
||||||
|
let target = element;
|
||||||
|
const selector = element.getAttribute("target");
|
||||||
|
if (selector) {
|
||||||
|
target =
|
||||||
|
document.getElementById(selector) ||
|
||||||
|
document.querySelector(selector);
|
||||||
|
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||||
|
} else {
|
||||||
|
target = document.createElement("py-terminal");
|
||||||
|
target.style.display = "block";
|
||||||
|
element.after(target);
|
||||||
|
}
|
||||||
|
const terminal = new Terminal({
|
||||||
|
theme: {
|
||||||
|
background: "#191A19",
|
||||||
|
foreground: "#F5F2E7",
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
terminal.loadAddon(fitAddon);
|
||||||
|
terminal.loadAddon(readline);
|
||||||
|
terminal.loadAddon(new WebLinksAddon());
|
||||||
|
terminal.open(target);
|
||||||
|
fitAddon.fit();
|
||||||
|
terminal.focus();
|
||||||
|
defineProperties(element, {
|
||||||
|
terminal: { value: terminal },
|
||||||
|
process: {
|
||||||
|
value: async (code) => {
|
||||||
|
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
|
||||||
|
terminal.paste(`${line}`);
|
||||||
|
terminal.write("\r\n");
|
||||||
|
do {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 0),
|
||||||
|
);
|
||||||
|
} while (!readline.activeRead?.resolve);
|
||||||
|
readline.activeRead.resolve(line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return terminal;
|
||||||
|
};
|
||||||
|
|
||||||
|
// branch logic for the worker
|
||||||
|
if (element.hasAttribute("worker")) {
|
||||||
|
// add a hook on the main thread to setup all sync helpers
|
||||||
|
// also bootstrapping the XTerm target on main *BUT* ...
|
||||||
|
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||||
|
// ... as multiple workers will add multiple callbacks
|
||||||
|
// be sure no xworker is ever initialized twice!
|
||||||
|
if (bootstrapped.has(xworker)) return;
|
||||||
|
bootstrapped.add(xworker);
|
||||||
|
|
||||||
|
// still cleanup this callback for future scripts/workers
|
||||||
|
hooks.main.onWorker.delete(worker);
|
||||||
|
|
||||||
|
init({
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: "block",
|
||||||
|
});
|
||||||
|
|
||||||
|
xworker.sync.is_pyterminal = () => true;
|
||||||
|
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||||
|
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup remote thread JS/Python code for whenever the
|
||||||
|
// worker is ready to become a terminal
|
||||||
|
hooks.worker.onReady.add(workerReady);
|
||||||
|
} else {
|
||||||
|
// in the main case, just bootstrap XTerm without
|
||||||
|
// allowing any input as that's not possible / awkward
|
||||||
|
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||||
|
if (type !== "py") return;
|
||||||
|
|
||||||
|
console.warn("py-terminal is read only on main thread");
|
||||||
|
hooks.main.onReady.delete(main);
|
||||||
|
|
||||||
|
// on main, it's easy to trash and clean the current terminal
|
||||||
|
globalThis.__py_terminal__ = init({
|
||||||
|
disableStdin: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: "underline",
|
||||||
|
});
|
||||||
|
run("from js import __py_terminal__ as __terminal__");
|
||||||
|
delete globalThis.__py_terminal__;
|
||||||
|
|
||||||
|
io.stderr = (error) => {
|
||||||
|
readline.write(String(error.message || error));
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = "";
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const generic = {
|
||||||
|
isatty: false,
|
||||||
|
write(buffer) {
|
||||||
|
data = decoder.decode(buffer);
|
||||||
|
readline.write(data);
|
||||||
|
return buffer.length;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
interpreter.setStdout(generic);
|
||||||
|
interpreter.setStderr(generic);
|
||||||
|
interpreter.setStdin({
|
||||||
|
isatty: false,
|
||||||
|
stdin: () => readline.read(data),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
70
pyscript.core/src/stdlib.js
Normal file
70
pyscript.core/src/stdlib.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Create through Python the pyscript module through
|
||||||
|
* the artifact generated at build time.
|
||||||
|
* This the returned value is a string that must be used
|
||||||
|
* either before a worker execute code or when the module
|
||||||
|
* is registered on the main thread.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import pyscript from "./stdlib/pyscript.js";
|
||||||
|
|
||||||
|
class Ignore extends Array {
|
||||||
|
#add = false;
|
||||||
|
#paths;
|
||||||
|
#array;
|
||||||
|
constructor(array, ...paths) {
|
||||||
|
super();
|
||||||
|
this.#array = array;
|
||||||
|
this.#paths = paths;
|
||||||
|
}
|
||||||
|
push(...values) {
|
||||||
|
if (this.#add) super.push(...values);
|
||||||
|
return this.#array.push(...values);
|
||||||
|
}
|
||||||
|
path(path) {
|
||||||
|
for (const _path of this.#paths) {
|
||||||
|
// bails out at the first `true` value
|
||||||
|
if ((this.#add = path.startsWith(_path))) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entries } = Object;
|
||||||
|
|
||||||
|
const python = [
|
||||||
|
"import os as _os",
|
||||||
|
"from pathlib import Path as _Path",
|
||||||
|
"_path = None",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ignore = new Ignore(python, "-");
|
||||||
|
|
||||||
|
const write = (base, literal) => {
|
||||||
|
for (const [key, value] of entries(literal)) {
|
||||||
|
ignore.path(`${base}/${key}`);
|
||||||
|
ignore.push(`_path = _Path("${base}/${key}")`);
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const code = JSON.stringify(value);
|
||||||
|
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
|
||||||
|
} else {
|
||||||
|
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
|
||||||
|
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
|
||||||
|
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
|
||||||
|
write(`${base}/${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
write(".", pyscript);
|
||||||
|
|
||||||
|
// in order to fix js.document in the Worker case
|
||||||
|
// we need to bootstrap pyscript module ASAP
|
||||||
|
python.push("import pyscript as _pyscript");
|
||||||
|
|
||||||
|
python.push(
|
||||||
|
...["_Path", "_path", "_os", "_pyscript"].map((ref) => `del ${ref}`),
|
||||||
|
);
|
||||||
|
python.push("\n");
|
||||||
|
|
||||||
|
export const stdlib = python.join("\n");
|
||||||
|
export const optional = ignore.join("\n");
|
||||||
57
pyscript.core/src/stdlib/pyscript/__init__.py
Normal file
57
pyscript.core/src/stdlib/pyscript/__init__.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Some notes about the naming conventions and the relationship between various
|
||||||
|
# similar-but-different names.
|
||||||
|
#
|
||||||
|
# import pyscript
|
||||||
|
# this package contains the main user-facing API offered by pyscript. All
|
||||||
|
# the names which are supposed be used by end users should be made
|
||||||
|
# available in pyscript/__init__.py (i.e., this file)
|
||||||
|
#
|
||||||
|
# import _pyscript
|
||||||
|
# this is an internal module implemented in JS. It is used internally by
|
||||||
|
# the pyscript package, end users should not use it directly. For its
|
||||||
|
# implementation, grep for `interpreter.registerJsModule("_pyscript",
|
||||||
|
# ...)` in core.js
|
||||||
|
#
|
||||||
|
# import js
|
||||||
|
# this is the JS globalThis, as exported by pyodide and/or micropython's
|
||||||
|
# FFIs. As such, it contains different things in the main thread or in a
|
||||||
|
# worker.
|
||||||
|
#
|
||||||
|
# import pyscript.magic_js
|
||||||
|
# this submodule abstracts away some of the differences between the main
|
||||||
|
# thread and the worker. In particular, it defines `window` and `document`
|
||||||
|
# in such a way that these names work in both cases: in the main thread,
|
||||||
|
# they are the "real" objects, in the worker they are proxies which work
|
||||||
|
# thanks to coincident.
|
||||||
|
#
|
||||||
|
# from pyscript import window, document
|
||||||
|
# these are just the window and document objects as defined by
|
||||||
|
# pyscript.magic_js. This is the blessed way to access them from pyscript,
|
||||||
|
# as it works transparently in both the main thread and worker cases.
|
||||||
|
|
||||||
|
from polyscript import lazy_py_modules as py_import
|
||||||
|
from pyscript.display import HTML, display
|
||||||
|
from pyscript.fetch import fetch
|
||||||
|
from pyscript.magic_js import (
|
||||||
|
RUNNING_IN_WORKER,
|
||||||
|
PyWorker,
|
||||||
|
config,
|
||||||
|
current_target,
|
||||||
|
document,
|
||||||
|
js_import,
|
||||||
|
js_modules,
|
||||||
|
sync,
|
||||||
|
window,
|
||||||
|
)
|
||||||
|
from pyscript.websocket import WebSocket
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyscript.event_handling import when
|
||||||
|
except:
|
||||||
|
# TODO: should we remove this? Or at the very least, we should capture
|
||||||
|
# the traceback otherwise it's very hard to debug
|
||||||
|
from pyscript.util import NotSupported
|
||||||
|
|
||||||
|
when = NotSupported(
|
||||||
|
"pyscript.when", "pyscript.when currently not available with this interpreter"
|
||||||
|
)
|
||||||
177
pyscript.core/src/stdlib/pyscript/display.py
Normal file
177
pyscript.core/src/stdlib/pyscript/display.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import base64
|
||||||
|
import html
|
||||||
|
import io
|
||||||
|
import re
|
||||||
|
|
||||||
|
from pyscript.magic_js import current_target, document, window
|
||||||
|
|
||||||
|
_MIME_METHODS = {
|
||||||
|
"savefig": "image/png",
|
||||||
|
"_repr_javascript_": "application/javascript",
|
||||||
|
"_repr_json_": "application/json",
|
||||||
|
"_repr_latex": "text/latex",
|
||||||
|
"_repr_png_": "image/png",
|
||||||
|
"_repr_jpeg_": "image/jpeg",
|
||||||
|
"_repr_pdf_": "application/pdf",
|
||||||
|
"_repr_svg_": "image/svg+xml",
|
||||||
|
"_repr_markdown_": "text/markdown",
|
||||||
|
"_repr_html_": "text/html",
|
||||||
|
"__repr__": "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _render_image(mime, value, meta):
|
||||||
|
# If the image value is using bytes we should convert it to base64
|
||||||
|
# otherwise it will return raw bytes and the browser will not be able to
|
||||||
|
# render it.
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = base64.b64encode(value).decode("utf-8")
|
||||||
|
|
||||||
|
# This is the pattern of base64 strings
|
||||||
|
base64_pattern = re.compile(
|
||||||
|
r"^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$"
|
||||||
|
)
|
||||||
|
# If value doesn't match the base64 pattern we should encode it to base64
|
||||||
|
if len(value) > 0 and not base64_pattern.match(value):
|
||||||
|
value = base64.b64encode(value.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
data = f"data:{mime};charset=utf-8;base64,{value}"
|
||||||
|
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
|
||||||
|
return f'<img src="{data}" {attrs}></img>'
|
||||||
|
|
||||||
|
|
||||||
|
def _identity(value, meta):
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
_MIME_RENDERERS = {
|
||||||
|
"text/plain": html.escape,
|
||||||
|
"text/html": _identity,
|
||||||
|
"image/png": lambda value, meta: _render_image("image/png", value, meta),
|
||||||
|
"image/jpeg": lambda value, meta: _render_image("image/jpeg", value, meta),
|
||||||
|
"image/svg+xml": _identity,
|
||||||
|
"application/json": _identity,
|
||||||
|
"application/javascript": lambda value, meta: f"<script>{value}<\\/script>",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HTML:
|
||||||
|
"""
|
||||||
|
Wrap a string so that display() can render it as plain HTML
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, html):
|
||||||
|
self._html = html
|
||||||
|
|
||||||
|
def _repr_html_(self):
|
||||||
|
return self._html
|
||||||
|
|
||||||
|
|
||||||
|
def _eval_formatter(obj, print_method):
|
||||||
|
"""
|
||||||
|
Evaluates a formatter method.
|
||||||
|
"""
|
||||||
|
if print_method == "__repr__":
|
||||||
|
return repr(obj)
|
||||||
|
elif hasattr(obj, print_method):
|
||||||
|
if print_method == "savefig":
|
||||||
|
buf = io.BytesIO()
|
||||||
|
obj.savefig(buf, format="png")
|
||||||
|
buf.seek(0)
|
||||||
|
return base64.b64encode(buf.read()).decode("utf-8")
|
||||||
|
return getattr(obj, print_method)()
|
||||||
|
elif print_method == "_repr_mimebundle_":
|
||||||
|
return {}, {}
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _format_mime(obj):
|
||||||
|
"""
|
||||||
|
Formats object using _repr_x_ methods.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return html.escape(obj), "text/plain"
|
||||||
|
|
||||||
|
mimebundle = _eval_formatter(obj, "_repr_mimebundle_")
|
||||||
|
if isinstance(mimebundle, tuple):
|
||||||
|
format_dict, _ = mimebundle
|
||||||
|
else:
|
||||||
|
format_dict = mimebundle
|
||||||
|
|
||||||
|
output, not_available = None, []
|
||||||
|
for method, mime_type in _MIME_METHODS.items():
|
||||||
|
if mime_type in format_dict:
|
||||||
|
output = format_dict[mime_type]
|
||||||
|
else:
|
||||||
|
output = _eval_formatter(obj, method)
|
||||||
|
|
||||||
|
if output is None:
|
||||||
|
continue
|
||||||
|
elif mime_type not in _MIME_RENDERERS:
|
||||||
|
not_available.append(mime_type)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if output is None:
|
||||||
|
if not_available:
|
||||||
|
window.console.warn(
|
||||||
|
f"Rendered object requested unavailable MIME renderers: {not_available}"
|
||||||
|
)
|
||||||
|
output = repr(output)
|
||||||
|
mime_type = "text/plain"
|
||||||
|
elif isinstance(output, tuple):
|
||||||
|
output, meta = output
|
||||||
|
else:
|
||||||
|
meta = {}
|
||||||
|
return _MIME_RENDERERS[mime_type](output, meta), mime_type
|
||||||
|
|
||||||
|
|
||||||
|
def _write(element, value, append=False):
|
||||||
|
html, mime_type = _format_mime(value)
|
||||||
|
if html == "\\n":
|
||||||
|
return
|
||||||
|
|
||||||
|
if append:
|
||||||
|
out_element = document.createElement("div")
|
||||||
|
element.append(out_element)
|
||||||
|
else:
|
||||||
|
out_element = element.lastElementChild
|
||||||
|
if out_element is None:
|
||||||
|
out_element = element
|
||||||
|
|
||||||
|
if mime_type in ("application/javascript", "text/html"):
|
||||||
|
script_element = document.createRange().createContextualFragment(html)
|
||||||
|
out_element.append(script_element)
|
||||||
|
else:
|
||||||
|
out_element.innerHTML = html
|
||||||
|
|
||||||
|
|
||||||
|
def display(*values, target=None, append=True):
|
||||||
|
if target is None:
|
||||||
|
target = current_target()
|
||||||
|
elif not isinstance(target, str):
|
||||||
|
raise TypeError(f"target must be str or None, not {target.__class__.__name__}")
|
||||||
|
elif target == "":
|
||||||
|
raise ValueError("Cannot have an empty target")
|
||||||
|
elif target.startswith("#"):
|
||||||
|
# note: here target is str and not None!
|
||||||
|
# align with @when behavior
|
||||||
|
target = target[1:]
|
||||||
|
|
||||||
|
element = document.getElementById(target)
|
||||||
|
|
||||||
|
# If target cannot be found on the page, a ValueError is raised
|
||||||
|
if element is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid selector with id={target}. Cannot be found in the page."
|
||||||
|
)
|
||||||
|
|
||||||
|
# if element is a <script type="py">, it has a 'target' attribute which
|
||||||
|
# points to the visual element holding the displayed values. In that case,
|
||||||
|
# use that.
|
||||||
|
if element.tagName == "SCRIPT" and hasattr(element, "target"):
|
||||||
|
element = element.target
|
||||||
|
|
||||||
|
for v in values:
|
||||||
|
if not append:
|
||||||
|
element.replaceChildren()
|
||||||
|
_write(element, v, append=append)
|
||||||
67
pyscript.core/src/stdlib/pyscript/event_handling.py
Normal file
67
pyscript.core/src/stdlib/pyscript/event_handling.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyodide.ffi.wrappers import add_event_listener
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def add_event_listener(el, event_type, func):
|
||||||
|
el.addEventListener(event_type, func)
|
||||||
|
|
||||||
|
|
||||||
|
from pyscript.magic_js import document
|
||||||
|
|
||||||
|
|
||||||
|
def when(event_type=None, selector=None):
|
||||||
|
"""
|
||||||
|
Decorates a function and passes py-* events to the decorated function
|
||||||
|
The events might or not be an argument of the decorated function
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
if isinstance(selector, str):
|
||||||
|
elements = document.querySelectorAll(selector)
|
||||||
|
else:
|
||||||
|
# TODO: This is a hack that will be removed when pyscript becomes a package
|
||||||
|
# and we can better manage the imports without circular dependencies
|
||||||
|
from pyweb import pydom
|
||||||
|
|
||||||
|
if isinstance(selector, pydom.Element):
|
||||||
|
elements = [selector._js]
|
||||||
|
elif isinstance(selector, pydom.ElementCollection):
|
||||||
|
elements = [el._js for el in selector]
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid selector: {selector}. Selector must"
|
||||||
|
" be a string, a pydom.Element or a pydom.ElementCollection."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
# Function doesn't receive events
|
||||||
|
if not sig.parameters:
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
func()
|
||||||
|
|
||||||
|
else:
|
||||||
|
wrapper = func
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
# TODO: this is very ugly hack to get micropython working because inspect.signature
|
||||||
|
# doesn't exist, but we need to actually properly replace inspect.signature.
|
||||||
|
# It may be actually better to not try any magic for now and raise the error
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except TypeError as e:
|
||||||
|
if "takes" in str(e) and "positional arguments" in str(e):
|
||||||
|
return func()
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
for el in elements:
|
||||||
|
add_event_listener(el, event_type, wrapper)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
87
pyscript.core/src/stdlib/pyscript/fetch.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
|
||||||
|
### wrap the response to grant Pythonic results
|
||||||
|
class _Response:
|
||||||
|
def __init__(self, response):
|
||||||
|
self._response = response
|
||||||
|
|
||||||
|
# grant access to response.ok and other fields
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._response, attr)
|
||||||
|
|
||||||
|
# exposed methods with Pythonic results
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
# works in Pyodide
|
||||||
|
if hasattr(buffer, "to_py"):
|
||||||
|
return buffer.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(buffer))
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
return await self._response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
buffer = await self._response.arrayBuffer()
|
||||||
|
return as_bytearray(buffer)
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
return json.loads(await self.text())
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
return await self._response.text()
|
||||||
|
|
||||||
|
|
||||||
|
### allow direct await to _Response methods
|
||||||
|
class _DirectResponse:
|
||||||
|
@staticmethod
|
||||||
|
def setup(promise, response):
|
||||||
|
promise._response = _Response(response)
|
||||||
|
return promise._response
|
||||||
|
|
||||||
|
def __init__(self, promise):
|
||||||
|
self._promise = promise
|
||||||
|
promise._response = None
|
||||||
|
promise.arrayBuffer = self.arrayBuffer
|
||||||
|
promise.blob = self.blob
|
||||||
|
promise.bytearray = self.bytearray
|
||||||
|
promise.json = self.json
|
||||||
|
promise.text = self.text
|
||||||
|
|
||||||
|
async def _response(self):
|
||||||
|
if not self._promise._response:
|
||||||
|
await self._promise
|
||||||
|
return self._promise._response
|
||||||
|
|
||||||
|
async def arrayBuffer(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.arrayBuffer()
|
||||||
|
|
||||||
|
async def blob(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.blob()
|
||||||
|
|
||||||
|
async def bytearray(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.bytearray()
|
||||||
|
|
||||||
|
async def json(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
async def text(self):
|
||||||
|
response = await self._response()
|
||||||
|
return await response.text()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, **kw):
|
||||||
|
# workaround Pyodide / MicroPython dict <-> js conversion
|
||||||
|
options = js.JSON.parse(json.dumps(kw))
|
||||||
|
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
|
||||||
|
promise = js.fetch(url, options).then(awaited)
|
||||||
|
_DirectResponse(promise)
|
||||||
|
return promise
|
||||||
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
18
pyscript.core/src/stdlib/pyscript/ffi.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
try:
|
||||||
|
import js
|
||||||
|
from pyodide.ffi import create_proxy as _cp
|
||||||
|
from pyodide.ffi import to_js as _py_tjs
|
||||||
|
|
||||||
|
from_entries = js.Object.fromEntries
|
||||||
|
|
||||||
|
def _tjs(value, **kw):
|
||||||
|
if not hasattr(kw, "dict_converter"):
|
||||||
|
kw["dict_converter"] = from_entries
|
||||||
|
return _py_tjs(value, **kw)
|
||||||
|
|
||||||
|
except:
|
||||||
|
from jsffi import create_proxy as _cp
|
||||||
|
from jsffi import to_js as _tjs
|
||||||
|
|
||||||
|
create_proxy = _cp
|
||||||
|
to_js = _tjs
|
||||||
81
pyscript.core/src/stdlib/pyscript/magic_js.py
Normal file
81
pyscript.core/src/stdlib/pyscript/magic_js.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import js as globalThis
|
||||||
|
from polyscript import config as _config
|
||||||
|
from polyscript import js_modules
|
||||||
|
from pyscript.util import NotSupported
|
||||||
|
|
||||||
|
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
|
||||||
|
|
||||||
|
config = json.loads(globalThis.JSON.stringify(_config))
|
||||||
|
|
||||||
|
|
||||||
|
# allow `from pyscript.js_modules.xxx import yyy`
|
||||||
|
class JSModule:
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __getattr__(self, field):
|
||||||
|
# avoid pyodide looking for non existent fields
|
||||||
|
if not field.startswith("_"):
|
||||||
|
return getattr(getattr(js_modules, self.name), field)
|
||||||
|
|
||||||
|
|
||||||
|
# generate N modules in the system that will proxy the real value
|
||||||
|
for name in globalThis.Reflect.ownKeys(js_modules):
|
||||||
|
sys.modules[f"pyscript.js_modules.{name}"] = JSModule(name)
|
||||||
|
sys.modules["pyscript.js_modules"] = js_modules
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
import polyscript
|
||||||
|
|
||||||
|
PyWorker = NotSupported(
|
||||||
|
"pyscript.PyWorker",
|
||||||
|
"pyscript.PyWorker works only when running in the main thread",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
globalThis.SharedArrayBuffer.new(4)
|
||||||
|
import js
|
||||||
|
|
||||||
|
window = polyscript.xworker.window
|
||||||
|
document = window.document
|
||||||
|
js.document = document
|
||||||
|
# this is the same as js_import on main and it lands modules on main
|
||||||
|
js_import = window.Function(
|
||||||
|
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
|
||||||
|
)()
|
||||||
|
except:
|
||||||
|
globalThis.console.debug("SharedArrayBuffer is not available")
|
||||||
|
# in this scenario none of the utilities would work
|
||||||
|
# as expected so we better export these as NotSupported
|
||||||
|
window = NotSupported(
|
||||||
|
"pyscript.window",
|
||||||
|
"pyscript.window in workers works only via SharedArrayBuffer",
|
||||||
|
)
|
||||||
|
document = NotSupported(
|
||||||
|
"pyscript.document",
|
||||||
|
"pyscript.document in workers works only via SharedArrayBuffer",
|
||||||
|
)
|
||||||
|
|
||||||
|
sync = polyscript.xworker.sync
|
||||||
|
|
||||||
|
# in workers the display does not have a default ID
|
||||||
|
# but there is a sync utility from xworker
|
||||||
|
def current_target():
|
||||||
|
return polyscript.target
|
||||||
|
|
||||||
|
else:
|
||||||
|
import _pyscript
|
||||||
|
from _pyscript import PyWorker, js_import
|
||||||
|
|
||||||
|
window = globalThis
|
||||||
|
document = globalThis.document
|
||||||
|
sync = NotSupported(
|
||||||
|
"pyscript.sync", "pyscript.sync works only when running in a worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
# in MAIN the current element target exist, just use it
|
||||||
|
def current_target():
|
||||||
|
return _pyscript.target
|
||||||
33
pyscript.core/src/stdlib/pyscript/util.py
Normal file
33
pyscript.core/src/stdlib/pyscript/util.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import js
|
||||||
|
|
||||||
|
|
||||||
|
def as_bytearray(buffer):
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
size = ui8a.length
|
||||||
|
ba = bytearray(size)
|
||||||
|
for i in range(0, size):
|
||||||
|
ba[i] = ui8a[i]
|
||||||
|
return ba
|
||||||
|
|
||||||
|
|
||||||
|
class NotSupported:
|
||||||
|
"""
|
||||||
|
Small helper that raises exceptions if you try to get/set any attribute on
|
||||||
|
it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, error):
|
||||||
|
object.__setattr__(self, "name", name)
|
||||||
|
object.__setattr__(self, "error", error)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<NotSupported {self.name} [{self.error}]>"
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
raise AttributeError(self.error)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
raise AttributeError(self.error)
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
raise TypeError(self.error)
|
||||||
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
67
pyscript.core/src/stdlib/pyscript/websocket.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import js
|
||||||
|
from pyscript.util import as_bytearray
|
||||||
|
|
||||||
|
code = "code"
|
||||||
|
protocols = "protocols"
|
||||||
|
reason = "reason"
|
||||||
|
|
||||||
|
|
||||||
|
class EventMessage:
|
||||||
|
def __init__(self, event):
|
||||||
|
self._event = event
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
value = getattr(self._event, attr)
|
||||||
|
|
||||||
|
if attr == "data" and not isinstance(value, str):
|
||||||
|
if hasattr(value, "to_py"):
|
||||||
|
return value.to_py()
|
||||||
|
# shims in MicroPython
|
||||||
|
return memoryview(as_bytearray(value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocket(object):
|
||||||
|
CONNECTING = 0
|
||||||
|
OPEN = 1
|
||||||
|
CLOSING = 2
|
||||||
|
CLOSED = 3
|
||||||
|
|
||||||
|
def __init__(self, **kw):
|
||||||
|
url = kw["url"]
|
||||||
|
if protocols in kw:
|
||||||
|
socket = js.WebSocket.new(url, kw[protocols])
|
||||||
|
else:
|
||||||
|
socket = js.WebSocket.new(url)
|
||||||
|
object.__setattr__(self, "_ws", socket)
|
||||||
|
|
||||||
|
for t in ["onclose", "onerror", "onmessage", "onopen"]:
|
||||||
|
if t in kw:
|
||||||
|
socket[t] = kw[t]
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self._ws, attr)
|
||||||
|
|
||||||
|
def __setattr__(self, attr, value):
|
||||||
|
if attr == "onmessage":
|
||||||
|
self._ws[attr] = lambda e: value(EventMessage(e))
|
||||||
|
else:
|
||||||
|
self._ws[attr] = value
|
||||||
|
|
||||||
|
def close(self, **kw):
|
||||||
|
if code in kw and reason in kw:
|
||||||
|
self._ws.close(kw[code], kw[reason])
|
||||||
|
elif code in kw:
|
||||||
|
self._ws.close(kw[code])
|
||||||
|
else:
|
||||||
|
self._ws.close()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
if isinstance(data, str):
|
||||||
|
self._ws.send(data)
|
||||||
|
else:
|
||||||
|
buffer = js.Uint8Array.new(len(data))
|
||||||
|
for pos, b in enumerate(data):
|
||||||
|
buffer[pos] = b
|
||||||
|
self._ws.send(buffer)
|
||||||
2
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
2
pyscript.core/src/stdlib/pyweb/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .pydom import JSProperty
|
||||||
|
from .pydom import dom as pydom
|
||||||
95
pyscript.core/src/stdlib/pyweb/media.py
Normal file
95
pyscript.core/src/stdlib/pyweb/media.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from pyodide.ffi import to_js
|
||||||
|
from pyscript import window
|
||||||
|
|
||||||
|
|
||||||
|
class Device:
|
||||||
|
"""Device represents a media input or output device, such as a microphone,
|
||||||
|
camera, or headset.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device):
|
||||||
|
self._js = device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._js.deviceId
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group(self):
|
||||||
|
return self._js.groupId
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kind(self):
|
||||||
|
return self._js.kind
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
return self._js.label
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def load(cls, audio=False, video=True):
|
||||||
|
"""Load the device stream."""
|
||||||
|
options = window.Object.new()
|
||||||
|
options.audio = audio
|
||||||
|
if isinstance(video, bool):
|
||||||
|
options.video = video
|
||||||
|
else:
|
||||||
|
# TODO: Think this can be simplified but need to check it on the pyodide side
|
||||||
|
|
||||||
|
# TODO: this is pyodide specific. shouldn't be!
|
||||||
|
options.video = window.Object.new()
|
||||||
|
for k in video:
|
||||||
|
setattr(
|
||||||
|
options.video,
|
||||||
|
k,
|
||||||
|
to_js(video[k], dict_converter=window.Object.fromEntries),
|
||||||
|
)
|
||||||
|
|
||||||
|
stream = await window.navigator.mediaDevices.getUserMedia(options)
|
||||||
|
return stream
|
||||||
|
|
||||||
|
async def get_stream(self):
|
||||||
|
key = self.kind.replace("input", "").replace("output", "")
|
||||||
|
options = {key: {"deviceId": {"exact": self.id}}}
|
||||||
|
|
||||||
|
return await self.load(**options)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_devices() -> list[dict]:
|
||||||
|
"""
|
||||||
|
Return the list of the currently available media input and output devices,
|
||||||
|
such as microphones, cameras, headsets, and so forth.
|
||||||
|
|
||||||
|
Output:
|
||||||
|
|
||||||
|
list(dict) - list of dictionaries representing the available media devices.
|
||||||
|
Each dictionary has the following keys:
|
||||||
|
* deviceId: a string that is an identifier for the represented device
|
||||||
|
that is persisted across sessions. It is un-guessable by other
|
||||||
|
applications and unique to the origin of the calling application.
|
||||||
|
It is reset when the user clears cookies (for Private Browsing, a
|
||||||
|
different identifier is used that is not persisted across sessions).
|
||||||
|
|
||||||
|
* groupId: a string that is a group identifier. Two devices have the same
|
||||||
|
group identifier if they belong to the same physical device — for
|
||||||
|
example a monitor with both a built-in camera and a microphone.
|
||||||
|
|
||||||
|
* kind: an enumerated value that is either "videoinput", "audioinput"
|
||||||
|
or "audiooutput".
|
||||||
|
|
||||||
|
* label: a string describing this device (for example "External USB
|
||||||
|
Webcam").
|
||||||
|
|
||||||
|
Note: the returned list will omit any devices that are blocked by the document
|
||||||
|
Permission Policy: microphone, camera, speaker-selection (for output devices),
|
||||||
|
and so on. Access to particular non-default devices is also gated by the
|
||||||
|
Permissions API, and the list will omit devices for which the user has not
|
||||||
|
granted explicit permission.
|
||||||
|
"""
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
||||||
|
return [
|
||||||
|
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
||||||
|
]
|
||||||
569
pyscript.core/src/stdlib/pyweb/pydom.py
Normal file
569
pyscript.core/src/stdlib/pyweb/pydom.py
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
import inspect
|
||||||
|
|
||||||
|
try:
|
||||||
|
from typing import Any
|
||||||
|
except ImportError:
|
||||||
|
Any = "Any"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import warnings
|
||||||
|
except ImportError:
|
||||||
|
# TODO: For now it probably means we are in MicroPython. We should figure
|
||||||
|
# out the "right" way to handle this. For now we just ignore the warning
|
||||||
|
# and logging to console
|
||||||
|
class warnings:
|
||||||
|
@staticmethod
|
||||||
|
def warn(*args, **kwargs):
|
||||||
|
print("WARNING: ", *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from functools import cached_property
|
||||||
|
except ImportError:
|
||||||
|
# TODO: same comment about micropython as above
|
||||||
|
cached_property = property
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyodide.ffi import JsProxy
|
||||||
|
except ImportError:
|
||||||
|
# TODO: same comment about micropython as above
|
||||||
|
def JsProxy(obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
from pyscript import display, document, window
|
||||||
|
|
||||||
|
alert = window.alert
|
||||||
|
|
||||||
|
|
||||||
|
class JSProperty:
|
||||||
|
"""JS property descriptor that directly maps to the property with the same
|
||||||
|
name in the underlying JS component."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, allow_nones: bool = False):
|
||||||
|
self.name = name
|
||||||
|
self.allow_nones = allow_nones
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return getattr(obj._js, self.name)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
if not self.allow_nones and value is None:
|
||||||
|
return
|
||||||
|
setattr(obj._js, self.name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseElement:
|
||||||
|
def __init__(self, js_element):
|
||||||
|
self._js = js_element
|
||||||
|
self._parent = None
|
||||||
|
self.style = StyleProxy(self)
|
||||||
|
self._proxies = {}
|
||||||
|
|
||||||
|
def __eq__(self, obj):
|
||||||
|
"""Check if the element is the same as the other element by comparing
|
||||||
|
the underlying JS element"""
|
||||||
|
return isinstance(obj, BaseElement) and obj._js == self._js
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
if self._parent:
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
if self._js.parentElement:
|
||||||
|
self._parent = self.__class__(self._js.parentElement)
|
||||||
|
|
||||||
|
return self._parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __class(self):
|
||||||
|
return self.__class__ if self.__class__ != PyDom else Element
|
||||||
|
|
||||||
|
def create(self, type_, is_child=True, classes=None, html=None, label=None):
|
||||||
|
js_el = document.createElement(type_)
|
||||||
|
element = self.__class(js_el)
|
||||||
|
|
||||||
|
if classes:
|
||||||
|
for class_ in classes:
|
||||||
|
element.add_class(class_)
|
||||||
|
|
||||||
|
if html is not None:
|
||||||
|
element.html = html
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
element.label = label
|
||||||
|
|
||||||
|
if is_child:
|
||||||
|
self.append(element)
|
||||||
|
|
||||||
|
return element
|
||||||
|
|
||||||
|
def find(self, selector):
|
||||||
|
"""Return an ElementCollection representing all the child elements that
|
||||||
|
match the specified selector.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
selector (str): A string containing a selector expression
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ElementCollection: A collection of elements matching the selector
|
||||||
|
"""
|
||||||
|
elements = self._js.querySelectorAll(selector)
|
||||||
|
if not elements:
|
||||||
|
return None
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
|
||||||
|
class Element(BaseElement):
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return [self.__class__(el) for el in self._js.children]
|
||||||
|
|
||||||
|
def append(self, child):
|
||||||
|
# TODO: this is Pyodide specific for now!!!!!!
|
||||||
|
# if we get passed a JSProxy Element directly we just map it to the
|
||||||
|
# higher level Python element
|
||||||
|
if inspect.isclass(JsProxy) and isinstance(child, JsProxy):
|
||||||
|
return self.append(Element(child))
|
||||||
|
|
||||||
|
elif isinstance(child, Element):
|
||||||
|
self._js.appendChild(child._js)
|
||||||
|
|
||||||
|
return child
|
||||||
|
|
||||||
|
elif isinstance(child, ElementCollection):
|
||||||
|
for el in child:
|
||||||
|
self.append(el)
|
||||||
|
|
||||||
|
# -------- Pythonic Interface to Element -------- #
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._js.innerHTML
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._js.innerHTML = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self):
|
||||||
|
return self._js.textContent
|
||||||
|
|
||||||
|
@text.setter
|
||||||
|
def text(self, value):
|
||||||
|
self._js.textContent = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self):
|
||||||
|
# TODO: This breaks with with standard template elements. Define how to best
|
||||||
|
# handle this specifica use case. Just not support for now?
|
||||||
|
if self._js.tagName == "TEMPLATE":
|
||||||
|
warnings.warn(
|
||||||
|
"Content attribute not supported for template elements.", stacklevel=2
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return self._js.innerHTML
|
||||||
|
|
||||||
|
@content.setter
|
||||||
|
def content(self, value):
|
||||||
|
# TODO: (same comment as above)
|
||||||
|
if self._js.tagName == "TEMPLATE":
|
||||||
|
warnings.warn(
|
||||||
|
"Content attribute not supported for template elements.", stacklevel=2
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
display(value, target=self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._js.id
|
||||||
|
|
||||||
|
@id.setter
|
||||||
|
def id(self, value):
|
||||||
|
self._js.id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
if "options" in self._proxies:
|
||||||
|
return self._proxies["options"]
|
||||||
|
|
||||||
|
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no options attribute."
|
||||||
|
)
|
||||||
|
self._proxies["options"] = OptionsProxy(self)
|
||||||
|
return self._proxies["options"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._js.value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
# in order to avoid confusion to the user, we don't allow setting the
|
||||||
|
# value of elements that don't have a value attribute
|
||||||
|
if not hasattr(self._js, "value"):
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||||
|
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||||
|
"javascript API attribute instead."
|
||||||
|
)
|
||||||
|
self._js.value = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
return self._js.selected
|
||||||
|
|
||||||
|
@selected.setter
|
||||||
|
def selected(self, value):
|
||||||
|
# in order to avoid confusion to the user, we don't allow setting the
|
||||||
|
# value of elements that don't have a value attribute
|
||||||
|
if not hasattr(self._js, "selected"):
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._js.tagName} has no value attribute. If you want to "
|
||||||
|
"force a value attribute, set it directly using the `_js.value = <value>` "
|
||||||
|
"javascript API attribute instead."
|
||||||
|
)
|
||||||
|
self._js.selected = value
|
||||||
|
|
||||||
|
def clone(self, new_id=None):
|
||||||
|
clone = Element(self._js.cloneNode(True))
|
||||||
|
clone.id = new_id
|
||||||
|
|
||||||
|
return clone
|
||||||
|
|
||||||
|
def remove_class(self, classname):
|
||||||
|
classList = self._js.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.remove(*classname)
|
||||||
|
else:
|
||||||
|
classList.remove(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_class(self, classname):
|
||||||
|
classList = self._js.classList
|
||||||
|
if isinstance(classname, list):
|
||||||
|
classList.add(*classname)
|
||||||
|
else:
|
||||||
|
self._js.classList.add(classname)
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def classes(self):
|
||||||
|
classes = self._js.classList.values()
|
||||||
|
return [x for x in classes]
|
||||||
|
|
||||||
|
def show_me(self):
|
||||||
|
self._js.scrollIntoView()
|
||||||
|
|
||||||
|
def snap(
|
||||||
|
self,
|
||||||
|
to: BaseElement | str = None,
|
||||||
|
width: int | None = None,
|
||||||
|
height: int | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Captures a snapshot of a video element. (Only available for video elements)
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
* to: element where to save the snapshot of the video frame to
|
||||||
|
* width: width of the image
|
||||||
|
* height: height of the image
|
||||||
|
|
||||||
|
Output:
|
||||||
|
(Element) canvas element where the video frame snapshot was drawn into
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "VIDEO":
|
||||||
|
raise AttributeError("Snap method is only available for video Elements")
|
||||||
|
|
||||||
|
if to is None:
|
||||||
|
canvas = self.create("canvas")
|
||||||
|
if width is None:
|
||||||
|
width = self._js.width
|
||||||
|
if height is None:
|
||||||
|
height = self._js.height
|
||||||
|
canvas._js.width = width
|
||||||
|
canvas._js.height = height
|
||||||
|
|
||||||
|
elif isinstance(to, Element):
|
||||||
|
if to._js.tagName != "CANVAS":
|
||||||
|
raise TypeError("Element to snap to must a canvas.")
|
||||||
|
canvas = to
|
||||||
|
elif getattr(to, "tagName", "") == "CANVAS":
|
||||||
|
canvas = Element(to)
|
||||||
|
elif isinstance(to, str):
|
||||||
|
canvas = pydom[to][0]
|
||||||
|
if canvas._js.tagName != "CANVAS":
|
||||||
|
raise TypeError("Element to snap to must a be canvas.")
|
||||||
|
|
||||||
|
canvas.draw(self, width, height)
|
||||||
|
|
||||||
|
return canvas
|
||||||
|
|
||||||
|
def download(self, filename: str = "snapped.png") -> None:
|
||||||
|
"""Download the current element (only available for canvas elements) with the filename
|
||||||
|
provided in input.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
* filename (str): name of the file being downloaded
|
||||||
|
|
||||||
|
Output:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "CANVAS":
|
||||||
|
raise AttributeError(
|
||||||
|
"The download method is only available for canvas Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
link = self.create("a")
|
||||||
|
link._js.download = filename
|
||||||
|
link._js.href = self._js.toDataURL()
|
||||||
|
link._js.click()
|
||||||
|
|
||||||
|
def draw(self, what, width, height):
|
||||||
|
"""Draw `what` on the current element (only available for canvas elements).
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
* what (canvas image source): An element to draw into the context. The specification permits any canvas
|
||||||
|
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
|
||||||
|
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
|
||||||
|
"""
|
||||||
|
if self._js.tagName != "CANVAS":
|
||||||
|
raise AttributeError(
|
||||||
|
"The draw method is only available for canvas Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(what, Element):
|
||||||
|
what = what._js
|
||||||
|
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
|
||||||
|
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsProxy:
|
||||||
|
"""This class represents the options of a select element. It
|
||||||
|
allows to access to add and remove options by using the `add` and `remove` methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, element: Element) -> None:
|
||||||
|
self._element = element
|
||||||
|
if self._element._js.tagName.lower() != "select":
|
||||||
|
raise AttributeError(
|
||||||
|
f"Element {self._element._js.tagName} has no options attribute."
|
||||||
|
)
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
value: Any = None,
|
||||||
|
html: str = None,
|
||||||
|
text: str = None,
|
||||||
|
before: Element | int = None,
|
||||||
|
**kws,
|
||||||
|
) -> None:
|
||||||
|
"""Add a new option to the select element"""
|
||||||
|
# create the option element and set the attributes
|
||||||
|
option = document.createElement("option")
|
||||||
|
if value is not None:
|
||||||
|
kws["value"] = value
|
||||||
|
if html is not None:
|
||||||
|
option.innerHTML = html
|
||||||
|
if text is not None:
|
||||||
|
kws["text"] = text
|
||||||
|
|
||||||
|
for key, value in kws.items():
|
||||||
|
option.setAttribute(key, value)
|
||||||
|
|
||||||
|
if before:
|
||||||
|
if isinstance(before, Element):
|
||||||
|
before = before._js
|
||||||
|
|
||||||
|
self._element._js.add(option, before)
|
||||||
|
|
||||||
|
def remove(self, item: int) -> None:
|
||||||
|
"""Remove the option at the specified index"""
|
||||||
|
self._element._js.remove(item)
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
"""Remove all the options"""
|
||||||
|
for i in range(len(self)):
|
||||||
|
self.remove(0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
"""Return the list of options"""
|
||||||
|
return [Element(opt) for opt in self._element._js.options]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selected(self):
|
||||||
|
"""Return the selected option"""
|
||||||
|
return self.options[self._element._js.selectedIndex]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self.options
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.options)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.options[key]
|
||||||
|
|
||||||
|
|
||||||
|
class StyleProxy: # (dict):
|
||||||
|
def __init__(self, element: Element) -> None:
|
||||||
|
self._element = element
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def _style(self):
|
||||||
|
return self._element._js.style
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._style.getPropertyValue(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self._style.setProperty(key, value)
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
self._style.removeProperty(key)
|
||||||
|
|
||||||
|
def set(self, **kws):
|
||||||
|
for k, v in kws.items():
|
||||||
|
self._element._js.style.setProperty(k, v)
|
||||||
|
|
||||||
|
# CSS Properties
|
||||||
|
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
|
||||||
|
# Following prperties automatically generated from the above reference using
|
||||||
|
# tools/codegen_css_proxy.py
|
||||||
|
@property
|
||||||
|
def visible(self):
|
||||||
|
return self._element._js.style.visibility
|
||||||
|
|
||||||
|
@visible.setter
|
||||||
|
def visible(self, value):
|
||||||
|
self._element._js.style.visibility = value
|
||||||
|
|
||||||
|
|
||||||
|
class StyleCollection:
|
||||||
|
def __init__(self, collection: "ElementCollection") -> None:
|
||||||
|
self._collection = collection
|
||||||
|
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
return obj._get_attribute("style")
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._collection._get_attribute("style")[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style[key] = value
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
for element in self._collection._elements:
|
||||||
|
element.style.remove(key)
|
||||||
|
|
||||||
|
|
||||||
|
class ElementCollection:
|
||||||
|
def __init__(self, elements: [Element]) -> None:
|
||||||
|
self._elements = elements
|
||||||
|
self.style = StyleCollection(self)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
# If it's an integer we use it to access the elements in the collection
|
||||||
|
if isinstance(key, int):
|
||||||
|
return self._elements[key]
|
||||||
|
# If it's a slice we use it to support slice operations over the elements
|
||||||
|
# in the collection
|
||||||
|
elif isinstance(key, slice):
|
||||||
|
return ElementCollection(self._elements[key])
|
||||||
|
|
||||||
|
# If it's anything else (basically a string) we use it as a selector
|
||||||
|
# TODO: Write tests!
|
||||||
|
elements = self._element.querySelectorAll(key)
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._elements)
|
||||||
|
|
||||||
|
def __eq__(self, obj):
|
||||||
|
"""Check if the element is the same as the other element by comparing
|
||||||
|
the underlying JS element"""
|
||||||
|
return isinstance(obj, ElementCollection) and obj._elements == self._elements
|
||||||
|
|
||||||
|
def _get_attribute(self, attr, index=None):
|
||||||
|
if index is None:
|
||||||
|
return [getattr(el, attr) for el in self._elements]
|
||||||
|
|
||||||
|
# As JQuery, when getting an attr, only return it for the first element
|
||||||
|
return getattr(self._elements[index], attr)
|
||||||
|
|
||||||
|
def _set_attribute(self, attr, value):
|
||||||
|
for el in self._elements:
|
||||||
|
setattr(el, attr, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html(self):
|
||||||
|
return self._get_attribute("html")
|
||||||
|
|
||||||
|
@html.setter
|
||||||
|
def html(self, value):
|
||||||
|
self._set_attribute("html", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self._get_attribute("value")
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value):
|
||||||
|
self._set_attribute("value", value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def children(self):
|
||||||
|
return self._elements
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield from self._elements
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
|
||||||
|
|
||||||
|
|
||||||
|
class DomScope:
|
||||||
|
def __getattr__(self, __name: str):
|
||||||
|
element = document[f"#{__name}"]
|
||||||
|
if element:
|
||||||
|
return element[0]
|
||||||
|
|
||||||
|
|
||||||
|
class PyDom(BaseElement):
|
||||||
|
# Add objects we want to expose to the DOM namespace since this class instance is being
|
||||||
|
# remapped as "the module" itself
|
||||||
|
BaseElement = BaseElement
|
||||||
|
Element = Element
|
||||||
|
ElementCollection = ElementCollection
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# PyDom is a special case of BaseElement where we don't want to create a new JS element
|
||||||
|
# and it really doesn't have a need for styleproxy or parent to to call to __init__
|
||||||
|
# (which actually fails in MP for some reason)
|
||||||
|
self._js = document
|
||||||
|
self._parent = None
|
||||||
|
self._proxies = {}
|
||||||
|
self.ids = DomScope()
|
||||||
|
self.body = Element(document.body)
|
||||||
|
self.head = Element(document.head)
|
||||||
|
|
||||||
|
def create(self, type_, classes=None, html=None):
|
||||||
|
return super().create(type_, is_child=False, classes=classes, html=html)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
elements = self._js.querySelectorAll(key)
|
||||||
|
if not elements:
|
||||||
|
return None
|
||||||
|
return ElementCollection([Element(el) for el in elements])
|
||||||
|
|
||||||
|
|
||||||
|
dom = PyDom()
|
||||||
1
pyscript.core/src/stdlib/pyweb/ui/__init__.py
Normal file
1
pyscript.core/src/stdlib/pyweb/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import elements
|
||||||
947
pyscript.core/src/stdlib/pyweb/ui/elements.py
Normal file
947
pyscript.core/src/stdlib/pyweb/ui/elements.py
Normal file
@@ -0,0 +1,947 @@
|
|||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pyscript import document, when, window
|
||||||
|
from pyweb import JSProperty, pydom
|
||||||
|
|
||||||
|
#: A flag to show if MicroPython is the current Python interpreter.
|
||||||
|
is_micropython = "MicroPython" in sys.version
|
||||||
|
|
||||||
|
|
||||||
|
def getmembers_static(cls):
|
||||||
|
"""Cross-interpreter implementation of inspect.getmembers_static."""
|
||||||
|
|
||||||
|
if is_micropython: # pragma: no cover
|
||||||
|
return [(name, getattr(cls, name)) for name, _ in inspect.getmembers(cls)]
|
||||||
|
|
||||||
|
return inspect.getmembers_static(cls)
|
||||||
|
|
||||||
|
|
||||||
|
class ElementBase(pydom.Element):
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
# GLOBAL ATTRIBUTES
|
||||||
|
# These are attribute that all elements have (this list is a subset of the official one)
|
||||||
|
# We are trying to capture the most used ones
|
||||||
|
accesskey = JSProperty("accesskey")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
className = JSProperty("className")
|
||||||
|
contenteditable = JSProperty("contenteditable")
|
||||||
|
draggable = JSProperty("draggable")
|
||||||
|
enterkeyhint = JSProperty("enterkeyhint")
|
||||||
|
hidden = JSProperty("hidden")
|
||||||
|
id = JSProperty("id")
|
||||||
|
lang = JSProperty("lang")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
part = JSProperty("part")
|
||||||
|
popover = JSProperty("popover")
|
||||||
|
slot = JSProperty("slot")
|
||||||
|
spellcheck = JSProperty("spellcheck")
|
||||||
|
tabindex = JSProperty("tabindex")
|
||||||
|
title = JSProperty("title")
|
||||||
|
translate = JSProperty("translate")
|
||||||
|
virtualkeyboardpolicy = JSProperty("virtualkeyboardpolicy")
|
||||||
|
|
||||||
|
def __init__(self, style=None, **kwargs):
|
||||||
|
super().__init__(document.createElement(self.tag))
|
||||||
|
|
||||||
|
# set all the style properties provided in input
|
||||||
|
if isinstance(style, dict):
|
||||||
|
for key, value in style.items():
|
||||||
|
self.style[key] = value
|
||||||
|
elif style is None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Style should be a dictionary, received {style} (type {type(style)}) instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
# IMPORTANT!!! This is used to auto-harvest all input arguments and set them as properties
|
||||||
|
self._init_properties(**kwargs)
|
||||||
|
|
||||||
|
def _init_properties(self, **kwargs):
|
||||||
|
"""Set all the properties (of type JSProperties) provided in input as properties
|
||||||
|
of the class instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: The properties to set
|
||||||
|
"""
|
||||||
|
# Look at all the properties of the class and see if they were provided in kwargs
|
||||||
|
for attr_name, attr in getmembers_static(self.__class__):
|
||||||
|
# For each one, actually check if it is a property of the class and set it
|
||||||
|
if isinstance(attr, JSProperty) and attr_name in kwargs:
|
||||||
|
try:
|
||||||
|
setattr(self, attr_name, kwargs[attr_name])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting {attr_name} to {kwargs[attr_name]}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class TextElementBase(ElementBase):
|
||||||
|
def __init__(self, content=None, style=None, **kwargs):
|
||||||
|
super().__init__(style=style, **kwargs)
|
||||||
|
|
||||||
|
# If it's an element, append the element
|
||||||
|
if isinstance(content, pydom.Element):
|
||||||
|
self.append(content)
|
||||||
|
# If it's a list of elements
|
||||||
|
elif isinstance(content, list):
|
||||||
|
for item in content:
|
||||||
|
self.append(item)
|
||||||
|
# If the content wasn't set just ignore
|
||||||
|
elif content is None:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Otherwise, set content as the html of the element
|
||||||
|
self.html = content
|
||||||
|
|
||||||
|
|
||||||
|
# IMPORTANT: For all HTML components defined below, we are not mapping all
|
||||||
|
# available attributes, just the global and the most common ones.
|
||||||
|
# If you need to access a specific attribute, you can always use the `_js.<attribute>`
|
||||||
|
class a(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"""
|
||||||
|
|
||||||
|
tag = "a"
|
||||||
|
|
||||||
|
download = JSProperty("download")
|
||||||
|
href = JSProperty("href")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
target = JSProperty("target")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class abbr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"""
|
||||||
|
|
||||||
|
tag = "abbr"
|
||||||
|
|
||||||
|
|
||||||
|
class address(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"""
|
||||||
|
|
||||||
|
tag = "address"
|
||||||
|
|
||||||
|
|
||||||
|
class area(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area"""
|
||||||
|
|
||||||
|
tag = "area"
|
||||||
|
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
coords = JSProperty("coords")
|
||||||
|
download = JSProperty("download")
|
||||||
|
href = JSProperty("href")
|
||||||
|
ping = JSProperty("ping")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
shape = JSProperty("shape")
|
||||||
|
target = JSProperty("target")
|
||||||
|
|
||||||
|
|
||||||
|
class article(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article"""
|
||||||
|
|
||||||
|
tag = "article"
|
||||||
|
|
||||||
|
|
||||||
|
class aside(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside"""
|
||||||
|
|
||||||
|
tag = "aside"
|
||||||
|
|
||||||
|
|
||||||
|
class audio(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio"""
|
||||||
|
|
||||||
|
tag = "audio"
|
||||||
|
|
||||||
|
autoplay = JSProperty("autoplay")
|
||||||
|
controls = JSProperty("controls")
|
||||||
|
controlslist = JSProperty("controlslist")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disableremoteplayback = JSProperty("disableremoteplayback")
|
||||||
|
loop = JSProperty("loop")
|
||||||
|
muted = JSProperty("muted")
|
||||||
|
preload = JSProperty("preload")
|
||||||
|
src = JSProperty("src")
|
||||||
|
|
||||||
|
|
||||||
|
class b(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"""
|
||||||
|
|
||||||
|
tag = "b"
|
||||||
|
|
||||||
|
|
||||||
|
class blockquote(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"""
|
||||||
|
|
||||||
|
tag = "blockquote"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
|
||||||
|
|
||||||
|
class br(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br"""
|
||||||
|
|
||||||
|
tag = "br"
|
||||||
|
|
||||||
|
|
||||||
|
class button(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"""
|
||||||
|
|
||||||
|
tag = "button"
|
||||||
|
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
formaction = JSProperty("formaction")
|
||||||
|
formenctype = JSProperty("formenctype")
|
||||||
|
formmethod = JSProperty("formmethod")
|
||||||
|
formnovalidate = JSProperty("formnovalidate")
|
||||||
|
formtarget = JSProperty("formtarget")
|
||||||
|
name = JSProperty("name")
|
||||||
|
type = JSProperty("type")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class canvas(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
|
||||||
|
|
||||||
|
tag = "canvas"
|
||||||
|
|
||||||
|
height = JSProperty("height")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class caption(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"""
|
||||||
|
|
||||||
|
tag = "caption"
|
||||||
|
|
||||||
|
|
||||||
|
class cite(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite"""
|
||||||
|
|
||||||
|
tag = "cite"
|
||||||
|
|
||||||
|
|
||||||
|
class code(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code"""
|
||||||
|
|
||||||
|
tag = "code"
|
||||||
|
|
||||||
|
|
||||||
|
class data(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"""
|
||||||
|
|
||||||
|
tag = "data"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class datalist(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist"""
|
||||||
|
|
||||||
|
tag = "datalist"
|
||||||
|
|
||||||
|
|
||||||
|
class dd(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd"""
|
||||||
|
|
||||||
|
tag = "dd"
|
||||||
|
|
||||||
|
|
||||||
|
class del_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del"""
|
||||||
|
|
||||||
|
tag = "del"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class details(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"""
|
||||||
|
|
||||||
|
tag = "details"
|
||||||
|
|
||||||
|
open = JSProperty("open")
|
||||||
|
|
||||||
|
|
||||||
|
class dialog(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"""
|
||||||
|
|
||||||
|
tag = "dialog"
|
||||||
|
|
||||||
|
open = JSProperty("open")
|
||||||
|
|
||||||
|
|
||||||
|
class div(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"""
|
||||||
|
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
|
||||||
|
class dl(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"""
|
||||||
|
|
||||||
|
tag = "dl"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class dt(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt"""
|
||||||
|
|
||||||
|
tag = "dt"
|
||||||
|
|
||||||
|
|
||||||
|
class em(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"""
|
||||||
|
|
||||||
|
tag = "em"
|
||||||
|
|
||||||
|
|
||||||
|
class embed(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed"""
|
||||||
|
|
||||||
|
tag = "embed"
|
||||||
|
|
||||||
|
height = JSProperty("height")
|
||||||
|
src = JSProperty("src")
|
||||||
|
type = JSProperty("type")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class fieldset(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"""
|
||||||
|
|
||||||
|
tag = "fieldset"
|
||||||
|
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class figcaption(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"""
|
||||||
|
|
||||||
|
tag = "figcaption"
|
||||||
|
|
||||||
|
|
||||||
|
class figure(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"""
|
||||||
|
|
||||||
|
tag = "figure"
|
||||||
|
|
||||||
|
|
||||||
|
class footer(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"""
|
||||||
|
|
||||||
|
tag = "footer"
|
||||||
|
|
||||||
|
|
||||||
|
class form(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"""
|
||||||
|
|
||||||
|
tag = "form"
|
||||||
|
|
||||||
|
accept_charset = JSProperty("accept-charset")
|
||||||
|
action = JSProperty("action")
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
autocomplete = JSProperty("autocomplete")
|
||||||
|
enctype = JSProperty("enctype")
|
||||||
|
name = JSProperty("name")
|
||||||
|
method = JSProperty("method")
|
||||||
|
nonvalidate = JSProperty("nonvalidate")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
target = JSProperty("target")
|
||||||
|
|
||||||
|
|
||||||
|
class h1(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1"""
|
||||||
|
|
||||||
|
tag = "h1"
|
||||||
|
|
||||||
|
|
||||||
|
class h2(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2"""
|
||||||
|
|
||||||
|
tag = "h2"
|
||||||
|
|
||||||
|
|
||||||
|
class h3(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3"""
|
||||||
|
|
||||||
|
tag = "h3"
|
||||||
|
|
||||||
|
|
||||||
|
class h4(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4"""
|
||||||
|
|
||||||
|
tag = "h4"
|
||||||
|
|
||||||
|
|
||||||
|
class h5(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5"""
|
||||||
|
|
||||||
|
tag = "h5"
|
||||||
|
|
||||||
|
|
||||||
|
class h6(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6"""
|
||||||
|
|
||||||
|
tag = "h6"
|
||||||
|
|
||||||
|
|
||||||
|
class header(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"""
|
||||||
|
|
||||||
|
tag = "header"
|
||||||
|
|
||||||
|
|
||||||
|
class hgroup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup"""
|
||||||
|
|
||||||
|
tag = "hgroup"
|
||||||
|
|
||||||
|
|
||||||
|
class hr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr"""
|
||||||
|
|
||||||
|
tag = "hr"
|
||||||
|
|
||||||
|
|
||||||
|
class i(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"""
|
||||||
|
|
||||||
|
tag = "i"
|
||||||
|
|
||||||
|
|
||||||
|
class iframe(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"""
|
||||||
|
|
||||||
|
tag = "iframe"
|
||||||
|
|
||||||
|
allow = JSProperty("allow")
|
||||||
|
allowfullscreen = JSProperty("allowfullscreen")
|
||||||
|
height = JSProperty("height")
|
||||||
|
loading = JSProperty("loading")
|
||||||
|
name = JSProperty("name")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sandbox = JSProperty("sandbox")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srcdoc = JSProperty("srcdoc")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class img(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"""
|
||||||
|
|
||||||
|
tag = "img"
|
||||||
|
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
decoding = JSProperty("decoding")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
height = JSProperty("height")
|
||||||
|
ismap = JSProperty("ismap")
|
||||||
|
loading = JSProperty("loading")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
src = JSProperty("src")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Input is a reserved keyword in Python, so we use input_ instead
|
||||||
|
class input_(ElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"""
|
||||||
|
|
||||||
|
tag = "input"
|
||||||
|
|
||||||
|
accept = JSProperty("accept")
|
||||||
|
alt = JSProperty("alt")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
capture = JSProperty("capture")
|
||||||
|
checked = JSProperty("checked")
|
||||||
|
dirname = JSProperty("dirname")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
formaction = JSProperty("formaction")
|
||||||
|
formenctype = JSProperty("formenctype")
|
||||||
|
formmethod = JSProperty("formmethod")
|
||||||
|
formnovalidate = JSProperty("formnovalidate")
|
||||||
|
formtarget = JSProperty("formtarget")
|
||||||
|
height = JSProperty("height")
|
||||||
|
list = JSProperty("list")
|
||||||
|
max = JSProperty("max")
|
||||||
|
maxlength = JSProperty("maxlength")
|
||||||
|
min = JSProperty("min")
|
||||||
|
minlength = JSProperty("minlength")
|
||||||
|
multiple = JSProperty("multiple")
|
||||||
|
name = JSProperty("name")
|
||||||
|
pattern = JSProperty("pattern")
|
||||||
|
placeholder = JSProperty("placeholder")
|
||||||
|
popovertarget = JSProperty("popovertarget")
|
||||||
|
popovertargetaction = JSProperty("popovertargetaction")
|
||||||
|
readonly = JSProperty("readonly")
|
||||||
|
required = JSProperty("required")
|
||||||
|
size = JSProperty("size")
|
||||||
|
src = JSProperty("src")
|
||||||
|
step = JSProperty("step")
|
||||||
|
type = JSProperty("type")
|
||||||
|
value = JSProperty("value")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class ins(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins"""
|
||||||
|
|
||||||
|
tag = "ins"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class kbd(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd"""
|
||||||
|
|
||||||
|
tag = "kbd"
|
||||||
|
|
||||||
|
|
||||||
|
class label(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label"""
|
||||||
|
|
||||||
|
tag = "label"
|
||||||
|
|
||||||
|
for_ = JSProperty("for")
|
||||||
|
|
||||||
|
|
||||||
|
class legend(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"""
|
||||||
|
|
||||||
|
tag = "legend"
|
||||||
|
|
||||||
|
|
||||||
|
class li(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li"""
|
||||||
|
|
||||||
|
tag = "li"
|
||||||
|
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class link(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link"""
|
||||||
|
|
||||||
|
tag = "link"
|
||||||
|
|
||||||
|
as_ = JSProperty("as")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
href = JSProperty("href")
|
||||||
|
imagesizes = JSProperty("imagesizes")
|
||||||
|
imagesrcset = JSProperty("imagesrcset")
|
||||||
|
integrity = JSProperty("integrity")
|
||||||
|
media = JSProperty("media")
|
||||||
|
rel = JSProperty("rel")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
title = JSProperty("title")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class main(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main"""
|
||||||
|
|
||||||
|
tag = "main"
|
||||||
|
|
||||||
|
|
||||||
|
class map_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"""
|
||||||
|
|
||||||
|
tag = "map"
|
||||||
|
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class mark(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"""
|
||||||
|
|
||||||
|
tag = "mark"
|
||||||
|
|
||||||
|
|
||||||
|
class menu(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu"""
|
||||||
|
|
||||||
|
tag = "menu"
|
||||||
|
|
||||||
|
|
||||||
|
class meter(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter"""
|
||||||
|
|
||||||
|
tag = "meter"
|
||||||
|
|
||||||
|
form = JSProperty("form")
|
||||||
|
high = JSProperty("high")
|
||||||
|
low = JSProperty("low")
|
||||||
|
max = JSProperty("max")
|
||||||
|
min = JSProperty("min")
|
||||||
|
optimum = JSProperty("optimum")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class nav(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav"""
|
||||||
|
|
||||||
|
tag = "nav"
|
||||||
|
|
||||||
|
|
||||||
|
class object_(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object"""
|
||||||
|
|
||||||
|
tag = "object"
|
||||||
|
|
||||||
|
data = JSProperty("data")
|
||||||
|
form = JSProperty("form")
|
||||||
|
height = JSProperty("height")
|
||||||
|
name = JSProperty("name")
|
||||||
|
type = JSProperty("type")
|
||||||
|
usemap = JSProperty("usemap")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
class ol(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol"""
|
||||||
|
|
||||||
|
tag = "ol"
|
||||||
|
|
||||||
|
reversed = JSProperty("reversed")
|
||||||
|
start = JSProperty("start")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class optgroup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup"""
|
||||||
|
|
||||||
|
tag = "optgroup"
|
||||||
|
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
label = JSProperty("label")
|
||||||
|
|
||||||
|
|
||||||
|
class option(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"""
|
||||||
|
|
||||||
|
tag = "option"
|
||||||
|
|
||||||
|
disabled = JSProperty("value")
|
||||||
|
label = JSProperty("label")
|
||||||
|
selected = JSProperty("selected")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class output(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"""
|
||||||
|
|
||||||
|
tag = "output"
|
||||||
|
|
||||||
|
for_ = JSProperty("for")
|
||||||
|
form = JSProperty("form")
|
||||||
|
name = JSProperty("name")
|
||||||
|
|
||||||
|
|
||||||
|
class p(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p"""
|
||||||
|
|
||||||
|
tag = "p"
|
||||||
|
|
||||||
|
|
||||||
|
class picture(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"""
|
||||||
|
|
||||||
|
tag = "picture"
|
||||||
|
|
||||||
|
|
||||||
|
class pre(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"""
|
||||||
|
|
||||||
|
tag = "pre"
|
||||||
|
|
||||||
|
|
||||||
|
class progress(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"""
|
||||||
|
|
||||||
|
tag = "progress"
|
||||||
|
|
||||||
|
max = JSProperty("max")
|
||||||
|
value = JSProperty("value")
|
||||||
|
|
||||||
|
|
||||||
|
class q(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q"""
|
||||||
|
|
||||||
|
tag = "q"
|
||||||
|
|
||||||
|
cite = JSProperty("cite")
|
||||||
|
|
||||||
|
|
||||||
|
class s(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s"""
|
||||||
|
|
||||||
|
tag = "s"
|
||||||
|
|
||||||
|
|
||||||
|
class script(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"""
|
||||||
|
|
||||||
|
tag = "script"
|
||||||
|
|
||||||
|
# Let's add async manually since it's a reserved keyword in Python
|
||||||
|
async_ = JSProperty("async")
|
||||||
|
blocking = JSProperty("blocking")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
defer = JSProperty("defer")
|
||||||
|
fetchpriority = JSProperty("fetchpriority")
|
||||||
|
integrity = JSProperty("integrity")
|
||||||
|
nomodule = JSProperty("nomodule")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
referrerpolicy = JSProperty("referrerpolicy")
|
||||||
|
src = JSProperty("src")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class section(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"""
|
||||||
|
|
||||||
|
tag = "section"
|
||||||
|
|
||||||
|
|
||||||
|
class select(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"""
|
||||||
|
|
||||||
|
tag = "select"
|
||||||
|
|
||||||
|
|
||||||
|
class small(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small"""
|
||||||
|
|
||||||
|
tag = "small"
|
||||||
|
|
||||||
|
|
||||||
|
class source(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"""
|
||||||
|
|
||||||
|
tag = "source"
|
||||||
|
|
||||||
|
media = JSProperty("media")
|
||||||
|
sizes = JSProperty("sizes")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srcset = JSProperty("srcset")
|
||||||
|
type = JSProperty("type")
|
||||||
|
|
||||||
|
|
||||||
|
class span(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span"""
|
||||||
|
|
||||||
|
tag = "span"
|
||||||
|
|
||||||
|
|
||||||
|
class strong(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"""
|
||||||
|
|
||||||
|
tag = "strong"
|
||||||
|
|
||||||
|
|
||||||
|
class style(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style"""
|
||||||
|
|
||||||
|
tag = "style"
|
||||||
|
|
||||||
|
blocking = JSProperty("blocking")
|
||||||
|
media = JSProperty("media")
|
||||||
|
nonce = JSProperty("nonce")
|
||||||
|
title = JSProperty("title")
|
||||||
|
|
||||||
|
|
||||||
|
class sub(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub"""
|
||||||
|
|
||||||
|
tag = "sub"
|
||||||
|
|
||||||
|
|
||||||
|
class summary(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"""
|
||||||
|
|
||||||
|
tag = "summary"
|
||||||
|
|
||||||
|
|
||||||
|
class sup(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup"""
|
||||||
|
|
||||||
|
tag = "sup"
|
||||||
|
|
||||||
|
|
||||||
|
class table(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"""
|
||||||
|
|
||||||
|
tag = "table"
|
||||||
|
|
||||||
|
|
||||||
|
class tbody(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody"""
|
||||||
|
|
||||||
|
tag = "tbody"
|
||||||
|
|
||||||
|
|
||||||
|
class td(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td"""
|
||||||
|
|
||||||
|
tag = "td"
|
||||||
|
|
||||||
|
colspan = JSProperty("colspan")
|
||||||
|
headers = JSProperty("headers")
|
||||||
|
rowspan = JSProperty("rowspan")
|
||||||
|
|
||||||
|
|
||||||
|
class template(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"""
|
||||||
|
|
||||||
|
tag = "template"
|
||||||
|
|
||||||
|
shadowrootmode = JSProperty("shadowrootmode")
|
||||||
|
|
||||||
|
|
||||||
|
class textarea(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"""
|
||||||
|
|
||||||
|
tag = "textarea"
|
||||||
|
|
||||||
|
autocapitalize = JSProperty("autocapitalize")
|
||||||
|
autocomplete = JSProperty("autocomplete")
|
||||||
|
autofocus = JSProperty("autofocus")
|
||||||
|
cols = JSProperty("cols")
|
||||||
|
dirname = JSProperty("dirname")
|
||||||
|
disabled = JSProperty("disabled")
|
||||||
|
form = JSProperty("form")
|
||||||
|
maxlength = JSProperty("maxlength")
|
||||||
|
minlength = JSProperty("minlength")
|
||||||
|
name = JSProperty("name")
|
||||||
|
placeholder = JSProperty("placeholder")
|
||||||
|
readonly = JSProperty("readonly")
|
||||||
|
required = JSProperty("required")
|
||||||
|
rows = JSProperty("rows")
|
||||||
|
spellcheck = JSProperty("spellcheck")
|
||||||
|
wrap = JSProperty("wrap")
|
||||||
|
|
||||||
|
|
||||||
|
class tfoot(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot"""
|
||||||
|
|
||||||
|
tag = "tfoot"
|
||||||
|
|
||||||
|
|
||||||
|
class th(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th"""
|
||||||
|
|
||||||
|
tag = "th"
|
||||||
|
|
||||||
|
|
||||||
|
class thead(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead"""
|
||||||
|
|
||||||
|
tag = "thead"
|
||||||
|
|
||||||
|
|
||||||
|
class time(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time"""
|
||||||
|
|
||||||
|
tag = "time"
|
||||||
|
|
||||||
|
datetime = JSProperty("datetime")
|
||||||
|
|
||||||
|
|
||||||
|
class title(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title"""
|
||||||
|
|
||||||
|
tag = "title"
|
||||||
|
|
||||||
|
|
||||||
|
class tr(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr"""
|
||||||
|
|
||||||
|
tag = "tr"
|
||||||
|
|
||||||
|
abbr = JSProperty("abbr")
|
||||||
|
colspan = JSProperty("colspan")
|
||||||
|
headers = JSProperty("headers")
|
||||||
|
rowspan = JSProperty("rowspan")
|
||||||
|
scope = JSProperty("scope")
|
||||||
|
|
||||||
|
|
||||||
|
class track(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"""
|
||||||
|
|
||||||
|
tag = "track"
|
||||||
|
|
||||||
|
default = JSProperty("default")
|
||||||
|
kind = JSProperty("kind")
|
||||||
|
label = JSProperty("label")
|
||||||
|
src = JSProperty("src")
|
||||||
|
srclang = JSProperty("srclang")
|
||||||
|
|
||||||
|
|
||||||
|
class u(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"""
|
||||||
|
|
||||||
|
tag = "u"
|
||||||
|
|
||||||
|
|
||||||
|
class ul(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul"""
|
||||||
|
|
||||||
|
tag = "ul"
|
||||||
|
|
||||||
|
|
||||||
|
class var(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var"""
|
||||||
|
|
||||||
|
tag = "var"
|
||||||
|
|
||||||
|
|
||||||
|
class video(TextElementBase):
|
||||||
|
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"""
|
||||||
|
|
||||||
|
tag = "video"
|
||||||
|
|
||||||
|
autoplay = JSProperty("autoplay")
|
||||||
|
controls = JSProperty("controls")
|
||||||
|
crossorigin = JSProperty("crossorigin")
|
||||||
|
disablepictureinpicture = JSProperty("disablepictureinpicture")
|
||||||
|
disableremoteplayback = JSProperty("disableremoteplayback")
|
||||||
|
height = JSProperty("height")
|
||||||
|
loop = JSProperty("loop")
|
||||||
|
muted = JSProperty("muted")
|
||||||
|
playsinline = JSProperty("playsinline")
|
||||||
|
poster = JSProperty("poster")
|
||||||
|
preload = JSProperty("preload")
|
||||||
|
src = JSProperty("src")
|
||||||
|
width = JSProperty("width")
|
||||||
|
|
||||||
|
|
||||||
|
# Custom Elements
|
||||||
|
class grid(TextElementBase):
|
||||||
|
tag = "div"
|
||||||
|
|
||||||
|
def __init__(self, layout, content=None, gap=None, **kwargs):
|
||||||
|
super().__init__(content, **kwargs)
|
||||||
|
self.style["display"] = "grid"
|
||||||
|
self.style["grid-template-columns"] = layout
|
||||||
|
|
||||||
|
# TODO: This should be a property
|
||||||
|
if not gap is None:
|
||||||
|
self.style["gap"] = gap
|
||||||
12
pyscript.core/src/sync.js
Normal file
12
pyscript.core/src/sync.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
// allow pyterminal checks to bootstrap
|
||||||
|
is_pyterminal: () => false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 'Sleep' for the given number of seconds. Used to implement Python's time.sleep in Worker threads.
|
||||||
|
* @param {number} seconds The number of seconds to sleep.
|
||||||
|
*/
|
||||||
|
sleep(seconds) {
|
||||||
|
return new Promise(($) => setTimeout($, seconds * 1000));
|
||||||
|
},
|
||||||
|
};
|
||||||
4
pyscript.core/src/types.js
Normal file
4
pyscript.core/src/types.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export default new Map([
|
||||||
|
["py", "pyodide"],
|
||||||
|
["mpy", "micropython"],
|
||||||
|
]);
|
||||||
1
pyscript.core/test/a.py
Normal file
1
pyscript.core/test/a.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print("a")
|
||||||
39
pyscript.core/test/all-done.html
Normal file
39
pyscript.core/test/all-done.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import '../dist/core.js';
|
||||||
|
|
||||||
|
document.body.append('loading ...', document.createElement('br'));
|
||||||
|
|
||||||
|
addEventListener(
|
||||||
|
'py:all-done',
|
||||||
|
() => {
|
||||||
|
document.body.append('all executed');
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py">
|
||||||
|
print(1)
|
||||||
|
</script>
|
||||||
|
<py-script>
|
||||||
|
print(2)
|
||||||
|
</py-script>
|
||||||
|
<py-script async>
|
||||||
|
print(3)
|
||||||
|
</py-script>
|
||||||
|
<script type="py" worker>
|
||||||
|
print(4)
|
||||||
|
</script>
|
||||||
|
<py-script async worker>
|
||||||
|
print(5)
|
||||||
|
</py-script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
pyscript.core/test/async.html
Normal file
15
pyscript.core/test/async.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<py-script async>
|
||||||
|
import asyncio
|
||||||
|
print('foo')
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
print('bar')
|
||||||
|
</py-script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
pyscript.core/test/bad.toml
Normal file
1
pyscript.core/test/bad.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
files = [
|
||||||
24
pyscript.core/test/camera.html
Normal file
24
pyscript.core/test/camera.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Media Example</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py" src="camera.py" async></script>
|
||||||
|
|
||||||
|
<label for="cars">Choose a device:</label>
|
||||||
|
|
||||||
|
<select name="devices" id="devices"></select>
|
||||||
|
|
||||||
|
<button id="pick-device">Select the device</button>
|
||||||
|
<button id="snap">Snap</button>
|
||||||
|
|
||||||
|
<div id="result"></div>
|
||||||
|
|
||||||
|
<video id="video" width="600" height="400" autoplay></video>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
pyscript.core/test/camera.py
Normal file
32
pyscript.core/test/camera.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from pyodide.ffi import create_proxy
|
||||||
|
from pyscript import display, document, when, window
|
||||||
|
from pyweb import media, pydom
|
||||||
|
|
||||||
|
devicesSelect = pydom["#devices"][0]
|
||||||
|
video = pydom["video"][0]
|
||||||
|
devices = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def list_media_devices(event=None):
|
||||||
|
"""List the available media devices."""
|
||||||
|
global devices
|
||||||
|
for i, device in enumerate(await media.list_devices()):
|
||||||
|
devices[device.id] = device
|
||||||
|
label = f"{i} - ({device.kind}) {device.label} [{device.id}]"
|
||||||
|
devicesSelect.options.add(value=device.id, html=label)
|
||||||
|
|
||||||
|
|
||||||
|
@when("click", "#pick-device")
|
||||||
|
async def connect_to_device(e):
|
||||||
|
"""Connect to the selected device."""
|
||||||
|
device = devices[devicesSelect.value]
|
||||||
|
video._js.srcObject = await device.get_stream()
|
||||||
|
|
||||||
|
|
||||||
|
@when("click", "#snap")
|
||||||
|
async def camera_click(e):
|
||||||
|
"""Take a picture and download it."""
|
||||||
|
video.snap().download()
|
||||||
|
|
||||||
|
|
||||||
|
await list_media_devices()
|
||||||
29
pyscript.core/test/click.html
Normal file
29
pyscript.core/test/click.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Next Plugin Bug?</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py">
|
||||||
|
from pyscript import display, document
|
||||||
|
from datetime import datetime as dt
|
||||||
|
from pyodide.ffi.wrappers import add_event_listener
|
||||||
|
|
||||||
|
element = document.querySelector("#just-a-button")
|
||||||
|
|
||||||
|
def on_click(event):
|
||||||
|
print(f"Hello from Python! {dt.now()}")
|
||||||
|
display(f"Hello from Python! {dt.now()}", append=False, target='result')
|
||||||
|
|
||||||
|
add_event_listener(element, "click", on_click)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button id="just-a-button">click and check the console</button>
|
||||||
|
|
||||||
|
<div id="result"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
pyscript.core/test/code-a-part.html
Normal file
19
pyscript.core/test/code-a-part.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module">
|
||||||
|
import { hooks } from "../dist/core.js";
|
||||||
|
hooks.main.codeBeforeRun.add('print(0)');
|
||||||
|
hooks.main.codeAfterRun.add('print(2)');
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py">
|
||||||
|
# raise an error instead to see it on line 1
|
||||||
|
print(1)
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
pyscript.core/test/combo.html
Normal file
17
pyscript.core/test/combo.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Error</title>
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<py-config>
|
||||||
|
[[fetch]]
|
||||||
|
files = ["a.py"]
|
||||||
|
</py-config>
|
||||||
|
<script type="py" worker>
|
||||||
|
import a
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
25
pyscript.core/test/config-url.html
Normal file
25
pyscript.core/test/config-url.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Next Plugin</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<mpy-config src="config-url/config.json"></mpy-config>
|
||||||
|
<script type="mpy">
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
|
from runtime import test
|
||||||
|
</script>
|
||||||
|
<script type="mpy" worker>
|
||||||
|
from pyscript import config
|
||||||
|
if config["files"]["{TO}"] != "./runtime":
|
||||||
|
raise Exception("wrong config tree")
|
||||||
|
|
||||||
|
from runtime import test
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
7
pyscript.core/test/config-url/config.json
Normal file
7
pyscript.core/test/config-url/config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files":{
|
||||||
|
"{FROM}": "./src",
|
||||||
|
"{TO}": "./runtime",
|
||||||
|
"{FROM}/test.py": "{TO}/test.py"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
pyscript.core/test/config-url/src/test.py
Normal file
8
pyscript.core/test/config-url/src/test.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from pyscript import RUNNING_IN_WORKER, document
|
||||||
|
|
||||||
|
classList = document.documentElement.classList
|
||||||
|
|
||||||
|
if RUNNING_IN_WORKER:
|
||||||
|
classList.add("worker")
|
||||||
|
else:
|
||||||
|
classList.add("main")
|
||||||
13
pyscript.core/test/config.html
Normal file
13
pyscript.core/test/config.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Next Plugin</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<py-config>
|
||||||
|
files = [
|
||||||
|
</py-config>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
5
pyscript.core/test/config.json
Normal file
5
pyscript.core/test/config.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"fetch": [{
|
||||||
|
"files": ["./a.py"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
19
pyscript.core/test/config/ambiguous-config.html
Normal file
19
pyscript.core/test/config/ambiguous-config.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<script>
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
document.body.innerHTML += `<p>${message}</p>`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<py-config>name = "first"</py-config>
|
||||||
|
<script type="py" config="second.toml"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
pyscript.core/test/config/index.html
Normal file
16
pyscript.core/test/config/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
<li><a href="./ambiguous-config.html">ambiguous py-config VS config attribute</a></li>
|
||||||
|
<li><a href="./too-many-config.html">too many config attributes</a></li>
|
||||||
|
<li><a href="./too-many-py-config.html">too many <py-config></a></li>
|
||||||
|
<li><a href="./same-config.html">same config attributes</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
pyscript.core/test/config/same-config.html
Normal file
20
pyscript.core/test/config/same-config.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<script>
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
document.body.innerHTML += `<p>${message}</p>`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
OK
|
||||||
|
<script type="py" config='{"name":"OK"}'></script>
|
||||||
|
<script type="py" config='{"name":"OK"}'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
pyscript.core/test/config/too-many-config.html
Normal file
19
pyscript.core/test/config/too-many-config.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<script>
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
document.body.innerHTML += `<p>${message}</p>`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py" config="first.toml"></script>
|
||||||
|
<script type="py" config="second.toml"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
pyscript.core/test/config/too-many-py-config.html
Normal file
19
pyscript.core/test/config/too-many-py-config.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<script>
|
||||||
|
addEventListener("error", ({ message }) => {
|
||||||
|
document.body.innerHTML += `<p>${message}</p>`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="../../dist/core.css">
|
||||||
|
<script type="module" src="../../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<py-config>name = "first"</py-config>
|
||||||
|
<py-config>name = "second"</py-config>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
pyscript.core/test/create-element.html
Normal file
36
pyscript.core/test/create-element.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
customElements.whenDefined('py-script').then(PyScript => {
|
||||||
|
const textContent = `
|
||||||
|
from pyscript import display
|
||||||
|
|
||||||
|
display("Hello World")
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.append(
|
||||||
|
// test <script type="py">
|
||||||
|
Object.assign(
|
||||||
|
document.createElement('script'),
|
||||||
|
{ type: "py", textContent }
|
||||||
|
),
|
||||||
|
|
||||||
|
// test <py-script>
|
||||||
|
Object.assign(
|
||||||
|
document.createElement('py-script'),
|
||||||
|
{ textContent }
|
||||||
|
),
|
||||||
|
|
||||||
|
// test PyScript class
|
||||||
|
Object.assign(
|
||||||
|
new PyScript(),
|
||||||
|
{ textContent }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
31
pyscript.core/test/dialog.html
Normal file
31
pyscript.core/test/dialog.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
const loader = document.querySelector('#loader');
|
||||||
|
loader.showModal();
|
||||||
|
addEventListener(
|
||||||
|
'py:all-done',
|
||||||
|
() => {
|
||||||
|
loader.close();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<script type="py">
|
||||||
|
from pyscript import document
|
||||||
|
|
||||||
|
document.body.textContent = "PyScript Ready";
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<dialog id="loader">
|
||||||
|
Loading PyScript ...
|
||||||
|
</dialog>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
pyscript.core/test/display.html
Normal file
30
pyscript.core/test/display.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>PyScript Next</title>
|
||||||
|
<script>
|
||||||
|
addEventListener("py:all-done", ({ type }) => console.log(type));
|
||||||
|
</script>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="py" worker async>
|
||||||
|
from pyscript import display
|
||||||
|
display('hello 1')
|
||||||
|
|
||||||
|
import js
|
||||||
|
import time
|
||||||
|
js.console.log('sleeping...')
|
||||||
|
time.sleep(2)
|
||||||
|
js.console.log('...done')
|
||||||
|
</script>
|
||||||
|
<p>hello 2</p>
|
||||||
|
<script type="py" worker async>
|
||||||
|
from pyscript import display
|
||||||
|
display('hello 3')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
pyscript.core/test/error.html
Normal file
23
pyscript.core/test/error.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>PyScript Next Plugin</title>
|
||||||
|
<link rel="stylesheet" href="../dist/core.css">
|
||||||
|
<script type="module" src="../dist/core.js"></script>
|
||||||
|
<script type="py">
|
||||||
|
print(1, 2, 3)
|
||||||
|
first()
|
||||||
|
</script>
|
||||||
|
<py-script>
|
||||||
|
print(4, 5, 6)
|
||||||
|
second()
|
||||||
|
</py-script>
|
||||||
|
<py-script src="whatever.py">
|
||||||
|
print(4, 5, 6)
|
||||||
|
second()
|
||||||
|
</py-script>
|
||||||
|
<py-script src="main.py" worker="worker.py"></py-script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user