Compare commits

...

36 Commits

Author SHA1 Message Date
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
Andrea Giammarchi
71ad1a40cb Update Polyscript with latest Micropython (#2357) 2025-07-04 09:34:12 +02:00
Andrea Giammarchi
e433275938 Readme update (#2356)
* Updated the README with latest PyScript 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-07-01 14:01:51 +02:00
Andrea Giammarchi
87256a662b Updated Polyscript to its latest (#2355)
* Updated Polyscript to its latest
* added tests for `experimental_ffi_timeout`
2025-07-01 13:07:28 +02:00
Andrea Giammarchi
7336ae545e The PyScript Bridge Helper (#2353)
* The PyScript Bridge Helper

* added importmap to test latest versions with ease
2025-06-26 12:41:29 +02:00
Nicholas Tollervey
d68260c0c7 Fix a bug in <label> handling where 'for_' attribute should be 'htmlFor' on underlying HTML element. (#2352)
* Fix bug in label handling where 'for_' attribute should be 'htmlFor' on underlying HTML element.

* Fix comment.
2025-06-18 15:01:33 +01:00
Nicholas Tollervey
14cc05fb80 Fix code example problem in the release HTML. (#2345) 2025-05-21 16:27:27 +01:00
58 changed files with 1297 additions and 587 deletions

View File

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

View File

@@ -16,10 +16,10 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install node - name: Install node
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 20.x node-version: 20.x
@@ -66,7 +66,7 @@ jobs:
run: tar -cvf ../release.tar * && mv ../release.tar . run: tar -cvf ../release.tar * && mv ../release.tar .
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v5
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }} role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}

View File

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

View File

@@ -21,10 +21,10 @@ jobs:
working-directory: ./core working-directory: ./core
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install node - name: Install node
uses: actions/setup-node@v4 uses: actions/setup-node@v5
with: with:
node-version: 20.x 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 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 - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v5
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }} role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
ISSUE_TEMPLATE ISSUE_TEMPLATE
*.min.* *.min.*
package-lock.json package-lock.json
bridge/

View File

@@ -13,15 +13,15 @@ Using PyScript is as simple as:
<title>PyScript!</title> <title>PyScript!</title>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://pyscript.net/snapshots/2024.9.2/core.css" href="https://pyscript.net/releases/2025.11.1/core.css"
/> />
<script <script
type="module" type="module"
src="https://pyscript.net/snapshots/2024.9.2/core.js" src="https://pyscript.net/releases/2025.11.1/core.js"
></script> ></script>
</head> </head>
<body> <body>
<!-- Use MicroPython to evaluate some Python --> <!-- type mpy (MicroPython) or py (Pyodide) to run some Python -->
<script type="mpy" terminal> <script type="mpy" terminal>
print("Hello, world!") print("Hello, world!")
</script> </script>

59
bridge/README.md Normal file
View File

@@ -0,0 +1,59 @@
# @pyscript/bridge
Import Python utilities directly in JS
```js
// main thread
const { ffi: { func_a, func_b } } = await import('./test.js');
// test.js
import bridge from 'https://esm.run/@pyscript/bridge';
export const ffi = bridge(import.meta.url, { type: 'mpy', worker: false });
// test.py
def func_a(value):
print(f"hello {value}")
def func_b():
import sys
return sys.version
```
### 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.
* **env**: to share the same environment across multiple modules loaded at different times.
## Tests
Run `npx mini-coi .` within this folder to then reach out `http://localhost:8080/test/` that will show:
```
PyScript Bridge
------------------
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`
* `env` as `undefined`
To test any variant use query string parameters so that `?type=py` will use `py` instead, `worker` will use a worker and `config` will use a basic *config* that brings in another file from the same folder which exposes the version.
To recap: `http://localhost:8080/test/?type=py&worker&config` will show this instead:
```
PyScript Bridge
------------------
3.12.7 (main, May 15 2025, 18:47:24) ...
```
Please note when a *config* is used, the `worker` attribute is always `true`.

163
bridge/index.js Normal file
View File

@@ -0,0 +1,163 @@
/*! (c) PyScript Development Team */
const { stringify } = JSON;
const { assign, create, entries } = Object;
const el = (name, props) => assign(document.createElement(name), props);
/**
* Transform a list of keys into a Python dictionary.
* `['a', 'b']` => `{ "a": a, "b": b }`
* @param {Iterable<string>} keys
* @returns {string}
*/
const dictionary = keys => {
const fields = [];
for (const key of keys)
fields.push(`${stringify(key)}: ${key}`);
return `{ ${fields.join(',')} }`;
};
/**
* Resolve properly config files relative URLs.
* @param {string|Object} config - The configuration to normalize.
* @param {string} base - The base URL to resolve relative URLs against.
* @returns {string} - The JSON serialized config.
*/
const normalize = async (config, base) => {
if (typeof config === 'string') {
base = config;
config = await fetch(config).then(res => res.json());
}
if (typeof config.files === 'object') {
const files = {};
for (const [key, value] of entries(config.files)) {
files[key.startsWith('{') ? key : new URL(key, base)] = value;
}
config.files = files;
}
return stringify(config);
};
// this logic is based on a 3 levels cache ...
const cache = new Map;
/**
* Return a bridge to a Python module via a `.js` file that has a `.py` alter ego.
* @param {string} url - The URL of the JS module that has a Python counterpart.
* @param {Object} options - The options for the bridge.
* @param {string} [options.type='py'] - The `py` or `mpy` interpreter type, `py` by default.
* @param {boolean} [options.worker=true] - Whether to use a worker, `true` by default.
* @param {string|Object} [options.config=null] - The configuration for the bridge, `null` by default.
* @param {string} [options.env=null] - The optional shared environment to use.
* @param {string} [options.serviceWorker=null] - The optional service worker to use as fallback.
* @returns {Object} - The bridge to the Python module.
*/
export default (url, {
type = 'py',
worker = true,
config = null,
env = null,
serviceWorker = null,
pyscript = null,
} = {}) => {
const { protocol, host, pathname } = new URL(url);
const py = pathname.replace(/\.m?js(?:\/\+\w+)?$/, '.py');
const file = `${protocol}//${host}${py}`;
// the first cache is about the desired file in the wild ...
if (!cache.has(file)) {
// the second cache is about all fields one needs to access out there
const exports = new Map;
let python;
cache.set(file, new Proxy(create(null), {
get(_, field) {
if (!exports.has(field)) {
// create an async callback once and always return the same later on
exports.set(field, async (...args) => {
// the third cache is about reaching lazily the code only once
// augmenting its content with exports once and drop it on done
if (!python) {
// do not await or multiple calls will fetch multiple times
// just assign the fetch `Promise` once and return it
python = fetch(file).then(async response => {
const code = await response.text();
// create a unique identifier for the Python context
const identifier = pathname.replace(/[^a-zA-Z0-9_]/g, '');
const name = `__pyscript_${identifier}${Date.now()}`;
// create a Python dictionary with all accessed fields
const detail = `{"detail":${dictionary(exports.keys())}}`;
// 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 = 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)
script.toggleAttribute('worker', !!config || !!worker);
if (config) {
const attribute = await normalize(config, file);
script.setAttribute('config', attribute);
}
if (env) script.setAttribute('env', env);
if (serviceWorker) script.setAttribute('service-worker', serviceWorker);
// let PyScript resolve and execute this script
document.body.appendChild(script);
// intercept once the unique event identifier with all exports
globalThis.addEventListener(
name,
event => {
resolve(event.detail);
script.remove();
},
{ once: true }
);
// 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;
});
}
// return the `Promise` that will after invoke the exported field
return python.then(foreign => foreign[field](...args));
});
}
// return the lazily to be resolved once callback to invoke
return exports.get(field);
}
}));
}
return cache.get(file);
};

31
bridge/package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "@pyscript/bridge",
"version": "0.2.2",
"description": "A JS based way to use PyScript modules",
"type": "module",
"module": "./index.js",
"unpkg": "./index.js",
"jsdelivr": "./jsdelivr.js",
"browser": "./index.js",
"main": "./index.js",
"keywords": [
"PyScript",
"JS",
"Python",
"bridge"
],
"files": [
"index.js",
"README.md"
],
"author": "Anaconda Inc.",
"license": "APACHE-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/pyscript/pyscript.git"
},
"bugs": {
"url": "https://github.com/pyscript/pyscript/issues"
},
"homepage": "https://github.com/pyscript/pyscript#readme"
}

