Compare commits

...

37 Commits

Author SHA1 Message Date
pre-commit-ci[bot]
112844a742 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black-pre-commit-mirror: 25.9.0 → 25.11.0](https://github.com/psf/black-pre-commit-mirror/compare/25.9.0...25.11.0)
- [github.com/astral-sh/ruff-pre-commit: v0.13.3 → v0.14.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.3...v0.14.7)
2025-12-01 19:23:26 +00:00
Nicholas Tollervey
90ae3cea95 Rename offline zip (#2413)
* Add release calver to offline.zip

* Update release template for correct offline.zip filename.

* Update sed for global search/replace of _VERSION_

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-26 15:02:30 +00:00
Andrea Giammarchi
97f5922973 Updated release version (#2412) 2025-11-25 12:35:50 +01:00
Nicholas Tollervey
4a801fa3a2 Update publish-release GH workflow to automatically include the offline.zip asset. (#2411)
* Update publish-release GH workflow to include offline.zip asset.

* Adjust upload offline.zip to use gh CLI instead of a third party GH action.

* Update release page template to reference offline.zip.

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-25 09:37:14 +00:00
Andrea Giammarchi
7afe5c55e1 Fix #2404 - avoid throwing on string config (#2405) 2025-11-25 09:26:14 +01:00
Andrea Giammarchi
7785e17bb5 Added logic to create offline.zip artifact (#2410)
* Added logic to create offline.zip artifact

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-24 15:30:59 +01:00
Andrea Giammarchi
c37c3a1f9a Added .sh utility to generate PyScript <-> Pyodide versions map (#2406)
* Added .sh utility to generate PyScript <-> Pyodide versions map

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-11-19 15:36:20 +01:00
Andrea Giammarchi
d8250f2c8c Updated README to 2025.11.1 (#2402) 2025-11-10 11:10:39 +01:00
Andrea Giammarchi
83b41f9928 Updated dev/dependencies + Polyscript (#2400) 2025-11-10 10:48:14 +01:00
Andrea Giammarchi
a8684a2168 Updated README with latest release (#2396) 2025-10-23 16:22:57 +02:00
Andrea Giammarchi
f8cf58d6c4 Fixed FS permission handler need to be asked twice (#2395)
* Fixed FS permission handler need to be asked twice

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-23 16:14:21 +02:00
Andrea Giammarchi
8cd9c4c382 Updated to latest release version (#2394) 2025-10-23 11:01:10 +02:00
Andrea Giammarchi
1f609233e7 Fixed issue in Pyodide remote packages (#2393) 2025-10-23 10:51:17 +02:00
Andrea Giammarchi
66966a732e Updated Polyscript to its latest (#2392)
* Updated Polyscript to its latest

* forgot to update the README for the next release
2025-10-21 12:53:35 +02:00
Andrea Giammarchi
ec090922cb Fix #2372 - Allow custom TOML parser (#2390)
* Fix #2372 - Allow custom TOML parser

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-10 17:07:36 +02:00
Andrea Giammarchi
f769f215b2 Updated Pyodide to its latest (#2389) 2025-10-08 12:35:21 +02:00
Nicholas Tollervey
ffc78ab6a2 Remove superfluous code now MicroPython supports inspect API for function signature inspection. (#2387)
* Remove superfluous code now MicroPython supports inspect API for function signature inspection. 
* Added test to ensure all callables are covered.
2025-10-08 09:27:53 +01:00
Jeremy Kawahara
b609b605f5 Fix py-editor execute code on ctrl-enter (#2385)
* Fix dist path

* Remove defaultKeymap

* Return true from listener

* Put defaultKeymap after custom key map
2025-10-07 22:57:14 +02:00
Andrea Giammarchi
100a1e4bc1 Updated MicroPython one more time (#2386) 2025-10-07 10:57:29 +02:00
pre-commit-ci[bot]
c848061a44 [pre-commit.ci] pre-commit autoupdate (#2384)
updates:
- https://github.com/psf/blackhttps://github.com/psf/black-pre-commit-mirror
- [github.com/psf/black-pre-commit-mirror: 25.1.0 → 25.9.0](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...25.9.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.13.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-06 21:43:10 -04:00
Andrea Giammarchi
2647e78480 Updated polyscript to bring in latest MicroPython (#2383)
* Updated polyscript to bring in latest MicroPython

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-06 11:13:10 +02:00
Andrea Giammarchi
482d57c27c Amend on MicroPython latest (#2382) 2025-10-02 15:32:38 +02:00
Andrea Giammarchi
4ce989acf3 Updated Polyscript (#2376) 2025-10-02 13:43:24 +02:00
Andrea Giammarchi
1e62d0b1fe Follow up on autostart (#2380) 2025-09-30 15:46:37 +02:00
dependabot[bot]
2d3ad0ab2d Bump the github-actions group with 2 updates (#2378)
Bumps the github-actions group with 2 updates: [actions/setup-node](https://github.com/actions/setup-node) and [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials).


Updates `actions/setup-node` from 4 to 5
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

Updates `aws-actions/configure-aws-credentials` from 4 to 5
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-15 09:53:25 +02:00
Andrea Giammarchi
3657492c52 Simplify even further the bridge with a fallback (#2379) 2025-09-12 09:40:23 +02:00
pre-commit-ci[bot]
a8b8e1de36 [pre-commit.ci] pre-commit autoupdate (#2377)
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.12.8 → v0.12.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.8...v0.12.11)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-01 21:31:15 -04:00
Andrea Giammarchi
726009029a Updated Pyodide to its 0.28.2 version (#2374) 2025-08-21 16:51:25 -04:00
Christian Clauss
8b35304ab4 Fix undefined names in Python code (#2371)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-08-18 13:57:33 +02:00
dependabot[bot]
9e4cb44d73 Bump actions/checkout from 4 to 5 in the github-actions group (#2373)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2025-08-18 13:51:33 +02:00
Christian Clauss
4bf3651c9a pre-commit: Upgrade the Python linter ruff (#2370) 2025-08-18 13:36:36 +02:00
Andrea Giammarchi
67fa31e4ea Bumped version to 2025.8.1 (#2369) 2025-08-07 09:59:11 +02:00
Andrea Giammarchi
4937a46731 Updated Polyscript to its latest (#2364)
* Fix #2360 - Better shared env/setup handling (#2361)

* Updated Polyscript to its latest

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

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

* changed is_null to a more Pythonic is_none

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-08-06 19:23:24 +02:00
Andrea Giammarchi
b4e9a3093c Fix #2338 - Added explicit fs.revoke(path) (#2368) 2025-08-06 14:40:52 +02:00
Andrea Giammarchi
a129be8136 WebSocket and PyWorker fixes (#2366)
* WebSocket and PyWorker fixes

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-24 15:03:51 +02:00
Andrea Giammarchi
eaa6711756 Fix #2360 - Better shared env/setup handling (#2361) 2025-07-11 10:56:14 +02:00
Andrea Giammarchi
b528ba67a9 Intermediate release with async worker handler fixes (#2359) 2025-07-10 15:21:50 +02:00
54 changed files with 986 additions and 545 deletions

View File

@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20.x

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20.x
@@ -57,22 +57,32 @@ jobs:
- name: build
run: npm run build
- name: build offline
run: npm run build:offline
- name: Rename offline.zip with version metadata
run: mv ./dist/offline.zip ./dist/offline_${{ github.ref_name }}.zip
- name: Generate index.html in snapshot
working-directory: .
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' -e 's#_DOC_VERSION_#${{ github.ref_name }}#' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#' -e 's#_VERSION_#${{ github.ref_name }}#' ./public/index.html > ./core/dist/index.html
run: sed -e 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#g' -e 's#_DOC_VERSION_#${{ github.ref_name }}#g' -e 's#_TAG_VERSION_#/tag/${{ github.ref_name }}#g' -e 's#_VERSION_#${{ github.ref_name }}#g' ./public/index.html > ./core/dist/index.html
- name: Generate release.tar from snapshot and put it in dist/
working-directory: .
run: tar -cvf ../release.tar * && mv ../release.tar .
- name: Upload offline.zip to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.ref_name }} ./dist/offline_${{ github.ref_name }}.zip
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3
run:
| # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./dist/ s3://pyscript.net/latest/
| # Create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./dist/ s3://pyscript.net/releases/${{ github.ref_name }}/

View File

@@ -20,10 +20,10 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20.x
@@ -62,7 +62,7 @@ jobs:
run: npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}

View File

@@ -21,10 +21,10 @@ jobs:
working-directory: ./core
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20.x
@@ -67,7 +67,7 @@ jobs:
run: sed -e 's#_PATH_#./#' -e 's#_DOC_VERSION_#latest#' -e 's#_TAG_VERSION_##' -e 's#_VERSION_#latest#' ./public/index.html > ./core/dist/index.html
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
uses: aws-actions/configure-aws-credentials@v5
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}

View File

@@ -24,7 +24,7 @@ jobs:
MINICONDA_VERSION: 4.11.0
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 3
@@ -37,7 +37,7 @@ jobs:
run: git log --graph -3
- name: Install node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 20.x

View File

@@ -7,7 +7,7 @@ ci:
default_stages: [pre-commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-builtin-literals
- id: check-case-conflict
@@ -24,8 +24,8 @@ repos:
exclude: core/dist|\.min\.js$
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 25.1.0
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.11.0
hooks:
- id: black
exclude: core/tests
@@ -40,7 +40,7 @@ repos:
- tomli
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.8
rev: v0.14.7
hooks:
- id: ruff
exclude: core/tests

View File

@@ -13,11 +13,11 @@ Using PyScript is as simple as:
<title>PyScript!</title>
<link
rel="stylesheet"
href="https://pyscript.net/releases/2025.7.2/core.css"
href="https://pyscript.net/releases/2025.11.2/core.css"
/>
<script
type="module"
src="https://pyscript.net/releases/2025.7.2/core.js"
src="https://pyscript.net/releases/2025.11.2/core.js"
></script>
</head>
<body>

View File

@@ -21,6 +21,7 @@ def func_b():
### Options
* **pyscript**: the release version to automatically import if not already available on the page. If no version is provided the *developers' channel* version will be used instead (for developers' purposes only).
* **type**: `py` by default to bootstrap *Pyodide*.
* **worker**: `true` by default to bootstrap in a *Web Worker*.
* **config**: either a *string* or a PyScript compatible config *JS literal* to make it possible to bootstrap files and whatnot. If specified, the `worker` becomes implicitly `true` to avoid multiple configs conflicting on the main thread.
@@ -39,6 +40,7 @@ no config
The [test.js](./test/test.js) files uses the following defaults:
* `pyscript` as `"2025.8.1"`
* `type` as `"mpy"`
* `worker` as `false`
* `config` as `undefined`

View File

@@ -1,7 +1,9 @@
/*! (c) PyScript Development Team */
const { stringify } = JSON;
const { create, entries } = Object;
const { assign, create, entries } = Object;
const el = (name, props) => assign(document.createElement(name), props);
/**
* Transform a list of keys into a Python dictionary.
@@ -57,6 +59,7 @@ export default (url, {
config = null,
env = null,
serviceWorker = null,
pyscript = null,
} = {}) => {
const { protocol, host, pathname } = new URL(url);
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
@@ -88,8 +91,17 @@ export default (url, {
// create the arguments for the `dispatchEvent` call
const eventArgs = `${stringify(name)},${name}to_ts(${detail})`;
// bootstrap the script element type and its attributes
const script = document.createElement('script');
script.type = type;
const script = el('script', { type, textContent: [
'\n', code, '\n',
// this is to avoid local scope name clashing
`from pyscript import window as ${name}`,
`from pyscript.ffi import to_js as ${name}to_ts`,
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
// remove these references even if non-clashing to keep
// the local scope clean from undesired entries
`del ${name}`,
`del ${name}to_ts`,
].join('\n') });
// if config is provided it needs to be a worker to avoid
// conflicting with main config on the main thread (just like always)
@@ -102,19 +114,6 @@ export default (url, {
if (env) script.setAttribute('env', env);
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
// augment the code with the previously accessed fields at the end
script.textContent = [
'\n', code, '\n',
// this is to avoid local scope name clashing
`from pyscript import window as ${name}`,
`from pyscript.ffi import to_js as ${name}to_ts`,
`${name}.dispatchEvent(${name}.CustomEvent.new(${eventArgs}))`,
// remove these references even if non-clashing to keep
// the local scope clean from undesired entries
`del ${name}`,
`del ${name}to_ts`,
].join('\n');
// let PyScript resolve and execute this script
document.body.appendChild(script);
@@ -131,6 +130,20 @@ export default (url, {
// return a promise that will resolve only once the event
// has been emitted and the interpreter evaluated the code
const { promise, resolve } = Promise.withResolvers();
if (!(Symbol.for('@pyscript/core') in globalThis)) {
// bring in PyScript if not available already
const cdn = pyscript ?
`https://pyscript.net/releases/${pyscript}` :
// ⚠️ fallback to developers' channel !!!
'https://cdn.jsdelivr.net/npm/@pyscript/core/dist'
;
document.head.appendChild(
el('link', { rel: 'stylesheet', href: `${cdn}/core.css` }),
);
try { await import(`${cdn}/core.js`) }
catch {}
}
return promise;
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/bridge",
"version": "0.1.0",
"version": "0.2.2",
"description": "A JS based way to use PyScript modules",
"type": "module",
"module": "./index.js",
@@ -14,6 +14,10 @@
"Python",
"bridge"
],
"files": [
"index.js",
"README.md"
],
"author": "Anaconda Inc.",
"license": "APACHE-2.0",
"repository": {

View File

@@ -5,8 +5,6 @@
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PyScript Bridge</title>
<style>body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }</style>
<link rel="stylesheet" href="https://pyscript.net/releases/2025.5.1/core.css" />
<script type="module" src="https://pyscript.net/releases/2025.5.1/core.js"></script>
<!-- for local testing purpose only-->
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
<script type="module">

View File

@@ -5,6 +5,7 @@ const { searchParams } = new URL(location.href);
// the named (or default) export for test.py
export const ffi = bridge(import.meta.url, {
pyscript: "2025.8.1",
env: searchParams.get("env"),
type: searchParams.get("type") || "mpy",
worker: searchParams.has("worker"),

736
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.6.63",
"version": "0.7.11",
"type": "module",
"description": "PyScript",
"module": "./index.js",
@@ -42,12 +42,13 @@
},
"scripts": {
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .",
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:integration; fi",
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && npm run build:tests-index && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run test:integration; fi",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:flatted": "node rollup/flatted.cjs",
"build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs",
"build:offline": "node rollup/offline.cjs | bash",
"build:tests-index": "node rollup/build_test_index.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
"test:integration": "npm run test:ws; static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; (playwright test tests/js_tests.spec.js && 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",
@@ -70,37 +71,37 @@
"@webreflection/utils": "^0.1.1",
"add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6",
"polyscript": "^0.17.34",
"polyscript": "^0.20.0",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@codemirror/commands": "^6.8.1",
"@codemirror/commands": "^6.10.0",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.11.2",
"@codemirror/language": "^6.11.3",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.0",
"@playwright/test": "^1.53.2",
"@rollup/plugin-commonjs": "^28.0.6",
"@rollup/plugin-node-resolve": "^16.0.1",
"@codemirror/view": "^6.38.8",
"@playwright/test": "^1.56.1",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.4",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"bun": "^1.2.17",
"bun": "^1.3.3",
"chokidar": "^4.0.3",
"codedent": "^0.1.2",
"codemirror": "^6.0.2",
"eslint": "^9.30.0",
"eslint": "^9.39.1",
"flatted": "^3.3.3",
"rollup": "^4.44.1",
"rollup": "^4.53.3",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3",
"string-width": "^7.2.0",
"typescript": "^5.8.3",
"string-width": "^8.1.0",
"typescript": "^5.9.3",
"xterm-readline": "^1.1.2"
},
"repository": {

48
core/pyodide.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# This script assumes the following folder structure:
# ./pyscript - it must be a GitHub clone/fork
# ./polyscript - it must be a GitHub clone/fork
#
# Running from ./pyscript/core via:
#
# cd ./pyscript/core
# bash ./pyodide.sh
#
# will print a JSON compatible string like:
#
# {
# "2024.10.1": "0.26.2",
# ...
# "2025.11.1": "0.29.0",
# "": null
# }
#
# Each key represents the PyScript release and each
# value represents the Pyodide version used by that PyScript release.
#
# The last empty key with `null` value is used just to close the JSON object.
# One could remove manually that entry as long as there are no dangling commas.
#
current_pyscript=$(git branch | grep \\* | cut -d ' ' -f2)
echo "{"
for release in $(git tag --list --sort=version:refname); do
git checkout ${release} > /dev/null 2>&1
if test -e "package.json"; then
polyscript=$(cat package.json | jq -r '.dependencies.polyscript')
tag="v${polyscript:1:${#polyscript}-1}"
cd ../../polyscript > /dev/null 2>&1
current_polyscript=$(git branch | grep \\* | cut -d ' ' -f2)
git checkout ${tag} > /dev/null 2>&1
if test -e "versions/pyodide"; then
echo " \"${release}\": \"$(cat versions/pyodide)\","
fi
git checkout ${current_polyscript} > /dev/null 2>&1
cd - > /dev/null 2>&1
fi
git checkout ${current_pyscript} > /dev/null 2>&1
done
echo " \"\": null"
echo "}"

79
core/rollup/offline.cjs Normal file
View File

@@ -0,0 +1,79 @@
const { readFileSync, writeFileSync } = require("node:fs");
const { join, resolve } = require("node:path");
const versions = resolve(
__dirname,
"..",
"node_modules",
"polyscript",
"versions",
);
let pyodide = String(readFileSync(join(versions, "pyodide"), "utf8")).trim();
let micropython = String(
readFileSync(join(versions, "micropython"), "utf8"),
).trim();
writeFileSync(
join(process.cwd(), "offline.html"),
`<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript Offline</title>
<script src="./mini-coi-fd.js"></script>
<script type="module" src="./pyscript/core.js" offline></script>
<link rel="stylesheet" href="./pyscript/core.css">
</head>
<body>
<script type="mpy">
from pyscript import document
document.body.append("MicroPython Offline", document.createElement("hr"))
</script>
<script type="py" worker>
from pyscript import document
document.body.append("Pyodide Offline")
</script>
</body>
</html>
`,
"utf8",
);
let bash = `#!/usr/bin/env bash
rm -rf dist/offline
mkdir -p dist/offline/node_modules
echo '{"dependencies":{"pyodide":"${pyodide}","@micropython/micropython-webassembly-pyscript":"${micropython}"}}' > dist/offline/package.json
cd dist/offline
curl -sLO https://raw.githubusercontent.com/WebReflection/mini-coi/refs/heads/main/mini-coi-fd.js
npm i
cd -
mkdir -p dist/offline/pyscript/pyodide
cd dist/offline/pyscript/pyodide
cp ../../node_modules/pyodide/pyodide* ./
cp ../../node_modules/pyodide/python_stdlib.zip ./
cd -
mkdir -p dist/offline/pyscript/micropython
cd dist/offline/pyscript/micropython
cp ../../node_modules/@micropython/micropython-webassembly-pyscript/micropython.* ./
cd -
rm -rf dist/offline/node_modules
rm -rf dist/offline/*.json
mv offline.html dist/offline/index.html
cp dist/*.* dist/offline/pyscript/
rm -f dist/offline/pyscript/offline.zip
cd dist
zip -r offline.zip offline
rm -rf offline
cd -
`;
console.log(bash);

View File

@@ -1,5 +1,5 @@
/**
* Minified by jsDelivr using clean-css v5.3.2.
* Minified by jsDelivr using clean-css v5.3.3.
* 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

View File

@@ -70,6 +70,7 @@ for (const [TYPE] of TYPES) {
let config,
type,
parser,
pyElement,
pyConfigs = $$(`${TYPE}-config`),
attrConfigs = $$(
@@ -92,9 +93,11 @@ for (const [TYPE] of TYPES) {
[pyElement] = pyConfigs;
config = pyElement.getAttribute("src") || pyElement.textContent;
type = pyElement.getAttribute("type");
parser = pyElement.getAttribute("config-parser");
} else if (attrConfigs.length) {
[pyElement, ...attrConfigs] = attrConfigs;
config = pyElement.getAttribute("config");
parser = pyElement.getAttribute("config-parser");
// throw an error if dirrent scripts use different configs
if (
attrConfigs.some((el) => el.getAttribute("config") !== config)
@@ -120,9 +123,12 @@ for (const [TYPE] of TYPES) {
}
} else if (toml || type === "toml") {
try {
const { parse } = await import(
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
const module = parser
? await import(parser)
: await import(
/* webpackIgnore: true */ "./3rd-party/toml.js"
);
const parse = module.parse || module.default;
parsed = parse(text);
} catch (e) {
error = syntaxError("TOML", url, e);

View File

@@ -117,7 +117,7 @@ for (const [TYPE, interpreter] of TYPES) {
else dispatch(element, TYPE, "done");
};
const { config, configURL, plugins, error } = configs.get(TYPE);
let { config, configURL, plugins, error } = configs.get(TYPE);
// create a unique identifier when/if needed
let id = 0;
@@ -311,13 +311,24 @@ for (const [TYPE, interpreter] of TYPES) {
hooked.set(TYPE, hooks);
// allow offline interpreter detection via [offline] attribute
let version = offline_interpreter(config);
if (!version) {
const css = "script[type='module'][offline]";
const s = document.querySelector(css)?.src;
if (s && import.meta.url.startsWith(s.replace(/\.js$/, ""))) {
version = `./pyscript/${interpreter}/${interpreter}.mjs`;
version = offline_interpreter({ interpreter: version });
}
}
define(TYPE, {
config,
configURL,
interpreter,
hooks,
version,
env: `${TYPE}-script`,
version: offline_interpreter(config),
onerror(error, element) {
errors.set(element, error);
},

View File

@@ -38,7 +38,7 @@ const getRelatedScript = (target, type) => {
return editor?.parentNode?.previousElementSibling;
};
async function execute({ currentTarget }) {
async function execute({ currentTarget, script }) {
const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget;
@@ -91,14 +91,13 @@ async function execute({ currentTarget }) {
// 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;
}
script = getRelatedScript(currentTarget, type);
if (script) break;
}
}
defineProperties(script, { xworker: { value: xworker } });
const { sync } = xworker;
const { promise, resolve } = withResolvers();
envs.set(env, promise);
@@ -157,6 +156,20 @@ async function execute({ currentTarget }) {
});
}
const replaceScript = (script, type) => {
script.xworker?.terminate();
const clone = script.cloneNode(true);
clone.type = `${type}-editor`;
const editor = editors.get(script);
if (editor) {
const content = editor.state.doc.toString();
clone.textContent = content;
editors.delete(script);
script.nextElementSibling.remove();
}
script.replaceWith(clone);
};
const makeRunButton = (handler, type) => {
const runButton = document.createElement("button");
runButton.className = `absolute ${type}-editor-run-button`;
@@ -169,15 +182,25 @@ const makeRunButton = (handler, type) => {
) {
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);
const env = script.getAttribute("env");
// remove the bootstrapped env which could be one or shared
if (env) {
for (const [key, value] of TYPES) {
if (key === type) {
configs.delete(`${value}-${env}`);
envs.delete(`${value}-${env}`);
break;
}
}
}
// lonley script without setup node should be replaced
if (script.xworker) replaceScript(script, type);
// all scripts sharing the same env should be replaced
else {
const sel = `script[type^="${type}-editor"][env="${env}"]`;
for (const script of document.querySelectorAll(sel))
replaceScript(script, type);
}
}
return;
}
@@ -365,7 +388,7 @@ const init = async (script, type, interpreter) => {
};
if (isSetup) {
await context.handleEvent({ currentTarget: null });
await context.handleEvent({ currentTarget: null, script });
notifyEditor();
return;
}
@@ -403,16 +426,17 @@ const init = async (script, type, interpreter) => {
// preserve user indentation, if any
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
const listener = () => runButton.click();
const listener = () => !runButton.click();
const editor = new EditorView({
extensions: [
indentUnit.of(indentation),
new Compartment().of(python()),
keymap.of([
...defaultKeymap,
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
{ key: "Cmd-Enter", run: listener, preventDefault: true },
{ key: "Shift-Enter", run: listener, preventDefault: true },
// Consider removing defaultKeymap as likely redundant with basicSetup
...defaultKeymap,
// @see https://codemirror.net/examples/tab/
indentWithTab,
]),

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@ import io
import re
from pyscript.magic_js import current_target, document, window
from pyscript.ffi import is_none
_MIME_METHODS = {
"savefig": "image/png",
@@ -105,13 +106,13 @@ def _format_mime(obj):
else:
output = _eval_formatter(obj, method)
if output is None:
if is_none(output):
continue
if mime_type not in _MIME_RENDERERS:
not_available.append(mime_type)
continue
break
if output is None:
if is_none(output):
if not_available:
window.console.warn(
f"Rendered object requested unavailable MIME renderers: {not_available}"
@@ -135,7 +136,7 @@ def _write(element, value, append=False):
element.append(out_element)
else:
out_element = element.lastElementChild
if out_element is None:
if is_none(out_element):
out_element = element
if mime_type in ("application/javascript", "text/html"):
@@ -146,7 +147,7 @@ def _write(element, value, append=False):
def display(*values, target=None, append=True):
if target is None:
if is_none(target):
target = current_target()
elif not isinstance(target, str):
msg = f"target must be str or None, not {target.__class__.__name__}"
@@ -162,7 +163,7 @@ def display(*values, target=None, append=True):
element = document.getElementById(target)
# If target cannot be found on the page, a ValueError is raised
if element is None:
if is_none(element):
msg = f"Invalid selector with id={target}. Cannot be found in the page."
raise ValueError(msg)

View File

@@ -92,60 +92,26 @@ def when(target, *args, **kwargs):
elements = selector if isinstance(selector, list) else [selector]
def decorator(func):
if config["type"] == "mpy": # Is MicroPython?
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):
"""
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
return await func()
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()
return func()
wrapper = wraps(func)(wrapper)
if isinstance(target, Event):

View File

@@ -2,8 +2,10 @@ try:
import js
from pyodide.ffi import create_proxy as _cp
from pyodide.ffi import to_js as _py_tjs
from pyodide.ffi import jsnull
from_entries = js.Object.fromEntries
is_none = lambda value: value is None or value is jsnull
def _tjs(value, **kw):
if not hasattr(kw, "dict_converter"):
@@ -13,6 +15,10 @@ try:
except:
from jsffi import create_proxy as _cp
from jsffi import to_js as _tjs
import js
jsnull = js.Object.getPrototypeOf(js.Object.prototype)
is_none = lambda value: value is None or value is jsnull
create_proxy = _cp
to_js = _tjs

View File

@@ -1,6 +1,13 @@
mounted = {}
async def get_handler(details):
handler = details.handler
options = details.options
permission = await handler.queryPermission(options)
return handler if permission == "granted" else None
async def mount(path, mode="readwrite", root="", id="pyscript"):
import js
from _pyscript import fs, interpreter
@@ -12,6 +19,7 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
js.console.warn("experimental pyscript.fs ⚠️")
details = None
handler = None
uid = f"{path}@{id}"
@@ -31,9 +39,16 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
if success:
from polyscript import IDBMap
from pyscript import window
idbm = IDBMap.new(fs.NAMESPACE)
details = await idbm.get(uid)
handler = await get_handler(details)
if handler is None:
# force await in either async or sync scenario
await js.Promise.resolve(sync.getFSHandler(details.options))
handler = details.handler
idb = IDBMap.new(fs.NAMESPACE)
handler = await idb.get(uid)
else:
raise RuntimeError(fs.ERROR)
@@ -41,14 +56,41 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
success = await fs.idb.has(uid)
if success:
handler = await fs.idb.get(uid)
details = await fs.idb.get(uid)
handler = await get_handler(details)
if handler is None:
handler = await fs.getFileSystemDirectoryHandle(details.options)
else:
handler = await fs.getFileSystemDirectoryHandle(to_js(options))
await fs.idb.set(uid, handler)
js_options = to_js(options)
handler = await fs.getFileSystemDirectoryHandle(js_options)
details = {"handler": handler, "options": js_options}
await fs.idb.set(uid, to_js(details))
mounted[path] = await interpreter.mountNativeFS(path, handler)
async def revoke(path, id="pyscript"):
from _pyscript import fs, interpreter
from pyscript.magic_js import (
RUNNING_IN_WORKER,
sync,
)
uid = f"{path}@{id}"
if RUNNING_IN_WORKER:
had = sync.deleteFSHandler(uid)
else:
had = await fs.idb.has(uid)
if had:
had = await fs.idb.delete(uid)
if had:
interpreter._module.FS.unmount(path)
return had
async def sync(path):
await mounted[path].syncfs()

View File

@@ -10,6 +10,9 @@ RUNNING_IN_WORKER = not hasattr(globalThis, "document")
config = json.loads(globalThis.JSON.stringify(_config))
if isinstance(config, str):
config = {}
if "MicroPython" in sys.version:
config["type"] = "mpy"
else:
@@ -67,7 +70,11 @@ if RUNNING_IN_WORKER:
else:
import _pyscript
from _pyscript import PyWorker, js_import
from _pyscript import PyWorker as _PyWorker, js_import
from pyscript.ffi import to_js
def PyWorker(url, **kw):
return _PyWorker(url, to_js(kw))
window = globalThis
document = globalThis.document

View File

@@ -1,11 +1,12 @@
from polyscript import storage as _storage
from pyscript.flatted import parse as _parse
from pyscript.flatted import stringify as _stringify
from pyscript.ffi import is_none
# convert a Python value into an IndexedDB compatible entry
def _to_idb(value):
if value is None:
if is_none(value):
return _stringify(["null", 0])
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value])

View File

@@ -6,7 +6,7 @@
# 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
from pyscript.ffi import create_proxy, is_none
def wrap_dom_element(dom_element):
@@ -68,8 +68,10 @@ class Element:
If `dom_element` is None we are being called to *create* a new element.
Otherwise, we are being called to *wrap* an existing DOM element.
"""
self._dom_element = dom_element or document.createElement(
type(self).get_tag_name()
self._dom_element = (
document.createElement(type(self).get_tag_name())
if is_none(dom_element)
else dom_element
)
# HTML on_events attached to the element become pyscript.Event instances.
@@ -195,7 +197,7 @@ class Element:
@property
def parent(self):
"""Return the element's `parent `Element`."""
if self._dom_element.parentElement is None:
if is_none(self._dom_element.parentElement):
return None
return Element.wrap_dom_element(self._dom_element.parentElement)
@@ -1134,7 +1136,7 @@ class video(ContainerElement):
width = width if width is not None else self.videoWidth
height = height if height is not None else self.videoHeight
if to is None:
if is_none(to):
to = canvas(width=width, height=height)
elif isinstance(to, Element):

View File

@@ -1,6 +1,6 @@
import js
from pyscript.ffi import create_proxy
from pyscript.util import as_bytearray
from pyscript.util import as_bytearray, is_awaitable
code = "code"
protocols = "protocols"
@@ -8,6 +8,23 @@ reason = "reason"
methods = ["onclose", "onerror", "onmessage", "onopen"]
def add_listener(socket, onevent, listener):
p = create_proxy(listener)
if is_awaitable(listener):
async def wrapper(e):
await p(EventMessage(e))
m = wrapper
else:
m = lambda e: p(EventMessage(e))
# Pyodide fails at setting socket[onevent] directly
setattr(socket, onevent, m)
class EventMessage:
def __init__(self, event):
self._event = event
@@ -36,20 +53,20 @@ class WebSocket:
socket = js.WebSocket.new(url, kw[protocols])
else:
socket = js.WebSocket.new(url)
socket.binaryType = "arraybuffer"
object.__setattr__(self, "_ws", socket)
for t in methods:
if t in kw:
# Pyodide fails at setting socket[t] directly
setattr(socket, t, create_proxy(kw[t]))
add_listener(socket, t, kw[t])
def __getattr__(self, attr):
return getattr(self._ws, attr)
def __setattr__(self, attr, value):
if attr in methods:
m = lambda e: value(EventMessage(e))
setattr(self._ws, attr, create_proxy(m))
add_listener(self._ws, attr, value)
else:
setattr(self._ws, attr, value)

View File

@@ -12,20 +12,33 @@ export default {
return new Promise(($) => setTimeout($, seconds * 1000));
},
getFSHandler: (options) => getFileSystemDirectoryHandle(options),
/**
* 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}
* @returns {Promise<boolean>}
*/
async storeFSHandler(uid, options = {}) {
if (await idb.has(uid)) return true;
return getFileSystemDirectoryHandle(options).then(
async (handler) => {
await idb.set(uid, handler);
await idb.set(uid, { handler, options });
return true;
},
() => false,
);
},
/**
* Explicitly remove the unique identifier for the FS handler.
* @param {string} uid
* @returns {Promise<boolean>}
*/
async deleteFSHandler(uid) {
const had = await idb.has(uid);
if (had) await idb.delete(uid);
return had;
},
};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
print("OK")

View File

@@ -0,0 +1,21 @@
<!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>
<py-config config-parser="https://esm.run/basic-toml">
[files]
'file.py' = ""
</py-config>
<script type="py">
import file
from pyscript import document
document.documentElement.classList.add("done")
document.body.append("OK")
</script>
</body>
</html>

View File

@@ -2,7 +2,7 @@ from pyscript import document
classList = document.documentElement.classList
if not __terminal__:
if not __terminal__: # noqa: F821 __terminal__ is defined in core/src/plugins/donkey.js
classList.add("error")
else:
classList.add("ok")

View File

@@ -0,0 +1,3 @@
{
"packages": ["numpy"]
}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import PyWorker
worker = PyWorker("./worker.py", type="pyodide", config="./config.json")
await worker.ready
</script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import numpy
from pyscript import document
document.body.append(numpy.__version__)
document.documentElement.classList.add("done")

View File

@@ -14,7 +14,7 @@
print(event.type)
ws.send("hello")
def onmessage(event):
async def onmessage(event):
print(event.type, event.data)
ws.close()

View File

@@ -2,6 +2,18 @@ import { test, expect } from '@playwright/test';
test.setTimeout(120 * 1000);
test('Worker config as relative URL', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/worker-config/index.html');
await page.waitForSelector('html.done');
});
test('config-parser custom TOML', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/config-parser/index.html');
await page.waitForSelector('html.done');
const body = await page.evaluate(() => document.body.innerText);
await expect(body.trim()).toBe('OK');
});
test('MicroPython display', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/mpy.html');
await page.waitForSelector('html.done.worker');

View File

@@ -19,6 +19,8 @@ if TEST == "implicit":
await fs.sync("/persistent")
# await fs.revoke("/persistent")
elif not RUNNING_IN_WORKER:
from pyscript import document
@@ -39,7 +41,7 @@ elif not RUNNING_IN_WORKER:
js.alert("unable to grant access")
async def unmount(event):
await fs.unmount("/persistent")
await fs.revoke("/persistent")
button.textContent = "mount"
button.onclick = mount

View File

@@ -318,16 +318,12 @@ async def main(winstyle=0):
if not fullscreen:
print("Changing to FULLSCREEN")
screen_backup = screen.copy()
screen = pygame.display.set_mode(
SCREENRECT.size, winstyle | pygame.FULLSCREEN, bestdepth
)
screen = pygame.display.set_mode(SCREENRECT.size, winstyle | pygame.FULLSCREEN)
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 = pygame.display.set_mode(SCREENRECT.size, winstyle)
screen.blit(screen_backup, (0, 0))
pygame.display.flip()
fullscreen = not fullscreen

View File

@@ -140,7 +140,7 @@ def get_stats_gl(renderer):
def bg_from_v(*vertices):
geometry = new(THREE.BufferGeometry)
vertices_f32a = new(Float32Array, vertices)
vertices_f32a = new(Float32Array, vertices) # noqa: F821 Float32Array is defined in js
attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
return geometry.setAttribute('position', attr)

View File

@@ -10,7 +10,7 @@ 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 libthree import line2, linemat, lsgeo
from libfft import BeatSync
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy

View File

@@ -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="py-editor">

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>PyScript Custom Package</title>
<style>
body {
font-family: system-ui, sans-serif;
}
</style>
<script>
const track = ({ detail }) => {
if (/^(Loading|Loaded)/.test(detail)) {
const state = RegExp.$1;
const what = detail.slice(state.length + 1);
if (state === 'Loading') console.time(what);
else console.timeEnd(what);
}
};
addEventListener("py:progress", track);
addEventListener("mpy:progress", track);
</script>
<!-- PyScript (locally) -->
<link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<div>loading ...</div>
<script type="py" config="./package.toml">
import custom_package
</script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
experimental_remote_packages = true
packages = ['https://webreflection.github.io/examples/pyscript/module/package.toml']

View File

@@ -358,3 +358,79 @@ def test_when_called_with_an_event_and_handler():
# The function should have been called when the whenable object was
# triggered.
assert counter == 1
def test_when_on_different_callables():
"""
The when function works with:
* Synchronous functions
* Asynchronous functions
* Inner functions
* Async inner functions
* Closure functions
* Async closure functions
"""
def func(x=1):
# A simple function.
return x
async def a_func(x=1):
# A simple async function.
return x
def make_inner_func():
# Creates a simple inner function.
def inner_func(x=1):
return x
return inner_func
def make_inner_a_func():
# Creates a simple async inner function.
async def inner_a_func(x=1):
return x
return inner_a_func
def make_closure():
# Creates a simple closure function.
a = 1
def closure_func(x=1):
return a + x
return closure_func
def make_a_closure():
# Creates a simple async closure function.
a = 1
async def closure_a_func(x=1):
return a + x
return closure_a_func
inner_func = make_inner_func()
inner_a_func = make_inner_a_func()
cl_func = make_closure()
cl_a_func = make_a_closure()
whenable = Event()
# Each of these should work with the when function.
when(whenable, func)
when(whenable, a_func)
when(whenable, inner_func)
when(whenable, inner_a_func)
when(whenable, cl_func)
when(whenable, cl_a_func)
# If we get here, everything worked.
assert True

View File

@@ -32,7 +32,7 @@ def test_js_module_is_available_on_worker():
@upytest.skip("Worker only.", skip_when=not RUNNING_IN_WORKER)
def test_js_module_is_available_on_worker():
def test_js_module_is_available_on_greeting_worker():
"""
The "hello" function in the example_js_worker_module.js file is available
via the js_modules object while running in a worker.

View File

@@ -2,7 +2,6 @@
Tests for the PyScript media module.
"""
from pyscript import media
import upytest
from pyscript import media

View File

@@ -11,7 +11,7 @@ def test_as_bytearray():
msg = b"Hello, world!"
buffer = js.ArrayBuffer.new(len(msg))
ui8a = js.Uint8Array.new(buffer)
for b in msg:
for i, b in enumerate(msg):
ui8a[i] = b
ba = util.as_bytearray(buffer)
assert isinstance(ba, bytearray)

View File

@@ -63,5 +63,5 @@ declare const exportedHooks: {
};
};
declare const exportedConfig: {};
declare const exportedWhenDefined: any;
declare const exportedWhenDefined: (type: string) => Promise<object>;
export { codemirror, stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };

10
core/types/sync.d.ts vendored
View File

@@ -9,12 +9,18 @@ declare namespace _default {
* 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}
* @returns {Promise<boolean>}
*/
function storeFSHandler(uid: string, options?: {
id?: string;
mode?: "read" | "readwrite";
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
}): boolean;
}): Promise<boolean>;
/**
* Explicitly remove the unique identifier for the FS handler.
* @param {string} uid
* @returns {Promise<boolean>}
*/
function deleteFSHandler(uid: string): Promise<boolean>;
}
export default _default;

View File

@@ -84,6 +84,10 @@
<li><a href="core.css">core.css</a></li>
<li><a href="core.js">core.js</a></li>
<li><a href="core.js.map">core.js.map</a></li>
<li>
<a href="offline__VERSION_.zip">offline__VERSION_.zip</a>
(PyScript _VERSION_ zipped up for offline use)
</li>
</ul>
<div id="out"></div>
<script type="py">
@@ -121,8 +125,9 @@
d="M43.3,6h-1.6c-0.5,0-0.8,0.3-0.8,0.8V10c0,3.5-2.8,6.4-6.3,6.4H17.4c-3.5,0-6.3-2.9-6.3-6.4V6.8
c0-0.5-0.3-0.8-0.8-0.8H8.7C6.1,6,4,8.2,4,10.8v34.4C4,47.8,6.1,50,8.7,50h34.6c2.6,0,4.7-2.2,4.7-4.8V10.8C48,8.2,45.9,6,43.3,6z"
/>
</g></svg
></a>
</g>
</svg>
</a>
</h2>
<pre id="example">
&lt;!DOCTYPE html&gt;

View File

@@ -5,5 +5,5 @@ skip = "*.js,*.json"
[tool.ruff]
line-length = 114
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"]
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "F811", "F821"]
lint.ignore = ["E402", "E722", "E731", "E741", "F401", "F704", "PLC0415"]
lint.mccabe.max-complexity = 27