mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
43 Commits
when-updat
...
2025.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42c6cb775e | ||
|
|
b11fb2e893 | ||
|
|
3223a9c7e9 | ||
|
|
139ce9b5fb | ||
|
|
3b1af0688c | ||
|
|
7284f7f15f | ||
|
|
16ebc50481 | ||
|
|
b911ea99fb | ||
|
|
46ca9154c4 | ||
|
|
afd7a8eb00 | ||
|
|
b22f384d73 | ||
|
|
caeab77a8e | ||
|
|
f2bbc6ed5f | ||
|
|
1d666b92a2 | ||
|
|
290eb03388 | ||
|
|
55031f2347 | ||
|
|
8168383653 | ||
|
|
3ff2c171bc | ||
|
|
edbac13713 | ||
|
|
46239caa19 | ||
|
|
0366e48fad | ||
|
|
b13317d32f | ||
|
|
57b1440a10 | ||
|
|
fc53356a1d | ||
|
|
5be99456f0 | ||
|
|
7adedcc704 | ||
|
|
d143b229ed | ||
|
|
0d74a60227 | ||
|
|
ce923a354f | ||
|
|
7e65836423 | ||
|
|
796373cfa6 | ||
|
|
0a1d3bb678 | ||
|
|
4e43d3e92d | ||
|
|
5acc2afaf3 | ||
|
|
56c64cbee7 | ||
|
|
4ff02a24d1 | ||
|
|
a5dc94792b | ||
|
|
0db79e0f02 | ||
|
|
283eabdb30 | ||
|
|
3f19e00410 | ||
|
|
9233d5e45a | ||
|
|
fe580cd90b | ||
|
|
00e6cfed29 |
@@ -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"]
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -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"
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -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
205
core/LICENSE
Normal 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.
|
||||||
974
core/package-lock.json
generated
974
core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@pyscript/core",
|
"name": "@pyscript/core",
|
||||||
"version": "0.6.7",
|
"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,41 +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",
|
"add-promise-listener": "^0.1.3",
|
||||||
"basic-devtools": "^0.1.6",
|
"basic-devtools": "^0.1.6",
|
||||||
"polyscript": "^0.16.3",
|
"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.1",
|
"@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",
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror.license.txt
Normal 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.
|
||||||
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_commands.license.txt
Normal 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.
|
||||||
@@ -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.
|
||||||
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_language.license.txt
Normal 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.
|
||||||
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_state.license.txt
Normal 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.
|
||||||
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal file
21
core/src/3rd-party-licenses/codemirror_view.license.txt
Normal 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.
|
||||||
21
core/src/3rd-party-licenses/toml.license.txt
Normal file
21
core/src/3rd-party-licenses/toml.license.txt
Normal 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.
|
||||||
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal file
25
core/src/3rd-party-licenses/xterm-readline.license.txt
Normal 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.
|
||||||
21
core/src/3rd-party-licenses/xterm.license.txt
Normal file
21
core/src/3rd-party-licenses/xterm.license.txt
Normal 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.
|
||||||
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal file
19
core/src/3rd-party-licenses/xterm_addon-fit.license.txt
Normal 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.
|
||||||
@@ -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.
|
||||||
4
core/src/3rd-party/README.md
vendored
4
core/src/3rd-party/README.md
vendored
@@ -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.
|
||||||
|
|||||||
6
core/src/3rd-party/xterm-readline.js
vendored
6
core/src/3rd-party/xterm-readline.js
vendored
File diff suppressed because one or more lines are too long
4
core/src/3rd-party/xterm.css
vendored
4
core/src/3rd-party/xterm.css
vendored
@@ -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}
|
||||||
|
|||||||
6
core/src/3rd-party/xterm.js
vendored
6
core/src/3rd-party/xterm.js
vendored
File diff suppressed because one or more lines are too long
2
core/src/3rd-party/xterm_addon-fit.js
vendored
2
core/src/3rd-party/xterm_addon-fit.js
vendored
@@ -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
|
||||||
|
|||||||
2
core/src/3rd-party/xterm_addon-web-links.js
vendored
2
core/src/3rd-party/xterm_addon-web-links.js
vendored
@@ -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
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,24 +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));
|
|
||||||
} else if (key === "error") {
|
|
||||||
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
|
||||||
}
|
}
|
||||||
}
|
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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
82
core/src/fs.js
Normal 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;
|
||||||
|
};
|
||||||
@@ -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>} */
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
31
core/src/plugins/codemirror.js
Normal file
31
core/src/plugins/codemirror.js
Normal 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"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ const { stringify } = JSON;
|
|||||||
|
|
||||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||||
|
|
||||||
const donkey = ({ type = "py", persistent, terminal, config }) => {
|
const donkey = ({
|
||||||
const args = persistent ? ["globals()", "__locals__"] : ["{}", "{}"];
|
type = "py",
|
||||||
|
persistent,
|
||||||
|
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([
|
||||||
@@ -45,6 +52,7 @@ const donkey = ({ type = "py", persistent, terminal, config }) => {
|
|||||||
typeof config === "string" ? config : stringify(config),
|
typeof config === "string" ? config : stringify(config),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||||
|
|
||||||
return addPromiseListener(
|
return addPromiseListener(
|
||||||
document.body.appendChild(script),
|
document.body.appendChild(script),
|
||||||
|
|||||||
@@ -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
112
core/src/plugins/py-game.js
Normal 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,
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
core/src/service-worker.js
Normal file
1
core/src/service-worker.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "polyscript/service-worker";
|
||||||
File diff suppressed because one or more lines are too long
@@ -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"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
168
core/src/stdlib/pyscript/events.py
Normal file
168
core/src/stdlib/pyscript/events.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
60
core/src/stdlib/pyscript/fs.py
Normal file
60
core/src/stdlib/pyscript/fs.py
Normal 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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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}"))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)]]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
7
core/src/zero-redirect.js
Normal file
7
core/src/zero-redirect.js
Normal 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
39
core/tests/javascript/media.html
Normal file
39
core/tests/javascript/media.html
Normal 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>
|
||||||
@@ -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)))
|
||||||
|
|||||||
17
core/tests/javascript/pyodide-lockfile/index.html
Normal file
17
core/tests/javascript/pyodide-lockfile/index.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<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>
|
||||||
@@ -4,4 +4,4 @@ def runtime_version():
|
|||||||
return sys.version
|
return sys.version
|
||||||
|
|
||||||
|
|
||||||
__export__ = ['runtime_version']
|
__export__ = ["runtime_version"]
|
||||||
|
|||||||
@@ -139,6 +139,25 @@ test('Pyodide lockFileURL vs CDN', async ({ page }) => {
|
|||||||
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 }) => {
|
test('MicroPython buffered error', async ({ page }) => {
|
||||||
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
||||||
await page.waitForSelector('html.ok');
|
await page.waitForSelector('html.ok');
|
||||||
@@ -152,3 +171,24 @@ test('MicroPython buffered NO error', async ({ page }) => {
|
|||||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||||
await expect(body).toBe('');
|
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);
|
||||||
|
});
|
||||||
|
|||||||
13
core/tests/manual/emoji.html
Normal file
13
core/tests/manual/emoji.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<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>
|
||||||
17
core/tests/manual/emoji.py
Normal file
17
core/tests/manual/emoji.py
Normal 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, "]")
|
||||||
12
core/tests/manual/fs/index.html
Normal file
12
core/tests/manual/fs/index.html
Normal 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>
|
||||||
46
core/tests/manual/fs/index.py
Normal file
46
core/tests/manual/fs/index.py
Normal 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
|
||||||
30
core/tests/manual/game/aliens.css
Normal file
30
core/tests/manual/game/aliens.css
Normal 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;
|
||||||
|
}
|
||||||
399
core/tests/manual/game/aliens.py
Normal file
399
core/tests/manual/game/aliens.py
Normal 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()
|
||||||
0
core/tests/manual/game/config.toml
Normal file
0
core/tests/manual/game/config.toml
Normal file
19
core/tests/manual/game/index.html
Normal file
19
core/tests/manual/game/index.html
Normal 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>
|
||||||
13
core/tests/manual/issue-2228/index.html
Normal file
13
core/tests/manual/issue-2228/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<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>
|
||||||
11
core/tests/manual/issue-2246/index.html
Normal file
11
core/tests/manual/issue-2246/index.html
Normal 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>
|
||||||
1
core/tests/manual/issue-2246/main.py
Normal file
1
core/tests/manual/issue-2246/main.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
print(input("What food would you like me to get from the shop? "))
|
||||||
BIN
core/tests/manual/issue-2302/assets/genuary25-18.m4a
Normal file
BIN
core/tests/manual/issue-2302/assets/genuary25-18.m4a
Normal file
Binary file not shown.
20
core/tests/manual/issue-2302/glue/multipyjs.py
Normal file
20
core/tests/manual/issue-2302/glue/multipyjs.py
Normal 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
|
||||||
Binary file not shown.
69
core/tests/manual/issue-2302/index.html
Normal file
69
core/tests/manual/issue-2302/index.html
Normal 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>
|
||||||
83
core/tests/manual/issue-2302/libfft.py
Normal file
83
core/tests/manual/issue-2302/libfft.py
Normal 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
|
||||||
189
core/tests/manual/issue-2302/libthree.py
Normal file
189
core/tests/manual/issue-2302/libthree.py
Normal 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"
|
||||||
285
core/tests/manual/issue-2302/main.py
Normal file
285
core/tests/manual/issue-2302/main.py
Normal 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...")
|
||||||
110
core/tests/manual/issue-2302/perlin_py.py
Normal file
110
core/tests/manual/issue-2302/perlin_py.py
Normal 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)
|
||||||
16
core/tests/manual/issue-2302/pyscript.toml
Normal file
16
core/tests/manual/issue-2302/pyscript.toml
Normal 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"
|
||||||
141
core/tests/manual/issue-2302/worker.py
Normal file
141
core/tests/manual/issue-2302/worker.py
Normal 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()
|
||||||
12
core/tests/manual/issue-2304/index.html
Normal file
12
core/tests/manual/issue-2304/index.html
Normal 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>
|
||||||
34
core/tests/manual/issue-2304/main.py
Normal file
34
core/tests/manual/issue-2304/main.py
Normal 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")
|
||||||
2
core/tests/manual/issue-2304/pyscript.toml
Normal file
2
core/tests/manual/issue-2304/pyscript.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
name = "PyGame Numpy Minimal Example Copy"
|
||||||
|
packages = [ "numpy", ]
|
||||||
21
core/tests/py_tests.main.spec.js
Normal file
21
core/tests/py_tests.main.spec.js
Normal 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.
|
||||||
|
});
|
||||||
@@ -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.
|
|
||||||
});
|
|
||||||
21
core/tests/py_tests.worker.spec.js
Normal file
21
core/tests/py_tests.worker.spec.js
Normal 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.
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ def test_current_target():
|
|||||||
"""
|
"""
|
||||||
expected = "py-0"
|
expected = "py-0"
|
||||||
if is_micropython:
|
if is_micropython:
|
||||||
if RUNNING_IN_WORKER:
|
expected = "mpy-w0-target" if RUNNING_IN_WORKER else "mpy-0"
|
||||||
expected = "mpy-w0-target"
|
|
||||||
else:
|
|
||||||
expected = "mpy-0"
|
|
||||||
elif RUNNING_IN_WORKER:
|
elif RUNNING_IN_WORKER:
|
||||||
expected = "py-w0-target"
|
expected = "py-w0-target"
|
||||||
assert current_target() == expected, f"Expected {expected} got {current_target()}"
|
assert current_target() == expected, f"Expected {expected} got {current_target()}"
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ async def test_image_display():
|
|||||||
"""
|
"""
|
||||||
Check an image is displayed correctly.
|
Check an image is displayed correctly.
|
||||||
"""
|
"""
|
||||||
mpl = await py_import("matplotlib")
|
_mpl = await py_import("matplotlib")
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
xpoints = [3, 6, 9]
|
xpoints = [3, 6, 9]
|
||||||
|
|||||||
360
core/tests/python/tests/test_events.py
Normal file
360
core/tests/python/tests/test_events.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
"""
|
||||||
|
Tests for the when function and Event class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import upytest
|
||||||
|
from pyscript import RUNNING_IN_WORKER, web, Event, when
|
||||||
|
|
||||||
|
|
||||||
|
def get_container():
|
||||||
|
return web.page.find("#test-element-container")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
container = get_container()
|
||||||
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
|
def teardown():
|
||||||
|
container = get_container()
|
||||||
|
container.innerHTML = ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_add_listener():
|
||||||
|
"""
|
||||||
|
Adding a listener to an event should add it to the list of listeners. It
|
||||||
|
should only be added once.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener = lambda x: x
|
||||||
|
event.add_listener(listener)
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert len(event._listeners) == 1 # Only one item added.
|
||||||
|
assert listener in event._listeners # The item is the expected listener.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_remove_listener():
|
||||||
|
"""
|
||||||
|
Removing a listener from an event should remove it from the list of
|
||||||
|
listeners.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener1 = lambda x: x
|
||||||
|
listener2 = lambda x: x
|
||||||
|
event.add_listener(listener1)
|
||||||
|
event.add_listener(listener2)
|
||||||
|
assert len(event._listeners) == 2 # Two listeners added.
|
||||||
|
assert listener1 in event._listeners # The first listener is in the list.
|
||||||
|
assert listener2 in event._listeners # The second listener is in the list.
|
||||||
|
event.remove_listener(listener1)
|
||||||
|
assert len(event._listeners) == 1 # Only one item remains.
|
||||||
|
assert listener2 in event._listeners # The second listener is in the list.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_remove_all_listeners():
|
||||||
|
"""
|
||||||
|
Removing all listeners from an event should clear the list of listeners.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
listener1 = lambda x: x
|
||||||
|
listener2 = lambda x: x
|
||||||
|
event.add_listener(listener1)
|
||||||
|
event.add_listener(listener2)
|
||||||
|
assert len(event._listeners) == 2 # Two listeners added.
|
||||||
|
event.remove_listener()
|
||||||
|
assert len(event._listeners) == 0 # No listeners remain.
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_trigger():
|
||||||
|
"""
|
||||||
|
Triggering an event should call all of the listeners with the provided
|
||||||
|
arguments.
|
||||||
|
"""
|
||||||
|
event = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def listener(x):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert x == "ok"
|
||||||
|
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert counter == 0 # The listener has not been triggered yet.
|
||||||
|
event.trigger("ok")
|
||||||
|
assert counter == 1 # The listener has been triggered with the expected result.
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_trigger_with_awaitable():
|
||||||
|
"""
|
||||||
|
Triggering an event with an awaitable listener should call the listener
|
||||||
|
with the provided arguments.
|
||||||
|
"""
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
event = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
async def listener(x):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert x == "ok"
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
event.add_listener(listener)
|
||||||
|
assert counter == 0 # The listener has not been triggered yet.
|
||||||
|
event.trigger("ok")
|
||||||
|
await call_flag.wait()
|
||||||
|
assert counter == 1 # The listener has been triggered with the expected result.
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_with_event():
|
||||||
|
"""
|
||||||
|
When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal called
|
||||||
|
called = evt
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called.target.id == "foo_id"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_without_event():
|
||||||
|
"""
|
||||||
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
def foo():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_with_event_as_async_handler():
|
||||||
|
"""
|
||||||
|
When the decorated function takes a single parameter,
|
||||||
|
it should be passed the event object. Async version.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
async def foo(evt):
|
||||||
|
nonlocal called
|
||||||
|
called = evt
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called.target.id == "foo_id"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_without_event_as_async_handler():
|
||||||
|
"""
|
||||||
|
When the decorated function takes no parameters (not including 'self'),
|
||||||
|
it should be called without the event object. Async version.
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
@web.when("click", selector="#foo_id")
|
||||||
|
async def foo():
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_two_when_decorators():
|
||||||
|
"""
|
||||||
|
When decorating a function twice, both should function
|
||||||
|
"""
|
||||||
|
btn = web.button("foo_button", id="foo_id")
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn)
|
||||||
|
|
||||||
|
called1 = False
|
||||||
|
called2 = False
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo1(evt):
|
||||||
|
nonlocal called1
|
||||||
|
called1 = True
|
||||||
|
call_flag1.set()
|
||||||
|
|
||||||
|
@when("click", selector="#foo_id")
|
||||||
|
def foo2(evt):
|
||||||
|
nonlocal called2
|
||||||
|
called2 = True
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
btn.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert called1
|
||||||
|
assert called2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_when_decorator_multiple_elements():
|
||||||
|
"""
|
||||||
|
The @when decorator's selector should successfully select multiple
|
||||||
|
DOM elements
|
||||||
|
"""
|
||||||
|
btn1 = web.button(
|
||||||
|
"foo_button1",
|
||||||
|
id="foo_id1",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
btn2 = web.button(
|
||||||
|
"foo_button2",
|
||||||
|
id="foo_id2",
|
||||||
|
classes=[
|
||||||
|
"foo_class",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
container = get_container()
|
||||||
|
container.append(btn1)
|
||||||
|
container.append(btn2)
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
call_flag1 = asyncio.Event()
|
||||||
|
call_flag2 = asyncio.Event()
|
||||||
|
|
||||||
|
@when("click", selector=".foo_class")
|
||||||
|
def foo(evt):
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
if evt.target.id == "foo_id1":
|
||||||
|
call_flag1.set()
|
||||||
|
else:
|
||||||
|
call_flag2.set()
|
||||||
|
|
||||||
|
assert counter == 0, counter
|
||||||
|
btn1.click()
|
||||||
|
await call_flag1.wait()
|
||||||
|
assert counter == 1, counter
|
||||||
|
btn2.click()
|
||||||
|
await call_flag2.wait()
|
||||||
|
assert counter == 2, counter
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Only works in Pyodide on main thread",
|
||||||
|
skip_when=upytest.is_micropython or RUNNING_IN_WORKER,
|
||||||
|
)
|
||||||
|
def test_when_decorator_invalid_selector():
|
||||||
|
"""
|
||||||
|
When the selector parameter of @when is invalid, it should raise an error.
|
||||||
|
"""
|
||||||
|
if upytest.is_micropython:
|
||||||
|
from jsffi import JsException
|
||||||
|
else:
|
||||||
|
from pyodide.ffi import JsException
|
||||||
|
|
||||||
|
with upytest.raises(JsException) as e:
|
||||||
|
|
||||||
|
@when("click", selector="#.bad")
|
||||||
|
def foo(evt): ...
|
||||||
|
|
||||||
|
assert "'#.bad' is not a valid selector" in str(e.exception), str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_decorates_an_event():
|
||||||
|
"""
|
||||||
|
When the @when decorator is used on a function to handle an Event instance,
|
||||||
|
the function should be called when the Event object is triggered.
|
||||||
|
"""
|
||||||
|
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
# When as a decorator.
|
||||||
|
@when(whenable)
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_when_called_with_an_event_and_handler():
|
||||||
|
"""
|
||||||
|
The when function should be able to be called with an Event object,
|
||||||
|
and a handler function.
|
||||||
|
"""
|
||||||
|
whenable = Event()
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def handler(result):
|
||||||
|
"""
|
||||||
|
A function that should be called when the whenable object is triggered.
|
||||||
|
|
||||||
|
The result generated by the whenable object should be passed to the
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
nonlocal counter
|
||||||
|
counter += 1
|
||||||
|
assert result == "ok"
|
||||||
|
|
||||||
|
# When as a function.
|
||||||
|
when(whenable, handler)
|
||||||
|
|
||||||
|
# The function should not be called until the whenable object is triggered.
|
||||||
|
assert counter == 0
|
||||||
|
# Trigger the whenable object.
|
||||||
|
whenable.trigger("ok")
|
||||||
|
# The function should have been called when the whenable object was
|
||||||
|
# triggered.
|
||||||
|
assert counter == 1
|
||||||
87
core/tests/python/tests/test_media.py
Normal file
87
core/tests/python/tests/test_media.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
""""
|
||||||
|
Tests for the PyScript media module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyscript import media
|
||||||
|
import upytest
|
||||||
|
|
||||||
|
from pyscript import media
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Uses Pyodide-specific to_js function in MicroPython",
|
||||||
|
skip_when=upytest.is_micropython,
|
||||||
|
)
|
||||||
|
async def test_device_enumeration():
|
||||||
|
"""Test enumerating media devices."""
|
||||||
|
devices = await media.list_devices()
|
||||||
|
assert isinstance(devices, list), "list_devices should return a list"
|
||||||
|
|
||||||
|
# If devices are found, verify they have the expected functionality
|
||||||
|
if devices:
|
||||||
|
device = devices[0]
|
||||||
|
|
||||||
|
# Test real device properties exist (but don't assert on their values)
|
||||||
|
# Browser security might restrict actual values until permissions are granted
|
||||||
|
assert hasattr(device, "id"), "Device should have id property"
|
||||||
|
assert hasattr(device, "kind"), "Device should have kind property"
|
||||||
|
assert device.kind in [
|
||||||
|
"videoinput",
|
||||||
|
"audioinput",
|
||||||
|
"audiooutput",
|
||||||
|
], f"Device should have a valid kind, got: {device.kind}"
|
||||||
|
|
||||||
|
# Verify dictionary access works with actual device
|
||||||
|
assert (
|
||||||
|
device["id"] == device.id
|
||||||
|
), "Dictionary access should match property access"
|
||||||
|
assert (
|
||||||
|
device["kind"] == device.kind
|
||||||
|
), "Dictionary access should match property access"
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
|
||||||
|
async def test_video_stream_acquisition():
|
||||||
|
"""Test video stream."""
|
||||||
|
try:
|
||||||
|
# Load a video stream
|
||||||
|
stream = await media.Device.load(video=True)
|
||||||
|
|
||||||
|
# Verify we get a real stream with expected properties
|
||||||
|
assert hasattr(stream, "active"), "Stream should have active property"
|
||||||
|
|
||||||
|
# Check for video tracks, but don't fail if permissions aren't granted
|
||||||
|
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
|
||||||
|
tracks = stream._dom_element.getVideoTracks()
|
||||||
|
if tracks.length > 0:
|
||||||
|
assert True, "Video stream has video tracks"
|
||||||
|
except Exception as e:
|
||||||
|
# If the browser blocks access, the test should still pass
|
||||||
|
# This is because we're testing the API works, not that permissions are granted
|
||||||
|
assert (
|
||||||
|
True
|
||||||
|
), f"Stream acquisition attempted but may require permissions: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@upytest.skip("Waiting on a bug-fix in MicroPython, for this test to work.", skip_when=upytest.is_micropython)
|
||||||
|
async def test_custom_video_constraints():
|
||||||
|
"""Test loading video with custom constraints."""
|
||||||
|
try:
|
||||||
|
# Define custom constraints
|
||||||
|
constraints = {"width": 640, "height": 480}
|
||||||
|
|
||||||
|
# Load stream with custom constraints
|
||||||
|
stream = await media.Device.load(video=constraints)
|
||||||
|
|
||||||
|
# Basic stream property check
|
||||||
|
assert hasattr(stream, "active"), "Stream should have active property"
|
||||||
|
|
||||||
|
# Check for tracks only if we have access
|
||||||
|
if stream._dom_element and hasattr(stream._dom_element, "getVideoTracks"):
|
||||||
|
tracks = stream._dom_element.getVideoTracks()
|
||||||
|
if tracks.length > 0 and hasattr(tracks[0], "getSettings"):
|
||||||
|
# Settings verification is optional - browsers may handle constraints differently
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
# If the browser blocks access, test that the API structure works
|
||||||
|
assert True, f"Custom constraint test attempted: {str(e)}"
|
||||||
@@ -65,7 +65,6 @@ async def test_storage_types():
|
|||||||
assert test_store["string"] == "hello"
|
assert test_store["string"] == "hello"
|
||||||
assert isinstance(test_store["string"], str)
|
assert isinstance(test_store["string"], str)
|
||||||
assert test_store["none"] is None
|
assert test_store["none"] is None
|
||||||
assert isinstance(test_store["none"], type(None))
|
|
||||||
assert test_store["list"] == [1, 2, 3]
|
assert test_store["list"] == [1, 2, 3]
|
||||||
assert isinstance(test_store["list"], list)
|
assert isinstance(test_store["list"], list)
|
||||||
assert test_store["dict"] == {"a": 1, "b": 2}
|
assert test_store["dict"] == {"a": 1, "b": 2}
|
||||||
|
|||||||
48
core/tests/python/tests/test_util.py
Normal file
48
core/tests/python/tests/test_util.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import upytest
|
||||||
|
import js
|
||||||
|
from pyscript import util
|
||||||
|
|
||||||
|
|
||||||
|
def test_as_bytearray():
|
||||||
|
"""
|
||||||
|
Test the as_bytearray function correctly converts a JavaScript ArrayBuffer
|
||||||
|
to a Python bytearray.
|
||||||
|
"""
|
||||||
|
msg = b"Hello, world!"
|
||||||
|
buffer = js.ArrayBuffer.new(len(msg))
|
||||||
|
ui8a = js.Uint8Array.new(buffer)
|
||||||
|
for b in msg:
|
||||||
|
ui8a[i] = b
|
||||||
|
ba = util.as_bytearray(buffer)
|
||||||
|
assert isinstance(ba, bytearray)
|
||||||
|
assert ba == msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_supported():
|
||||||
|
"""
|
||||||
|
Test the NotSupported class raises an exception when trying to access
|
||||||
|
attributes or call the object.
|
||||||
|
"""
|
||||||
|
ns = util.NotSupported("test", "This is not supported.")
|
||||||
|
with upytest.raises(AttributeError) as e:
|
||||||
|
ns.test
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
with upytest.raises(AttributeError) as e:
|
||||||
|
ns.test = 1
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
with upytest.raises(TypeError) as e:
|
||||||
|
ns()
|
||||||
|
assert str(e.exception) == "This is not supported.", str(e.exception)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_awaitable():
|
||||||
|
"""
|
||||||
|
Test the is_awaitable function correctly identifies an asynchronous
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def async_func():
|
||||||
|
yield
|
||||||
|
|
||||||
|
assert util.is_awaitable(async_func)
|
||||||
|
assert not util.is_awaitable(lambda: None)
|
||||||
@@ -164,6 +164,57 @@ class TestElement:
|
|||||||
await call_flag.wait()
|
await call_flag.wait()
|
||||||
assert called
|
assert called
|
||||||
|
|
||||||
|
async def test_when_decorator_on_event(self):
|
||||||
|
called = False
|
||||||
|
|
||||||
|
another_button = web.page.find("#another-test-button")[0]
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
assert another_button.on_click is not None
|
||||||
|
assert isinstance(another_button.on_click, web.Event)
|
||||||
|
|
||||||
|
@when(another_button.on_click)
|
||||||
|
def on_click(event):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert not called
|
||||||
|
another_button._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called
|
||||||
|
|
||||||
|
async def test_on_event_with_default_handler(self):
|
||||||
|
called = False
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
nonlocal called
|
||||||
|
called = True
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
b = web.button("Click me", on_click=handler)
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert not called
|
||||||
|
b._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert called
|
||||||
|
|
||||||
|
def test_on_event_must_be_actual_event(self):
|
||||||
|
"""
|
||||||
|
Any on_FOO event must relate to an actual FOO event on the element.
|
||||||
|
"""
|
||||||
|
b = web.button("Click me")
|
||||||
|
# Non-existent event causes a ValueError
|
||||||
|
with upytest.raises(ValueError):
|
||||||
|
b.on_chicken
|
||||||
|
# Buttons have an underlying "click" event so this will work.
|
||||||
|
assert b.on_click
|
||||||
|
|
||||||
def test_inner_html_attribute(self):
|
def test_inner_html_attribute(self):
|
||||||
# GIVEN an existing element on the page with a known empty text content
|
# GIVEN an existing element on the page with a known empty text content
|
||||||
div = web.page.find("#element_attribute_tests")[0]
|
div = web.page.find("#element_attribute_tests")[0]
|
||||||
@@ -197,7 +248,7 @@ class TestCollection:
|
|||||||
|
|
||||||
def test_iter_eq_children(self):
|
def test_iter_eq_children(self):
|
||||||
elements = web.page.find(".multi-elems")
|
elements = web.page.find(".multi-elems")
|
||||||
assert [el for el in elements] == [el for el in elements.elements]
|
assert list(elements) == list(elements.elements)
|
||||||
assert len(elements) == 3
|
assert len(elements) == 3
|
||||||
|
|
||||||
def test_slices(self):
|
def test_slices(self):
|
||||||
@@ -227,11 +278,15 @@ class TestCollection:
|
|||||||
assert el.style["background-color"] != "red"
|
assert el.style["background-color"] != "red"
|
||||||
assert elements[i].style["background-color"] != "red"
|
assert elements[i].style["background-color"] != "red"
|
||||||
|
|
||||||
|
@upytest.skip(
|
||||||
|
"Flakey in Pyodide on Worker",
|
||||||
|
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
||||||
|
)
|
||||||
async def test_when_decorator(self):
|
async def test_when_decorator(self):
|
||||||
called = False
|
called = False
|
||||||
call_flag = asyncio.Event()
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
buttons_collection = web.page.find("button")
|
buttons_collection = web.page["button"]
|
||||||
|
|
||||||
@when("click", buttons_collection)
|
@when("click", buttons_collection)
|
||||||
def on_click(event):
|
def on_click(event):
|
||||||
@@ -249,6 +304,28 @@ class TestCollection:
|
|||||||
called = False
|
called = False
|
||||||
call_flag.clear()
|
call_flag.clear()
|
||||||
|
|
||||||
|
async def test_when_decorator_on_event(self):
|
||||||
|
call_counter = 0
|
||||||
|
call_flag = asyncio.Event()
|
||||||
|
|
||||||
|
buttons_collection = web.page.find("button")
|
||||||
|
number_of_clicks = len(buttons_collection)
|
||||||
|
|
||||||
|
@when(buttons_collection.on_click)
|
||||||
|
def on_click(event):
|
||||||
|
nonlocal call_counter
|
||||||
|
call_counter += 1
|
||||||
|
if call_counter == number_of_clicks:
|
||||||
|
call_flag.set()
|
||||||
|
|
||||||
|
# Now let's simulate a click on the button (using the low level JS API)
|
||||||
|
# so we don't risk dom getting in the way
|
||||||
|
assert call_counter == 0
|
||||||
|
for button in buttons_collection:
|
||||||
|
button._dom_element.click()
|
||||||
|
await call_flag.wait()
|
||||||
|
assert call_counter == number_of_clicks
|
||||||
|
|
||||||
|
|
||||||
class TestCreation:
|
class TestCreation:
|
||||||
|
|
||||||
@@ -350,18 +427,18 @@ class TestInput:
|
|||||||
class TestSelect:
|
class TestSelect:
|
||||||
|
|
||||||
def test_select_options_iter(self):
|
def test_select_options_iter(self):
|
||||||
select = web.page.find(f"#test_select_element_w_options")[0]
|
select = web.page.find("#test_select_element_w_options")[0]
|
||||||
|
|
||||||
for i, option in enumerate(select.options, 1):
|
for i, option in enumerate(select.options, 1):
|
||||||
assert option.value == f"{i}"
|
assert option.value == f"{i}"
|
||||||
assert option.innerHTML == f"Option {i}"
|
assert option.innerHTML == f"Option {i}"
|
||||||
|
|
||||||
def test_select_options_len(self):
|
def test_select_options_len(self):
|
||||||
select = web.page.find(f"#test_select_element_w_options")[0]
|
select = web.page.find("#test_select_element_w_options")[0]
|
||||||
assert len(select.options) == 2
|
assert len(select.options) == 2
|
||||||
|
|
||||||
def test_select_options_clear(self):
|
def test_select_options_clear(self):
|
||||||
select = web.page.find(f"#test_select_element_to_clear")[0]
|
select = web.page.find("#test_select_element_to_clear")[0]
|
||||||
assert len(select.options) == 3
|
assert len(select.options) == 3
|
||||||
|
|
||||||
select.options.clear()
|
select.options.clear()
|
||||||
@@ -370,7 +447,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_element_add(self):
|
def test_select_element_add(self):
|
||||||
# GIVEN the existing select element with no options
|
# GIVEN the existing select element with no options
|
||||||
select = web.page.find(f"#test_select_element")[0]
|
select = web.page.find("#test_select_element")[0]
|
||||||
|
|
||||||
# EXPECT the select element to have no options
|
# EXPECT the select element to have no options
|
||||||
assert len(select.options) == 0
|
assert len(select.options) == 0
|
||||||
@@ -421,20 +498,14 @@ class TestSelect:
|
|||||||
# EXPECT the middle option to have the value and html we passed in
|
# EXPECT the middle option to have the value and html we passed in
|
||||||
assert select.options[0].value == "1"
|
assert select.options[0].value == "1"
|
||||||
assert select.options[0].innerHTML == "Option 1"
|
assert select.options[0].innerHTML == "Option 1"
|
||||||
assert (
|
assert select.options[0].selected == select.options[0]._dom_element.selected
|
||||||
select.options[0].selected
|
assert select.options[0].selected is False
|
||||||
== select.options[0]._dom_element.selected
|
|
||||||
== False
|
|
||||||
)
|
|
||||||
assert select.options[1].value == "2"
|
assert select.options[1].value == "2"
|
||||||
assert select.options[1].innerHTML == "Option 2"
|
assert select.options[1].innerHTML == "Option 2"
|
||||||
assert select.options[2].value == "3"
|
assert select.options[2].value == "3"
|
||||||
assert select.options[2].innerHTML == "Option 3"
|
assert select.options[2].innerHTML == "Option 3"
|
||||||
assert (
|
assert select.options[2].selected == select.options[2]._dom_element.selected
|
||||||
select.options[2].selected
|
assert select.options[2].selected is True
|
||||||
== select.options[2]._dom_element.selected
|
|
||||||
== True
|
|
||||||
)
|
|
||||||
assert select.options[3].value == ""
|
assert select.options[3].value == ""
|
||||||
assert select.options[3].innerHTML == ""
|
assert select.options[3].innerHTML == ""
|
||||||
|
|
||||||
@@ -461,7 +532,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_options_remove(self):
|
def test_select_options_remove(self):
|
||||||
# GIVEN the existing select element with 3 options
|
# GIVEN the existing select element with 3 options
|
||||||
select = web.page.find(f"#test_select_element_to_remove")[0]
|
select = web.page.find("#test_select_element_to_remove")[0]
|
||||||
|
|
||||||
# EXPECT the select element to have 3 options
|
# EXPECT the select element to have 3 options
|
||||||
assert len(select.options) == 4
|
assert len(select.options) == 4
|
||||||
@@ -483,7 +554,7 @@ class TestSelect:
|
|||||||
|
|
||||||
def test_select_get_selected_option(self):
|
def test_select_get_selected_option(self):
|
||||||
# GIVEN the existing select element with one selected option
|
# GIVEN the existing select element with one selected option
|
||||||
select = web.page.find(f"#test_select_element_w_options")[0]
|
select = web.page.find("#test_select_element_w_options")[0]
|
||||||
|
|
||||||
# WHEN we get the selected option
|
# WHEN we get the selected option
|
||||||
selected_option = select.options.selected
|
selected_option = select.options.selected
|
||||||
@@ -491,7 +562,8 @@ class TestSelect:
|
|||||||
# EXPECT the selected option to be correct
|
# EXPECT the selected option to be correct
|
||||||
assert selected_option.value == "2"
|
assert selected_option.value == "2"
|
||||||
assert selected_option.innerHTML == "Option 2"
|
assert selected_option.innerHTML == "Option 2"
|
||||||
assert selected_option.selected == selected_option._dom_element.selected == True
|
assert selected_option.selected == selected_option._dom_element.selected
|
||||||
|
assert selected_option.selected is True
|
||||||
|
|
||||||
|
|
||||||
class TestElements:
|
class TestElements:
|
||||||
@@ -548,7 +620,8 @@ class TestElements:
|
|||||||
el = klass(*args, **kwargs)
|
el = klass(*args, **kwargs)
|
||||||
container.append(el)
|
container.append(el)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
assert False, f"Failed to create element {el_type}: {e}"
|
msg = f"Failed to create element {el_type}: {e}"
|
||||||
|
raise AssertionError(msg)
|
||||||
|
|
||||||
# Let's keep the tag in 2 variables, one for the selector and another to
|
# Let's keep the tag in 2 variables, one for the selector and another to
|
||||||
# check the return tag from the selector
|
# check the return tag from the selector
|
||||||
@@ -759,14 +832,13 @@ class TestElements:
|
|||||||
self._create_el_and_basic_asserts("iframe", properties=properties)
|
self._create_el_and_basic_asserts("iframe", properties=properties)
|
||||||
|
|
||||||
@upytest.skip(
|
@upytest.skip(
|
||||||
"Flakey on Pyodide in worker.",
|
"Flakey in worker.",
|
||||||
skip_when=RUNNING_IN_WORKER and not upytest.is_micropython,
|
skip_when=RUNNING_IN_WORKER,
|
||||||
)
|
)
|
||||||
async def test_img(self):
|
async def test_img(self):
|
||||||
"""
|
"""
|
||||||
This test contains a bespoke version of the _create_el_and_basic_asserts
|
This test, thanks to downloading an image from the internet, is flakey
|
||||||
function so we can await asyncio.sleep if in a worker, so the DOM state
|
when run in a worker. It's skipped when running in a worker.
|
||||||
is in sync with the worker before property based asserts can happen.
|
|
||||||
"""
|
"""
|
||||||
properties = {
|
properties = {
|
||||||
"src": "https://picsum.photos/600/400",
|
"src": "https://picsum.photos/600/400",
|
||||||
@@ -774,39 +846,7 @@ class TestElements:
|
|||||||
"width": 250,
|
"width": 250,
|
||||||
"height": 200,
|
"height": 200,
|
||||||
}
|
}
|
||||||
|
self._create_el_and_basic_asserts("img", properties=properties)
|
||||||
def parse_value(v):
|
|
||||||
if isinstance(v, bool):
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
return f"{v}"
|
|
||||||
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
if properties:
|
|
||||||
kwargs = {k: parse_value(v) for k, v in properties.items()}
|
|
||||||
|
|
||||||
# Let's make sure the target div to contain the element is empty.
|
|
||||||
container = web.page["#test-element-container"][0]
|
|
||||||
container.innerHTML = ""
|
|
||||||
assert container.innerHTML == "", container.innerHTML
|
|
||||||
# Let's create the element
|
|
||||||
try:
|
|
||||||
klass = getattr(web, "img")
|
|
||||||
el = klass(*args, **kwargs)
|
|
||||||
container.append(el)
|
|
||||||
except Exception as e:
|
|
||||||
assert False, f"Failed to create element img: {e}"
|
|
||||||
|
|
||||||
if RUNNING_IN_WORKER:
|
|
||||||
# Needed to sync the DOM with the worker.
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
# Check the img element was created correctly and all its properties
|
|
||||||
# were set correctly.
|
|
||||||
for k, v in properties.items():
|
|
||||||
assert v == getattr(el, k), f"{k} should be {v} but is {getattr(el, k)}"
|
|
||||||
|
|
||||||
def test_input(self):
|
def test_input(self):
|
||||||
# TODO: we need multiple input tests
|
# TODO: we need multiple input tests
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user