31
bridge/test/index.html Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<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>
<!-- for local testing purpose only-->
<script type="importmap">{"imports":{"https://esm.run/@pyscript/bridge":"../index.js"}}</script>
<script type="module">
const { ffi: { test_func, test_other, version } } = await import('./test.js');
console.time("⏱️ first invoke");
const result = await test_func("PyScript Bridge");
console.timeEnd("⏱️ first invoke");
document.body.append(
Object.assign(
document.createElement("h3"),
{ textContent: result },
),
document.createElement("hr"),
await version(),
);
console.time("⏱️ other invokes");
await test_other("🐍");
console.timeEnd("⏱️ other invokes");
</script>
</head>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PyScript Bridge</title>
<script type="importmap">
{
"imports": {
"https://esm.run/@pyscript/bridge": "https://esm.run/@pyscript/bridge@latest",
"https://esm.run/@pyscript/bridge/test/test.js": "https://esm.run/@pyscript/bridge@latest/test/test.js"
}
}
</script>
<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>
<script type="module">
const cdn_test = 'https://esm.run/@pyscript/bridge/test/test.js';
const { ffi: { test_func, test_other, version } } = await import(cdn_test);
console.time("⏱️ first invoke");
const result = await test_func("PyScript Bridge");
console.timeEnd("⏱️ first invoke");
document.body.append(
Object.assign(
document.createElement("h3"),
{ textContent: result },
),
document.createElement("hr"),
await version(),
);
console.time("⏱️ other invokes");
await test_other("🐍");
console.timeEnd("⏱️ other invokes");
</script>
</head>
</html>

