mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-20 10:47:35 -05:00
Compare commits
55 Commits
reverted-4
...
event-refi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f6cfbd28 | ||
|
|
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 | ||
|
|
6b1330d28a | ||
|
|
5d751493f6 | ||
|
|
c3517f7973 | ||
|
|
b1c33b7f79 | ||
|
|
722abda895 | ||
|
|
8061bc0143 | ||
|
|
993e812e7b | ||
|
|
a6b6dd8479 | ||
|
|
febbb031ac | ||
|
|
f827efe2fc | ||
|
|
8c6bfecbff | ||
|
|
f562860e49 | ||
|
|
62c78b0f42 | ||
|
|
4fe6ad80af | ||
|
|
89a0a556f3 | ||
|
|
af06bc4826 | ||
|
|
6fab9a1c26 | ||
|
|
386966ebd2 |
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -4,11 +4,9 @@
|
||||
|
||||
## Changes
|
||||
|
||||
<!-- List the changes done to fix a bug or introduce a new feature.Please note both user-facing changes and changes to internal API's here -->
|
||||
<!-- List the technical changes done to fix a bug or introduce a new feature. -->
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Note: Only user-facing changes require a changelog entry. Internal-only API changes do not require a changelog entry. Changes in documentation do not require a changelog entry. -->
|
||||
|
||||
- [ ] All tests pass locally
|
||||
- [ ] I have created / updated documentation for this (if applicable)
|
||||
- [ ] I have checked `make build` works locally.
|
||||
- [ ] I have created / updated documentation for this change (if applicable).
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -11,7 +11,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest-4core
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/publish-release.yml
vendored
2
.github/workflows/publish-release.yml
vendored
@@ -10,7 +10,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest-4core
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/publish-snapshot.yml
vendored
2
.github/workflows/publish-snapshot.yml
vendored
@@ -14,7 +14,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
publish-snapshot:
|
||||
runs-on: ubuntu-latest-4core
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
2
.github/workflows/publish-unstable.yml
vendored
2
.github/workflows/publish-unstable.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish-unstable:
|
||||
runs-on: ubuntu-latest-4core
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
|
||||
jobs:
|
||||
BuildAndTest:
|
||||
runs-on: ubuntu-latest-4core
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MINICONDA_PYTHON_VERSION: py38
|
||||
MINICONDA_VERSION: 4.11.0
|
||||
|
||||
@@ -4,10 +4,10 @@ ci:
|
||||
#skip: [eslint]
|
||||
autoupdate_schedule: monthly
|
||||
|
||||
default_stages: [commit]
|
||||
default_stages: [pre-commit]
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: check-builtin-literals
|
||||
- id: check-case-conflict
|
||||
@@ -25,29 +25,29 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.8.0
|
||||
rev: 25.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: core/src/stdlib/pyscript/__init__\.py
|
||||
exclude: core/tests
|
||||
args: ["-l", "88", "--skip-string-normalization"]
|
||||
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.3.0
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell # See 'pyproject.toml' for args
|
||||
exclude: \.js\.map$
|
||||
exclude: fs\.py|\.js\.map$
|
||||
additional_dependencies:
|
||||
- tomli
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
hooks:
|
||||
- id: ruff
|
||||
exclude: core/tests
|
||||
|
||||
- repo: https://github.com/hoodmane/pyscript-prettier-precommit
|
||||
rev: "v3.0.0-alpha.6"
|
||||
hooks:
|
||||
- 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"]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
args: [--profile, black]
|
||||
|
||||
181
CONTRIBUTING.md
181
CONTRIBUTING.md
@@ -1,181 +1,4 @@
|
||||
# Contributing to PyScript
|
||||
|
||||
Thank you for wanting to contribute to the PyScript project!
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Contributing to PyScript](#contributing-to-pyscript)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Contributing](#contributing)
|
||||
- [Reporting bugs](#reporting-bugs)
|
||||
- [Creating useful issues](#creating-useful-issues)
|
||||
- [Reporting security issues](#reporting-security-issues)
|
||||
- [Asking questions](#asking-questions)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Developing](#developing)
|
||||
- [Rebasing changes](#rebasing-changes)
|
||||
- [Building the docs](#building-the-docs)
|
||||
- [Places to start](#places-to-start)
|
||||
- [Setting up your local environment and developing](#setting-up-your-local-environment-and-developing)
|
||||
- [Submitting a change](#submitting-a-change)
|
||||
- [License terms for contributions](#license-terms-for-contributions)
|
||||
- [Becoming a maintainer](#becoming-a-maintainer)
|
||||
- [Trademarks](#trademarks)
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
|
||||
|
||||
# Contributing
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
|
||||
|
||||
## Creating useful issues
|
||||
|
||||
- Use a clear and descriptive title.
|
||||
- Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
|
||||
- Describe the behavior you observed, and the behavior you had expected.
|
||||
- Include screenshots if they help make the issue clear.
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
|
||||
|
||||
## Asking questions
|
||||
|
||||
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
|
||||
|
||||
## Places to start
|
||||
|
||||
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions:
|
||||
|
||||
- **Read over the existing documentation.** Are there things missing, or could they be clearer? Make some changes/additions to those documents.
|
||||
- **Review the open issues.** Are they clear? Can you reproduce them? You can add comments, clarifications, or additions to those issues. If you think you have an idea of how to address the issue, submit a fix!
|
||||
- **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
|
||||
- **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
|
||||
|
||||
## Setting up your local environment and developing
|
||||
|
||||
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/contributing/#set-up-your-development-environment) will help you get started.
|
||||
|
||||
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
|
||||
|
||||
## License terms for contributions
|
||||
|
||||
This Project welcomes contributions, suggestions, and feedback. All contributions, suggestions, and feedback you submitted are accepted under the [Apache 2.0](./LICENSE) license. You represent that if you do not own copyright in the code that you have the authority to submit it under the [Apache 2.0](./LICENSE) license. All feedback, suggestions, or contributions are not confidential.
|
||||
|
||||
## Becoming a maintainer
|
||||
|
||||
Contributors are invited to be maintainers of the project by demonstrating good decision making in their contributions, a commitment to the goals of the project, and consistent adherence to the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md). New maintainers are invited by a 3/4 vote of the existing maintainers.
|
||||
|
||||
## Trademarks
|
||||
|
||||
The Project abides by the Organization's [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md).
|
||||
|
||||
---
|
||||
|
||||
Part of MVG-0.1-beta.
|
||||
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
|
||||
# Quick guide to pytest
|
||||
|
||||
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
|
||||
|
||||
- To run all tests in the current directory and subdirectories: pytest
|
||||
|
||||
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
|
||||
|
||||
- -s: disables output capturing
|
||||
|
||||
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
|
||||
|
||||
- -v: verbose mode
|
||||
|
||||
- -x: stop the execution as soon as one test fails
|
||||
|
||||
- -k foo: run only the tests whose full name contains foo
|
||||
|
||||
- -k 'foo and bar'
|
||||
|
||||
- -k 'foo and not bar'
|
||||
|
||||
## Running integration tests under pytest
|
||||
|
||||
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
|
||||
|
||||
### To run all the integration tests, single or multi core
|
||||
|
||||
$ pytest -xv
|
||||
...
|
||||
|
||||
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
|
||||
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
|
||||
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
|
||||
...
|
||||
|
||||
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
|
||||
|
||||
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
|
||||
|
||||
$ pytest -n 4
|
||||
|
||||
### To run a single test, headless
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s
|
||||
...
|
||||
[ 0.00 page.goto ] pyscript_hello.html
|
||||
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
|
||||
...
|
||||
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
|
||||
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
|
||||
...
|
||||
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
|
||||
[ 3.60 console.log ] hello pyscript
|
||||
|
||||
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
|
||||
|
||||
### To run a single test, headed
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
|
||||
...
|
||||
|
||||
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
|
||||
|
||||
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
|
||||
...
|
||||
|
||||
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
|
||||
|
||||
Finally:
|
||||
|
||||
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
|
||||
...
|
||||
|
||||
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
|
||||
|
||||
### To run only main thread or worker tests
|
||||
|
||||
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
|
||||
|
||||
$ pytest -m main # run only the tests in the main thread
|
||||
$ pytest -m worker # ron only the tests in the web worker
|
||||
|
||||
## Fake server, HTTP cache
|
||||
|
||||
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
|
||||
|
||||
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
|
||||
|
||||
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
|
||||
|
||||
If you want to clear the cache, you can use the special option --clear-http-cache:
|
||||
|
||||
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.
|
||||
Please see our guide to contributing to PyScript
|
||||
[in our documentation](https://docs.pyscript.net/latest/contributing/).
|
||||
|
||||
18
Makefile
18
Makefile
@@ -9,6 +9,7 @@ PY_OK := $(shell python3 -c "print(int($(PY3_VER) >= $(MIN_PY3_VER)))")
|
||||
all:
|
||||
@echo "\nThere is no default Makefile target right now. Try:\n"
|
||||
@echo "make setup - check your environment and install the dependencies."
|
||||
@echo "make update - update dependencies."
|
||||
@echo "make clean - clean up auto-generated assets."
|
||||
@echo "make build - build PyScript."
|
||||
@echo "make precommit-check - run the precommit checks (run eslint)."
|
||||
@@ -39,9 +40,9 @@ check-python:
|
||||
|
||||
# Check the environment, install the dependencies.
|
||||
setup: check-node check-npm check-python
|
||||
cd core && npm install && cd ..
|
||||
ifeq ($(VIRTUAL_ENV),)
|
||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv is not activated.\033[0m"
|
||||
cd core && npm ci && cd ..
|
||||
ifeq (,$(VIRTUAL_ENV)$(CONDA_PREFIX))
|
||||
echo "\n\n\033[0;31mCannot install Python dependencies. Your virtualenv or conda env is not activated.\033[0m"
|
||||
false
|
||||
else
|
||||
python -m pip install -r requirements.txt
|
||||
@@ -54,9 +55,13 @@ clean:
|
||||
rm -rf .pytest_cache .coverage coverage.xml
|
||||
|
||||
# Build PyScript.
|
||||
build:
|
||||
build: precommit-check
|
||||
cd core && npx playwright install chromium && npm run build
|
||||
|
||||
# Update the dependencies.
|
||||
update:
|
||||
python -m pip install -r requirements.txt --upgrade
|
||||
|
||||
# Run the precommit checks (run eslint).
|
||||
precommit-check:
|
||||
pre-commit run --all-files
|
||||
@@ -65,6 +70,10 @@ precommit-check:
|
||||
test:
|
||||
cd core && npm run test:integration
|
||||
|
||||
# Serve the repository with the correct headers.
|
||||
serve:
|
||||
npx mini-coi .
|
||||
|
||||
# Format the code.
|
||||
fmt: fmt-py
|
||||
@echo "Format completed"
|
||||
@@ -76,7 +85,6 @@ fmt-check: fmt-py-check
|
||||
# Format Python code.
|
||||
fmt-py:
|
||||
black -l 88 --skip-string-normalization .
|
||||
isort --profile black .
|
||||
|
||||
# Check the format of Python code.
|
||||
fmt-py-check:
|
||||
|
||||
139
README.md
139
README.md
@@ -1,97 +1,94 @@
|
||||
# PyScript
|
||||
|
||||
## What is PyScript
|
||||
## PyScript is an open source platform for Python in the browser.
|
||||
|
||||
### Summary
|
||||
Using PyScript is as simple as:
|
||||
|
||||
PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [MicroPython](https://micropython.org/) and [WASM](https://webassembly.org/), and modern web technologies.
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>PyScript!</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/snapshots/2024.9.2/core.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://pyscript.net/snapshots/2024.9.2/core.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Use MicroPython to evaluate some Python -->
|
||||
<script type="mpy" terminal>
|
||||
print("Hello, world!")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
To get started see the [Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||
PyScript enables the creation of rich Python applications in the browser using
|
||||
[Pyodide](https://pyodide.org/en/stable/) (a version of
|
||||
[CPython](https://python.org/)), [MicroPython](https://micropython.org/),
|
||||
[WASM](https://webassembly.org/), and modern web technologies. It means Python
|
||||
now runs anywhere a browser runs: desktop, laptop, mobile, tablet, or any other
|
||||
browser enabled device.
|
||||
|
||||
For examples see [here](https://pyscript.com/@examples).
|
||||
To start building, read the
|
||||
[Beginning PyScript tutorial](https://docs.pyscript.net/latest/beginning-pyscript/).
|
||||
|
||||
For example applications, see [here](https://pyscript.com/@examples).
|
||||
|
||||
Other useful resources:
|
||||
|
||||
- Our [Home Page](https://pyscript.net/) as an open source project.
|
||||
- The [official technical docs](https://docs.pyscript.net/).
|
||||
- Our current [Home Page](https://pyscript.net/) on the web.
|
||||
- A free-to-use [online editor](https://pyscript.com/) for trying PyScript.
|
||||
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in touch .
|
||||
- A [YouTube channel](https://www.youtube.com/@PyScriptTV) with helpful videos
|
||||
and community content.
|
||||
- A free-to-use [online IDE](https://pyscript.com/) for trying PyScript.
|
||||
- Our community [Discord Channel](https://discord.gg/BYB2kvyFwm), to keep in
|
||||
touch .
|
||||
|
||||
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom, where we can talk about PyScript development in the open. Most of the maintainers regularly participate in the call, and everybody is welcome to join.
|
||||
Every Tuesday at 15:30 UTC there is the _PyScript Community Call_ on zoom,
|
||||
where we can talk about PyScript development in the open. Most of the
|
||||
maintainers regularly participate in the call, and everybody is welcome to
|
||||
join. This meeting is recorded and uploaded to our YouTube channel.
|
||||
|
||||
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: this is a call in which everybody is encouraged to show what they did with PyScript.
|
||||
Every other Thursday at 16:00 UTC there is the _PyScript FUN_ call: the focus
|
||||
of this call is to share fun projects, goofy hacks or clever uses of PyScript.
|
||||
It's a supportive, energetic and entertaining meeting. This meeting is also
|
||||
recorded and uploaded to our YouTube channel.
|
||||
|
||||
For more details on how to join the calls and up to date schedule, consult the official calendar:
|
||||
For more details on how to join the calls and up to date schedule, consult the
|
||||
official calendar:
|
||||
|
||||
- [Google calendar](https://calendar.google.com/calendar/u/0/embed?src=d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0@group.calendar.google.com&ctz=UTC) in UTC time;
|
||||
- [iCal format](https://calendar.google.com/calendar/ical/d3afdd81f9c132a8c8f3290f5cc5966adebdf61017fca784eef0f6be9fd519e0%40group.calendar.google.com/public/basic.ics).
|
||||
|
||||
### Longer Version
|
||||
## Contribute
|
||||
|
||||
PyScript is a meta project that aims to combine multiple open technologies into a framework that allows users to create sophisticated browser applications with Python. It integrates seamlessly with the way the DOM works in the browser and allows users to add Python logic in a way that feels natural both to web and Python developers.
|
||||
For technical details of the code, please see the [README](core/README.md) in
|
||||
the `core` directory.
|
||||
|
||||
## Try PyScript
|
||||
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/)
|
||||
to learn about our development process, reporting bugs and improvements,
|
||||
creating issues and asking questions.
|
||||
|
||||
To try PyScript, import the appropriate pyscript files into the `<head>` tag of your html page:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://pyscript.net/releases/2024.8.2/core.css"
|
||||
/>
|
||||
<script
|
||||
type="module"
|
||||
src="https://pyscript.net/releases/2024.8.2/core.js"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" terminal>
|
||||
from pyscript import display
|
||||
display("Hello World!") # this goes to the DOM
|
||||
print("Hello terminal") # this goes to the terminal
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
You can then use PyScript components in your html page. PyScript currently offers various ways of running Python code:
|
||||
|
||||
- `<script type="py">`: can be used to define python code that is executable within the web page.
|
||||
- `<script type="py" src="hello.py">`: same as above, but the python source is fetched from the given URL.
|
||||
- `<script type="py" terminal>`: same as above, but also creates a terminal where to display stdout and stderr (e.g., the output of `print()`); `input()` does not work.
|
||||
- `<script type="py" terminal worker>`: run Python inside a web worker: the terminal is fully functional and `input()` works.
|
||||
- `<py-script>`: same as `<script type="py">`, but it is not recommended because if the code contains HTML tags, they could be parsed wrongly.
|
||||
- `<script type="mpy">`: same as above but use MicroPython instead of Python.
|
||||
|
||||
Check out the [official docs](https://docs.pyscript.net/) for more detailed documentation.
|
||||
|
||||
## How to Contribute
|
||||
|
||||
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
|
||||
|
||||
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
|
||||
|
||||
For technical details of the code, please see the [README](pyscript.core/README) in `pyscript.core`.
|
||||
Check out the [development process](https://docs.pyscript.net/latest/developers/)
|
||||
documentation for more information on how to setup your development environment.
|
||||
|
||||
## Governance
|
||||
|
||||
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
|
||||
The [PyScript organization governance](https://github.com/pyscript/governance)
|
||||
is documented in a separate repository.
|
||||
|
||||
## Release
|
||||
## Supporters
|
||||
|
||||
To cut a new release of PyScript simply
|
||||
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||
remembering to write a comprehensive changelog. A [GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||
will kick in and ensure the release is described and deployed to a URL with the
|
||||
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||
our [CalVer](https://calver.org/) versioning scheme).
|
||||
PyScript is an independent open source project.
|
||||
|
||||
Then, the following three separate repositories need updating:
|
||||
|
||||
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||
file in the root of the directory and then `node version-update.js`.
|
||||
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||
referenced in `index.html` is the latest version.
|
||||
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||
their project templates.
|
||||
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/)!
|
||||
|
||||
193
core/README.md
193
core/README.md
@@ -1,83 +1,168 @@
|
||||
# @pyscript/core
|
||||
|
||||
We have moved and renamed previous _core_ module as [polyscript](https://github.com/pyscript/polyscript/#readme), which is the base module used in here to build up _PyScript Next_, now hosted in this folder.
|
||||
PyScript brings two Python interpreters to the browser:
|
||||
|
||||
## Documentation
|
||||
- [MicroPython](https://micropython.org/) - a lean and efficient implementation
|
||||
of the Python 3 programming language that includes a small subset of the
|
||||
Python standard library and is optimised to run on microcontrollers and in
|
||||
constrained environments (like the browser).
|
||||
- [Pyodide](https://pyodide.org)) - a port of all CPython to WebAssembly.
|
||||
|
||||
Please read [core documentation](https://docs.pyscript.net/) to know more about this project.
|
||||
These interpreters are compiled to [WebAssembly](https://webassembly.org/)
|
||||
(shortened to WASM). The browser provides a secure WASM computing sandbox. Both
|
||||
interpreters are compiled to web assembly with
|
||||
[Emscripten](https://emscripten.org/). PyScript core maintainers work closely
|
||||
with the core maintainers of both MicroPython and Pyodide (and CPython). We
|
||||
work hard to ensure PyScript works efficiently in browsers on all platforms:
|
||||
desktop, mobile, or elsewhere.
|
||||
|
||||
## Development
|
||||
Our technical documentation for using this project can be
|
||||
[found here](https://docs.pyscript.net/).
|
||||
|
||||
Clone this repository then run `npm install` within this folder.
|
||||
PyScript sits on two further projects (both written in JavaScript):
|
||||
|
||||
Use `npm run build` to create all artifacts and _dist_ files.
|
||||
1. [polyscript](https://github.com/pyscript/polyscript/#readme) - used to
|
||||
bootstrap WASM compiled interpreters in a browser.
|
||||
2. [coincident](https://github.com/WebReflection/coincident) - used to simplify
|
||||
worker based tasks.
|
||||
|
||||
Use `npm run server` to test locally, via the `http://localhost:8080/tests/` url, smoke tests or to test manually anything you'd like to check.
|
||||
PyScript itself is mostly written in JavaScript. The test suite for JavaScript
|
||||
is in two parts: automated tests run in [playwright](https://playwright.dev/),
|
||||
and manual tests you have to run in a browser and check yourself. PyScript also
|
||||
has a plugin system so third parties can extend its capabilities with
|
||||
JavaScript. Our built-in core plugins can be found in the `src/plugins`
|
||||
directory. We describe how to write third party plugins in our
|
||||
[developer documentation](https://docs.pyscript.net/latest/user-guide/plugins/).
|
||||
|
||||
### Artifacts
|
||||
We provide a `pyscript` namespace containing Python modules for common browser
|
||||
based APIs and features (i.e. you can `import pyscript` in Python code running
|
||||
inside PyScript, to access these features). The Python code for the `pyscript`
|
||||
namespace is in `src/stdlib/pyscript` with the associated test suite in
|
||||
`tests/python`. The tests use the browser friendly
|
||||
[uPyTest](https://github.com/ntoll/upytest) test framework for checking Python
|
||||
code running _within_ PyScript. All the Python tests are run in each each
|
||||
available interpreter in both the main thread and a web worker (i.e. the
|
||||
test suite is run four times, accounting for each combination of interpreter
|
||||
and main/worker context).
|
||||
|
||||
When you create a local build all the automated tests (JavaScript and Python)
|
||||
are run.
|
||||
|
||||
## Developer Guide
|
||||
|
||||
Full instructions for setting up a working development environment, how to
|
||||
build PyScript and how to test it can be
|
||||
[found in our official docs](https://docs.pyscript.net/latest/developers/).
|
||||
|
||||
The short version is:
|
||||
|
||||
- Ensure you have Python, node and npm installed.
|
||||
- Create a Python virtual environment.
|
||||
- In the root of this repository `make setup`.
|
||||
- `make build` to build PyScript.
|
||||
- As dependencies change over time, `make update` to keep in sync.
|
||||
|
||||
To start using the locally built version of PyScript, you'll need an HTML
|
||||
page something like this (note the relative paths to assets in the `dist`
|
||||
directory, in the `<head>` of the document):
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pure Python PyScript tests</title>
|
||||
<link rel="stylesheet" href="../../dist/core.css" />
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" src="./main.py" config="./conf.toml"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Once set up, you should be able to run the most common activities via the
|
||||
`make` command:
|
||||
|
||||
```
|
||||
$ make
|
||||
|
||||
There is no default Makefile target right now. Try:
|
||||
|
||||
make setup - check your environment and install the dependencies.
|
||||
make update - update dependencies.
|
||||
make clean - clean up auto-generated assets.
|
||||
make build - build PyScript.
|
||||
make precommit-check - run the precommit checks (run eslint).
|
||||
make test - run all automated tests in playwright.
|
||||
make fmt - format the code.
|
||||
make fmt-check - check the code formatting.
|
||||
```
|
||||
|
||||
## Artifacts
|
||||
|
||||
There are two main artifacts in this project:
|
||||
|
||||
- **stdlib** and its content, where `src/stdlib/pyscript.js` exposes as object literal all the _Python_ content within the folder (recursively)
|
||||
- **plugins** and its content, where `src/plugins.js` exposes all available _dynamic imports_, able to instrument the bundler to create files a part within the _dist/_ folder, so that by default _core_ remains as small as possible
|
||||
- **stdlib** and its content: `src/stdlib/pyscript.js` exposes, as a
|
||||
JavaScript object literal, all the _Python_ content within the folder
|
||||
(recursively).
|
||||
- **plugins** and its content: `src/plugins.js` exposes all available
|
||||
_dynamic imports_, and is able to instrument the bundler to create files
|
||||
apart from the `_dist/_` folder, so that by default _core_ remains as small
|
||||
as possible.
|
||||
|
||||
Accordingly, whenever a file contains this warning at its first line, please do not change such file directly before submitting a merge request, as that file will be overwritten at the next `npm run build` command, either here or in _CI_:
|
||||
Accordingly, whenever a file contains this warning at its first line, **please
|
||||
do not change such file directly before submitting a merge request**, as that
|
||||
file will be overwritten at the next `npm run build` command, either here or
|
||||
in _CI_:
|
||||
|
||||
```js
|
||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||
```
|
||||
|
||||
### Running tests
|
||||
## Plugins
|
||||
|
||||
Before running the tests, we need to create a tests environment first. To do so run the following command from the root folder of the project:
|
||||
While community or third party plugins don't need to be part of this repository
|
||||
and can be added just importing `@pyscript/core` as module, there are a few
|
||||
plugins that we would like to make available by default and these are
|
||||
considered _core plugins_.
|
||||
|
||||
```
|
||||
make setup
|
||||
```
|
||||
To add a _core plugin_ to this project define the plugin entry-point and name
|
||||
in the `src/plugins` folder (see the `error.js` example) and create, if
|
||||
necessary, a folder with the same name where extra files or dependencies can be
|
||||
added.
|
||||
|
||||
This will create a tests environment [in the root of the project, named `./env`] and install all the dependencies needed to run the tests.
|
||||
The _build_ command will include plugins by name as artifacts so that the
|
||||
bundler can create ad-hoc files within the `dist/` folder.
|
||||
|
||||
A lot of problems related to `make setup` are related to node and npm being outdated. Once npm and node are updated, `make setup` should work. You can follow the steps on the [npm documentation](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) to update npm (the update command for Linux should work for Mac as well). Once npm has been updated you can continue to the instructions to update node below.
|
||||
## Python
|
||||
|
||||
To update Node run the following commands in order (most likely you'll be prompted for your user password, this is normal):
|
||||
The `pyscript` package available in _Python_ lives in the folder
|
||||
`src/stdlib/pyscript/`.
|
||||
|
||||
```
|
||||
sudo npm cache clean -f
|
||||
sudo npm install -g n
|
||||
sudo n stable
|
||||
```
|
||||
All _Python_ files will be embedded automatically whenever `npm run build`
|
||||
happens and reflected into the `src/stdlib/pyscript.js` file.
|
||||
|
||||
After the `make setup` command has completed, you can run the **automated tests** with
|
||||
the following command:
|
||||
Its _core_ responsibility is to ensure those files will be available through
|
||||
the filesystem in either the _main_ thread, or any _worker_.
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
## Release
|
||||
|
||||
(This essentially runs the `npm run test:integration` command in the right place. This is defined in PyScript's `package.json` file.)
|
||||
To cut a new release of PyScript simply
|
||||
[add a new release](https://github.com/pyscript/pyscript/releases) while
|
||||
remembering to write a comprehensive changelog. A
|
||||
[GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
|
||||
will kick in and ensure the release is described and deployed to a URL with the
|
||||
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
|
||||
our [CalVer](https://calver.org/) versioning scheme).
|
||||
|
||||
Tests are found in the `tests` directory. These are organised into three locations:
|
||||
Then, the following three separate repositories need updating:
|
||||
|
||||
1. `python` - the Python based test suite to exercise Python code **within** PyScript.
|
||||
2. `javascript` - JavaScript tests to exercise PyScript itself, in the browser.
|
||||
3. `manual` - containing tests to run manually in a browser, due to the complex nature of the tests.
|
||||
|
||||
We use [Playwright](https://playwright.dev/) to automate the running of the Python and JavaScript test suites. We use [uPyTest](https://github.com/ntoll/upytest) as a test framework for the Python test suite. uPyTest is a "PyTest inspired" framework for running tests in the browser on both MicroPython and Pyodide.
|
||||
|
||||
The automated (Playwright) tests are specified in the `tests/integration.spec.js` file.
|
||||
|
||||
## `pyscript` python package
|
||||
|
||||
The `pyscript` package available in _Python_ lives in the folder `src/stdlib/pyscript/`.
|
||||
|
||||
All _Python_ files will be embedded automatically whenever `npm run build` happens and reflected into the `src/stdlib/pyscript.js` file.
|
||||
|
||||
It is _core_ responsibility to ensure those files will be available through the Filesystem in either the _main_ thread, or any _worker_.
|
||||
|
||||
## JS plugins
|
||||
|
||||
While community or third party plugins don't need to be part of this repository and can be added just importing `@pyscript/core` as module, there are a few plugins that we would like to make available by default and these are considered _core plugins_.
|
||||
|
||||
To add a _core plugin_ to this project you can define your plugin entry-point and name in the `src/plugins` folder (see the `error.js` example) and create, if necessary, a folder with the same name where extra files or dependencies can be added.
|
||||
|
||||
The _build_ command will bring plugins by name as artifact so that the bundler can create ad-hoc files within the `dist/` folder.
|
||||
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
|
||||
file in the root of the directory and then `node version-update.js`.
|
||||
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
|
||||
referenced in `index.html` is the latest version.
|
||||
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
|
||||
Anaconda) to let the PSDC team know there's a new version, so they can update
|
||||
their project templates.
|
||||
|
||||
1156
core/package-lock.json
generated
1156
core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@pyscript/core",
|
||||
"version": "0.5.16",
|
||||
"version": "0.6.39",
|
||||
"type": "module",
|
||||
"description": "PyScript",
|
||||
"module": "./index.js",
|
||||
@@ -25,6 +25,10 @@
|
||||
"types": "./types/core.d.ts",
|
||||
"import": "./src/core.js"
|
||||
},
|
||||
"./js": {
|
||||
"types": "./types/core.d.ts",
|
||||
"import": "./dist/core.js"
|
||||
},
|
||||
"./css": {
|
||||
"import": "./dist/core.css"
|
||||
},
|
||||
@@ -43,8 +47,8 @@
|
||||
"build:3rd-party": "node rollup/3rd-party.cjs",
|
||||
"build:tests-index": "node rollup/build_test_index.cjs",
|
||||
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
|
||||
"test:integration": "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:ws": "bun tests/ws/index.js & playwright test tests/ws.spec.js",
|
||||
"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",
|
||||
"dev": "node dev.cjs",
|
||||
"release": "npm run build && npm run zip",
|
||||
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
|
||||
@@ -59,41 +63,42 @@
|
||||
"license": "APACHE-2.0",
|
||||
"dependencies": {
|
||||
"@ungap/with-resolvers": "^0.1.0",
|
||||
"@webreflection/idb-map": "^0.3.1",
|
||||
"@webreflection/idb-map": "^0.3.2",
|
||||
"add-promise-listener": "^0.1.3",
|
||||
"basic-devtools": "^0.1.6",
|
||||
"polyscript": "^0.15.12",
|
||||
"sabayon": "^0.5.2",
|
||||
"polyscript": "^0.16.22",
|
||||
"sabayon": "^0.6.6",
|
||||
"sticky-module": "^0.1.1",
|
||||
"to-json-callback": "^0.1.1",
|
||||
"type-checked-collections": "^0.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@codemirror/commands": "^6.6.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@playwright/test": "1.45.3",
|
||||
"@rollup/plugin-commonjs": "^28.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.3.0",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-python": "^6.1.7",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-commonjs": "^28.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@webreflection/toml-j0.4": "^1.1.3",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"bun": "^1.1.29",
|
||||
"chokidar": "^4.0.1",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"bun": "^1.2.5",
|
||||
"chokidar": "^4.0.3",
|
||||
"codedent": "^0.1.2",
|
||||
"codemirror": "^6.0.1",
|
||||
"eslint": "^9.11.1",
|
||||
"flatted": "^3.3.1",
|
||||
"rollup": "^4.22.5",
|
||||
"eslint": "^9.22.0",
|
||||
"flatted": "^3.3.3",
|
||||
"rollup": "^4.36.0",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-string": "^3.0.0",
|
||||
"static-handler": "^0.5.3",
|
||||
"string-width": "^7.2.0",
|
||||
"typescript": "^5.6.2",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-readline": "^1.1.1"
|
||||
"typescript": "^5.8.2",
|
||||
"xterm-readline": "^1.1.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -46,7 +46,7 @@ const modules = {
|
||||
"toml.js": join(node_modules, "@webreflection", "toml-j0.4", "toml.js"),
|
||||
|
||||
// xterm
|
||||
"xterm.js": resolve("xterm"),
|
||||
"xterm.js": resolve("@xterm/xterm"),
|
||||
"xterm-readline.js": resolve("xterm-readline"),
|
||||
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
|
||||
b.text(),
|
||||
@@ -54,9 +54,9 @@ const modules = {
|
||||
"xterm_addon-web-links.js": fetch(
|
||||
`${CDN}/@xterm/addon-web-links/+esm`,
|
||||
).then((b) => b.text()),
|
||||
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
|
||||
(b) => b.text(),
|
||||
),
|
||||
"xterm.css": fetch(
|
||||
`${CDN}/@xterm/xterm@${v("@xterm/xterm")}/css/xterm.min.css`,
|
||||
).then((b) => b.text()),
|
||||
|
||||
// codemirror
|
||||
"codemirror.js": reBundle("codemirror"),
|
||||
|
||||
4
core/src/3rd-party/xterm-readline.js
vendored
4
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.
|
||||
* 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
|
||||
*/
|
||||
.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}
|
||||
|
||||
4
core/src/3rd-party/xterm.js
vendored
4
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.37.0.
|
||||
* 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
|
||||
|
||||
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.37.0.
|
||||
* 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
|
||||
|
||||
@@ -25,7 +25,7 @@ const badURL = (url, expected = "") => {
|
||||
* @param {string?} type the optional type to enforce
|
||||
* @returns {{json: boolean, toml: boolean, text: string}}
|
||||
*/
|
||||
const configDetails = async (config, type) => {
|
||||
export const configDetails = async (config, type) => {
|
||||
let text = config?.trim();
|
||||
// we only support an object as root config
|
||||
let url = "",
|
||||
@@ -56,7 +56,7 @@ const syntaxError = (type, url, { message }) => {
|
||||
const configs = new Map();
|
||||
|
||||
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;
|
||||
|
||||
/** @type {any} The PyScript configuration parsed from the JSON or TOML object*. May be any of the return types of JSON.parse() or toml-j0.4's parse() ( {number | string | boolean | null | object | Array} ) */
|
||||
@@ -135,22 +135,24 @@ for (const [TYPE] of TYPES) {
|
||||
|
||||
// parse all plugins and optionally ignore only
|
||||
// those flagged as "undesired" via `!` prefix
|
||||
const toBeAwaited = [];
|
||||
for (const [key, value] of Object.entries(allPlugins)) {
|
||||
if (error) {
|
||||
if (key === "error") {
|
||||
// show on page the config is broken, meaning that
|
||||
// it was not possible to disable error plugin neither
|
||||
// as that part wasn't correctly parsed anyway
|
||||
value().then(({ notify }) => notify(error.message));
|
||||
plugins = async () => {
|
||||
const toBeAwaited = [];
|
||||
for (const [key, value] of Object.entries(allPlugins)) {
|
||||
if (error) {
|
||||
if (key === "error") {
|
||||
// show on page the config is broken, meaning that
|
||||
// it was not possible to disable error plugin neither
|
||||
// as that part wasn't correctly parsed anyway
|
||||
value().then(({ notify }) => notify(error.message));
|
||||
}
|
||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||
} else if (key === "error") {
|
||||
toBeAwaited.push(value().then(({ notOnDOM }) => notOnDOM()));
|
||||
}
|
||||
} else if (!parsed?.plugins?.includes(`!${key}`)) {
|
||||
toBeAwaited.push(value().then(({ default: p }) => p));
|
||||
}
|
||||
}
|
||||
|
||||
// assign plugins as Promise.all only if needed
|
||||
plugins = Promise.all(toBeAwaited);
|
||||
return await Promise.all(toBeAwaited);
|
||||
};
|
||||
|
||||
configs.set(TYPE, { config: parsed, configURL, plugins, error });
|
||||
}
|
||||
|
||||
@@ -28,48 +28,34 @@ mpy-config {
|
||||
.py-editor-run-button,
|
||||
.mpy-editor-run-button {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
right: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
}
|
||||
.py-editor-box:hover .py-editor-run-button,
|
||||
.mpy-editor-box:hover .mpy-editor-run-button,
|
||||
.py-editor-run-button:focus,
|
||||
.py-editor-run-button:disabled,
|
||||
.py-editor-run-button.running,
|
||||
.mpy-editor-run-button:focus,
|
||||
.mpy-editor-run-button:disabled {
|
||||
.mpy-editor-run-button.running {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
py-terminal span,
|
||||
mpy-terminal span {
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
.py-editor-run-button:disabled > *,
|
||||
.mpy-editor-run-button:disabled > * {
|
||||
display: none; /* hide all the child elements of the run button when it is disabled */
|
||||
|
||||
dialog.pyscript-fs {
|
||||
border-radius: 8px;
|
||||
border-width: 1px;
|
||||
}
|
||||
.py-editor-run-button:disabled,
|
||||
.mpy-editor-run-button:disabled {
|
||||
border-width: 0;
|
||||
}
|
||||
.py-editor-run-button:disabled::before,
|
||||
.mpy-editor-run-button:disabled::before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
|
||||
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
|
||||
border-radius: 50%;
|
||||
border: 2px solid #aaa;
|
||||
border-top-color: #000;
|
||||
background-color: #fff;
|
||||
animation: spinner 0.6s linear infinite;
|
||||
|
||||
dialog.pyscript-fs > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,19 @@ import {
|
||||
createFunction,
|
||||
inputFailure,
|
||||
} from "./hooks.js";
|
||||
import * as fs from "./fs.js";
|
||||
|
||||
import codemirror from "./plugins/codemirror.js";
|
||||
export { codemirror };
|
||||
|
||||
import { stdlib, optional } from "./stdlib.js";
|
||||
export { stdlib, optional, inputFailure };
|
||||
|
||||
export const donkey = (options) =>
|
||||
import(/* webpackIgnore: true */ "./plugins/donkey.js").then((module) =>
|
||||
module.default(options),
|
||||
);
|
||||
|
||||
// generic helper to disambiguate between custom element and script
|
||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||
|
||||
@@ -159,6 +168,8 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// enrich the Python env with some JS utility for main
|
||||
interpreter.registerJsModule("_pyscript", {
|
||||
PyWorker,
|
||||
fs,
|
||||
interpreter,
|
||||
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
|
||||
get target() {
|
||||
return isScript(currentElement)
|
||||
@@ -174,7 +185,7 @@ for (const [TYPE, interpreter] of TYPES) {
|
||||
// ensure plugins are bootstrapped already before custom type definition
|
||||
// NOTE: we cannot top-level await in here as plugins import other utilities
|
||||
// from core.js itself so that custom definition should not be blocking.
|
||||
plugins.then(() => {
|
||||
plugins().then(() => {
|
||||
// possible early errors sent by polyscript
|
||||
const errors = new Map();
|
||||
|
||||
|
||||
81
core/src/fs.js
Normal file
81
core/src/fs.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import IDBMap from "@webreflection/idb-map";
|
||||
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 } = Promise.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>} */
|
||||
onBeforeRun: new SetFunction(),
|
||||
/** @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>} */
|
||||
onAfterRun: new SetFunction(),
|
||||
/** @type {Set<function>} */
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
// ⚠️ This file is an artifact: DO NOT MODIFY
|
||||
export default {
|
||||
codemirror: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/codemirror.js"
|
||||
),
|
||||
["deprecations-manager"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/deprecations-manager.js"
|
||||
),
|
||||
donkey: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/donkey.js"
|
||||
),
|
||||
error: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
@@ -15,6 +25,11 @@ export default {
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-editor.js"
|
||||
),
|
||||
["py-game"]: () =>
|
||||
import(
|
||||
/* webpackIgnore: true */
|
||||
"./plugins/py-game.js"
|
||||
),
|
||||
["py-terminal"]: () =>
|
||||
import(
|
||||
/* 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
|
||||
import { hooks } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { hooks } from "../core.js";
|
||||
|
||||
// react lazily on PyScript bootstrap
|
||||
hooks.main.onReady.add(checkDeprecations);
|
||||
|
||||
121
core/src/plugins/donkey.js
Normal file
121
core/src/plugins/donkey.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import addPromiseListener from "add-promise-listener";
|
||||
import { assign, dedent } from "polyscript/exports";
|
||||
|
||||
const { stringify } = JSON;
|
||||
|
||||
const invoke = (name, args) => `${name}(code, ${args.join(", ")})`;
|
||||
|
||||
const donkey = ({
|
||||
type = "py",
|
||||
persistent,
|
||||
terminal,
|
||||
config,
|
||||
serviceWorker,
|
||||
}) => {
|
||||
const globals = terminal ? '{"__terminal__":__terminal__}' : "{}";
|
||||
const args = persistent ? ["globals()", "__locals__"] : [globals, "{}"];
|
||||
|
||||
const src = URL.createObjectURL(
|
||||
new Blob([
|
||||
[
|
||||
// this array is to better minify this code once in production
|
||||
"from pyscript import sync, config",
|
||||
'__message__ = lambda e,v: f"\x1b[31m\x1b[1m{e.__name__}\x1b[0m: {v}"',
|
||||
"__locals__ = {}",
|
||||
'if config["type"] == "py":',
|
||||
" import sys",
|
||||
" def __error__(_):",
|
||||
" info = sys.exc_info()",
|
||||
" return __message__(info[0], info[1])",
|
||||
"else:",
|
||||
" __error__ = lambda e: __message__(e.__class__, e.value)",
|
||||
"def execute(code):",
|
||||
` try: return ${invoke("exec", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"def evaluate(code):",
|
||||
` try: return ${invoke("eval", args)};`,
|
||||
" except Exception as e: print(__error__(e));",
|
||||
"sync.execute = execute",
|
||||
"sync.evaluate = evaluate",
|
||||
].join("\n"),
|
||||
]),
|
||||
);
|
||||
|
||||
// create the script that exposes the code to execute or evaluate
|
||||
const script = assign(document.createElement("script"), { type, src });
|
||||
script.toggleAttribute("worker", true);
|
||||
script.toggleAttribute("terminal", true);
|
||||
if (terminal) script.setAttribute("target", terminal);
|
||||
if (config) {
|
||||
script.setAttribute(
|
||||
"config",
|
||||
typeof config === "string" ? config : stringify(config),
|
||||
);
|
||||
}
|
||||
if (serviceWorker) script.setAttribute("service-worker", serviceWorker);
|
||||
|
||||
return addPromiseListener(
|
||||
document.body.appendChild(script),
|
||||
`${type}:done`,
|
||||
{ stopPropagation: true },
|
||||
).then(() => {
|
||||
URL.revokeObjectURL(src);
|
||||
return script;
|
||||
});
|
||||
};
|
||||
|
||||
const utils = async (options) => {
|
||||
const script = await donkey(options);
|
||||
const { xworker, process, terminal } = script;
|
||||
const { execute, evaluate } = xworker.sync;
|
||||
script.remove();
|
||||
return {
|
||||
xworker,
|
||||
process,
|
||||
terminal,
|
||||
execute,
|
||||
evaluate,
|
||||
};
|
||||
};
|
||||
|
||||
export default async (options = {}) => {
|
||||
let farmer = await utils(options);
|
||||
let working = false;
|
||||
const kill = () => {
|
||||
if (farmer) {
|
||||
farmer.xworker.terminate();
|
||||
farmer.terminal.dispose();
|
||||
farmer = null;
|
||||
}
|
||||
working = false;
|
||||
};
|
||||
const reload = async () => {
|
||||
kill();
|
||||
farmer = await utils(options);
|
||||
};
|
||||
const asyncTask = (method) => async (code) => {
|
||||
// race condition ... a new task has been
|
||||
// assigned while the previous one didn't finish
|
||||
if (working) await reload();
|
||||
working = true;
|
||||
try {
|
||||
return await farmer[method](dedent(code));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
working = false;
|
||||
}
|
||||
};
|
||||
const asyncMethod = (method) => async () => {
|
||||
if (working) await reload();
|
||||
else farmer?.terminal[method]();
|
||||
};
|
||||
return {
|
||||
process: asyncTask("process"),
|
||||
execute: asyncTask("execute"),
|
||||
evaluate: asyncTask("evaluate"),
|
||||
clear: asyncMethod("clear"),
|
||||
reset: asyncMethod("reset"),
|
||||
kill,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,12 @@
|
||||
// PyScript Error Plugin
|
||||
import { buffered } from "polyscript/exports";
|
||||
import { hooks } from "../core.js";
|
||||
|
||||
let dontBotherDOM = false;
|
||||
export function notOnDOM() {
|
||||
dontBotherDOM = true;
|
||||
}
|
||||
|
||||
hooks.main.onReady.add(function override(pyScript) {
|
||||
// be sure this override happens only once
|
||||
hooks.main.onReady.delete(override);
|
||||
@@ -8,13 +14,15 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
// trap generic `stderr` to propagate to it regardless
|
||||
const { stderr } = pyScript.io;
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = (error, ...rest) => {
|
||||
const cb = (error, ...rest) => {
|
||||
notify(error.message || error);
|
||||
// let other plugins or stderr hook, if any, do the rest
|
||||
return stderr(error, ...rest);
|
||||
};
|
||||
|
||||
// override it with our own logic
|
||||
pyScript.io.stderr = pyScript.type === "py" ? cb : buffered(cb);
|
||||
|
||||
// be sure uncaught Python errors are also visible
|
||||
addEventListener("error", ({ message }) => {
|
||||
if (message.startsWith("Uncaught PythonError")) notify(message);
|
||||
@@ -30,6 +38,7 @@ hooks.main.onReady.add(function override(pyScript) {
|
||||
* @param {string} message
|
||||
*/
|
||||
export function notify(message) {
|
||||
if (dontBotherDOM) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = "py-error";
|
||||
div.textContent = message;
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
|
||||
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.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;
|
||||
const getID = (type) => `${type}-editor-${id++}`;
|
||||
|
||||
const envs = new Map();
|
||||
const configs = new Map();
|
||||
const editors = new WeakMap();
|
||||
|
||||
const hooks = {
|
||||
worker: {
|
||||
@@ -29,12 +32,18 @@ const validate = (config, result) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const getRelatedScript = (target, type) => {
|
||||
const editor = target.closest(`.${type}-editor-box`);
|
||||
return editor?.parentNode?.previousElementSibling;
|
||||
};
|
||||
|
||||
async function execute({ currentTarget }) {
|
||||
const { env, pySrc, outDiv } = this;
|
||||
const hasRunButton = !!currentTarget;
|
||||
|
||||
if (hasRunButton) {
|
||||
currentTarget.disabled = true;
|
||||
currentTarget.classList.add("running");
|
||||
currentTarget.innerHTML = STOP_BUTTON;
|
||||
outDiv.innerHTML = "";
|
||||
}
|
||||
|
||||
@@ -77,6 +86,18 @@ async function execute({ currentTarget }) {
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
|
||||
// expose xworker like in terminal or other workers to allow
|
||||
// creation and destruction of editors on the fly
|
||||
if (hasRunButton) {
|
||||
for (const type of TYPES.keys()) {
|
||||
const script = getRelatedScript(currentTarget, type);
|
||||
if (script) {
|
||||
defineProperties(script, { xworker: { value: xworker } });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { sync } = xworker;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
envs.set(env, promise);
|
||||
@@ -102,7 +123,10 @@ async function execute({ currentTarget }) {
|
||||
};
|
||||
|
||||
const enable = () => {
|
||||
if (hasRunButton) currentTarget.disabled = false;
|
||||
if (hasRunButton) {
|
||||
currentTarget.classList.remove("running");
|
||||
currentTarget.innerHTML = RUN_BUTTON;
|
||||
}
|
||||
};
|
||||
const { sync } = xworker;
|
||||
sync.write = (str) => {
|
||||
@@ -130,6 +154,24 @@ const makeRunButton = (handler, type) => {
|
||||
runButton.innerHTML = RUN_BUTTON;
|
||||
runButton.setAttribute("aria-label", "Python Script Run Button");
|
||||
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();
|
||||
await handler.handleEvent(event);
|
||||
});
|
||||
@@ -181,14 +223,12 @@ const init = async (script, type, interpreter) => {
|
||||
{ keymap },
|
||||
{ defaultKeymap, indentWithTab },
|
||||
] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
|
||||
import(
|
||||
/* webpackIgnore: true */ "../3rd-party/codemirror_lang-python.js"
|
||||
),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_language.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_view.js"),
|
||||
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
|
||||
codemirror.core,
|
||||
codemirror.state,
|
||||
codemirror.python,
|
||||
codemirror.language,
|
||||
codemirror.view,
|
||||
codemirror.commands,
|
||||
]);
|
||||
|
||||
let isSetup = script.hasAttribute("setup");
|
||||
@@ -375,6 +415,7 @@ const init = async (script, type, interpreter) => {
|
||||
doc,
|
||||
});
|
||||
|
||||
editors.set(script, editor);
|
||||
editor.focus();
|
||||
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,6 @@
|
||||
// PyScript pyodide terminal plugin
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
import { hooks, inputFailure } from "../../core.js";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
@@ -34,6 +34,8 @@ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
pyterminal_write(String(error.message || error));
|
||||
};
|
||||
|
||||
sync.pyterminal_stream_write = () => {};
|
||||
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
@@ -71,6 +73,7 @@ export default async (element) => {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
lineHeight: 1.2,
|
||||
};
|
||||
|
||||
let stream;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { hooks } from "../../core.js";
|
||||
import { defineProperties } from "polyscript/exports";
|
||||
import { hooks } from "../../core.js";
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
|
||||
@@ -126,6 +126,7 @@ export default async (element) => {
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
lineHeight: 1.2,
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
@@ -136,6 +137,18 @@ export default async (element) => {
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
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 {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
|
||||
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.
|
||||
|
||||
from polyscript import lazy_py_modules as py_import
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.magic_js import (
|
||||
RUNNING_IN_WORKER,
|
||||
PyWorker,
|
||||
@@ -43,19 +41,11 @@ from pyscript.magic_js import (
|
||||
sync,
|
||||
window,
|
||||
)
|
||||
from pyscript.display import HTML, display
|
||||
from pyscript.fetch import fetch
|
||||
from pyscript.storage import Storage, storage
|
||||
from pyscript.websocket import WebSocket
|
||||
from pyscript.events import when, Event
|
||||
|
||||
if not RUNNING_IN_WORKER:
|
||||
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__":
|
||||
return repr(obj)
|
||||
elif hasattr(obj, print_method):
|
||||
if hasattr(obj, print_method):
|
||||
if print_method == "savefig":
|
||||
buf = io.BytesIO()
|
||||
obj.savefig(buf, format="png")
|
||||
buf.seek(0)
|
||||
return base64.b64encode(buf.read()).decode("utf-8")
|
||||
return getattr(obj, print_method)()
|
||||
elif print_method == "_repr_mimebundle_":
|
||||
if print_method == "_repr_mimebundle_":
|
||||
return {}, {}
|
||||
return None
|
||||
|
||||
@@ -107,7 +107,7 @@ def _format_mime(obj):
|
||||
|
||||
if output is None:
|
||||
continue
|
||||
elif mime_type not in _MIME_RENDERERS:
|
||||
if mime_type not in _MIME_RENDERERS:
|
||||
not_available.append(mime_type)
|
||||
continue
|
||||
break
|
||||
@@ -149,9 +149,11 @@ def display(*values, target=None, append=True):
|
||||
if target is None:
|
||||
target = current_target()
|
||||
elif not isinstance(target, str):
|
||||
raise TypeError(f"target must be str or None, not {target.__class__.__name__}")
|
||||
msg = f"target must be str or None, not {target.__class__.__name__}"
|
||||
raise TypeError(msg)
|
||||
elif target == "":
|
||||
raise ValueError("Cannot have an empty target")
|
||||
msg = "Cannot have an empty target"
|
||||
raise ValueError(msg)
|
||||
elif target.startswith("#"):
|
||||
# note: here target is str and not None!
|
||||
# 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 element is None:
|
||||
raise ValueError(
|
||||
f"Invalid selector with id={target}. Cannot be found in the page."
|
||||
)
|
||||
msg = 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
|
||||
# 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
|
||||
225
core/src/stdlib/pyscript/events.py
Normal file
225
core/src/stdlib/pyscript/events.py
Normal file
@@ -0,0 +1,225 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
|
||||
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:
|
||||
"""
|
||||
Events represent something that may happen at some point in time (usually
|
||||
the future). They're used to coordinate code when the timing of an event is
|
||||
not known in advance (e.g. a button click or a network response).
|
||||
|
||||
An event is triggered with an arbitrary result. If no result is given, then
|
||||
None is assumed as the result.
|
||||
|
||||
Add listener functions to the event, to be called with the result when the
|
||||
event is triggered. The listener functions can be callable or awaitable. If
|
||||
the listener is added several times, it will be called only once.
|
||||
|
||||
If the event was triggered before a listener is added, the listener will be
|
||||
called as soon as it is added, with the result of the event.
|
||||
|
||||
If the event is never triggered, then its listeners will never be called.
|
||||
It's also possible to remove listeners from the event.
|
||||
|
||||
If the result of the event is not available, a ValueError will be raised
|
||||
when trying to access the result property. A RuntimeError will be raised if
|
||||
the event is triggered more than once.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# To contain the listeners to be called when the event is triggered.
|
||||
self._listeners = []
|
||||
# The result associated with the event.
|
||||
self._result = None
|
||||
# A flag to indicate if the event has been triggered.
|
||||
self._triggered = False
|
||||
|
||||
@property
|
||||
def triggered(self):
|
||||
"""
|
||||
A boolean flag to indicate if the event has been triggered.
|
||||
"""
|
||||
return self._triggered
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
"""
|
||||
The result of the event.
|
||||
"""
|
||||
if self.triggered:
|
||||
return self._result
|
||||
msg = "Event has not been triggered yet. No result available."
|
||||
raise ValueError(msg)
|
||||
|
||||
def trigger(self, result=None):
|
||||
"""
|
||||
Trigger the event with an arbitrary result to pass into the listeners.
|
||||
An event may only be triggered once (otherwise a RuntimeError is
|
||||
raised).
|
||||
"""
|
||||
if self.triggered:
|
||||
msg = "Event has already been triggered."
|
||||
raise RuntimeError(msg)
|
||||
self._triggered = True
|
||||
self._result = result
|
||||
for listener in self._listeners:
|
||||
self._call_listener(listener)
|
||||
|
||||
def add_listener(self, listener):
|
||||
"""
|
||||
Add a callable/awaitable that listens for the result, when this event
|
||||
is triggered.
|
||||
"""
|
||||
if is_awaitable(listener) or callable(listener):
|
||||
if listener not in self._listeners:
|
||||
self._listeners.append(listener)
|
||||
if self.triggered:
|
||||
# If the event was already triggered, call the listener
|
||||
# immediately with the result.
|
||||
self._call_listener(listener)
|
||||
else:
|
||||
msg = "Listener must be callable or awaitable."
|
||||
raise ValueError(msg)
|
||||
|
||||
def remove_listener(self, *args):
|
||||
"""
|
||||
Clear the specified listener functions in *args. If no listeners are
|
||||
provided, clear all the listeners.
|
||||
"""
|
||||
if args:
|
||||
for listener in args:
|
||||
self._listeners.remove(listener)
|
||||
else:
|
||||
self._listeners = []
|
||||
|
||||
def _call_listener(self, listener):
|
||||
"""
|
||||
Call the referenced listener with the event's result.
|
||||
"""
|
||||
if is_awaitable(listener):
|
||||
asyncio.create_task(listener(self._result))
|
||||
else:
|
||||
listener(self._result)
|
||||
|
||||
|
||||
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):
|
||||
return isinstance(value, list) or isinstance(value, tuple)
|
||||
return isinstance(value, (list, tuple))
|
||||
|
||||
|
||||
def _is_object(value):
|
||||
@@ -60,10 +60,10 @@ def _loop(keys, 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)
|
||||
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)
|
||||
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
|
||||
if not field.startswith("_"):
|
||||
return getattr(getattr(js_modules, self.name), field)
|
||||
return None
|
||||
|
||||
|
||||
# generate N modules in the system that will proxy the real value
|
||||
|
||||
87
core/src/stdlib/pyscript/media.py
Normal file
87
core/src/stdlib/pyscript/media.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from pyscript import window
|
||||
from pyscript.ffi import to_js
|
||||
|
||||
|
||||
class Device:
|
||||
"""Device represents a media input or output device, such as a microphone,
|
||||
camera, or headset.
|
||||
"""
|
||||
|
||||
def __init__(self, device):
|
||||
self._dom_element = device
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._dom_element.deviceId
|
||||
|
||||
@property
|
||||
def group(self):
|
||||
return self._dom_element.groupId
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
return self._dom_element.kind
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self._dom_element.label
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
@classmethod
|
||||
async def load(cls, audio=False, video=True):
|
||||
"""
|
||||
Load the device stream.
|
||||
"""
|
||||
options = {}
|
||||
options["audio"] = audio
|
||||
if isinstance(video, bool):
|
||||
options["video"] = video
|
||||
else:
|
||||
options["video"] = {}
|
||||
for k in video:
|
||||
options["video"][k] = video[k]
|
||||
return await window.navigator.mediaDevices.getUserMedia(to_js(options))
|
||||
|
||||
async def get_stream(self):
|
||||
key = self.kind.replace("input", "").replace("output", "")
|
||||
options = {key: {"deviceId": {"exact": self.id}}}
|
||||
return await self.load(**options)
|
||||
|
||||
|
||||
async def list_devices() -> list[dict]:
|
||||
"""
|
||||
Return the list of the currently available media input and output devices,
|
||||
such as microphones, cameras, headsets, and so forth.
|
||||
|
||||
Output:
|
||||
|
||||
list(dict) - list of dictionaries representing the available media devices.
|
||||
Each dictionary has the following keys:
|
||||
* deviceId: a string that is an identifier for the represented device
|
||||
that is persisted across sessions. It is un-guessable by other
|
||||
applications and unique to the origin of the calling application.
|
||||
It is reset when the user clears cookies (for Private Browsing, a
|
||||
different identifier is used that is not persisted across sessions).
|
||||
|
||||
* groupId: a string that is a group identifier. Two devices have the same
|
||||
group identifier if they belong to the same physical device — for
|
||||
example a monitor with both a built-in camera and a microphone.
|
||||
|
||||
* kind: an enumerated value that is either "videoinput", "audioinput"
|
||||
or "audiooutput".
|
||||
|
||||
* label: a string describing this device (for example "External USB
|
||||
Webcam").
|
||||
|
||||
Note: the returned list will omit any devices that are blocked by the document
|
||||
Permission Policy: microphone, camera, speaker-selection (for output devices),
|
||||
and so on. Access to particular non-default devices is also gated by the
|
||||
Permissions API, and the list will omit devices for which the user has not
|
||||
granted explicit permission.
|
||||
"""
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
||||
return [
|
||||
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
|
||||
]
|
||||
@@ -10,10 +10,11 @@ def _to_idb(value):
|
||||
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
|
||||
return _stringify(["generic", value])
|
||||
if isinstance(value, bytearray):
|
||||
return _stringify(["bytearray", [v for v in value]])
|
||||
return _stringify(["bytearray", list(value)])
|
||||
if isinstance(value, memoryview):
|
||||
return _stringify(["memoryview", [v for v in value]])
|
||||
raise TypeError(f"Unexpected value: {value}")
|
||||
return _stringify(["memoryview", list(value)])
|
||||
msg = f"Unexpected value: {value}"
|
||||
raise TypeError(msg)
|
||||
|
||||
|
||||
# convert an IndexedDB compatible entry into a Python value
|
||||
@@ -56,5 +57,6 @@ class Storage(dict):
|
||||
|
||||
async def storage(name="", storage_class=Storage):
|
||||
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}"))
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import js
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
|
||||
def as_bytearray(buffer):
|
||||
"""
|
||||
Given a JavaScript ArrayBuffer, convert it to a Python bytearray in a
|
||||
MicroPython friendly manner.
|
||||
"""
|
||||
ui8a = js.Uint8Array.new(buffer)
|
||||
size = ui8a.length
|
||||
ba = bytearray(size)
|
||||
for i in range(0, size):
|
||||
for i in range(size):
|
||||
ba[i] = ui8a[i]
|
||||
return ba
|
||||
|
||||
@@ -31,3 +37,22 @@ class NotSupported:
|
||||
|
||||
def __call__(self, *args):
|
||||
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
|
||||
# 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):
|
||||
@@ -68,6 +72,18 @@ class Element:
|
||||
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`.
|
||||
self._classes = Classes(self)
|
||||
|
||||
@@ -75,7 +91,7 @@ class Element:
|
||||
self._style = Style(self)
|
||||
|
||||
# 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):
|
||||
"""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
|
||||
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.find(key)
|
||||
|
||||
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
|
||||
# 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
|
||||
# Element instance via `for_`).
|
||||
if name.endswith("_"):
|
||||
name = name[:-1]
|
||||
|
||||
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
|
||||
return getattr(self._dom_element, name)
|
||||
|
||||
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
|
||||
# Element instance via `for_`).
|
||||
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)
|
||||
|
||||
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
|
||||
def children(self):
|
||||
"""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
|
||||
# a JS Nodelist which is handled explicitly below.
|
||||
# NodeList.
|
||||
elif isinstance(item, list) or isinstance(item, tuple):
|
||||
elif isinstance(item, (list, tuple)):
|
||||
for child in item:
|
||||
self.append(child)
|
||||
|
||||
@@ -181,10 +232,11 @@ class Element:
|
||||
|
||||
except AttributeError:
|
||||
# Nope! This is not an element or a NodeList.
|
||||
raise TypeError(
|
||||
msg = (
|
||||
f'Element "{item}" is a proxy object, "'
|
||||
f"but not a valid element or a NodeList."
|
||||
)
|
||||
raise TypeError(msg)
|
||||
|
||||
def clone(self, clone_id=None):
|
||||
"""Make a clone of the element (clones the underlying DOM object too)."""
|
||||
@@ -355,9 +407,8 @@ class Options:
|
||||
|
||||
new_option = option(**kwargs)
|
||||
|
||||
if before:
|
||||
if isinstance(before, Element):
|
||||
before = before._dom_element
|
||||
if before and isinstance(before, Element):
|
||||
before = before._dom_element
|
||||
|
||||
self._element._dom_element.add(new_option._dom_element, before)
|
||||
|
||||
@@ -417,7 +468,7 @@ class ContainerElement(Element):
|
||||
)
|
||||
|
||||
for child in list(args) + (children or []):
|
||||
if isinstance(child, Element) or isinstance(child, ElementCollection):
|
||||
if isinstance(child, (Element, ElementCollection)):
|
||||
self.append(child)
|
||||
|
||||
else:
|
||||
@@ -447,14 +498,13 @@ class ClassesCollection:
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
for class_name in self._all_class_names():
|
||||
yield class_name
|
||||
yield from self._all_class_names()
|
||||
|
||||
def __len__(self):
|
||||
return len(self._all_class_names())
|
||||
|
||||
def __repr__(self):
|
||||
return f"ClassesCollection({repr(self._collection)})"
|
||||
return f"ClassesCollection({self._collection!r})"
|
||||
|
||||
def __str__(self):
|
||||
return " ".join(self._all_class_names())
|
||||
@@ -507,7 +557,7 @@ class StyleCollection:
|
||||
element.style[key] = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"StyleCollection({repr(self._collection)})"
|
||||
return f"StyleCollection({self._collection!r})"
|
||||
|
||||
def remove(self, key):
|
||||
"""Remove a CSS property from the elements in the collection."""
|
||||
@@ -542,7 +592,7 @@ class ElementCollection:
|
||||
if isinstance(key, int):
|
||||
return self._elements[key]
|
||||
|
||||
elif isinstance(key, slice):
|
||||
if isinstance(key, slice):
|
||||
return ElementCollection(self._elements[key])
|
||||
|
||||
return self.find(key)
|
||||
@@ -1079,7 +1129,8 @@ class video(ContainerElement):
|
||||
|
||||
elif isinstance(to, Element):
|
||||
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":
|
||||
to = canvas(dom_element=to)
|
||||
@@ -1088,10 +1139,12 @@ class video(ContainerElement):
|
||||
elif isinstance(to, str):
|
||||
nodelist = document.querySelectorAll(to) # NOQA
|
||||
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":
|
||||
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])
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class EventMessage:
|
||||
return value
|
||||
|
||||
|
||||
class WebSocket(object):
|
||||
class WebSocket:
|
||||
CONNECTING = 0
|
||||
OPEN = 1
|
||||
CLOSING = 2
|
||||
|
||||
@@ -25,10 +25,12 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
||||
from json import dumps
|
||||
|
||||
if not src:
|
||||
raise ValueError("Named workers require src")
|
||||
msg = "Named workers require src"
|
||||
raise ValueError(msg)
|
||||
|
||||
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.type = type
|
||||
@@ -37,7 +39,7 @@ async def create_named_worker(src="", name="", config=None, type="py"):
|
||||
_set(s, "name", name)
|
||||
|
||||
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)
|
||||
return await workers[name]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { idb, getFileSystemDirectoryHandle } from "./fs.js";
|
||||
|
||||
export default {
|
||||
// allow pyterminal checks to bootstrap
|
||||
is_pyterminal: () => false,
|
||||
@@ -9,4 +11,21 @@ export default {
|
||||
sleep(seconds) {
|
||||
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,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
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>
|
||||
14
core/tests/javascript/mpy-error.html
Normal file
14
core/tests/javascript/mpy-error.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<script type="mpy">
|
||||
from pyscript import document
|
||||
import sys
|
||||
print("This is an error", file=sys.stderr)
|
||||
document.documentElement.classList.add("ok");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
14
core/tests/javascript/mpy-no-error.html
Normal file
14
core/tests/javascript/mpy-no-error.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<script type="mpy" config="mpy-no-error.toml">
|
||||
from pyscript import document
|
||||
import sys
|
||||
print("This is an error", file=sys.stderr)
|
||||
document.documentElement.classList.add("ok");
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
1
core/tests/javascript/mpy-no-error.toml
Normal file
1
core/tests/javascript/mpy-no-error.toml
Normal file
@@ -0,0 +1 @@
|
||||
plugins = ["!error"]
|
||||
1
core/tests/javascript/pyodide-cache/cached.toml
Normal file
1
core/tests/javascript/pyodide-cache/cached.toml
Normal file
@@ -0,0 +1 @@
|
||||
packages = ["numpy", "matplotlib"]
|
||||
41
core/tests/javascript/pyodide-cache/index.html
Normal file
41
core/tests/javascript/pyodide-cache/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module">
|
||||
import '../../../dist/core.js';
|
||||
|
||||
addEventListener('py:progress', ({ detail }) => {
|
||||
console.log(detail);
|
||||
});
|
||||
|
||||
test([]);
|
||||
|
||||
function test($) {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'py';
|
||||
script.src = 'index.py';
|
||||
script.toggleAttribute('worker', true);
|
||||
script.setAttribute('config', `${$.length ? 'cached' : 'never'}.toml`);
|
||||
script.addEventListener(
|
||||
'py:done',
|
||||
() => {
|
||||
if ($.push(performance.now() - time) === 3) {
|
||||
const [_, justCDN, lockFile] = $;
|
||||
console.log({ justCDN, lockFile });
|
||||
document.body.textContent = justCDN > lockFile ? 'OK' : 'NO';
|
||||
document.documentElement.classList.add('done');
|
||||
}
|
||||
else setTimeout(test, 0, $);
|
||||
script.remove();
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
document.body.append(script);
|
||||
const time = performance.now();
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
</html>
|
||||
6
core/tests/javascript/pyodide-cache/index.py
Normal file
6
core/tests/javascript/pyodide-cache/index.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import numpy as np
|
||||
import matplotlib as mpl
|
||||
|
||||
# just do something with the packages
|
||||
print(len(dir(np)))
|
||||
print(len(dir(mpl)))
|
||||
2
core/tests/javascript/pyodide-cache/never.toml
Normal file
2
core/tests/javascript/pyodide-cache/never.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages_cache = "never"
|
||||
packages = ["numpy", "matplotlib"]
|
||||
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>
|
||||
18
core/tests/javascript/workers/create_named/index.html
Normal file
18
core/tests/javascript/workers/create_named/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>mpy using py named worker</title>
|
||||
<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="mpy" config="../config.toml">
|
||||
from pyscript import create_named_worker
|
||||
await create_named_worker("../worker.py", name="pyodide_version", type="py")
|
||||
|
||||
from test import test
|
||||
await test("pyodide_version")
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
<!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="mpy" async>
|
||||
from pyscript import create_named_worker
|
||||
|
||||
await create_named_worker("./worker.py", name="micropython_version", type="mpy")
|
||||
</script>
|
||||
<script type="mpy" config="./config.toml" async>
|
||||
from test import test
|
||||
await test("mpy")
|
||||
</script>
|
||||
<script type="py" config="./config.toml" async>
|
||||
from test import test
|
||||
await test("py")
|
||||
</script>
|
||||
<script type="py" name="pyodide_version" worker>
|
||||
def pyodide_version():
|
||||
import sys
|
||||
return sys.version
|
||||
|
||||
__export__ = ['pyodide_version']
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
core/tests/javascript/workers/mpy/index.html
Normal file
16
core/tests/javascript/workers/mpy/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>mpy using py named worker</title>
|
||||
<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="mpy" config="../config.toml">
|
||||
from test import test
|
||||
await test("pyodide_version")
|
||||
</script>
|
||||
<script type="py" src="../worker.py" name="pyodide_version" worker></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,29 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>named workers</title>
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" async>
|
||||
from pyscript import workers
|
||||
|
||||
await (await workers["mpy"]).greetings()
|
||||
await (await workers["py"]).greetings()
|
||||
</script>
|
||||
<script type="mpy" worker name="mpy">
|
||||
def greetings():
|
||||
print("micropython")
|
||||
|
||||
__export__ = ['greetings']
|
||||
</script>
|
||||
<script type="py" worker name="py">
|
||||
def greetings():
|
||||
print("pyodide")
|
||||
|
||||
__export__ = ['greetings']
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
core/tests/javascript/workers/py/index.html
Normal file
16
core/tests/javascript/workers/py/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>py using mpy named worker</title>
|
||||
<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="../config.toml">
|
||||
from test import test
|
||||
await test("micropython_version")
|
||||
</script>
|
||||
<script type="mpy" src="../worker.py" name="micropython_version" worker></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +1,13 @@
|
||||
from pyscript import document, workers
|
||||
|
||||
|
||||
async def test(interpreter):
|
||||
# accessed as item
|
||||
named = await workers.micropython_version
|
||||
async def test(name):
|
||||
# retrieve sync utilities from the named worker
|
||||
named = await workers[name]
|
||||
|
||||
version = await named.micropython_version()
|
||||
# invoke the runtime_version __export__ + show it
|
||||
version = await named.runtime_version()
|
||||
document.body.append(version)
|
||||
document.body.append(document.createElement("hr"))
|
||||
|
||||
# accessed as attribute
|
||||
named = await workers["pyodide_version"]
|
||||
|
||||
version = await named.pyodide_version()
|
||||
document.body.append(version)
|
||||
document.body.append(document.createElement("hr"))
|
||||
|
||||
document.documentElement.classList.add(interpreter)
|
||||
# flag the expectations around name done
|
||||
document.documentElement.classList.add(name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
def micropython_version():
|
||||
def runtime_version():
|
||||
import sys
|
||||
|
||||
return sys.version
|
||||
|
||||
|
||||
__export__ = ["micropython_version"]
|
||||
__export__ = ["runtime_version"]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<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>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="mpy" worker>
|
||||
@@ -14,7 +14,7 @@ serve({
|
||||
const url = new URL(req.url);
|
||||
let { pathname } = url;
|
||||
if (pathname === '/') pathname = '/index.html';
|
||||
else if (/^\/dist\//.test(pathname)) pathname = `/../..${pathname}`;
|
||||
else if (/^\/dist\//.test(pathname)) pathname = `/../../..${pathname}`;
|
||||
else if (pathname === '/favicon.ico')
|
||||
return new Response('Not Found', { status: 404 });
|
||||
const response = new Response(file(`${dir}${pathname}`));
|
||||
@@ -95,9 +95,19 @@ test('MicroPython + JS Storage', async ({ page }) => {
|
||||
await page.waitForSelector('html.ok');
|
||||
});
|
||||
|
||||
test('MicroPython + workers', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/workers/index.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
test('MicroPython using named Pyodide Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/workers/mpy/index.html');
|
||||
await page.waitForSelector('html.pyodide_version');
|
||||
});
|
||||
|
||||
test('MicroPython creating a named Pyodide Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/workers/create_named/index.html');
|
||||
await page.waitForSelector('html.pyodide_version');
|
||||
});
|
||||
|
||||
test('Pyodide using named MicroPython Worker', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/workers/py/index.html');
|
||||
await page.waitForSelector('html.micropython_version');
|
||||
});
|
||||
|
||||
test('MicroPython Editor setup error', async ({ page }) => {
|
||||
@@ -121,3 +131,64 @@ test('Py and Mpy config["type"]', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/config_type.html');
|
||||
await page.waitForSelector('html.mpy.py');
|
||||
});
|
||||
|
||||
test('Pyodide lockFileURL vs CDN', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/pyodide-cache/');
|
||||
await page.waitForSelector('html.done');
|
||||
const body = await page.evaluate(() => document.body.textContent);
|
||||
await expect(body).toBe('OK');
|
||||
});
|
||||
|
||||
test('Pyodide pinned lockFileURL', async ({ page }) => {
|
||||
const logs = [];
|
||||
page.on('console', msg => {
|
||||
const text = msg.text();
|
||||
if (!text.startsWith('['))
|
||||
logs.push(text);
|
||||
});
|
||||
await page.goto('http://localhost:8080/tests/javascript/pyodide-lockfile/');
|
||||
await page.waitForSelector('html.done');
|
||||
let body = await page.evaluate(() => document.body.lastChild.textContent);
|
||||
await expect(body).toBe('OK');
|
||||
await expect(!!logs.splice(0).length).toBe(true);
|
||||
await page.reload();
|
||||
await page.waitForSelector('html.done');
|
||||
body = await page.evaluate(() => document.body.lastChild.textContent);
|
||||
await expect(body).toBe('OK');
|
||||
await expect(logs.splice(0).length).toBe(0);
|
||||
});
|
||||
|
||||
test('MicroPython buffered error', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy-error.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||
await expect(body).toBe('This is an error');
|
||||
});
|
||||
|
||||
test('MicroPython buffered NO error', async ({ page }) => {
|
||||
await page.goto('http://localhost:8080/tests/javascript/mpy-no-error.html');
|
||||
await page.waitForSelector('html.ok');
|
||||
const body = await page.evaluate(() => document.body.textContent.trim());
|
||||
await expect(body).toBe('');
|
||||
});
|
||||
|
||||
test('Pyodide media module', async ({ page }) => {
|
||||
await page.context().grantPermissions(['camera', 'microphone']);
|
||||
await page.context().addInitScript(() => {
|
||||
const originalEnumerateDevices = navigator.mediaDevices.enumerateDevices;
|
||||
navigator.mediaDevices.enumerateDevices = async function() {
|
||||
const realDevices = await originalEnumerateDevices.call(this);
|
||||
if (!realDevices || realDevices.length === 0) {
|
||||
return [
|
||||
{ deviceId: 'camera1', groupId: 'group1', kind: 'videoinput', label: 'Simulated Camera' },
|
||||
{ deviceId: 'mic1', groupId: 'group2', kind: 'audioinput', label: 'Simulated Microphone' }
|
||||
];
|
||||
}
|
||||
return realDevices;
|
||||
};
|
||||
});
|
||||
await page.goto('http://localhost:8080/tests/javascript/media.html');
|
||||
await page.waitForSelector('html.media-ok', { timeout: 10000 });
|
||||
const isSuccess = await page.evaluate(() => document.documentElement.classList.contains('media-ok'));
|
||||
expect(isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" src="camera.py" async></script>
|
||||
<script type="mpy" src="camera.py" async></script>
|
||||
|
||||
<label for="cars">Choose a device:</label>
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from pyodide.ffi import create_proxy
|
||||
from pyscript import display, document, when, window
|
||||
from pyweb import media, pydom
|
||||
from pyscript import display, document, media, when, window
|
||||
from pyscript.web import page
|
||||
|
||||
devicesSelect = pydom["#devices"][0]
|
||||
video = pydom["video"][0]
|
||||
devicesSelect = page["#devices"][0]
|
||||
video = page["video"][0]
|
||||
devices = {}
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ async def list_media_devices(event=None):
|
||||
async def connect_to_device(e):
|
||||
"""Connect to the selected device."""
|
||||
device = devices[devicesSelect.value]
|
||||
video._js.srcObject = await device.get_stream()
|
||||
video.srcObject = await device.get_stream()
|
||||
|
||||
|
||||
@when("click", "#snap")
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
document.body.innerHTML += `<p>${message}</p>`;
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config>name = "first"</py-config>
|
||||
@@ -9,8 +9,8 @@
|
||||
document.body.innerHTML += `<p>${message}</p>`;
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
OK
|
||||
@@ -9,8 +9,8 @@
|
||||
document.body.innerHTML += `<p>${message}</p>`;
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" config="first.toml"></script>
|
||||
@@ -9,8 +9,8 @@
|
||||
document.body.innerHTML += `<p>${message}</p>`;
|
||||
});
|
||||
</script>
|
||||
<link rel="stylesheet" href="../../dist/core.css">
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config>name = "first"</py-config>
|
||||
15
core/tests/manual/donkey/index.html
Normal file
15
core/tests/manual/donkey/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!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="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container"></div>
|
||||
<button id="run" disabled>Run</button>
|
||||
<button id="clear" disabled>Clear</button>
|
||||
<button id="kill" disabled>Kill</button>
|
||||
</body>
|
||||
</html>
|
||||
43
core/tests/manual/donkey/index.js
Normal file
43
core/tests/manual/donkey/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { donkey } from '../../../dist/core.js';
|
||||
|
||||
const runButton = document.querySelector('#run');
|
||||
const clearButton = document.querySelector('#clear');
|
||||
const killButton = document.querySelector('#kill');
|
||||
|
||||
const {
|
||||
execute, // exec(expression)
|
||||
evaluate, // eval(expression)
|
||||
process, // process(code)
|
||||
clear,
|
||||
kill,
|
||||
} = await donkey({ terminal: '#container' });
|
||||
|
||||
clearButton.onclick = async () => {
|
||||
killButton.disabled = true;
|
||||
clearButton.disabled = true;
|
||||
await clear();
|
||||
runButton.disabled = false;
|
||||
};
|
||||
killButton.onclick = () => {
|
||||
killButton.disabled = true;
|
||||
clearButton.disabled = true;
|
||||
runButton.disabled = true;
|
||||
kill();
|
||||
};
|
||||
|
||||
runButton.disabled = false;
|
||||
runButton.onclick = async () => {
|
||||
killButton.disabled = false;
|
||||
clearButton.disabled = false;
|
||||
runButton.disabled = true;
|
||||
// multiline code
|
||||
await execute(`
|
||||
a = 1 + 2
|
||||
print(f'1 + 2 = {a}')
|
||||
`);
|
||||
// single expression evaluation
|
||||
const name = await evaluate('input("what is your name? ")');
|
||||
alert(`Hello ${name}`);
|
||||
killButton.disabled = true;
|
||||
runButton.disabled = false;
|
||||
};
|
||||
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, "]")
|
||||
39
core/tests/manual/error/index.html
Normal file
39
core/tests/manual/error/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PyScript Error Bug?</title>
|
||||
<link rel="stylesheet" href="../../../dist/core.css">
|
||||
<script type="module" src="../../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-config>
|
||||
plugins = ["!error"]
|
||||
</py-config>
|
||||
<script type="py">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script>
|
||||
<!-- Attempt 2; inline config
|
||||
<script type="py" config='plugins=["!error"]'>
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<!-- Attempt 3; external pyscript.toml
|
||||
<script type="py" config="pyscript.toml">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<!-- Attempt 4; micropython
|
||||
<script type="mpy">
|
||||
import sys
|
||||
print("This is normal content")
|
||||
print("This is error content", file=sys.stderr)
|
||||
</script> -->
|
||||
<div id="result"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
core/tests/manual/error/pyscript.toml
Normal file
1
core/tests/manual/error/pyscript.toml
Normal file
@@ -0,0 +1 @@
|
||||
plugins = ["!error"]
|
||||
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>
|
||||
@@ -11,11 +11,11 @@
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<py-script>
|
||||
input("what's your name?")
|
||||
<py-script worker terminal>
|
||||
print(input("what's your name? "))
|
||||
</py-script>
|
||||
<mpy-script>
|
||||
input("what's your name?")
|
||||
<mpy-script worker terminal>
|
||||
print(input("what's your name? "))
|
||||
</mpy-script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<script type="module" src="../../dist/core.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="py" config='{"interpreter":"https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.mjs"}' worker>
|
||||
<script type="py" config='{"interpreter":"https://cdn.jsdelivr.net/pyodide/v0.26.0/full/pyodide.mjs"}' worker>
|
||||
import pyodide
|
||||
print(pyodide.__version__)
|
||||
</script>
|
||||
|
||||
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...")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user