Compare commits

...

47 Commits

Author SHA1 Message Date
Andrea Giammarchi
42c6cb775e Added all 3rd party licenses to this project (#2343) 2025-05-21 14:30:55 +01:00
Andrea Giammarchi
b11fb2e893 Added polyscript/service-worker as workaround for missing sabayon (#2334)
* Added polyscript/service-worker as workaround for missing sabayon

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* updated polyscript to its latest

* Updated polyscript one mor time

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-15 18:20:38 +02:00
pre-commit-ci[bot]
3223a9c7e9 [pre-commit.ci] pre-commit autoupdate (#2336)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-06 09:22:05 -04:00
Andrea Giammarchi
139ce9b5fb Updated Polyscript & Coincident to their latest (#2327) 2025-04-23 17:56:06 +02:00
pre-commit-ci[bot]
3b1af0688c [pre-commit.ci] pre-commit autoupdate (#2307)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.9.6 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.6...v0.11.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Dan Yeaw <dan@yeaw.me>
2025-04-18 12:02:24 -04:00
Andrea Giammarchi
7284f7f15f Fix #2320 - Dispatch an event on code run in PyEditor (#2329)
* Fix #2320 - Dispatch an event on code run in PyEditor
2025-04-16 22:01:40 +02:00
Andrea Giammarchi
16ebc50481 Fix #2326 - Redirect to localhost when on 0.0.0.0 (#2328)
* Fix #2326 - Redirect to localhost when on 0.0.0.0
2025-04-14 15:55:10 +02:00
Dan Yeaw
b911ea99fb Add media module tests (#2306)
* Add media Python tests

* Add media js test

* Remove try except blocks

* Make Python tests more end-to-end

* Add media Python tests

* Add media js test

* Remove try except blocks

* Make Python tests more end-to-end

* MicroPython explorations.

* Fix websocket tests, so they just skip.

* Fix MicroPython media tests, if no permission is given for a video device.

---------

Co-authored-by: Nicholas H.Tollervey <ntoll@ntoll.org>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-03-20 14:35:01 +01:00
Dan Yeaw
46ca9154c4 Add conda as a make setup option (#2305)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-03-20 12:10:31 +01:00
Andrea Giammarchi
afd7a8eb00 Updated Pyodide to v0.27.4 (#2318) 2025-03-19 15:00:20 +01:00
Andrea Giammarchi
b22f384d73 PyGame - TOML + JSON absolute URL + input patch (#2313)
* Make config URL canonical

* Better baseURL + input patch
2025-03-11 11:32:30 +01:00
Andrea Giammarchi
caeab77a8e Fix #2304 - Make pyimport work as expected (#2311)
* Fix #2304 - Make pyimport work as expected

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-03-10 16:28:42 +01:00
Andrea Giammarchi
f2bbc6ed5f Fix #2309 - Use all config options (#2310)
* Fix #2309 - Use all config options

* dropped websocket test as it takes forever even locally
2025-03-10 15:57:12 +01:00
Andrea Giammarchi
1d666b92a2 Dispatch py-game event right before executing code (#2287)
deal, thanks 🙏
2025-02-27 12:39:58 +01:00
Andrea Giammarchi
290eb03388 Fix #2302 - Updated Polyscript to its latest (#2303)
* Fix #2302 - Updated Polyscript to its latest
2025-02-27 11:09:46 +01:00
Nicholas Tollervey
55031f2347 Update README to include acknowledgement of Anaconda's role in the project. (#2297) 2025-02-26 16:50:51 +00:00
Andrea Giammarchi
8168383653 Updated Pyodide to v0.27.3 (#2300)
This has been published on *npm* as `https://cdn.jsdelivr.net/npm/@pyscript/core@0.6.33/dist/core.js` and `https://cdn.jsdelivr.net/npm/@pyscript/core@0.6.33/dist/core.css`
2025-02-26 15:06:17 +01:00
Andrea Giammarchi
3ff2c171bc PyEditor kill switch (#2295) 2025-02-26 14:39:17 +01:00
Andrea Giammarchi
edbac13713 Splitting integration tests (#2296) 2025-02-20 15:16:36 +01:00
Christian Clauss
46239caa19 Re ruff (#2292)
* Ruff fixes

* Ruff fixes

* from __future__ import annotations breaks MicroPython

* noqa: FURB188 because there is no str.replacesuffix() in MicroPython

* Add ruff to pre-commit
2025-02-20 09:43:09 +01:00
Andrea Giammarchi
0366e48fad Introducing pyscript.fs namespace/module (#2289)
* introducing pyscript.fs namespace/module

* Added proper rejection when showDirectoryPicker is not supported

* Improved exports to make explicit import in 3rd party modules easier

* implemented `fs.unmount(path)`:

  * verified that RAM gets freed
  * allowed to mount different handlers within the same path through different `id` as that's the Web best way to do so
2025-02-17 14:45:43 +01:00
Andrea Giammarchi
b13317d32f Updated interpreters (#2283) 2025-02-07 09:50:01 +01:00
Andrea Giammarchi
57b1440a10 Latest 2024 (#2270)
* Introducing <script type="py-game">

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-05 14:36:45 +01:00
Andrea Giammarchi
fc53356a1d Introducing <script type="py-game"> (#2265)
* Introducing <script type="py-game">

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-05 13:24:35 +01:00
pre-commit-ci[bot]
5be99456f0 [pre-commit.ci] pre-commit autoupdate (#2278)
updates:
- [github.com/psf/black: 24.10.0 → 25.1.0](https://github.com/psf/black/compare/24.10.0...25.1.0)
- [github.com/codespell-project/codespell: v2.3.0 → v2.4.1](https://github.com/codespell-project/codespell/compare/v2.3.0...v2.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-04 09:45:22 +01:00
Joshua Lowe
7adedcc704 Enable service-worker attribute for donkey worker (#2263)
* Enable service-worker attribute for Donkey worker

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-19 17:08:38 +01:00
Andrea Giammarchi
d143b229ed Added __terminal__ in non-persistent donkey (#2260) 2024-12-06 14:53:09 +01:00
Andrea Giammarchi
0d74a60227 Fix #2257 - Updated polyscript (#2258) 2024-12-03 20:33:25 +01:00
Andrea Giammarchi
ce923a354f Updated to latest Pyodide + cleaner Polyscript (#2256) 2024-11-27 13:25:37 +01:00
Andrea Giammarchi
7e65836423 Factored out codemirror as chunked lazy based import (#2252)
* Factored out codemirror as chunked lazy based import
2024-11-26 14:17:07 +01:00
Andrea Giammarchi
796373cfa6 Fix #2246 - Override builtins.input to avoid duplicating it (#2254) 2024-11-25 14:17:30 +01:00
Andrea Giammarchi
0a1d3bb678 Fix #2245 - Filter all versions w/ Pyodide frozen cache (#2251) 2024-11-19 11:47:47 +01:00
Andrea Giammarchi
4e43d3e92d Fix #2242 - Improved Xterm.js glyphs handling (#2248)
* Updated dev/dependencies

* Fix #2242 - Improved Xterm.js glyphs handling
2024-11-14 10:28:38 +01:00
Andrea Giammarchi
5acc2afaf3 WIP: Xterm update (#2237)
* WIP: Xterm Update
2024-11-12 10:21:36 +01:00
Nicholas Tollervey
56c64cbee7 Refactor @when and add Event (#2239)
* Add two unit tests for illustrative purposes.

* Radical simplification of @when, more tests and some minor refactoring.
Handle ElementCollections, tests for ElementCollection, make serve for running tests locally.

* Skip flakey Pyodide in worker test (it works 50/50 and appears to be a timing issue).

* Ensure onFOO relates to an underlying FOO event in an Element.

* Minor comment cleanup.

* Add async test for Event listeners.

* Handlers no longer require an event parameter.

* Add tests for async handling via when.

* Docstring cleanup.

* Refactor onFOO to on_FOO.

* Minor typo tidy ups.

* Use correct check for MicroPython.

---------

Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-11-05 13:55:28 +00:00
Andrea Giammarchi
4ff02a24d1 Fix #2240 - Update polyscript to allow targeting /* multiple times (#2241) 2024-11-05 14:20:26 +01:00
Andrea Giammarchi
a5dc94792b Fixed MicroPython error on non-worker terminal (#2238) 2024-10-30 16:08:50 +01:00
Andrea Giammarchi
0db79e0f02 Fix #2065 - Allow usage of index_urls for Pyodide (#2235) 2024-10-30 10:34:45 +01:00
Andrea Giammarchi
283eabdb30 Added pinned lockFileURL test (#2234) 2024-10-29 11:12:21 +01:00
Nicholas Tollervey
3f19e00410 Update the size of the Py icon on the release page template (#2232)
* Update the size of the Py icon on the release page template, so it works in a mobile friendly manner.

* Use CSS with media for responsive icon.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-28 11:00:51 +00:00
Andrea Giammarchi
9233d5e45a Fix #2220 - Delay plugins resolution due Safari 17.6 greedy resolution (#2229)
* Fix #2220 - Delay plugins resolution due Safari 17.6 greedy resolution

* Fix #2228 - Workaround in Polyscript for lockFileURL
2024-10-28 11:00:23 +01:00
Nicholas Tollervey
fe580cd90b Ensure precommit check is done before each build when using make. (#2231) 2024-10-24 15:27:29 +01:00
Nicholas Tollervey
00e6cfed29 Fix Makefile so JS dependencies don't get into a bad state. (#2230) 2024-10-24 11:13:02 +01:00
Andrea Giammarchi
6b1330d28a Fix #2220 - Avoid DOM notifications on errors (#2226)
* Fix #2220 - Avoid DOM notifications on errors
2024-10-17 16:18:05 +02:00
Carlovo
5d751493f6 Fix typo in README.md (#2219)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-10-15 12:57:28 +02:00
Andrea Giammarchi
c3517f7973 Donkey clear and reset now terminate when busy (#2225)
* Donkey clear and reset now terminate when busy
2024-10-15 12:56:11 +02:00
Andrea Giammarchi
b1c33b7f79 Kill previous worker if another eval/execute is asked to the donkey (#2218)
Kill previous worker if another eval/execute is asked to the donkey
2024-10-11 16:12:56 +02:00
124 changed files with 4430 additions and 1105 deletions

View File

@@ -25,22 +25,29 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.10.0 rev: 25.1.0
hooks: hooks:
- id: black - id: black
exclude: core/tests
args: ["-l", "88", "--skip-string-normalization"] args: ["-l", "88", "--skip-string-normalization"]
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.3.0 rev: v2.4.1
hooks: hooks:
- id: codespell # See 'pyproject.toml' for args - id: codespell # See 'pyproject.toml' for args
exclude: \.js\.map$ exclude: fs\.py|\.js\.map$
additional_dependencies: additional_dependencies:
- tomli - tomli
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
hooks:
- id: ruff
exclude: core/tests
- repo: https://github.com/hoodmane/pyscript-prettier-precommit - repo: https://github.com/hoodmane/pyscript-prettier-precommit
rev: "v3.0.0-alpha.6" rev: "v3.0.0-alpha.6"
hooks: hooks:
- id: prettier - id: prettier
exclude: core/test|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party exclude: core/tests|core/dist|core/types|core/src/stdlib/pyscript.js|pyscript\.sw/|core/src/3rd-party
args: [--tab-width, "4"] args: [--tab-width, "4"]

View File

@@ -40,9 +40,9 @@ check-python:
# Check the environment, install the dependencies. # Check the environment, install the dependencies.
setup: check-node check-npm check-python setup: check-node check-npm check-python
cd core && npm install && cd .. cd core && npm ci && cd ..
ifeq ($(VIRTUAL_ENV),) ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m" echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
false false
else else
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
@@ -55,12 +55,11 @@ clean:
rm -rf .pytest_cache .coverage coverage.xml rm -rf .pytest_cache .coverage coverage.xml
# Build PyScript. # Build PyScript.
build: build: precommit-check
cd core && npx playwright install chromium && npm run build cd core && npx playwright install chromium && npm run build
# Update the dependencies. # Update the dependencies.
update: update:
cd core && npm update && cd ..
python -m pip install -r requirements.txt --upgrade python -m pip install -r requirements.txt --upgrade
# Run the precommit checks (run eslint). # Run the precommit checks (run eslint).
@@ -71,6 +70,10 @@ precommit-check:
test: test:
cd core && npm run test:integration cd core && npm run test:integration
# Serve the repository with the correct headers.
serve:
npx mini-coi .
# Format the code. # Format the code.
fmt: fmt-py fmt: fmt-py
@echo "Format completed" @echo "Format completed"

View File

@@ -1,6 +1,6 @@
# PyScript # PyScript
## PyScrcipt is an open source platform for Python in the browser. ## PyScript is an open source platform for Python in the browser.
Using PyScript is as simple as: Using PyScript is as simple as:
@@ -76,10 +76,19 @@ Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
to learn about our development process, reporting bugs and improvements, to learn about our development process, reporting bugs and improvements,
creating issues and asking questions. creating issues and asking questions.
Check out the [developing process](https://docs.pyscript.net/latest/developers/) Check out the [development process](https://docs.pyscript.net/latest/developers/)
documentation for more information on how to setup your development environment. documentation for more information on how to setup your development environment.
## Governance ## Governance
The [PyScript organization governance](https://github.com/pyscript/governance) The [PyScript organization governance](https://github.com/pyscript/governance)
is documented in a separate repository. is documented in a separate repository.
## Supporters
PyScript is an independent open source project.
However, PyScript was born at [Anaconda Inc](https://anaconda.com/) and its
core contributors are currently employed by Anaconda to work on PyScript. We
would like to acknowledge and celebrate Anaconda's continued support of this
project. Thank you [Anaconda Inc](https://anaconda.com/)!

205
core/LICENSE Normal file
View File

@@ -0,0 +1,205 @@
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.

1000
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.6.4", "version": "0.6.53",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -25,12 +25,19 @@
"types": "./types/core.d.ts", "types": "./types/core.d.ts",
"import": "./src/core.js" "import": "./src/core.js"
}, },
"./js": {
"types": "./types/core.d.ts",
"import": "./dist/core.js"
},
"./css": { "./css": {
"import": "./dist/core.css" "import": "./dist/core.css"
}, },
"./storage": { "./storage": {
"import": "./dist/storage.js" "import": "./dist/storage.js"
}, },
"./service-worker": {
"import": "./dist/service-worker.js"
},
"./package.json": "./package.json" "./package.json": "./package.json"
}, },
"scripts": { "scripts": {
@@ -43,7 +50,7 @@
"build:3rd-party": "node rollup/3rd-party.cjs", "build:3rd-party": "node rollup/3rd-party.cjs",
"build:tests-index": "node rollup/build_test_index.cjs", "build:tests-index": "node rollup/build_test_index.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css", "clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test tests/js_tests.spec.js tests/py_tests.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE", "test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && playwright test tests/py_tests.main.spec.js && playwright test tests/py_tests.worker.spec.js) || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js", "test:ws": "bun tests/javascript/ws/index.js & playwright test tests/javascript/ws/index.spec.js",
"dev": "node dev.cjs", "dev": "node dev.cjs",
"release": "npm run build && npm run zip", "release": "npm run build && npm run zip",
@@ -60,40 +67,41 @@
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2", "@webreflection/idb-map": "^0.3.2",
"@webreflection/utils": "^0.1.0",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"polyscript": "^0.16.2", "polyscript": "^0.17.20",
"sabayon": "^0.5.2",
"sticky-module": "^0.1.1", "sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1", "to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7" "type-checked-collections": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.7.0", "@codemirror/commands": "^6.8.1",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.10.3", "@codemirror/language": "^6.11.0",
"@codemirror/state": "^6.4.1", "@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.34.1", "@codemirror/view": "^6.36.8",
"@playwright/test": "1.45.3", "@playwright/test": "^1.52.0",
"@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3", "@webreflection/toml-j0.4": "^1.1.4",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.30", "@xterm/xterm": "^5.5.0",
"chokidar": "^4.0.1", "bun": "^1.2.13",
"chokidar": "^4.0.3",
"codedent": "^0.1.2", "codedent": "^0.1.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"eslint": "^9.12.0", "eslint": "^9.27.0",
"flatted": "^3.3.1", "flatted": "^3.3.3",
"rollup": "^4.24.0", "rollup": "^4.41.0",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3", "static-handler": "^0.5.3",
"string-width": "^7.2.0", "string-width": "^7.2.0",
"typescript": "^5.6.3", "typescript": "^5.8.3",
"xterm": "^5.3.0", "xterm-readline": "^1.1.2"
"xterm-readline": "^1.1.1"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -46,7 +46,7 @@ const modules = {
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"), "toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
// xterm // xterm
"xterm.js": resolve("xterm"), "xterm.js": resolve("@xterm/xterm"),
"xterm-readline.js": resolve("xterm-readline"), "xterm-readline.js": resolve("xterm-readline"),
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) => "xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
b.text(), b.text(),
@@ -54,9 +54,9 @@ const modules = {
"xterm_addon-web-links.js": fetch( "xterm_addon-web-links.js": fetch(
`${CDN}/@xterm/addon-web-links/+esm`, `${CDN}/@xterm/addon-web-links/+esm`,
).then((b) => b.text()), ).then((b) => b.text()),
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then( "xterm.css": fetch(
(b) => b.text(), `${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
), ).then((b) => b.text()),
// codemirror // codemirror
"codemirror.js": reBundle("codemirror"), "codemirror.js": reBundle("codemirror"),

View File

@@ -53,4 +53,15 @@ export default [
sourcemap: true, sourcemap: true,
}, },
}, },
{
input: "./src/service-worker.js",
plugins: plugins.concat(
process.env.NO_MIN
? [nodeResolve(), commonjs()]
: [nodeResolve(), commonjs(), terser()],
),
output: {
file: "./dist/service-worker.js",
},
},
]; ];

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jak Wings
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,25 @@
Copyright 2021 Erik Bremen
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -5,3 +5,7 @@ This folder contains artifacts created via [3rd-party.cjs](../../rollup/3rd-part
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. 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. 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.
## Licenses
All licenses provided by 3rd-party authors can be found in [3rd-party-licenses](../3rd-party-licenses/) folder.

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/** /**
* Minified by jsDelivr using clean-css v5.3.2. * Minified by jsDelivr using clean-css v5.3.2.
* Original file: /npm/xterm@5.3.0/css/xterm.css * Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
* *
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/ */
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility,.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative} .xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:0}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
/** /**
* Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
* Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js * Original file: /npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js
* *
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files

View File

@@ -1,5 +1,5 @@
/** /**
* Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0.
* Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js * Original file: /npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.js
* *
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files

View File

@@ -1,3 +1,4 @@
import withResolvers from "@webreflection/utils/with-resolvers";
import TYPES from "./types.js"; import TYPES from "./types.js";
const waitForIt = []; const waitForIt = [];
@@ -5,7 +6,7 @@ const waitForIt = [];
for (const [TYPE] of TYPES) { for (const [TYPE] of TYPES) {
const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`]; const selectors = [`script[type="${TYPE}"]`, `${TYPE}-script`];
for (const element of document.querySelectorAll(selectors.join(","))) { for (const element of document.querySelectorAll(selectors.join(","))) {
const { promise, resolve } = Promise.withResolvers(); const { promise, resolve } = withResolvers();
waitForIt.push(promise); waitForIt.push(promise);
element.addEventListener(`${TYPE}:done`, resolve, { once: true }); element.addEventListener(`${TYPE}:done`, resolve, { once: true });
} }

View File

@@ -25,7 +25,7 @@ const badURL = (url, expected = "") => {
* @param {string?} type the optional type to enforce * @param {string?} type the optional type to enforce
* @returns {{json: boolean, toml: boolean, text: string}} * @returns {{json: boolean, toml: boolean, text: string}}
*/ */
const configDetails = async (config, type) => { export const configDetails = async (config, type) => {
let text = config?.trim(); let text = config?.trim();
// we only support an object as root config // we only support an object as root config
let url = "", let url = "",
@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
const configs = new Map(); const configs = new Map();
for (const [TYPE] of TYPES) { for (const [TYPE] of TYPES) {
/** @type {Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */ /** @type {() => Promise<[...any]>} A Promise wrapping any plugins which should be loaded. */
let plugins; 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} ) */ /** @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} ) */
@@ -135,22 +135,24 @@ for (const [TYPE] of TYPES) {
// parse all plugins and optionally ignore only // parse all plugins and optionally ignore only
// those flagged as "undesired" via `!` prefix // those flagged as "undesired" via `!` prefix
const toBeAwaited = []; plugins = async () => {
for (const [key, value] of Object.entries(allPlugins)) { const toBeAwaited = [];
if (error) { for (const [key, value] of Object.entries(allPlugins)) {
if (key === "error") { if (error) {
// show on page the config is broken, meaning that if (key === "error") {
// it was not possible to disable error plugin neither // show on page the config is broken, meaning that
// as that part wasn't correctly parsed anyway // it was not possible to disable error plugin neither
value().then(({ notify }) => notify(error.message)); // 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));
} else if (key === "error") {
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
} }
} else if (!parsed?.plugins?.includes(`!${key}`)) {
toBeAwaited.push(value().then(({ default: p }) => p));
} }
} return await Promise.all(toBeAwaited);
};
// assign plugins as Promise.all only if needed
plugins = Promise.all(toBeAwaited);
configs.set(TYPE, { config: parsed, configURL, plugins, error }); configs.set(TYPE, { config: parsed, configURL, plugins, error });
} }

View File

@@ -28,48 +28,34 @@ mpy-config {
.py-editor-run-button, .py-editor-run-button,
.mpy-editor-run-button { .mpy-editor-run-button {
position: absolute; position: absolute;
display: flex;
right: 0.5rem; right: 0.5rem;
bottom: 0.5rem; bottom: 0.5rem;
opacity: 0; opacity: 0;
transition: opacity 0.25s; transition: opacity 0.25s;
z-index: 1; z-index: 1;
padding: 0;
} }
.py-editor-box:hover .py-editor-run-button, .py-editor-box:hover .py-editor-run-button,
.mpy-editor-box:hover .mpy-editor-run-button, .mpy-editor-box:hover .mpy-editor-run-button,
.py-editor-run-button:focus, .py-editor-run-button:focus,
.py-editor-run-button:disabled, .py-editor-run-button.running,
.mpy-editor-run-button:focus, .mpy-editor-run-button:focus,
.mpy-editor-run-button:disabled { .mpy-editor-run-button.running {
opacity: 1; opacity: 1;
} }
@keyframes spinner { py-terminal span,
to { mpy-terminal span {
transform: rotate(360deg); letter-spacing: 0 !important;
}
} }
.py-editor-run-button:disabled > *,
.mpy-editor-run-button:disabled > * { dialog.pyscript-fs {
display: none; /* hide all the child elements of the run button when it is disabled */ border-radius: 8px;
border-width: 1px;
} }
.py-editor-run-button:disabled,
.mpy-editor-run-button:disabled { dialog.pyscript-fs > div {
border-width: 0; display: flex;
} justify-content: space-between;
.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;
} }

View File

@@ -1,7 +1,8 @@
/*! (c) PyScript Development Team */ /*! (c) PyScript Development Team */
import "./zero-redirect.js";
import stickyModule from "sticky-module"; import stickyModule from "sticky-module";
import "@ungap/with-resolvers"; import withResolvers from "@webreflection/utils/with-resolvers";
import { import {
INVALID_CONTENT, INVALID_CONTENT,
@@ -33,6 +34,10 @@ import {
createFunction, createFunction,
inputFailure, inputFailure,
} from "./hooks.js"; } from "./hooks.js";
import * as fs from "./fs.js";
import codemirror from "./plugins/codemirror.js";
export { codemirror };
import { stdlib, optional } from "./stdlib.js"; import { stdlib, optional } from "./stdlib.js";
export { stdlib, optional, inputFailure }; export { stdlib, optional, inputFailure };
@@ -164,6 +169,8 @@ for (const [TYPE, interpreter] of TYPES) {
// enrich the Python env with some JS utility for main // enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", { interpreter.registerJsModule("_pyscript", {
PyWorker, PyWorker,
fs,
interpreter,
js_import: (...urls) => Promise.all(urls.map((url) => import(url))), js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
get target() { get target() {
return isScript(currentElement) return isScript(currentElement)
@@ -179,7 +186,7 @@ for (const [TYPE, interpreter] of TYPES) {
// ensure plugins are bootstrapped already before custom type definition // ensure plugins are bootstrapped already before custom type definition
// NOTE: we cannot top-level await in here as plugins import other utilities // 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. // from core.js itself so that custom definition should not be blocking.
plugins.then(() => { plugins().then(() => {
// possible early errors sent by polyscript // possible early errors sent by polyscript
const errors = new Map(); const errors = new Map();
@@ -321,7 +328,7 @@ for (const [TYPE, interpreter] of TYPES) {
class extends HTMLElement { class extends HTMLElement {
constructor() { constructor() {
assign(super(), { assign(super(), {
_wrap: Promise.withResolvers(), _wrap: withResolvers(),
srcCode: "", srcCode: "",
executed: false, executed: false,
}); });

82
core/src/fs.js Normal file
View File

@@ -0,0 +1,82 @@
import IDBMap from "@webreflection/idb-map";
import withResolvers from "@webreflection/utils/with-resolvers";
import { assign } from "polyscript/exports";
import { $$ } from "basic-devtools";
const stop = (event) => {
event.preventDefault();
event.stopImmediatePropagation();
};
// ⚠️ these two constants MUST be passed as `fs`
// within the worker onBeforeRunAsync hook!
export const NAMESPACE = "@pyscript.fs";
export const ERROR = "storage permissions not granted";
export const idb = new IDBMap(NAMESPACE);
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {Promise<FileSystemDirectoryHandle>}
*/
export const getFileSystemDirectoryHandle = async (options) => {
if (!("showDirectoryPicker" in globalThis)) {
return Promise.reject(
new Error("showDirectoryPicker is not supported"),
);
}
const { promise, resolve, reject } = withResolvers();
const how = { id: "pyscript", mode: "readwrite", ...options };
if (options.hint) how.startIn = options.hint;
const transient = async () => {
try {
/* eslint-disable */
const handler = await showDirectoryPicker(how);
/* eslint-enable */
if ((await handler.requestPermission(how)) === "granted") {
resolve(handler);
return true;
}
} catch ({ message }) {
console.warn(message);
}
return false;
};
// in case the user decided to attach the event itself
// as opposite of relying our dialog walkthrough
if (navigator.userActivation?.isActive) {
if (!(await transient())) reject(new Error(ERROR));
} else {
const dialog = assign(document.createElement("dialog"), {
className: "pyscript-fs",
innerHTML: [
"<strong> Persistent FileSystem</strong><hr>",
"<p><small>PyScript would like to access a local folder.</small></p>",
"<div><button title='ok'>✅ Authorize</button>",
"<button title='cancel'>❌</button></div>",
].join(""),
});
const [ok, cancel] = $$("button", dialog);
ok.addEventListener("click", async (event) => {
stop(event);
if (await transient()) dialog.close();
});
cancel.addEventListener("click", async (event) => {
stop(event);
reject(new Error(ERROR));
dialog.close();
});
document.body.appendChild(dialog).showModal();
}
return promise;
};

View File

@@ -88,7 +88,19 @@ export const hooks = {
/** @type {Set<function>} */ /** @type {Set<function>} */
onBeforeRun: new SetFunction(), onBeforeRun: new SetFunction(),
/** @type {Set<function>} */ /** @type {Set<function>} */
onBeforeRunAsync: new SetFunction(), onBeforeRunAsync: new SetFunction([
({ interpreter }) => {
interpreter.registerJsModule("_pyscript", {
// cannot be imported from fs.js
// because this code is stringified
fs: {
ERROR: "storage permissions not granted",
NAMESPACE: "@pyscript.fs",
},
interpreter,
});
},
]),
/** @type {Set<function>} */ /** @type {Set<function>} */
onAfterRun: new SetFunction(), onAfterRun: new SetFunction(),
/** @type {Set<function>} */ /** @type {Set<function>} */

View File

@@ -1,5 +1,10 @@
// ⚠️ This file is an artifact: DO NOT MODIFY // ⚠️ This file is an artifact: DO NOT MODIFY
export default { export default {
codemirror: () =>
import(
/* webpackIgnore: true */
"./plugins/codemirror.js"
),
["deprecations-manager"]: () => ["deprecations-manager"]: () =>
import( import(
/* webpackIgnore: true */ /* webpackIgnore: true */
@@ -20,6 +25,11 @@ export default {
/* webpackIgnore: true */ /* webpackIgnore: true */
"./plugins/py-editor.js" "./plugins/py-editor.js"
), ),
["py-game"]: () =>
import(
/* webpackIgnore: true */
"./plugins/py-game.js"
),
["py-terminal"]: () => ["py-terminal"]: () =>
import( import(
/* webpackIgnore: true */ /* webpackIgnore: true */

View File

@@ -0,0 +1,31 @@
// lazy loaded on-demand codemirror related files
export default {
get core() {
return import(/* webpackIgnore: true */ "../3rd-party/codemirror.js");
},
get state() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"
);
},
get python() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
);
},
get language() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"
);
},
get view() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"
);
},
get commands() {
return import(
/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"
);
},
};

View File

@@ -1,6 +1,6 @@
// PyScript Derepcations Plugin // PyScript Derepcations Plugin
import { hooks } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import { hooks } from "../core.js";
// react lazily on PyScript bootstrap // react lazily on PyScript bootstrap
hooks.main.onReady.add(checkDeprecations); hooks.main.onReady.add(checkDeprecations);

View File

@@ -1,64 +1,121 @@
import addPromiseListener from "add-promise-listener";
import { assign, dedent } from "polyscript/exports"; import { assign, dedent } from "polyscript/exports";
const { stringify } = JSON;
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`; const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
export default (options = {}) => { const donkey = ({
const type = options.type || "py"; type = "py",
const args = options.persistent persistent,
? ["globals()", "__locals__"] terminal,
: ["{}", "{}"]; config,
serviceWorker,
}) => {
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
const src = URL.createObjectURL( const src = URL.createObjectURL(
new Blob([ new Blob([
dedent(` [
from pyscript import sync, config // this array is to better minify this code once in production
__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}" "from pyscript import sync, config",
__locals__ = {} '__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
if config["type"] == "py": "__locals__ = {}",
import sys 'if config["type"] == "py":',
def __error__(_): " import sys",
info = sys.exc_info() " def __error__(_):",
return __message__(info[0], info[1]) " info = sys.exc_info()",
else: " return __message__(info[0], info[1])",
__error__ = lambda e: __message__(e.__class__, e.value) "else:",
def execute(code): " __error__ = lambda e: __message__(e.__class__, e.value)",
try: return ${invoke("exec", args)}; "def execute(code):",
except Exception as e: print(__error__(e)); ` try: return ${invoke("exec", args)};`,
def evaluate(code): " except Exception as e: print(__error__(e));",
try: return ${invoke("eval", args)}; "def evaluate(code):",
except Exception as e: print(__error__(e)); ` try: return ${invoke("eval", args)};`,
sync.execute = execute " except Exception as e: print(__error__(e));",
sync.evaluate = evaluate "sync.execute = execute",
`), "sync.evaluate = evaluate",
].join("\n"),
]), ]),
); );
// create the script that exposes the code to execute or evaluate
const script = assign(document.createElement("script"), { type, src }); const script = assign(document.createElement("script"), { type, src });
script.toggleAttribute("worker", true); script.toggleAttribute("worker", true);
script.toggleAttribute("terminal", true); script.toggleAttribute("terminal", true);
if (options.terminal) script.setAttribute("target", options.terminal); if (terminal) script.setAttribute("target", terminal);
if (options.config) if (config) {
script.setAttribute("config", JSON.stringify(options.config)); script.setAttribute(
"config",
typeof config === "string" ? config : stringify(config),
);
}
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
return new Promise((resolve) => { return addPromiseListener(
script.addEventListener(`${type}:done`, (event) => { document.body.appendChild(script),
event.stopPropagation(); `${type}:done`,
URL.revokeObjectURL(src); { stopPropagation: true },
const { xworker, process, terminal } = script; ).then(() => {
const { execute, evaluate } = xworker.sync; URL.revokeObjectURL(src);
script.remove(); return script;
resolve({
process,
execute: (code) => execute(dedent(code)),
evaluate: (code) => evaluate(dedent(code)),
clear: () => terminal.clear(),
reset: () => terminal.reset(),
kill: () => {
xworker.terminate();
terminal.dispose();
},
});
});
document.body.append(script);
}); });
}; };
const utils = async (options) => {
const script = await donkey(options);
const { xworker, process, terminal } = script;
const { execute, evaluate } = xworker.sync;
script.remove();
return {
xworker,
process,
terminal,
execute,
evaluate,
};
};
export default async (options = {}) => {
let farmer = await utils(options);
let working = false;
const kill = () => {
if (farmer) {
farmer.xworker.terminate();
farmer.terminal.dispose();
farmer = null;
}
working = false;
};
const reload = async () => {
kill();
farmer = await utils(options);
};
const asyncTask = (method) => async (code) => {
// race condition ... a new task has been
// assigned while the previous one didn't finish
if (working) await reload();
working = true;
try {
return await farmer[method](dedent(code));
} catch (e) {
console.error(e);
} finally {
working = false;
}
};
const asyncMethod = (method) => async () => {
if (working) await reload();
else farmer?.terminal[method]();
};
return {
process: asyncTask("process"),
execute: asyncTask("execute"),
evaluate: asyncTask("evaluate"),
clear: asyncMethod("clear"),
reset: asyncMethod("reset"),
kill,
};
};

View File

@@ -1,6 +1,12 @@
// PyScript Error Plugin // PyScript Error Plugin
import { buffered } from "polyscript/exports";
import { hooks } from "../core.js"; import { hooks } from "../core.js";
let dontBotherDOM = false;
export function notOnDOM() {
dontBotherDOM = true;
}
hooks.main.onReady.add(function override(pyScript) { hooks.main.onReady.add(function override(pyScript) {
// be sure this override happens only once // be sure this override happens only once
hooks.main.onReady.delete(override); hooks.main.onReady.delete(override);
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
// trap generic `stderr` to propagate to it regardless // trap generic `stderr` to propagate to it regardless
const { stderr } = pyScript.io; const { stderr } = pyScript.io;
// override it with our own logic const cb = (error, ...rest) => {
pyScript.io.stderr = (error, ...rest) => {
notify(error.message || error); notify(error.message || error);
// let other plugins or stderr hook, if any, do the rest // let other plugins or stderr hook, if any, do the rest
return stderr(error, ...rest); return stderr(error, ...rest);
}; };
// override it with our own logic
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
// be sure uncaught Python errors are also visible // be sure uncaught Python errors are also visible
addEventListener("error", ({ message }) => { addEventListener("error", ({ message }) => {
if (message.startsWith("Uncaught PythonError")) notify(message); if (message.startsWith("Uncaught PythonError")) notify(message);
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
* @param {string} message * @param {string} message
*/ */
export function notify(message) { export function notify(message) {
if (dontBotherDOM) return;
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "py-error"; div.className = "py-error";
div.textContent = message; div.textContent = message;

View File

@@ -1,15 +1,19 @@
// PyScript py-editor plugin // PyScript py-editor plugin
import withResolvers from "@webreflection/utils/with-resolvers";
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports"; import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js"; import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import codemirror from "./codemirror.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>`; const RUN_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,12a1,1,0,0,1-.55.89l-10,5A1,1,0,0,1,8,18a1,1,0,0,1-.53-.15A1,1,0,0,1,7,17V7a1,1,0,0,1,1.45-.89l10,5A1,1,0,0,1,19,12Z" fill="#464646"/></svg>`;
const STOP_BUTTON = `<svg style="height:24px;width:24px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 7h10v10H7z" style="fill:#464646;stroke:#464646;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-dasharray:none;paint-order:normal"/></svg>`;
let id = 0; let id = 0;
const getID = (type) => `${type}-editor-${id++}`; const getID = (type) => `${type}-editor-${id++}`;
const envs = new Map(); const envs = new Map();
const configs = new Map(); const configs = new Map();
const editors = new WeakMap();
const hooks = { const hooks = {
worker: { worker: {
@@ -29,12 +33,18 @@ const validate = (config, result) => {
return result; return result;
}; };
const getRelatedScript = (target, type) => {
const editor = target.closest(`.${type}-editor-box`);
return editor?.parentNode?.previousElementSibling;
};
async function execute({ currentTarget }) { async function execute({ currentTarget }) {
const { env, pySrc, outDiv } = this; const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget; const hasRunButton = !!currentTarget;
if (hasRunButton) { if (hasRunButton) {
currentTarget.disabled = true; currentTarget.classList.add("running");
currentTarget.innerHTML = STOP_BUTTON;
outDiv.innerHTML = ""; outDiv.innerHTML = "";
} }
@@ -81,8 +91,7 @@ async function execute({ currentTarget }) {
// creation and destruction of editors on the fly // creation and destruction of editors on the fly
if (hasRunButton) { if (hasRunButton) {
for (const type of TYPES.keys()) { for (const type of TYPES.keys()) {
const editor = currentTarget.closest(`.${type}-editor-box`); const script = getRelatedScript(currentTarget, type);
const script = editor?.parentNode?.previousElementSibling;
if (script) { if (script) {
defineProperties(script, { xworker: { value: xworker } }); defineProperties(script, { xworker: { value: xworker } });
break; break;
@@ -91,7 +100,7 @@ async function execute({ currentTarget }) {
} }
const { sync } = xworker; const { sync } = xworker;
const { promise, resolve } = Promise.withResolvers(); const { promise, resolve } = withResolvers();
envs.set(env, promise); envs.set(env, promise);
sync.revoke = () => { sync.revoke = () => {
URL.revokeObjectURL(srcLink); URL.revokeObjectURL(srcLink);
@@ -115,7 +124,18 @@ async function execute({ currentTarget }) {
}; };
const enable = () => { const enable = () => {
if (hasRunButton) currentTarget.disabled = false; if (hasRunButton) {
currentTarget.classList.remove("running");
currentTarget.innerHTML = RUN_BUTTON;
const { previousElementSibling } =
currentTarget.closest("[data-env]").parentElement;
previousElementSibling?.dispatchEvent(
new Event("py-editor:done", {
bubbles: true,
cancelable: true,
}),
);
}
}; };
const { sync } = xworker; const { sync } = xworker;
sync.write = (str) => { sync.write = (str) => {
@@ -143,6 +163,24 @@ const makeRunButton = (handler, type) => {
runButton.innerHTML = RUN_BUTTON; runButton.innerHTML = RUN_BUTTON;
runButton.setAttribute("aria-label", "Python Script Run Button"); runButton.setAttribute("aria-label", "Python Script Run Button");
runButton.addEventListener("click", async (event) => { runButton.addEventListener("click", async (event) => {
if (
runButton.classList.contains("running") &&
confirm("Stop evaluating this code?")
) {
const script = getRelatedScript(runButton, type);
if (script) {
const editor = editors.get(script);
const content = editor.state.doc.toString();
const clone = script.cloneNode(true);
clone.type = `${type}-editor`;
clone.textContent = content;
script.xworker.terminate();
script.nextElementSibling.remove();
script.replaceWith(clone);
editors.delete(script);
}
return;
}
runButton.blur(); runButton.blur();
await handler.handleEvent(event); await handler.handleEvent(event);
}); });
@@ -194,14 +232,12 @@ const init = async (script, type, interpreter) => {
{ keymap }, { keymap },
{ defaultKeymap, indentWithTab }, { defaultKeymap, indentWithTab },
] = await Promise.all([ ] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"), codemirror.core,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"), codemirror.state,
import( codemirror.python,
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js" codemirror.language,
), codemirror.view,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"), codemirror.commands,
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
]); ]);
let isSetup = script.hasAttribute("setup"); let isSetup = script.hasAttribute("setup");
@@ -388,6 +424,7 @@ const init = async (script, type, interpreter) => {
doc, doc,
}); });
editors.set(script, editor);
editor.focus(); editor.focus();
notifyEditor(); notifyEditor();
}; };

112
core/src/plugins/py-game.js Normal file
View File

@@ -0,0 +1,112 @@
import {
dedent,
define,
createProgress,
loadProgress,
} from "polyscript/exports";
import { stdlib } from "../core.js";
import { configDetails } from "../config.js";
import { getText } from "../fetch.js";
const progress = createProgress("py-game");
const inputPatch = `
import builtins
def input(prompt=""):
import js
return js.prompt(prompt)
builtins.input = input
del builtins
del input
`;
let toBeWarned = true;
const hooks = {
main: {
onReady: async (wrap, script) => {
if (toBeWarned) {
toBeWarned = false;
console.warn("⚠️ EXPERIMENTAL `py-game` FEATURE");
}
let config = {};
if (script.hasAttribute("config")) {
const value = script.getAttribute("config");
const { json, toml, text, url } = await configDetails(value);
if (json) config = JSON.parse(text);
else if (toml) {
const { parse } = await import(
/* webpackIgnore: true */ "../3rd-party/toml.js"
);
config = parse(text);
}
if (config.packages) {
await wrap.interpreter.loadPackage("micropip");
const micropip = wrap.interpreter.pyimport("micropip");
await micropip.install(config.packages, {
keep_going: true,
});
micropip.destroy();
}
await loadProgress(
"py-game",
progress,
wrap.interpreter,
config,
url ? new URL(url, location.href).href : location.href,
);
}
wrap.interpreter.registerJsModule("_pyscript", {
PyWorker() {
throw new Error(
"Unable to use PyWorker in py-game scripts",
);
},
js_import: (...urls) =>
Promise.all(urls.map((url) => import(url))),
get target() {
return script.id;
},
});
await wrap.interpreter.runPythonAsync(stdlib);
wrap.interpreter.runPython(inputPatch);
let code = dedent(script.textContent);
if (script.src) code = await fetch(script.src).then(getText);
const target = script.getAttribute("target") || "canvas";
const canvas = document.getElementById(target);
wrap.interpreter.canvas.setCanvas2D(canvas);
// allow 3rd party to hook themselves right before
// the code gets executed
const event = new CustomEvent("py-game", {
bubbles: true,
cancelable: true,
detail: {
canvas,
code,
config,
wrap,
},
});
script.dispatchEvent(event);
// run only if the default was not prevented
if (!event.defaultPrevented)
await wrap.interpreter.runPythonAsync(code);
},
},
};
define("py-game", {
config: { packages: ["pygame-ce"] },
configURL: new URL("./config.txt", location.href).href,
interpreter: "pyodide",
env: "py-game",
hooks,
});

View File

@@ -1,6 +1,7 @@
// PyScript pyodide terminal plugin // PyScript pyodide terminal plugin
import { hooks, inputFailure } from "../../core.js"; import withResolvers from "@webreflection/utils/with-resolvers";
import { defineProperties } from "polyscript/exports"; import { defineProperties } from "polyscript/exports";
import { hooks, inputFailure } from "../../core.js";
const bootstrapped = new WeakSet(); const bootstrapped = new WeakSet();
@@ -34,6 +35,8 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
pyterminal_write(String(error.message || error)); pyterminal_write(String(error.message || error));
}; };
sync.pyterminal_stream_write = () => {};
// tiny shim of the code module with only interact // tiny shim of the code module with only interact
// to bootstrap a REPL like environment // to bootstrap a REPL like environment
interpreter.registerJsModule("code", { interpreter.registerJsModule("code", {
@@ -71,6 +74,7 @@ export default async (element) => {
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: true,
cursorStyle: "block", cursorStyle: "block",
lineHeight: 1.2,
}; };
let stream; let stream;
@@ -143,7 +147,7 @@ export default async (element) => {
// frees the worker on \r // frees the worker on \r
sync.pyterminal_read = (buffer) => { sync.pyterminal_read = (buffer) => {
terminal.write(buffer); terminal.write(buffer);
promisedChunks = Promise.withResolvers(); promisedChunks = withResolvers();
return promisedChunks.promise; return promisedChunks.promise;
}; };

View File

@@ -1,6 +1,6 @@
// PyScript py-terminal plugin // PyScript py-terminal plugin
import { hooks } from "../../core.js";
import { defineProperties } from "polyscript/exports"; import { defineProperties } from "polyscript/exports";
import { hooks } from "../../core.js";
const bootstrapped = new WeakSet(); const bootstrapped = new WeakSet();
@@ -126,6 +126,7 @@ export default async (element) => {
disableStdin: false, disableStdin: false,
cursorBlink: true, cursorBlink: true,
cursorStyle: "block", cursorStyle: "block",
lineHeight: 1.2,
}); });
xworker.sync.is_pyterminal = () => true; xworker.sync.is_pyterminal = () => true;
@@ -136,6 +137,18 @@ export default async (element) => {
// setup remote thread JS/Python code for whenever the // setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal // worker is ready to become a terminal
hooks.worker.onReady.add(workerReady); hooks.worker.onReady.add(workerReady);
// @see https://github.com/pyscript/pyscript/issues/2246
const patchInput = [
"import builtins as _b",
"from pyscript import sync as _s",
"_b.input = _s.pyterminal_read",
"del _b",
"del _s",
].join("\n");
hooks.worker.codeBeforeRun.add(patchInput);
hooks.worker.codeBeforeRunAsync.add(patchInput);
} else { } else {
// in the main case, just bootstrap XTerm without // in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward // allowing any input as that's not possible / awkward

View File

@@ -0,0 +1 @@
import "polyscript/service-worker";

File diff suppressed because one or more lines are too long

View File

@@ -30,8 +30,6 @@
# as it works transparently in both the main thread and worker cases. # as it works transparently in both the main thread and worker cases.
from polyscript import lazy_py_modules as py_import 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 ( from pyscript.magic_js import (
RUNNING_IN_WORKER, RUNNING_IN_WORKER,
PyWorker, PyWorker,
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
sync, sync,
window, window,
) )
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.storage import Storage, storage from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket from pyscript.websocket import WebSocket
from pyscript.events import when, Event
if not RUNNING_IN_WORKER: if not RUNNING_IN_WORKER:
from pyscript.workers import create_named_worker, workers from pyscript.workers import create_named_worker, workers
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"
)

View File

@@ -73,14 +73,14 @@ def _eval_formatter(obj, print_method):
""" """
if print_method == "__repr__": if print_method == "__repr__":
return repr(obj) return repr(obj)
elif hasattr(obj, print_method): if hasattr(obj, print_method):
if print_method == "savefig": if print_method == "savefig":
buf = io.BytesIO() buf = io.BytesIO()
obj.savefig(buf, format="png") obj.savefig(buf, format="png")
buf.seek(0) buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8") return base64.b64encode(buf.read()).decode("utf-8")
return getattr(obj, print_method)() return getattr(obj, print_method)()
elif print_method == "_repr_mimebundle_": if print_method == "_repr_mimebundle_":
return {}, {} return {}, {}
return None return None
@@ -107,7 +107,7 @@ def _format_mime(obj):
if output is None: if output is None:
continue continue
elif mime_type not in _MIME_RENDERERS: if mime_type not in _MIME_RENDERERS:
not_available.append(mime_type) not_available.append(mime_type)
continue continue
break break
@@ -149,9 +149,11 @@ def display(*values, target=None, append=True):
if target is None: if target is None:
target = current_target() target = current_target()
elif not isinstance(target, str): elif not isinstance(target, str):
raise TypeError(f"target must be str or None, not {target.__class__.__name__}") msg = f"target must be str or None, not {target.__class__.__name__}"
raise TypeError(msg)
elif target == "": elif target == "":
raise ValueError("Cannot have an empty target") msg = "Cannot have an empty target"
raise ValueError(msg)
elif target.startswith("#"): elif target.startswith("#"):
# note: here target is str and not None! # note: here target is str and not None!
# align with @when behavior # align with @when behavior
@@ -161,9 +163,8 @@ def display(*values, target=None, append=True):
# If target cannot be found on the page, a ValueError is raised # If target cannot be found on the page, a ValueError is raised
if element is None: if element is None:
raise ValueError( msg = f"Invalid selector with id={target}. Cannot be found in the page."
f"Invalid selector with id={target}. Cannot be found in the page." raise ValueError(msg)
)
# if element is a <script type="py">, it has a 'target' attribute which # 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, # points to the visual element holding the displayed values. In that case,

View File

@@ -1,76 +0,0 @@
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):
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
elements = document.querySelectorAll(selector)
# TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies
elif isinstance(selector, Element):
elements = [selector._dom_element]
elif isinstance(selector, ElementCollection):
elements = [el._dom_element for el in selector]
else:
if isinstance(selector, list):
elements = selector
else:
elements = [selector]
try:
sig = inspect.signature(func)
# Function doesn't receive events
if not sig.parameters:
# Function is async: must be awaited
if inspect.iscoroutinefunction(func):
async def wrapper(*args, **kwargs):
await func()
else:
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

View File

@@ -0,0 +1,168 @@
import asyncio
import inspect
import sys
from functools import wraps
from pyscript.magic_js import document
from pyscript.ffi import create_proxy
from pyscript.util import is_awaitable
from pyscript import config
class Event:
"""
Represents something that may happen at some point in the future.
"""
def __init__(self):
self._listeners = []
def trigger(self, result):
"""
Trigger the event with a result to pass into the handlers.
"""
for listener in self._listeners:
if is_awaitable(listener):
# Use create task to avoid making this an async function.
asyncio.create_task(listener(result))
else:
listener(result)
def add_listener(self, listener):
"""
Add a callable/awaitable to listen to when this event is triggered.
"""
if is_awaitable(listener) or callable(listener):
if listener not in self._listeners:
self._listeners.append(listener)
else:
msg = "Listener must be callable or awaitable."
raise ValueError(msg)
def remove_listener(self, *args):
"""
Clear the specified handler functions in *args. If no handlers
provided, clear all handlers.
"""
if args:
for listener in args:
self._listeners.remove(listener)
else:
self._listeners = []
def when(target, *args, **kwargs):
"""
Add an event listener to the target element(s) for the specified event type.
The target can be a string representing the event type, or an Event object.
If the target is an Event object, the event listener will be added to that
object. If the target is a string, the event listener will be added to the
element(s) that match the (second) selector argument.
If a (third) handler argument is provided, it will be called when the event
is triggered; thus allowing this to be used as both a function and a
decorator.
"""
# If "when" is called as a function, try to grab the handler from the
# arguments. If there's no handler, this must be a decorator based call.
handler = None
if args and (callable(args[0]) or is_awaitable(args[0])):
handler = args[0]
elif callable(kwargs.get("handler")) or is_awaitable(kwargs.get("handler")):
handler = kwargs.pop("handler")
# If the target is a string, it is the "older" use of `when` where it
# represents the name of a DOM event.
if isinstance(target, str):
# Extract the selector from the arguments or keyword arguments.
selector = args[0] if args else kwargs.pop("selector")
if not selector:
msg = "No selector provided."
raise ValueError(msg)
# Grab the DOM elements to which the target event will be attached.
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
elements = document.querySelectorAll(selector)
elif isinstance(selector, Element):
elements = [selector._dom_element]
elif isinstance(selector, ElementCollection):
elements = [el._dom_element for el in selector]
else:
elements = selector if isinstance(selector, list) else [selector]
def decorator(func):
if config["type"] == "mpy": # Is MicroPython?
if is_awaitable(func):
async def wrapper(*args, **kwargs):
"""
This is a very ugly hack to get micropython working because
`inspect.signature` doesn't exist. It may be actually better
to not try any magic for now and raise the error.
"""
try:
return await func(*args, **kwargs)
except TypeError as e:
if "takes" in str(e) and "positional arguments" in str(e):
return await func()
raise
else:
def wrapper(*args, **kwargs):
"""
This is a very ugly hack to get micropython working because
`inspect.signature` doesn't exist. It may be actually better
to not try any magic for now and raise the error.
"""
try:
return func(*args, **kwargs)
except TypeError as e:
if "takes" in str(e) and "positional arguments" in str(e):
return func()
raise
else:
sig = inspect.signature(func)
if sig.parameters:
if is_awaitable(func):
async def wrapper(event):
return await func(event)
else:
wrapper = func
else:
# Function doesn't receive events.
if is_awaitable(func):
async def wrapper(*args, **kwargs):
return await func()
else:
def wrapper(*args, **kwargs):
return func()
wrapper = wraps(func)(wrapper)
if isinstance(target, Event):
# The target is a single Event object.
target.add_listener(wrapper)
elif isinstance(target, list) and all(isinstance(t, Event) for t in target):
# The target is a list of Event objects.
for evt in target:
evt.add_listener(wrapper)
else:
# The target is a string representing an event type, and so a
# DOM element or collection of elements is found in "elements".
for el in elements:
el.addEventListener(target, create_proxy(wrapper))
return wrapper
# If "when" was called as a decorator, return the decorator function,
# otherwise just call the internal decorator function with the supplied
# handler.
return decorator(handler) if handler else decorator

View File

@@ -31,7 +31,7 @@ def _object_keys(value):
def _is_array(value): def _is_array(value):
return isinstance(value, list) or isinstance(value, tuple) return isinstance(value, (list, tuple))
def _is_object(value): def _is_object(value):
@@ -60,10 +60,10 @@ def _loop(keys, input, known, output):
def _ref(key, value, input, known, output): def _ref(key, value, input, known, output):
if _is_array(value) and not value in known: if _is_array(value) and value not in known:
known.append(value) known.append(value)
value = _loop(_array_keys(value), input, known, value) value = _loop(_array_keys(value), input, known, value)
elif _is_object(value) and not value in known: elif _is_object(value) and value not in known:
known.append(value) known.append(value)
value = _loop(_object_keys(value), input, known, value) value = _loop(_object_keys(value), input, known, value)

View File

@@ -0,0 +1,60 @@
mounted = {}
async def mount(path, mode="readwrite", root="", id="pyscript"):
import js
from _pyscript import fs, interpreter
from pyscript.ffi import to_js
from pyscript.magic_js import (
RUNNING_IN_WORKER,
sync,
)
js.console.warn("experimental pyscript.fs ⚠️")
handler = None
uid = f"{path}@{id}"
options = {"id": id, "mode": mode}
if root != "":
options["startIn"] = root
if RUNNING_IN_WORKER:
fsh = sync.storeFSHandler(uid, to_js(options))
# allow both async and/or SharedArrayBuffer use case
if isinstance(fsh, bool):
success = fsh
else:
success = await fsh
if success:
from polyscript import IDBMap
idb = IDBMap.new(fs.NAMESPACE)
handler = await idb.get(uid)
else:
raise RuntimeError(fs.ERROR)
else:
success = await fs.idb.has(uid)
if success:
handler = await fs.idb.get(uid)
else:
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
await fs.idb.set(uid, handler)
mounted[path] = await interpreter.mountNativeFS(path, handler)
async def sync(path):
await mounted[path].syncfs()
async def unmount(path):
from _pyscript import interpreter
await sync(path)
interpreter._module.FS.unmount(path)

View File

@@ -25,6 +25,7 @@ class JSModule:
# avoid pyodide looking for non existent fields # avoid pyodide looking for non existent fields
if not field.startswith("_"): if not field.startswith("_"):
return getattr(getattr(js_modules, self.name), field) return getattr(getattr(js_modules, self.name), field)
return None
# generate N modules in the system that will proxy the real value # generate N modules in the system that will proxy the real value

View File

@@ -31,26 +31,22 @@ class Device:
@classmethod @classmethod
async def load(cls, audio=False, video=True): async def load(cls, audio=False, video=True):
"""Load the device stream.""" """
options = window.Object.new() Load the device stream.
options.audio = audio """
options = {}
options["audio"] = audio
if isinstance(video, bool): if isinstance(video, bool):
options.video = video options["video"] = video
else: else:
# TODO: Think this can be simplified but need to check it on the pyodide side options["video"] = {}
# TODO: this is pyodide specific. shouldn't be!
options.video = window.Object.new()
for k in video: for k in video:
setattr(options.video, k, to_js(video[k])) options["video"][k] = video[k]
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
stream = await window.navigator.mediaDevices.getUserMedia(options)
return stream
async def get_stream(self): async def get_stream(self):
key = self.kind.replace("input", "").replace("output", "") key = self.kind.replace("input", "").replace("output", "")
options = {key: {"deviceId": {"exact": self.id}}} options = {key: {"deviceId": {"exact": self.id}}}
return await self.load(**options) return await self.load(**options)

View File

@@ -10,10 +10,11 @@ def _to_idb(value):
if isinstance(value, (bool, float, int, str, list, dict, tuple)): if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value]) return _stringify(["generic", value])
if isinstance(value, bytearray): if isinstance(value, bytearray):
return _stringify(["bytearray", [v for v in value]]) return _stringify(["bytearray", list(value)])
if isinstance(value, memoryview): if isinstance(value, memoryview):
return _stringify(["memoryview", [v for v in value]]) return _stringify(["memoryview", list(value)])
raise TypeError(f"Unexpected value: {value}") msg = f"Unexpected value: {value}"
raise TypeError(msg)
# convert an IndexedDB compatible entry into a Python value # convert an IndexedDB compatible entry into a Python value
@@ -56,5 +57,6 @@ class Storage(dict):
async def storage(name="", storage_class=Storage): async def storage(name="", storage_class=Storage):
if not name: if not name:
raise ValueError("The storage name must be defined") msg = "The storage name must be defined"
raise ValueError(msg)
return storage_class(await _storage(f"@pyscript/{name}")) return storage_class(await _storage(f"@pyscript/{name}"))

View File

@@ -1,11 +1,17 @@
import js import js
import sys
import inspect
def as_bytearray(buffer): def as_bytearray(buffer):
"""
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
MicroPython friendly manner.
"""
ui8a = js.Uint8Array.new(buffer) ui8a = js.Uint8Array.new(buffer)
size = ui8a.length size = ui8a.length
ba = bytearray(size) ba = bytearray(size)
for i in range(0, size): for i in range(size):
ba[i] = ui8a[i] ba[i] = ui8a[i]
return ba return ba
@@ -31,3 +37,22 @@ class NotSupported:
def __call__(self, *args): def __call__(self, *args):
raise TypeError(self.error) raise TypeError(self.error)
def is_awaitable(obj):
"""
Returns a boolean indication if the passed in obj is an awaitable
function. (MicroPython treats awaitables as generator functions, and if
the object is a closure containing an async function we need to work
carefully.)
"""
from pyscript import config
if config["type"] == "mpy": # Is MicroPython?
# MicroPython doesn't appear to have a way to determine if a closure is
# an async function except via the repr. This is a bit hacky.
if "<closure <generator>" in repr(obj):
return True
return inspect.isgeneratorfunction(obj)
return inspect.iscoroutinefunction(obj)

View File

@@ -2,7 +2,11 @@
# `when` is not used in this module. It is imported here save the user an additional # `when` is not used in this module. It is imported here save the user an additional
# import (i.e. they can get what they need from `pyscript.web`). # import (i.e. they can get what they need from `pyscript.web`).
from pyscript import document, when # NOQA
# from __future__ import annotations # CAUTION: This is not supported in MicroPython.
from pyscript import document, when, Event # noqa: F401
from pyscript.ffi import create_proxy
def wrap_dom_element(dom_element): def wrap_dom_element(dom_element):
@@ -68,6 +72,18 @@ class Element:
type(self).get_tag_name() type(self).get_tag_name()
) )
# HTML on_events attached to the element become pyscript.Event instances.
self._on_events = {}
# Handle kwargs for handling named events with a default handler function.
properties = {}
for name, handler in kwargs.items():
if name.startswith("on_"):
ev = self.get_event(name) # Create the default Event instance.
ev.add_listener(handler)
else:
properties[name] = handler
# A set-like interface to the element's `classList`. # A set-like interface to the element's `classList`.
self._classes = Classes(self) self._classes = Classes(self)
@@ -75,7 +91,7 @@ class Element:
self._style = Style(self) self._style = Style(self)
# Set any specified classes, styles, and DOM properties. # Set any specified classes, styles, and DOM properties.
self.update(classes=classes, style=style, **kwargs) self.update(classes=classes, style=style, **properties)
def __eq__(self, obj): def __eq__(self, obj):
"""Check for equality by comparing the underlying DOM element.""" """Check for equality by comparing the underlying DOM element."""
@@ -87,19 +103,27 @@ class Element:
If `key` is an integer or a slice we use it to index/slice the element's If `key` is an integer or a slice we use it to index/slice the element's
children. Otherwise, we use `key` as a query selector. children. Otherwise, we use `key` as a query selector.
""" """
if isinstance(key, int) or isinstance(key, slice): if isinstance(key, (int, slice)):
return self.children[key] return self.children[key]
return self.find(key) return self.find(key)
def __getattr__(self, name): def __getattr__(self, name):
"""
Get an attribute from the element.
If the attribute is an event (e.g. "on_click"), we wrap it in an `Event`
instance and return that. Otherwise, we return the attribute from the
underlying DOM element.
"""
if name.startswith("on_"):
return self.get_event(name)
# This allows us to get attributes on the underlying DOM element that clash # This allows us to get attributes on the underlying DOM element that clash
# with Python keywords or built-ins (e.g. the output element has an # with Python keywords or built-ins (e.g. the output element has an
# attribute `for` which is a Python keyword, so you can access it on the # attribute `for` which is a Python keyword, so you can access it on the
# Element instance via `for_`). # Element instance via `for_`).
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
return getattr(self._dom_element, name) return getattr(self._dom_element, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
@@ -117,10 +141,37 @@ class Element:
# attribute `for` which is a Python keyword, so you can access it on the # attribute `for` which is a Python keyword, so you can access it on the
# Element instance via `for_`). # Element instance via `for_`).
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
if name.startswith("on_"):
# Ensure on-events are cached in the _on_events dict if the
# user is setting them directly.
self._on_events[name] = value
setattr(self._dom_element, name, value) setattr(self._dom_element, name, value)
def get_event(self, name):
"""
Get an `Event` instance for the specified event name.
"""
if not name.startswith("on_"):
msg = "Event names must start with 'on_'."
raise ValueError(msg)
event_name = name[3:] # Remove the "on_" prefix.
if not hasattr(self._dom_element, event_name):
msg = f"Element has no '{event_name}' event."
raise ValueError(msg)
if name in self._on_events:
return self._on_events[name]
# Such an on-event exists in the DOM element, but we haven't yet
# wrapped it in an Event instance. Let's do that now. When the
# underlying DOM element's event is triggered, the Event instance
# will be triggered too.
ev = Event()
self._on_events[name] = ev
self._dom_element.addEventListener(event_name, create_proxy(ev.trigger))
return ev
@property @property
def children(self): def children(self):
"""Return the element's children as an `ElementCollection`.""" """Return the element's children as an `ElementCollection`."""
@@ -157,7 +208,7 @@ class Element:
# We check for list/tuple here and NOT for any iterable as it will match # We check for list/tuple here and NOT for any iterable as it will match
# a JS Nodelist which is handled explicitly below. # a JS Nodelist which is handled explicitly below.
# NodeList. # NodeList.
elif isinstance(item, list) or isinstance(item, tuple): elif isinstance(item, (list, tuple)):
for child in item: for child in item:
self.append(child) self.append(child)
@@ -181,10 +232,11 @@ class Element:
except AttributeError: except AttributeError:
# Nope! This is not an element or a NodeList. # Nope! This is not an element or a NodeList.
raise TypeError( msg = (
f'Element "{item}" is a proxy object, "' f'Element "{item}" is a proxy object, "'
f"but not a valid element or a NodeList." f"but not a valid element or a NodeList."
) )
raise TypeError(msg)
def clone(self, clone_id=None): def clone(self, clone_id=None):
"""Make a clone of the element (clones the underlying DOM object too).""" """Make a clone of the element (clones the underlying DOM object too)."""
@@ -355,9 +407,8 @@ class Options:
new_option = option(**kwargs) new_option = option(**kwargs)
if before: if before and isinstance(before, Element):
if isinstance(before, Element): before = before._dom_element
before = before._dom_element
self._element._dom_element.add(new_option._dom_element, before) self._element._dom_element.add(new_option._dom_element, before)
@@ -417,7 +468,7 @@ class ContainerElement(Element):
) )
for child in list(args) + (children or []): for child in list(args) + (children or []):
if isinstance(child, Element) or isinstance(child, ElementCollection): if isinstance(child, (Element, ElementCollection)):
self.append(child) self.append(child)
else: else:
@@ -447,14 +498,13 @@ class ClassesCollection:
) )
def __iter__(self): def __iter__(self):
for class_name in self._all_class_names(): yield from self._all_class_names()
yield class_name
def __len__(self): def __len__(self):
return len(self._all_class_names()) return len(self._all_class_names())
def __repr__(self): def __repr__(self):
return f"ClassesCollection({repr(self._collection)})" return f"ClassesCollection({self._collection!r})"
def __str__(self): def __str__(self):
return " ".join(self._all_class_names()) return " ".join(self._all_class_names())
@@ -507,7 +557,7 @@ class StyleCollection:
element.style[key] = value element.style[key] = value
def __repr__(self): def __repr__(self):
return f"StyleCollection({repr(self._collection)})" return f"StyleCollection({self._collection!r})"
def remove(self, key): def remove(self, key):
"""Remove a CSS property from the elements in the collection.""" """Remove a CSS property from the elements in the collection."""
@@ -542,7 +592,7 @@ class ElementCollection:
if isinstance(key, int): if isinstance(key, int):
return self._elements[key] return self._elements[key]
elif isinstance(key, slice): if isinstance(key, slice):
return ElementCollection(self._elements[key]) return ElementCollection(self._elements[key])
return self.find(key) return self.find(key)
@@ -1079,7 +1129,8 @@ class video(ContainerElement):
elif isinstance(to, Element): elif isinstance(to, Element):
if to.tag != "canvas": if to.tag != "canvas":
raise TypeError("Element to snap to must be a canvas.") msg = "Element to snap to must be a canvas."
raise TypeError(msg)
elif getattr(to, "tagName", "") == "CANVAS": elif getattr(to, "tagName", "") == "CANVAS":
to = canvas(dom_element=to) to = canvas(dom_element=to)
@@ -1088,10 +1139,12 @@ class video(ContainerElement):
elif isinstance(to, str): elif isinstance(to, str):
nodelist = document.querySelectorAll(to) # NOQA nodelist = document.querySelectorAll(to) # NOQA
if nodelist.length == 0: if nodelist.length == 0:
raise TypeError("No element with selector {to} to snap to.") msg = "No element with selector {to} to snap to."
raise TypeError(msg)
if nodelist[0].tagName != "CANVAS": if nodelist[0].tagName != "CANVAS":
raise TypeError("Element to snap to must be a canvas.") msg = "Element to snap to must be a canvas."
raise TypeError(msg)
to = canvas(dom_element=nodelist[0]) to = canvas(dom_element=nodelist[0])

View File

@@ -24,7 +24,7 @@ class EventMessage:
return value return value
class WebSocket(object): class WebSocket:
CONNECTING = 0 CONNECTING = 0
OPEN = 1 OPEN = 1
CLOSING = 2 CLOSING = 2

View File

@@ -25,10 +25,12 @@ async def create_named_worker(src="", name="", config=None, type="py"):
from json import dumps from json import dumps
if not src: if not src:
raise ValueError("Named workers require src") msg = "Named workers require src"
raise ValueError(msg)
if not name: if not name:
raise ValueError("Named workers require a name") msg = "Named workers require a name"
raise ValueError(msg)
s = _js.document.createElement("script") s = _js.document.createElement("script")
s.type = type s.type = type
@@ -37,7 +39,7 @@ async def create_named_worker(src="", name="", config=None, type="py"):
_set(s, "name", name) _set(s, "name", name)
if config: if config:
_set(s, "config", isinstance(config, str) and config or dumps(config)) _set(s, "config", (isinstance(config, str) and config) or dumps(config))
_js.document.body.append(s) _js.document.body.append(s)
return await workers[name] return await workers[name]

View File

@@ -1,14 +1,14 @@
import { ArrayBuffer, TypedArray } from "sabayon/shared";
import IDBMapSync from "@webreflection/idb-map/sync"; import IDBMapSync from "@webreflection/idb-map/sync";
import { parse, stringify } from "flatted"; import { parse, stringify } from "flatted";
const { isView } = ArrayBuffer;
const to_idb = (value) => { const to_idb = (value) => {
if (value == null) return stringify(["null", 0]); if (value == null) return stringify(["null", 0]);
/* eslint-disable no-fallthrough */ /* eslint-disable no-fallthrough */
switch (typeof value) { switch (typeof value) {
case "object": { case "object": {
if (value instanceof TypedArray) if (isView(value)) return stringify(["memoryview", [...value]]);
return stringify(["memoryview", [...value]]);
if (value instanceof ArrayBuffer) if (value instanceof ArrayBuffer)
return stringify(["bytearray", [...new Uint8Array(value)]]); return stringify(["bytearray", [...new Uint8Array(value)]]);
} }

View File

@@ -1,3 +1,5 @@
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
export default { export default {
// allow pyterminal checks to bootstrap // allow pyterminal checks to bootstrap
is_pyterminal: () => false, is_pyterminal: () => false,
@@ -9,4 +11,21 @@ export default {
sleep(seconds) { sleep(seconds) {
return new Promise(($) => setTimeout($, seconds * 1000)); return new Promise(($) => setTimeout($, seconds * 1000));
}, },
/**
* Ask a user action via dialog and returns the directory handler once granted.
* @param {string} uid
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {boolean}
*/
async storeFSHandler(uid, options = {}) {
if (await idb.has(uid)) return true;
return getFileSystemDirectoryHandle(options).then(
async (handler) => {
await idb.set(uid, handler);
return true;
},
() => false,
);
},
}; };

View File

@@ -0,0 +1,7 @@
/* eslint no-unused-vars: 0 */
try {
crypto.randomUUID();
} catch (_) {
if (location.href.startsWith("http://0.0.0.0"))
location.href = location.href.replace("0.0.0.0", "localhost");
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<title>Pyodide Media Module Test</title>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<h1>Pyodide Media Module Test</h1>
<div id="test-results">Running tests...</div>
<script type="py" terminal>
from pyscript import window, document
from pyscript import media
async def run_tests():
# Test basic module structure
assert hasattr(media, "Device"), "media module should have Device class"
assert hasattr(media, "list_devices"), "media module should have list_devices function"
# Test device enumeration
devices = await media.list_devices()
assert isinstance(devices, list), "list_devices should return a list"
# If we have devices, test properties of one
if devices:
device = devices[0]
assert hasattr(device, "id"), "Device should have id property"
assert hasattr(device, "group"), "Device should have group property"
assert hasattr(device, "kind"), "Device should have kind property"
assert hasattr(device, "label"), "Device should have label property"
document.getElementById('test-results').innerText = "Success!"
document.documentElement.classList.add('media-ok')
await run_tests()
</script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="../../dist/core.js"></script>
<script type="mpy">
from pyscript import document
import sys
print("This is an error", file=sys.stderr)
document.documentElement.classList.add("ok");
</script>
</head>
</html>

View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="../../dist/core.js"></script>
<script type="mpy" config="mpy-no-error.toml">
from pyscript import document
import sys
print("This is an error", file=sys.stderr)
document.documentElement.classList.add("ok");
</script>
</head>
</html>

View File

@@ -0,0 +1 @@
plugins = ["!error"]

View File

@@ -1,6 +1,6 @@
import numpy import numpy as np
import matplotlib import matplotlib as mpl
# just do something with the packages # just do something with the packages
print(len(dir(numpy))) print(len(dir(np)))
print(len(dir(matplotlib))) print(len(dir(mpl)))

View 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">
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py" config='{"packages":["jsonpointer==3.0.0"]}'>
import jsonpointer
from pyscript import document
document.documentElement.classList.add("done")
document.body.append("OK")
</script>
</body>
</html>

View File

@@ -4,4 +4,4 @@ def runtime_version():
return sys.version return sys.version
__export__ = ['runtime_version'] __export__ = ["runtime_version"]

View File

@@ -138,3 +138,57 @@ test('Pyodide lockFileURL vs CDN', async ({ page }) => {
const body = await page.evaluate(() => document.body.textContent); const body = await page.evaluate(() => document.body.textContent);
await expect(body).toBe('OK'); await expect(body).toBe('OK');
}); });
test('Pyodide pinned lockFileURL', async ({ page }) => {
const logs = [];
page.on('console', msg => {
const text = msg.text();
if (!text.startsWith('['))
logs.push(text);
});
await page.goto('http://localhost:8080/tests/javascript/pyodide-lockfile/');
await page.waitForSelector('html.done');
let body = await page.evaluate(() => document.body.lastChild.textContent);
await expect(body).toBe('OK');
await expect(!!logs.splice(0).length).toBe(true);
await page.reload();
await page.waitForSelector('html.done');
body = await page.evaluate(() => document.body.lastChild.textContent);
await expect(body).toBe('OK');
await expect(logs.splice(0).length).toBe(0);
});
test('MicroPython buffered error', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
await page.waitForSelector('html.ok');
const body = await page.evaluate(() => document.body.textContent.trim());
await expect(body).toBe('This is an error');
});
test('MicroPython buffered NO error', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/mpy-no-error.html');
await page.waitForSelector('html.ok');
const body = await page.evaluate(() => document.body.textContent.trim());
await expect(body).toBe('');
});
test('Pyodide media module', async ({ page }) => {
await page.context().grantPermissions(['camera', 'microphone']);
await page.context().addInitScript(() => {
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = async function() {
const realDevices = await originalEnumerateDevices.call(this);
if (!realDevices || realDevices.length === 0) {
return [
{ deviceId: 'camera1', groupId: 'group1', kind: 'videoinput', label: 'Simulated Camera' },
{ deviceId: 'mic1', groupId: 'group2', kind: 'audioinput', label: 'Simulated Microphone' }
];
}
return realDevices;
};
});
await page.goto('http://localhost:8080/tests/javascript/media.html');
await page.waitForSelector('html.media-ok', { timeout: 10000 });
const isSuccess = await page.evaluate(() => document.documentElement.classList.contains('media-ok'));
expect(isSuccess).toBe(true);
});

View File

@@ -12,13 +12,23 @@ const {
kill, kill,
} = await donkey({ terminal: '#container' }); } = await donkey({ terminal: '#container' });
clearButton.onclick = clear; clearButton.onclick = async () => {
killButton.onclick = kill; killButton.disabled = true;
clearButton.disabled = true;
await clear();
runButton.disabled = false;
};
killButton.onclick = () => {
killButton.disabled = true;
clearButton.disabled = true;
runButton.disabled = true;
kill();
};
runButton.disabled = false; runButton.disabled = false;
runButton.onclick = async () => { runButton.onclick = async () => {
killButton.disabled = false; killButton.disabled = false;
clearButton.disabled = true; clearButton.disabled = false;
runButton.disabled = true; runButton.disabled = true;
// multiline code // multiline code
await execute(` await execute(`
@@ -29,6 +39,5 @@ runButton.onclick = async () => {
const name = await evaluate('input("what is your name? ")'); const name = await evaluate('input("what is your name? ")');
alert(`Hello ${name}`); alert(`Hello ${name}`);
killButton.disabled = true; killButton.disabled = true;
clearButton.disabled = false;
runButton.disabled = false; runButton.disabled = false;
}; };

View 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">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" src="emoji.py" terminal worker></script>
<script type="py" src="emoji.py" terminal worker></script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
import sys
print(sys.version)
RED = chr(0x1F534) # LARGE RED CIRCLE
GREEN = chr(0x1F7E2) # LARGE GREEN CIRCLE
MOUSE = chr(0x1F42D) # MOUSE FACE
EARTH = chr(0x1F30E) # EARTH GLOBE AMERICAS
FACE = chr(0x1F610) # NEUTRAL FACE
BASMALA = chr(0xFDFD) # ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM
print("[", RED, "]")
print("[", MOUSE, "]")
print("[", EARTH, "]")
print("[", FACE, "]")
print("[", FACE * 3, "]")
print("[", BASMALA, "]")
print("[", BASMALA + GREEN, "]")

View 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">
<title>PyScript Error Bug?</title>
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<py-config>
plugins = ["!error"]
</py-config>
<script type="py">
import sys
print("This is normal content")
print("This is error content", file=sys.stderr)
</script>
<!-- Attempt 2; inline config
<script type="py" config='plugins=["!error"]'>
import sys
print("This is normal content")
print("This is error content", file=sys.stderr)
</script> -->
<!-- Attempt 3; external pyscript.toml
<script type="py" config="pyscript.toml">
import sys
print("This is normal content")
print("This is error content", file=sys.stderr)
</script> -->
<!-- Attempt 4; micropython
<script type="mpy">
import sys
print("This is normal content")
print("This is error content", file=sys.stderr)
</script> -->
<div id="result"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
plugins = ["!error"]

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="mpy" src="index.py"></script>
</body>
</html>

View File

@@ -0,0 +1,46 @@
import os
from pyscript import RUNNING_IN_WORKER, fs
TEST = "implicit"
if TEST == "implicit":
await fs.mount("/persistent")
print(
(RUNNING_IN_WORKER and "Worker") or "Main",
os.listdir("/persistent"),
)
from random import random
with open("/persistent/random.txt", "w") as f:
f.write(str(random()))
await fs.sync("/persistent")
elif not RUNNING_IN_WORKER:
from pyscript import document
button = document.createElement("button")
button.textContent = "mount"
document.body.append(button)
async def mount(event):
try:
await fs.mount("/persistent")
print(os.listdir("/persistent"))
button.textContent = "unmount"
button.onclick = unmount
except:
import js
js.alert("unable to grant access")
async def unmount(event):
await fs.unmount("/persistent")
button.textContent = "mount"
button.onclick = mount
button.onclick = mount

View File

@@ -0,0 +1,30 @@
/* (c) https://github.com/ryanking13/pyodide-pygame-demo/blob/main/examples/aliens.html */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
color: #333;
}
.demo {
background-color: #fff;
margin: 20px auto;
max-width: 1000px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-radius: 8px;
overflow: hidden;
}
.demo-header {
background-color: #007bff;
color: #fff;
padding: 15px 20px;
font-size: 20px;
}
.demo-content {
padding: 20px;
}
#canvas {
margin: 0 auto;
display: block;
}

View File

@@ -0,0 +1,399 @@
"""(c) https://github.com/ryanking13/pyodide-pygame-demo/blob/main/examples/aliens.html
pygame.examples.aliens
Shows a mini game where you have to defend against aliens.
What does it show you about pygame?
* pygame.sprite, the difference between Sprite and Group.
* dirty rectangle optimization for processing for speed.
* music with pygame.mixer.music, including fadeout
* sound effects with pygame.Sound
* event processing, keyboard handling, QUIT handling.
* a main loop frame limited with a game clock from the pygame.time module
* fullscreen switching.
Controls
--------
* Left and right arrows to move.
* Space bar to shoot.
* f key to toggle between fullscreen.
"""
import asyncio
import random
import os
import pathlib
import pyscript
# import basic pygame modules
import pygame
# see if we can load more than standard BMP
if not pygame.image.get_extended():
msg = "Sorry, extended image module required"
raise SystemExit(msg)
# game constants
MAX_SHOTS = 2 # most player bullets onscreen
ALIEN_ODDS = 22 # chances a new alien appears
BOMB_ODDS = 60 # chances a new bomb will drop
ALIEN_RELOAD = 12 # frames between new aliens
SCREENRECT = pygame.Rect(0, 0, 640, 480)
SCORE = 0
main_dir = str(pathlib.Path(pygame.__file__).parent / "examples")
def load_image(file):
"""loads an image, prepares it for play"""
file = os.path.join(main_dir, "data", file)
try:
surface = pygame.image.load(file)
except pygame.error:
msg = f'Could not load image "{file}" {pygame.get_error()}'
raise SystemExit(msg)
return surface.convert()
def load_sound(file):
"""because pygame can be be compiled without mixer."""
if not pygame.mixer:
return None
file = os.path.join(main_dir, "data", file)
try:
return pygame.mixer.Sound(file)
except pygame.error:
print(f"Warning, unable to load, {file}")
return None
# Each type of game object gets an init and an update function.
# The update function is called once per frame, and it is when each object should
# change its current position and state.
#
# The Player object actually gets a "move" function instead of update,
# since it is passed extra information about the keyboard.
class Player(pygame.sprite.Sprite):
"""Representing the player as a moon buggy type car."""
speed = 10
bounce = 24
gun_offset = -11
images = []
def __init__(self):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
self.reloading = False
self.origtop = self.rect.top
self.facing = -1
def move(self, direction):
if direction:
self.facing = direction
self.rect.move_ip(direction * self.speed, 0)
self.rect = self.rect.clamp(SCREENRECT)
if direction < 0:
self.image = self.images[0]
elif direction > 0:
self.image = self.images[1]
self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)
def gunpos(self):
pos = self.facing * self.gun_offset + self.rect.centerx
return pos, self.rect.top
class Alien(pygame.sprite.Sprite):
"""An alien space ship. That slowly moves down the screen."""
speed = 13
animcycle = 12
images = []
def __init__(self):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect()
self.facing = random.choice((-1, 1)) * Alien.speed
self.frame = 0
if self.facing < 0:
self.rect.right = SCREENRECT.right
def update(self):
self.rect.move_ip(self.facing, 0)
if not SCREENRECT.contains(self.rect):
self.facing = -self.facing
self.rect.top = self.rect.bottom + 1
self.rect = self.rect.clamp(SCREENRECT)
self.frame = self.frame + 1
self.image = self.images[self.frame // self.animcycle % 3]
class Explosion(pygame.sprite.Sprite):
"""An explosion. Hopefully the Alien and not the player!"""
defaultlife = 12
animcycle = 3
images = []
def __init__(self, actor):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect(center=actor.rect.center)
self.life = self.defaultlife
def update(self):
"""called every time around the game loop.
Show the explosion surface for 'defaultlife'.
Every game tick(update), we decrease the 'life'.
Also we animate the explosion.
"""
self.life = self.life - 1
self.image = self.images[self.life // self.animcycle % 2]
if self.life <= 0:
self.kill()
class Shot(pygame.sprite.Sprite):
"""a bullet the Player sprite fires."""
speed = -11
images = []
def __init__(self, pos):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect(midbottom=pos)
def update(self):
"""called every time around the game loop.
Every tick we move the shot upwards.
"""
self.rect.move_ip(0, self.speed)
if self.rect.top <= 0:
self.kill()
class Bomb(pygame.sprite.Sprite):
"""A bomb the aliens drop."""
speed = 9
images = []
def __init__(self, alien):
pygame.sprite.Sprite.__init__(self, self.containers)
self.image = self.images[0]
self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
def update(self):
"""called every time around the game loop.
Every frame we move the sprite 'rect' down.
When it reaches the bottom we:
- make an explosion.
- remove the Bomb.
"""
self.rect.move_ip(0, self.speed)
if self.rect.bottom >= 470:
Explosion(self)
self.kill()
class Score(pygame.sprite.Sprite):
"""to keep track of the score."""
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.font = pygame.Font(None, 20)
self.font.set_italic(1)
self.color = "white"
self.lastscore = -1
self.update()
self.rect = self.image.get_rect().move(10, 450)
def update(self):
"""We only update the score in update() when it has changed."""
if self.lastscore != SCORE:
self.lastscore = SCORE
msg = "Score: %d" % SCORE
self.image = self.font.render(msg, 0, self.color)
async def main(winstyle=0):
# Initialize pygame
pygame.mixer.pre_init(44100, 32, 2, 1024)
pygame.init()
if pygame.mixer and not pygame.mixer.get_init():
print("Warning, no sound")
pygame.mixer = None
fullscreen = False
# Set the display mode
winstyle = 0 # |FULLSCREEN
screen = pygame.display.set_mode(SCREENRECT.size, winstyle)
# Load images, assign to sprite classes
# (do this before the classes are used, after screen setup)
img = load_image("player1.gif")
Player.images = [img, pygame.transform.flip(img, 1, 0)]
img = load_image("explosion1.gif")
Explosion.images = [img, pygame.transform.flip(img, 1, 1)]
Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
Bomb.images = [load_image("bomb.gif")]
Shot.images = [load_image("shot.gif")]
# decorate the game window
icon = pygame.transform.scale(Alien.images[0], (32, 32))
pygame.display.set_icon(icon)
pygame.display.set_caption("Pygame Aliens")
pygame.mouse.set_visible(0)
# create the background, tile the bgd image
bgdtile = load_image("background.gif")
background = pygame.Surface(SCREENRECT.size)
for x in range(0, SCREENRECT.width, bgdtile.get_width()):
background.blit(bgdtile, (x, 0))
screen.blit(background, (0, 0))
pygame.display.flip()
# load the sound effects
boom_sound = load_sound("boom.wav")
shoot_sound = load_sound("car_door.wav")
if pygame.mixer:
music = os.path.join(main_dir, "data", "house_lo.wav")
pygame.mixer.music.load(music)
pygame.mixer.music.play(-1)
# Initialize Game Groups
aliens = pygame.sprite.Group()
shots = pygame.sprite.Group()
bombs = pygame.sprite.Group()
all = pygame.sprite.RenderUpdates()
lastalien = pygame.sprite.GroupSingle()
# assign default groups to each sprite class
Player.containers = all
Alien.containers = aliens, all, lastalien
Shot.containers = shots, all
Bomb.containers = bombs, all
Explosion.containers = all
Score.containers = all
# Create Some Starting Values
global score
alienreload = ALIEN_RELOAD
_clock = pygame.Clock()
# initialize our starting sprites
global SCORE
player = Player()
Alien() # note, this 'lives' because it goes into a sprite group
if pygame.font:
all.add(Score())
# Run our main loop whilst the player is alive.
while player.alive():
# get input
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return
if event.type == pygame.KEYDOWN and event.key == pygame.K_f:
if not fullscreen:
print("Changing to FULLSCREEN")
screen_backup = screen.copy()
screen = pygame.display.set_mode(
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
)
screen.blit(screen_backup, (0, 0))
else:
print("Changing to windowed mode")
screen_backup = screen.copy()
screen = pygame.display.set_mode(
SCREENRECT.size, winstyle, bestdepth
)
screen.blit(screen_backup, (0, 0))
pygame.display.flip()
fullscreen = not fullscreen
keystate = pygame.key.get_pressed()
# clear/erase the last drawn sprites
all.clear(screen, background)
# update all the sprites
all.update()
# handle player input
direction = keystate[pygame.K_RIGHT] - keystate[pygame.K_LEFT]
player.move(direction)
firing = keystate[pygame.K_SPACE]
if not player.reloading and firing and len(shots) < MAX_SHOTS:
Shot(player.gunpos())
if pygame.mixer:
shoot_sound.play()
player.reloading = firing
# Create new alien
if alienreload:
alienreload = alienreload - 1
elif not int(random.random() * ALIEN_ODDS):
Alien()
alienreload = ALIEN_RELOAD
# Drop bombs
if lastalien and not int(random.random() * BOMB_ODDS):
Bomb(lastalien.sprite)
# Detect collisions between aliens and players.
for alien in pygame.sprite.spritecollide(player, aliens, 1):
if pygame.mixer:
boom_sound.play()
Explosion(alien)
Explosion(player)
SCORE = SCORE + 1
player.kill()
# See if shots hit the aliens.
for alien in pygame.sprite.groupcollide(aliens, shots, 1, 1):
if pygame.mixer:
boom_sound.play()
Explosion(alien)
SCORE = SCORE + 1
# See if alien bombs hit the player.
for bomb in pygame.sprite.spritecollide(player, bombs, 1):
if pygame.mixer:
boom_sound.play()
Explosion(player)
Explosion(bomb)
player.kill()
# draw the scene
dirty = all.draw(screen)
pygame.display.update(dirty)
# cap the framerate at 40fps. Also called 40HZ or 40 times per second.
await asyncio.sleep(0.025)
if pygame.mixer:
pygame.mixer.music.fadeout(1000)
main()

View File

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="aliens.css" />
<link rel="stylesheet" href="../../../dist/core.css" />
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py-game" src="aliens.py" config="./config.toml"></script>
<div class="demo">
<div class="demo-header">pygame.examples.aliens</div>
<div class="demo-content">
<canvas id="canvas"></canvas>
</div>
</div>
</body>
</html>

View 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">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py" config='{"packages":["jsonpointer==3.0.0"]}'>
print('Hello World')
</script>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py" src="./main.py" terminal worker></script>
</body>
</html>

View File

@@ -0,0 +1 @@
print(input("What food would you like me to get from the shop? "))

Binary file not shown.

View File

@@ -0,0 +1,20 @@
from pyscript import config
MICROPYTHON = config["type"] == "mpy"
if MICROPYTHON:
def new(obj, *args, **kwargs):
return obj.new(*args, kwargs) if kwargs else obj.new(*args)
def call(obj, *args, **kwargs):
return obj(*args, kwargs) if kwargs else obj(*args)
else:
def new(obj, *args, **kwargs):
return obj.new(*args, **kwargs)
def call(obj, *args, **kwargs):
return obj(*args, **kwargs)
if not MICROPYTHON:
import pyodide_js
pyodide_js.setDebug(True)
from pyscript.ffi import to_js, create_proxy

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Genuary</title>
<!-- Recommended meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<!-- PyScript CSS -->
<link rel="stylesheet" href="../../../dist/core.css">
<style>
body {
margin: 0;
overflow: hidden;
background-color: #4a315e;
color: white;
font-family: Inconsolata, Consolas, Monaco, Courier New;
}
.gutter {
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
cursor: row-resize;
}
py-terminal {
max-height: 7em;
max-width: calc(100vw - 90px);
}
#pyterm {
background-color: #191a1a;
}
#pyterm,
#threejs {
position: relative;
overflow: hidden;
}
</style>
<!-- This script tag bootstraps PyScript -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js"
}
}
</script>
<script type="module" src="../../../dist/core.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.5/split.min.js"></script>
</head>
<body>
<div id="stats"></div>
<div id="stats-off"></div>
<div class="split">
<div id="pyterm"></div>
<div id="threejs"></div>
</div>
<script type="py" src="./main.py" config="./pyscript.toml" async terminal></script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
from dataclasses import dataclass, field
import sys
@dataclass
class BeatSync:
fft_res: int = field()
on_beat: bool = False
beat: int = -1
since_last_beat: float = sys.maxsize
_prev: int = 0
_count: int = 0
_bins: list[int] = field(default_factory=list)
_last_detection: float = -1.0
_threshold: int = 50
_diff: int = 40
_cooldown: float = 0.2
_highest: int = 0
def __post_init__(self):
self._bins = [int(13/16*self.fft_res/2)+17, int(13/16*self.fft_res/2)+18]
def reset(self):
self.beat = -1
self._prev = 0
self._count = 0
self._last_detection = -1.0
self.since_last_beat = sys.maxsize
# print('bs reset')
def update(self, data, running_time):
self._count += 1
self.since_last_beat = running_time - self._last_detection
d = sum(data[bin] for bin in self._bins)
if d < self._threshold:
self.on_beat = False
elif d - self._prev < self._diff:
self.on_beat = False
elif self.since_last_beat < self._cooldown:
self.on_beat = False
else:
self._last_detection = running_time
self.since_last_beat = 0
self.on_beat = True
self.beat += 1
self._prev = d
@dataclass
class FreqIntensity:
freq: float = field()
fft_res: int = field()
intensity: float = 0.0
intensity_slew: float = 0.0
scale_min: float = 0.0
scale_max: float = 350
max: float = 0.0
_sample_rate: int = 48000
_bin_indexes: list[int] = field(default_factory=list)
_harmonics: int = 8
_slew_factor: float = 0.8
def __post_init__(self):
self._bin_indexes = [
round((harmonic+1) * self.freq / self._sample_rate * self.fft_res / 2)
for harmonic in range(self._harmonics)
]
print(self._bin_indexes)
def update(self, data):
intensity = 0.0
for bin in range(self._harmonics):
intensity += data[self._bin_indexes[bin]]/(bin+1)
self.intensity = intensity
self.intensity_slew = self._slew_factor * self.intensity_slew + (1 - self._slew_factor) * intensity
self.max = max(intensity, self.max)
@property
def intensity_scaled(self):
raw = max(0, min(1.0, (self.intensity_slew - self.scale_min)/(self.scale_max - self.scale_min)))
return raw * raw

View File

@@ -0,0 +1,189 @@
import asyncio
from dataclasses import dataclass, field
from typing import Callable
from pyscript import document, window
from pyscript.js_modules import three as THREE
from pyscript.js_modules.stats_gl import default as StatsGL
from pyscript.js_modules import lsgeo, line2, linemat
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
@dataclass
class SoundPlayer:
sound: THREE.Audio = field()
on_start: Callable[[], None] = field()
on_stop: Callable[[], None] = field(default=lambda: None)
_start_time: float = -1.0
def play(self):
self.sound.stop()
self.on_start()
self._start_time = self.sound.context.currentTime
self.sound.play()
def stop(self):
self.sound.stop()
self.on_stop()
self._start_time = -1.0
def toggle(self):
if self.sound.isPlaying:
self.stop()
else:
self.play()
@property
def running_time(self):
if self.sound.isPlaying:
return self.sound.context.currentTime - self._start_time
elif self._start_time != -1.0:
self.stop()
return 0.0
def get_renderer():
renderer = new(THREE.WebGLRenderer, antialias=True)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0xF5F0DC)
pyterms = list(document.getElementsByTagName("py-terminal"))
if pyterms:
pyterm = pyterms[0]
pyterm.parentNode.removeChild(pyterm)
document.getElementById("pyterm").appendChild(pyterm)
document.getElementById("threejs").appendChild(renderer.domElement)
initial = {0: "115px", 1: "calc(100vh - 120px)"}
@create_proxy
def split_element_style(dimension, size, gutter_size, index):
if index in initial:
result = {dimension: initial.pop(index)}
else:
result = {dimension: f"calc({int(size)}vh - {gutter_size}px)"}
return to_js(result)
call(
window.Split,
["#pyterm", "#threejs"],
direction="vertical",
elementStyle=split_element_style,
minSize=0,
maxSize=to_js([120, 10000]),
)
return renderer
def get_ortho_camera(view_size):
aspect_ratio = window.innerWidth / window.innerHeight
camera = new(
THREE.OrthographicCamera,
-view_size * aspect_ratio, # Left
view_size * aspect_ratio, # Right
view_size, # Top
-view_size, # Bottom
-view_size, # Near plane
view_size, # Far plane
)
camera.updateProjectionMatrix()
camera.position.set(0, 0, 0)
return camera
def get_loading_manager():
loading_mgr = new(THREE.LoadingManager)
ev = asyncio.Event()
@create_proxy
def on_start(url, itemsLoaded, itemsTotal):
print(f'[{itemsLoaded}/{itemsTotal}] Started loading file: {url}')
loading_mgr.onStart = on_start
@create_proxy
def on_progress(url, itemsLoaded, itemsTotal):
print(f'[{itemsLoaded}/{itemsTotal}] Loading file: {url}')
loading_mgr.onProgress = on_progress
@create_proxy
def on_error(url):
print(f'There was a problem loading {url}')
loading_mgr.onError = on_error
@create_proxy
def on_load():
print('Loading assets complete!')
ev.set()
loading_mgr.onLoad = on_load
return loading_mgr, ev
def get_perspective_camera():
aspect_ratio = window.innerWidth / window.innerHeight
camera = new(
THREE.PerspectiveCamera,
45, # fov
aspect_ratio,
0.25, # near plane
300, # far plane
)
camera.position.set(0, 0, 30)
return camera
def get_stats_gl(renderer):
stats = new(StatsGL, trackGPU=True, horizontal=False)
stats.init(renderer)
stats.dom.style.removeProperty("left")
stats.dom.style.right = "90px"
document.getElementById("stats").appendChild(stats.dom)
return stats
def bg_from_v(*vertices):
geometry = new(THREE.BufferGeometry)
vertices_f32a = new(Float32Array, vertices)
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
return geometry.setAttribute('position', attr)
def bg_from_p(*points):
buf = new(THREE.BufferGeometry)
buf.setFromPoints(
[new(THREE.Vector3, p[0], p[1], p[2]) for p in points]
)
return buf
def clear():
# toggle stats and terminal?
stats_style = document.getElementById("stats-off").style
if stats_style.display == "none":
# turn stuff back on
stats_style.removeProperty("display")
document.getElementById("pyterm").style.height = "115px"
document.getElementById("threejs").style.height = "calc(100vh - 120px)"
for e in document.getElementsByClassName("gutter"):
e.style.removeProperty("display")
for e in document.getElementsByClassName("xterm-helper-textarea"):
e.focus()
break
return
# no longer focus on xterm
document.activeElement.blur()
# hide stats
document.getElementById("stats-off").style.display = "none"
# hide pyterm and split gutter
document.getElementById("pyterm").style.height = "0vh"
document.getElementById("threejs").style.height = "100vh"
for e in document.getElementsByClassName("gutter"):
e.style.display = "none"
# hide ltk ad
for e in document.getElementsByClassName("ltk-built-with"):
e.style.display = "none"
# hide pyscript ad
for e in document.getElementsByTagName("div"):
style = e.getAttribute("style")
if style and style.startswith("z-index:999"):
e.style.display = "none"
for e in document.getElementsByTagName("svg"):
style = e.getAttribute("style")
if style and style.startswith("z-index:999"):
e.style.display = "none"

View File

@@ -0,0 +1,285 @@
print("Starting up...")
from array import array
import asyncio
import math
import time
from pyscript import document, window, PyWorker
from libthree import THREE, clear, SoundPlayer
from libthree import get_renderer, get_ortho_camera
from libthree import get_loading_manager, get_stats_gl
from libthree import lsgeo, line2, linemat, lsgeo
from libfft import BeatSync
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy
from js import Float32Array
scene = new(THREE.Scene)
view_size = 1
renderer = get_renderer()
camera = get_ortho_camera(view_size)
loading_mgr, loaded_event = get_loading_manager()
t_loader = new(THREE.TextureLoader, loading_mgr)
t_loader.setPath('assets/')
light = new(THREE.AmbientLight, 0xffffff, 1.0)
scene.add(light)
fft_res = 2048
audio_listener = new(THREE.AudioListener)
camera.add(audio_listener)
sound = new(THREE.Audio, audio_listener)
audio_loader = new(THREE.AudioLoader, loading_mgr)
analyser = new(THREE.AudioAnalyser, sound, fft_res)
@create_proxy
def on_audio_load(buffer):
sound.setBuffer(buffer)
sound.setVolume(0.9)
sound.setLoop(False)
audio_loader.load("assets/genuary25-18.m4a", on_audio_load)
spheres = new(THREE.Group)
scene.add(spheres)
line_basic_mat = new(
THREE.LineBasicMaterial,
color=0xffffff,
)
zero_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=3,
)
other_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=1.5,
)
grid_mat = new(
linemat.LineMaterial,
color=0x662503,
linewidth=1,
dashed=True,
dashScale=1,
dashSize=0.5,
gapSize=1,
dashOffset=0,
)
lines = [new(THREE.Group), new(THREE.Group)]
scene.add(lines[0])
scene.add(lines[1])
def draw_lines(line_coords, mat_name, spy=False):
if spy:
line_coords_f32a = new(Float32Array, line_coords.length)
_it = line_coords.items
for i in range(line_coords.length):
line_coords_f32a[i] = _it[i]
else:
line_coords_f32a = new(Float32Array, line_coords)
if mat_name == 'zero':
mat = zero_mat
elif mat_name == 'grid':
mat = grid_mat
else:
mat = other_mat
geo = new(THREE.BufferGeometry)
geo.setAttribute('position', new(THREE.BufferAttribute, line_coords_f32a, 3))
seg = new(THREE.LineSegments, geo, line_basic_mat)
lsg = new(lsgeo.LineSegmentsGeometry)
lsg.fromLineSegments(seg)
l1 = new(line2.Line2, lsg, mat)
l1.computeLineDistances()
l2 = new(line2.Line2, lsg, mat)
l2.computeLineDistances()
lines[0].add(l1)
lines[1].add(l2)
seg.geometry.dispose()
del geo
del seg
def drawing_done():
maybe_with_spy = "with SPy" if USE_SPY else "with pure Python"
print(f"Time elapsed computing {maybe_with_spy}:", time.time() - start_ts)
drawing_event.set()
grid_width = 0
grid_height = 0
scroll_offset = 0
def scale_lines(grid_ws=None, grid_hs=None, offset=None):
global grid_width, grid_height, scroll_offset
if grid_ws:
grid_width = grid_ws
else:
grid_ws = grid_width
if grid_hs:
grid_height = grid_hs
else:
grid_hs = grid_height
if offset:
scroll_offset = offset
else:
offset = scroll_offset
scale = 2.04/grid_hs
lines[0].scale.set(scale, scale, scale)
lines[1].scale.set(scale, scale, scale)
lines[0].position.set((offset - grid_ws/2) * scale, -grid_hs/2 * scale, 0)
lines[1].position.set((offset + grid_ws/2) * scale, -grid_hs/2 * scale, 0)
def append_p(lines, p1, p2):
lines.append(p1[0])
lines.append(p1[1])
lines.append(0)
lines.append(p2[0])
lines.append(p2[1])
lines.append(0)
def initial_calc():
grid_w = int(1920 * 4)
grid_h = 1080 * 2
grid_scale = 10
noise_factor = 500
grid_hs = int(grid_h/grid_scale)
grid_ws = int(grid_w/grid_scale)
crossfade_range = int(grid_ws/12.5)
def grid_lines():
lines = array("d")
grid_goal = 24
grid_size_i = int(round((grid_ws - crossfade_range) / grid_goal))
grid_actual = (grid_ws - crossfade_range) / grid_size_i
for i in range(0, grid_size_i):
x = i * grid_actual
append_p(lines, (x, 0), (x, grid_hs))
for y in range(0, grid_hs, grid_goal):
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
return lines
import perlin
spy_perlin = perlin.lib
spy_perlin.init()
spy_perlin.seed(44)
scale_lines(grid_ws - crossfade_range, grid_hs)
print("Computing the height map")
spy_perlin.make_height_map(grid_ws, grid_hs)
spy_perlin.update_height_map(grid_ws, grid_hs, grid_scale / noise_factor, 0)
print("Cross-fading the height map")
spy_perlin.crossfade_height_map(grid_ws, grid_hs, crossfade_range)
print("Drawing grid")
draw_lines(grid_lines(), 'grid')
print("Marching squares")
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0), 'zero', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.3), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.3), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.45), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.45), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.6), 'positive', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.6), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, -0.8), 'negative', spy=True)
draw_lines(spy_perlin.marching_squares(grid_ws, grid_hs, 0.8), 'positive', spy=True)
drawing_done()
drawing_event = asyncio.Event()
start_ts = time.time()
USE_SPY = True
if USE_SPY:
initial_calc()
else:
worker = PyWorker("./worker.py", type="pyodide", configURL="./pyscript.toml")
worker.sync.draw_lines = draw_lines
worker.sync.drawing_done = drawing_done
worker.sync.scale_lines = scale_lines
worker.sync.print = print
@create_proxy
def on_tap(event):
clear()
player.toggle()
document.addEventListener("click", on_tap)
@create_proxy
def on_key_down(event):
element = document.activeElement
_class = element.getAttribute("class")
in_xterm = element.tagName != "BODY" and _class and "xterm" in _class
if event.code == "Backquote":
# Screenshot mode.
clear()
elif not in_xterm:
# Don't react to those bindings when typing code.
if event.code == "Space":
player.toggle()
document.addEventListener("keydown", on_key_down)
@create_proxy
def on_window_resize(event):
aspect_ratio = window.innerWidth / window.innerHeight
if camera.type == "OrthographicCamera":
camera.left = -view_size * aspect_ratio
camera.right = view_size * aspect_ratio
camera.top = view_size
camera.bottom = -view_size
camera.updateProjectionMatrix()
elif camera.type == "PerspectiveCamera":
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
else:
raise ValueError("Unknown camera type")
renderer.setSize(window.innerWidth, window.innerHeight)
scale_lines()
window.addEventListener("resize", on_window_resize)
@create_proxy
def animate(now=0.0):
data = analyser.getFrequencyData()#.to_py() in Pyodide
audio_now = player.running_time
bs.update(data, audio_now)
if grid_width:
offset = -((20 * audio_now) % grid_width)
scale_lines(offset=offset)
renderer.render(scene, camera)
stats_gl.update()
def reset():
global scroll_offset
bs.reset()
scale_lines()
def on_stop():
global scroll_offset
bs.reset()
scale_lines()
await loaded_event.wait()
stats_gl = get_stats_gl(renderer)
player = SoundPlayer(sound=sound, on_start=reset, on_stop=on_stop)
bs = BeatSync(fft_res=fft_res)
renderer.setAnimationLoop(animate)
print("Waiting for the contours...")
await drawing_event.wait()
print("Tap the map to start...")

View File

@@ -0,0 +1,110 @@
# Translated from https://github.com/josephg/noisejs.
from libthree import THREE
from multipyjs import new
class V3:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"V3({self.x}, {self.y}, {self.z})"
def dot2(self, x, y):
return self.x * x + self.y * y
def dot3(self, x, y, z):
return self.x * x + self.y * y + self.z * z
def to_js(self, scale=1.0):
return new(THREE.Vector3, self.x * scale, self.y * scale, self.z * scale)
PERM = [0] * 512
V3_P = [0] * 512 # assigned V3s in seed()
P = [151, 160, 137, 91, 90, 15,
131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180]
V3_I = [V3(1, 1, 0), V3(-1, 1, 0), V3(1, -1, 0), V3(-1, -1, 0),
V3(1, 0, 1), V3(-1, 0, 1), V3(1, 0, -1), V3(-1, 0, -1),
V3(0, 1, 1), V3(0, -1, 1), V3(0, 1, -1), V3(0, -1, -1)]
def seed(s):
if isinstance(s, float) and 0.0 < s < 1.0:
s *= 65536
s = int(s)
if s < 256:
s |= s << 8
for i in range(256):
if i & 1:
v = P[i] ^ (s & 255)
else:
v = P[i] ^ ((s >> 8) & 255)
PERM[i] = PERM[i + 256] = v
V3_P[i] = V3_P[i + 256] = V3_I[v % 12]
seed(0)
def fade(t):
return t * t * t * (t * (t * 6 - 15) + 10)
def lerp(a, b, t):
return (1 - t) * a + t * b
def perlin3(x, y, z):
# grid cells
x_c = int(x)
y_c = int(y)
z_c = int(z)
# relative coords within the cell
x -= x_c
y -= y_c
z -= z_c
# wrap cells
x_c &= 255
y_c &= 255
z_c &= 255
# noise contributions to corners
n000 = V3_P[x_c + PERM[y_c + PERM[z_c]]].dot3(x, y, z)
n001 = V3_P[x_c + PERM[y_c + PERM[z_c + 1]]].dot3(x, y, z - 1)
n010 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c]]].dot3(x, y - 1, z)
n011 = V3_P[x_c + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x, y - 1, z - 1)
n100 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c]]].dot3(x - 1, y, z)
n101 = V3_P[x_c + 1 + PERM[y_c + PERM[z_c + 1]]].dot3(x - 1, y, z - 1)
n110 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c]]].dot3(x - 1, y - 1, z)
n111 = V3_P[x_c + 1 + PERM[y_c + 1 + PERM[z_c + 1]]].dot3(x - 1, y - 1, z - 1)
# fade curve
u = fade(x)
v = fade(y)
w = fade(z)
# interpolation
return lerp(
lerp(lerp(n000, n100, u), lerp(n001, n101, u), w),
lerp(lerp(n010, n110, u), lerp(n011, n111, u), w),
v,
)
def curl2(x, y, z):
# https://www.bit-101.com/2017/2021/07/curl-noise/
delta = 0.01
n1 = perlin3(x + delta, y, z)
n2 = perlin3(x - delta, y, z)
cy = -(n1 - n2) / (delta * 2)
n1 = perlin3(x, y + delta, z)
n2 = perlin3(x, y - delta, z)
cx = -(n1 - n2) / (delta * 2)
print(n1, n2)
return V3(cx, cy, 0)

View File

@@ -0,0 +1,16 @@
name = "Marching Squares with SPy Copy Copy"
packages = [ "cffi", "./glue/perlin-0.0.0-cp312-cp312-pyodide_2024_0_wasm32.whl",]
[files]
"./libthree.py" = ""
"./libfft.py" = ""
"./perlin_py.py" = ""
"./worker.py" = ""
"./glue/multipyjs.py" = "./multipyjs.py"
[js_modules.main]
"https://cdn.jsdelivr.net/npm/three@v0.173.0/build/three.module.js" = "three"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineMaterial.js" = "linemat"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/Line2.js" = "line2"
"https://cdn.jsdelivr.net/npm/three@v0.173.0/examples/jsm/lines/LineSegmentsGeometry.js" = "lsgeo"
"https://cdn.jsdelivr.net/npm/stats-gl@3.6.0/dist/main.js" = "stats_gl"

View File

@@ -0,0 +1,141 @@
from array import array
from pyscript import sync, window
from perlin_py import perlin3, seed
grid_w = int(1920 * 4)
grid_h = 1080 * 2
grid_scale = 10
noise_factor = 500
grid_hs = int(grid_h/grid_scale)
grid_ws = int(grid_w/grid_scale)
crossfade_range = int(grid_ws/12.5)
height_map = array("d", [0.0] * (grid_hs * grid_ws))
edge_table = [
(), # 0
((3, 2),), # 1
((2, 1),), # 2
((3, 1),), # 3
((0, 1),), # 4
((0, 3), (1, 2)), # 5 (ambiguous)
((0, 2),), # 6
((0, 3),), # 7
((0, 3),), # 8
((0, 2),), # 9
((0, 1), (2, 3)), # 10 (ambiguous)
((0, 1),), # 11
((3, 1),), # 12
((2, 1),), # 13
((3, 2),), # 14
(), # 15
]
def update_height_map(z):
i = 0
for y in range(0, grid_h, grid_scale):
for x in range(0, grid_w, grid_scale):
# 3 octaves of noise
n = perlin3(x/noise_factor, y/noise_factor, z)
n += 0.50 * perlin3(2*x/noise_factor, 2*y/noise_factor, z)
n += 0.25 * perlin3(4*x/noise_factor, 4*y/noise_factor, z)
height_map[i] = n
i += 1
def crossfade_height_map():
for y in range(grid_hs):
for x in range(crossfade_range):
pos_i = y*grid_ws + x
neg_i = y*grid_ws + grid_ws - crossfade_range + x
weight = x/crossfade_range
old_pos = height_map[pos_i]
old_neg = height_map[neg_i]
height_map[neg_i] = height_map[pos_i] = weight * old_pos + (1.0 - weight) * old_neg
def _crossfade_height_map():
for y in range(grid_hs):
for x in range(crossfade_range):
pos_i = y*grid_ws + x
neg_i = y*grid_ws + grid_ws - x - 1
old_pos = height_map[pos_i]
old_neg = height_map[neg_i]
weight = 0.5 - x/crossfade_range/2
height_map[pos_i] = (1.0 - weight) * old_pos + weight * old_neg
height_map[neg_i] = (1.0 - weight) * old_neg + weight * old_pos
def interpolate(sq_threshold, v1, v2):
if v1 == v2:
return v1
return (sq_threshold - v1) / (v2 - v1)
stats = {'maxx': 0, 'maxy': 0, 'minx': 0, 'miny': 0}
def append_p(lines, p1, p2):
lines.append(p1[0])
lines.append(p1[1])
lines.append(0)
lines.append(p2[0])
lines.append(p2[1])
lines.append(0)
stats['maxy'] = max(p1[1], p2[1], stats['maxy'])
stats['miny'] = min(p1[1], p2[1], stats['miny'])
stats['maxx'] = max(p1[0], p2[0], stats['maxx'])
stats['minx'] = min(p1[0], p2[0], stats['minx'])
def marching_squares(height_map, sq_threshold):
lines = array("d")
for y in range(grid_hs-1):
for x in range(grid_ws-1): #cf
tl = height_map[y*grid_ws + x]
tr = height_map[y*grid_ws + x+1]
bl = height_map[(y+1)*grid_ws + x]
br = height_map[(y+1)*grid_ws + x+1]
sq_idx = 0
if tl > sq_threshold:
sq_idx |= 8
if tr > sq_threshold:
sq_idx |= 4
if br > sq_threshold:
sq_idx |= 2
if bl > sq_threshold:
sq_idx |= 1
edge_points = [
(x + interpolate(sq_threshold, tl, tr), y),
(x + 1, y + interpolate(sq_threshold, tr, br)),
(x + interpolate(sq_threshold, bl, br), y + 1),
(x, y + interpolate(sq_threshold, tl, bl)),
]
for a, b in edge_table[sq_idx]:
append_p(lines, edge_points[a], edge_points[b])
return lines
def grid_lines():
lines = array("d")
for x in range(0, grid_ws - crossfade_range, 26):
append_p(lines, (x, 0), (x, grid_hs))
for y in range(0, grid_hs, 24):
append_p(lines, (0, y), (grid_ws-crossfade_range, y))
return lines
seed(44)
sync.scale_lines(grid_ws - crossfade_range, grid_hs)
sync.print("Computing the height map")
update_height_map(0)
sync.print("Cross-fading the height map")
crossfade_height_map()
sync.draw_lines(grid_lines(), 'grid')
sync.draw_lines(marching_squares(height_map, 0), 'zero')
sync.draw_lines(marching_squares(height_map, 0.3), 'positive')
sync.draw_lines(marching_squares(height_map, -0.3), 'negative')
sync.draw_lines(marching_squares(height_map, 0.45), 'positive')
sync.draw_lines(marching_squares(height_map, -0.45), 'negative')
sync.draw_lines(marching_squares(height_map, 0.6), 'positive')
sync.draw_lines(marching_squares(height_map, -0.6), 'negative')
sync.draw_lines(marching_squares(height_map, -0.8), 'negative')
sync.draw_lines(marching_squares(height_map, 0.8), 'positive')
print(stats)
sync.drawing_done()

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<div id="status">Status:</div>
<canvas id="canvas" width="200" height="200"></canvas>
<script type="py-game" src="./main.py" config="./pyscript.toml"></script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
import sys
print("Starting test...")
# Try NumPy
try:
import numpy as np
arr = np.array([1, 2, 3])
print(f"NumPy works: {arr.mean()}")
except Exception as e:
print(f"NumPy error: {e}")
# Try PyGame without NumPy first
try:
print("Testing PyGame...")
import pygame
screen = pygame.display.set_mode((200, 200))
screen.fill((255, 0, 0)) # Fill with red
pygame.display.flip()
print("PyGame works!")
except Exception as e:
print(f"PyGame error: {e}")
# Now try PyGame with NumPy
try:
print("Testing PyGame+NumPy...")
color_array = np.random.randint(0, 255, size=(50, 50, 3), dtype=np.uint8)
surface = pygame.surfarray.make_surface(color_array)
screen.blit(surface, (75, 75))
pygame.display.flip()
print("PyGame+NumPy integration works!")
except Exception as e:
print(f"PyGame+NumPy integration error: {e}")
print("Test completed")

View File

@@ -0,0 +1,2 @@
name = "PyGame Numpy Minimal Example Copy"
packages = [ "numpy", ]

View File

@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';
const timeout = 60 * 1000;
test.setTimeout(timeout);
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -1,35 +0,0 @@
import { test, expect } from '@playwright/test';
test.setTimeout(120 * 1000);
test('Python unit tests - MicroPython on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on MAIN thread', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor(); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';
const timeout = 120 * 1000;
test.setTimeout(timeout);
test('Python unit tests - MicroPython on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});
test('Python unit tests - Pyodide on WORKER', async ({ page }) => {
await page.goto('http://localhost:8080/tests/python/index.html?type=py&worker');
const result = page.locator("#result"); // Payload for results will be here.
await result.waitFor({ timeout }); // wait for the result.
const data = JSON.parse(await result.textContent()); // get the result data.
await expect(data.fails).toMatchObject([]); // ensure no test failed.
});

View File

@@ -62,6 +62,7 @@
<button id="a-test-button">I'm a button to be clicked</button> <button id="a-test-button">I'm a button to be clicked</button>
<button>I'm another button you can click</button> <button>I'm another button you can click</button>
<button id="a-third-button">2 is better than 3 :)</button> <button id="a-third-button">2 is better than 3 :)</button>
<button id="another-test-button">I'm another button to be clicked</button>
<div id="element-append-tests"></div> <div id="element-append-tests"></div>
<p class="collection"></p> <p class="collection"></p>

View File

@@ -1,6 +1,6 @@
{ {
"files": { "files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "", "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py", "./tests/test_config.py": "tests/test_config.py",
"./tests/test_current_target.py": "tests/test_current_target.py", "./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py", "./tests/test_display.py": "tests/test_display.py",
@@ -8,11 +8,12 @@
"./tests/test_fetch.py": "tests/test_fetch.py", "./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py", "./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py", "./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_media.py": "tests/test_media.py",
"./tests/test_storage.py": "tests/test_storage.py", "./tests/test_storage.py": "tests/test_storage.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py", "./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py", "./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py", "./tests/test_websocket.py": "tests/test_websocket.py",
"./tests/test_when.py": "tests/test_when.py", "./tests/test_events.py": "tests/test_events.py",
"./tests/test_window.py": "tests/test_window.py" "./tests/test_window.py": "tests/test_window.py"
}, },
"js_modules": { "js_modules": {

View File

@@ -1,18 +1,19 @@
{ {
"files": { "files": {
"https://raw.githubusercontent.com/ntoll/upytest/1.0.8/upytest.py": "", "https://raw.githubusercontent.com/ntoll/upytest/1.0.9/upytest.py": "",
"./tests/test_config.py": "tests/test_config.py", "./tests/test_config.py": "tests/test_config.py",
"./tests/test_current_target.py": "tests/test_current_target.py", "./tests/test_current_target.py": "tests/test_current_target.py",
"./tests/test_display.py": "tests/test_display.py", "./tests/test_display.py": "tests/test_display.py",
"./tests/test_document.py": "tests/test_document.py", "./tests/test_document.py": "tests/test_document.py",
"./tests/test_fetch.py": "tests/test_fetch.py", "./tests/test_fetch.py": "tests/test_fetch.py",
"./tests/test_ffi.py": "tests/test_ffi.py", "./tests/test_ffi.py": "tests/test_ffi.py",
"./tests/test_media.py": "tests/test_media.py",
"./tests/test_js_modules.py": "tests/test_js_modules.py", "./tests/test_js_modules.py": "tests/test_js_modules.py",
"./tests/test_storage.py": "tests/test_storage.py", "./tests/test_storage.py": "tests/test_storage.py",
"./tests/test_running_in_worker.py": "tests/test_running_in_worker.py", "./tests/test_running_in_worker.py": "tests/test_running_in_worker.py",
"./tests/test_web.py": "tests/test_web.py", "./tests/test_web.py": "tests/test_web.py",
"./tests/test_websocket.py": "tests/test_websocket.py", "./tests/test_websocket.py": "tests/test_websocket.py",
"./tests/test_when.py": "tests/test_when.py", "./tests/test_events.py": "tests/test_events.py",
"./tests/test_window.py": "tests/test_window.py" "./tests/test_window.py": "tests/test_window.py"
}, },
"js_modules": { "js_modules": {

Some files were not shown because too many files have changed in this diff Show More