View File

@@ -0,0 +1,5 @@
import sys
def version():
return sys.version

18
bridge/test/test.js Normal file
View File

@@ -0,0 +1,18 @@
import bridge from "https://esm.run/@pyscript/bridge";
// for local testing purpose only
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"),
config: searchParams.has("config") ?
({
files: {
"./sys_version.py": "./sys_version.py",
},
}) : undefined,
});

22
bridge/test/test.py Normal file
View File

@@ -0,0 +1,22 @@
from pyscript import config, RUNNING_IN_WORKER
type = config["type"]
print(f"{type}-script", RUNNING_IN_WORKER and "worker" or "main")
def test_func(message):
print("Python", message)
return message
def test_other(message):
print("Python", message)
return message
def version():
try:
from sys_version import version
except ImportError:
version = lambda: "no config"
return version()

846
core/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@pyscript/core", "name": "@pyscript/core",
"version": "0.6.53", "version": "0.7.11",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -42,7 +42,7 @@
}, },
"scripts": { "scripts": {
"server": "echo \"➡️ TESTS @ $(tput bold)http://localhost:8080/tests/$(tput sgr0)\"; npx static-handler --coi .", "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:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:flatted": "node rollup/flatted.cjs", "build:flatted": "node rollup/flatted.cjs",
"build:plugins": "node rollup/plugins.cjs", "build:plugins": "node rollup/plugins.cjs",
@@ -67,40 +67,40 @@
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"@webreflection/idb-map": "^0.3.2", "@webreflection/idb-map": "^0.3.2",
"@webreflection/utils": "^0.1.0", "@webreflection/utils": "^0.1.1",
"add-promise-listener": "^0.1.3", "add-promise-listener": "^0.1.3",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"polyscript": "^0.17.20", "polyscript": "^0.19.10",
"sticky-module": "^0.1.1", "sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1", "to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7" "type-checked-collections": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.8.1", "@codemirror/commands": "^6.10.0",
"@codemirror/lang-python": "^6.2.1", "@codemirror/lang-python": "^6.2.1",
"@codemirror/language": "^6.11.0", "@codemirror/language": "^6.11.3",
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.8", "@codemirror/view": "^6.38.6",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.56.1",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.4", "@webreflection/toml-j0.4": "^1.1.4",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0", "@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"bun": "^1.2.13", "bun": "^1.3.1",
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
"codedent": "^0.1.2", "codedent": "^0.1.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.2",
"eslint": "^9.27.0", "eslint": "^9.39.1",
"flatted": "^3.3.3", "flatted": "^3.3.3",
"rollup": "^4.41.0", "rollup": "^4.52.5",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"static-handler": "^0.5.3", "static-handler": "^0.5.3",
"string-width": "^7.2.0", "string-width": "^8.1.0",
"typescript": "^5.8.3", "typescript": "^5.9.3",
"xterm-readline": "^1.1.2" "xterm-readline": "^1.1.2"
}, },
"repository": { "repository": {

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 * Original file: /npm/@xterm/xterm@5.5.0/css/xterm.css
* *
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -92,40 +92,6 @@ def when(target, *args, **kwargs):
elements = selector if isinstance(selector, list) else [selector] elements = selector if isinstance(selector, list) else [selector]
def decorator(func): 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) sig = inspect.signature(func)
if sig.parameters: if sig.parameters:
if is_awaitable(func): if is_awaitable(func):

View File

@@ -2,8 +2,10 @@ try:
import js import js
from pyodide.ffi import create_proxy as _cp from pyodide.ffi import create_proxy as _cp
from pyodide.ffi import to_js as _py_tjs from pyodide.ffi import to_js as _py_tjs
from pyodide.ffi import jsnull
from_entries = js.Object.fromEntries from_entries = js.Object.fromEntries
is_none = lambda value: value is None or value is jsnull
def _tjs(value, **kw): def _tjs(value, **kw):
if not hasattr(kw, "dict_converter"): if not hasattr(kw, "dict_converter"):
@@ -13,6 +15,34 @@ try:
except: except:
from jsffi import create_proxy as _cp from jsffi import create_proxy as _cp
from jsffi import to_js as _tjs 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 create_proxy = _cp
to_js = _tjs to_js = _tjs
try:
from polyscript import ffi as _ffi
direct = _ffi.direct
gather = _ffi.gather
query = _ffi.query
def assign(source, *args):
for arg in args:
_ffi.assign(source, to_js(arg))
return source
except:
import js
_assign = js.Object.assign
direct = lambda source: source
def assign(source, *args):
for arg in args:
_assign(source, to_js(arg))
return source

View File

@@ -1,6 +1,13 @@
mounted = {} 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"): async def mount(path, mode="readwrite", root="", id="pyscript"):
import js import js
from _pyscript import fs, interpreter from _pyscript import fs, interpreter
@@ -12,6 +19,7 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
js.console.warn("experimental pyscript.fs ⚠️") js.console.warn("experimental pyscript.fs ⚠️")
details = None
handler = None handler = None
uid = f"{path}@{id}" uid = f"{path}@{id}"
@@ -31,9 +39,16 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
if success: if success:
from polyscript import IDBMap 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: else:
raise RuntimeError(fs.ERROR) raise RuntimeError(fs.ERROR)
@@ -41,14 +56,41 @@ async def mount(path, mode="readwrite", root="", id="pyscript"):
success = await fs.idb.has(uid) success = await fs.idb.has(uid)
if success: 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: else:
handler = await fs.getFileSystemDirectoryHandle(to_js(options)) js_options = to_js(options)
await fs.idb.set(uid, handler) 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) 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): async def sync(path):
await mounted[path].syncfs() await mounted[path].syncfs()

View File

@@ -67,7 +67,11 @@ if RUNNING_IN_WORKER:
else: else:
import _pyscript 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 window = globalThis
document = globalThis.document document = globalThis.document

View File

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

View File

@@ -6,7 +6,7 @@
# from __future__ import annotations # CAUTION: This is not supported in MicroPython. # from __future__ import annotations # CAUTION: This is not supported in MicroPython.
from pyscript import document, when, Event # noqa: F401 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): 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. 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. Otherwise, we are being called to *wrap* an existing DOM element.
""" """
self._dom_element = dom_element or document.createElement( self._dom_element = (
type(self).get_tag_name() 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. # HTML on_events attached to the element become pyscript.Event instances.
@@ -124,6 +126,11 @@ class Element:
# Element instance via `for_`). # Element instance via `for_`).
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython. name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
if name == "for":
# The `for` attribute is a special case as it is a keyword in both
# Python and JavaScript.
# We need to get it from the underlying DOM element as `htmlFor`.
name = "htmlFor"
return getattr(self._dom_element, name) return getattr(self._dom_element, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
@@ -142,6 +149,11 @@ class Element:
# Element instance via `for_`). # Element instance via `for_`).
if name.endswith("_"): if name.endswith("_"):
name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython. name = name[:-1] # noqa: FURB188 No str.removesuffix() in MicroPython.
if name == "for":
# The `for` attribute is a special case as it is a keyword in both
# Python and JavaScript.
# We need to set it on the underlying DOM element as `htmlFor`.
name = "htmlFor"
if name.startswith("on_"): if name.startswith("on_"):
# Ensure on-events are cached in the _on_events dict if the # Ensure on-events are cached in the _on_events dict if the
@@ -185,7 +197,7 @@ class Element:
@property @property
def parent(self): def parent(self):
"""Return the element's `parent `Element`.""" """Return the element's `parent `Element`."""
if self._dom_element.parentElement is None: if is_none(self._dom_element.parentElement):
return None return None
return Element.wrap_dom_element(self._dom_element.parentElement) return Element.wrap_dom_element(self._dom_element.parentElement)
@@ -1124,7 +1136,7 @@ class video(ContainerElement):
width = width if width is not None else self.videoWidth width = width if width is not None else self.videoWidth
height = height if height is not None else self.videoHeight height = height if height is not None else self.videoHeight
if to is None: if is_none(to):
to = canvas(width=width, height=height) to = canvas(width=width, height=height)
elif isinstance(to, Element): elif isinstance(to, Element):

View File

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

View File

@@ -12,20 +12,33 @@ export default {
return new Promise(($) => setTimeout($, seconds * 1000)); return new Promise(($) => setTimeout($, seconds * 1000));
}, },
getFSHandler: (options) => getFileSystemDirectoryHandle(options),
/** /**
* Ask a user action via dialog and returns the directory handler once granted. * Ask a user action via dialog and returns the directory handler once granted.
* @param {string} uid * @param {string} uid
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options * @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {boolean} * @returns {Promise<boolean>}
*/ */
async storeFSHandler(uid, options = {}) { async storeFSHandler(uid, options = {}) {
if (await idb.has(uid)) return true; if (await idb.has(uid)) return true;
return getFileSystemDirectoryHandle(options).then( return getFileSystemDirectoryHandle(options).then(
async (handler) => { async (handler) => {
await idb.set(uid, handler); await idb.set(uid, { handler, options });
return true; return true;
}, },
() => false, () => 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 classList = document.documentElement.classList
if not __terminal__: if not __terminal__: # noqa: F821 __terminal__ is defined in core/src/plugins/donkey.js
classList.add("error") classList.add("error")
else: else:
classList.add("ok") classList.add("ok")

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PyScript VS Symbols</title>
<script>
globalThis.hasSymbol = (symbol, ref) => symbol in ref;
globalThis.getSymbol = (symbol, ref) => ref[symbol];
// some 3rd party JS library might use symbols to brand-check
// so it's not about symbols traveling from MicroPython
// it's about MicroPython proxies traps not understanding symbols
globalThis.hasIterator = ref => Symbol.iterator in ref;
</script>
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
<script type="mpy">
import js
symbol = js.Symbol.iterator
if js.getSymbol(symbol, []) and js.hasSymbol(symbol, []) and js.hasIterator([]):
js.document.documentElement.classList.add("main")
</script>
<script type="mpy" worker>
from pyscript import window
import js
symbol = js.Symbol.iterator
if window.getSymbol(symbol, []) and window.hasSymbol(symbol, []) and window.hasIterator([]):
window.document.documentElement.classList.add("worker")
</script>
</head>
</html>

View File

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

View File

@@ -2,6 +2,13 @@ import { test, expect } from '@playwright/test';
test.setTimeout(120 * 1000); test.setTimeout(120 * 1000);
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 }) => { test('MicroPython display', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/mpy.html'); await page.goto('http://localhost:8080/tests/javascript/mpy.html');
await page.waitForSelector('html.done.worker'); await page.waitForSelector('html.done.worker');
@@ -59,6 +66,11 @@ test('MicroPython + configURL', async ({ page }) => {
await page.waitForSelector('html.main.worker'); await page.waitForSelector('html.main.worker');
}); });
test('MicroPython + Symbols', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/worker-symbols.html');
await page.waitForSelector('html.main.worker');
});
test('Pyodide + terminal on Main', async ({ page }) => { test('Pyodide + terminal on Main', async ({ page }) => {
await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html'); await page.goto('http://localhost:8080/tests/javascript/py-terminal-main.html');
await page.waitForSelector('html.ok'); await page.waitForSelector('html.ok');

View File

@@ -0,0 +1,21 @@
<!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>
window.Worker = class extends Worker {
constructor(url, ...rest) {
console.log(rest[0]);
return super(url, ...rest);
}
};
window.start = Date.now();
</script>
<script type="module" src="../../../dist/core.js"></script>
</head>
<body>
<script type="py" config="./index.toml" src="index.py" worker></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
from pyscript import document, window
document.body.append(window.Date.now() - window.start)

View File

@@ -0,0 +1,2 @@
experimental_ffi_timeout = 0
package_cache = "passthrough"

View File

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

View File

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

View File

@@ -140,7 +140,7 @@ def get_stats_gl(renderer):
def bg_from_v(*vertices): def bg_from_v(*vertices):
geometry = new(THREE.BufferGeometry) 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) attr = new(THREE.Float32BufferAttribute, vertices_f32a, 3)
return geometry.setAttribute('position', attr) 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 THREE, clear, SoundPlayer
from libthree import get_renderer, get_ortho_camera from libthree import get_renderer, get_ortho_camera
from libthree import get_loading_manager, get_stats_gl 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 libfft import BeatSync
from multipyjs import MICROPYTHON, new, call, to_js, create_proxy from multipyjs import MICROPYTHON, new, call, to_js, create_proxy

View File

@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css"> <link rel="stylesheet" href="../../../dist/core.css">
<script type="module" src="../../dist/core.js"></script> <script type="module" src="../../../dist/core.js"></script>
</head> </head>
<body> <body>
<script type="py-editor"> <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

@@ -24,5 +24,6 @@
"./example_js_worker_module.js": "greeting_worker" "./example_js_worker_module.js": "greeting_worker"
} }
}, },
"packages": ["Pillow" ] "packages": ["Pillow" ],
"experimental_ffi_timeout": 0
} }

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 # The function should have been called when the whenable object was
# triggered. # triggered.
assert counter == 1 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) @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 The "hello" function in the example_js_worker_module.js file is available
via the js_modules object while running in a worker. via the js_modules object while running in a worker.

View File

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

View File

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

View File

@@ -871,7 +871,17 @@ class TestElements:
self._create_el_and_basic_asserts("kbd", "some text") self._create_el_and_basic_asserts("kbd", "some text")
def test_label(self): def test_label(self):
self._create_el_and_basic_asserts("label", "some text") label_text = "Luke, I am your father"
label_for = "some-id"
# Let's create the element
el = web.label(label_text, for_=label_for)
# Let's check the element was configured correctly.
assert isinstance(el, web.label), "The new element should be a label."
assert el.textContent == label_text, "The label text should match."
assert el._dom_element.tagName == "LABEL"
assert el.for_ == label_for, "The label should have the correct for attribute."
# Ensure the label element is rendered with the correct "for" attribute
assert f'for="{label_for}"' in el.outerHTML, "The label should have the correct 'for' attribute in its HTML."
def test_legend(self): def test_legend(self):
self._create_el_and_basic_asserts("legend", "some text") self._create_el_and_basic_asserts("legend", "some text")

View File

@@ -63,5 +63,5 @@ declare const exportedHooks: {
}; };
}; };
declare const exportedConfig: {}; 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 }; 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. * Ask a user action via dialog and returns the directory handler once granted.
* @param {string} uid * @param {string} uid
* @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options * @param {{id?:string, mode?:"read"|"readwrite", hint?:"desktop"|"documents"|"downloads"|"music"|"pictures"|"videos"}} options
* @returns {boolean} * @returns {Promise<boolean>}
*/ */
function storeFSHandler(uid: string, options?: { function storeFSHandler(uid: string, options?: {
id?: string; id?: string;
mode?: "read" | "readwrite"; mode?: "read" | "readwrite";
hint?: "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; 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; export default _default;

View File

@@ -137,12 +137,14 @@
&lt;body&gt; &lt;body&gt;
&lt;h1&gt;Hello world!&lt;/h1&gt; &lt;h1&gt;Hello world!&lt;/h1&gt;
&lt;p&gt;These are the Python interpreters in PyScript _VERSION_:&lt;/p&gt; &lt;p&gt;These are the Python interpreters in PyScript _VERSION_:&lt;/p&gt;
&lt;script type=&quot;py&quot;&gt; &lt;!-- Pyodide --&gt; &lt;script type=&quot;py&quot;&gt;
# Pyodide
from pyscript import display from pyscript import display
import sys import sys
display(sys.version) display(sys.version)
&lt;/script&gt; &lt;/script&gt;
&lt;script type=&quot;mpy&quot;&gt; &lt;!-- MicroPython --&gt; &lt;script type=&quot;mpy&quot;&gt;
# MicroPython
from pyscript import display from pyscript import display
import sys import sys
display(sys.version) display(sys.version)

View File

@@ -5,5 +5,5 @@ skip = "*.js,*.json"
[tool.ruff] [tool.ruff]
line-length = 114 line-length = 114
lint.select = ["C4", "C90", "E", "EM", "F", "PIE", "PYI", "PLC", "Q", "RET", "W"] 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 lint.mccabe.max-complexity = 27